Skip to content

Commit 037fc0f

Browse files
shyimclaude
andcommitted
Index templates by view path and sw_extends target
Template resolution went through FilenameIndex with hundreds of matches for common names like index.html.twig, chain walking loaded every parent file's text, and completion iterated all Twig files of the project on every invocation. The new ShopwareTemplateIndex keys every template by its path relative to Resources/views and stores the sw_extends target as value: path resolution becomes an exact key lookup, the chain is walked from index values without loading files, and the completion list is built from the index and cached until the VFS structure changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent ce7c2b8 commit 037fc0f

5 files changed

Lines changed: 143 additions & 45 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Replaced the Symfony plugin integration for `sw_extends` / `sw_include` with own navigation and autocompletion: navigating a template reference now offers all templates of that view path (the referenced bundle first, then all plugin overrides), and completion suggests templates of every bundle including plugins in `custom/plugins`
88
- Added navigation from a Twig block name to the upstream block it overrides (nearest parent first, following the `sw_extends` chain)
9+
- Templates are indexed by their view path and `sw_extends` target, so template resolution, chain walking and completion no longer scan files
910

1011
- Twig block versioning comments now also work for templates of third-party extensions, both installed via Composer and in `custom/plugins` (block changed / removed / comment missing inspections). The versioning comment records the version of the extension the block belongs to (from the Composer package or the extension's composer.json). Showing a diff of the upstream changes is only supported for Shopware core templates.
1112
- The "versioning comment missing" inspection only reports files that extend another template via `sw_extends`
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package de.shyim.shopware6.index
2+
3+
import com.intellij.util.indexing.DataIndexer
4+
import com.intellij.util.indexing.DefaultFileTypeSpecificInputFilter
5+
import com.intellij.util.indexing.FileBasedIndex
6+
import com.intellij.util.indexing.FileBasedIndexExtension
7+
import com.intellij.util.indexing.FileContent
8+
import com.intellij.util.indexing.ID
9+
import com.intellij.util.io.EnumeratorStringDescriptor
10+
import com.intellij.util.io.KeyDescriptor
11+
import com.jetbrains.twig.TwigFileType
12+
import de.shyim.shopware6.util.TwigUtil
13+
14+
/**
15+
* Indexes every template by its path relative to Resources/views. The value is the template
16+
* reference of the sw_extends tag inside the file (empty when the file does not extend).
17+
*/
18+
class ShopwareTemplateIndex : FileBasedIndexExtension<String, String>() {
19+
override fun getName(): ID<String, String> {
20+
return key
21+
}
22+
23+
override fun getIndexer(): DataIndexer<String, String, FileContent> {
24+
return DataIndexer { inputData ->
25+
if (!inputData.file.path.contains("Resources/views/")) {
26+
return@DataIndexer mapOf()
27+
}
28+
29+
mapOf(
30+
TwigUtil.getRelativePath(inputData.file.path) to
31+
(TwigUtil.findExtendsTargetReference(inputData.contentAsText) ?: "")
32+
)
33+
}
34+
}
35+
36+
override fun getKeyDescriptor(): KeyDescriptor<String> {
37+
return EnumeratorStringDescriptor.INSTANCE
38+
}
39+
40+
override fun getValueExternalizer(): EnumeratorStringDescriptor {
41+
return EnumeratorStringDescriptor.INSTANCE
42+
}
43+
44+
override fun getVersion(): Int {
45+
return 1
46+
}
47+
48+
override fun getInputFilter(): FileBasedIndex.InputFilter {
49+
return object : DefaultFileTypeSpecificInputFilter(TwigFileType.INSTANCE) {
50+
}
51+
}
52+
53+
override fun dependsOnFileContent(): Boolean {
54+
return true
55+
}
56+
57+
companion object {
58+
val key = ID.create<String, String>("de.shyim.shopware6.frontend.twig_templates")
59+
}
60+
}

src/main/kotlin/de/shyim/shopware6/util/ShopwareTemplateUtil.kt

Lines changed: 61 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,42 @@ import com.intellij.codeInsight.lookup.LookupElement
44
import com.intellij.codeInsight.lookup.LookupElementBuilder
55
import com.intellij.openapi.project.Project
66
import com.intellij.openapi.vfs.VirtualFile
7-
import com.intellij.psi.search.FileTypeIndex
8-
import com.intellij.psi.search.FilenameIndex
7+
import com.intellij.openapi.vfs.VirtualFileManager
98
import com.intellij.psi.search.GlobalSearchScope
10-
import com.jetbrains.twig.TwigFileType
9+
import com.intellij.psi.util.CachedValueProvider
10+
import com.intellij.psi.util.CachedValuesManager
11+
import com.intellij.util.indexing.FileBasedIndex
12+
import de.shyim.shopware6.index.ShopwareTemplateIndex
1113
import icons.ShopwareToolBoxIcons
1214

1315
object ShopwareTemplateUtil {
1416
fun findTemplateInBundle(project: Project, bundleName: String, templatePath: String): VirtualFile? {
15-
val suffix = "Resources/views/$templatePath"
16-
17-
return filterByBundle(getTemplatesByViewPath(project, templatePath), suffix, bundleName)
17+
return filterByBundle(getTemplatesByViewPath(project, templatePath), templatePath, bundleName)
1818
.sortedWith(templateOrder())
1919
.firstOrNull()
2020
}
2121

22+
fun findTemplateByPath(project: Project, path: String): VirtualFile? {
23+
return getTemplatesByViewPath(project, TwigUtil.getRelativePath(path)).firstOrNull { it.path == path }
24+
}
25+
26+
fun getExtendsTarget(project: Project, file: VirtualFile): String? {
27+
var target: String? = null
28+
29+
FileBasedIndex.getInstance().processValues(
30+
ShopwareTemplateIndex.key,
31+
TwigUtil.getRelativePath(file.path),
32+
file,
33+
{ _, value ->
34+
target = value
35+
true
36+
},
37+
GlobalSearchScope.allScope(project)
38+
)
39+
40+
return target?.takeIf { it.isNotEmpty() }
41+
}
42+
2243
fun resolveTemplateReference(project: Project, reference: String): List<VirtualFile> {
2344
val bundleName: String?
2445
val templatePath: String
@@ -43,30 +64,20 @@ object ShopwareTemplateUtil {
4364

4465
// the referenced bundle first, then every other template with the same view path, as
4566
// they are all part of the inheritance chain at runtime
46-
val bundleMatches = filterByBundle(candidates, "Resources/views/$templatePath", bundleName)
67+
val bundleMatches = filterByBundle(candidates, templatePath, bundleName)
4768

4869
return bundleMatches.sortedWith(templateOrder()) +
4970
(candidates - bundleMatches.toSet()).sortedWith(templateOrder())
5071
}
5172

5273
fun getTemplateLookupElements(project: Project): List<LookupElement> {
53-
val elements = HashMap<String, LookupElement>()
54-
55-
FileTypeIndex.getFiles(TwigFileType.INSTANCE, GlobalSearchScope.allScope(project)).forEach { file ->
56-
if (!file.path.contains("Resources/views/")) {
57-
return@forEach
58-
}
59-
60-
val bundleName = getBundleNameForPath(file.path) ?: return@forEach
61-
val reference = "@$bundleName/${TwigUtil.getRelativePath(file.path)}"
62-
63-
elements.putIfAbsent(
64-
reference,
65-
LookupElementBuilder.create(reference).withIcon(ShopwareToolBoxIcons.SHOPWARE)
74+
// the template set only changes when files are created, moved or deleted
75+
return CachedValuesManager.getManager(project).getCachedValue(project) {
76+
CachedValueProvider.Result.create(
77+
buildTemplateLookupElements(project),
78+
VirtualFileManager.VFS_STRUCTURE_MODIFICATIONS
6679
)
6780
}
68-
69-
return elements.values.toList()
7081
}
7182

7283
fun getBundleNameForPath(path: String): String? {
@@ -103,20 +114,42 @@ object ShopwareTemplateUtil {
103114
return if (last == "src") root.getOrNull(root.size - 2) else last
104115
}
105116

106-
private fun getTemplatesByViewPath(project: Project, templatePath: String): List<VirtualFile> {
107-
val suffix = "Resources/views/$templatePath"
117+
private fun buildTemplateLookupElements(project: Project): List<LookupElement> {
118+
val elements = HashMap<String, LookupElement>()
119+
val index = FileBasedIndex.getInstance()
120+
val scope = GlobalSearchScope.allScope(project)
121+
122+
index.processAllKeys(ShopwareTemplateIndex.key, { viewPath ->
123+
index.getContainingFiles(ShopwareTemplateIndex.key, viewPath, scope).forEach { file ->
124+
val bundleName = getBundleNameForPath(file.path) ?: return@forEach
125+
val reference = "@$bundleName/$viewPath"
126+
127+
elements.putIfAbsent(
128+
reference,
129+
LookupElementBuilder.create(reference).withIcon(ShopwareToolBoxIcons.SHOPWARE)
130+
)
131+
}
108132

109-
return FilenameIndex.getVirtualFilesByName(
110-
templatePath.substringAfterLast('/'),
133+
true
134+
}, project)
135+
136+
return elements.values.toList()
137+
}
138+
139+
private fun getTemplatesByViewPath(project: Project, templatePath: String): List<VirtualFile> {
140+
return FileBasedIndex.getInstance().getContainingFiles(
141+
ShopwareTemplateIndex.key,
142+
templatePath,
111143
GlobalSearchScope.allScope(project)
112-
).filter { it.path.endsWith(suffix) }
144+
).filter { it.path.endsWith("Resources/views/$templatePath") }
113145
}
114146

115147
private fun filterByBundle(
116148
candidates: List<VirtualFile>,
117-
suffix: String,
149+
templatePath: String,
118150
bundleName: String
119151
): List<VirtualFile> {
152+
val suffix = "Resources/views/$templatePath"
120153
val normalizedBundle = normalize(bundleName)
121154

122155
// prefer a path segment exactly matching the bundle name, fall back to a substring

src/main/kotlin/de/shyim/shopware6/util/TwigUtil.kt

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import com.intellij.psi.PsiFile
99
import com.intellij.psi.PsiFileFactory
1010
import com.intellij.psi.PsiManager
1111
import com.intellij.psi.PsiRecursiveElementWalkingVisitor
12-
import com.intellij.psi.search.FilenameIndex
1312
import com.intellij.psi.search.GlobalSearchScope
1413
import com.intellij.util.indexing.FileBasedIndex
1514
import com.jetbrains.php.composer.actions.update.ComposerInstalledPackagesService
@@ -64,42 +63,49 @@ object TwigUtil {
6463
}
6564

6665
private val EXTENDS_PATTERN = Regex("\\{%-?\\s*(sw_)?extends\\s")
67-
private val EXTENDS_TARGET_PATTERN = Regex("\\{%-?\\s*(?:sw_)?extends\\s+['\"]@([A-Za-z0-9_]+)/([^'\"]+)['\"]")
66+
private val EXTENDS_TARGET_PATTERN = Regex("\\{%-?\\s*(?:sw_)?extends\\s+['\"](@[A-Za-z0-9_]+/[^'\"]+)['\"]")
6867

6968
fun isExtendingTemplate(file: PsiFile): Boolean {
7069
return EXTENDS_PATTERN.containsMatchIn(file.text)
7170
}
7271

72+
fun findExtendsTargetReference(content: CharSequence): String? {
73+
return EXTENDS_TARGET_PATTERN.find(content)?.groupValues?.get(1)
74+
}
75+
7376
fun getExtendsChainPaths(file: PsiFile): List<String> {
7477
val project = file.project
7578
val paths = ArrayList<String>()
7679
val visited = HashSet<String>()
7780
file.originalFile.virtualFile?.path?.let { visited.add(it) }
7881

79-
var current: PsiFile? = file
82+
var target = findExtendsTargetReference(file.text)
8083

81-
while (current != null && paths.size < 10) {
82-
val target = EXTENDS_TARGET_PATTERN.find(current.text) ?: break
83-
val parent =
84-
ShopwareTemplateUtil.findTemplateInBundle(project, target.groupValues[1], target.groupValues[2])
85-
?: break
84+
while (target != null && paths.size < 10) {
85+
val bundleName = target.substring(1).substringBefore("/")
86+
val templatePath = target.substringAfter("/", "")
87+
88+
if (templatePath.isEmpty()) {
89+
break
90+
}
91+
92+
val parent = ShopwareTemplateUtil.findTemplateInBundle(project, bundleName, templatePath) ?: break
8693

8794
if (!visited.add(parent.path)) {
8895
break
8996
}
9097

9198
paths.add(parent.path)
92-
current = PsiManager.getInstance(project).findFile(parent)
99+
100+
// the parents' extends targets come from the index, so no further files need to be loaded
101+
target = ShopwareTemplateUtil.getExtendsTarget(project, parent)
93102
}
94103

95104
return paths
96105
}
97106

98107
fun findBlockTagInFile(project: Project, path: String, blockName: String): PsiElement? {
99-
val virtualFile = FilenameIndex.getVirtualFilesByName(
100-
path.substringAfterLast('/'),
101-
GlobalSearchScope.allScope(project)
102-
).firstOrNull { it.path == path } ?: return null
108+
val virtualFile = ShopwareTemplateUtil.findTemplateByPath(project, path) ?: return null
103109

104110
val psiFile = PsiManager.getInstance(project).findFile(virtualFile) ?: return null
105111

@@ -265,10 +271,7 @@ object TwigUtil {
265271
}
266272

267273
// extensions in custom/plugins: read the version from the extension's composer.json
268-
val templateFile = FilenameIndex.getVirtualFilesByName(
269-
path.substringAfterLast('/'),
270-
GlobalSearchScope.allScope(project)
271-
).firstOrNull { it.path == path } ?: return null
274+
val templateFile = ShopwareTemplateUtil.findTemplateByPath(project, path) ?: return null
272275

273276
var dir = templateFile.parent
274277
while (dir != null) {

src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<fileBasedIndex implementation="de.shyim.shopware6.index.AdminModuleIndex"/>
3535
<fileBasedIndex implementation="de.shyim.shopware6.index.TwigBlockDeprecationIndex"/>
3636
<fileBasedIndex implementation="de.shyim.shopware6.index.TwigBlockHashIndex"/>
37+
<fileBasedIndex implementation="de.shyim.shopware6.index.ShopwareTemplateIndex"/>
3738

3839
<gotoDeclarationHandler implementation="de.shyim.shopware6.navigation.FeatureFlagGoToDeclareHandler"/>
3940
<gotoDeclarationHandler implementation="de.shyim.shopware6.navigation.AdminComponentGoToDeclareHandler"/>

0 commit comments

Comments
 (0)