[devkit] Handle <action> without 'id' correctly (IJPL-177709)

GitOrigin-RevId: 012c879eb66e914667517359390dd884ac056705
This commit is contained in:
Yann Cébron
2025-02-12 18:26:00 +00:00
committed by intellij-monorepo-bot
parent aad8b7218b
commit d6a56f86c3
20 changed files with 137 additions and 47 deletions
@@ -510,6 +510,8 @@
<psi.referenceContributor language="UAST" implementation="org.jetbrains.idea.devkit.references.RegistryKeyIdReferenceContributor"/>
<psi.referenceContributor language="UAST" implementation="org.jetbrains.idea.devkit.references.ActionOrGroupIdReferenceContributor"/>
<referenceInjector implementation="org.jetbrains.idea.devkit.references.ActionOrGroupIdReferenceInjector"/>
<referencesSearch implementation="org.jetbrains.idea.devkit.references.ActionWithoutIdReferencesQueryExecutor"/>
<psi.referenceContributor language="UAST"
implementation="org.jetbrains.idea.devkit.references.NotificationGroupIdReferenceContributor"/>
<psi.referenceContributor language="UAST"
@@ -2,9 +2,11 @@
package org.jetbrains.idea.devkit.dom;
import com.intellij.ide.presentation.Presentation;
import com.intellij.openapi.util.text.StringUtilRt;
import com.intellij.psi.PsiClass;
import com.intellij.util.xml.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.devkit.dom.impl.ActionOrGroupPresentationProvider;
import org.jetbrains.idea.devkit.dom.impl.KeymapConverter;
import org.jetbrains.idea.devkit.dom.impl.PluginPsiClassConverter;
@@ -15,6 +17,25 @@ import java.util.List;
@Presentation(typeName = DevkitDomPresentationConstants.ACTION, provider = ActionOrGroupPresentationProvider.class)
public interface Action extends ActionOrGroup {
@Override
@Nullable
default String getEffectiveId() {
String id = ActionOrGroup.super.getEffectiveId();
if (id != null) return id;
String clazzValue = getClazz().getStringValue();
return clazzValue != null ? StringUtilRt.getShortName(clazzValue) : null;
}
@Override
default GenericAttributeValue<?> getEffectiveIdAttribute() {
if (DomUtil.hasXml(getId())) {
return getId();
}
return getClazz();
}
@NotNull
@Attribute("class")
@Required
@@ -5,6 +5,7 @@ import com.intellij.psi.PsiClass;
import com.intellij.util.xml.*;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.devkit.dom.impl.ActionOrGroupReferencingConverter;
import java.util.List;
@@ -12,6 +13,25 @@ import java.util.function.Function;
public interface ActionOrGroup extends DomElement {
/**
* @return possibly fallback ID if {@link #getId()} is not specified
* @see #getEffectiveIdAttribute()
*/
@Nullable
default String getEffectiveId() {
return getId().getStringValue();
}
/**
* @return underlying attribute for {@link #getEffectiveId()}, used for navigation purposes
*/
default GenericAttributeValue<?> getEffectiveIdAttribute() {
return getId();
}
/**
* @see #getEffectiveId()
*/
@NotNull
@NameValue
@Stubbed
@@ -96,12 +116,12 @@ public interface ActionOrGroup extends DomElement {
}
public String getMessageKey(ActionOrGroup actionOrGroup) {
return getMessageKeyPrefix(actionOrGroup) + actionOrGroup.getId().getStringValue() +
return getMessageKeyPrefix(actionOrGroup) + actionOrGroup.getEffectiveId() +
myPropertyKeySuffix;
}
public String getMessageKey(ActionOrGroup actionOrGroup, @NotNull OverrideText overrideText) {
return getMessageKeyPrefix(actionOrGroup) + actionOrGroup.getId().getStringValue() +
return getMessageKeyPrefix(actionOrGroup) + actionOrGroup.getEffectiveId() +
"." + overrideText.getPlace().getStringValue() +
myPropertyKeySuffix;
}
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.dom.index;
import com.intellij.openapi.project.Project;
@@ -40,7 +40,7 @@ import java.util.*;
* <ul>
* <li>Application/Project/Module-component class - {@link Component#getInterfaceClass()} / {@link Component#getImplementationClass()} / {@link Component#getHeadlessImplementationClass()}</li>
* <li>Action/ActionGroup class - {@link Action#getClazz()}/{@link Group#getClazz()}</li>
* <li>Action/ActionGroup ID - {@link ActionOrGroup#getId()}</li>
* <li>Action/ActionGroup ID - {@link ActionOrGroup#getEffectiveId()}</li>
* <li>Application/Project Listener class - {@link Listeners.Listener#getListenerClassName()}</li>
* <li>Listener topic class - {@link Listeners.Listener#getTopicClassName()}</li>
* </ul>
@@ -48,7 +48,7 @@ import java.util.*;
@SuppressWarnings("UnusedReturnValue")
public final class IdeaPluginRegistrationIndex extends PluginXmlIndexBase<String, List<RegistrationEntry>> {
private static final int INDEX_VERSION = 8;
private static final int INDEX_VERSION = 9;
private static final ID<String, List<RegistrationEntry>> NAME = ID.create("IdeaPluginRegistrationIndex");
@@ -1,4 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.dom.index;
import com.intellij.openapi.util.text.StringUtil;
@@ -9,9 +9,9 @@ import com.intellij.util.SmartList;
import com.intellij.util.containers.FactoryMap;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomUtil;
import com.intellij.util.xml.GenericAttributeValue;
import com.intellij.util.xml.GenericDomValue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.devkit.dom.*;
import java.util.List;
@@ -106,25 +106,24 @@ class RegistrationIndexer {
private void processActionContainer(ActionContainer actionContainer) {
for (Action action : actionContainer.getActions()) {
addEntry(action, action.getClazz(), RegistrationEntry.RegistrationType.ACTION);
addIdEntry(action, action.getId(), RegistrationEntry.RegistrationType.ACTION_ID);
addIdEntry(action, action.getEffectiveId(), RegistrationEntry.RegistrationType.ACTION_ID);
}
for (Group group : actionContainer.getGroups()) {
addEntry(group, group.getClazz(), RegistrationEntry.RegistrationType.ACTION);
addIdEntry(group, group.getId(), RegistrationEntry.RegistrationType.ACTION_GROUP_ID);
addIdEntry(group, group.getEffectiveId(), RegistrationEntry.RegistrationType.ACTION_GROUP_ID);
processActionContainer(group);
}
}
private void addIdEntry(DomElement domElement,
GenericAttributeValue<String> idValue,
@Nullable String idValue,
RegistrationEntry.RegistrationType type) {
if (StringUtil.isEmptyOrSpaces(idValue)) return;
if (!DomUtil.hasXml(domElement)) return;
String id = idValue.getStringValue();
if (StringUtil.isEmptyOrSpaces(id)) return;
storeEntry(id, domElement, type);
storeEntry(idValue, domElement, type);
}
private void addEntry(DomElement domElement,
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 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.RelatedItemLineMarkerInfo;
@@ -80,15 +80,13 @@ final class LineMarkerInfoHelper {
static RelatedItemLineMarkerInfo<?> createActionLineMarkerInfo(List<? extends ActionCandidate> targets, PsiElement element) {
return createPluginLineMarkerInfo(targets, element,
DevKitBundle.message("gutter.related.navigation.choose.action"),
(NullableFunction<Action, String>)action ->
ObjectUtils.chooseNotNull(action.getId().getStringValue(), action.getClazz().getStringValue()));
(NullableFunction<Action, String>)action -> action.getEffectiveId());
}
static RelatedItemLineMarkerInfo<?> createActionGroupLineMarkerInfo(List<? extends ActionCandidate> targets, PsiElement element) {
return createPluginLineMarkerInfo(targets, element,
DevKitBundle.message("gutter.related.navigation.choose.action.group"),
(NullableFunction<Group, String>)group ->
ObjectUtils.chooseNotNull(group.getId().getStringValue(), group.getClazz().getStringValue()));
(NullableFunction<Group, String>)group -> group.getEffectiveId());
}
static RelatedItemLineMarkerInfo<?> createComponentLineMarkerInfo(List<? extends ComponentCandidate> targets, PsiElement element) {
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 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.structure;
import com.intellij.icons.AllIcons;
@@ -63,8 +63,8 @@ public final class PluginDescriptorStructureUtil {
return safeGetTagDisplayText(tag);
}
if (element instanceof Action) {
String actionId = ((Action)element).getId().getStringValue();
if (element instanceof Action action) {
String actionId = action.getEffectiveId();
if (StringUtil.isNotEmpty(actionId)) {
return actionId;
}
@@ -196,7 +196,7 @@ public final class PluginDescriptorStructureUtil {
}
private static @Nullable String getGroupLocation(ActionOrGroup element) {
return element.getId().getStringValue();
return element.getEffectiveId();
}
private static @Nullable String getAddToGroupLocation(AddToGroup element) {
@@ -19,10 +19,7 @@ import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.ProjectIconsAccessor;
import com.intellij.util.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomTarget;
import com.intellij.util.xml.ElementPresentationManager;
import com.intellij.util.xml.GenericAttributeValue;
import com.intellij.util.xml.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.devkit.DevKitBundle;
@@ -101,7 +98,7 @@ public final class ActionOrGroupIdReference extends PsiPolyVariantReferenceBase<
}
final List<PsiElement> psiElements =
ContainerUtil.mapNotNull(processor.getResults(), actionOrGroup -> getDomTargetPsi(actionOrGroup));
ContainerUtil.mapNotNull(processor.getResults(), actionOrGroup -> getActionOrGroupDomTargetPsi(actionOrGroup));
return PsiElementResolveResult.createResults(psiElements);
}
@@ -172,8 +169,8 @@ public final class ActionOrGroupIdReference extends PsiPolyVariantReferenceBase<
final GlobalSearchScope domSearchScope = PluginRelatedLocatorsUtils.getCandidatesScope(project);
IdeaPluginRegistrationIndex.processAllActionOrGroup(project, domSearchScope, actionOrGroup -> {
if (isRelevantForVariant(actionOrGroup)) {
PsiElement psiElement = getDomTargetPsi(actionOrGroup);
String name = StringUtil.notNullize(actionOrGroup.getId().getStringValue(),
PsiElement psiElement = getActionOrGroupDomTargetPsi(actionOrGroup);
String name = StringUtil.notNullize(actionOrGroup.getEffectiveId(),
DevKitBundle.message("plugin.xml.convert.action.or.group.invalid.name"));
LookupElementBuilder builder = LookupElementBuilder.create(psiElement, name)
.withRenderer(ActionOrGroupLookupRenderer.INSTANCE);
@@ -245,6 +242,12 @@ public final class ActionOrGroupIdReference extends PsiPolyVariantReferenceBase<
};
}
private static PsiElement getActionOrGroupDomTargetPsi(ActionOrGroup actionOrGroup) {
DomTarget target = DomTarget.getTarget(actionOrGroup, actionOrGroup.getEffectiveIdAttribute());
assert target != null;
return PomService.convertToPsi(target);
}
private static PsiElement getDomTargetPsi(DomElement domElement) {
DomTarget target = DomTarget.getTarget(domElement);
assert target != null;
@@ -0,0 +1,33 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.references
import com.intellij.openapi.application.QueryExecutorBase
import com.intellij.psi.PsiReference
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.psi.xml.XmlAttributeValue
import com.intellij.util.Processor
import com.intellij.util.xml.DomUtil
import org.jetbrains.idea.devkit.dom.Action
/**
* Search for implicit [Action] `id` if not specified.
* Reference for [Action.getEffectiveId] to [Action.getClazz] (short class name).
*
* @see DevKitRelatedPropertiesProvider
*/
internal class ActionWithoutIdReferencesQueryExecutor : QueryExecutorBase<PsiReference, ReferencesSearch.SearchParameters>() {
override fun processQuery(queryParameters: ReferencesSearch.SearchParameters, consumer: Processor<in PsiReference>) {
val elementToSearch = queryParameters.elementToSearch
if (elementToSearch !is XmlAttributeValue) return
if (elementToSearch.hostName != "class") return
val domElement = DomUtil.getDomElement(elementToSearch) ?: return
val action = DomUtil.getParentOfType(domElement, Action::class.java, true) ?: return
if (DomUtil.hasXml(action.id)) return
val effectiveId = action.effectiveId ?: return
queryParameters.optimizer.searchWord(effectiveId, queryParameters.effectiveSearchScope, true, elementToSearch)
}
}
@@ -45,6 +45,7 @@ import java.util.Collections;
* </p>
*
* @see MessageBundleReferenceContributor
* @see ActionWithoutIdReferencesQueryExecutor
*/
final class DevKitRelatedPropertiesProvider extends DevkitRelatedLineMarkerProviderBase {
@@ -72,7 +73,7 @@ final class DevKitRelatedPropertiesProvider extends DevkitRelatedLineMarkerProvi
DomElement domElement = DomUtil.getDomElement(leaf);
if (domElement instanceof ActionOrGroup actionOrGroup) {
createLineMarker(leaf, result, domElement, actionOrGroup.getId());
createLineMarker(leaf, result, domElement, actionOrGroup.getEffectiveIdAttribute());
}
else if (domElement instanceof OverrideText overrideText) {
createLineMarker(leaf, result, domElement, overrideText.getPlace());
@@ -90,10 +91,10 @@ final class DevKitRelatedPropertiesProvider extends DevkitRelatedLineMarkerProvi
final XmlElement valueXmlElement = DomUtil.getValueElement(referenceElement);
if (valueXmlElement == null) return;
PropertiesFile file = DescriptorI18nUtil.findBundlePropertiesFile(domElement);
if (file == null) return;
PropertiesFile propertiesFile = DescriptorI18nUtil.findBundlePropertiesFile(domElement);
if (propertiesFile == null) return;
final Query<PsiReference> query = ReferencesSearch.search(valueXmlElement, new LocalSearchScope(file.getContainingFile()));
final Query<PsiReference> query = ReferencesSearch.search(valueXmlElement, new LocalSearchScope(propertiesFile.getContainingFile()));
if (query.findFirst() == null) return;
result.add(
@@ -5,6 +5,7 @@
<action id="actionId" class="foo.bar.BarAction" text="ActionId Text" description="ActionId description"/>
<action id="actionId.localized" class="foo.bar.BarAction"/>
<action id="actionId.missing.localized" class="foo.bar.BarAction"/>
<action class="foo.bar.BarAction"/>
<action class="foo.bar.BarAction" use-shortcut-of="<caret>"/>
</actions>
@@ -1,2 +1,4 @@
action.actionId.localized.text=_Action Localized Text
action.actionId.localized.description=Action localized description
action.BarAction.text=Action without ID Localized Text
action.BarAction.description=Action without ID localized description
@@ -65,6 +65,6 @@
<action class="com.jetbrains.test.SomeAction1Class" id="SomeAction1" icon="AllIcons.Actions.Back"/>
<add-to-group group-id="MainMenu"/>
</group>
<action class="com.jetbrains.test.SomeAction2Class" id="SomeAction2"/>
<action class="com.jetbrains.test.SomeAction2Class"/>
</actions>
</idea-plugin>
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.codeInsight;
import com.intellij.codeInsight.TargetElementUtil;
@@ -748,6 +748,7 @@ public class PluginXmlFunctionalTest extends JavaCodeInsightFixtureTestCase {
LookupElement[] lookupElements = myFixture.getLookupElements();
assertLookupElement(lookupElements, "actionId", " \"ActionId Text\"", "ActionId description");
assertLookupElement(lookupElements, "actionId.localized", " \"Action Localized Text\"", "Action localized description");
assertLookupElement(lookupElements, "BarAction", " \"Action without ID Localized Text\"", "Action without ID localized description");
assertLookupElement(lookupElements, "actionId.missing.localized", null, null);
}
@@ -1,4 +1,4 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 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.structure;
import com.intellij.icons.AllIcons;
@@ -117,7 +117,7 @@ public class PluginDescriptorStructureTest extends JavaCodeInsightFixtureTestCas
TreeElement groupNode = actionNodes[0];
assertEquals("Group", groupNode.getPresentation().getPresentableText());
assertEquals("MyPlugin.MyGroup", groupNode.getPresentation().getLocationString());
assertEquals("SomeAction2", actionNodes[1].getPresentation().getPresentableText());
assertEquals("SomeAction2Class", actionNodes[1].getPresentation().getPresentableText());
assertEquals("SomeAction2Class", actionNodes[1].getPresentation().getLocationString());
TreeElement[] inGroupNodes = groupNode.getChildren();
@@ -106,6 +106,7 @@ class KtActionReferenceTest : JavaCodeInsightFixtureTestCase() {
myFixture.createFile("plugin.xml", pluginXmlActions("""
<group id="myGroup"></group>
<action id="myAction" class="foo.bar.BarAction"></action>
<action class="foo.bar.myActionWithoutExplicitId"/>
"""
));
myFixture.configureByText("Caller.kt", """
@@ -115,7 +116,7 @@ class KtActionReferenceTest : JavaCodeInsightFixtureTestCase() {
}
""".trimIndent())
assertSameElements(myFixture.getCompletionVariants("Caller.kt").orEmpty(), "myAction", "myGroup")
assertSameElements(myFixture.getCompletionVariants("Caller.kt").orEmpty(), "myAction", "myGroup", "myActionWithoutExplicitId")
}
fun testActionReferenceHighlighting() {
@@ -1,4 +1,4 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.i18n;
import com.intellij.codeInsight.intention.impl.config.IntentionManagerImpl;
@@ -180,9 +180,6 @@ final class PluginXmlI18nInspection extends DevKitPluginXmlInspectionBase {
}
private static void highlightActionOrGroup(@NotNull DomElementAnnotationHolder holder, @NotNull ActionOrGroup actionOrGroup) {
String id = actionOrGroup.getId().getStringValue();
if (id == null) return;
String text = actionOrGroup.getText().getStringValue();
String desc = actionOrGroup.getDescription().getStringValue();
if (text == null && desc == null) return;
@@ -540,6 +537,13 @@ final class PluginXmlI18nInspection extends DevKitPluginXmlInspectionBase {
}
}
@Nullable
private static String getEffectiveActionOrGroupId(@Nullable XmlTag tag) {
ActionOrGroup actionOrGroup = DomUtil.findDomElement(tag, ActionOrGroup.class);
assert actionOrGroup != null;
return actionOrGroup.getEffectiveId();
}
private static void extractTextAndDescription(@NotNull Project project,
Collection<XmlTag> tags,
PropertiesFile propertiesFile,
@@ -552,10 +556,10 @@ final class PluginXmlI18nInspection extends DevKitPluginXmlInspectionBase {
String id;
if (tag.getName().equals("override-text")) {
id = Objects.requireNonNull(tag.getParentTag()).getAttributeValue("id") + "." + tag.getAttributeValue("place");
id = getEffectiveActionOrGroupId(tag.getParentTag()) + "." + tag.getAttributeValue("place");
}
else {
id = tag.getAttributeValue("id");
id = getEffectiveActionOrGroupId(tag);
}
List<PropertiesFile> propertiesFiles = Collections.singletonList(propertiesFile);
@@ -6,6 +6,9 @@ action.BundleActionWithoutDescription.text=My Text
action.BundleActionWrongCasing.text=lower case text
action.BundleActionWrongCasing.description=lower case description
action.MyActionWithoutId.text=action without id lower case text
action.MyActionWithoutId.description=action without id lower case description
group.BundleGroup.text=Bundle Group
group.BundleGroup.description=Bundle group description
@@ -32,6 +32,8 @@
<<warning descr="String 'lower case description' is not properly capitalized. It should have sentence capitalization"><warning descr="String 'lower case text' is not properly capitalized. It should have title capitalization">action</warning></warning> id="BundleActionWrongCasing"/>
<action id="BundleActionWithConstructor" class="MyAction"/>
<<warning descr="String 'action without id lower case description' is not properly capitalized. It should have sentence capitalization"><warning descr="String 'action without id lower case text' is not properly capitalized. It should have title capitalization">action</warning></warning> class="MyActionWithoutId"/>
<group id="BundleGroupWithoutAnyKeysButItsNotRequired"/>
<group id="BundleGroup"/>
@@ -3,8 +3,7 @@
<!-- Action ======================= -->
<!-- skip if 'id' not specified -->
<action description="Not localized"
<<warning descr="Extract text/description for i18n">action</warning> description="Not localized"
text="Not Localized"
class="foo.bar.BarAction"/>
@@ -23,7 +22,7 @@
<!-- Group ======================= -->
<!-- skip if 'id' not specified -->
<group description="Not localized"
<<warning descr="Extract text/description for i18n">group</warning> description="Not localized"
text="Not Localized"/>
<!-- skip if internal -->