[devkit] Resolve action IDs for Activate$ToolwindowID$ToolWindow (IJPL-165055)

GitOrigin-RevId: 44ad796d26ce253134675264e169af55bbc6dc1b
This commit is contained in:
Yann Cébron
2025-01-16 17:48:29 +00:00
committed by intellij-monorepo-bot
parent aa4640209b
commit 518f9c1879
10 changed files with 308 additions and 89 deletions
@@ -36,6 +36,8 @@ import org.jetbrains.idea.devkit.util.PluginRelatedLocatorsUtils;
import java.util.*;
import static com.intellij.openapi.util.NullableLazyValue.lazyNullable;
import static org.jetbrains.idea.devkit.references.ActionOrGroupIdResolveUtil.ACTIVATE_TOOLWINDOW_ACTION_PREFIX;
import static org.jetbrains.idea.devkit.references.ActionOrGroupIdResolveUtil.ACTIVATE_TOOLWINDOW_ACTION_SUFFIX;
public class ActionOrGroupResolveConverter extends ResolvingConverter<ActionOrGroup> {
@@ -48,13 +50,25 @@ public class ActionOrGroupResolveConverter extends ResolvingConverter<ActionOrGr
public @NotNull Set<String> getAdditionalVariants(@NotNull ConvertContext context) {
if (!isActionsAllowed()) return Collections.emptySet();
// add executor IDs here as valid results
Set<String> executorIds = new HashSet<>();
ActionOrGroupIdResolveUtil.processExecutors(context.getProject(), (id, psiClass) -> {
executorIds.add(id);
// add executor/activate toolwindow IDs here as valid results
Project project = context.getProject();
Set<String> additionalIds = new HashSet<>();
ActionOrGroupIdResolveUtil.processExecutors(project, (id, psiClass) -> {
additionalIds.add(id);
return true;
});
return executorIds;
ActionOrGroupIdResolveUtil.processActivateToolWindowActions(project, extension -> {
additionalIds.add(ACTIVATE_TOOLWINDOW_ACTION_PREFIX +
ActionOrGroupIdResolveUtil.getToolWindowIdValue(extension) +
ACTIVATE_TOOLWINDOW_ACTION_SUFFIX);
return true;
});
ActionOrGroupIdResolveUtil.processToolWindowId(project, (id, field) -> {
additionalIds.add(ACTIVATE_TOOLWINDOW_ACTION_PREFIX + id + ACTIVATE_TOOLWINDOW_ACTION_SUFFIX);
return true;
});
return additionalIds;
}
@ApiStatus.Internal
@@ -4,8 +4,10 @@ package org.jetbrains.idea.devkit.references;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.execution.Executor;
import com.intellij.ide.actions.QualifiedNameProviderUtil;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
@@ -19,15 +21,18 @@ import com.intellij.util.PairProcessor;
import com.intellij.util.SmartList;
import com.intellij.util.ThreeState;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomTarget;
import com.intellij.util.xml.GenericAttributeValue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.devkit.DevKitBundle;
import org.jetbrains.idea.devkit.dom.ActionOrGroup;
import org.jetbrains.idea.devkit.dom.Extension;
import org.jetbrains.idea.devkit.dom.OverrideText;
import org.jetbrains.idea.devkit.dom.impl.ActionOrGroupResolveConverter;
import org.jetbrains.idea.devkit.dom.index.IdeaPluginRegistrationIndex;
import org.jetbrains.idea.devkit.util.DevKitDomUtil;
import org.jetbrains.idea.devkit.util.PluginRelatedLocatorsUtils;
import org.jetbrains.uast.UExpression;
@@ -35,6 +40,9 @@ import javax.swing.*;
import java.util.List;
import java.util.Objects;
import static org.jetbrains.idea.devkit.references.ActionOrGroupIdResolveUtil.ACTIVATE_TOOLWINDOW_ACTION_PREFIX;
import static org.jetbrains.idea.devkit.references.ActionOrGroupIdResolveUtil.ACTIVATE_TOOLWINDOW_ACTION_SUFFIX;
final class ActionOrGroupIdReference extends PsiPolyVariantReferenceBase<PsiElement> implements PluginConfigReference {
private final String myId;
@@ -64,10 +72,44 @@ final class ActionOrGroupIdReference extends PsiPolyVariantReferenceBase<PsiElem
}
return true;
};
ActionOrGroupIdResolveUtil.processExecutors(getElement().getProject(), pairProcessor);
ActionOrGroupIdResolveUtil.processExecutors(project, pairProcessor);
if (!executor.isNull()) {
return PsiElementResolveResult.createResults(executor.get());
}
if (myId.startsWith(ACTIVATE_TOOLWINDOW_ACTION_PREFIX) && myId.endsWith(ACTIVATE_TOOLWINDOW_ACTION_SUFFIX)) {
String toolwindowId = StringUtil.substringBeforeLast(
Objects.requireNonNull(StringUtil.substringAfter(myId, ACTIVATE_TOOLWINDOW_ACTION_PREFIX)),
ACTIVATE_TOOLWINDOW_ACTION_SUFFIX);
// ToolWindow EPs - process all as we need to remove spaces from ID
Ref<Extension> toolwindowExtension = Ref.create();
ActionOrGroupIdResolveUtil.processActivateToolWindowActions(project, extension -> {
String idValue = ActionOrGroupIdResolveUtil.getToolWindowIdValue(extension);
if (Comparing.strEqual(toolwindowId, idValue)) {
toolwindowExtension.set(extension);
return false;
}
return true;
});
if (!toolwindowExtension.isNull()) {
return PsiElementResolveResult.createResults(getDomTargetPsi(toolwindowExtension.get()));
}
// known (programmatic) ToolWindow IDs
Ref<PsiField> knownToolWindowIdField = Ref.create();
ActionOrGroupIdResolveUtil.processToolWindowId(project, (s, field) -> {
if (Comparing.strEqual(s, toolwindowId)) {
knownToolWindowIdField.set(field);
return false;
}
return true;
});
if (!knownToolWindowIdField.isNull()) {
return PsiElementResolveResult.createResults(knownToolWindowIdField.get());
}
return ResolveResult.EMPTY_ARRAY;
}
}
// action|group.ActionId.<override-text@place>.text
@@ -92,11 +134,7 @@ final class ActionOrGroupIdReference extends PsiPolyVariantReferenceBase<PsiElem
}
final List<PsiElement> psiElements =
JBIterable.from(processor.getResults())
.map(actionOrGroup -> {
final DomTarget target = DomTarget.getTarget(actionOrGroup);
return target == null ? null : PomService.convertToPsi(project, target);
}).filter(Objects::nonNull).toList();
ContainerUtil.mapNotNull(processor.getResults(), actionOrGroup -> getDomTargetPsi(actionOrGroup));
return PsiElementResolveResult.createResults(psiElements);
}
@@ -113,24 +151,52 @@ final class ActionOrGroupIdReference extends PsiPolyVariantReferenceBase<PsiElem
converter = new ActionOrGroupResolveConverter();
}
List<ActionOrGroup> domVariants =
converter.getVariants(getElement().getProject(), ModuleUtilCore.findModuleForPsiElement(getElement()));
Project project = getElement().getProject();
List<ActionOrGroup> domVariants = converter.getVariants(project, ModuleUtilCore.findModuleForPsiElement(getElement()));
List<LookupElement> domLookupElements = ContainerUtil.map(domVariants, actionOrGroup -> converter.createLookupElement(actionOrGroup));
if (myIsAction == ThreeState.NO) {
return domLookupElements.toArray();
}
List<LookupElement> executorLookupElements = new SmartList<>();
ActionOrGroupIdResolveUtil.processExecutors(getElement().getProject(), (id, psiClass) -> {
ActionOrGroupIdResolveUtil.processExecutors(project, (id, psiClass) -> {
LookupElementBuilder builder = LookupElementBuilder.create(psiClass, id)
.bold()
.withIcon(computeIcon(id, psiClass))
.withTailText(computeTailText(id), true)
.withIcon(computeExecutorIcon(id, psiClass))
.withTailText(computeExecutorTailText(id), true)
.withTypeText(psiClass.getQualifiedName(), true);
executorLookupElements.add(builder);
return true;
});
return ContainerUtil.concat(domLookupElements, executorLookupElements).toArray();
List<LookupElement> activateToolWindowElements = new SmartList<>();
ActionOrGroupIdResolveUtil.processActivateToolWindowActions(project, extension -> {
GenericAttributeValue<?> factoryClass = DevKitDomUtil.getAttribute(extension, "factoryClass");
LookupElementBuilder builder =
LookupElementBuilder.create(getDomTargetPsi(extension),
ACTIVATE_TOOLWINDOW_ACTION_PREFIX +
ActionOrGroupIdResolveUtil.getToolWindowIdValue(extension) +
ACTIVATE_TOOLWINDOW_ACTION_SUFFIX)
.bold()
.withTypeText(factoryClass != null ? factoryClass.getStringValue() : "", true);
activateToolWindowElements.add(builder);
return true;
});
ActionOrGroupIdResolveUtil.processToolWindowId(project, (value, field) -> {
LookupElementBuilder builder =
LookupElementBuilder.create(field,
ACTIVATE_TOOLWINDOW_ACTION_PREFIX + value + ACTIVATE_TOOLWINDOW_ACTION_SUFFIX)
.bold()
.withStrikeoutness(field.isDeprecated())
.withTypeText(QualifiedNameProviderUtil.getQualifiedName(field), true);
activateToolWindowElements.add(builder);
return true;
});
return ContainerUtil.concat(domLookupElements, executorLookupElements, activateToolWindowElements).toArray();
}
@Override
@@ -151,7 +217,13 @@ final class ActionOrGroupIdReference extends PsiPolyVariantReferenceBase<PsiElem
}
}
private static Icon computeIcon(String id, PsiClass executor) {
private static PsiElement getDomTargetPsi(DomElement domElement) {
DomTarget target = DomTarget.getTarget(domElement);
assert target != null;
return PomService.convertToPsi(target);
}
private static Icon computeExecutorIcon(@NotNull String id, PsiClass executor) {
final ProjectIconsAccessor iconsAccessor = ProjectIconsAccessor.getInstance(executor.getProject());
final UExpression icon = ActionOrGroupIdResolveUtil.getReturnExpression(executor, "getIcon");
if (icon == null) {
@@ -162,7 +234,7 @@ final class ActionOrGroupIdReference extends PsiPolyVariantReferenceBase<PsiElem
return iconFile == null ? null : iconsAccessor.getIcon(iconFile);
}
private static String computeTailText(@NotNull String id) {
private static String computeExecutorTailText(@NotNull String id) {
Executor executorById = findExecutor(id);
return executorById != null ? " \"" + executorById.getActionName() + "\"" : "";
}
@@ -5,16 +5,28 @@ import com.intellij.execution.Executor
import com.intellij.openapi.project.Project
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiField
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.GlobalSearchScopesCore.projectProductionScope
import com.intellij.psi.search.ProjectScope.getLibrariesScope
import com.intellij.psi.search.searches.ClassInheritorsSearch
import com.intellij.util.PairProcessor
import com.intellij.util.Processor
import org.jetbrains.annotations.NonNls
import org.jetbrains.idea.devkit.dom.Extension
import org.jetbrains.idea.devkit.util.PsiUtil
import org.jetbrains.idea.devkit.util.processExtensionCandidates
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UField
import org.jetbrains.uast.evaluateString
import org.jetbrains.uast.toUElement
internal object ActionOrGroupIdResolveUtil {
const val ACTIVATE_TOOLWINDOW_ACTION_PREFIX: @NonNls String = "Activate"
const val ACTIVATE_TOOLWINDOW_ACTION_SUFFIX: @NonNls String = "ToolWindow"
@JvmStatic
fun processExecutors(project: Project, processor: PairProcessor<String, PsiClass>) {
// not necessarily in the element's resolve scope
@@ -40,6 +52,29 @@ internal object ActionOrGroupIdResolveUtil {
}
}
@JvmStatic
fun processActivateToolWindowActions(project: Project, processor: Processor<Extension>) {
processExtensionCandidates(project, "com.intellij.toolWindow", processor, null, null)
}
@JvmStatic
fun getToolWindowIdValue(extension : Extension) : String? {
return extension.id.stringValue?.replace(" ", "")
}
@JvmStatic
fun processToolWindowId(project: Project, processor: PairProcessor<String, PsiField>) {
val toolWindowIdClass =
JavaPsiFacade.getInstance(project).findClass("com.intellij.openapi.wm.ToolWindowId", GlobalSearchScope.allScope(project))
if (toolWindowIdClass == null) return
for (field in toolWindowIdClass.getFields()) {
val initializer = field.toUElement<UField>(UField::class.java)?.uastInitializer ?: continue
val value = initializer.evaluateString() ?: continue
if (!processor.process(value, field)) return
}
}
private fun computeConstantReturnValue(
psiClass: PsiClass,
methodName: String,
@@ -1,18 +1,14 @@
// 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.references;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.references.PomService;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReferenceBase;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.CommonProcessors;
import com.intellij.util.Processor;
import com.intellij.util.xml.DomElement;
import com.intellij.util.xml.DomManager;
import com.intellij.util.xml.DomTarget;
import com.intellij.util.xml.GenericAttributeValue;
import com.intellij.util.xml.reflect.DomAttributeChildDescription;
@@ -20,12 +16,7 @@ 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 org.jetbrains.idea.devkit.dom.index.ExtensionPointIndex;
import org.jetbrains.idea.devkit.util.ExtensionCandidate;
import org.jetbrains.idea.devkit.util.ExtensionLocatorKt;
import org.jetbrains.idea.devkit.util.PluginRelatedLocatorsUtils;
import java.util.List;
abstract class ExtensionReferenceBase extends PsiReferenceBase<PsiElement> implements PluginConfigReference {
@@ -57,7 +48,8 @@ abstract class ExtensionReferenceBase extends PsiReferenceBase<PsiElement> imple
if (StringUtil.isEmptyOrSpaces(resolveId)) return null;
final CommonProcessors.FindProcessor<Extension> resolveProcessor = new CommonProcessors.FindFirstProcessor<>();
processCandidates(resolveProcessor, resolveId);
ExtensionLocatorKt.processExtensionCandidates(myElement.getProject(), getExtensionPointFqn(), resolveProcessor,
resolveId, extension -> getNameElement(extension));
final Extension value = resolveProcessor.getFoundValue();
if (value == null) return null;
@@ -85,40 +77,7 @@ abstract class ExtensionReferenceBase extends PsiReferenceBase<PsiElement> imple
}
protected void processCandidates(Processor<? super Extension> processor) {
processCandidates(processor, null);
}
/**
* @param extensionPointId To locate a specific extension instance by ID or {@code null} to process all.
*/
private void processCandidates(Processor<? super Extension> processor,
@Nullable String extensionPointId) {
final Project project = myElement.getProject();
final ExtensionPoint extensionPointDomElement =
ExtensionPointIndex.findExtensionPoint(project, PluginRelatedLocatorsUtils.getCandidatesScope(project), getExtensionPointFqn());
if (extensionPointDomElement == null) return;
final List<ExtensionCandidate> candidates;
if (extensionPointId == null) {
candidates = ExtensionLocatorKt.locateExtensionsByExtensionPoint(extensionPointDomElement);
}
else {
candidates = ExtensionLocatorKt.
locateExtensionsByExtensionPointAndId(extensionPointDomElement, extensionPointId,
extension -> {
final GenericAttributeValue<String> nameElement = getNameElement(extension);
return nameElement != null ? nameElement.getStringValue() : null;
}).findCandidates();
}
final DomManager manager = DomManager.getDomManager(project);
for (ExtensionCandidate candidate : candidates) {
final XmlTag element = candidate.pointer.getElement();
final DomElement domElement = manager.getDomElement(element);
if (domElement instanceof Extension) {
if (!processor.process((Extension)domElement)) return;
}
}
ExtensionLocatorKt.processExtensionCandidates(myElement.getProject(), getExtensionPointFqn(), processor,
null, null);
}
}
@@ -3,21 +3,23 @@ package org.jetbrains.idea.devkit.util
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil
import com.intellij.platform.ide.progress.ModalTaskOwner.project
import com.intellij.psi.PsiClass
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.PsiSearchHelper
import com.intellij.psi.search.SearchScope
import com.intellij.psi.search.UsageSearchContext
import com.intellij.psi.util.ClassUtil
import com.intellij.psi.xml.XmlTag
import com.intellij.util.PerformanceAssertions
import com.intellij.util.Processor
import com.intellij.util.SmartList
import com.intellij.util.xml.DomManager
import com.intellij.util.xml.GenericAttributeValue
import org.jetbrains.idea.devkit.dom.Extension
import org.jetbrains.idea.devkit.dom.ExtensionPoint
import org.jetbrains.idea.devkit.dom.index.ExtensionPointIndex
import java.util.*
import java.util.function.Function
fun locateExtensionsByPsiClass(psiClass: PsiClass): List<ExtensionCandidate> {
return findExtensionsByClassName(psiClass.project, ClassUtil.getJVMClassName(psiClass) ?: return emptyList())
@@ -34,22 +36,62 @@ fun locateExtensionsByExtensionPointAndId(extensionPoint: ExtensionPoint, extens
/**
* @param extensionIdFunction in case EP is located via custom attribute instead of [Extension.getId].
*/
fun locateExtensionsByExtensionPointAndId(extensionPoint: ExtensionPoint,
extensionId: String,
extensionIdFunction: (Extension) -> String?): ExtensionLocator {
fun locateExtensionsByExtensionPointAndId(
extensionPoint: ExtensionPoint,
extensionId: String,
extensionIdFunction: (Extension) -> String?,
): ExtensionLocator {
return ExtensionByExtensionPointLocator(extensionPoint.xmlTag.project, extensionPoint, extensionId, extensionIdFunction)
}
/**
* @param extensionPointId To locate a specific extension instance by ID or `null` to process all.
* @param nameElementFunction Returns the DOM `id` element when [extensionPointId] is passed.
*/
fun processExtensionCandidates(
project: Project,
extensionPointFqn: String,
processor: Processor<in Extension>,
extensionPointId: String?,
nameElementFunction: Function<Extension, GenericAttributeValue<String>?>?,
) {
val extensionPointDomElement =
ExtensionPointIndex.findExtensionPoint(project, PluginRelatedLocatorsUtils.getCandidatesScope(project), extensionPointFqn)
if (extensionPointDomElement == null) return
val candidates: List<ExtensionCandidate>
if (extensionPointId == null) {
candidates = locateExtensionsByExtensionPoint(extensionPointDomElement)
}
else {
candidates = locateExtensionsByExtensionPointAndId(extensionPointDomElement, extensionPointId) { extension: Extension ->
val nameElement = nameElementFunction!!.apply(extension)
nameElement?.getStringValue()
}.findCandidates()
}
val manager = DomManager.getDomManager(project)
for (candidate in candidates) {
val element = candidate.pointer.getElement()
val domElement = manager.getDomElement(element)
if (domElement is Extension) {
if (!processor.process(domElement)) return
}
}
}
// TODO consider converting to a stream-like entity to avoid IDEA-277738, EA-139648, etc.
/**
* A synchronized collection should be used as an accumulator in callbacks.
*/
@JvmOverloads
fun processExtensionDeclarations(name: String,
project: Project,
strictMatch: Boolean = true,
scope : SearchScope = PluginRelatedLocatorsUtils.getCandidatesScope(project),
callback: (Extension, XmlTag) -> Boolean) {
fun processExtensionDeclarations(
name: String,
project: Project,
strictMatch: Boolean = true,
scope: SearchScope = PluginRelatedLocatorsUtils.getCandidatesScope(project),
callback: (Extension, XmlTag) -> Boolean,
) {
PerformanceAssertions.assertDoesNotAffectHighlighting()
val searchWord = name.substringBeforeLast('$')
@@ -82,18 +124,22 @@ private fun findExtensionsByClassName(project: Project, className: String): List
return result
}
internal inline fun processExtensionsByClassName(project: Project,
className: String,
crossinline processor: (XmlTag, ExtensionPoint) -> Boolean) {
internal inline fun processExtensionsByClassName(
project: Project,
className: String,
crossinline processor: (XmlTag, ExtensionPoint) -> Boolean,
) {
processExtensionDeclarations(className, project) { extension, tag ->
extension.extensionPoint?.let { processor(tag, it) } ?: true
}
}
internal class ExtensionByExtensionPointLocator(private val project: Project,
extensionPoint: ExtensionPoint,
private val extensionId: String?,
private val extensionIdFunction: (Extension) -> String? = { extension -> extension.id.stringValue }) : ExtensionLocator() {
private class ExtensionByExtensionPointLocator(
private val project: Project,
extensionPoint: ExtensionPoint,
private val extensionId: String?,
private val extensionIdFunction: (Extension) -> String? = { extension -> extension.id.stringValue },
) : ExtensionLocator() {
private val pointQualifiedName = extensionPoint.effectiveQualifiedName
private fun processCandidates(processor: (XmlTag) -> Boolean) {
@@ -114,4 +114,20 @@
<add-to-group group-id="<error descr="Cannot resolve group 'MyExecutorId'">MyExecutorId</error>"/>
</action>
</actions>
<!-- ToolWindow -->
<extensionPoints>
<extensionPoint qualifiedName="com.intellij.toolWindow" beanClass="java.lang.String"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<toolWindow id="ToolWindowId"/>
</extensions>
<actions>
<action class="foo.bar.BarAction" use-shortcut-of="ActivateToolWindowIdToolWindow"/>
<action class="foo.bar.BarAction" use-shortcut-of="ActivateToolWindowIdFromConstantsToolWindow"/>
<action class="foo.bar.BarAction">
<add-to-group group-id="<error descr="Cannot resolve group 'ActivateToolWindowIdToolWindow'">ActivateToolWindowIdToolWindow</error>"/>
</action>
</actions>
</idea-plugin>
@@ -58,10 +58,10 @@ public class PluginXmlDomInspectionActionHighlightingTest extends PluginXmlDomIn
// fake Executor
myFixture.addClass("""
package com.intellij.execution;
public abstract class Executor {
public abstract String getId();
public abstract String getContextActionId();
}
public abstract class Executor {
public abstract String getId();
public abstract String getContextActionId();
}
""");
myFixture.addClass("""
public class MyExecutor extends com.intellij.execution.Executor {
@@ -76,6 +76,13 @@ public class PluginXmlDomInspectionActionHighlightingTest extends PluginXmlDomIn
}
}
""");
// toolwindow ID
myFixture.addClass("""
package com.intellij.openapi.wm;
interface ToolWindowId {
String FAVORITES = "ToolWindowIdFromConstants";
}
""");
myFixture.addFileToProject("keymaps/MyKeymap.xml", "<keymap/>");
myFixture.testHighlighting("ActionComplexHighlighting.xml");
@@ -0,0 +1,17 @@
import com.intellij.openapi.actionSystem.ex.ActionUtil
class ActionReferenceToolWindowHighlighting {
fun testToolWindowExtensionHighlighting() {
ActionUtil.wrap("ActivateToolWindowIdToolWindow")
ActionUtil.wrap("ActivateToolWindowIdWithSpacesToolWindow")
ActionUtil.wrap("<error descr="Cannot resolve action id 'ActivateINVALID_VALUEToolWindow'">ActivateINVALID_VALUEToolWindow</error>")
ActionUtil.getActionGroup("<error descr="Cannot resolve group id 'ActivateToolWindowIdToolWindow'">ActivateToolWindowIdToolWindow</error>")
}
fun testToolWindowToolWindowIdHighlighting() {
ActionUtil.wrap("ActivateToolWindowIdFromConstantsToolWindow")
}
}
@@ -0,0 +1,7 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<toolWindow factoryClass="FactoryClass" id="ToolWindowId"/>
<toolWindow factoryClass="FactoryClassForSpaces" id="Tool Window Id With Spaces"/>
</extensions>
</idea-plugin>
@@ -1,6 +1,8 @@
// 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.kotlin.codeInsight
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.codeInspection.LocalInspectionEP
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.openapi.actionSystem.ActionManager
@@ -13,7 +15,6 @@ import com.intellij.ui.components.JBList
import com.intellij.util.PathUtil
import org.jetbrains.idea.devkit.inspections.UnresolvedPluginConfigReferenceInspection
import org.jetbrains.idea.devkit.kotlin.DevkitKtTestsUtil
import kotlin.jvm.java
@TestDataPath("\$CONTENT_ROOT/testData/codeInsight/actionReference")
class KtActionReferenceTest : JavaCodeInsightFixtureTestCase() {
@@ -128,6 +129,46 @@ class KtActionReferenceTest : JavaCodeInsightFixtureTestCase() {
myFixture.testHighlighting("ActionReferenceHighlighting.kt")
}
fun testActionReferenceToolWindowHighlighting() {
myFixture.enableInspections(UnresolvedPluginConfigReferenceInspection::class.java)
configureToolWindowTest()
myFixture.testHighlighting("ActionReferenceToolWindowHighlighting.kt")
}
fun testActionReferenceToolWindowCompletion() {
configureToolWindowTest()
myFixture.configureByText("Caller.kt", """
fun usage(actionManager: com.intellij.openapi.actionSystem.ActionManager){
actionManager.getAction("ActivateT<caret>")
}
""".trimIndent())
assertContainsElements(myFixture.getCompletionVariants("Caller.kt").orEmpty(),
"ActivateToolWindowIdToolWindow", "ActivateToolWindowIdWithSpacesToolWindow",
"ActivateToolWindowIdFromConstantsToolWindow", "ActivateToolWindowIdFromConstants_DeprecatedToolWindow")
val extension = getLookupElementPresentation("ActivateToolWindowIdToolWindow")
assertTrue(extension.isItemTextBold)
assertEquals("FactoryClass", extension.typeText)
val deprecated = getLookupElementPresentation("ActivateToolWindowIdFromConstants_DeprecatedToolWindow")
assertTrue(deprecated.isStrikeout)
assertEquals("com.intellij.openapi.wm.ToolWindowId#DEPRECATED", deprecated.typeText)
}
private fun configureToolWindowTest() {
myFixture.copyFileToProject("actionReferenceToolWindowHighlighting.xml")
myFixture.addClass("""
package com.intellij.openapi.wm;
interface ToolWindowId {
String FAVORITES = "ToolWindowIdFromConstants";
@Deprecated
String DEPRECATED = "ToolWindowIdFromConstants_Deprecated";
}
""".trimIndent())
}
fun testRenameGroup() {
myFixture.createFile("plugin.xml", pluginXmlActions("""
<group id="myGroup"></group>
@@ -151,4 +192,9 @@ class KtActionReferenceTest : JavaCodeInsightFixtureTestCase() {
private val DLR = '$'.toString()
}
private fun getLookupElementPresentation(lookupString: String): LookupElementPresentation {
val lookupElement: LookupElement? = myFixture.getLookupElements()!!.find({ element: LookupElement -> element.getLookupString() == lookupString })
assertNotNull(lookupString, lookupElement)
return LookupElementPresentation.renderElement(lookupElement)
}
}