[command-completion] IDEA-373902 Command completion. New actions: Safe Delete

GitOrigin-RevId: fe6389cad1553d577539b4800bed5bbaae1cd99f
This commit is contained in:
Mikhail Pyltsin
2025-08-04 19:35:36 +00:00
committed by intellij-monorepo-bot
parent 784c38c413
commit 786d15fc38
12 changed files with 554 additions and 4 deletions
@@ -1494,6 +1494,7 @@
<codeInsight.completion.command.provider language="JAVA" implementationClass="com.intellij.codeInsight.completion.commands.impl.JavaQuickDocumentationCompletionCommand" order="last"/>
<codeInsight.completion.command.provider language="JAVA" implementationClass="com.intellij.codeInsight.completion.commands.impl.JavaGenerateConstructorCompletionCommandProvider" order="last"/>
<codeInsight.completion.command.provider language="JAVA" implementationClass="com.intellij.codeInsight.completion.commands.impl.JavaSurroundWithCompletionCommandProvider" order="last"/>
<codeInsight.completion.command.provider language="JAVA" implementationClass="com.intellij.codeInsight.completion.commands.impl.JavaSafeDeleteCompletionCommandProvider" order="last"/>
<weigher implementationClass="com.intellij.codeInsight.completion.LoggerWeigher" key="completion" id="logger"/>
@@ -0,0 +1,44 @@
// Copyright 2000-2025 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.command.CompletionCommand
import com.intellij.codeInsight.completion.command.commands.AbstractSafeDeleteCompletionCommandProvider
import com.intellij.codeInsight.completion.command.commands.DirectInspectionFixCompletionCommand
import com.intellij.codeInsight.completion.command.getCommandContext
import com.intellij.codeInsight.completion.command.getTargetContext
import com.intellij.openapi.editor.Editor
import com.intellij.psi.*
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.elementType
public class JavaSafeDeleteCompletionCommandProvider : AbstractSafeDeleteCompletionCommandProvider() {
override fun findElement(offset: Int, psiFile: PsiFile, editor: Editor?): PsiElement? {
if (editor == null) return null
var element = getCommandContext(offset, psiFile) ?: return null
if (element is PsiWhiteSpace) element = PsiTreeUtil.skipWhitespacesBackward(element) ?: return null
if (element is PsiIdentifier && (element.parent is PsiMember || element.parent is PsiVariable)) {
return element
}
if (element.elementType == JavaTokenType.RBRACE) {
val parentElement = element.parent
if (parentElement is PsiMember) {
val targetElement = (if (parentElement is PsiNameIdentifierOwner) parentElement.nameIdentifier
else PsiTreeUtil.findChildOfType(parentElement, PsiIdentifier::class.java)) ?: return null
return targetElement
}
val grandparentElement = element.parent?.parent
if (grandparentElement is PsiMember) {
val targetElement = (if (grandparentElement is PsiNameIdentifierOwner) grandparentElement.nameIdentifier
else PsiTreeUtil.findChildOfType(grandparentElement, PsiIdentifier::class.java)) ?: return null
return targetElement
}
}
val targetContext = getTargetContext(offset, editor)
if (targetContext?.isWritable != true) return null
return element
}
override fun skipForHighlightCommand(command: CompletionCommand): Boolean {
return command is DirectInspectionFixCompletionCommand && command.inspectionId == "unused"
}
}
@@ -0,0 +1,152 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInsight.completion.commands
import com.intellij.codeInsight.completion.LightFixtureCompletionTestCase
import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.util.registry.Registry
import com.intellij.testFramework.NeedsIndex
@NeedsIndex.SmartMode(reason = "it requires highlighting")
class JavaCommandsCompletionSafeDeleteTest : LightFixtureCompletionTestCase() {
override fun setUp() {
super.setUp()
Registry.get("ide.completion.command.enabled").setValue(false, getTestRootDisposable())
Registry.get("ide.completion.command.force.enabled").setValue(true, getTestRootDisposable())
}
fun testSafeDeleteClassEnd() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A {
int a;
}.<caret>
class B {
public B(){
new A();
}
}
""".trimIndent())
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testSafeDeleteClassIdentifier() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A.<caret> {
int a;
}
class B {
public B(){
new A();
}
}
""".trimIndent())
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testSafeDeleteClassReference() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A {
int a;
}
class B {
public B(){
new A.<caret>();
}
}
""".trimIndent())
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testSafeDeleteMethodEnd() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A {
int a;
public void foo(){}.<caret>
}
class B {
public B(){
new A().foo();
}
}
""".trimIndent())
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testSafeDeleteMethodIdentifier() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A {
int a;
public void foo.<caret>(){}
}
class B {
public B(){
new A().foo();
}
}
""".trimIndent())
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testSafeDeleteMethodReference() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A {
int a;
public void foo(){}
}
class B {
public B(){
new A().foo.<caret>();
}
}
""".trimIndent())
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testSafeDeleteVariable() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A {
int a;
public void foo(){}
}
class B {
public B(){
A a.<caret> = new A().foo();
System.out.println(a);
}
}
""".trimIndent())
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testSafeDeleteVariableReference() {
myFixture.configureByText(JavaFileType.INSTANCE, """
class A {
int a;
public void foo(){}
}
class B {
public B(){
A a = new A().foo();
System.out.println(a.<caret>);
}
}
""".trimIndent())
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
}
@@ -267,6 +267,14 @@ c:com.intellij.codeInsight.actions.VcsFacade
- p:createCommand(com.intellij.codeInsight.completion.command.CommandCompletionProviderContext):com.intellij.codeInsight.completion.command.commands.ActionCompletionCommand
- a:findRenameOffset(I,com.intellij.psi.PsiFile):java.lang.Integer
- p:isApplicable(I,com.intellij.psi.PsiFile,com.intellij.openapi.editor.Editor):Z
*a:com.intellij.codeInsight.completion.command.commands.AbstractSafeDeleteCompletionCommandProvider
- com.intellij.codeInsight.completion.command.commands.ActionCommandProvider
- com.intellij.codeInsight.completion.command.commands.HighlightCommandSkipper
- <init>():V
- p:createCommand(com.intellij.codeInsight.completion.command.CommandCompletionProviderContext):com.intellij.codeInsight.completion.command.commands.ActionCompletionCommand
- a:findElement(I,com.intellij.psi.PsiFile,com.intellij.openapi.editor.Editor):com.intellij.psi.PsiElement
- p:isApplicable(I,com.intellij.psi.PsiFile,com.intellij.openapi.editor.Editor):Z
- a:skipForHighlightCommand(com.intellij.codeInsight.completion.command.CompletionCommand):Z
*a:com.intellij.codeInsight.completion.command.commands.AbstractShowUsagesActionCompletionCommandProvider
- com.intellij.codeInsight.completion.command.commands.ActionCommandProvider
- <init>():V
@@ -322,11 +330,23 @@ c:com.intellij.codeInsight.actions.VcsFacade
- getPriority():java.lang.Integer
- getSynonyms():java.util.List
- f:setActionId(java.lang.String):V
*f:com.intellij.codeInsight.completion.command.commands.DirectInspectionFixCompletionCommand
- com.intellij.codeInsight.completion.command.CompletionCommand
- <init>(java.lang.String,java.lang.String,java.lang.Integer,javax.swing.Icon,com.intellij.codeInsight.completion.command.HighlightInfoLookup,I,kotlin.jvm.functions.Function0):V
- execute(I,com.intellij.psi.PsiFile,com.intellij.openapi.editor.Editor):V
- getHighlightInfo():com.intellij.codeInsight.completion.command.HighlightInfoLookup
- getIcon():javax.swing.Icon
- f:getInspectionId():java.lang.String
- getPresentableName():java.lang.String
- getPreview():com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
- getPriority():java.lang.Integer
*:com.intellij.codeInsight.completion.command.commands.ErrorFixCommandProvider
- com.intellij.openapi.project.PossiblyDumbAware
- *sf:Companion:com.intellij.codeInsight.completion.command.commands.ErrorFixCommandProvider$Companion
- a:getCommands(com.intellij.psi.PsiFile,java.util.List,I):java.util.List
*f:com.intellij.codeInsight.completion.command.commands.ErrorFixCommandProvider$Companion
*:com.intellij.codeInsight.completion.command.commands.HighlightCommandSkipper
- a:skipForHighlightCommand(com.intellij.codeInsight.completion.command.CompletionCommand):Z
*:com.intellij.codeInsight.completion.command.commands.IntentionCommandOffsetProvider
- *sf:Companion:com.intellij.codeInsight.completion.command.commands.IntentionCommandOffsetProvider$Companion
- findOffsets(com.intellij.psi.PsiFile,I):java.util.List
@@ -2,6 +2,8 @@
package com.intellij.codeInsight.completion.command
import com.intellij.codeInsight.completion.*
import com.intellij.codeInsight.completion.command.commands.DirectIntentionCommandProvider
import com.intellij.codeInsight.completion.command.commands.HighlightCommandSkipper
import com.intellij.codeInsight.completion.command.configuration.ApplicationCommandCompletionService
import com.intellij.codeInsight.completion.impl.CamelHumpMatcher
import com.intellij.codeInsight.completion.ml.MLWeigherUtil
@@ -237,7 +239,11 @@ internal class CommandCompletionProvider : CompletionProvider<CompletionParamete
) {
val element = copyFile.findElementAt(offset - 1) ?: return
if (!ApplicationCommandCompletionService.getInstance().commandCompletionEnabled()) return
for (provider in commandCompletionFactory.commandProviders(project, element.language)) {
val commandProviders = commandCompletionFactory.commandProviders(project, element.language)
val highlightCommandProvider = commandProviders.filterIsInstance<DirectIntentionCommandProvider>().firstOrNull()
val highlightCommandSkippers = commandProviders.filter { it is HighlightCommandSkipper }.toSet()
highlightCommandProvider?.setSkippers(highlightCommandSkippers)
for (provider in commandProviders.filter { it !is HighlightCommandSkipper }) {
try {
if (isReadOnly && !provider.supportsReadOnly()) continue
copyEditor.caretModel.moveToOffset(offset)
@@ -0,0 +1,47 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.completion.command.commands
import com.intellij.codeInsight.completion.command.CommandCompletionProviderContext
import com.intellij.codeInsight.completion.command.CompletionCommand
import com.intellij.codeInsight.completion.command.HighlightInfoLookup
import com.intellij.idea.ActionsBundle
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
abstract class AbstractSafeDeleteCompletionCommandProvider : HighlightCommandSkipper,
ActionCommandProvider(actionId = "SafeDelete",
presentableName = ActionsBundle.message("action.SafeDelete.text"),
synonyms = listOf("delete", "safe delete"),
icon = null,
priority = -100,
previewText = ActionsBundle.message("action.SafeDelete.description")) {
abstract fun findElement(offset: Int, psiFile: PsiFile, editor: Editor?): PsiElement?
abstract override fun skipForHighlightCommand(command: CompletionCommand): Boolean
override fun isApplicable(offset: Int, psiFile: PsiFile, editor: Editor?): Boolean {
val element = findElement(offset, psiFile, editor) ?: return false
editor?.caretModel?.moveToOffset(element.textRange.endOffset)
return super.isApplicable(element.textRange.endOffset, psiFile, editor)
}
override fun createCommand(context: CommandCompletionProviderContext): ActionCompletionCommand? {
val element = findElement(context.offset, context.psiFile, context.editor) ?: return null
val startOffset = element.textRange.endOffset
return object : ActionCompletionCommand(actionId = super.actionId,
presentableActionName = super.presentableName,
icon = super.icon,
priority = super.priority,
previewText = super.previewText,
highlightInfo = HighlightInfoLookup(element.textRange, EditorColors.SEARCH_RESULT_ATTRIBUTES, 0),
synonyms = super.synonyms) {
override fun execute(offset: Int, psiFile: PsiFile, editor: Editor?) {
editor?.caretModel?.moveToOffset(startOffset)
super.execute(startOffset, psiFile, editor)
}
}
}
}
@@ -31,8 +31,8 @@ import kotlinx.coroutines.job
import org.jetbrains.annotations.Nls
import javax.swing.Icon
internal class DirectInspectionFixCompletionCommand(
private val inspectionId: String,
class DirectInspectionFixCompletionCommand(
val inspectionId: String,
override val presentableName: @Nls String,
override val priority: Int?,
override val icon: Icon?,
@@ -72,6 +72,11 @@ import java.util.function.Predicate
* based on intentions, errors, and inspections within the given context.
*/
internal class DirectIntentionCommandProvider : CommandProvider {
private var mySkipper: Set<CommandProvider> = emptySet()
fun setSkippers(skippers: Set<CommandProvider>) {
mySkipper = skippers
}
override fun getCommands(context: CommandCompletionProviderContext): List<CompletionCommand> {
if (!ApplicationCommandCompletionService.getInstance().commandCompletionEnabled()) return emptyList()
val originalEditor = context.originalEditor
@@ -136,10 +141,32 @@ internal class DirectIntentionCommandProvider : CommandProvider {
val intentions = asyncIntentions.await()
result.addAll(intentions)
result.addAll(processSkippers(result, context))
return@runBlockingCancellable result
}
}
private fun processSkippers(fromHighlights: MutableList<CompletionCommand>, context: CommandCompletionProviderContext): Collection<CompletionCommand> {
val result = mutableListOf<CompletionCommand>()
if (mySkipper.isEmpty()) return emptyList()
for (provider in mySkipper) {
if (provider is HighlightCommandSkipper) {
if (fromHighlights.any { provider.skipForHighlightCommand(it) }) continue
try {
context.editor.caretModel.moveToOffset(context.offset)
result.addAll(provider.getCommands(context))
}
catch (e: Exception) {
if (e is ControlFlowException || e is CancellationException) {
throw e
}
thisLogger().error("Can't collect commands", e)
}
}
}
return result
}
private fun createCustomEditor(
psiFile: PsiFile,
editor: Editor,
@@ -485,7 +512,7 @@ internal class DirectIntentionCommandProvider : CommandProvider {
modCommandAction.getPresentation(ActionContext.from(editor, psiFile))
?.rangesToHighlight()
?.firstOrNull { highlightRange ->
highlightRange.highlightingKind() == Presentation.HighlightingKind.APPLICABLE_TO_RANGE &&
highlightRange.highlightingKind() == Presentation.HighlightingKind.APPLICABLE_TO_RANGE &&
highlightRange.range.startOffset <= currentOffset
}
?.let {
@@ -0,0 +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 com.intellij.codeInsight.completion.command.commands
import com.intellij.codeInsight.completion.command.CompletionCommand
interface HighlightCommandSkipper {
fun skipForHighlightCommand(command: CompletionCommand): Boolean
}
@@ -0,0 +1,58 @@
// 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.kotlin.idea.completion.impl.k2.contributors.commands
import com.intellij.codeInsight.completion.command.CompletionCommand
import com.intellij.codeInsight.completion.command.commands.AbstractSafeDeleteCompletionCommandProvider
import com.intellij.codeInsight.completion.command.commands.DirectInspectionFixCompletionCommand
import com.intellij.codeInsight.completion.command.getCommandContext
import com.intellij.codeInsight.completion.command.getTargetContext
import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFunction
internal class KotlinSafeDeleteCompletionCommandProvider : AbstractSafeDeleteCompletionCommandProvider() {
override fun findElement(
offset: Int,
psiFile: PsiFile,
editor: Editor?
): PsiElement? {
var currentOffset = offset
if (currentOffset == 0 || editor == null) return null
var element = getCommandContext(offset, psiFile) ?: return null
if (element is PsiWhiteSpace) {
element = PsiTreeUtil.prevVisibleLeaf(element) ?: return null
currentOffset = element.textRange.endOffset
}
val method = element.parentOfType<KtFunction>()
if (method != null &&
(method.identifyingElement?.textRange?.endOffset == currentOffset ||
method.bodyBlockExpression?.textRange?.endOffset == currentOffset)
) return method.identifyingElement
val psiClass = element.parentOfType<KtClass>()
if (psiClass != null && psiClass.getBody()?.rBrace != null && psiClass.getBody()?.rBrace?.textRange?.endOffset == currentOffset) {
return psiClass.identifyingElement
}
val nameIdentifierOwner = element.parentOfType<PsiNameIdentifierOwner>()
if (nameIdentifierOwner != null && nameIdentifierOwner.nameIdentifier?.textRange?.endOffset == currentOffset) {
return nameIdentifierOwner.nameIdentifier
}
val targetContext = getTargetContext(offset, editor)
if (targetContext?.isWritable != true) return null
return element
}
override fun skipForHighlightCommand(command: CompletionCommand): Boolean {
return command is DirectInspectionFixCompletionCommand &&
(command.inspectionId == "UnusedVariable" || command.inspectionId == "unused")
}
}
@@ -0,0 +1,186 @@
// 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.kotlin.idea.fir.completion.commands
import com.intellij.openapi.util.registry.Registry
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
class K2CommandCompletionSafeDeleteTest : KotlinLightCodeInsightFixtureTestCase() {
override val pluginMode = KotlinPluginMode.K2
override fun setUp() {
super.setUp()
Registry.get("ide.completion.command.enabled").setValue(false, getTestRootDisposable())
Registry.get("ide.completion.command.force.enabled").setValue(true, getTestRootDisposable())
}
fun testClassIdentifier() {
myFixture.configureByText(
"x.kt", """
class A.<caret> {
fun foo(a: String) {
var b = a
}
}
class B{
fun bar(){
var c = A()
}
}
""".trimIndent()
)
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull() { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testClassEnd() {
myFixture.configureByText(
"x.kt", """
class A {
fun foo(a: String) {
var b = a
}
}.<caret>
class B{
fun bar(){
var c = A()
}
}
""".trimIndent()
)
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull() { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testClassReference() {
myFixture.configureByText(
"x.kt", """
class A {
fun foo(a: String) {
var b = a
}
}
class B{
fun bar(){
var c = A.<caret>()
}
}
""".trimIndent()
)
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull() { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testMethodIdentifier() {
myFixture.configureByText(
"x.kt", """
class A {
fun foo.<caret>(a: String) {
var b = a
}
}
class B{
fun bar(){
var c = A().foo("1")
}
}
""".trimIndent()
)
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull() { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testMethodEnd() {
myFixture.configureByText(
"x.kt", """
class A {
fun foo(a: String) {
var b = a
} .<caret>
}
class B{
fun bar(){
var c = A().foo("1")
}
}
""".trimIndent()
)
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull() { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testMethodReference() {
myFixture.configureByText(
"x.kt", """
class A {
fun foo(a: String) {
var b = a
}
}
class B{
fun bar(){
var c = A().foo.<caret>("1")
}
}
""".trimIndent()
)
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull() { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testVariable() {
myFixture.configureByText(
"x.kt", """
class A {
fun foo(a.<caret>: String) {
var b = a
}
}
class B{
fun bar(){
var c = A().foo("1")
}
}
""".trimIndent()
)
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
fun testVariableReference() {
myFixture.configureByText(
"x.kt", """
class A {
fun foo(a: String) {
var b = a.<caret>
}
}
class B{
fun bar(){
var c = A().foo("1")
}
}
""".trimIndent()
)
val elements = myFixture.completeBasic()
assertNotNull(elements.firstOrNull { element -> element.lookupString.contains("Safe delete", ignoreCase = true) })
}
}
@@ -45,6 +45,7 @@
<codeInsight.completion.command.provider language="kotlin" implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinTypeInfoCompletionCommandProvider" order="last"/>
<codeInsight.completion.command.provider language="kotlin" implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinQuickDocumentationCompletionCommandProvider" order="last"/>
<codeInsight.completion.command.provider language="kotlin" implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinSurroundWithCompletionCommandProvider" order="last"/>
<codeInsight.completion.command.provider language="kotlin" implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinSafeDeleteCompletionCommandProvider" order="last"/>
</extensions>
</idea-plugin>