From 222371d543ba705416cf8169dda97b01f2b10388 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Thu, 23 Sep 2021 21:39:44 +0200 Subject: [PATCH] feat: Feature flag autocompletion --- gradle.properties | 2 +- .../completion/PhpCompletionProvider.kt | 51 ++++++++ .../completion/TwigCompletionProvider.kt | 30 +++++ .../shyim/shopware6/index/FeatureFlagIndex.kt | 121 ++++++++++++++++++ .../shopware6/index/FrontendSnippetIndex.kt | 3 +- .../shyim/shopware6/index/dict/FeatureFlag.kt | 32 +++++ .../shopware6/index/{ => dict}/SnippetFile.kt | 2 +- .../FeatureFlagGoToDeclareHandler.kt | 76 +++++++++++ .../de/shyim/shopware6/util/PHPPattern.kt | 21 +++ .../de/shyim/shopware6/util/SnippetUtil.kt | 2 +- .../de/shyim/shopware6/util/TwigPattern.kt | 36 ++++++ src/main/resources/META-INF/plugin.xml | 8 +- 12 files changed, 379 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/de/shyim/shopware6/completion/PhpCompletionProvider.kt create mode 100644 src/main/kotlin/de/shyim/shopware6/index/FeatureFlagIndex.kt create mode 100644 src/main/kotlin/de/shyim/shopware6/index/dict/FeatureFlag.kt rename src/main/kotlin/de/shyim/shopware6/index/{ => dict}/SnippetFile.kt (93%) create mode 100644 src/main/kotlin/de/shyim/shopware6/navigation/FeatureFlagGoToDeclareHandler.kt create mode 100644 src/main/kotlin/de/shyim/shopware6/util/PHPPattern.kt diff --git a/gradle.properties b/gradle.properties index 193c53c..056de29 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,7 +20,7 @@ platformDownloadSources = true # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 -platformPlugins=com.jetbrains.php:212.4746.100,com.jetbrains.twig:212.4746.2,JavaScriptLanguage,Git4Idea,fr.adrienbrault.idea.symfony2plugin:0.23.212,de.espend.idea.php.annotation:8.0.0 +platformPlugins=com.jetbrains.php:212.4746.100,com.jetbrains.twig:212.4746.2,JavaScriptLanguage,Git4Idea,fr.adrienbrault.idea.symfony2plugin:0.23.212,de.espend.idea.php.annotation:8.0.0,org.jetbrains.plugins.yaml # Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 javaVersion = 11 diff --git a/src/main/kotlin/de/shyim/shopware6/completion/PhpCompletionProvider.kt b/src/main/kotlin/de/shyim/shopware6/completion/PhpCompletionProvider.kt new file mode 100644 index 0000000..de95011 --- /dev/null +++ b/src/main/kotlin/de/shyim/shopware6/completion/PhpCompletionProvider.kt @@ -0,0 +1,51 @@ +package de.shyim.shopware6.completion + +import com.intellij.codeInsight.completion.* +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.patterns.PlatformPatterns +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.ProcessingContext +import com.intellij.util.indexing.FileBasedIndex +import com.jetbrains.php.lang.psi.elements.ParameterList +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression +import de.shyim.shopware6.index.FeatureFlagIndex +import de.shyim.shopware6.util.PHPPattern +import icons.ShopwareToolBoxIcons + + +class PhpCompletionProvider : CompletionContributor() { + init { + extend( + CompletionType.BASIC, + PlatformPatterns.psiElement().withParent( + PlatformPatterns.psiElement(StringLiteralExpression::class.java).inside( + PlatformPatterns.psiElement(ParameterList::class.java) + ) + ), + object : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + val element = parameters.originalPosition ?: return + val project = element.project + + if (PHPPattern.isFeatureFlagFunction(element)) { + for (key in FileBasedIndex.getInstance().getAllKeys(FeatureFlagIndex.key, project)) { + val vals = FileBasedIndex.getInstance() + .getValues(FeatureFlagIndex.key, key, GlobalSearchScope.allScope(project)) + + vals.forEach { + result.addElement( + LookupElementBuilder.create(it.name).withTypeText(it.description) + .withIcon(ShopwareToolBoxIcons.SHOPWARE) + ) + } + } + } + } + } + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/shyim/shopware6/completion/TwigCompletionProvider.kt b/src/main/kotlin/de/shyim/shopware6/completion/TwigCompletionProvider.kt index 6dd6f53..dc4c4cf 100644 --- a/src/main/kotlin/de/shyim/shopware6/completion/TwigCompletionProvider.kt +++ b/src/main/kotlin/de/shyim/shopware6/completion/TwigCompletionProvider.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.project.Project import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.ProcessingContext import com.intellij.util.indexing.FileBasedIndex +import de.shyim.shopware6.index.FeatureFlagIndex import de.shyim.shopware6.index.FrontendSnippetIndex import de.shyim.shopware6.util.TwigPattern import icons.ShopwareToolBoxIcons @@ -39,5 +40,34 @@ class TwigCompletionProvider() : CompletionContributor() { } } ) + + extend( + CompletionType.BASIC, + TwigPattern.getPrintBlockOrTagFunctionPattern("feature")!!, + object : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + for (key in FileBasedIndex.getInstance() + .getAllKeys(FeatureFlagIndex.key, parameters.position.project)) { + val vals = FileBasedIndex.getInstance() + .getValues( + FeatureFlagIndex.key, + key, + GlobalSearchScope.allScope(parameters.position.project) + ) + + vals.forEach { + result.addElement( + LookupElementBuilder.create(it.name).withTypeText(it.description) + .withIcon(ShopwareToolBoxIcons.SHOPWARE) + ) + } + } + } + } + ) } } \ No newline at end of file diff --git a/src/main/kotlin/de/shyim/shopware6/index/FeatureFlagIndex.kt b/src/main/kotlin/de/shyim/shopware6/index/FeatureFlagIndex.kt new file mode 100644 index 0000000..3e446fc --- /dev/null +++ b/src/main/kotlin/de/shyim/shopware6/index/FeatureFlagIndex.kt @@ -0,0 +1,121 @@ +package de.shyim.shopware6.index + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiRecursiveElementWalkingVisitor +import com.intellij.util.indexing.* +import com.intellij.util.io.EnumeratorStringDescriptor +import com.intellij.util.io.KeyDescriptor +import de.shyim.shopware6.index.dict.FeatureFlag +import de.shyim.shopware6.index.externalizer.ObjectStreamDataExternalizer +import gnu.trove.THashMap +import org.jetbrains.yaml.YAMLFileType +import org.jetbrains.yaml.psi.YAMLKeyValue +import org.jetbrains.yaml.psi.impl.YAMLSequenceItemImpl + +class FeatureFlagIndex : FileBasedIndexExtension() { + private val EXTERNALIZER = ObjectStreamDataExternalizer() + + override fun getName(): ID { + return FeatureFlagIndex.key + } + + override fun getVersion(): Int { + return 1 + } + + override fun dependsOnFileContent(): Boolean { + return true + } + + override fun getIndexer(): DataIndexer { + return DataIndexer { inputData -> + if (!inputData.getFile().getName().equals("shopware.yaml")) { + return@DataIndexer mapOf() + } + + if (!inputData.file.parent.name.equals("packages")) { + return@DataIndexer mapOf() + } + + val flags = THashMap() + + inputData.psiFile.acceptChildren(object : PsiRecursiveElementWalkingVisitor() { + override fun visitElement(element: PsiElement) { + if (element is YAMLKeyValue) { + if (element.firstChild.text != "shopware" && element.firstChild.text != "feature" && element.firstChild.text != "flags") { + return + } + + if (element.firstChild.text == "flags") { + element.acceptChildren(object : PsiRecursiveElementWalkingVisitor() { + override fun visitElement(element: PsiElement) { + if (element is YAMLSequenceItemImpl) { + val map = HashMap() + + element.acceptChildren(object : PsiRecursiveElementWalkingVisitor() { + override fun visitElement(element: PsiElement) { + if (element is YAMLKeyValue) { + map.set(element.firstChild.text, element.lastChild.firstChild.text) + } + + super.visitElement(element) + } + }) + + if (map.containsKey("name")) { + var default = false + var major = false + + if (map.containsKey("default") && map.get("default") == "true") { + default = true + } + + if (map.containsKey("major") && map.get("major") == "true") { + major = true + } + + flags[map.get("name")] = FeatureFlag( + map.getOrDefault("name", ""), + default, + major, + map.getOrDefault("description", ""), + inputData.file.path + ) + } + + return + } + + super.visitElement(element) + } + }) + + return + } + } + + super.visitElement(element) + } + }) + + return@DataIndexer flags + } + } + + override fun getKeyDescriptor(): KeyDescriptor { + return EnumeratorStringDescriptor.INSTANCE + } + + override fun getValueExternalizer(): ObjectStreamDataExternalizer { + return EXTERNALIZER + } + + companion object { + val key = ID.create("de.shyim.shopware6.core.feature_flag") + } + + override fun getInputFilter(): FileBasedIndex.InputFilter { + return object : DefaultFileTypeSpecificInputFilter(YAMLFileType.YML) { + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/shyim/shopware6/index/FrontendSnippetIndex.kt b/src/main/kotlin/de/shyim/shopware6/index/FrontendSnippetIndex.kt index b125bb3..fd0748b 100644 --- a/src/main/kotlin/de/shyim/shopware6/index/FrontendSnippetIndex.kt +++ b/src/main/kotlin/de/shyim/shopware6/index/FrontendSnippetIndex.kt @@ -4,6 +4,7 @@ import com.intellij.json.JsonFileType import com.intellij.util.indexing.* import com.intellij.util.io.EnumeratorStringDescriptor import com.intellij.util.io.KeyDescriptor +import de.shyim.shopware6.index.dict.SnippetFile import de.shyim.shopware6.index.externalizer.ObjectStreamDataExternalizer import de.shyim.shopware6.util.SnippetUtil import gnu.trove.THashMap @@ -17,7 +18,7 @@ open class FrontendSnippetIndex : FileBasedIndexExtension() } override fun getVersion(): Int { - return 2 + return 3 } override fun dependsOnFileContent(): Boolean { diff --git a/src/main/kotlin/de/shyim/shopware6/index/dict/FeatureFlag.kt b/src/main/kotlin/de/shyim/shopware6/index/dict/FeatureFlag.kt new file mode 100644 index 0000000..0010c3a --- /dev/null +++ b/src/main/kotlin/de/shyim/shopware6/index/dict/FeatureFlag.kt @@ -0,0 +1,32 @@ +package de.shyim.shopware6.index.dict + +import org.apache.commons.lang.builder.HashCodeBuilder +import java.io.Serializable +import java.util.* + +class FeatureFlag( + var name: String, + var default: Boolean, + var major: Boolean, + var description: String, + var file: String +) : Serializable { + override fun hashCode(): Int { + return HashCodeBuilder() + .append(this.name) + .append(this.default) + .append(this.major) + .append(this.description) + .append(this.file) + .toHashCode() + } + + override fun equals(obj: Any?): Boolean { + return obj is FeatureFlag && + Objects.equals(obj.name, this.name) && + Objects.equals(obj.default, this.default) && + Objects.equals(obj.major, this.major) && + Objects.equals(obj.description, this.description) && + Objects.equals(obj.file, this.file) + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/shyim/shopware6/index/SnippetFile.kt b/src/main/kotlin/de/shyim/shopware6/index/dict/SnippetFile.kt similarity index 93% rename from src/main/kotlin/de/shyim/shopware6/index/SnippetFile.kt rename to src/main/kotlin/de/shyim/shopware6/index/dict/SnippetFile.kt index a865ccc..5bc72b5 100644 --- a/src/main/kotlin/de/shyim/shopware6/index/SnippetFile.kt +++ b/src/main/kotlin/de/shyim/shopware6/index/dict/SnippetFile.kt @@ -1,4 +1,4 @@ -package de.shyim.shopware6.index +package de.shyim.shopware6.index.dict import org.apache.commons.lang.builder.HashCodeBuilder import java.io.Serializable diff --git a/src/main/kotlin/de/shyim/shopware6/navigation/FeatureFlagGoToDeclareHandler.kt b/src/main/kotlin/de/shyim/shopware6/navigation/FeatureFlagGoToDeclareHandler.kt new file mode 100644 index 0000000..1e1e1f9 --- /dev/null +++ b/src/main/kotlin/de/shyim/shopware6/navigation/FeatureFlagGoToDeclareHandler.kt @@ -0,0 +1,76 @@ +package de.shyim.shopware6.navigation + +import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiRecursiveElementWalkingVisitor +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.indexing.FileBasedIndex +import de.shyim.shopware6.index.FeatureFlagIndex +import de.shyim.shopware6.index.dict.FeatureFlag +import de.shyim.shopware6.util.PHPPattern +import de.shyim.shopware6.util.TwigPattern +import org.jetbrains.yaml.psi.impl.YAMLPlainTextImpl + +class FeatureFlagGoToDeclareHandler : GotoDeclarationHandler { + override fun getGotoDeclarationTargets( + element: PsiElement?, + offset: Int, + editor: Editor? + ): Array? { + if (editor === null || editor.project === null || element == null) { + return null + } + + val project = editor.project!! + + val psiElements: MutableList = ArrayList() + + if (PHPPattern.isFeatureFlagFunction(element) || TwigPattern.getPrintBlockOrTagFunctionPattern("feature")!! + .accepts(element) + ) { + for (key in FileBasedIndex.getInstance().getAllKeys(FeatureFlagIndex.key, project)) { + if (key == element.text) { + val vals = FileBasedIndex.getInstance() + .getValues(FeatureFlagIndex.key, key, GlobalSearchScope.allScope(project)) + + vals.forEach { + lookupPsiElementFromFile(psiElements, it, project) + } + } + } + } + + return psiElements.toTypedArray() + } + + private fun lookupPsiElementFromFile( + psiElements: MutableList, + featureFlag: FeatureFlag, + project: Project + ) { + val file = LocalFileSystem.getInstance().findFileByPath(featureFlag.file) + + if (file != null) { + val psi = PsiManager.getInstance(project).findFile(file) + + if (psi != null) { + psi.acceptChildren(object : PsiRecursiveElementWalkingVisitor() { + override fun visitElement(element: PsiElement) { + if (element is YAMLPlainTextImpl) { + if (element.text == featureFlag.name) { + psiElements.add(element) + return + } + } + + super.visitElement(element) + } + }) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/shyim/shopware6/util/PHPPattern.kt b/src/main/kotlin/de/shyim/shopware6/util/PHPPattern.kt new file mode 100644 index 0000000..43f47c5 --- /dev/null +++ b/src/main/kotlin/de/shyim/shopware6/util/PHPPattern.kt @@ -0,0 +1,21 @@ +package de.shyim.shopware6.util + +import com.intellij.psi.PsiElement +import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher + +object PHPPattern { + private val FEATURE_FLAG_SIGNATURES: Array = arrayOf( + MethodMatcher.CallToSignature("\\Shopware\\Core\\Framework\\Feature", "isActive"), + MethodMatcher.CallToSignature("\\Shopware\\Core\\Framework\\Feature", "ifActive"), + MethodMatcher.CallToSignature("\\Shopware\\Core\\Framework\\Feature", "ifActiveCall"), + MethodMatcher.CallToSignature("\\Shopware\\Core\\Framework\\Feature", "skipTestIfInActive"), + MethodMatcher.CallToSignature("\\Shopware\\Core\\Framework\\Feature", "skipTestIfActive"), + MethodMatcher.CallToSignature("\\Shopware\\Core\\Framework\\Feature", "triggerDeprecated"), + MethodMatcher.CallToSignature("\\Shopware\\Core\\Framework\\Feature", "throwException"), + MethodMatcher.CallToSignature("\\Shopware\\Core\\Framework\\Feature", "has"), + ) + + fun isFeatureFlagFunction(element: PsiElement): Boolean { + return MethodMatcher.getMatchedSignatureWithDepth(element.getContext(), FEATURE_FLAG_SIGNATURES) != null + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/shyim/shopware6/util/SnippetUtil.kt b/src/main/kotlin/de/shyim/shopware6/util/SnippetUtil.kt index 8fc7737..2e4172f 100644 --- a/src/main/kotlin/de/shyim/shopware6/util/SnippetUtil.kt +++ b/src/main/kotlin/de/shyim/shopware6/util/SnippetUtil.kt @@ -1,6 +1,6 @@ package de.shyim.shopware6.util -import de.shyim.shopware6.index.SnippetFile +import de.shyim.shopware6.index.dict.SnippetFile import gnu.trove.THashMap import org.codehaus.jettison.json.JSONException import org.codehaus.jettison.json.JSONObject diff --git a/src/main/kotlin/de/shyim/shopware6/util/TwigPattern.kt b/src/main/kotlin/de/shyim/shopware6/util/TwigPattern.kt index 9f7dceb..3d698f4 100644 --- a/src/main/kotlin/de/shyim/shopware6/util/TwigPattern.kt +++ b/src/main/kotlin/de/shyim/shopware6/util/TwigPattern.kt @@ -6,6 +6,8 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiWhiteSpace import com.jetbrains.twig.TwigLanguage import com.jetbrains.twig.TwigTokenTypes +import com.jetbrains.twig.elements.TwigElementTypes + object TwigPattern { fun getTranslationKeyPattern(vararg type: String?): ElementPattern { @@ -30,4 +32,38 @@ object TwigPattern { ) .withLanguage(TwigLanguage.INSTANCE) } + + fun getPrintBlockOrTagFunctionPattern(vararg functionName: String?): ElementPattern? { + return PlatformPatterns + .psiElement(TwigTokenTypes.STRING_TEXT) + .withParent( + getFunctionCallScopePattern()!! + ) + .afterLeafSkipping( + PlatformPatterns.or( + PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), + PlatformPatterns.psiElement(PsiWhiteSpace::class.java), + PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), + PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), + PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) + ), + PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER) + .withText(PlatformPatterns.string().oneOf(*functionName)) + ) + .withLanguage(TwigLanguage.INSTANCE) + } + + private fun getFunctionCallScopePattern(): ElementPattern? { + return PlatformPatterns.or( // old and inconsistently implementations of FUNCTION_CALL: + // eg {% if asset('') %} does not provide a FUNCTION_CALL whereas a print block does + PlatformPatterns.psiElement(TwigElementTypes.PRINT_BLOCK), + PlatformPatterns.psiElement(TwigElementTypes.TAG), + PlatformPatterns.psiElement(TwigElementTypes.IF_TAG), + PlatformPatterns.psiElement(TwigElementTypes.SET_TAG), + PlatformPatterns.psiElement(TwigElementTypes.ELSE_TAG), + PlatformPatterns.psiElement(TwigElementTypes.ELSEIF_TAG), + PlatformPatterns.psiElement(TwigElementTypes.FOR_TAG), // PhpStorm 2017.3.2: {{ asset('') }} + PlatformPatterns.psiElement(TwigElementTypes.FUNCTION_CALL) + ) + } } \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 81a945a..599f0fb 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -7,6 +7,7 @@ com.intellij.modules.platform com.jetbrains.php com.jetbrains.twig + org.jetbrains.plugins.yaml JavaScript Git4Idea fr.adrienbrault.idea.symfony2plugin @@ -19,10 +20,15 @@ + + + + + -