[grazie] IJPL-232354 Exception when trying to fix a typo in Kotlin code

Merge-request: IJ-MR-189452
Merged-by: Ilia Permiashkin <ilia.permiashkin@jetbrains.com>

GitOrigin-RevId: d0cbdb76d0b4a6e402a8c1a3a914d55d1668bdef
This commit is contained in:
Ilia Permiashkin
2026-02-03 20:39:48 +00:00
committed by intellij-monorepo-bot
parent 46cfb3792a
commit 224cbb3ef8
7 changed files with 78 additions and 25 deletions
@@ -23,6 +23,7 @@ jvm_library(
"//plugins/grazie/src/test:grazie-tests_test_lib",
"//plugins/kotlin/base/test:test_test_lib",
"//platform/core-api:core",
"//platform/analysis-api:analysis",
"//plugins/kotlin/test-framework:test-framework_test_lib",
],
runtime_deps = [":kotlin-grazie-tests"]
@@ -15,6 +15,7 @@
<orderEntry type="module" module-name="intellij.grazie.tests" scope="TEST" />
<orderEntry type="module" module-name="kotlin.base.test" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.analysis" scope="TEST" />
<orderEntry type="module" module-name="intellij.kotlin.testFramework" scope="TEST" />
</component>
</module>
@@ -54,4 +54,22 @@ class KotlinGrazieSupportTest28 : GrazieTestBase(), ExpectedPluginModeProvider {
val content = TextExtractor.findTextAt(file, 10, TextContent.TextDomain.ALL)
assertEquals("foo | bar", TextContentTest.unknownOffsets(content))
}
fun `test meaningful single suggestion in RenameTo action`() {
myFixture.configureByText("a.kt", """
class A {
// <TYPO descr="Typo: In word 'tagret'">tagret</TYPO>
val <TYPO descr="Typo: In word 'tagret'">tag<caret>ret</TYPO> = 1
}
""")
myFixture.checkHighlighting()
val intention = myFixture.findSingleIntention("Typo: Rename to 'target'")
myFixture.launchAction(intention)
myFixture.checkResult("""
class A {
// target
val target = 1
}
""")
}
}
@@ -48,4 +48,22 @@ class KotlinGrazieSupportTest28 : GrazieTestBase(), ExpectedPluginModeProvider {
val content = TextExtractor.findTextAt(file, 10, TextContent.TextDomain.ALL)
assertEquals("foo | bar", TextContentTest.unknownOffsets(content))
}
fun `test meaningful single suggestion in RenameTo action`() {
myFixture.configureByText("a.kt", """
class A {
// <TYPO descr="Typo: In word 'tagret'">tagret</TYPO>
val <TYPO descr="Typo: In word 'tagret'">tag<caret>ret</TYPO> = 1
}
""")
myFixture.checkHighlighting()
val intention = myFixture.findSingleIntention("Typo: Rename to 'target'")
myFixture.launchAction(intention)
myFixture.checkResult("""
class A {
// target
val target = 1
}
""")
}
}
@@ -2,6 +2,17 @@
package ide.language.kotlin
interface A {
val <TYPO descr="Typo: In word 'typpo'">typpo</TYPO>: Int
fun <TYPO descr="Typo: In word 'typpo'">typpo</TYPO>(<TYPO descr="Typo: In word 'typpo'">typpo</TYPO>: Int)
}
class B : A {
// typos are ignored because of `override` keyword
override val typpo: Int = 0
override fun typpo(typpo: Int) { }
}
val variableWith<TYPO descr="Typo: In word 'Eror'">Eror</TYPO> = "error"
fun <TYPO descr="Typo: In word 'eror'">eror</TYPO>Function(<TYPO descr="Typo: In word 'eror'">eror</TYPO>: Int) {}
@@ -6,51 +6,57 @@ import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.registry.Registry
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiNameIdentifierOwner
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.spellchecker.inspections.PlainTextSplitter
import com.intellij.spellchecker.inspections.Splitter
import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy
import com.intellij.spellchecker.tokenizer.TokenConsumer
import com.intellij.spellchecker.tokenizer.Tokenizer
import com.intellij.spellchecker.tokenizer.TokenizerBase
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
import org.jetbrains.kotlin.psi.KtModifierListOwner
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtParameterList
internal class KotlinSpellcheckingStrategy : SpellcheckingStrategy(), DumbAware {
private val plainTextTokenizer: Tokenizer<PsiElement> = TokenizerBase(PlainTextSplitter.getInstance())
private val codeTokenizer: Tokenizer<PsiElement> = object : TokenizerBase<PsiElement>(PlainTextSplitter.getInstance()) {
override fun consumeToken(element: PsiElement, consumer: TokenConsumer, splitter: Splitter) {
consumer.consumeToken(element, true, splitter)
}
}
private val emptyTokenizer: Tokenizer<PsiElement> = EMPTY_TOKENIZER
override fun getTokenizer(element: PsiElement): Tokenizer<out PsiElement?> {
if (useTextLevelSpellchecking() && (element is PsiComment || element is KtLiteralStringTemplateEntry)) {
// use [KotlinTextExtractor] and [GrazieTextLevelSpellCheckingExtension] if enabled for non-code constructs
// use [KotlinTextExtractor] and [SpellingTextChecker] if enabled for non-code constructs
return emptyTokenizer
}
return when (element) {
is PsiComment -> super.getTokenizer(element)
is KtParameter -> {
val function = (element.parent as? KtParameterList)?.parent as? KtNamedFunction
when {
function?.hasModifier(KtTokens.OVERRIDE_KEYWORD) == true -> emptyTokenizer
else -> super.getTokenizer(element)
}
}
is PsiNameIdentifierOwner -> {
when {
element is KtModifierListOwner && element.hasModifier(KtTokens.OVERRIDE_KEYWORD) -> emptyTokenizer
else -> super.getTokenizer(element)
}
}
is KtLiteralStringTemplateEntry if !isInjectedLanguageFragment(element.parent) -> plainTextTokenizer
is LeafPsiElement -> {
if (element.elementType != KtTokens.IDENTIFIER) return emptyTokenizer
val parent = element.parent
if (parent is KtNameReferenceExpression) return emptyTokenizer
if (parent is KtParameter) {
val function = (parent.parent as? KtParameterList)?.parent as? KtNamedFunction
if (function is KtNamedFunction) return getTokenizer(function)
}
if (parent is KtModifierListOwner) return getTokenizer(parent)
codeTokenizer
}
else -> emptyTokenizer
}
}
override fun useTextLevelSpellchecking(): Boolean {
return Registry.`is`("spellchecker.grazie.enabled", false)
private fun getTokenizer(parent: KtModifierListOwner): Tokenizer<PsiElement> = when {
parent.hasModifier(KtTokens.OVERRIDE_KEYWORD) -> emptyTokenizer
else -> codeTokenizer
}
override fun useTextLevelSpellchecking(): Boolean = Registry.`is`("spellchecker.grazie.enabled", false)
}
@@ -136,7 +136,7 @@ public class RenameTo extends IntentionAndQuickFixAction implements Iconable, Ev
private void generateSuggestions(String name, PsiElement element) {
if (suggestions == null) {
TextRange range = restoreRange(name);
TextRange range = restoreRange();
if (range == null) return;
this.suggestions = SpellCheckerManager.getInstance(pointer.getProject()).getSuggestions(typo)
.stream()
@@ -147,15 +147,13 @@ public class RenameTo extends IntentionAndQuickFixAction implements Iconable, Ev
}
}
private @Nullable TextRange restoreRange(String currentElementName) {
private @Nullable TextRange restoreRange() {
PsiElement element = pointer.getElement();
Segment rangeRelativeToFile = this.rangeRelativeToFile.getRange();
if (element == null || rangeRelativeToFile == null) return null;
int offset = element.getText().indexOf(currentElementName);
return TextRange.create(rangeRelativeToFile)
.shiftLeft(element.getTextRange().getStartOffset())
.shiftLeft(offset);
.shiftLeft(element.getTextRange().getStartOffset());
}
private void runRenamer(PsiElement element, String suggestion) {