diff --git a/platform/extensions/src/com/intellij/openapi/extensions/LoadingOrder.java b/platform/extensions/src/com/intellij/openapi/extensions/LoadingOrder.java index e25efeb602ed..200ce4f92346 100644 --- a/platform/extensions/src/com/intellij/openapi/extensions/LoadingOrder.java +++ b/platform/extensions/src/com/intellij/openapi/extensions/LoadingOrder.java @@ -43,12 +43,14 @@ import java.util.*; * @author Alexander Kireyev */ public class LoadingOrder { - @NonNls private static final String FIRST_STR = "FIRST"; - @NonNls private static final String LAST_STR = "LAST"; - @NonNls private static final String BEFORE_STR = "BEFORE "; - @NonNls private static final String BEFORE_STR_OLD = "BEFORE:"; - @NonNls private static final String AFTER_STR = "AFTER "; - @NonNls private static final String AFTER_STR_OLD = "AFTER:"; + @NonNls public static final String FIRST_STR = "first"; + @NonNls public static final String LAST_STR = "last"; + @NonNls public static final String BEFORE_STR = "before "; + @NonNls public static final String BEFORE_STR_OLD = "before:"; + @NonNls public static final String AFTER_STR = "after "; + @NonNls public static final String AFTER_STR_OLD = "after:"; + + @NonNls public static final String ORDER_RULE_SEPARATOR = ","; public static final LoadingOrder ANY = new LoadingOrder(); public static final LoadingOrder FIRST = new LoadingOrder(FIRST_STR); @@ -70,7 +72,7 @@ public class LoadingOrder { myName = text; boolean last = false; boolean first = false; - for (final String string : StringUtil.split(text, ",")) { + for (final String string : StringUtil.split(text, ORDER_RULE_SEPARATOR)) { String trimmed = string.trim(); if (trimmed.equalsIgnoreCase(FIRST_STR)) first = true; else if (trimmed.equalsIgnoreCase(LAST_STR)) last = true; diff --git a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java index 66f755759416..9f7835b25bdd 100644 --- a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java +++ b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java @@ -1389,11 +1389,23 @@ public class StringUtil extends StringUtilRt { @NotNull @Contract(pure = true) public static List getWordIndicesIn(@NotNull String text) { + return getWordIndicesIn(text, null); + } + + /** + * @param text text to get word ranges in. + * @param separatorsSet if not null, only these characters will be considered as separators (i.e. not a part of word). + * Otherwise {@link Character#isJavaIdentifierPart(char)} will be used to determine whether a symbol is part of word. + * @return ranges ranges of words in passed text. + */ + @NotNull + @Contract(pure = true) + public static List getWordIndicesIn(@NotNull String text, @Nullable Set separatorsSet) { List result = new SmartList(); int start = -1; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); - boolean isIdentifierPart = Character.isJavaIdentifierPart(c); + boolean isIdentifierPart = separatorsSet == null ? Character.isJavaIdentifierPart(c) : !separatorsSet.contains(c); if (isIdentifierPart && start == -1) { start = i; } diff --git a/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java b/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java index 35ed3fe3a68a..23c518295e26 100644 --- a/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java +++ b/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java @@ -2,6 +2,7 @@ package com.intellij.util.text; import com.intellij.openapi.util.Comparing; +import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.NaturalComparator; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.LineSeparator; @@ -616,4 +617,16 @@ public class StringUtilTest { assertNull(StringUtil.substringAfterLast("abc", "1")); assertNull(StringUtil.substringAfterLast("", "1")); } + + @Test + public void testGetWordIndicesIn() { + assertEquals(ContainerUtil.list(new TextRange(0, 5), new TextRange(6, 12)), StringUtil.getWordIndicesIn("first second")); + assertEquals(ContainerUtil.list(new TextRange(1, 6), new TextRange(7, 13)), StringUtil.getWordIndicesIn(" first second")); + assertEquals(ContainerUtil.list(new TextRange(1, 6), new TextRange(7, 13)), StringUtil.getWordIndicesIn(" first second ")); + assertEquals(ContainerUtil.list(new TextRange(0, 5), new TextRange(6, 12)), StringUtil.getWordIndicesIn("first:second")); + assertEquals(ContainerUtil.list(new TextRange(0, 5), new TextRange(6, 12)), StringUtil.getWordIndicesIn("first-second")); + assertEquals(ContainerUtil.list(new TextRange(0, 12)), StringUtil.getWordIndicesIn("first-second", ContainerUtil.set(' ', '_', '.'))); + assertEquals(ContainerUtil.list(new TextRange(0, 5), new TextRange(6, 12)), + StringUtil.getWordIndicesIn("first-second", ContainerUtil.set('-'))); + } } diff --git a/plugins/devkit/resources/META-INF/plugin.xml b/plugins/devkit/resources/META-INF/plugin.xml index 41c8c17c9254..042146018ca5 100644 --- a/plugins/devkit/resources/META-INF/plugin.xml +++ b/plugins/devkit/resources/META-INF/plugin.xml @@ -62,6 +62,8 @@ + + @@ -174,7 +176,7 @@ - + diff --git a/plugins/devkit/resources/org/jetbrains/idea/devkit/DevKitBundle.properties b/plugins/devkit/resources/org/jetbrains/idea/devkit/DevKitBundle.properties index a28e106ac24a..e7e8e8207f80 100644 --- a/plugins/devkit/resources/org/jetbrains/idea/devkit/DevKitBundle.properties +++ b/plugins/devkit/resources/org/jetbrains/idea/devkit/DevKitBundle.properties @@ -172,6 +172,8 @@ inspections.component.not.registered.quickfix.error=Cannot Register {0} inspections.component.postfix.template.not.found.description.name=Postfix template Description Checker inspections.presentation.cannot.resolve.icon=Cannot resolve icon ''{0}'' +inspections.plugin.xml.invalid.order.attribute=Invalid 'order' attribute value +invalid.order.attribute.part=Invalid ''order'' attribute value part: ''{0}'', must be ''first'', ''last'', ''before '' or ''after '' ant.build.jar.comment=Build archive for plugin ''{0}'' ant.build.jar.description=Build plugin archive for module ''{0}'' diff --git a/plugins/devkit/src/completion/ExtensionOrderKeywordCompletionContributor.java b/plugins/devkit/src/completion/ExtensionOrderKeywordCompletionContributor.java new file mode 100644 index 000000000000..3ad333277d21 --- /dev/null +++ b/plugins/devkit/src/completion/ExtensionOrderKeywordCompletionContributor.java @@ -0,0 +1,97 @@ +// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.idea.devkit.completion; + +import com.intellij.codeInsight.completion.*; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.extensions.LoadingOrder; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.patterns.*; +import com.intellij.psi.PsiElement; +import com.intellij.psi.xml.XmlElement; +import com.intellij.psi.xml.XmlTag; +import com.intellij.util.ProcessingContext; +import com.intellij.util.containers.ContainerUtil; +import com.intellij.util.xml.DomManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.idea.devkit.dom.Extension; + +import java.util.List; + +import static com.intellij.patterns.PlatformPatterns.psiElement; + +public class ExtensionOrderKeywordCompletionContributor extends CompletionContributor { + private static final List KEYWORD_COMPLETION_VARIANTS = ContainerUtil.list( + LookupElementBuilder.create(LoadingOrder.FIRST_STR), LookupElementBuilder.create(LoadingOrder.LAST_STR), + LookupElementBuilder.create(LoadingOrder.BEFORE_STR.trim()).withInsertHandler(new AddSpaceInsertHandler(true)), + LookupElementBuilder.create(LoadingOrder.AFTER_STR.trim()).withInsertHandler(new AddSpaceInsertHandler(true)) + ); + + public ExtensionOrderKeywordCompletionContributor() { + extend(CompletionType.BASIC, getCapture(), new CompletionProvider() { + @Override + protected void addCompletions(@NotNull CompletionParameters parameters, + ProcessingContext context, + @NotNull CompletionResultSet result) { + String prefix = getCompletionPrefix(parameters); + if (shouldProposeKeywordsAfterPrefix(prefix)) { + result.addAllElements(KEYWORD_COMPLETION_VARIANTS); + } + } + }); + } + + @NotNull + private static PsiElementPattern.Capture getCapture() { + //TODO write a method for attribute value in XmlPatterns + return psiElement().inside( + XmlPatterns.xmlAttributeValue("order").inside( + XmlPatterns.xmlTag().with(new PatternCondition("extension tag") { + @Override + public boolean accepts(@NotNull XmlTag tag, ProcessingContext context) { + Project project = tag.getProject(); + DomManager domManager = DomManager.getDomManager(project); + return domManager.getDomElement(tag) instanceof Extension; + } + }))); + } + + @NotNull + private static String getCompletionPrefix(@NotNull CompletionParameters parameters) { + XmlElement position = (XmlElement)parameters.getPosition(); + int startOffset = position.getTextOffset(); + int endOffset = parameters.getOffset(); + Document document = parameters.getEditor().getDocument(); + return document.getText(new TextRange(startOffset, endOffset)); + } + + @NotNull + private static String getPrefixLastPart(String prefix) { + String lastPart = StringUtil.substringAfterLast(prefix, LoadingOrder.ORDER_RULE_SEPARATOR); + if (lastPart == null) { + lastPart = prefix; + } + lastPart = StringUtil.trimLeading(lastPart); + return lastPart; + } + + private static boolean shouldProposeKeywordsAfterPrefix(String prefix) { + return !getPrefixLastPart(prefix).contains(" "); // propose keywords if there's only a single word (or empty prefix) + } + + @Override + public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) { + String prefix = result.getPrefixMatcher().getPrefix(); + if (prefix.endsWith(LoadingOrder.ORDER_RULE_SEPARATOR)) { + result = result.withPrefixMatcher(""); // keywords should be proposed after comma even without space + } + else { + result = result.withPrefixMatcher(getPrefixLastPart(prefix)); + } + + super.fillCompletionVariants(parameters, result); + } +} diff --git a/plugins/devkit/src/dom/Extension.java b/plugins/devkit/src/dom/Extension.java index e323308a1e0d..8f72c3123f6f 100644 --- a/plugins/devkit/src/dom/Extension.java +++ b/plugins/devkit/src/dom/Extension.java @@ -15,21 +15,18 @@ */ package org.jetbrains.idea.devkit.dom; -import com.intellij.util.xml.DomElement; -import com.intellij.util.xml.GenericAttributeValue; -import com.intellij.util.xml.NameValue; -import com.intellij.util.xml.Required; +import com.intellij.ide.presentation.Presentation; +import com.intellij.util.xml.*; import org.jetbrains.annotations.Nullable; -/** - * @author mike - */ +@Presentation(typeName = "Extension") public interface Extension extends DomElement { @NameValue @Required(value = false) GenericAttributeValue getId(); + @Referencing(ExtensionOrderConverter.class) @Required(value = false) GenericAttributeValue getOrder(); diff --git a/plugins/devkit/src/dom/ExtensionOrderConverter.java b/plugins/devkit/src/dom/ExtensionOrderConverter.java new file mode 100644 index 000000000000..cd4172a61f2b --- /dev/null +++ b/plugins/devkit/src/dom/ExtensionOrderConverter.java @@ -0,0 +1,255 @@ +// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.idea.devkit.dom; + +import com.intellij.codeInsight.completion.CompletionUtil; +import com.intellij.codeInsight.daemon.EmptyResolveMessageProvider; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.extensions.LoadingOrder; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.pom.references.PomService; +import com.intellij.psi.ElementManipulators; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiReferenceBase; +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.xml.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.idea.devkit.DevKitBundle; +import org.jetbrains.idea.devkit.util.ExtensionCandidate; +import org.jetbrains.idea.devkit.util.ExtensionLocator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class ExtensionOrderConverter implements CustomReferenceConverter { + private static final Logger LOG = Logger.getInstance(ExtensionOrderConverter.class); + + @NotNull + @Override + public PsiReference[] createReferences(GenericDomValue value, PsiElement element, ConvertContext context) { + PsiElement originalElement = CompletionUtil.getOriginalOrSelf(element); // avoid 'IntellijIdeaRulezzz' placeholder + String orderValue = ElementManipulators.getValueText(originalElement); + if (StringUtil.isEmpty(orderValue)) { + return PsiReference.EMPTY_ARRAY; + } + Extension extension = value.getParentOfType(Extension.class, false); + if (extension == null) { + return PsiReference.EMPTY_ARRAY; + } + + return new ReferenceSetBase(orderValue, element, 0, ',') { + @Override + protected List createReferences(TextRange range, int index) { + String orderPart = range.substring(orderValue); + + // reference range and attribute text range are not equal + range = new TextRange(range.getStartOffset() + 1, range.getEndOffset() + 1); + + List subParts = splitOrderPart(orderPart); + if (subParts.isEmpty()) { + // last symbol is ',' + return Collections.emptyList(); + } + + String idSubPart = null; // second one, after keyword subpart + if (subParts.size() == 2) { + idSubPart = subParts.get(1); + } + else if (isBeforeOrAfterKeyword(StringUtil.trimLeading(orderPart), false)) { + // This order part is, for instance, 'after ' or 'before:'. In such cases IDs reference should be provided. + idSubPart = ""; + } + + if (subParts.size() > 2 || (idSubPart != null && !isBeforeOrAfterKeyword(subParts.get(0)))) { + // Order value can't contain more than 3 subparts. If there are two subparts, first one must be 'before' or 'after'. + return Collections.singletonList(new InvalidOrderPartPsiReference(getElement(), range, orderPart)); + } + + List wordIndices = getWordIndicesInOrderPart(orderPart); + if (wordIndices.isEmpty()) { + LOG.error("Unexpected empty word indices list for 'order' part: " + orderPart); + return Collections.singletonList(new InvalidOrderPartPsiReference(getElement(), range, orderPart)); + } + if (idSubPart != null && idSubPart.isEmpty()) { // right after the before/after keyword + wordIndices.add(new TextRange(orderPart.length(), orderPart.length())); + } + + if (idSubPart == null) { + return Collections.emptyList(); + } + assert wordIndices.size() == 2 : wordIndices.toString(); + TextRange idSubPartRange = wordIndices.get(1).shiftRight(range.getStartOffset()); + return ContainerUtil.list(new OrderReferencedIdPsiReference(getElement(), idSubPartRange, idSubPart, extension)); + } + }.getPsiReferences(); + } + + private static List splitOrderPart(String orderPart) { + List result = new ArrayList<>(); + List subParts = StringUtil.split(orderPart, " "); + subParts.forEach(s -> { + if (":".equals(s)) { + result.add(s); + } + else { + Collections.addAll(result, s.split(":")); + } + }); + return result; + } + + private static List getWordIndicesInOrderPart(String orderPart) { + return StringUtil.getWordIndicesIn(orderPart, ContainerUtil.set(' ', ':')); + } + + private static boolean isBeforeOrAfterKeyword(String str) { + return isBeforeOrAfterKeyword(str, true); + } + + private static boolean isBeforeOrAfterKeyword(String str, boolean trimKeyword) { + return (trimKeyword ? LoadingOrder.BEFORE_STR.trim() : LoadingOrder.BEFORE_STR).equalsIgnoreCase(str) || + (trimKeyword ? LoadingOrder.AFTER_STR.trim(): LoadingOrder.AFTER_STR).equalsIgnoreCase(str) || + LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase(str) || + LoadingOrder.BEFORE_STR_OLD.equalsIgnoreCase(str); + } + + + private static class InvalidOrderPartPsiReference extends PsiReferenceBase implements EmptyResolveMessageProvider { + private final String myOrderPart; + + public InvalidOrderPartPsiReference(@NotNull PsiElement element, @NotNull TextRange rangeInElement, String orderPart) { + super(element, rangeInElement); + myOrderPart = orderPart; + } + + @Nullable + @Override + public PsiElement resolve() { + return null; + } + + @NotNull + @Override + public Object[] getVariants() { + return ArrayUtil.EMPTY_OBJECT_ARRAY; + } + + @NotNull + @Override + public String getUnresolvedMessagePattern() { + return DevKitBundle.message("invalid.order.attribute.part", myOrderPart.trim()); + } + + @Override + public boolean isSoft() { + return true; + } + } + + private static class OrderReferencedIdPsiReference extends PsiReferenceBase implements EmptyResolveMessageProvider { + private final String myReferencedId; + private final Extension myExtension; + + public OrderReferencedIdPsiReference(@NotNull PsiElement element, @NotNull TextRange rangeInElement, + @NotNull String referencedId, @NotNull Extension extension) { + super(element, rangeInElement); + myReferencedId = referencedId; + myExtension = extension; + } + + @Nullable + @Override + public PsiElement resolve() { + ExtensionPoint extensionPoint = myExtension.getExtensionPoint(); + if (extensionPoint == null) { + return null; + } + + ExtensionLocator epAndIdLocator = ExtensionLocator.byExtensionPointAndId(extensionPoint, myReferencedId); + List candidates = epAndIdLocator.findCandidates(); + if (candidates.isEmpty()) { + return null; + } + XmlTag referencedElement = candidates.iterator().next().pointer.getElement(); + if (referencedElement == null) { + return null; + } + + // return DOM target PSI for "Find Usages" to work + DomManager domManager = DomManager.getDomManager(referencedElement.getProject()); + DomElement domElement = domManager.getDomElement(referencedElement); + if (domElement == null) { + return referencedElement; // fallback + } + DomTarget target = DomTarget.getTarget(domElement); + if (target == null) { + return referencedElement; // fallback + } + return PomService.convertToPsi(target); + } + + @NotNull + @Override + public Object[] getVariants() { + ExtensionPoint extensionPoint = myExtension.getExtensionPoint(); + if (extensionPoint == null) { + return ArrayUtil.EMPTY_OBJECT_ARRAY; + } + + ExtensionLocator epLocator = ExtensionLocator.byExtensionPoint(extensionPoint); + List candidates = epLocator.findCandidates(); + DomManager domManager = DomManager.getDomManager(getElement().getProject()); + + List extensionsForThisEp = new ArrayList<>(); + for (ExtensionCandidate candidate : candidates) { + XmlTag tag = candidate.pointer.getElement(); + DomElement domElement = domManager.getDomElement(tag); + if (domElement instanceof Extension) { + extensionsForThisEp.add((Extension)domElement); + } + } + + 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); + if (extensionTarget != null) { + PsiElement extensionPsi = PomService.convertToPsi(extensionTarget); + idCompletionVariantsList.add(LookupElementBuilder.create(extensionPsi, id)); + } + else { + // shouldn't happen, fallback for additional safety + idCompletionVariantsList.add(LookupElementBuilder.create(e.getXmlTag(), id)); + } + } + } + return idCompletionVariantsList.toArray(new LookupElement[idCompletionVariantsList.size()]); + } + + @NotNull + @Override + public String getUnresolvedMessagePattern() { + ExtensionPoint ep = myExtension.getExtensionPoint(); + return "Cannot resolve ''{0}'' " + (ep != null ? ep.getEffectiveName() + " " : "") + "extension"; + } + + @Override + public boolean isSoft() { + return true; + } + } +} diff --git a/plugins/devkit/src/inspections/PluginXmlDomInspection.java b/plugins/devkit/src/inspections/PluginXmlDomInspection.java index 258f3d46560c..0966530d87c8 100644 --- a/plugins/devkit/src/inspections/PluginXmlDomInspection.java +++ b/plugins/devkit/src/inspections/PluginXmlDomInspection.java @@ -23,7 +23,9 @@ import com.intellij.diagnostic.ITNReporter; import com.intellij.ide.plugins.IdeaPluginDescriptorImpl; import com.intellij.ide.plugins.PluginManagerCore; import com.intellij.ide.plugins.PluginManagerMain; +import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.extensions.LoadingOrder; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ModuleRootManager; @@ -269,6 +271,9 @@ public class PluginXmlDomInspection extends BasicDomElementsInspection targets = locator.findCandidates(); if (isQuickFix || !targets.isEmpty()) { boolean isProjectComponent = InheritanceUtil.isInheritor(psiClass, ProjectComponent.class.getCanonicalName()); diff --git a/plugins/devkit/src/navigation/ExtensionDeclarationRelatedItemLineMarkerProvider.java b/plugins/devkit/src/navigation/ExtensionDeclarationRelatedItemLineMarkerProvider.java index 9b0dec11efda..c7a108df0369 100644 --- a/plugins/devkit/src/navigation/ExtensionDeclarationRelatedItemLineMarkerProvider.java +++ b/plugins/devkit/src/navigation/ExtensionDeclarationRelatedItemLineMarkerProvider.java @@ -41,7 +41,7 @@ public class ExtensionDeclarationRelatedItemLineMarkerProvider extends DevkitRel return; } - ExtensionLocator locator = new ExtensionLocator(psiClass); + ExtensionLocator locator = ExtensionLocator.byPsiClass(psiClass); List targets = locator.findCandidates(); if (targets.isEmpty()) { return; diff --git a/plugins/devkit/src/references/IconsReferencesContributor.java b/plugins/devkit/src/references/IconsReferencesContributor.java index d3f2589c72aa..17bb24fe4c68 100644 --- a/plugins/devkit/src/references/IconsReferencesContributor.java +++ b/plugins/devkit/src/references/IconsReferencesContributor.java @@ -53,7 +53,6 @@ import com.intellij.util.QueryExecutor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.devkit.DevKitBundle; -import org.jetbrains.idea.devkit.util.DescriptorUtil; import org.jetbrains.idea.devkit.util.PsiUtil; import java.util.ArrayList; @@ -122,8 +121,7 @@ public class IconsReferencesContributor extends PsiReferenceContributor @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull final PsiElement element, @NotNull ProcessingContext context) { - if (!PsiUtil.isPluginProject(element.getProject()) || - !DescriptorUtil.isPluginXml(element.getContainingFile())) { + if (!PsiUtil.isPluginXmlPsiElement(element)) { return PsiReference.EMPTY_ARRAY; } diff --git a/plugins/devkit/src/util/DescriptorUtil.java b/plugins/devkit/src/util/DescriptorUtil.java index b915bfb4d64f..d609163828cd 100644 --- a/plugins/devkit/src/util/DescriptorUtil.java +++ b/plugins/devkit/src/util/DescriptorUtil.java @@ -88,7 +88,7 @@ public final class DescriptorUtil { return ideaPlugin.getRootElement().getPluginId(); } - public static boolean isPluginXml(PsiFile file) { + public static boolean isPluginXml(@Nullable PsiFile file) { if (!(file instanceof XmlFile)) return false; return getIdeaPlugin((XmlFile)file) != null; } diff --git a/plugins/devkit/src/util/ExtensionLocator.java b/plugins/devkit/src/util/ExtensionLocator.java index 358f08865d36..f7d9b188cd02 100644 --- a/plugins/devkit/src/util/ExtensionLocator.java +++ b/plugins/devkit/src/util/ExtensionLocator.java @@ -27,83 +27,135 @@ import com.intellij.psi.search.PsiSearchHelper; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlToken; -import com.intellij.util.CommonProcessors; -import com.intellij.util.Processor; import com.intellij.util.SmartList; import com.intellij.util.xml.DomElement; +import com.intellij.util.xml.DomManager; import com.intellij.util.xml.DomUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.devkit.dom.Extension; +import org.jetbrains.idea.devkit.dom.ExtensionPoint; import java.util.Collections; import java.util.List; -public class ExtensionLocator { - private final PsiClass myPsiClass; - - public ExtensionLocator(PsiClass aClass) { - myPsiClass = aClass; - } - +public abstract class ExtensionLocator { @NotNull - public List findCandidates() { - String name = myPsiClass.getQualifiedName(); - if (name == null) { - return Collections.emptyList(); - } + public abstract List findCandidates(); - List result = new SmartList<>(); - processExtensionDeclarations(myPsiClass, new ReferenceProcessor(name, tag -> { - result.add(new ExtensionCandidate(SmartPointerManager.getInstance(tag.getProject()).createSmartPsiElementPointer(tag))); - return true; // continue processing - })); - return result; + public static ExtensionLocator byPsiClass(PsiClass psiClass) { + return new ExtensionByPsiClassLocator(psiClass); } - public static boolean isRegisteredExtension(@NotNull PsiClass psiClass) { - String name = psiClass.getQualifiedName(); - if (name == null) { - return false; - } - - CommonProcessors.FindFirstProcessor processor = new CommonProcessors.FindFirstProcessor<>(); - processExtensionDeclarations(psiClass, new ReferenceProcessor(name, processor)); - return processor.isFound(); + public static ExtensionLocator byExtensionPoint(@NotNull ExtensionPoint extensionPoint) { + return new ExtensionByExtensionPointLocator(extensionPoint, null); } - private static void processExtensionDeclarations(PsiClass psiClass, PsiNonJavaFileReferenceProcessor referenceProcessor) { - String name = psiClass.getQualifiedName(); + public static ExtensionLocator byExtensionPointAndId(@NotNull ExtensionPoint extensionPoint, @NotNull String extensionId) { + return new ExtensionByExtensionPointLocator(extensionPoint, extensionId); + } + + + private static class ExtensionByPsiClassLocator extends ExtensionLocator { + private final PsiClass myPsiClass; + + ExtensionByPsiClassLocator(PsiClass psiClass) { + myPsiClass = psiClass; + } + + @NotNull + public List findCandidates() { + String name = myPsiClass.getQualifiedName(); + if (name == null) { + return Collections.emptyList(); + } + + List result = new SmartList<>(); + processExtensionDeclarations(myPsiClass.getQualifiedName(), myPsiClass.getProject(), (file, startOffset, endOffset) -> { + XmlTag tag = getXmlTagOfTokenElement(file, startOffset, name, true); + DomElement dom = DomUtil.getDomElement(tag); + if (dom instanceof Extension && ((Extension)dom).getExtensionPoint() != null) { + result.add(new ExtensionCandidate(SmartPointerManager.getInstance(tag.getProject()).createSmartPsiElementPointer(tag))); + } + return true; // continue processing + }); + + return result; + } + } + + private static class ExtensionByExtensionPointLocator extends ExtensionLocator { + private final ExtensionPoint myExtensionPoint; + private final String myExtensionId; + + private ExtensionByExtensionPointLocator(@NotNull ExtensionPoint extensionPoint, @Nullable String extensionId) { + myExtensionPoint = extensionPoint; + myExtensionId = extensionId; + } + + @NotNull + @Override + public List findCandidates() { + XmlTag epTag = myExtensionPoint.getXmlTag(); + if (epTag == null) { + return Collections.emptyList(); + } + + Project project = epTag.getProject(); + DomManager domManager = DomManager.getDomManager(project); + String epName = myExtensionPoint.getEffectiveName(); + + List result = new SmartList<>(); + processExtensionDeclarations(myExtensionPoint.getEffectiveName(), project, (file, startOffset, endOffset) -> { + XmlTag tag = getXmlTagOfTokenElement(file, startOffset, epName, false); + if (tag == null) { + return true; + } + + DomElement domElement = domManager.getDomElement(tag); + if (!(domElement instanceof Extension)) { + return true; + } + + Extension extension = (Extension)domElement; + ExtensionPoint ep = extension.getExtensionPoint(); + if (ep == null) { + return true; + } + + if (StringUtil.equals(ep.getEffectiveQualifiedName(), myExtensionPoint.getEffectiveQualifiedName()) + && (myExtensionId == null || myExtensionId.equals(extension.getId().getStringValue()))) { + result.add(new ExtensionCandidate(SmartPointerManager.getInstance(tag.getProject()).createSmartPsiElementPointer(tag))); + return myExtensionId == null; // stop after the first found candidate if ID is specified + } + + return true; + }); + + return result; + } + } + + + private static void processExtensionDeclarations(String name, Project project, PsiNonJavaFileReferenceProcessor referenceProcessor) { if (name == null) return; - - Project project = psiClass.getProject(); GlobalSearchScope scope = PluginRelatedLocatorsUtils.getCandidatesScope(project); - PsiSearchHelper.SERVICE.getInstance(project).processUsagesInNonJavaFiles(name, referenceProcessor, scope); } - private static class ReferenceProcessor implements PsiNonJavaFileReferenceProcessor { - private final String myExtensionClassName; - private final Processor myExtensionTagHandler; - - private ReferenceProcessor(String name, Processor extensionTagHandler) { - myExtensionClassName = name; - myExtensionTagHandler = extensionTagHandler; + @Nullable + private static XmlTag getXmlTagOfTokenElement(PsiFile file, int startOffset, String matchStr, boolean strictMatch) { + PsiElement element = file.findElementAt(startOffset); + String tokenText = element instanceof XmlToken ? element.getText() : null; + if (tokenText == null) { + return null; } - - @Override - public boolean process(PsiFile file, int startOffset, int endOffset) { - PsiElement element = file.findElementAt(startOffset); - String tokenText = element instanceof XmlToken ? element.getText() : null; - if (!StringUtil.equals(myExtensionClassName, tokenText)) return true; - - XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class); - if (tag == null) return true; - - DomElement dom = DomUtil.getDomElement(tag); - if (dom instanceof Extension && ((Extension)dom).getExtensionPoint() != null) { - return myExtensionTagHandler.process(tag); - } - return true; + if (!strictMatch && !StringUtil.contains(tokenText, matchStr)) { + return null; } + if (strictMatch && !StringUtil.equals(tokenText, matchStr)) { + return null; + } + return PsiTreeUtil.getParentOfType(element, XmlTag.class); } } diff --git a/plugins/devkit/src/util/PsiUtil.java b/plugins/devkit/src/util/PsiUtil.java index b6da196411d8..9e70d18edca8 100644 --- a/plugins/devkit/src/util/PsiUtil.java +++ b/plugins/devkit/src/util/PsiUtil.java @@ -194,4 +194,8 @@ public class PsiUtil { public static SmartPsiElementPointer createPointer(@NotNull E e) { return SmartPointerManager.getInstance(e.getProject()).createSmartPsiElementPointer(e); } + + public static boolean isPluginXmlPsiElement(@NotNull PsiElement element) { + return isPluginProject(element.getProject()) && DescriptorUtil.isPluginXml(element.getContainingFile()); + } } diff --git a/plugins/devkit/testData/codeInsight/orderAttributeCompletionBeforeKeyword.xml b/plugins/devkit/testData/codeInsight/orderAttributeCompletionBeforeKeyword.xml new file mode 100644 index 000000000000..85e7f93051f2 --- /dev/null +++ b/plugins/devkit/testData/codeInsight/orderAttributeCompletionBeforeKeyword.xml @@ -0,0 +1,13 @@ + + com.intellij + + + + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/testData/codeInsight/orderAttributeCompletionBeforeKeyword_after.xml b/plugins/devkit/testData/codeInsight/orderAttributeCompletionBeforeKeyword_after.xml new file mode 100644 index 000000000000..f558facad5e5 --- /dev/null +++ b/plugins/devkit/testData/codeInsight/orderAttributeCompletionBeforeKeyword_after.xml @@ -0,0 +1,13 @@ + + com.intellij + + + + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/testData/codeInsight/orderAttributeCompletionFirstKeywordWithId.xml b/plugins/devkit/testData/codeInsight/orderAttributeCompletionFirstKeywordWithId.xml new file mode 100644 index 000000000000..4502b139700d --- /dev/null +++ b/plugins/devkit/testData/codeInsight/orderAttributeCompletionFirstKeywordWithId.xml @@ -0,0 +1,14 @@ + + com.intellij + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/testData/codeInsight/orderAttributeCompletionFirstKeywordWithId_after.xml b/plugins/devkit/testData/codeInsight/orderAttributeCompletionFirstKeywordWithId_after.xml new file mode 100644 index 000000000000..fc0668a26b23 --- /dev/null +++ b/plugins/devkit/testData/codeInsight/orderAttributeCompletionFirstKeywordWithId_after.xml @@ -0,0 +1,14 @@ + + com.intellij + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/testData/codeInsight/orderAttributeCompletionIds.xml b/plugins/devkit/testData/codeInsight/orderAttributeCompletionIds.xml new file mode 100644 index 000000000000..8ed1f9715477 --- /dev/null +++ b/plugins/devkit/testData/codeInsight/orderAttributeCompletionIds.xml @@ -0,0 +1,16 @@ + + com.intellij + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/testData/codeInsight/orderAttributeCompletionIdsWithFirst.xml b/plugins/devkit/testData/codeInsight/orderAttributeCompletionIdsWithFirst.xml new file mode 100644 index 000000000000..89fcf2fb7c80 --- /dev/null +++ b/plugins/devkit/testData/codeInsight/orderAttributeCompletionIdsWithFirst.xml @@ -0,0 +1,16 @@ + + com.intellij + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/testData/codeInsight/orderAttributeCompletionKeywords.xml b/plugins/devkit/testData/codeInsight/orderAttributeCompletionKeywords.xml new file mode 100644 index 000000000000..6178b1f06e81 --- /dev/null +++ b/plugins/devkit/testData/codeInsight/orderAttributeCompletionKeywords.xml @@ -0,0 +1,13 @@ + + com.intellij + + + + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/testData/codeInsight/orderAttributeCompletionKeywordsInEmptyValue.xml b/plugins/devkit/testData/codeInsight/orderAttributeCompletionKeywordsInEmptyValue.xml new file mode 100644 index 000000000000..66cf2e012386 --- /dev/null +++ b/plugins/devkit/testData/codeInsight/orderAttributeCompletionKeywordsInEmptyValue.xml @@ -0,0 +1,13 @@ + + com.intellij + + + + + + + + + + \ No newline at end of file diff --git a/plugins/devkit/testData/codeInsight/orderAttributeHighlighting.xml b/plugins/devkit/testData/codeInsight/orderAttributeHighlighting.xml new file mode 100644 index 000000000000..4ff97d7c9330 --- /dev/null +++ b/plugins/devkit/testData/codeInsight/orderAttributeHighlighting.xml @@ -0,0 +1,18 @@ + + com.intellij + + + + + + + + + + wrongId"/> + + wrongId"/> + wrongId"/> + + \ No newline at end of file diff --git a/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy b/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy index 5c3d4c015143..951ff2a753b3 100644 --- a/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy +++ b/plugins/devkit/testSources/codeInsight/PluginXmlFunctionalTest.groovy @@ -23,6 +23,7 @@ import com.intellij.codeInspection.xml.DeprecatedClassUsageInspection import com.intellij.diagnostic.ITNReporter import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.PluginPathManager +import com.intellij.openapi.extensions.LoadingOrder import com.intellij.psi.ElementDescriptionUtil import com.intellij.psi.PsiElement import com.intellij.testFramework.PsiTestUtil @@ -361,6 +362,40 @@ class PluginXmlFunctionalTest extends JavaCodeInsightFixtureTestCase { } } + void testOrderAttributeHighlighting() { + myFixture.testHighlighting("orderAttributeHighlighting.xml") + } + + // separate tests for 'order' attribute completion because cannot test all cases with completeBasicAllCarets + + void testOrderAttributeCompletionKeywordsInEmptyValue() { + myFixture.testCompletionVariants(getTestName(true) + ".xml", + LoadingOrder.FIRST_STR, LoadingOrder.LAST_STR, + LoadingOrder.BEFORE_STR.trim(), LoadingOrder.AFTER_STR.trim()) + } + + void testOrderAttributeCompletionBeforeKeyword() { + myFixture.testCompletion(getTestName(true) + ".xml", getTestName(true) + "_after.xml") + } + + void testOrderAttributeCompletionKeywords() { + myFixture.testCompletionVariants(getTestName(true) + ".xml", + LoadingOrder.FIRST_STR, LoadingOrder.LAST_STR, + LoadingOrder.BEFORE_STR.trim(), LoadingOrder.AFTER_STR.trim()) + } + + void testOrderAttributeCompletionIds() { + myFixture.testCompletionVariants(getTestName(true) + ".xml", "id1", "id2", "id3") + } + + void testOrderAttributeCompletionIdsWithFirst() { + myFixture.testCompletionVariants(getTestName(true) + ".xml", "id1", "id2", "id3") + } + + void testOrderAttributeCompletionFirstKeywordWithId() { + myFixture.testCompletion(getTestName(true) + ".xml", getTestName(true) + "_after.xml") + } + private void testHighlightingInIdeaProject(String path) { myFixture.enableInspections(PluginXmlDomInspection.class) PsiUtil.markAsIdeaProject(project, true)