diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityHelper.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityHelper.java index f3cac78f9..6b2f2de8c 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityHelper.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/doctrine/EntityHelper.java @@ -2,12 +2,13 @@ import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; -import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.*; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.util.Function; @@ -45,6 +46,8 @@ public class EntityHelper { public static final ExtensionPointName MODEL_POINT_NAME = new ExtensionPointName<>("fr.adrienbrault.idea.symfony2plugin.extension.DoctrineModelProvider"); + private static final Key>> SYMFONY_DOCTRINE_MODEL_CACHE = new Key<>("SYMFONY_DOCTRINE_MODEL_CACHE"); + final public static String[] ANNOTATION_FIELDS = new String[] { "\\Doctrine\\ORM\\Mapping\\Column", "\\Doctrine\\ORM\\Mapping\\OneToOne", @@ -693,11 +696,10 @@ public static void attachAnnotationInformation(@NotNull PhpClass phpClass, @NotN } - /** - * One PhpClass can have multiple targets and names @TODO: refactor - */ - public static Collection getModelClasses(final Project project) { + private record DoctrineModelCached(@NotNull String phpClass, @Nullable String doctrineShortcut, @Nullable String doctrineNamespace) { + } + private static Collection getModelClassesInner(final Project project) { HashMap shortcutNames = new HashMap<>() {{ putAll(ServiceXmlParserFactory.getInstance(project, EntityNamesServiceParser.class).getEntityNameMap()); putAll(ServiceXmlParserFactory.getInstance(project, DocumentNamespacesParser.class).getNamespaceMap()); @@ -715,7 +717,7 @@ public static Collection getModelClasses(final Project project) { // class fqn fallback Collection doctrineModels = getModelClasses(project, shortcutNames); for (PhpClass phpClass : DoctrineMetadataUtil.getModels(project)) { - if(containsDoctrineModelClass(doctrineModels, phpClass)) { + if (containsDoctrineModelClass(doctrineModels, phpClass)) { continue; } @@ -729,6 +731,40 @@ public static Collection getModelClasses(final Project project) { } } + return doctrineModels.stream().map( + doctrineModel -> new DoctrineModelCached(doctrineModel.getPhpClass().getFQN(), doctrineModel.getDoctrineShortcut(), doctrineModel.getDoctrineNamespace()) + ).toList(); + } + + /** + * One PhpClass can have multiple targets and names @TODO: refactor + */ + public static Collection getModelClasses(@NotNull final Project project) { + Collection modelClasses = CachedValuesManager.getManager(project).getCachedValue( + project, + SYMFONY_DOCTRINE_MODEL_CACHE, + () -> CachedValueProvider.Result.create(getModelClassesInner(project), PsiModificationTracker.MODIFICATION_COUNT), + false + ); + + PhpIndex phpIndex = PhpIndex.getInstance(project); + Collection doctrineModels = new ArrayList<>(); + for (DoctrineModelCached doctrineModelCached : modelClasses) { + Collection classesByFQN = phpIndex.getClassesByFQN(doctrineModelCached.phpClass); + if (classesByFQN.isEmpty()) { + continue; + } + + doctrineModels.add(new DoctrineModel(classesByFQN.iterator().next(), doctrineModelCached.doctrineShortcut, doctrineModelCached.doctrineNamespace)); + } + + DoctrineModelProviderParameter containerLoaderExtensionParameter = new DoctrineModelProviderParameter(project, new ArrayList<>()); + for (DoctrineModelProvider provider : EntityHelper.MODEL_POINT_NAME.getExtensions()) { + for (DoctrineModelProviderParameter.DoctrineModel doctrineModel: provider.collectModels(containerLoaderExtensionParameter)) { + doctrineModels.add(new DoctrineModel(doctrineModel.getPhpClass(), doctrineModel.getName())); + } + } + return doctrineModels; } @@ -742,18 +778,17 @@ private static boolean containsDoctrineModelClass(@NotNull Collection getModelClasses(Project project, Map shortcutNames) { - + public static Collection getModelClasses(@NotNull Project project, @NotNull Map shortcutNames) { PhpClass repositoryInterface = PhpElementsUtil.getInterface(PhpIndex.getInstance(project), DoctrineTypes.REPOSITORY_INTERFACE); - if(repositoryInterface == null) { + if (repositoryInterface == null) { repositoryInterface = PhpElementsUtil.getInterface(PhpIndex.getInstance(project), "\\Doctrine\\Persistence\\ObjectRepository"); } Collection models = new ArrayList<>(); for (Map.Entry entry : shortcutNames.entrySet()) { - for(PhpClass phpClass: PhpIndexUtil.getPhpClassInsideNamespace(project, entry.getValue())) { - if(repositoryInterface != null && !isEntity(phpClass, repositoryInterface)) { + for (PhpClass phpClass: PhpIndexUtil.getPhpClassInsideNamespace(project, entry.getValue(), true)) { + if (repositoryInterface != null && !isEntity(phpClass, repositoryInterface)) { continue; } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpIndexUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpIndexUtil.java index b3c447960..a20fe6e07 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpIndexUtil.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/PhpIndexUtil.java @@ -2,18 +2,12 @@ import com.intellij.codeInsight.completion.PrefixMatcher; import com.intellij.openapi.project.Project; -import com.intellij.util.Processor; import com.jetbrains.php.PhpIndex; import com.jetbrains.php.lang.psi.elements.PhpClass; import com.jetbrains.php.util.PhpContractUtil; -import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.*; /** * @author Daniel Espendiller @@ -29,7 +23,19 @@ public class PhpIndexUtil { */ @NotNull public static Collection getPhpClassInsideNamespace(@NotNull Project project, @NotNull String namespaceName) { - return getPhpClassInsideNamespace(PhpIndex.getInstance(project), namespaceName, 10); + return getPhpClassInsideNamespace(PhpIndex.getInstance(project), namespaceName, false); + } + + /** + * Collect PhpClass which are inside current namespace and in sub-namespaces + * + * @param project current project + * @param namespaceName namespace name should start with \ and end with "\" + * @return classes inside namespace and sub-namespace + */ + @NotNull + public static Collection getPhpClassInsideNamespace(@NotNull Project project, @NotNull String namespaceName, boolean excludeInterfaces) { + return getPhpClassInsideNamespace(PhpIndex.getInstance(project), namespaceName, excludeInterfaces); } public static Collection getAllSubclasses(@NotNull Project project, @NotNull String clazz) { @@ -45,20 +51,16 @@ public static Collection getAllSubclasses(@NotNull Project project, @N @NotNull - private static Collection getPhpClassInsideNamespace(@NotNull PhpIndex phpIndex, @NotNull String namespaceName, int maxDeep) { + private static Collection getPhpClassInsideNamespace(@NotNull PhpIndex phpIndex, @NotNull String namespaceName, boolean excludeInterfaces) { PhpContractUtil.assertFqn(namespaceName); - Collection classes = new HashSet<>() {{ - addAll(phpIndex.getAllClassFqns(PrefixMatcher.ALWAYS_TRUE)); - addAll(phpIndex.getAllInterfacesFqns(PrefixMatcher.ALWAYS_TRUE)); - }}; - - Set stringStream = classes.stream() - .filter(s -> s.toLowerCase().startsWith(StringUtils.stripEnd(namespaceName.toLowerCase(), "\\") + "\\")) - .collect(Collectors.toSet()); + Collection classes = new HashSet<>(phpIndex.getAllClassFqns(new MyPrefixMatcher(namespaceName))); + if (!excludeInterfaces) { + classes.addAll(phpIndex.getAllInterfacesFqns(new MyPrefixMatcher(namespaceName))); + } Collection clazzes = new HashSet<>(); - for (String s : stringStream) { + for (String s : classes) { clazzes.addAll(phpIndex.getAnyByFQN(s)); } @@ -73,4 +75,23 @@ public static boolean hasNamespace(@NotNull Project project, @NotNull String nam return !PhpIndex.getInstance(project).getChildNamespacesByParentName(namespaceName + "\\").isEmpty(); } + + private static class MyPrefixMatcher extends PrefixMatcher { + private final String namespaceName; + + public MyPrefixMatcher(@NotNull String namespaceName) { + super(namespaceName); + this.namespaceName = namespaceName; + } + + @Override + public boolean prefixMatches(@NotNull String name) { + return name.startsWith(namespaceName); + } + + @Override + public @NotNull PrefixMatcher cloneWithPrefix(@NotNull String prefix) { + return new MyPrefixMatcher(prefix); + } + } } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/dict/DoctrineModel.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/dict/DoctrineModel.java index 0623413bf..33fed3181 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/dict/DoctrineModel.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/util/dict/DoctrineModel.java @@ -27,6 +27,11 @@ public DoctrineModel(@NotNull PhpClass phpClass, @Nullable String doctrineShortc this.doctrineNamespace = doctrineNamespace; } + @Nullable + public String getDoctrineShortcut() { + return doctrineShortcut; + } + @NotNull public PhpClass getPhpClass() { return phpClass; diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/util/PhpIndexUtilTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/util/PhpIndexUtilTest.java index 1d32eb9c7..cfe6aae36 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/util/PhpIndexUtilTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/util/PhpIndexUtilTest.java @@ -3,7 +3,6 @@ import com.jetbrains.php.lang.psi.elements.PhpNamedElement; import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase; import fr.adrienbrault.idea.symfony2plugin.util.PhpIndexUtil; -import org.apache.commons.lang3.StringUtils; import java.util.List; import java.util.stream.Collectors; @@ -26,22 +25,32 @@ public void testGetPhpClassInsideNamespace() { List foobar = PhpIndexUtil.getPhpClassInsideNamespace(getProject(), "\\Foobar").stream() .map(PhpNamedElement::getFQN) - .sorted() .collect(Collectors.toList()); - assertEquals( - "\\Foobar\\Class1,\\Foobar\\Class2,\\Foobar\\Foobar2\\Foobar3\\Class1,\\Foobar\\Foobar2\\Foobar3\\Class2,\\Foobar\\Foobar2\\Foobar3\\Interface1,\\Foobar\\Foobar2\\Foobar3\\Interface2,\\Foobar\\Foobar2\\FoobarNot\\Class1,\\Foobar\\Foobar2\\FoobarNot\\Class2,\\Foobar\\Foobar2\\Foobar\\Foobar4\\Class1,\\Foobar\\Foobar2\\Foobar\\Foobar4\\Class2,\\Foobar\\Interface1,\\Foobar\\Interface2", - StringUtils.join(foobar, ",") + assertContainsElements( + foobar, + "\\Foobar\\Class1", + "\\Foobar\\Class2", + "\\Foobar\\Foobar2\\Foobar3\\Class1", + "\\Foobar\\Foobar2\\Foobar3\\Class2", + "\\Foobar\\Foobar2\\Foobar3\\Interface1", + "\\Foobar\\Foobar2\\Foobar3\\Interface2", + "\\Foobar\\Foobar2\\FoobarNot\\Class1", + "\\Foobar\\Foobar2\\FoobarNot\\Class2", + "\\Foobar\\Foobar2\\Foobar\\Foobar4\\Class1", + "\\Foobar\\Foobar2\\Foobar\\Foobar4\\Class2", + "\\Foobar\\Interface1", + "\\Foobar\\Interface2" ); List foobar2 = PhpIndexUtil.getPhpClassInsideNamespace(getProject(), "\\Foobar\\Foobar2\\Foobar\\Foobar4\\").stream() .map(PhpNamedElement::getFQN) - .sorted() .collect(Collectors.toList()); - assertEquals( - "\\Foobar\\Foobar2\\Foobar\\Foobar4\\Class1,\\Foobar\\Foobar2\\Foobar\\Foobar4\\Class2", - StringUtils.join(foobar2, ",") + assertContainsElements( + foobar2, + "\\Foobar\\Foobar2\\Foobar\\Foobar4\\Class1", + "\\Foobar\\Foobar2\\Foobar\\Foobar4\\Class2" ); }