@@ -2,42 +2,51 @@ package fr.adrienbrault.idea.symfony2plugin.templating.usages
22
33import com.intellij.find.findUsages.FindUsagesHandler
44import com.intellij.find.findUsages.FindUsagesOptions
5+ import com.intellij.openapi.application.ApplicationManager
56import com.intellij.psi.PsiElement
7+ import com.intellij.psi.PsiRecursiveElementWalkingVisitor
68import com.intellij.psi.search.GlobalSearchScope
79import com.intellij.usageView.UsageInfo
810import com.intellij.util.Processor
11+ import com.jetbrains.twig.TwigFileType
12+ import fr.adrienbrault.idea.symfony2plugin.templating.TwigPattern
913
1014/* *
11- * Delegates Twig-started Find Usages to the resolved PHP targets instead of searching the raw Twig PSI locally.
15+ * Handles Find Usages started from Twig, either by delegating to resolved PHP targets or by
16+ * searching exact Twig extension symbol names when the Twig symbol itself is the usage identity.
1217 *
1318 * @author Daniel Espendiller <daniel@espendiller.net>
1419 */
1520class TwigFindUsagesHandler (
16- twigElement : PsiElement ,
17- private val phpTargets : List <PsiElement >,
18- ) : FindUsagesHandler(twigElement) {
21+ private val target : TwigFindUsagesTarget ,
22+ ) : FindUsagesHandler(target.primaryElement) {
1923 /* *
20- * Exposes the resolved PHP targets as the primary search targets for the Usage View.
24+ * Exposes the effective primary search targets for the Usage View.
2125 */
22- override fun getPrimaryElements (): Array <PsiElement > = phpTargets .toTypedArray()
26+ override fun getPrimaryElements (): Array <PsiElement > = target.primaryElements .toTypedArray()
2327
2428 /* *
25- * Delegates the actual search work to the PHP targets so existing PHP and Twig reference search logic is reused .
29+ * Delegates member/class usages to PHP targets and searches Twig extension symbols directly by name .
2630 */
2731 override fun processElementUsages (
2832 element : PsiElement ,
2933 processor : Processor <in UsageInfo >,
3034 options : FindUsagesOptions ,
3135 ): Boolean {
32- val targets = getEffectiveTargets(element)
36+ return when (target) {
37+ is TwigPhpFindUsagesTarget -> {
38+ val targets = getEffectivePhpTargets(element, target.phpTargets)
3339
34- for (target in targets) {
35- if (! super .processElementUsages(target, processor, options)) {
36- return false
40+ for (phpTarget in targets) {
41+ if (! super .processElementUsages(phpTarget, processor, options)) {
42+ return false
43+ }
44+ }
45+
46+ true
3747 }
48+ is TwigExtensionSymbolFindUsagesTarget -> processTwigExtensionUsages(target.symbol, processor, options)
3849 }
39-
40- return true
4150 }
4251
4352 /* *
@@ -48,21 +57,107 @@ class TwigFindUsagesHandler(
4857 processor : Processor <in UsageInfo >,
4958 searchScope : GlobalSearchScope ,
5059 ): Boolean {
51- val targets = getEffectiveTargets(element)
60+ return when (target) {
61+ is TwigPhpFindUsagesTarget -> {
62+ val targets = getEffectivePhpTargets(element, target.phpTargets)
63+
64+ for (phpTarget in targets) {
65+ if (! super .processUsagesInText(phpTarget, processor, searchScope)) {
66+ return false
67+ }
68+ }
5269
53- for (target in targets) {
54- if (! super .processUsagesInText(target, processor, searchScope)) {
55- return false
70+ true
5671 }
72+ is TwigExtensionSymbolFindUsagesTarget -> true
5773 }
58-
59- return true
6074 }
6175
6276 /* *
6377 * Supports both direct calls with the original Twig PSI and platform calls with one of the primary PHP targets.
6478 */
65- private fun getEffectiveTargets (element : PsiElement ): List <PsiElement > {
79+ private fun getEffectivePhpTargets (element : PsiElement , phpTargets : List < PsiElement > ): List <PsiElement > {
6680 return phpTargets.firstOrNull { it.isEquivalentTo(element) }?.let { listOf (it) } ? : phpTargets
6781 }
82+
83+ /* *
84+ * Searches Twig templates directly for the exact extension symbol name under Find Usages.
85+ */
86+ private fun processTwigExtensionUsages (
87+ symbol : TwigExtensionUsageSymbol ,
88+ processor : Processor <in UsageInfo >,
89+ options : FindUsagesOptions ,
90+ ): Boolean {
91+ return ApplicationManager .getApplication().runReadAction<Boolean > {
92+ val project = target.primaryElement.project
93+ val searchScope = options.searchScope as ? GlobalSearchScope ? : GlobalSearchScope .projectScope(project)
94+ val twigScope = GlobalSearchScope .getScopeRestrictedByFileTypes(searchScope, TwigFileType .INSTANCE )
95+
96+ for (twigFile in TwigMethodReferencesSearchExecutor ().collectTwigFiles(project, twigScope, setOf (symbol.name))) {
97+ twigFile.accept(object : PsiRecursiveElementWalkingVisitor () {
98+ override fun visitElement (element : PsiElement ) {
99+ if (matchesTwigExtensionSymbol(element, symbol)) {
100+ processor.process(UsageInfo (element))
101+ }
102+
103+ super .visitElement(element)
104+ }
105+ })
106+ }
107+
108+ true
109+ }
110+ }
111+ }
112+
113+ /* *
114+ * Describes the effective identity of a Twig Find Usages session.
115+ */
116+ sealed interface TwigFindUsagesTarget {
117+ /* *
118+ * Anchor PSI element used to create the handler and open the Usage View.
119+ */
120+ val primaryElement: PsiElement
121+
122+ /* *
123+ * Effective search targets exposed to the Usage View, either PHP delegates or the Twig symbol itself.
124+ */
125+ val primaryElements: List <PsiElement >
126+ }
127+
128+ /* *
129+ * Twig usage target that delegates the search to one or more resolved PHP elements.
130+ */
131+ data class TwigPhpFindUsagesTarget (
132+ override val primaryElement : PsiElement ,
133+ val phpTargets : List <PsiElement >,
134+ ) : TwigFindUsagesTarget {
135+ override val primaryElements: List <PsiElement > = phpTargets
136+ }
137+
138+ /* *
139+ * Twig extension usage target that keeps the Twig symbol itself as the usage identity, for example `form_start` or `trans`.
140+ */
141+ data class TwigExtensionSymbolFindUsagesTarget (
142+ override val primaryElement : PsiElement ,
143+ val symbol : TwigExtensionUsageSymbol ,
144+ ) : TwigFindUsagesTarget {
145+ override val primaryElements: List <PsiElement > = listOf (primaryElement)
146+ }
147+
148+ /* *
149+ * Matches only the exact Twig extension symbol leaf, such as `form_start` in `{{ form_start() }}` or `trans` in `{% apply trans %}`.
150+ */
151+ private fun matchesTwigExtensionSymbol (
152+ element : PsiElement ,
153+ symbol : TwigExtensionUsageSymbol ,
154+ ): Boolean {
155+ if (! element.text.equals(symbol.name, ignoreCase = true )) {
156+ return false
157+ }
158+
159+ return when (symbol.kind) {
160+ TwigExtensionUsageKind .FUNCTION -> TwigPattern .getPrintBlockFunctionPattern().accepts(element)
161+ TwigExtensionUsageKind .FILTER -> TwigPattern .getFilterPattern().accepts(element) || TwigPattern .getApplyFilterPattern().accepts(element)
162+ }
68163}
0 commit comments