1717import fr .adrienbrault .idea .symfony2plugin .Symfony2Icons ;
1818import fr .adrienbrault .idea .symfony2plugin .Symfony2ProjectComponent ;
1919import fr .adrienbrault .idea .symfony2plugin .intentions .php .AddRouteAttributeIntention ;
20+ import fr .adrienbrault .idea .symfony2plugin .stubs .indexes .UxTemplateStubIndex ;
21+ import fr .adrienbrault .idea .symfony2plugin .stubs .util .IndexUtil ;
2022import fr .adrienbrault .idea .symfony2plugin .util .CodeUtil ;
2123import fr .adrienbrault .idea .symfony2plugin .util .PhpElementsUtil ;
2224import org .apache .commons .lang3 .StringUtils ;
3133 * Triggers when typing "#<caret>" before a public method or class
3234 *
3335 * Supports:
34- * - Class-level attributes: #[Route], #[AsController], #[IsGranted]
36+ * - Class-level attributes: #[Route], #[AsController], #[IsGranted], #[AsTwigComponent]
3537 * - Method-level attributes: #[Route], #[IsGranted], #[Cache]
3638 * - Twig extension attributes: #[AsTwigFilter], #[AsTwigFunction], #[AsTwigTest]
3739 *
@@ -46,6 +48,7 @@ public class PhpAttributeCompletionContributor extends CompletionContributor {
4648 private static final String AS_TWIG_FILTER_ATTRIBUTE_FQN = "\\ Twig\\ Attribute\\ AsTwigFilter" ;
4749 private static final String AS_TWIG_FUNCTION_ATTRIBUTE_FQN = "\\ Twig\\ Attribute\\ AsTwigFunction" ;
4850 private static final String AS_TWIG_TEST_ATTRIBUTE_FQN = "\\ Twig\\ Attribute\\ AsTwigTest" ;
51+ private static final String AS_TWIG_COMPONENT_ATTRIBUTE_FQN = "\\ Symfony\\ UX\\ TwigComponent\\ Attribute\\ AsTwigComponent" ;
4952 private static final String TWIG_EXTENSION_FQN = "\\ Twig\\ Extension\\ AbstractExtension" ;
5053
5154 public PhpAttributeCompletionContributor () {
@@ -95,6 +98,10 @@ protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull
9598 if (AddRouteAttributeIntention .isControllerClass (phpClass )) {
9699 lookupElements .addAll (getControllerClassCompletions (project ));
97100 }
101+
102+ if (isTwigComponentClass (project , phpClass )) {
103+ lookupElements .addAll (getTwigComponentClassCompletions (project ));
104+ }
98105 }
99106 }
100107
@@ -237,6 +244,75 @@ private Collection<LookupElement> getTwigExtensionCompletions(@NotNull Project p
237244 return lookupElements ;
238245 }
239246
247+ /**
248+ * Get Twig component class-level attribute completions (for component classes)
249+ */
250+ private Collection <LookupElement > getTwigComponentClassCompletions (@ NotNull Project project ) {
251+ Collection <LookupElement > lookupElements = new ArrayList <>();
252+
253+ // Add AsTwigComponent attribute completion
254+ if (PhpElementsUtil .hasClassOrInterface (project , AS_TWIG_COMPONENT_ATTRIBUTE_FQN )) {
255+ LookupElement lookupElement = LookupElementBuilder
256+ .create ("#[AsTwigComponent]" )
257+ .withIcon (Symfony2Icons .SYMFONY_ATTRIBUTE )
258+ .withTypeText (StringUtils .stripStart (AS_TWIG_COMPONENT_ATTRIBUTE_FQN , "\\ " ), true )
259+ .withInsertHandler (new PhpAttributeInsertHandler (AS_TWIG_COMPONENT_ATTRIBUTE_FQN , CursorPosition .INSIDE_QUOTES ))
260+ .bold ();
261+
262+ lookupElements .add (lookupElement );
263+ }
264+
265+ return lookupElements ;
266+ }
267+
268+ /**
269+ * Check if the class is a Twig component class.
270+ * A class is considered a Twig component if:
271+ * - Its namespace contains "\\Components\\" or ends with "\\Components", OR
272+ * - There are existing component classes (from index) in the same namespace
273+ * (e.g., App\Twig\Components\Button, Foo\Components\Form\Input)
274+ */
275+ private boolean isTwigComponentClass (@ NotNull Project project , @ NotNull PhpClass phpClass ) {
276+ String fqn = phpClass .getFQN ();
277+ if (fqn .isBlank ()) {
278+ return false ;
279+ }
280+
281+ fqn = StringUtils .stripStart (fqn , "\\ " );
282+
283+ int lastBackslash = fqn .lastIndexOf ('\\' );
284+ if (lastBackslash == -1 ) {
285+ return false ; // No namespace
286+ }
287+
288+ String namespace = fqn .substring (0 , lastBackslash );
289+ if (namespace .contains ("\\ Components\\ " ) ||
290+ namespace .endsWith ("\\ Components" ) ||
291+ namespace .equals ("Components" )) {
292+ return true ;
293+ }
294+
295+ // Check if there are any component classes in the same namespace from the index
296+ // keys are FQN class names of components with #[AsTwigComponent] attribute
297+ for (String key : IndexUtil .getAllKeysForProject (UxTemplateStubIndex .KEY , project )) {
298+ String componentFqn = StringUtils .stripStart (key , "\\ " );
299+
300+ // Extract namespace from the component FQN
301+ int componentLastBackslash = componentFqn .lastIndexOf ('\\' );
302+ if (componentLastBackslash == -1 ) {
303+ continue ;
304+ }
305+
306+ // Check if the current class's namespace matches the component namespace
307+ String componentNamespace = componentFqn .substring (0 , componentLastBackslash );
308+ if (namespace .equals (componentNamespace )) {
309+ return true ;
310+ }
311+ }
312+
313+ return false ;
314+ }
315+
240316 /**
241317 * Check if the class is a TwigExtension class.
242318 * A class is considered a TwigExtension if:
0 commit comments