Skip to content

Commit

Permalink
Merge pull request #2259 from Haehnchen/feature/index-twig-component-ns
Browse files Browse the repository at this point in the history
add Twig component namespace config index
  • Loading branch information
Haehnchen authored Dec 3, 2023
2 parents 1efa7e9 + c19b031 commit b4d0db7
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
*/
public class ConfigIndex implements Serializable {
@NotNull
private final String name;

@NotNull
private final TreeMap<String, TreeMap<String, String>> configs;

public ConfigIndex(@NotNull String name, @NotNull TreeMap<String, TreeMap<String, String>> configs) {
this.name = name;
this.configs = configs;
}

@NotNull
public String getName() {
return name;
}

@NotNull
public Map<String, TreeMap<String, String>> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
*/
public class ConfigStubIndex extends FileBasedIndexExtension<String, ConfigIndex> {
public static final ID<String, ConfigIndex> KEY = ID.create("fr.adrienbrault.idea.symfony2plugin.config_stub_index");
private final KeyDescriptor<String> myKeyDescriptor = new EnumeratorStringDescriptor();
private static final int MAX_FILE_BYTE_SIZE = 2097152;
private static final ObjectStreamDataExternalizer<ConfigIndex> EXTERNALIZER = new ObjectStreamDataExternalizer<>();

@NotNull
@Override
public ID<String, ConfigIndex> getName() {
return KEY;
}

@Override
public @NotNull DataIndexer<String, ConfigIndex, FileContent> getIndexer() {
return new DataIndexer<>() {
@Override
public @NotNull Map<String, ConfigIndex> map(@NotNull FileContent inputData) {
if (!(inputData.getPsiFile() instanceof YAMLFile yamlFile) ||
!Symfony2ProjectComponent.isEnabledForIndex(yamlFile.getProject()) ||
!isValidForIndex(inputData, yamlFile)
) {
return Collections.emptyMap();
}

Map<String, ConfigIndex> map = new HashMap<>();

TreeMap<String, TreeMap<String, String>> 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<String, TreeMap<String, String>> 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<String, String> items = new TreeMap<>();
items.put("template_directory", s);
configs.put(keyText1, items);
}
} else if (value2 instanceof YAMLMapping yamlMapping2) {
TreeMap<String, String> 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<String> getKeyDescriptor() {
return this.myKeyDescriptor;
}

@Override
public @NotNull DataExternalizer<ConfigIndex> 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;
}
}
38 changes: 34 additions & 4 deletions src/main/java/fr/adrienbrault/idea/symfony2plugin/util/UxUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -40,11 +45,36 @@ public class UxUtil {

private static final String ATTRIBUTE_EXPOSE_IN_TEMPLATE = "\\Symfony\\UX\\TwigComponent\\Attribute\\ExposeInTemplate";

private static final Key<CachedValue<Collection<TwigComponentNamespace>>> TWIG_COMPONENTS_NAMESPACES = new Key<>("SYMFONY_TWIG_COMPONENTS_NAMESPACES");

public static Collection<TwigComponentNamespace> 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<TwigComponentNamespace> getNamespacesInner(@NotNull Project project) {
Collection<TwigComponentNamespace> 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<String, TreeMap<String, String>> 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;
}
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.SerializerClassUsageStubIndex"/>
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.UxTemplateStubIndex"/>
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigBlockEmbedIndex"/>
<fileBasedIndex implementation="fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ConfigStubIndex"/>

<codeInsight.lineMarkerProvider language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.config.ServiceLineMarkerProvider"/>
<codeInsight.lineMarkerProvider language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.dic.ControllerMethodLineMarkerProvider"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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<TwigComponentNamespace> 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(), "<?php\n" +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
twig_component:
anonymous_template_directory: 'components/'
defaults:
# Namespace & directory for components
App\Twig\Components\: 'components/'
App\Twig\Foobar\: components


# long form
App\Twig\Components2\:
template_directory: components
# component names will have an extra "AppBar:" prefix
# App\Twig\Components2\Alert => 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/'

0 comments on commit b4d0db7

Please sign in to comment.