Skip to content

Commit 9b09ff4

Browse files
authored
Merge pull request #300 from shopware/twig-native-navigation
Replace Symfony plugin sw_extends/sw_include integration with own navigation
2 parents e1836bf + 037fc0f commit 9b09ff4

17 files changed

Lines changed: 510 additions & 115 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
## Unreleased
66

7+
- 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`
8+
- 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
10+
711
- 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.
812
- The "versioning comment missing" inspection only reports files that extend another template via `sw_extends`
913
- The upstream of a block is now resolved through the `sw_extends` chain of the template, so versioning also works when extending a template at a different relative path and sibling overrides of other plugins cannot be mistaken for the upstream

doc/twig-versioning.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Versioning works for blocks of the Shopware core **and any other extension** —
3838

3939
Once an override is reviewed, re-run the *"Add/Update"* intention (or the quick fix) to record the new hash, which clears the warning.
4040

41+
To compare an override with its origin, **Ctrl+Click (Go to Declaration) on a block name** navigates to the upstream block it overrides — nearest parent in the `sw_extends` chain first. Template references in `sw_extends` / `sw_include` navigate to all templates of that view path: the referenced bundle first, followed by every plugin override.
42+
4143
## How the upstream of a block is resolved
4244

4345
All blocks of all templates under `Resources/views/` are indexed with their content hash. For a block in your file, the upstream candidates are determined in two steps:

src/main/kotlin/de/shyim/shopware6/completion/TwigCompletionProvider.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ class TwigCompletionProvider : CompletionContributor() {
7777
}
7878
)
7979

80+
extend(
81+
CompletionType.BASIC,
82+
TwigPattern.getShopwareIncludeExtendsTagPattern(),
83+
object : CompletionProvider<CompletionParameters>() {
84+
override fun addCompletions(
85+
parameters: CompletionParameters,
86+
context: ProcessingContext,
87+
result: CompletionResultSet
88+
) {
89+
if (!TwigPattern.isShopwareIncludeExtendsTag(parameters.position)) {
90+
return
91+
}
92+
93+
result.addAllElements(ShopwareTemplateUtil.getTemplateLookupElements(parameters.position.project))
94+
}
95+
}
96+
)
97+
8098
extend(
8199
CompletionType.BASIC,
82100
TwigPattern.getPrintBlockOrTagFunctionPattern("theme_config"),
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+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package de.shyim.shopware6.navigation
2+
3+
import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler
4+
import com.intellij.openapi.editor.Editor
5+
import com.intellij.psi.PsiElement
6+
import com.jetbrains.twig.elements.TwigBlockTag
7+
import de.shyim.shopware6.util.TwigUtil
8+
9+
class TwigBlockGoToDeclareHandler : GotoDeclarationHandler {
10+
override fun getGotoDeclarationTargets(
11+
element: PsiElement?,
12+
offset: Int,
13+
editor: Editor?
14+
): Array<PsiElement>? {
15+
if (element == null) {
16+
return null
17+
}
18+
19+
val blockTag = element.parent as? TwigBlockTag ?: return null
20+
val blockName = blockTag.name ?: return null
21+
22+
if (blockName != element.text) {
23+
return null
24+
}
25+
26+
// navigate to the upstream block this one overrides, nearest parent first
27+
val targets = TwigUtil.getUpstreamBlocks(element.containingFile.originalFile, blockName)
28+
.mapNotNull { TwigUtil.findBlockTagInFile(element.project, it.absolutePath, blockName) }
29+
30+
return if (targets.isEmpty()) null else targets.toTypedArray()
31+
}
32+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package de.shyim.shopware6.navigation
2+
3+
import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler
4+
import com.intellij.openapi.editor.Editor
5+
import com.intellij.psi.PsiElement
6+
import com.intellij.psi.PsiManager
7+
import com.intellij.psi.util.elementType
8+
import com.jetbrains.twig.TwigTokenTypes
9+
import com.jetbrains.twig.elements.TwigElementTypes
10+
import de.shyim.shopware6.util.ShopwareTemplateUtil
11+
12+
class TwigTemplateGoToDeclareHandler : GotoDeclarationHandler {
13+
override fun getGotoDeclarationTargets(
14+
element: PsiElement?,
15+
offset: Int,
16+
editor: Editor?
17+
): Array<PsiElement>? {
18+
if (element == null || element.elementType != TwigTokenTypes.STRING_TEXT) {
19+
return null
20+
}
21+
22+
val tag = element.parent ?: return null
23+
24+
if (tag.elementType != TwigElementTypes.TAG) {
25+
return null
26+
}
27+
28+
val tagName = tag.node.findChildByType(TwigTokenTypes.TAG_NAME)?.text
29+
if (tagName != "sw_extends" && tagName != "sw_include") {
30+
return null
31+
}
32+
33+
// only the template reference, not other strings inside the tag (e.g. sw_include with)
34+
if (tag.node.getChildren(null).firstOrNull { it.elementType == TwigTokenTypes.STRING_TEXT }?.psi != element) {
35+
return null
36+
}
37+
38+
val project = element.project
39+
val currentPath = element.containingFile.originalFile.virtualFile?.path
40+
41+
val targets = ShopwareTemplateUtil.resolveTemplateReference(project, element.text)
42+
.filter { it.path != currentPath }
43+
.mapNotNull { PsiManager.getInstance(project).findFile(it) }
44+
45+
return if (targets.isEmpty()) null else targets.toTypedArray()
46+
}
47+
}

src/main/kotlin/de/shyim/shopware6/symfonyplugin/SymfonyTwigCompletionProvider.kt

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import com.intellij.codeInsight.completion.CompletionType
88
import com.intellij.util.ProcessingContext
99
import de.shyim.shopware6.util.TwigPattern
1010
import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper
11-
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil
12-
import java.util.Collections
1311

1412
class SymfonyTwigCompletionProvider : CompletionContributor() {
1513
init {
@@ -27,24 +25,5 @@ class SymfonyTwigCompletionProvider : CompletionContributor() {
2725
}
2826
)
2927

30-
extend(
31-
CompletionType.BASIC,
32-
TwigPattern.getShopwareIncludeExtendsTagPattern(),
33-
object : CompletionProvider<CompletionParameters>() {
34-
override fun addCompletions(
35-
parameters: CompletionParameters,
36-
context: ProcessingContext,
37-
result: CompletionResultSet
38-
) {
39-
result.addAllElements(
40-
TwigUtil.getTwigLookupElements(
41-
parameters.position.project,
42-
Collections.emptyList()
43-
)
44-
)
45-
}
46-
}
47-
)
48-
4928
}
5029
}

src/main/kotlin/de/shyim/shopware6/symfonyplugin/TwigFileUsageProvider.kt

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)