diff --git a/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/BUILD.bazel b/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/BUILD.bazel index a3e21226cdf7..1e0afa25f4ae 100644 --- a/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/BUILD.bazel +++ b/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/BUILD.bazel @@ -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"] diff --git a/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/intellij.kotlin.grazie.tests.iml b/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/intellij.kotlin.grazie.tests.iml index 3288c197bb7b..770ca87a182a 100644 --- a/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/intellij.kotlin.grazie.tests.iml +++ b/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/intellij.kotlin.grazie.tests.iml @@ -15,6 +15,7 @@ + \ No newline at end of file diff --git a/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/test/org/jetbrains/kotlin/idea/fir/grazie/KotlinGrazieSupportTest28.kt b/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/test/org/jetbrains/kotlin/idea/fir/grazie/KotlinGrazieSupportTest28.kt index 38fe1f6a1713..16d8eae67ab2 100644 --- a/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/test/org/jetbrains/kotlin/idea/fir/grazie/KotlinGrazieSupportTest28.kt +++ b/plugins/kotlin/grazie/intellij.kotlin.grazie.tests/test/org/jetbrains/kotlin/idea/fir/grazie/KotlinGrazieSupportTest28.kt @@ -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 { + // tagret + val tagret = 1 + } + """) + myFixture.checkHighlighting() + val intention = myFixture.findSingleIntention("Typo: Rename to 'target'") + myFixture.launchAction(intention) + myFixture.checkResult(""" + class A { + // target + val target = 1 + } + """) + } } \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/grazie/KotlinGrazieSupportTest.kt b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/grazie/KotlinGrazieSupportTest.kt index 6847976d68c4..42989b378faf 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/grazie/KotlinGrazieSupportTest.kt +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/grazie/KotlinGrazieSupportTest.kt @@ -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 { + // tagret + val tagret = 1 + } + """) + myFixture.checkHighlighting() + val intention = myFixture.findSingleIntention("Typo: Rename to 'target'") + myFixture.launchAction(intention) + myFixture.checkResult(""" + class A { + // target + val target = 1 + } + """) + } } diff --git a/plugins/kotlin/idea/tests/testData/grazie/Constructs.kt b/plugins/kotlin/idea/tests/testData/grazie/Constructs.kt index ac948b85cff5..674a127b7102 100644 --- a/plugins/kotlin/idea/tests/testData/grazie/Constructs.kt +++ b/plugins/kotlin/idea/tests/testData/grazie/Constructs.kt @@ -2,6 +2,17 @@ package ide.language.kotlin +interface A { + val typpo: Int + fun typpo(typpo: Int) +} + +class B : A { + // typos are ignored because of `override` keyword + override val typpo: Int = 0 + override fun typpo(typpo: Int) { } +} + val variableWithEror = "error" fun erorFunction(eror: Int) {} diff --git a/plugins/kotlin/spellchecker/src/org/jetbrains/kotlin/idea/spellchecker/KotlinSpellcheckingStrategy.kt b/plugins/kotlin/spellchecker/src/org/jetbrains/kotlin/idea/spellchecker/KotlinSpellcheckingStrategy.kt index 1d4baf51c125..9cc8893926f2 100644 --- a/plugins/kotlin/spellchecker/src/org/jetbrains/kotlin/idea/spellchecker/KotlinSpellcheckingStrategy.kt +++ b/plugins/kotlin/spellchecker/src/org/jetbrains/kotlin/idea/spellchecker/KotlinSpellcheckingStrategy.kt @@ -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 = TokenizerBase(PlainTextSplitter.getInstance()) + private val codeTokenizer: Tokenizer = object : TokenizerBase(PlainTextSplitter.getInstance()) { + override fun consumeToken(element: PsiElement, consumer: TokenConsumer, splitter: Splitter) { + consumer.consumeToken(element, true, splitter) + } + } private val emptyTokenizer: Tokenizer = EMPTY_TOKENIZER override fun getTokenizer(element: PsiElement): Tokenizer { 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 = when { + parent.hasModifier(KtTokens.OVERRIDE_KEYWORD) -> emptyTokenizer + else -> codeTokenizer } + + override fun useTextLevelSpellchecking(): Boolean = Registry.`is`("spellchecker.grazie.enabled", false) } diff --git a/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java b/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java index e1e42868c5e4..9cebb08ae8de 100644 --- a/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java +++ b/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java @@ -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) {