IDEA-115118 Support for 'order' attribute in extensions in plugin.xml files

This commit is contained in:
Yaroslav Pankratyev
2017-11-07 18:34:05 +07:00
parent 5d379c83ea
commit 4a7aa8721d
25 changed files with 698 additions and 78 deletions
@@ -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;
@@ -1389,11 +1389,23 @@ public class StringUtil extends StringUtilRt {
@NotNull
@Contract(pure = true)
public static List<TextRange> 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<TextRange> getWordIndicesIn(@NotNull String text, @Nullable Set<Character> separatorsSet) {
List<TextRange> result = new SmartList<TextRange>();
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;
}
@@ -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('-')));
}
}
+3 -1
View File
@@ -62,6 +62,8 @@
<lang.documentationProvider language="XML"
implementationClass="org.jetbrains.idea.devkit.references.extensions.ExtensionPointDocumentationProvider"/>
<completion.contributor implementationClass="org.jetbrains.idea.devkit.completion.ExtensionOrderKeywordCompletionContributor" language="XML" />
<localInspection language="XML" shortName="PluginXmlValidity" displayName="Plugin.xml Validity" applyToDialects="false"
groupKey="inspections.group.name" enabledByDefault="true" level="ERROR"
implementationClass="org.jetbrains.idea.devkit.inspections.PluginXmlDomInspection"/>
@@ -174,7 +176,7 @@
<implicitUsageProvider implementation="org.jetbrains.idea.devkit.inspections.DevKitImplicitUsageProvider"/>
<psi.referenceContributor language="XML" implementation="org.jetbrains.idea.devkit.dom.impl.I18nReferenceContributor"/>
<psi.referenceContributor implementation="org.jetbrains.idea.devkit.references.IconsReferencesContributor"/>
<psi.referenceContributor implementation="org.jetbrains.idea.devkit.references.TestDataFilesReferencesContributor" />
<psi.referenceContributor implementation="org.jetbrains.idea.devkit.references.TestDataFilesReferencesContributor"/>
<referencesSearch implementation="org.jetbrains.idea.devkit.references.IconsReferencesContributor"/>
<unusedDeclarationFixProvider implementation="org.jetbrains.idea.devkit.inspections.quickfix.RegisterExtensionFixProvider"/>
@@ -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 <id>'' or ''after <id>''
ant.build.jar.comment=Build archive for plugin ''{0}''
ant.build.jar.description=Build plugin archive for module ''{0}''
@@ -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<LookupElement> 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<CompletionParameters>() {
@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<PsiElement> getCapture() {
//TODO write a method for attribute value in XmlPatterns
return psiElement().inside(
XmlPatterns.xmlAttributeValue("order").inside(
XmlPatterns.xmlTag().with(new PatternCondition<XmlTag>("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);
}
}
+4 -7
View File
@@ -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<String> getId();
@Referencing(ExtensionOrderConverter.class)
@Required(value = false)
GenericAttributeValue<String> getOrder();
@@ -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<String> {
private static final Logger LOG = Logger.getInstance(ExtensionOrderConverter.class);
@NotNull
@Override
public PsiReference[] createReferences(GenericDomValue<String> 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<PsiReference>(orderValue, element, 0, ',') {
@Override
protected List<PsiReference> 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<String> 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<TextRange> 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<String> splitOrderPart(String orderPart) {
List<String> result = new ArrayList<>();
List<String> subParts = StringUtil.split(orderPart, " ");
subParts.forEach(s -> {
if (":".equals(s)) {
result.add(s);
}
else {
Collections.addAll(result, s.split(":"));
}
});
return result;
}
private static List<TextRange> 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<PsiElement> 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<PsiElement> 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<ExtensionCandidate> 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<ExtensionCandidate> candidates = epLocator.findCandidates();
DomManager domManager = DomManager.getDomManager(getElement().getProject());
List<Extension> 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<LookupElement> 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;
}
}
}
@@ -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<IdeaPlugi
if ("icon".equals(attributeDescription.getXmlElementName())) {
annotateResolveProblems(holder, attributeValue);
}
else if ("order".equals(attributeDescription.getXmlElementName())) {
annotateOrderAttributeProblems(holder, attributeValue);
}
final PsiElement declaration = attributeDescription.getDeclaration(extension.getManager().getProject());
if (declaration instanceof PsiField) {
@@ -336,6 +341,22 @@ public class PluginXmlDomInspection extends BasicDomElementsInspection<IdeaPlugi
}
}
private static void annotateOrderAttributeProblems(DomElementAnnotationHolder holder, GenericAttributeValue attributeValue) {
String orderValue = attributeValue.getStringValue();
if (StringUtil.isEmpty(orderValue)) {
return;
}
try {
LoadingOrder.readOrder(orderValue);
} catch (AssertionError ignore) {
holder.createProblem(attributeValue, HighlightSeverity.ERROR, DevKitBundle.message("inspections.plugin.xml.invalid.order.attribute"));
return; // no need to resolve references for invalid attribute value
}
annotateResolveProblems(holder, attributeValue);
}
private static void annotateResolveProblems(DomElementAnnotationHolder holder, GenericAttributeValue attributeValue) {
final XmlAttributeValue value = attributeValue.getXmlAttributeValue();
if (value != null) {
@@ -44,7 +44,7 @@ public class StatefulEpInspection extends DevKitUastInspectionBase {
if (fields.length == 0) return super.checkClass(psiClass, manager, isOnTheFly);
final boolean isQuickFix = InheritanceUtil.isInheritor(psiClass, LocalQuickFix.class.getCanonicalName());
ExtensionLocator locator = new ExtensionLocator(psiClass);
ExtensionLocator locator = ExtensionLocator.byPsiClass(psiClass);
List<ExtensionCandidate> targets = locator.findCandidates();
if (isQuickFix || !targets.isEmpty()) {
boolean isProjectComponent = InheritanceUtil.isInheritor(psiClass, ProjectComponent.class.getCanonicalName());
@@ -41,7 +41,7 @@ public class ExtensionDeclarationRelatedItemLineMarkerProvider extends DevkitRel
return;
}
ExtensionLocator locator = new ExtensionLocator(psiClass);
ExtensionLocator locator = ExtensionLocator.byPsiClass(psiClass);
List<ExtensionCandidate> targets = locator.findCandidates();
if (targets.isEmpty()) {
return;
@@ -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;
}
+1 -1
View File
@@ -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;
}
+108 -56
View File
@@ -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<ExtensionCandidate> findCandidates() {
String name = myPsiClass.getQualifiedName();
if (name == null) {
return Collections.emptyList();
}
public abstract List<ExtensionCandidate> findCandidates();
List<ExtensionCandidate> 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<XmlTag> 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<ExtensionCandidate> findCandidates() {
String name = myPsiClass.getQualifiedName();
if (name == null) {
return Collections.emptyList();
}
List<ExtensionCandidate> 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<ExtensionCandidate> 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<ExtensionCandidate> 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<XmlTag> myExtensionTagHandler;
private ReferenceProcessor(String name, Processor<XmlTag> 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);
}
}
+4
View File
@@ -194,4 +194,8 @@ public class PsiUtil {
public static <E extends PsiElement> SmartPsiElementPointer<E> 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());
}
}
@@ -0,0 +1,13 @@
<idea-plugin>
<id>com.intellij</id>
<extensionPoints>
<!--suppress PluginXmlValidity -->
<extensionPoint name="myEp2"
beanClass="java.lang.String"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<myEp2 id="id1" order="b<caret>"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,13 @@
<idea-plugin>
<id>com.intellij</id>
<extensionPoints>
<!--suppress PluginXmlValidity -->
<extensionPoint name="myEp2"
beanClass="java.lang.String"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<myEp2 id="id1" order="before "/>
</extensions>
</idea-plugin>
@@ -0,0 +1,14 @@
<idea-plugin>
<id>com.intellij</id>
<extensionPoints>
<!--suppress PluginXmlValidity -->
<extensionPoint name="myEp6"
beanClass="java.lang.String"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<myEp6 id="id1"/>
<myEp6 id="id2" order="before id1, fi<caret>"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,14 @@
<idea-plugin>
<id>com.intellij</id>
<extensionPoints>
<!--suppress PluginXmlValidity -->
<extensionPoint name="myEp6"
beanClass="java.lang.String"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<myEp6 id="id1"/>
<myEp6 id="id2" order="before id1, first"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,16 @@
<idea-plugin>
<id>com.intellij</id>
<extensionPoints>
<!--suppress PluginXmlValidity -->
<extensionPoint name="myEp4"
beanClass="java.lang.String"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<myEp4 id="id1"/>
<myEp4 id="id2"/>
<myEp4 id="id3"/>
<myEp4 id="id4" order="before <caret>"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,16 @@
<idea-plugin>
<id>com.intellij</id>
<extensionPoints>
<!--suppress PluginXmlValidity -->
<extensionPoint name="myEp5"
beanClass="java.lang.String"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<myEp5 id="id1"/>
<myEp5 id="id2"/>
<myEp5 id="id3"/>
<myEp5 id="id4" order="first, after <caret>"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,13 @@
<idea-plugin>
<id>com.intellij</id>
<extensionPoints>
<!--suppress PluginXmlValidity -->
<extensionPoint name="myEp3"
beanClass="java.lang.String"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<myEp3 id="id1" order="first, <caret>"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,13 @@
<idea-plugin>
<id>com.intellij</id>
<extensionPoints>
<!--suppress PluginXmlValidity -->
<extensionPoint name="myEp1"
beanClass="java.lang.String"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<myEp1 id="id1" order="<caret>"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,18 @@
<idea-plugin>
<id>com.intellij</id>
<extensionPoints>
<!--suppress PluginXmlValidity -->
<extensionPoint name="myEp"
beanClass="java.lang.String"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<myEp id="id1" order="first"/>
<myEp id="id2" order="last"/>
<myEp id="id3" order="after id1, before <error descr="Cannot resolve 'wrongId' myEp extension">wrongId</error>"/>
<myEp id="id4" order="before id2, after id1"/>
<myEp id="id5" order="before id2, after <error descr="Cannot resolve 'wrongId' myEp extension">wrongId</error>"/>
<myEp id="id6" order="before id6, before id5, after id1, before <error descr="Cannot resolve 'wrongId' myEp extension">wrongId</error>"/>
</extensions>
</idea-plugin>
@@ -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)