[devkit] goto declaration in plugin.xml for Action/ActionGroup (IDEA-129958)

GitOrigin-RevId: 16d2dd5bac02c211e33168f6e334247526491911
This commit is contained in:
Yann Cébron
2022-05-18 07:22:19 +00:00
committed by intellij-monorepo-bot
parent 72102f2de6
commit 9a78cb32e7
13 changed files with 247 additions and 28 deletions
@@ -359,6 +359,8 @@ implemented.at.runtime.jamElement=JAM Element implemented at runtime
line.marker.tooltip.extension.declaration=<a href="#navigation/{0}:{1}">{2}</a> declaration in {3}
line.marker.tooltip.extension.point.declaration=<a href="#navigation/{0}:{1}">{2}</a> extension point declaration in {3}
line.marker.tooltip.listener.declaration=<a href="#navigation/{0}:{1}">{2}</a> listener in {3}
line.marker.tooltip.action.declaration=<a href="#navigation/{0}:{1}">{2}</a> action in {3}
line.marker.tooltip.action.group.declaration=<a href="#navigation/{0}:{1}">{2}</a> action group in {3}
inspections.usage.uelement.as.psi=Usage of UElement as PsiElement is not recommended
@@ -487,6 +489,8 @@ gutter.related.navigation.popup.description.tooltip=Description
gutter.related.navigation.choose.extension=Choose Extension
gutter.related.navigation.choose.extension.point=Choose Extension Point
gutter.related.navigation.choose.listener=Choose Listener
gutter.related.navigation.choose.action=Choose Action
gutter.related.navigation.choose.action.group=Choose Action Group
# plugin.xml resolving
plugin.xml.convert.action.or.group.cannot.resolve=Cannot resolve {0} ''{1}''
@@ -23,6 +23,7 @@ import com.intellij.util.io.KeyDescriptor;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.devkit.dom.*;
import org.jetbrains.idea.devkit.dom.index.RegistrationEntry.RegistrationType;
@@ -95,20 +96,24 @@ public class IdeaPluginRegistrationIndex extends PluginXmlIndexBase<String, List
return INDEX_VERSION;
}
public static boolean isRegisteredClass(PsiClass psiClass, GlobalSearchScope scope) {
return isRegisteredClass(psiClass, scope, null);
}
public static boolean isRegisteredApplicationComponent(PsiClass psiClass, GlobalSearchScope scope) {
return isRegistered(psiClass, scope, RegistrationType.APPLICATION_COMPONENT);
return isRegisteredClass(psiClass, scope, RegistrationType.APPLICATION_COMPONENT);
}
public static boolean isRegisteredProjectComponent(PsiClass psiClass, GlobalSearchScope scope) {
return isRegistered(psiClass, scope, RegistrationType.PROJECT_COMPONENT);
return isRegisteredClass(psiClass, scope, RegistrationType.PROJECT_COMPONENT);
}
public static boolean isRegisteredModuleComponent(PsiClass psiClass, GlobalSearchScope scope) {
return isRegistered(psiClass, scope, RegistrationType.MODULE_COMPONENT);
return isRegisteredClass(psiClass, scope, RegistrationType.MODULE_COMPONENT);
}
public static boolean isRegisteredAction(PsiClass psiClass, GlobalSearchScope scope) {
return isRegistered(psiClass, scope, RegistrationType.ACTION);
public static boolean isRegisteredActionOrGroup(PsiClass psiClass, GlobalSearchScope scope) {
return isRegisteredClass(psiClass, scope, RegistrationType.ACTION);
}
public static boolean processListener(@NotNull Project project,
@@ -129,7 +134,10 @@ public class IdeaPluginRegistrationIndex extends PluginXmlIndexBase<String, List
});
}
private static boolean isRegistered(PsiClass psiClass, GlobalSearchScope scope, RegistrationType type) {
/**
* @param type {@code null} for any
*/
private static boolean isRegisteredClass(PsiClass psiClass, GlobalSearchScope scope, @Nullable RegistrationType type) {
final String qualifiedName = psiClass.getQualifiedName();
if (qualifiedName == null) {
return false;
@@ -137,7 +145,14 @@ public class IdeaPluginRegistrationIndex extends PluginXmlIndexBase<String, List
return !FileBasedIndex.getInstance()
.processValues(NAME, qualifiedName, null,
(file, value) -> ContainerUtil.process(value, entry -> !(entry.getRegistrationType() == type)),
(file, value) -> ContainerUtil.process(value, entry -> {
RegistrationType registrationType = entry.getRegistrationType();
if (type == null) {
return !(registrationType.isClass());
}
return !(registrationType == type);
}),
scope);
}
@@ -149,6 +164,18 @@ public class IdeaPluginRegistrationIndex extends PluginXmlIndexBase<String, List
return ContainerUtil.process(keys, s -> processActionOrGroup(project, s, scope, processor));
}
public static boolean processActionOrGroupClass(@NotNull Project project,
PsiClass actionOrGroupClass,
GlobalSearchScope scope,
Processor<? super ActionOrGroup> processor) {
String fqn = actionOrGroupClass.getQualifiedName();
if (fqn == null) {
return true;
}
return doProcessActionOrGroup(project, fqn, scope, EnumSet.of(RegistrationType.ACTION), processor);
}
public static boolean processActionOrGroup(@NotNull Project project,
@NotNull String actionOrGroupId,
GlobalSearchScope scope,
@@ -173,11 +200,11 @@ public class IdeaPluginRegistrationIndex extends PluginXmlIndexBase<String, List
}
private static boolean doProcessActionOrGroup(@NotNull Project project,
@NotNull String actionOrGroupId,
@NotNull String key,
GlobalSearchScope scope,
EnumSet<RegistrationType> types,
Processor<? super ActionOrGroup> processor) {
List<XmlTag> tags = collectTags(project, actionOrGroupId, scope, types);
List<XmlTag> tags = collectTags(project, key, scope, types);
return ContainerUtil.process(tags, tag -> {
final DomElement domElement = DomManager.getDomManager(project).getDomElement(tag);
@@ -41,16 +41,26 @@ class RegistrationEntry {
// do not change order
enum RegistrationType {
ACTION,
ACTION(true),
APPLICATION_COMPONENT,
PROJECT_COMPONENT,
MODULE_COMPONENT,
APPLICATION_COMPONENT(true),
PROJECT_COMPONENT(true),
MODULE_COMPONENT(true),
ACTION_ID,
ACTION_GROUP_ID,
ACTION_ID(false),
ACTION_GROUP_ID(false),
APPLICATION_LISTENER,
PROJECT_LISTENER
APPLICATION_LISTENER(true),
PROJECT_LISTENER(true);
private final boolean myIsClass;
RegistrationType(boolean isClass) {
myIsClass = isClass;
}
boolean isClass() {
return myIsClass;
}
}
}
@@ -1,4 +1,4 @@
// Copyright 2000-2020 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.
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.inspections;
import com.intellij.openapi.module.Module;
@@ -177,8 +177,8 @@ final class RegistrationCheckerUtil {
}
if (findAll || myRegistrationType == RegistrationType.ACTION) {
if (IdeaPluginRegistrationIndex.isRegisteredAction(myPsiClass,
scope)) {
if (IdeaPluginRegistrationIndex.isRegisteredActionOrGroup(myPsiClass,
scope)) {
addType(ActionType.ACTION.myClassName);
return false;
}
@@ -2,11 +2,13 @@
package org.jetbrains.idea.devkit.navigation;
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo;
import com.intellij.codeInsight.navigation.DomGotoRelatedItem;
import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder;
import com.intellij.navigation.GotoRelatedItem;
import com.intellij.openapi.editor.markup.GutterIconRenderer;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
@@ -25,13 +27,16 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.PropertyKey;
import org.jetbrains.idea.devkit.DevKitBundle;
import org.jetbrains.idea.devkit.DevKitIcons;
import org.jetbrains.idea.devkit.dom.ActionOrGroup;
import org.jetbrains.idea.devkit.dom.Extension;
import org.jetbrains.idea.devkit.dom.ExtensionPoint;
import org.jetbrains.idea.devkit.dom.Listeners;
import org.jetbrains.idea.devkit.util.ActionCandidate;
import org.jetbrains.idea.devkit.util.ListenerCandidate;
import org.jetbrains.idea.devkit.util.PointableCandidate;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
final class LineMarkerInfoHelper {
@@ -39,7 +44,7 @@ final class LineMarkerInfoHelper {
private static final NotNullFunction<PointableCandidate, Collection<? extends PsiElement>> CONVERTER =
candidate -> ContainerUtil.createMaybeSingletonList(candidate.pointer.getElement());
private static final NotNullFunction<PointableCandidate, Collection<? extends GotoRelatedItem>> RELATED_ITEM_PROVIDER =
candidate -> GotoRelatedItem.createItems(ContainerUtil.createMaybeSingletonList(candidate.pointer.getElement()), "DevKit");
candidate -> Collections.singleton(new DomGotoRelatedItem(DomUtil.getDomElement(candidate.pointer.getElement()), "DevKit"));
private static final NullableFunction<PointableCandidate, String> EXTENSION_NAMER =
createNamer("line.marker.tooltip.extension.declaration", tag -> {
@@ -65,6 +70,18 @@ final class LineMarkerInfoHelper {
return StringUtil.notNullize(((Listeners.Listener)element).getTopicClassName().getStringValue(), "?");
});
private static final NotNullFunction<XmlTag, @NlsContexts.ListItem String> ACTION_OR_GROUP_NAME_FUNCTION = tag -> {
final DomElement element = DomUtil.getDomElement(tag);
if (!(element instanceof ActionOrGroup)) return "?";
return StringUtil.notNullize(((ActionOrGroup)element).getId().getStringValue(), "?");
};
private static final NullableFunction<PointableCandidate, String> ACTION_NAMER =
createNamer("line.marker.tooltip.action.declaration", ACTION_OR_GROUP_NAME_FUNCTION);
private static final NullableFunction<PointableCandidate, String> ACTION_GROUP_NAMER =
createNamer("line.marker.tooltip.action.group.declaration", ACTION_OR_GROUP_NAME_FUNCTION);
private LineMarkerInfoHelper() {
}
@@ -89,6 +106,18 @@ final class LineMarkerInfoHelper {
LISTENER_NAMER);
}
static RelatedItemLineMarkerInfo<?> createActionLineMarkerInfo(List<? extends ActionCandidate> targets, PsiElement element) {
return createPluginLineMarkerInfo(targets, element,
DevKitBundle.message("gutter.related.navigation.choose.action"),
ACTION_NAMER);
}
static RelatedItemLineMarkerInfo<?> createActionGroupLineMarkerInfo(List<? extends ActionCandidate> targets, PsiElement element) {
return createPluginLineMarkerInfo(targets, element,
DevKitBundle.message("gutter.related.navigation.choose.action.group"),
ACTION_GROUP_NAMER);
}
private static @NotNull RelatedItemLineMarkerInfo<PsiElement> createPluginLineMarkerInfo(@NotNull List<? extends PointableCandidate> targets,
@NotNull PsiElement element,
@Nls(capitalization = Nls.Capitalization.Title) String popup,
@@ -2,10 +2,12 @@
package org.jetbrains.idea.devkit.navigation;
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.util.SmartList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.idea.devkit.DevKitBundle;
@@ -20,6 +22,7 @@ import java.util.List;
/**
* Related declaration(s) in {@code plugin.xml} for class registered as:
* <ul>
* <li>Action/ActionGroup</li>
* <li>Listener</li>
* <li>Extension</li>
* </ul>
@@ -48,6 +51,32 @@ public final class PluginDescriptorDeclarationRelatedItemLineMarkerProvider exte
Project project = psiClass.getProject();
GlobalSearchScope candidatesScope = PluginRelatedLocatorsUtils.getCandidatesScope(project);
// only EPs are not indexed via IdeaPluginRegistrationIndex
if (!IdeaPluginRegistrationIndex.isRegisteredClass(psiClass, candidatesScope)) {
List<ExtensionCandidate> epTargets = ExtensionLocatorKt.locateExtensionsByPsiClass(psiClass);
if (!epTargets.isEmpty()) {
result.add(LineMarkerInfoHelper.createExtensionLineMarkerInfo(epTargets, identifier));
}
return;
}
// Action/ActionGroup (most likely)
// search all candidates, e.g. EmptyAction/NonTrivialActionGroup is registered multiple times
if (IdeaPluginRegistrationIndex.isRegisteredActionOrGroup(psiClass, candidatesScope)) {
List<ActionCandidate> targets = new SmartList<>();
IdeaPluginRegistrationIndex.processActionOrGroupClass(project, psiClass, candidatesScope, actionOrGroup -> {
targets.add(new ActionCandidate(actionOrGroup));
return true;
});
if (InheritanceUtil.isInheritor(psiClass, ActionGroup.class.getName())) {
result.add(LineMarkerInfoHelper.createActionGroupLineMarkerInfo(targets, identifier));
}
else {
result.add(LineMarkerInfoHelper.createActionLineMarkerInfo(targets, identifier));
}
return;
}
// Listeners: search for _all_ candidates as
// - some listeners are registered on both application- and project-level, e.g. com.intellij.notification.Notifications
// - some classes implement multiple listeners, e.g. com.intellij.notification.impl.widget.NotificationWidgetListener
@@ -58,13 +87,6 @@ public final class PluginDescriptorDeclarationRelatedItemLineMarkerProvider exte
});
if (!listenerTargets.isEmpty()) {
result.add(LineMarkerInfoHelper.createListenerLineMarkerInfo(listenerTargets, identifier));
return;
}
List<ExtensionCandidate> epTargets = ExtensionLocatorKt.locateExtensionsByPsiClass(psiClass);
if (!epTargets.isEmpty()) {
result.add(LineMarkerInfoHelper.createExtensionLineMarkerInfo(epTargets, identifier));
}
}
}
@@ -0,0 +1,13 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.util;
import com.intellij.psi.SmartPointerManager;
import org.jetbrains.idea.devkit.dom.ActionOrGroup;
import java.util.Objects;
public class ActionCandidate extends PointableCandidate {
public ActionCandidate(ActionOrGroup actionOrGroup) {
super(SmartPointerManager.createPointer(Objects.requireNonNull(actionOrGroup.getXmlTag())));
}
}
@@ -0,0 +1 @@
public class My<caret>Action extends com.intellij.openapi.actionSystem.AnAction {}
@@ -0,0 +1 @@
public class My<caret>ActionGroup extends com.intellij.openapi.actionSystem.ActionGroup {}
@@ -0,0 +1,8 @@
<idea-plugin>
<actions>
<group class="MyActionGroup" id="firstDeclaration"/>
</actions>
<actions>
<group class="MyActionGroup" id="secondDeclaration"/>
</actions>
</idea-plugin>
@@ -0,0 +1,8 @@
<idea-plugin>
<actions>
<action class="MyAction" id="firstDeclaration" icon="AllIcons.Actions.AddFile"/>
</actions>
<actions>
<action class="MyAction" id="secondDeclaration"/>
</actions>
</idea-plugin>
@@ -0,0 +1,5 @@
<idea-plugin>
<actions>
<action class="MyAction" id="singleDeclaration"/>
</actions>
</idea-plugin>
@@ -0,0 +1,91 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.navigation;
import com.intellij.codeInsight.daemon.GutterMark;
import com.intellij.codeInsight.navigation.DomGotoRelatedItem;
import com.intellij.codeInsight.navigation.NavigationUtil;
import com.intellij.icons.AllIcons;
import com.intellij.navigation.GotoRelatedItem;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.testFramework.TestDataPath;
import com.intellij.testFramework.builders.JavaModuleFixtureBuilder;
import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase;
import com.intellij.ui.ColorUtil;
import com.intellij.ui.components.JBList;
import com.intellij.util.PathUtil;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.idea.devkit.DevKitIcons;
import org.jetbrains.idea.devkit.DevkitJavaTestsUtil;
import java.util.List;
@TestDataPath("$CONTENT_ROOT/testData/navigation/actionDeclaration")
public class ActionDeclarationRelatedItemLineMarkerProviderTest extends JavaCodeInsightFixtureTestCase {
@Override
protected String getBasePath() {
return DevkitJavaTestsUtil.TESTDATA_PATH + "navigation/actionDeclaration";
}
@Override
protected void tuneFixture(JavaModuleFixtureBuilder moduleBuilder) {
String platformEditorJar = PathUtil.getJarPathForClass(AnAction.class);
moduleBuilder.addLibrary("platform-editor", platformEditorJar);
String platformIdeJar = PathUtil.getJarPathForClass(JBList.class);
moduleBuilder.addLibrary("platform-ide", platformIdeJar);
String platformUtilJar = PathUtil.getJarPathForClass(AllIcons.class);
moduleBuilder.addLibrary("platform-util", platformUtilJar);
}
public void testActionSingleDeclaration() {
VirtualFile pluginXmlFile = myFixture.copyFileToProject("pluginActionSingleDeclaration.xml");
GutterMark gutter = myFixture.findGutter("MyAction.java");
DevKitGutterTargetsChecker.checkGutterTargets(gutter,
"<html><body>" +
buildTooltipText(pluginXmlFile, 30, "singleDeclaration", "action") +
"</body></html>",
DevKitIcons.Gutter.Plugin, "action");
}
public void testActionMultipleDeclarations() {
VirtualFile pluginXmlFile = myFixture.copyFileToProject("pluginActionMultipleDeclarations.xml");
GutterMark gutter = myFixture.findGutter("MyAction.java");
DevKitGutterTargetsChecker.checkGutterTargets(gutter,
"<html><body>" +
buildTooltipText(pluginXmlFile, 140, "secondDeclaration", "action") +
buildTooltipText(pluginXmlFile, 30, "firstDeclaration", "action") +
"</body></html>",
DevKitIcons.Gutter.Plugin, "action");
List<GotoRelatedItem> relatedItems = NavigationUtil.collectRelatedItems(myFixture.findClass("MyAction"), null);
assertSize(2, relatedItems);
DomGotoRelatedItem first = assertInstanceOf(relatedItems.get(0), DomGotoRelatedItem.class);
assertEquals("DevKit", first.getGroup());
assertEquals("firstDeclaration", first.getCustomName());
assertEquals(AllIcons.Actions.AddFile, first.getCustomIcon());
}
public void testActionGroupMultipleDeclarations() {
VirtualFile pluginXmlFile = myFixture.copyFileToProject("pluginActionGroupMultipleDeclarations.xml");
GutterMark gutter = myFixture.findGutter("MyActionGroup.java");
DevKitGutterTargetsChecker.checkGutterTargets(gutter,
"<html><body>" +
buildTooltipText(pluginXmlFile, 112, "secondDeclaration", "action group") +
buildTooltipText(pluginXmlFile, 30, "firstDeclaration", "action group") +
"</body></html>",
DevKitIcons.Gutter.Plugin, "group");
}
private String buildTooltipText(VirtualFile pluginXmlFile, int expectedTagPosition, String actionId, String actionType) {
String pluginXmlPath = pluginXmlFile.getPath();
String color = ColorUtil.toHex(UIUtil.getInactiveTextColor());
return "&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"#navigation/" + pluginXmlPath
+ ":" + expectedTagPosition + "\">" + actionId + "</a> " + actionType + " in " + pluginXmlFile.getName() +
" <font color=\"" + color + "\">[" + getModule().getName() + "]</font><br>";
}
}