[java-completion] IDEA-359174 first draft of command completion

- run gutters

GitOrigin-RevId: cf7e3d0190149dd6ca9406d867bf8df6bc58e4bf
This commit is contained in:
Mikhail Pyltsin
2024-12-20 13:34:02 +00:00
committed by intellij-monorepo-bot
parent ec3e5d135c
commit a77f2ccd60
7 changed files with 219 additions and 53 deletions
@@ -1498,6 +1498,7 @@
<codeInsight.completion.command.provider implementation="com.intellij.codeInsight.completion.commands.impl.ExtensionPointCommandProvider" />
<codeInsight.completion.command.provider implementation="com.intellij.codeInsight.completion.commands.impl.DirectIntentionCommandProvider" order="last"/>
<codeInsight.completion.command.provider implementation="com.intellij.codeInsight.completion.commands.impl.GenerateCommandProvider" />
<codeInsight.completion.command.provider implementation="com.intellij.codeInsight.completion.commands.impl.SimpleRunMarkerCommandProvider" />
<codeInsight.completion.command implementation="com.intellij.codeInsight.completion.commands.impl.GoToDeclarationCompletionCommand" />
<codeInsight.completion.command implementation="com.intellij.codeInsight.completion.commands.impl.RenameActionCompletionCommand" />
<codeInsight.completion.command implementation="com.intellij.codeInsight.completion.commands.impl.OptimizeImportCompletionCommand" />
@@ -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
}
}
@@ -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
@@ -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<List<CompletionCommand>> = 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<IntentionActionDescriptor>()
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)
}
}
@@ -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<CompletionCommand> {
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)
@@ -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
@@ -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<CompletionCommand> {
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<CompletionCommand>()
val myGuttersRaw: List<AnAction> = collectedActions
val dumbService = DumbService.getInstance(project)
val group = DefaultActionGroup(ArrayList<AnAction?>(LinkedHashSet<AnAction?>(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<PsiElement?, MutableList<AnAction>> {
val runLineMarkerProvider = RunLineMarkerProvider()
var currentElement = psiFile.findElementAt(offset)
val dumbService = DumbService.getInstance(project)
var collectedActions = mutableListOf<AnAction>()
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)
}
}
}