[kotlin] KTIJ-33049 Initial support command completion for kotlin

GitOrigin-RevId: 53f1d4411940cf0f8b516dc2fe5949b9da482a32
This commit is contained in:
Mikhail Pyltsin
2025-02-13 09:57:47 +00:00
committed by intellij-monorepo-bot
parent 260c4da89a
commit 230e228215
14 changed files with 462 additions and 6 deletions
@@ -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()
}
@@ -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
}
@@ -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
}
}
@@ -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<KtNamedDeclaration>()
val name = member?.nameIdentifier ?: return false
val fileDocument = element.containingFile.fileDocument
return fileDocument.getLineNumber(name.textRange.startOffset) == fileDocument.getLineNumber(offset)
}
}
@@ -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<PsiElement?> = SmartPointerManager.createPointer(psiElement.parent ?: psiFile)
psiElement.delete()
PsiDocumentManager.getInstance(psiFile.project).commitDocument(psiFile.fileDocument)
parent.element?.let {
ReformatCodeProcessor(psiFile, arrayOf(it.textRange)).run()
}
}, psiFile)
}
}
@@ -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)
}
}
}
@@ -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
}
}
@@ -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<KtReferenceExpression>() ?: return false
analyze(ref) {
return ref.mainReference.resolve() != null
}
}
}
@@ -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<KtNamedDeclaration>()
val name = member?.nameIdentifier ?: return false
val fileDocument = element.containingFile.fileDocument
return fileDocument.getLineNumber(name.textRange.startOffset) == fileDocument.getLineNumber(offset)
}
}
@@ -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
}
}
@@ -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;
@@ -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.<caret>
}
""".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"
}.<caret>
""".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".<caret>
}
""".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"
}.<caret>
""".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"
}.<caret>
""".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{
.<caret>
}
""".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.<caret>{
}
""".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.<caret>)
}
""".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 <caret>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()
}
}
@@ -0,0 +1,36 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<codeInsight.completion.command.factory language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinCommandCompletionFactory"/>
<codeInsight.completion.command.provider language="kotlin"
implementationClass="com.intellij.codeInsight.completion.command.commands.SimpleRunMarkerCommandProvider"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="com.intellij.codeInsight.completion.command.commands.RenameActionCompletionCommand"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="com.intellij.codeInsight.completion.command.commands.OptimizeImportCompletionCommand"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="com.intellij.codeInsight.completion.command.commands.RecentFilesCompletionCommand"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="com.intellij.codeInsight.completion.command.commands.ProjectViewCompletionCommand"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="com.intellij.codeInsight.completion.command.commands.LineCommentCompletionCommand"/>
<codeInsight.completion.command.provider language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinGenerateCommandCompletion"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinGoToDeclarationCommandCompletion"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinGoToImplementationCommandCompletion"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinFormatCodeCompletionCommand"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinCopyFQNCompletionCommand"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinShowUsagesActionCompletionCommand"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="com.intellij.codeInsight.completion.command.commands.PsiElementCommentCompletionCommand"/>
<codeInsight.completion.applicable.command language="kotlin"
implementationClass="org.jetbrains.kotlin.idea.completion.impl.k2.contributors.commands.KotlinDeleteCompletionCommand"/>
</extensions>
</idea-plugin>
@@ -39,6 +39,10 @@
<xi:fallback/>
</xi:include>
<xi:include href="command-completion-k2.xml">
<xi:fallback/>
</xi:include>
<extensions defaultExtensionNs="org.jetbrains.kotlin">
<scriptAdditionalIdeaDependenciesProvider implementation="org.jetbrains.kotlin.idea.core.script.ModuleScriptAdditionalIdeaDependenciesProvider"/>