From 230e228215c2c11bc32e77f077c04f24fa1d7c05 Mon Sep 17 00:00:00 2001 From: Mikhail Pyltsin Date: Fri, 7 Feb 2025 14:35:00 +0100 Subject: [PATCH] [kotlin] KTIJ-33049 Initial support command completion for kotlin GitOrigin-RevId: 53f1d4411940cf0f8b516dc2fe5949b9da482a32 --- .../PsiElementCommentCompletionCommand.kt | 18 +- .../idea/base/analysis/RootKindMatcherImpl.kt | 4 +- .../KotlinCommandCompletionFactory.kt | 53 +++++ .../KotlinCopyFQNCompletionCommand.kt | 19 ++ .../commands/KotlinDeleteCompletionCommand.kt | 48 +++++ .../KotlinFormatCodeCompletionCommand.kt | 16 ++ .../KotlinGenerateCommandCompletion.kt | 19 ++ .../KotlinGoToDeclarationCommandCompletion.kt | 21 ++ ...tlinGoToImplementationCommandCompletion.kt | 19 ++ ...KotlinShowUsagesActionCompletionCommand.kt | 15 ++ .../contributors/commands/package-info.java | 8 + .../fir/completion/K2CommandCompletionTest.kt | 188 ++++++++++++++++++ .../META-INF/command-completion-k2.xml | 36 ++++ .../plugin/k2/resources/kotlin.plugin.k2.xml | 4 + 14 files changed, 462 insertions(+), 6 deletions(-) create mode 100644 plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinCommandCompletionFactory.kt create mode 100644 plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinCopyFQNCompletionCommand.kt create mode 100644 plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinDeleteCompletionCommand.kt create mode 100644 plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinFormatCodeCompletionCommand.kt create mode 100644 plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGenerateCommandCompletion.kt create mode 100644 plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGoToDeclarationCommandCompletion.kt create mode 100644 plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGoToImplementationCommandCompletion.kt create mode 100644 plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinShowUsagesActionCompletionCommand.kt create mode 100644 plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/package-info.java create mode 100644 plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/completion/K2CommandCompletionTest.kt create mode 100644 plugins/kotlin/plugin/k2/resources/META-INF/command-completion-k2.xml diff --git a/platform/lang-impl/src/com/intellij/codeInsight/completion/command/commands/PsiElementCommentCompletionCommand.kt b/platform/lang-impl/src/com/intellij/codeInsight/completion/command/commands/PsiElementCommentCompletionCommand.kt index 787845ad6313..3576a26c2674 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/completion/command/commands/PsiElementCommentCompletionCommand.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/completion/command/commands/PsiElementCommentCompletionCommand.kt @@ -3,22 +3,23 @@ package com.intellij.codeInsight.completion.command.commands import com.intellij.codeInsight.CodeInsightBundle import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiComment import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil internal class PsiElementCommentCompletionCommand : AbstractActionCompletionCommand("CommentByLineComment", "Comment element", - CodeInsightBundle.message("command.completion.psi.element.comment.text"), + CodeInsightBundle.message( + "command.completion.psi.element.comment.text"), null) { override fun isApplicable(offset: Int, psiFile: PsiFile, editor: Editor?): Boolean { if (!super.isApplicable(offset, psiFile, editor)) return false val fileDocument = psiFile.fileDocument - val immutableCharSequence = fileDocument.immutableCharSequence if (offset - 1 < 0) return false - val ch = immutableCharSequence[offset - 1] - if (ch != '}' && ch != ']' && ch != ')') return false val context = getHighLevelContext(offset, psiFile) ?: return false + if (PsiTreeUtil.skipWhitespacesBackward(context) is PsiComment) return true val startLineNumber = fileDocument.getLineNumber(context.textRange.startOffset) val endLineNumber = fileDocument.getLineNumber(context.textRange.endOffset) return startLineNumber != endLineNumber @@ -42,7 +43,14 @@ internal class PsiElementCommentCompletionCommand : AbstractActionCompletionComm val selectionModel = editor.selectionModel val highLevelContext = getHighLevelContext(offset, psiFile) ?: return val textRange = highLevelContext.textRange - selectionModel.setSelection(textRange.startOffset, textRange.endOffset) + var startOffset = textRange.startOffset + val endOffset = textRange.endOffset + var current: PsiElement? = highLevelContext + while (current is PsiComment) { + startOffset = current.textRange.endOffset + current = PsiTreeUtil.skipWhitespacesBackward(current) + } + selectionModel.setSelection(startOffset, endOffset) super.execute(offset, psiFile, editor) selectionModel.removeSelection() } diff --git a/plugins/kotlin/base/analysis/src/org/jetbrains/kotlin/idea/base/analysis/RootKindMatcherImpl.kt b/plugins/kotlin/base/analysis/src/org/jetbrains/kotlin/idea/base/analysis/RootKindMatcherImpl.kt index 9727539cb126..2347bc92b966 100644 --- a/plugins/kotlin/base/analysis/src/org/jetbrains/kotlin/idea/base/analysis/RootKindMatcherImpl.kt +++ b/plugins/kotlin/base/analysis/src/org/jetbrains/kotlin/idea/base/analysis/RootKindMatcherImpl.kt @@ -11,6 +11,7 @@ import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.originalFileOrSelf import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.base.projectStructure.RootKindFilter import org.jetbrains.kotlin.idea.base.projectStructure.RootKindMatcher @@ -53,7 +54,8 @@ internal class RootKindMatcherImpl(private val project: Project) : RootKindMatch nameSequence.endsWith(BuiltInSerializerProtocol.DOT_DEFAULT_EXTENSION) || nameSequence.endsWith(DOT_METADATA_FILE_EXTENSION) - if (virtualFile !is VirtualFileWindow && !hasBinaryFileExtension && fileIndex.isUnderSourceRootOfType(virtualFile, rootType)) { + if (virtualFile !is VirtualFileWindow && !hasBinaryFileExtension && + fileIndex.isUnderSourceRootOfType(virtualFile.originalFileOrSelf(), rootType)) { return filter.includeProjectSourceFiles } diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinCommandCompletionFactory.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinCommandCompletionFactory.kt new file mode 100644 index 000000000000..6e2f0ce90934 --- /dev/null +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinCommandCompletionFactory.kt @@ -0,0 +1,53 @@ +// 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.CommandCompletionFactory +import com.intellij.openapi.project.DumbAware +import com.intellij.psi.PsiFile +import com.intellij.testFramework.LightVirtualFile +import org.jetbrains.kotlin.idea.base.psi.copied +import org.jetbrains.kotlin.psi.KtContainerNode +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtForExpression +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.psiUtil.isAncestor + +class KotlinCommandCompletionFactory : CommandCompletionFactory, DumbAware { + override fun isApplicable(psiFile: PsiFile, offset: Int): Boolean { + if (psiFile !is KtFile) return false + var element = psiFile.findElementAt(offset) + val parent = element?.parent + if (parent !is KtForExpression) return true + element = element.prevSibling?.let { + if (it is KtContainerNode) it.firstChild else it + } ?: return true + return !parent.loopRange.isAncestor(element) + } + + override fun createFile(originalFile: PsiFile, text: String): PsiFile? { + val newFile = KtPsiFactory.contextual(originalFile, eventSystemEnabled = true).createFile(originalFile.name, text) + newFile.originalFile = originalFile + if (originalFile.name.endsWith(".kts")) { + createCopyOfScript(originalFile, newFile)?.let { return it } + } + + val virtualFile = newFile.virtualFile + val originalVirtualFile = originalFile.virtualFile + if (virtualFile is LightVirtualFile && originalVirtualFile != null) { + virtualFile.originalFile = originalVirtualFile + virtualFile.fileType = originalVirtualFile.fileType + + } + return newFile + } + + private fun createCopyOfScript(originalFile: PsiFile, newFile: KtFile): KtFile? { + // We copy the original file, which retains the correct script context + val newFileCopy = originalFile.copied() as KtFile + // replace the old script block with the new script block + val copyOfOriginalBlockExpression = newFileCopy.script?.blockExpression ?: return null + val copiedNewBlockExpression = newFile.script?.blockExpression ?: return null + copyOfOriginalBlockExpression.replace(copiedNewBlockExpression.copy()) + return newFileCopy + } +} diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinCopyFQNCompletionCommand.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinCopyFQNCompletionCommand.kt new file mode 100644 index 000000000000..50c2060f2e07 --- /dev/null +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinCopyFQNCompletionCommand.kt @@ -0,0 +1,19 @@ +// 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.commands.AbstractCopyFQNCompletionCommand +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import org.jetbrains.kotlin.idea.util.CommentSaver.Companion.tokenType +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtNamedDeclaration + +class KotlinCopyFQNCompletionCommand : AbstractCopyFQNCompletionCommand() { + override fun placeIsApplicable(element: PsiElement, offset: Int): Boolean { + if (element.tokenType != KtTokens.IDENTIFIER) return false + val member = element.parentOfType() + val name = member?.nameIdentifier ?: return false + val fileDocument = element.containingFile.fileDocument + return fileDocument.getLineNumber(name.textRange.startOffset) == fileDocument.getLineNumber(offset) + } +} \ No newline at end of file diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinDeleteCompletionCommand.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinDeleteCompletionCommand.kt new file mode 100644 index 000000000000..23e8db1432f7 --- /dev/null +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinDeleteCompletionCommand.kt @@ -0,0 +1,48 @@ +// 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.actions.ReformatCodeProcessor +import com.intellij.codeInsight.completion.command.ApplicableCompletionCommand +import com.intellij.idea.ActionsBundle +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.DumbAware +import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.annotations.Nls +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtNamedDeclaration +import javax.swing.Icon + +class KotlinDeleteCompletionCommand : ApplicableCompletionCommand(), DumbAware { + override val name: String + get() = "Delete" + override val i18nName: @Nls String + get() = ActionsBundle.message("action.EditorDelete.text") + override val icon: Icon? + get() = null + + override fun isApplicable(offset: Int, psiFile: PsiFile, editor: Editor?): Boolean { + val element = getContext(offset, psiFile) ?: return false + val psiElement = PsiTreeUtil.getParentOfType(element, KtExpression::class.java, KtNamedDeclaration::class.java) ?: return false + return psiElement.textRange.endOffset == offset + } + + override fun execute(offset: Int, psiFile: PsiFile, editor: Editor?) { + val element = getContext(offset, psiFile) ?: return + var psiElement = PsiTreeUtil.getParentOfType(element, KtExpression::class.java, KtNamedDeclaration::class.java) ?: return + var curElement = psiElement + while (curElement.textRange.endOffset == offset) { + psiElement = curElement + curElement = PsiTreeUtil.getParentOfType(curElement, KtExpression::class.java, KtNamedDeclaration::class.java) ?: break + } + WriteCommandAction.runWriteCommandAction(psiFile.project, null, null, { + val parent: SmartPsiElementPointer = SmartPointerManager.createPointer(psiElement.parent ?: psiFile) + psiElement.delete() + PsiDocumentManager.getInstance(psiFile.project).commitDocument(psiFile.fileDocument) + parent.element?.let { + ReformatCodeProcessor(psiFile, arrayOf(it.textRange)).run() + } + }, psiFile) + } +} \ No newline at end of file diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinFormatCodeCompletionCommand.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinFormatCodeCompletionCommand.kt new file mode 100644 index 000000000000..cb1ef11cafc2 --- /dev/null +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinFormatCodeCompletionCommand.kt @@ -0,0 +1,16 @@ +// 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.commands.AbstractFormatCodeCompletionCommand +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parents +import org.jetbrains.kotlin.psi.KtDeclaration +import org.jetbrains.kotlin.psi.KtExpression + +class KotlinFormatCodeCompletionCommand : AbstractFormatCodeCompletionCommand() { + override fun findTargetToRefactor(element: PsiElement): PsiElement { + return element.parents(true).first { + (it is KtDeclaration || it is KtExpression) + } + } +} \ No newline at end of file diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGenerateCommandCompletion.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGenerateCommandCompletion.kt new file mode 100644 index 000000000000..627fae35a95c --- /dev/null +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGenerateCommandCompletion.kt @@ -0,0 +1,19 @@ +// 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.commands.AbstractGenerateCommandProvider +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.psi.KtClass +import org.jetbrains.kotlin.psi.KtClassBody +import org.jetbrains.kotlin.psi.KtFile + +class KotlinGenerateCommandCompletion : AbstractGenerateCommandProvider() { + override fun generationIsAvailable(element: PsiElement, offset: Int): Boolean { + val parent = element.parent + if (parent !is KtClass && parent !is KtFile && parent !is KtClassBody) return false + val onlySpaceInLine = isOnlySpaceInLine(element.containingFile.fileDocument, offset) + if (onlySpaceInLine) return true + if (parent is KtClass && parent.nameIdentifier == element) return true + return false + } +} \ No newline at end of file diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGoToDeclarationCommandCompletion.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGoToDeclarationCommandCompletion.kt new file mode 100644 index 000000000000..c479b5df7427 --- /dev/null +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGoToDeclarationCommandCompletion.kt @@ -0,0 +1,21 @@ +// 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.commands.AbstractGoToDeclarationCompletionCommand +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.analysis.api.analyze +import org.jetbrains.kotlin.analysis.utils.printer.parentOfType +import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.idea.util.CommentSaver.Companion.tokenType +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtReferenceExpression + +class KotlinGoToDeclarationCommandCompletion : AbstractGoToDeclarationCompletionCommand() { + override fun canNavigateToDeclaration(context: PsiElement): Boolean { + if (context.tokenType != KtTokens.IDENTIFIER) return false + val ref = context.parentOfType() ?: return false + analyze(ref) { + return ref.mainReference.resolve() != null + } + } +} \ No newline at end of file diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGoToImplementationCommandCompletion.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGoToImplementationCommandCompletion.kt new file mode 100644 index 000000000000..eb00a3537e22 --- /dev/null +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinGoToImplementationCommandCompletion.kt @@ -0,0 +1,19 @@ +// 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.commands.AbstractGoToImplementationCompletionCommand +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import org.jetbrains.kotlin.idea.util.CommentSaver.Companion.tokenType +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtNamedDeclaration + +class KotlinGoToImplementationCommandCompletion : AbstractGoToImplementationCompletionCommand() { + override fun canGoToImplementation(element: PsiElement, offset: Int): Boolean { + if (element.tokenType != KtTokens.IDENTIFIER) return false + val member = element.parentOfType() + val name = member?.nameIdentifier ?: return false + val fileDocument = element.containingFile.fileDocument + return fileDocument.getLineNumber(name.textRange.startOffset) == fileDocument.getLineNumber(offset) + } +} diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinShowUsagesActionCompletionCommand.kt b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinShowUsagesActionCompletionCommand.kt new file mode 100644 index 000000000000..4ca1253d2643 --- /dev/null +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/KotlinShowUsagesActionCompletionCommand.kt @@ -0,0 +1,15 @@ +// 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.commands.AbstractShowUsagesActionCompletionCommand +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.idea.util.CommentSaver.Companion.tokenType +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtNamedDeclaration + +class KotlinShowUsagesActionCompletionCommand : AbstractShowUsagesActionCompletionCommand() { + override fun hasToShow(element: PsiElement): Boolean { + if (element.tokenType != KtTokens.IDENTIFIER) return false + return element.parent is KtNamedDeclaration + } +} \ No newline at end of file diff --git a/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/package-info.java b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/package-info.java new file mode 100644 index 000000000000..82ba4bdbece1 --- /dev/null +++ b/plugins/kotlin/completion/impl-k2/src/org/jetbrains/kotlin/idea/completion/impl/k2/contributors/commands/package-info.java @@ -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 contains experimental implementations for kotlin command completion + */ +@ApiStatus.Experimental +package org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/completion/K2CommandCompletionTest.kt b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/completion/K2CommandCompletionTest.kt new file mode 100644 index 000000000000..107561ae9d6c --- /dev/null +++ b/plugins/kotlin/fir/tests/test/org/jetbrains/kotlin/idea/fir/completion/K2CommandCompletionTest.kt @@ -0,0 +1,188 @@ +// 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 + +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupEvent +import com.intellij.codeInsight.lookup.impl.LookupImpl +import com.intellij.openapi.actionSystem.IdeActions +import com.intellij.openapi.application.impl.NonBlockingReadActionImpl +import com.intellij.openapi.util.registry.Registry +import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode +import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase + +class K2CommandCompletionTest : KotlinLightCodeInsightFixtureTestCase() { + override val pluginMode = KotlinPluginMode.K2 + + fun testRedCompletion() { + Registry.get("ide.completion.command.enabled").setValue(true, getTestRootDisposable()) + myFixture.configureByText( + "x.kt", """ + fun main() { + val a: String = 1. + } + """.trimIndent() + ) + val elements = myFixture.completeBasic() + selectItem(elements.first { element -> element.lookupString.contains("Change type of", ignoreCase = true) }) + myFixture.checkResult( + """ + fun main() { + val a: Int = 1 + } + """.trimIndent() + ) + } + + + fun testFormat() { + Registry.get("ide.completion.command.enabled").setValue(true, getTestRootDisposable()) + myFixture.configureByText( + "x.kt", """ + fun main() { + val a: String = "1" + }. + """.trimIndent() + ) + val elements = myFixture.completeBasic() + selectItem(elements.first { element -> element.lookupString.contains("Format", ignoreCase = true) }) + myFixture.checkResult( + """ + fun main() { + val a: String = "1" + } + """.trimIndent() + ) + } + + fun testComment() { + Registry.get("ide.completion.command.enabled").setValue(true, getTestRootDisposable()) + myFixture.configureByText( + "x.kt", """ + fun main() { + val a: String = "1". + } + """.trimIndent() + ) + val elements = myFixture.completeBasic() + selectItem(elements.first { element -> element.lookupString.contains("Comment", ignoreCase = true) }) + myFixture.checkResult( + """ + fun main() { + // val a: String = "1" + } + """.trimIndent() + ) + } + + fun testCommentPsiElement() { + Registry.get("ide.completion.command.enabled").setValue(true, getTestRootDisposable()) + myFixture.configureByText( + "x.kt", """ + fun main() { + val a: String = "1" + }. + """.trimIndent() + ) + val elements = myFixture.completeBasic() + selectItem(elements.first { element -> element.lookupString.contains("Comment element", ignoreCase = true) }) + myFixture.checkResult( + """ + //fun main() { + // val a: String = "1" + //} + """.trimIndent() + ) + } + + fun testDeletePsiElement() { + Registry.get("ide.completion.command.enabled").setValue(true, getTestRootDisposable()) + myFixture.configureByText( + "x.kt", """ + fun main() { + val a: String = "1" + }. + """.trimIndent() + ) + val elements = myFixture.completeBasic() + selectItem(elements.first { element -> element.lookupString.contains("Delete", ignoreCase = true) }) + myFixture.checkResult( + "" + ) + } + + fun testGenerate() { + Registry.get("ide.completion.command.enabled").setValue(true, getTestRootDisposable()) + myFixture.configureByText( + "x.kt", """ + class A{ + . + } + """.trimIndent() + ) + val elements = myFixture.completeBasic() + selectItem(elements.first { element -> element.lookupString.contains("Generate 'Secondary", ignoreCase = true) }) + myFixture.checkResult( + """ + class A{ + constructor() + } + """.trimIndent() + ) + } + + fun testCopyFqn() { + Registry.get("ide.completion.command.enabled").setValue(true, getTestRootDisposable()) + myFixture.configureByText( + "x.kt", """ + class A.{ + + } + """.trimIndent() + ) + val elements = myFixture.completeBasic() + selectItem(elements.first { element -> element.lookupString.contains("Copy ref", ignoreCase = true) }) + myFixture.performEditorAction(IdeActions.ACTION_EDITOR_PASTE) + myFixture.checkResult( + """ + class AA{ + + } + """.trimIndent() + ) + } + + fun testGoToDeclaration() { + Registry.get("ide.completion.command.enabled").setValue(true, getTestRootDisposable()) + myFixture.configureByText( + "x.kt", """ + fun main() { + val a = "1" + print(a.) + } + """.trimIndent() + ) + val elements = myFixture.completeBasic() + selectItem(elements.first { element -> element.lookupString.contains("Go to decl", ignoreCase = true) }) + myFixture.performEditorAction(IdeActions.ACTION_EDITOR_PASTE) + myFixture.checkResult( + """ + fun main() { + val a = "1" + print(a) + } + """.trimIndent() + ) + } + + private fun selectItem(item: LookupElement, completionChar: Char = 0.toChar()) { + val lookup: LookupImpl = myFixture.lookup as LookupImpl + lookup.setCurrentItem(item) + if (LookupEvent.isSpecialCompletionChar(completionChar)) { + lookup.finishLookup(completionChar) + } else { + myFixture.type(completionChar) + } + NonBlockingReadActionImpl.waitForAsyncTaskCompletion() + } + +} \ No newline at end of file diff --git a/plugins/kotlin/plugin/k2/resources/META-INF/command-completion-k2.xml b/plugins/kotlin/plugin/k2/resources/META-INF/command-completion-k2.xml new file mode 100644 index 000000000000..f8a6f641d50c --- /dev/null +++ b/plugins/kotlin/plugin/k2/resources/META-INF/command-completion-k2.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/kotlin/plugin/k2/resources/kotlin.plugin.k2.xml b/plugins/kotlin/plugin/k2/resources/kotlin.plugin.k2.xml index f9b2884787da..9be1fd66ea4e 100644 --- a/plugins/kotlin/plugin/k2/resources/kotlin.plugin.k2.xml +++ b/plugins/kotlin/plugin/k2/resources/kotlin.plugin.k2.xml @@ -39,6 +39,10 @@ + + + +