From c19b031db0acc439b9f46508e7d9cb981e70e820 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Sun, 3 Dec 2023 17:03:01 +0100 Subject: [PATCH] add Twig component namespace config index --- .../stubs/dict/ConfigIndex.java | 49 +++++ .../stubs/indexes/ConfigStubIndex.java | 173 ++++++++++++++++++ .../idea/symfony2plugin/util/UxUtil.java | 38 +++- src/main/resources/META-INF/plugin.xml | 1 + .../symfony2plugin/tests/util/UxUtilTest.java | 14 +- .../tests/util/fixtures/twig_component.yaml | 21 +++ 6 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 src/main/java/fr/adrienbrault/idea/symfony2plugin/stubs/dict/ConfigIndex.java create mode 100644 src/main/java/fr/adrienbrault/idea/symfony2plugin/stubs/indexes/ConfigStubIndex.java create mode 100644 src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/util/fixtures/twig_component.yaml diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/stubs/dict/ConfigIndex.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/stubs/dict/ConfigIndex.java new file mode 100644 index 000000000..81185b345 --- /dev/null +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/stubs/dict/ConfigIndex.java @@ -0,0 +1,49 @@ +package fr.adrienbrault.idea.symfony2plugin.stubs.dict; + +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +/** + * @author Daniel Espendiller + */ +public class ConfigIndex implements Serializable { + @NotNull + private final String name; + + @NotNull + private final TreeMap> configs; + + public ConfigIndex(@NotNull String name, @NotNull TreeMap> configs) { + this.name = name; + this.configs = configs; + } + + @NotNull + public String getName() { + return name; + } + + @NotNull + public Map> getConfigs() { + return configs; + } + + public int hashCode() { + return new HashCodeBuilder() + .append(this.name) + .append(this.configs.hashCode()) + .toHashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof ConfigIndex && + Objects.equals(((ConfigIndex) obj).name, this.name) && + Objects.equals(((ConfigIndex) obj).configs, this.configs); + } +} diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/stubs/indexes/ConfigStubIndex.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/stubs/indexes/ConfigStubIndex.java new file mode 100644 index 000000000..d3cc5ffe1 --- /dev/null +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/stubs/indexes/ConfigStubIndex.java @@ -0,0 +1,173 @@ +package fr.adrienbrault.idea.symfony2plugin.stubs.indexes; + +import com.intellij.openapi.vfs.VfsUtil; +import com.intellij.psi.PsiFile; +import com.intellij.util.indexing.*; +import com.intellij.util.io.DataExternalizer; +import com.intellij.util.io.EnumeratorStringDescriptor; +import com.intellij.util.io.KeyDescriptor; +import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; +import fr.adrienbrault.idea.symfony2plugin.stubs.dict.ConfigIndex; +import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.externalizer.ObjectStreamDataExternalizer; +import fr.adrienbrault.idea.symfony2plugin.util.ProjectUtil; +import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; +import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.yaml.YAMLFileType; +import org.jetbrains.yaml.psi.*; +import org.jetbrains.yaml.psi.impl.YAMLPlainTextImpl; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * @author Daniel Espendiller + */ +public class ConfigStubIndex extends FileBasedIndexExtension { + public static final ID KEY = ID.create("fr.adrienbrault.idea.symfony2plugin.config_stub_index"); + private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor(); + private static final int MAX_FILE_BYTE_SIZE = 2097152; + private static final ObjectStreamDataExternalizer EXTERNALIZER = new ObjectStreamDataExternalizer<>(); + + @NotNull + @Override + public ID getName() { + return KEY; + } + + @Override + public @NotNull DataIndexer getIndexer() { + return new DataIndexer<>() { + @Override + public @NotNull Map map(@NotNull FileContent inputData) { + if (!(inputData.getPsiFile() instanceof YAMLFile yamlFile) || + !Symfony2ProjectComponent.isEnabledForIndex(yamlFile.getProject()) || + !isValidForIndex(inputData, yamlFile) + ) { + return Collections.emptyMap(); + } + + Map map = new HashMap<>(); + + TreeMap> configs = new TreeMap<>(); + + for (YAMLKeyValue yamlKeyValue : YamlHelper.getTopLevelKeyValues(yamlFile)) { + + String keyText = yamlKeyValue.getKeyText(); + if ("twig_component".equals(keyText)) { + visitKey(yamlKeyValue, configs); + } + + if (keyText.startsWith("when@")) { + YAMLValue value = yamlKeyValue.getValue(); + if (value instanceof YAMLMapping) { + for (YAMLKeyValue yamlKeyValue2 : ((YAMLMapping) value).getKeyValues()) { + String keyText2 = yamlKeyValue2.getKeyText(); + if ("twig_component".equals(keyText2)) { + visitKey(yamlKeyValue2, configs); + } + } + } + } + } + + if (!configs.isEmpty()) { + map.put("twig_component_defaults", new ConfigIndex("twig_component_defaults", configs)); + } + + return map; + } + + private static void visitKey(@NotNull YAMLKeyValue yamlKeyValue, @NotNull TreeMap> configs) { + YAMLValue value = yamlKeyValue.getValue(); + if (value instanceof YAMLMapping yamlMapping) { + YAMLKeyValue defaults = YamlHelper.getYamlKeyValue(yamlMapping, "defaults"); + if (defaults == null) { + return; + } + + YAMLValue value1 = defaults.getValue(); + if (value1 instanceof YAMLMapping yamlMapping1) { + for (YAMLKeyValue keyValue : yamlMapping1.getKeyValues()) { + String keyText1 = keyValue.getKeyText(); + + YAMLValue value2 = keyValue.getValue(); + if (value2 instanceof YAMLQuotedText || value2 instanceof YAMLPlainTextImpl) { + String s = PsiElementUtils.trimQuote(value2.getText()); + if (!StringUtils.isBlank(s)) { + TreeMap items = new TreeMap<>(); + items.put("template_directory", s); + configs.put(keyText1, items); + } + } else if (value2 instanceof YAMLMapping yamlMapping2) { + TreeMap items = new TreeMap<>(); + + String templateDirectory = YamlHelper.getYamlKeyValueAsString(yamlMapping2, "template_directory"); + if (templateDirectory == null) { + continue; + } + + items.put("template_directory", templateDirectory); + + String namePrefix = YamlHelper.getYamlKeyValueAsString(yamlMapping2, "name_prefix"); + if (namePrefix != null) { + items.put("name_prefix", namePrefix); + } + + configs.put(keyText1, items); + } + } + } + } + } + }; + } + + @Override + public @NotNull KeyDescriptor getKeyDescriptor() { + return this.myKeyDescriptor; + } + + @Override + public @NotNull DataExternalizer getValueExternalizer() { + return EXTERNALIZER; + } + + @Override + public int getVersion() { + return 1; + } + + @Override + public FileBasedIndex.@NotNull InputFilter getInputFilter() { + return virtualFile -> virtualFile.getFileType() == YAMLFileType.YML; + } + + + @Override + public boolean dependsOnFileContent() { + return true; + } + + private static boolean isValidForIndex(FileContent inputData, PsiFile psiFile) { + String fileName = psiFile.getName(); + if(fileName.startsWith(".") || fileName.endsWith("Test")) { + return false; + } + + // is Test file in path name + String relativePath = VfsUtil.getRelativePath(inputData.getFile(), ProjectUtil.getProjectDir(inputData.getProject()), '/'); + if(relativePath != null && (relativePath.contains("/Test/") || relativePath.contains("/Tests/") || relativePath.contains("/Fixture/") || relativePath.contains("/Fixtures/"))) { + return false; + } + + if(inputData.getFile().getLength() > MAX_FILE_BYTE_SIZE) { + return false; + } + + return true; + } +} diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/UxUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/UxUtil.java index e2db8d58e..46bfdde7f 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/UxUtil.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/UxUtil.java @@ -3,16 +3,21 @@ import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.util.CachedValue; +import com.intellij.psi.util.CachedValueProvider; +import com.intellij.psi.util.CachedValuesManager; import com.intellij.util.indexing.FileBasedIndex; import com.jetbrains.php.lang.psi.PhpFile; import com.jetbrains.php.lang.psi.elements.*; import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; -import fr.adrienbrault.idea.symfony2plugin.stubs.dict.TemplateInclude; +import fr.adrienbrault.idea.symfony2plugin.stubs.cache.FileIndexCaches; +import fr.adrienbrault.idea.symfony2plugin.stubs.dict.ConfigIndex; import fr.adrienbrault.idea.symfony2plugin.stubs.dict.UxComponent; -import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigIncludeStubIndex; +import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ConfigStubIndex; import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.UxTemplateStubIndex; import fr.adrienbrault.idea.symfony2plugin.stubs.util.IndexUtil; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigTypeResolveUtil; @@ -40,11 +45,36 @@ public class UxUtil { private static final String ATTRIBUTE_EXPOSE_IN_TEMPLATE = "\\Symfony\\UX\\TwigComponent\\Attribute\\ExposeInTemplate"; + private static final Key>> TWIG_COMPONENTS_NAMESPACES = new Key<>("SYMFONY_TWIG_COMPONENTS_NAMESPACES"); + public static Collection getNamespaces(@NotNull Project project) { + return CachedValuesManager.getManager(project).getCachedValue( + project, + TWIG_COMPONENTS_NAMESPACES, + () -> CachedValueProvider.Result.create(getNamespacesInner(project), FileIndexCaches.getModificationTrackerForIndexId(project, ConfigStubIndex.KEY)), + false + ); + } + private static Collection getNamespacesInner(@NotNull Project project) { Collection namespaces = new ArrayList<>(); - // @TODO: config parsing - namespaces.add(new TwigComponentNamespace("App\\Twig\\Components\\", "components/", null)); + for (String key : IndexUtil.getAllKeysForProject(ConfigStubIndex.KEY, project)) { + for (ConfigIndex value : FileBasedIndex.getInstance().getValues(ConfigStubIndex.KEY, key, GlobalSearchScope.allScope(project))) { + for (Map.Entry> entry : value.getConfigs().entrySet()) { + String templateDirectory = entry.getValue().get("template_directory"); + if (templateDirectory == null) { + continue; + } + + namespaces.add(new TwigComponentNamespace(entry.getKey(), templateDirectory, entry.getValue().get("name_prefix"))); + } + } + } + + // add default if not presented + if (namespaces.stream().noneMatch(n -> "App\\Twig\\Components".equals(StringUtils.strip(n.namespace(), "\\")))) { + namespaces.add(new TwigComponentNamespace("App\\Twig\\Components\\", "components/", null)); + } return namespaces; } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 6703a6299..1e194791d 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -274,6 +274,7 @@ + diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/util/UxUtilTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/util/UxUtilTest.java index 48653b158..f25eb0be0 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/util/UxUtilTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/util/UxUtilTest.java @@ -12,12 +12,12 @@ import com.jetbrains.php.lang.psi.elements.PhpNamedElement; import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase; import fr.adrienbrault.idea.symfony2plugin.util.UxUtil; +import fr.adrienbrault.idea.symfony2plugin.util.dict.TwigComponentNamespace; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; -import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -33,6 +33,18 @@ public void setUp() throws Exception { public String getTestDataPath() { return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/util/fixtures"; } + public void testUxUtil() { + myFixture.copyFileToProject("twig_component.yaml"); + + Collection namespaces = UxUtil.getNamespaces(getProject()); + assertEquals("components/", namespaces.stream().filter(n -> "App\\Twig\\Components\\".equals(n.namespace())).findFirst().get().templateDirectory()); + assertEquals("components", namespaces.stream().filter(n -> "App\\Twig\\Foobar\\".equals(n.namespace())).findFirst().get().templateDirectory()); + assertEquals("foobar/", namespaces.stream().filter(n -> "App\\Twig\\WhenSwitch\\".equals(n.namespace())).findFirst().get().templateDirectory()); + + TwigComponentNamespace n1 = namespaces.stream().filter(n -> "App\\Twig\\Components2\\".equals(n.namespace())).findFirst().get(); + assertEquals("components", n1.templateDirectory()); + assertEquals("AppBar", n1.namePrefix()); + } public void testVisitAsTwigComponent() { PhpFile phpFile = (PhpFile) PhpPsiElementFactory.createPsiFileFromText(getProject(), " AppBar:Alert + # App\Twig\Components2\Button\Primary => AppBar:Button:Primary + name_prefix: AppBar + +when@test: + twig_component: + defaults: + # Namespace & directory for components + App\Twig\WhenSwitch\: 'foobar/'