From a77f2ccd60fadc475e2d4b1de7072181a3270af1 Mon Sep 17 00:00:00 2001 From: Mikhail Pyltsin Date: Fri, 20 Dec 2024 10:28:21 +0100 Subject: [PATCH] [java-completion] IDEA-359174 first draft of command completion - run gutters GitOrigin-RevId: cf7e3d0190149dd6ca9406d867bf8df6bc58e4bf --- .../resources/META-INF/JavaPlugin.xml | 1 + .../commands/api/CompletionCommand.kt | 90 +++++----- .../impl/AbstractActionCompletionCommand.kt | 2 + .../impl/DirectIntentionCommandProvider.kt | 10 +- .../commands/impl/GenerateCommandProvider.kt | 12 +- .../commands/impl/IntroduceVariableCommand.kt | 2 + .../impl/SimpleRunMarkerCommandProvider.kt | 155 ++++++++++++++++++ 7 files changed, 219 insertions(+), 53 deletions(-) create mode 100644 java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/SimpleRunMarkerCommandProvider.kt diff --git a/java/java-impl/resources/META-INF/JavaPlugin.xml b/java/java-impl/resources/META-INF/JavaPlugin.xml index 283abb0ed833..6b1758815cb8 100644 --- a/java/java-impl/resources/META-INF/JavaPlugin.xml +++ b/java/java-impl/resources/META-INF/JavaPlugin.xml @@ -1498,6 +1498,7 @@ + diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/commands/api/CompletionCommand.kt b/java/java-impl/src/com/intellij/codeInsight/completion/commands/api/CompletionCommand.kt index a20b94e7f587..eec3d9f02d56 100644 --- a/java/java-impl/src/com/intellij/codeInsight/completion/commands/api/CompletionCommand.kt +++ b/java/java-impl/src/com/intellij/codeInsight/completion/commands/api/CompletionCommand.kt @@ -100,49 +100,49 @@ abstract class ApplicableCompletionCommand : CompletionCommand() { */ @RequiresReadLock abstract fun isApplicable(offset: Int, psiFile: PsiFile, editor: Editor?): Boolean - - /** - * Constructs a [DataContext] instance using the specified parameters, including the PSI file, - * editor, and optionally a context element. The resulting data context is populated with - * relevant keys and values to be used within the context of the provided elements. - * - * @param psiFile The [PsiFile] associated with the current context. - * @param editor The [Editor] instance corresponding to the current context. - * @param context An optional [PsiElement] representing the context; can be null. - * @return A [DataContext] instance containing the provided context information. - */ - internal fun dataContext( - psiFile: PsiFile, - editor: Editor, - context: PsiElement?, - ): DataContext { - val dataContext = SimpleDataContext.builder() - .add(CommonDataKeys.PROJECT, psiFile.project) - .add(CommonDataKeys.EDITOR, editor) - .add(CommonDataKeys.PSI_ELEMENT, context) - .add(CommonDataKeys.PSI_FILE, psiFile) - .add(LangDataKeys.CONTEXT_LANGUAGES, arrayOf(psiFile.language)) - .build() - return dataContext - } - - /** - * Retrieves the PSI element located at the specified offset within the editor's context. - * This method attempts to find a target PSI element based on the editor's state and the offset, - * allowing interaction with the underlying code structure. - * - * @param offset The offset in the editor's document where the target element is to be located. - * @param editor The editor instance in which the method searches for the target element. - * @return The PSI element found at the specified offset, or null if no suitable element is found - * or if the index is not ready. - */ - internal fun getTargetContext(offset: Int, editor: Editor): PsiElement? { - try { - val util = TargetElementUtil.getInstance() - return util.findTargetElement(editor, util.getReferenceSearchFlags(), offset) - } - catch (_: IndexNotReadyException) { - return null - } - } } + +/** + * Constructs a [DataContext] instance using the specified parameters, including the PSI file, + * editor, and optionally a context element. The resulting data context is populated with + * relevant keys and values to be used within the context of the provided elements. + * + * @param psiFile The [PsiFile] associated with the current context. + * @param editor The [Editor] instance corresponding to the current context. + * @param context An optional [PsiElement] representing the context; can be null. + * @return A [DataContext] instance containing the provided context information. + */ +internal fun dataContext( + psiFile: PsiFile, + editor: Editor, + context: PsiElement?, +): DataContext { + val dataContext = SimpleDataContext.builder() + .add(CommonDataKeys.PROJECT, psiFile.project) + .add(CommonDataKeys.EDITOR, editor) + .add(CommonDataKeys.PSI_ELEMENT, context) + .add(CommonDataKeys.PSI_FILE, psiFile) + .add(LangDataKeys.CONTEXT_LANGUAGES, arrayOf(psiFile.language)) + .build() + return dataContext +} + +/** + * Retrieves the PSI element located at the specified offset within the editor's context. + * This method attempts to find a target PSI element based on the editor's state and the offset, + * allowing interaction with the underlying code structure. + * + * @param offset The offset in the editor's document where the target element is to be located. + * @param editor The editor instance in which the method searches for the target element. + * @return The PSI element found at the specified offset, or null if no suitable element is found + * or if the index is not ready. + */ +internal fun getTargetContext(offset: Int, editor: Editor): PsiElement? { + try { + val util = TargetElementUtil.getInstance() + return util.findTargetElement(editor, util.getReferenceSearchFlags(), offset) + } + catch (_: IndexNotReadyException) { + return null + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/AbstractActionCompletionCommand.kt b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/AbstractActionCompletionCommand.kt index fc7a1d098d48..c1500e9f4111 100644 --- a/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/AbstractActionCompletionCommand.kt +++ b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/AbstractActionCompletionCommand.kt @@ -2,6 +2,8 @@ package com.intellij.codeInsight.completion.commands.impl import com.intellij.codeInsight.completion.commands.api.ApplicableCompletionCommand +import com.intellij.codeInsight.completion.commands.api.dataContext +import com.intellij.codeInsight.completion.commands.api.getTargetContext import com.intellij.ide.DataManager import com.intellij.openapi.actionSystem.* import com.intellij.openapi.actionSystem.ex.ActionUtil diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/DirectIntentionCommandProvider.kt b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/DirectIntentionCommandProvider.kt index 23cd4442564b..88cddbf5dbb8 100644 --- a/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/DirectIntentionCommandProvider.kt +++ b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/DirectIntentionCommandProvider.kt @@ -11,6 +11,7 @@ import com.intellij.codeInsight.daemon.impl.HighlightInfo.IntentionActionDescrip import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo import com.intellij.codeInsight.daemon.impl.quickfix.ExpensivePsiIntentionAction import com.intellij.codeInsight.intention.EmptyIntentionAction +import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.IntentionManager import com.intellij.codeInsight.intention.impl.CachedIntentions import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler @@ -52,6 +53,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.job +import java.util.function.Predicate class DirectIntentionCommandProvider : CommandProvider { override fun getCommands( @@ -250,6 +252,7 @@ class DirectIntentionCommandProvider : CommandProvider { originalFile: PsiFile, ): Deferred> = async { return@async readAction { + val availableIntentions = IntentionManager.getInstance().getAvailableIntentions(mutableListOf(originalFile.language.id)) val actionsToShow = IntentionsInfo() for (action in availableIntentions) { @@ -260,12 +263,17 @@ class DirectIntentionCommandProvider : CommandProvider { val dumbService = DumbService.getInstance(originalFile.project) val intentionsCache = CachedIntentions(originalFile.project, psiFile, editor) var toRemove = mutableListOf() + val filter = Predicate { action: IntentionAction? -> + IntentionActionFilter.EXTENSION_POINT_NAME.extensionList.all { f: IntentionActionFilter -> action != null && f.accept(action, psiFile, editor.caretModel.offset) } + } + for (intention in actionsToShow.intentionsToShow) { try { ProgressManager.checkCanceled() if (!dumbService.isUsableInCurrentContext(intention) || !intention.action.isAvailable(originalFile.project, editor, psiFile) && - intention.action.familyName !in ("AI Actions…")) { + intention.action.familyName !in ("AI Actions…") || + !filter.test(intention.action)) { toRemove.add(intention) } } diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/GenerateCommandProvider.kt b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/GenerateCommandProvider.kt index fda8ffa3ac1d..543998f62710 100644 --- a/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/GenerateCommandProvider.kt +++ b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/GenerateCommandProvider.kt @@ -3,10 +3,11 @@ package com.intellij.codeInsight.completion.commands.impl import com.intellij.codeInsight.completion.commands.api.CommandProvider import com.intellij.codeInsight.completion.commands.api.CompletionCommand +import com.intellij.codeInsight.completion.commands.api.dataContext +import com.intellij.codeInsight.completion.commands.api.getTargetContext import com.intellij.codeInsight.generation.actions.BaseGenerateAction import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.openapi.actionSystem.* -import com.intellij.openapi.actionSystem.impl.SimpleDataContext import com.intellij.openapi.actionSystem.impl.Utils import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.DumbAware @@ -29,11 +30,8 @@ class GenerateCommandProvider : CommandProvider, DumbAware { ): List { if (InjectedLanguageManager.getInstance(psiFile.project).isInjectedFragment(psiFile)) return emptyList() val element = psiFile.findElementAt(offset) - val dataContext = SimpleDataContext.builder() - .add(CommonDataKeys.PROJECT, project) - .add(CommonDataKeys.EDITOR, editor) - .add(CommonDataKeys.PSI_ELEMENT, element) - .build() + val context = getTargetContext(offset, editor) + val dataContext = dataContext(psiFile, editor, context) val actionEvent = AnActionEvent.createEvent(dataContext, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null) actionEvent.updateSession = UpdateSession.EMPTY Utils.initUpdateSession(actionEvent) @@ -44,7 +42,7 @@ class GenerateCommandProvider : CommandProvider, DumbAware { val dumbService = DumbService.getInstance(project) val activeActions = from(session.expandedChildren(ActionManager.getInstance().getAction(IdeActions.GROUP_GENERATE) as ActionGroup)) .filter { dumbService.isUsableInCurrentContext(it) } - .filter { o: AnAction? -> o !is Separator && session.presentation(o!!).isEnabledAndVisible } + .filter { o: AnAction? -> o !is Separator && o != null && session.presentation(o).isEnabledAndVisible } for (action in activeActions) { if (action is BaseGenerateAction) { generateActions.add(action) diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/IntroduceVariableCommand.kt b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/IntroduceVariableCommand.kt index 12d6a9b2e3d2..4712ee505996 100644 --- a/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/IntroduceVariableCommand.kt +++ b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/IntroduceVariableCommand.kt @@ -3,6 +3,8 @@ package com.intellij.codeInsight.completion.commands.impl import com.intellij.analysis.AnalysisBundle.message import com.intellij.codeInsight.completion.commands.api.ApplicableCompletionCommand +import com.intellij.codeInsight.completion.commands.api.dataContext +import com.intellij.codeInsight.completion.commands.api.getTargetContext import com.intellij.icons.AllIcons import com.intellij.lang.ContextAwareActionHandler import com.intellij.openapi.actionSystem.ActionPlaces diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/SimpleRunMarkerCommandProvider.kt b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/SimpleRunMarkerCommandProvider.kt new file mode 100644 index 000000000000..fff0eb29fc08 --- /dev/null +++ b/java/java-impl/src/com/intellij/codeInsight/completion/commands/impl/SimpleRunMarkerCommandProvider.kt @@ -0,0 +1,155 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight.completion.commands.impl + +import com.intellij.codeInsight.completion.commands.api.CommandProvider +import com.intellij.codeInsight.completion.commands.api.CompletionCommand +import com.intellij.codeInsight.completion.commands.api.dataContext +import com.intellij.codeInsight.completion.commands.api.getTargetContext +import com.intellij.codeInsight.daemon.MergeableLineMarkerInfo +import com.intellij.codeInsight.daemon.impl.GutterIntentionAction +import com.intellij.codeInsight.daemon.impl.IntentionActionFilter +import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.codeInsight.intention.impl.CachedIntentions +import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler +import com.intellij.execution.lineMarker.LineMarkerActionWrapper +import com.intellij.execution.lineMarker.RunLineMarkerContributor.Info +import com.intellij.execution.lineMarker.RunLineMarkerProvider +import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.actionSystem.impl.Utils +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.keymap.KeymapUtil +import com.intellij.openapi.progress.runBlockingCancellable +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.PossiblyDumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Iconable +import com.intellij.openapi.util.NlsSafe +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.util.containers.JBIterable.from +import org.jetbrains.annotations.Nls +import java.util.function.Predicate +import javax.swing.Icon + +class SimpleRunMarkerCommandProvider : CommandProvider { + override fun getCommands( + project: Project, + editor: Editor, + offset: Int, + psiFile: PsiFile, + originalEditor: Editor, + originalOffset: Int, + originalFile: PsiFile, + ): List { + return runBlockingCancellable { + var (currentElement, collectedActions) = collectActions(psiFile, offset, project) + @Suppress("SENSELESS_COMPARISON") + if (currentElement == null) return@runBlockingCancellable emptyList() + val filter = Predicate { action: IntentionAction? -> + IntentionActionFilter.EXTENSION_POINT_NAME.extensionList.all { f: IntentionActionFilter? -> + f != null && action != null && f.accept(action, psiFile, offset) + } + } + val commands = mutableListOf() + val myGuttersRaw: List = collectedActions + val dumbService = DumbService.getInstance(project) + val group = DefaultActionGroup(ArrayList(LinkedHashSet(myGuttersRaw))) + val context = getTargetContext(offset, editor) + val dataContext = dataContext(psiFile, editor, context) + val actionEvent = AnActionEvent.createEvent(dataContext, null, ActionPlaces.UNKNOWN, ActionUiKind.NONE, null) + actionEvent.updateSession = UpdateSession.EMPTY + Utils.initUpdateSession(actionEvent) + + val session = actionEvent.updateSession + val activeActions = from(session.expandedChildren(group)) + .filter { dumbService.isUsableInCurrentContext(it) } + .filter { o: AnAction? -> o !is Separator && o != null && session.presentation(o).isEnabledAndVisible } + + for (action in activeActions) { + try { + if (action !is LineMarkerActionWrapper) continue + val delegate = action.delegate + val presentation: Presentation = action.templatePresentation.clone() + val runLineMarkerContributorInfo = Info(delegate) + val text = runLineMarkerContributorInfo.tooltipProvider.apply(currentElement) + if (text?.isEmpty() == true) continue + val intentionAction = GutterIntentionAction(action, 0, false) + intentionAction.updateFromPresentation(presentation) + if (!filter.test(intentionAction)) continue + val shortcutText = KeymapUtil.getFirstKeyboardShortcutText(intentionAction.action) + commands.add(RunMarkerCompletionCommand(currentElement.textRange.startOffset, text, intentionAction.getIcon(Iconable.ICON_FLAG_VISIBILITY), shortcutText)) + } + catch (_: Exception) { + + } + } + + return@runBlockingCancellable commands + } + } + + override fun getId(): String { + return "RunMarkerCommandProvider" + } +} + +private fun collectActions( + psiFile: PsiFile, + offset: Int, + project: Project, +): Pair> { + val runLineMarkerProvider = RunLineMarkerProvider() + var currentElement = psiFile.findElementAt(offset) + val dumbService = DumbService.getInstance(project) + var collectedActions = mutableListOf() + while (currentElement != null) { + for (child in currentElement.children) { + val lineMarkerInfo = runLineMarkerProvider.getLineMarkerInfo(child) + if (lineMarkerInfo is MergeableLineMarkerInfo) { + val r = lineMarkerInfo.createGutterRenderer() + val group: ActionGroup? = r.popupMenuActions + if (group == null) continue + val children = if (group is DefaultActionGroup) group.getChildren(ActionManager.getInstance()) else group.getChildren(null) + for (action in children) { + if (!dumbService.isUsableInCurrentContext(action)) continue + collectedActions.add(action) + } + } + } + if (collectedActions.isNotEmpty()) break + currentElement = currentElement.parent + } + return Pair(currentElement, collectedActions) +} + +private class RunMarkerCompletionCommand( + private val offsetElement: Int, + @NlsSafe override val name: String, + override val icon: Icon?, + override val additionalInfo: String?, +) : CompletionCommand(), PossiblyDumbAware { + override val i18nName: @Nls String + get() = "" + + override fun execute(offset: Int, psiFile: PsiFile, editor: Editor?) { + var (_, collectedActions) = collectActions(psiFile, offsetElement, psiFile.project) + val cachedIntentions = CachedIntentions(psiFile.project, psiFile, editor) + val intentionsInfo = ShowIntentionsPass.IntentionsInfo() + intentionsInfo.guttersToShow.addAll(collectedActions) + cachedIntentions.wrapAndUpdateActions(intentionsInfo, true) + cachedIntentions.wrapAndUpdateGutters() + var intentionAction: IntentionAction? = null + for (caching in cachedIntentions.gutters) { + if (caching.text == name) { + intentionAction = caching.action + break + } + } + if (intentionAction == null) return + if (editor == null) return + if (ShowIntentionActionsHandler.availableFor(psiFile, editor, offset, intentionAction)) { + ShowIntentionActionsHandler.chooseActionAndInvoke(psiFile, editor, intentionAction, name) + } + } +}