diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/annotation/TwigUxToolkitAnnotator.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/annotation/TwigUxToolkitAnnotator.java
new file mode 100644
index 000000000..55ceeab92
--- /dev/null
+++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/annotation/TwigUxToolkitAnnotator.java
@@ -0,0 +1,150 @@
+package fr.adrienbrault.idea.symfony2plugin.templating.annotation;
+
+import com.intellij.lang.annotation.AnnotationHolder;
+import com.intellij.lang.annotation.Annotator;
+import com.intellij.lang.annotation.HighlightSeverity;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.php.lang.highlighter.PhpHighlightingData;
+import com.jetbrains.twig.TwigTokenTypes;
+import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Provides syntax highlighting for Symfony UX Toolkit Twig comment annotations.
+ *
+ * Supports:
+ * - {# @prop name type Description #}
+ * - {# @block name Description #}
+ *
+ * Uses PHP's PHPDoc highlighting colors for consistency with `@property` annotations.
+ *
+ * @see Symfony UX Toolkit
+ */
+public class TwigUxToolkitAnnotator implements Annotator {
+ /**
+ * Pattern for @prop annotations.
+ * Format: @prop name type Description
+ * Example: @prop open boolean Whether the item is open by default.
+ *
+ * Supports complex types:
+ * - Simple: string, boolean, int
+ * - Nullable: ?string, string|null
+ * - Union: string|int|null
+ * - Generic: array, Collection
+ * - Literal strings: 'vertical'|'horizontal'
+ * - FQCN: App\Entity\Item, \DateTime
+ * - Arrays: string[], Item[]
+ *
+ * @see Regex101
+ */
+ private static final Pattern PROP_PATTERN = Pattern.compile(
+ "(@prop)\\s+(\\w+)\\s+(\\S+)\\s+(.+?)\\s*$",
+ Pattern.DOTALL
+ );
+
+ /**
+ * Pattern for @block annotations.
+ * Format: @block name Description
+ * Example: @block content The item content.
+ *
+ * @see Regex101
+ */
+ private static final Pattern BLOCK_PATTERN = Pattern.compile(
+ "(@block)\\s+(\\w+)\\s+(.+?)\\s*$",
+ Pattern.DOTALL
+ );
+
+ @Override
+ public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
+ if (!Symfony2ProjectComponent.isEnabled(element.getProject())) {
+ return;
+ }
+
+ // Only process Twig comment text tokens
+ if (element.getNode().getElementType() != TwigTokenTypes.COMMENT_TEXT) {
+ return;
+ }
+
+ String text = element.getText();
+ int startOffset = element.getTextRange().getStartOffset();
+
+ // Try to match @prop pattern
+ Matcher propMatcher = PROP_PATTERN.matcher(text);
+ if (propMatcher.find()) {
+ annotateProp(holder, startOffset, propMatcher);
+ return;
+ }
+
+ // Try to match @block pattern
+ Matcher blockMatcher = BLOCK_PATTERN.matcher(text);
+ if (blockMatcher.find()) {
+ annotateBlock(holder, startOffset, blockMatcher);
+ }
+ }
+
+ /**
+ * Annotates a @prop comment with syntax highlighting.
+ * Highlights: @prop keyword, property name, and type.
+ *
+ * Uses PHP PHPDoc colors:
+ * - DOC_TAG for @prop keyword (like @property in PHPDoc)
+ * - DOC_PROPERTY_IDENTIFIER for property name (like $foo in @property string $foo)
+ * - DOC_IDENTIFIER for type (like string in @property string $foo)
+ */
+ private void annotateProp(@NotNull AnnotationHolder holder, int startOffset, @NotNull Matcher matcher) {
+ // Highlight @prop keyword (group 1) - like @property
+ highlightRange(holder, startOffset, matcher, 1, PhpHighlightingData.DOC_TAG);
+
+ // Highlight property name (group 2) - like $foo in @property string $foo
+ highlightRange(holder, startOffset, matcher, 2, PhpHighlightingData.DOC_PROPERTY_IDENTIFIER);
+
+ // Highlight type (group 3) - like string in @property string $foo
+ highlightRange(holder, startOffset, matcher, 3, PhpHighlightingData.DOC_IDENTIFIER);
+ }
+
+ /**
+ * Annotates a @block comment with syntax highlighting.
+ * Highlights: @block keyword and block name.
+ *
+ * Uses PHP PHPDoc colors:
+ * - DOC_TAG for @block keyword
+ * - DOC_PROPERTY_IDENTIFIER for block name
+ */
+ private void annotateBlock(@NotNull AnnotationHolder holder, int startOffset, @NotNull Matcher matcher) {
+ // Highlight @block keyword (group 1)
+ highlightRange(holder, startOffset, matcher, 1, PhpHighlightingData.DOC_TAG);
+
+ // Highlight block name (group 2)
+ highlightRange(holder, startOffset, matcher, 2, PhpHighlightingData.DOC_PROPERTY_IDENTIFIER);
+ }
+
+ /**
+ * Creates a silent annotation with the specified text attributes for a regex group match.
+ */
+ private void highlightRange(
+ @NotNull AnnotationHolder holder,
+ int baseOffset,
+ @NotNull Matcher matcher,
+ int group,
+ @NotNull TextAttributesKey textAttributesKey
+ ) {
+ if (matcher.group(group) == null) {
+ return;
+ }
+
+ TextRange range = new TextRange(
+ baseOffset + matcher.start(group),
+ baseOffset + matcher.end(group)
+ );
+
+ holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
+ .range(range)
+ .textAttributes(textAttributesKey)
+ .create();
+ }
+}
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 22a1c5e4a..635a665ef 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -230,6 +230,8 @@
+
+
diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/annotation/TwigUxToolkitAnnotatorTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/annotation/TwigUxToolkitAnnotatorTest.java
new file mode 100644
index 000000000..a469298b4
--- /dev/null
+++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/annotation/TwigUxToolkitAnnotatorTest.java
@@ -0,0 +1,186 @@
+package fr.adrienbrault.idea.symfony2plugin.tests.templating.annotation;
+
+import com.intellij.codeInsight.daemon.impl.HighlightInfo;
+import com.intellij.openapi.editor.DefaultLanguageHighlighterColors;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import fr.adrienbrault.idea.symfony2plugin.templating.annotation.TwigUxToolkitAnnotator;
+import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
+
+import java.util.List;
+
+/**
+ * @author Daniel Espendiller
+ * @see TwigUxToolkitAnnotator
+ */
+public class TwigUxToolkitAnnotatorTest extends SymfonyLightCodeInsightFixtureTestCase {
+
+ public void testPropAnnotationIsHighlighted() {
+ myFixture.configureByText(
+ "test.html.twig",
+ "{# @prop open boolean Whether the item is open by default. #}"
+ );
+
+ List highlighting = myFixture.doHighlighting();
+
+ // Verify that highlighting is applied (INFORMATION level annotations from our annotator)
+ assertTrue(
+ "Expected highlighting for @prop annotation",
+ highlighting.stream().anyMatch(info ->
+ info.getSeverity().getName().equals("INFORMATION") &&
+ hasTextAttributesKey(info, DefaultLanguageHighlighterColors.DOC_COMMENT_TAG)
+ )
+ );
+ }
+
+ public void testPropAnnotationHighlightsPropertyName() {
+ myFixture.configureByText(
+ "test.html.twig",
+ "{# @prop open boolean Whether the item is open by default. #}"
+ );
+
+ List highlighting = myFixture.doHighlighting();
+
+ assertTrue(
+ "Expected highlighting for property name",
+ highlighting.stream().anyMatch(info ->
+ info.getSeverity().getName().equals("INFORMATION") &&
+ hasTextAttributesKey(info, DefaultLanguageHighlighterColors.DOC_COMMENT_TAG_VALUE)
+ )
+ );
+ }
+
+ public void testPropAnnotationHighlightsType() {
+ myFixture.configureByText(
+ "test.html.twig",
+ "{# @prop open boolean Whether the item is open by default. #}"
+ );
+
+ List highlighting = myFixture.doHighlighting();
+
+ assertTrue(
+ "Expected highlighting for type",
+ highlighting.stream().anyMatch(info ->
+ info.getSeverity().getName().equals("INFORMATION") &&
+ hasTextAttributesKey(info, DefaultLanguageHighlighterColors.CLASS_REFERENCE)
+ )
+ );
+ }
+
+ public void testBlockAnnotationIsHighlighted() {
+ myFixture.configureByText(
+ "test.html.twig",
+ "{# @block content The item content. #}"
+ );
+
+ List highlighting = myFixture.doHighlighting();
+
+ assertTrue(
+ "Expected highlighting for @block annotation",
+ highlighting.stream().anyMatch(info ->
+ info.getSeverity().getName().equals("INFORMATION") &&
+ hasTextAttributesKey(info, DefaultLanguageHighlighterColors.DOC_COMMENT_TAG)
+ )
+ );
+ }
+
+ public void testBlockAnnotationHighlightsBlockName() {
+ myFixture.configureByText(
+ "test.html.twig",
+ "{# @block content The item content. #}"
+ );
+
+ List highlighting = myFixture.doHighlighting();
+
+ assertTrue(
+ "Expected highlighting for block name",
+ highlighting.stream().anyMatch(info ->
+ info.getSeverity().getName().equals("INFORMATION") &&
+ hasTextAttributesKey(info, DefaultLanguageHighlighterColors.DOC_COMMENT_TAG_VALUE)
+ )
+ );
+ }
+
+ public void testPropWithComplexType() {
+ myFixture.configureByText(
+ "test.html.twig",
+ "{# @prop items App\\Entity\\Item[] List of items #}"
+ );
+
+ List highlighting = myFixture.doHighlighting();
+
+ assertTrue(
+ "Expected highlighting for complex type",
+ highlighting.stream().anyMatch(info ->
+ info.getSeverity().getName().equals("INFORMATION") &&
+ hasTextAttributesKey(info, DefaultLanguageHighlighterColors.CLASS_REFERENCE)
+ )
+ );
+ }
+
+ public void testPropWithUnionType() {
+ myFixture.configureByText(
+ "test.html.twig",
+ "{# @prop value string|int|null The value #}"
+ );
+
+ List highlighting = myFixture.doHighlighting();
+
+ assertTrue(
+ "Expected highlighting for union type",
+ highlighting.stream().anyMatch(info ->
+ info.getSeverity().getName().equals("INFORMATION") &&
+ hasTextAttributesKey(info, DefaultLanguageHighlighterColors.CLASS_REFERENCE)
+ )
+ );
+ }
+
+ public void testRegularCommentNotHighlighted() {
+ myFixture.configureByText(
+ "test.html.twig",
+ "{# This is a regular comment #}"
+ );
+
+ List highlighting = myFixture.doHighlighting();
+
+ // Regular comments should not have our specific highlighting
+ assertFalse(
+ "Regular comments should not have DOC_COMMENT_TAG highlighting",
+ highlighting.stream().anyMatch(info ->
+ info.getSeverity().getName().equals("INFORMATION") &&
+ hasTextAttributesKey(info, DefaultLanguageHighlighterColors.DOC_COMMENT_TAG)
+ )
+ );
+ }
+
+ public void testVarCommentNotAffected() {
+ // @var comments are handled by a different mechanism, ensure we don't interfere
+ myFixture.configureByText(
+ "test.html.twig",
+ "{# @var foo \\App\\Entity\\Foo #}"
+ );
+
+ List highlighting = myFixture.doHighlighting();
+
+ // @var should not be highlighted by our annotator (it's not @prop or @block)
+ assertFalse(
+ "@var comments should not be highlighted by TwigUxToolkitAnnotator",
+ highlighting.stream().anyMatch(info ->
+ info.getSeverity().getName().equals("INFORMATION") &&
+ hasTextAttributesKey(info, DefaultLanguageHighlighterColors.DOC_COMMENT_TAG) &&
+ info.getText() != null && info.getText().contains("@var")
+ )
+ );
+ }
+
+ /**
+ * Helper method to check if a HighlightInfo has a specific TextAttributesKey.
+ */
+ private boolean hasTextAttributesKey(HighlightInfo info, TextAttributesKey expectedKey) {
+ if (info.forcedTextAttributesKey != null) {
+ return info.forcedTextAttributesKey.equals(expectedKey) ||
+ info.forcedTextAttributesKey.getFallbackAttributeKey() != null &&
+ info.forcedTextAttributesKey.getFallbackAttributeKey().equals(expectedKey);
+ }
+ return false;
+ }
+}