[command-completion] IDEA-380020 Command Completion shouldn't be proposed inside string literals if they are not injection points

GitOrigin-RevId: ccf69ade720cd70ec878e36c386aeb21fe90b41e
This commit is contained in:
Mikhail Pyltsin
2025-09-29 15:50:36 +02:00
committed by intellij-monorepo-bot
parent 73369fee2a
commit fde7f496b4
4 changed files with 119 additions and 11 deletions

View File

@@ -27,16 +27,33 @@ class JavaCommandCompletionFactory implements CommandCompletionFactory, DumbAwar
if (!(psiFile instanceof PsiJavaFile)) return false;
//Doesn't work well. Disable for now
if (psiFile instanceof PsiJShellFile) return false;
PsiElement elementAt = psiFile.findElementAt(offset);
if (elementAt == null) return true;
if (!(elementAt.getParent() instanceof PsiParameterList)) return true;
PsiElement prevLeaf = PsiTreeUtil.prevLeaf(elementAt, true);
if (!(prevLeaf instanceof PsiJavaToken javaToken && javaToken.textMatches("."))) return true;
PsiElement prevPrevLeaf = PsiTreeUtil.prevLeaf(prevLeaf, true);
if (PsiTreeUtil.getParentOfType(prevPrevLeaf, PsiTypeElement.class) != null) return false;
if (isInsideParameterList(psiFile, offset)) return false;
if (isInsideStringLiteral(psiFile, offset)) return false;
return true;
}
private static boolean isInsideStringLiteral(@NotNull PsiFile file, int offset) {
PsiElement elementAt = file.findElementAt(offset);
if (!(elementAt instanceof PsiJavaToken psiJavaToken &&
(psiJavaToken.getTokenType() == JavaTokenType.STRING_LITERAL ||
psiJavaToken.getTokenType() == JavaTokenType.TEXT_BLOCK_LITERAL
))) {
return false;
}
return psiJavaToken.getTextRange().containsOffset(offset);
}
private static boolean isInsideParameterList(@NotNull PsiFile psiFile, int offset) {
PsiElement elementAt = psiFile.findElementAt(offset);
if (elementAt == null) return false;
if (!(elementAt.getParent() instanceof PsiParameterList)) return false;
PsiElement prevLeaf = PsiTreeUtil.prevLeaf(elementAt, true);
if (!(prevLeaf instanceof PsiJavaToken javaToken && javaToken.textMatches("."))) return false;
PsiElement prevPrevLeaf = PsiTreeUtil.prevLeaf(prevLeaf, true);
return PsiTreeUtil.getParentOfType(prevPrevLeaf, PsiTypeElement.class) != null;
}
static class JavaIntentionCommandSkipper implements IntentionCommandSkipper {
@Override
public boolean skip(@NotNull CommonIntentionAction action, @NotNull PsiFile psiFile, int offset) {

View File

@@ -1560,6 +1560,20 @@ class JavaCommandsCompletionTest : LightFixtureCompletionTestCase() {
myFixture.completeBasic()
}
fun testCompletionInsideLiteral() {
Registry.get("ide.completion.command.force.enabled").setValue(true, getTestRootDisposable())
myFixture.configureByText(JavaFileType.INSTANCE, """
public class B<T> {
static void main() {
call("class A{}.<caret>");
}
private static void call(String number) {}
}""".trimIndent())
val basic = myFixture.completeBasic()
assertTrue(basic.none { element -> element.`as`(CommandCompletionLookupElement::class.java) != null })
}
fun testHighlightingFormat() {
Registry.get("ide.completion.command.force.enabled").setValue(true, getTestRootDisposable())

View File

@@ -7,6 +7,7 @@ import com.intellij.lang.injection.InjectedLanguageManager
import com.intellij.openapi.project.DumbAware
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiLanguageInjectionHost
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.testFramework.LightVirtualFile
import org.jetbrains.kotlin.analysis.api.KaExperimentalApi
import org.jetbrains.kotlin.analysis.api.KaImplementationDetail
@@ -24,16 +25,33 @@ internal class KotlinCommandCompletionFactory : CommandCompletionFactory, DumbAw
if (InjectedLanguageManager.getInstance(psiFile.project).isInjectedFragment(psiFile)) return false
if (offset < 1) return false
if (psiFile !is KtFile) return false
if (isInsideFor(psiFile, offset)) return false
if (isInsideStringLiteral(psiFile, offset)) return false
return true
}
private fun isInsideStringLiteral(psiFile: KtFile, offset: Int): Boolean {
val element = psiFile.findElementAt(offset)
if (element == null) return false
val templateExpression = PsiTreeUtil.getParentOfType(
element,
KtStringTemplateExpression::class.java,
KtStringTemplateEntryWithExpression::class.java
)
return templateExpression is KtStringTemplateExpression
}
private fun isInsideFor(psiFile: KtFile, offset: Int): Boolean {
var element = psiFile.findElementAt(offset - 1)
val parent = element?.parent
if (parent !is KtForExpression && parent !is KtNamedFunction) return true
if (parent !is KtForExpression && parent !is KtNamedFunction) return false
if (parent is KtForExpression) {
element = element.prevSibling?.let {
if (it is KtContainerNode) it.firstChild else it
} ?: return true
return !parent.loopRange.isAncestor(element)
} ?: return false
return parent.loopRange.isAncestor(element)
}
return true
return false
}
override fun supportFiltersWithDoublePrefix(): Boolean = false

View File

@@ -677,6 +677,65 @@ class K2CommandCompletionTest : KotlinLightCodeInsightFixtureTestCase() {
assertTrue(elements[0].`as`(CommandCompletionLookupElement::class.java) != null)
}
fun testNoCompletionInsideStringBlock() {
Registry.get("ide.completion.command.force.enabled").setValue(true, getTestRootDisposable())
myFixture.configureByText(
"x.kt", """
class A {
fun test() {
call(
""${'"'}some.a.<caret>
""${'"'}.trimMargin(), "1"
)
}
}
fun call(key: String, key2: String) {
println(key + key2)
}""".trimIndent()
)
val elements = myFixture.complete(CompletionType.BASIC, 0)
assertTrue(elements.none { it.`as`(CommandCompletionLookupElement::class.java) != null })
}
fun testNoCompletionInsideStringLiteral() {
Registry.get("ide.completion.command.force.enabled").setValue(true, getTestRootDisposable())
myFixture.configureByText(
"x.kt", """
class A {
fun test3() {
call("some.a.<caret>", "1")
}
}
fun call(key: String, key2: String) {
println(key + key2)
}""".trimIndent()
)
val elements = myFixture.complete(CompletionType.BASIC, 0)
assertTrue(elements.none { it.`as`(CommandCompletionLookupElement::class.java) != null })
}
fun testNoCompletionInsideStringInsideInterpolation() {
Registry.get("ide.completion.command.force.enabled").setValue(true, getTestRootDisposable())
myFixture.configureByText(
"x.kt", """
class A {
fun test2(a: String) {
call(
"some.a.${'$'}{a.<caret>}".trimMargin(), "1"
)
}
}
fun call(key: String, key2: String) {
println(key + key2)
}""".trimIndent()
)
val elements = myFixture.complete(CompletionType.BASIC, 0)
assertFalse(elements.none { it.`as`(CommandCompletionLookupElement::class.java) != null })
}
fun testNotFirstCompletion() {
Registry.get("ide.completion.command.force.enabled").setValue(true, getTestRootDisposable())
myFixture.configureByText(