diff --git a/plugins/devkit/src/dom/ExtensionOrderConverter.java b/plugins/devkit/src/dom/ExtensionOrderConverter.java index 55cf0e61f5ca..a752e4659cfe 100644 --- a/plugins/devkit/src/dom/ExtensionOrderConverter.java +++ b/plugins/devkit/src/dom/ExtensionOrderConverter.java @@ -6,18 +6,24 @@ import com.intellij.codeInsight.daemon.EmptyResolveMessageProvider; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.icons.AllIcons; +import com.intellij.lang.LanguageExtensionPoint; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.LoadingOrder; +import com.intellij.openapi.fileTypes.FileTypeExtensionPoint; import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.pom.references.PomService; import com.intellij.psi.*; +import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.ReferenceSetBase; import com.intellij.psi.xml.XmlTag; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.containers.HashMap; import com.intellij.util.xml.*; +import com.intellij.util.xml.reflect.DomAttributeChildDescription; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.devkit.DevKitBundle; @@ -27,7 +33,7 @@ import org.jetbrains.idea.devkit.util.ExtensionLocator; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; +import java.util.Map; public class ExtensionOrderConverter implements CustomReferenceConverter { private static final Logger LOG = Logger.getInstance(ExtensionOrderConverter.class); @@ -206,7 +212,8 @@ public class ExtensionOrderConverter implements CustomReferenceConverter ExtensionLocator epLocator = ExtensionLocator.byExtensionPoint(extensionPoint); List candidates = epLocator.findCandidates(); - DomManager domManager = DomManager.getDomManager(getElement().getProject()); + Project project = getElement().getProject(); + DomManager domManager = DomManager.getDomManager(project); List extensionsForThisEp = new ArrayList<>(); for (ExtensionCandidate candidate : candidates) { @@ -217,34 +224,9 @@ public class ExtensionOrderConverter implements CustomReferenceConverter } } - String currentExtensionId = myExtension.getId().getStringValue(); - List idCompletionVariantsList = new ArrayList<>(); - for (Extension e : extensionsForThisEp) { - if (Objects.equals(currentExtensionId, e.getId().getStringValue())) { - continue; // do not suggest the same extension id - } - String id = e.getId().getStringValue(); - if (StringUtil.isNotEmpty(id)) { - DomTarget extensionTarget = DomTarget.getTarget(e); - PsiElement targetElement; - if (extensionTarget != null) { - targetElement = PomService.convertToPsi(extensionTarget); - } - else { - // shouldn't happen, fallback for additional safety - targetElement = e.getXmlTag(); - } - - LookupElementBuilder element = LookupElementBuilder.create(targetElement, id); - Module module = e.getModule(); - if (module != null) { - element = element.withTypeText(module.getName(), AllIcons.Actions.Module, false); - } - - idCompletionVariantsList.add(element); - } - } - return idCompletionVariantsList.toArray(new LookupElement[idCompletionVariantsList.size()]); + Map targetExtensionsWithMarks = filterAndMarkExtensions(extensionsForThisEp, project); + List idCompletionVariants = getLookupElements(targetExtensionsWithMarks); + return idCompletionVariants.toArray(new LookupElement[idCompletionVariants.size()]); } @NotNull @@ -258,5 +240,125 @@ public class ExtensionOrderConverter implements CustomReferenceConverter public boolean isSoft() { return true; } + + private Map filterAndMarkExtensions(List extensionsForThisEp, Project project) { + JavaPsiFacade javaPsiFacade = JavaPsiFacade.getInstance(project); + GlobalSearchScope resolveScope = getElement().getResolveScope(); + PsiClass languageEpClass = javaPsiFacade.findClass(LanguageExtensionPoint.class.getCanonicalName(), resolveScope); + if (languageEpClass == null) { + LOG.error("Cannot find LanguageExtensionPoint class; extension: " + myExtension); + return Collections.emptyMap(); + } + PsiClass fileTypeEpClass = javaPsiFacade.findClass(FileTypeExtensionPoint.class.getCanonicalName(), resolveScope); + if (fileTypeEpClass == null) { + LOG.error("Cannot find FileTypeExtensionPoint class; extension: " + myExtension); + return Collections.emptyMap(); + } + + String currentExtensionId = myExtension.getId().getStringValue(); + String currentExtensionLanguage = getSpecificExtensionAttribute(myExtension, languageEpClass, "language"); + String currentExtensionFileType = getSpecificExtensionAttribute(myExtension, fileTypeEpClass, "filetype"); + + Map result = new HashMap<>(); + for (Extension extension : extensionsForThisEp) { + String id = extension.getId().getStringValue(); + if (StringUtil.isEmpty(id) || id.equals(currentExtensionId)) { + continue; + } + + String extensionMark = null; // to display {language} or {file type} + if (currentExtensionLanguage != null) { + String language = getSpecificExtensionAttribute(extension, languageEpClass, "language"); + if (language != null) { + if (!language.equalsIgnoreCase("ANY") && !language.equalsIgnoreCase(currentExtensionLanguage)) { + continue; + } + extensionMark = language; + } + } + if (currentExtensionFileType != null) { + String fileType = getSpecificExtensionAttribute(extension, fileTypeEpClass, "filetype"); + if (fileType != null) { + if (!currentExtensionFileType.equalsIgnoreCase(fileType)) { + continue; + } + extensionMark = extensionMark != null ? null : fileType; // null if _somehow_ both filetype and language are present + } + } + + result.put(extension, extensionMark); + } + + return result; + } + + @NotNull + private static List getLookupElements(@NotNull Map targetExtensionsWithMarks) { + List result = new ArrayList<>(targetExtensionsWithMarks.size()); + for (Map.Entry entry : targetExtensionsWithMarks.entrySet()) { + Extension extension = entry.getKey(); + String mark = entry.getValue(); + + PsiElement targetElement = getTargetElement(extension); + String id = extension.getId().getStringValue(); + if (StringUtil.isEmpty(id)) { + LOG.error("Unexpected empty id in target extension: " + extension); + continue; + } + + result.add(createLookupElement(targetElement, id, extension.getModule(), mark)); + } + return result; + } + + @Nullable + private static String getSpecificExtensionAttribute(@NotNull Extension e, + @NotNull PsiClass parentBeanClass, + @NotNull String attribute) { + ExtensionPoint ep = e.getExtensionPoint(); + if (ep == null) { + return null; + } + PsiClass beanClass = ep.getBeanClass().getValue(); + if (beanClass == null) { + return null; + } + if (!beanClass.equals(parentBeanClass) && !beanClass.isInheritor(parentBeanClass, true)) { + return null; + } + + DomAttributeChildDescription attributeDescription = e.getGenericInfo().getAttributeChildDescription(attribute); + if (attributeDescription == null) { + return null; + } + return attributeDescription.getDomAttributeValue(e).getStringValue(); + } + + @NotNull + private static PsiElement getTargetElement(Extension e) { + DomTarget extensionTarget = DomTarget.getTarget(e); + if (extensionTarget != null) { + return PomService.convertToPsi(extensionTarget); + } + else { + // shouldn't happen, fallback for additional safety + return e.getXmlTag(); + } + } + + @NotNull + private static LookupElement createLookupElement(@NotNull PsiElement targetElement, + @NotNull String id, + @Nullable Module module, + @Nullable String mark) { + LookupElementBuilder element = LookupElementBuilder.create(targetElement, id); + if (module != null) { + element = element.withTypeText(module.getName(), AllIcons.Actions.Module, false); + } + if (mark != null) { + element = element.withTailText(" {" + mark + "}", true); + } + return element; + } } } diff --git a/plugins/devkit/testData/codeInsight/orderAttributeCompletionFileType.xml b/plugins/devkit/testData/codeInsight/orderAttributeCompletionFileType.xml new file mode 100644 index 000000000000..489fd2177892 --- /dev/null +++ b/plugins/devkit/testData/codeInsight/orderAttributeCompletionFileType.xml @@ -0,0 +1,17 @@ + + com.intellij + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/testData/codeInsight/orderAttributeCompletionLanguage.xml b/plugins/devkit/testData/codeInsight/orderAttributeCompletionLanguage.xml new file mode 100644 index 000000000000..1a65c259d117 --- /dev/null +++ b/plugins/devkit/testData/codeInsight/orderAttributeCompletionLanguage.xml @@ -0,0 +1,18 @@ + + com.intellij + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy b/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy index 3cb9155d2f4b..b7ded6d079c0 100644 --- a/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy +++ b/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy @@ -16,11 +16,13 @@ package org.jetbrains.idea.devkit.codeInsight import com.intellij.codeInsight.TargetElementUtil +import com.intellij.codeInsight.completion.CompletionContributorEP import com.intellij.codeInsight.completion.CompletionType import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementPresentation import com.intellij.codeInspection.xml.DeprecatedClassUsageInspection import com.intellij.diagnostic.ITNReporter +import com.intellij.lang.LanguageExtensionPoint import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.PluginPathManager import com.intellij.openapi.extensions.LoadingOrder @@ -66,6 +68,10 @@ class PluginXmlFunctionalTest extends JavaCodeInsightFixtureTestCase { moduleBuilder.addLibrary("platform-api", platformApiJar) String platformImplJar = PathUtil.getJarPathForClass(ITNReporter.class) moduleBuilder.addLibrary("platform-impl", platformImplJar) + String langApiJar = PathUtil.getJarPathForClass(CompletionContributorEP.class) + moduleBuilder.addLibrary("lang-api", langApiJar) + String coreApiJar = PathUtil.getJarPathForClass(LanguageExtensionPoint.class) // FileTypeExtensionPoint is also there + moduleBuilder.addLibrary("core-api", coreApiJar) } void testExtensionsHighlighting() { @@ -208,6 +214,7 @@ class PluginXmlFunctionalTest extends JavaCodeInsightFixtureTestCase { myFixture.checkResultByFile(getTestName(false) + "_after.xml") } + @SuppressWarnings("ComponentNotRegistered") void testShowPackagesInActionClass() { myFixture.addClass("package com.intellij.openapi.actionSystem; public class AnAction { }") myFixture.addClass("package foo.bar; public class BarAction extends com.intellij.openapi.actionSystem.AnAction { }") @@ -218,6 +225,7 @@ class PluginXmlFunctionalTest extends JavaCodeInsightFixtureTestCase { assert myFixture.lookup.advertisements.find { it.contains('to see inheritors of com.intellij.openapi.actionSystem.AnAction') } } + @SuppressWarnings("ComponentNotRegistered") void testShowAnActionInheritorsOnSmartCompletion() { myFixture.addClass("package com.intellij.openapi.actionSystem; public class AnAction { }") myFixture.addClass("package foo.bar; public class BarAction extends com.intellij.openapi.actionSystem.AnAction { }") @@ -281,6 +289,7 @@ class PluginXmlFunctionalTest extends JavaCodeInsightFixtureTestCase { myFixture.allowTreeAccessForFile(myFixture.copyFileToProject("MyLanguage.java")) } + @SuppressWarnings("ComponentNotRegistered") void testIconAttribute() { myFixture.addClass("package com.intellij.openapi.actionSystem; public class AnAction { }") myFixture.addClass("package foo; public class FooAction extends com.intellij.openapi.actionSystem.AnAction { }") @@ -396,6 +405,15 @@ class PluginXmlFunctionalTest extends JavaCodeInsightFixtureTestCase { myFixture.testCompletion(getTestName(true) + ".xml", getTestName(true) + "_after.xml") } + void testOrderAttributeCompletionLanguage() { + myFixture.testCompletionVariants(getTestName(true) + ".xml", "id1", "id2", "id3") + } + + void testOrderAttributeCompletionFileType() { + myFixture.testCompletionVariants(getTestName(true) + ".xml", "id1", "id2") + } + + private void testHighlightingInIdeaProject(String path) { myFixture.enableInspections(PluginXmlDomInspection.class) PsiUtil.markAsIdeaProject(project, true) @@ -440,6 +458,7 @@ public class MyErrorHandler extends ErrorReportSubmitter {} myFixture.checkResultByFile(getTestName(true) + "_after.xml") } + @SuppressWarnings("ComponentNotRegistered") void testActionHighlighting() { configureByFile() myFixture.addClass("package com.intellij.openapi.actionSystem; public class AnAction { }")