[grazie] IJPL-203135 Perform text spellchecking in the cloud if available

Co-authored-by: Peter Gromov <peter.gromov@jetbrains.com>

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

GitOrigin-RevId: 92e5ca3aa506b7650069ec0aa08b9259242c9a01
This commit is contained in:
Ilia Permiashkin
2025-10-09 08:39:29 +00:00
committed by intellij-monorepo-bot
co-authored by Peter Gromov
parent e42aade206
commit 1dc57b7167
12 changed files with 217 additions and 88 deletions
@@ -111,6 +111,7 @@
<nameSuggestionProvider id="SpellcheckingSuggestionProvider" implementation="com.intellij.grazie.spellcheck.suggestion.SpellcheckingNameSuggestionProvider" order="first"/>
<registryKey key="spellchecker.grazie.enabled" defaultValue="true" description="Enables Grazie text-level spellchecking"/>
<registryKey key="spellchecker.cloud.enabled" defaultValue="false" description="Enables spellchecking via Cloud when it is available"/>
<registryKey key="grazie.skip.precommit.checks"
defaultValue="true"
description="Controls whether expensive linguistic checks should be performed during pre-commit code analysis"/>
@@ -144,7 +144,7 @@ object DependencyParser {
private class Batcher(language: Language): SentenceBatcher<Tree>(language, TreeSupport.CLOUD_BATCH_SIZE, quoteMarkup = true) {
override suspend fun parse(sentences: List<SentenceWithExclusions>, project: Project): Map<SentenceWithExclusions, Tree> {
if (GrazieCloudConnector.EP_NAME.extensionList.any { it.isAfterRecentGecError() }) {
if (GrazieCloudConnector.isAfterRecentGecError()) {
return emptyMap()
}
val support = obtainSupport(language) ?: return emptyMap()
@@ -46,7 +46,12 @@ interface GrazieCloudConnector {
/**
* Returns machine learning errors for the given [sentences] in the given [language].
*/
suspend fun mlec(sentences: List<SentenceWithExclusions>, lang: Language, project: Project): List<SentenceWithProblems>?
suspend fun mlec(sentences: List<SentenceWithExclusions>, language: Language, project: Project): List<SentenceWithProblems>?
/**
* Returns spelling errors for the given [text] in the given [language].
*/
suspend fun spell(sentences: List<SentenceWithExclusions>, language: Language, project: Project): List<SentenceWithProblems>?
/**
* Subscribe to authorization state change events.
@@ -57,5 +62,7 @@ interface GrazieCloudConnector {
val EP_NAME = ExtensionPointName<GrazieCloudConnector>("com.intellij.grazie.cloudConnector")
fun seemsCloudConnected(): Boolean = EP_NAME.extensionList.any { it.seemsCloudConnected() }
fun isAfterRecentGecError(): Boolean = EP_NAME.extensionList.any { it.isAfterRecentGecError() }
}
}
@@ -1,6 +1,5 @@
package com.intellij.grazie.mlec
import ai.grazie.gec.model.problem.Problem
import ai.grazie.gec.model.problem.SentenceWithProblems
import ai.grazie.nlp.langs.Language
import ai.grazie.nlp.langs.locale
@@ -10,23 +9,15 @@ import com.intellij.codeInspection.util.InspectionMessage
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.cloud.GrazieCloudConnector
import com.intellij.grazie.rule.SentenceBatcher
import com.intellij.grazie.rule.SentenceTokenizer.Sentence
import com.intellij.grazie.rule.SentenceTokenizer.tokenize
import com.intellij.grazie.text.*
import com.intellij.grazie.utils.*
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.FileViewProvider
import org.slf4j.LoggerFactory
import java.util.*
import kotlin.coroutines.cancellation.CancellationException
private val logger = LoggerFactory.getLogger(MlecChecker::class.java)
class MlecChecker : ExternalTextChecker() {
private val incompleteSentenceMessages = setOf(
@@ -46,7 +37,7 @@ class MlecChecker : ExternalTextChecker() {
val rules = Constants.mlecRules[context.language] ?: return emptyList()
if (rules.none { it.isCurrentlyEnabled(context.text) }) return emptyList()
val typos = getTypos(context.language, context.text, context.stripPrefix.length).takeIf { it.isNotEmpty() } ?: return emptyList()
val typos = getProblems(context, MlecServerBatcherHolder::class.java)?.takeIf { it.isNotEmpty() } ?: return emptyList()
return typos
.mapNotNull { typo ->
@@ -92,47 +83,6 @@ class MlecChecker : ExternalTextChecker() {
}
}
private suspend fun getTypos(language: Language, text: TextContent, stripPrefixLength: Int): List<Problem> {
val subText = text.subText(TextRange(stripPrefixLength, text.length)) ?: return emptyList()
val sentences = tokenize(subText)
val parsed: Map<SentenceWithExclusions, SentenceWithProblems?>? = runMlec(sentences, language, text.containingFile.viewProvider)
if (parsed.isNullOrEmpty()) return emptyList()
val result = ArrayList<Problem>()
for (sentence in sentences) {
val corrections = parsed[sentence.swe()]?.problems ?: continue
val start = sentence.start + stripPrefixLength
if (!text.hasUnknownFragmentsIn(TextRange.from(start, sentence.text.trimEnd().length))) {
corrections.forEach {
result.add(it.withOffset(start))
}
}
}
return result
}
private suspend fun runMlec(
sentences: List<Sentence>, language: Language,
vp: FileViewProvider,
): Map<SentenceWithExclusions, SentenceWithProblems?>? {
val queries = sentences.map { it.swe() }
if (!GrazieCloudConnector.seemsCloudConnected()) {
return LinkedHashMap()
}
return try {
val parser = service<ServerBatcherHolder>().get(language)?.forFile(vp)
parser?.parseAsync(queries)
}
catch (e: CancellationException) {
throw e
}
catch (e: RuntimeException) {
logger.warn("Got exception from MLEC", e)
LinkedHashMap()
}
}
@Suppress("NonAsciiCharacters")
object Constants {
const val MLEC_RULE_PREFIX = "Grazie.MLEC"
@@ -189,12 +139,12 @@ class MlecChecker : ExternalTextChecker() {
}
@Service
private class ServerBatcherHolder : LanguageHolder<SentenceBatcher<SentenceWithProblems>>() {
private class MlecServerBatcherHolder : LanguageHolder<SentenceBatcher<SentenceWithProblems>>() {
private class ServerBatcher(
language: Language,
) : SentenceBatcher<SentenceWithProblems>(language, 32), Disposable {
override suspend fun parse(sentences: List<SentenceWithExclusions>, project: Project): Map<SentenceWithExclusions, SentenceWithProblems> {
if (GrazieCloudConnector.EP_NAME.extensionList.any { it.isAfterRecentGecError() }) {
if (GrazieCloudConnector.isAfterRecentGecError()) {
return emptyMap()
}
return GrazieCloudConnector.EP_NAME.extensionList
@@ -1,14 +1,19 @@
package com.intellij.grazie.rule
import ai.grazie.gec.model.problem.SentenceWithProblems
import ai.grazie.nlp.langs.Language
import ai.grazie.rules.util.BatchParser
import ai.grazie.text.exclusions.SentenceWithExclusions
import com.intellij.grazie.cloud.GrazieCloudConnector
import com.intellij.grazie.mlec.LanguageHolder
import com.intellij.grazie.rule.SentenceTokenizer.Sentence
import com.intellij.grazie.text.TextContent
import com.intellij.grazie.text.TextExtractor.findAllTextContents
import com.intellij.grazie.utils.HighlightingUtil
import com.intellij.grazie.utils.NaturalTextDetector
import com.intellij.grazie.utils.getLanguageIfAvailable
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.runBlockingCancellable
@@ -25,6 +30,7 @@ import com.intellij.util.containers.ContainerUtil
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.coroutines.cancellation.CancellationException
abstract class SentenceBatcher<T>(val language: Language, private val batchSize: Int, private val quoteMarkup: Boolean = false) : Disposable {
@Volatile
@@ -33,7 +39,7 @@ abstract class SentenceBatcher<T>(val language: Language, private val batchSize:
private val executorScope = CoroutineScope(SupervisorJob() + CoroutineName(name = "SentenceBatcherScope-${this::class.java.name}"))
private val taskMutex = Mutex()
protected abstract suspend fun parse(sentences: List<SentenceWithExclusions>, project: Project): Map<SentenceWithExclusions, T>
protected abstract suspend fun parse(sentences: List<SentenceWithExclusions>, project: Project): Map<SentenceWithExclusions, T>?
override fun dispose() {
executorScope.cancel()
@@ -131,7 +137,8 @@ abstract class SentenceBatcher<T>(val language: Language, private val batchSize:
}
private suspend fun parseAndCache(batch: LinkedHashSet<SentenceWithExclusions>): Map<SentenceWithExclusions, T?> {
val parsed: Map<SentenceWithExclusions, T?> = parse(ArrayList(batch), project)
val parsed = parse(ArrayList(batch), project)
if (parsed == null) return LinkedHashMap()
for (entry in parsed.entries) {
if (entry.value != null) {
globalCache[entry.key] = entry.value
@@ -196,6 +203,31 @@ abstract class SentenceBatcher<T>(val language: Language, private val batchSize:
fun findInstalledLTLanguage(language: Language): org.languagetool.Language? {
return HighlightingUtil.findInstalledLang(language)?.jLanguage
}
@JvmStatic
suspend fun <T : LanguageHolder<SentenceBatcher<SentenceWithProblems>>> runWithSentenceBatcher(
sentences: List<Sentence>,
language: Language,
vp: FileViewProvider,
parserClass: Class<T>
): Map<SentenceWithExclusions, SentenceWithProblems?>? {
val queries = sentences.map { it.swe() }
if (!GrazieCloudConnector.seemsCloudConnected() || GrazieCloudConnector.isAfterRecentGecError()) {
return LinkedHashMap()
}
return try {
val parser = ApplicationManager.getApplication().getService(parserClass).get(language)?.forFile(vp)
parser?.parseAsync(queries)
}
catch (e: CancellationException) {
throw e
}
catch (e: RuntimeException) {
LOG.warn("Got exception from $parserClass", e)
LinkedHashMap()
}
}
}
}
@@ -149,14 +149,14 @@ public final class GrazieSpellCheckingInspection extends SpellCheckingInspection
}
private static void addRegularDescriptor(@NotNull PsiElement element, @NotNull TextRange textRange, @NotNull ProblemsHolder holder,
boolean useRename, String wordWithTypo) {
boolean useRename, String wordWithTypo, Set<String> suggestions) {
SpellcheckingStrategy strategy = getSpellcheckingStrategy(element);
LocalQuickFix[] fixes = strategy != null
? strategy.getRegularFixes(element, textRange, useRename, wordWithTypo)
: SpellcheckingStrategy.getDefaultRegularFixes(useRename, wordWithTypo, element, textRange);
? strategy.getRegularFixes(element, textRange, useRename, wordWithTypo, suggestions)
: SpellcheckingStrategy.getDefaultRegularFixes(useRename, wordWithTypo, element, textRange, suggestions);
final ProblemDescriptor problemDescriptor = createProblemDescriptor(element, textRange, fixes, true);
ProblemDescriptor problemDescriptor = createProblemDescriptor(element, textRange, fixes, true);
holder.registerProblem(problemDescriptor);
}
@@ -362,7 +362,7 @@ public final class GrazieSpellCheckingInspection extends SpellCheckingInspection
}
private static void registerProblem(@NotNull SpellingTypo typo, @NotNull ProblemsHolder holder) {
registerProblem(holder, typo.getElement(), typo.getRange(), false, typo.getWord());
registerProblem(holder, typo.getElement(), typo.getRange(), false, typo.getWord(), typo.getFixes());
}
private static void registerProblem(@NotNull ProblemsHolder holder,
@@ -370,8 +370,17 @@ public final class GrazieSpellCheckingInspection extends SpellCheckingInspection
@NotNull TextRange range,
boolean useRename,
String word) {
registerProblem(holder, element, range, useRename, word, null);
}
private static void registerProblem(@NotNull ProblemsHolder holder,
@NotNull PsiElement element,
@NotNull TextRange range,
boolean useRename,
String word,
Set<String> suggestions) {
if (holder.isOnTheFly()) {
addRegularDescriptor(element, range, holder, useRename, word);
addRegularDescriptor(element, range, holder, useRename, word, suggestions);
}
else {
addBatchDescriptor(element, range, word, holder);
@@ -1,5 +1,7 @@
package com.intellij.grazie.spellcheck
import ai.grazie.gec.model.problem.ProblemFix
import ai.grazie.gec.model.problem.SentenceWithProblems
import ai.grazie.nlp.langs.Language
import ai.grazie.nlp.langs.LanguageWithVariant
import ai.grazie.nlp.tokenizer.Tokenizer
@@ -8,20 +10,34 @@ import ai.grazie.rules.common.KnownPhrases
import ai.grazie.spell.Speller
import ai.grazie.spell.text.TextSpeller
import ai.grazie.spell.text.Typo
import ai.grazie.text.exclusions.SentenceWithExclusions
import ai.grazie.utils.LinkedSet
import ai.grazie.utils.toLinkedSet
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.cloud.GrazieCloudConnector
import com.intellij.grazie.ide.inspection.grammar.GrazieInspection.Companion.sortByPriority
import com.intellij.grazie.mlec.LanguageHolder
import com.intellij.grazie.rule.SentenceBatcher
import com.intellij.grazie.spellcheck.engine.GrazieSpellCheckerEngine
import com.intellij.grazie.text.ProblemFilter
import com.intellij.grazie.text.TextContent
import com.intellij.grazie.text.TextExtractor
import com.intellij.grazie.utils.NaturalTextDetector
import com.intellij.grazie.utils.getProblems
import com.intellij.grazie.utils.toProofreadingContext
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.getOrCreateUserData
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.text.StringUtil.BombedCharSequence
import com.intellij.psi.PsiElement
import com.intellij.spellchecker.SpellCheckerManager
import com.intellij.spellchecker.inspections.IdentifierSplitter.MINIMAL_TYPO_LENGTH
import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy
import com.intellij.util.containers.ContainerUtil
@@ -58,7 +74,7 @@ object GrazieTextLevelSpellCheckingExtension {
val textSpeller = getTextSpeller(element.project) ?: return SpellCheckingResult.Ignored
filteredTexts.asSequence()
.map { it to findTypos(it, session, textSpeller) }
.map { it to findTypos(it, session, textSpeller, element.project) }
.flatMap { mapTypo(it.first, it.second, element) }
.filterNot { it.word.length < MINIMAL_TYPO_LENGTH }
.forEach { consumer.accept(it) }
@@ -99,30 +115,94 @@ object GrazieTextLevelSpellCheckingExtension {
private fun mapTypo(text: TextContent, typos: List<Typo>, element: PsiElement): List<SpellingTypo> {
val psiRange = element.textRange
return typos.mapNotNull {
val range = text.textRangeToFile(mapRange(it.range))
return typos.mapNotNull { typo ->
val range = text.textRangeToFile(mapRange(typo.range))
if (!psiRange.contains(range)) return@mapNotNull null
createTypo(it.word, range.shiftLeft(element.textRange.startOffset), element)
createTypo(typo.word, range.shiftLeft(element.textRange.startOffset), element) {
if (typo is CloudTypo) typo.fixes else LinkedSet()
}
}
}
private fun mapRange(range: ai.grazie.text.TextRange): TextRange = TextRange(range.start, range.endExclusive)
private fun findTypos(text: TextContent, session: LocalInspectionToolSession, textSpeller: TextSpeller): List<Typo> {
private fun findTypos(text: TextContent, session: LocalInspectionToolSession, textSpeller: TextSpeller, project: Project): List<Typo> {
val typos = session.getOrCreateUserData(KEY_TYPO_CACHE) { ConcurrentHashMap() }
return typos.computeIfAbsent(text) {
textSpeller.checkText(object : BombedCharSequence(text) {
override fun checkCanceled() {
ProgressManager.checkCanceled()
}
})
}
val detectedTypos = typos[text]
if (detectedTypos != null) return detectedTypos
val localTypos = textSpeller.checkText(object : BombedCharSequence(text) {
override fun checkCanceled() {
ProgressManager.checkCanceled()
}
})
return typos.computeIfAbsent(text) { findTyposInCloud(text, localTypos, project) }
}
private fun createTypo(word: String, range: TextRange, element: PsiElement) = object : SpellingTypo {
private fun findTyposInCloud(text: TextContent, localTypos: List<Typo>, project: Project): List<Typo> {
if (!Registry.`is`("spellchecker.cloud.enabled", false)
|| localTypos.isEmpty()
|| !GrazieCloudConnector.seemsCloudConnected()
|| GrazieCloudConnector.isAfterRecentGecError()
|| !NaturalTextDetector.seemsNatural(text.toString())) {
return localTypos
}
val context = text.toProofreadingContext()
val cloudTypos = runBlockingCancellable { getProblems(context, SpellServerBatcherHolder::class.java) }
if (cloudTypos == null) return localTypos
val manager = SpellCheckerManager.getInstance(project)
return cloudTypos
.mapNotNull {
val parts = it.fixes.flatMap { fix -> fix.parts.toList() }
.filterIsInstance<ProblemFix.Part.Change>()
.filter { part -> part.type == ProblemFix.Part.Change.ChangeType.REPLACE }
if (parts.isEmpty()) return@mapNotNull null
val word = parts.first().range.substring(text.toString())
if (!manager.hasProblem(word)) return@mapNotNull null
CloudTypo(word, parts.first().range, parts.map { part -> part.text }.toLinkedSet())
}
}
private fun createTypo(word: String, range: TextRange, element: PsiElement, lazyFixes: () -> LinkedSet<String>) = object : SpellingTypo {
override val word: String = word
override val range: TextRange = range
override val element: PsiElement = element
override val fixes: LinkedSet<String> = lazyFixes()
}
private class CloudTypo(override val word: String, override val range: ai.grazie.text.TextRange, override val fixes: LinkedSet<String>): Typo
@Service
private class SpellServerBatcherHolder : LanguageHolder<SentenceBatcher<SentenceWithProblems>>() {
private class ServerBatcher(
language: Language,
) : SentenceBatcher<SentenceWithProblems>(language, 32), Disposable {
override suspend fun parse(sentences: List<SentenceWithExclusions>, project: Project): Map<SentenceWithExclusions, SentenceWithProblems>? {
return GrazieCloudConnector.EP_NAME.extensionList
.firstNotNullOfOrNull { it.spell(sentences, language, project) }
?.zip(sentences)
?.associate { it.second to it.first }
}
override fun dispose() {}
init {
GrazieConfig.subscribe(this) { clearCache() }
GrazieCloudConnector.EP_NAME.forEachExtensionSafe { it.subscribeToAuthorizationStateEvents(this) { clearCache() } }
}
}
init {
update(mapOf(
Language.ENGLISH to ServerBatcher(Language.ENGLISH),
Language.UKRAINIAN to ServerBatcher(Language.UKRAINIAN),
Language.GERMAN to ServerBatcher(Language.GERMAN),
Language.RUSSIAN to ServerBatcher(Language.RUSSIAN)
))
}
}
}
@@ -136,6 +216,9 @@ interface SpellingTypo {
/** Element that contains a misspelled [word] within the given text [range] */
val element: PsiElement
/** Suggested corrections for the [word], possibly calculated lazily */
val fixes: LinkedSet<String>
}
enum class SpellCheckingResult { Checked, Ignored }
@@ -1,6 +1,8 @@
package com.intellij.grazie.utils
import ai.grazie.gec.model.problem.Problem
import ai.grazie.gec.model.problem.ProblemHighlighting
import ai.grazie.gec.model.problem.SentenceWithProblems
import ai.grazie.nlp.langs.Language
import ai.grazie.nlp.langs.Language.UNKNOWN
import ai.grazie.rules.Rule
@@ -11,8 +13,11 @@ import com.intellij.grazie.GrazieConfig
import com.intellij.grazie.detection.LangDetector
import com.intellij.grazie.ide.ui.configurable.StyleConfigurable.Companion.ruleEngineLanguages
import com.intellij.grazie.jlanguage.LangTool
import com.intellij.grazie.mlec.LanguageHolder
import com.intellij.grazie.rule.RuleIdeClient
import com.intellij.grazie.text.TextChecker
import com.intellij.grazie.rule.SentenceBatcher
import com.intellij.grazie.rule.SentenceBatcher.Companion.runWithSentenceBatcher
import com.intellij.grazie.rule.SentenceTokenizer.tokenize
import com.intellij.grazie.text.TextChecker.ProofreadingContext
import com.intellij.grazie.text.TextContent
import com.intellij.openapi.util.TextRange
@@ -65,6 +70,27 @@ fun TextContent.toProofreadingContext(): ProofreadingContext {
}
}
suspend fun <T : LanguageHolder<SentenceBatcher<SentenceWithProblems>>> getProblems(context: ProofreadingContext, parserClass: Class<T>): List<Problem>? {
val stripPrefixLength = context.stripPrefix.length
val subText = context.text.subText(TextRange(stripPrefixLength, context.text.length)) ?: return emptyList()
val sentences = tokenize(subText)
val parsed = runWithSentenceBatcher(sentences, context.language, context.text.containingFile.viewProvider, parserClass)
if (parsed == null) return null
if (parsed.isEmpty()) return emptyList()
val result = ArrayList<Problem>()
for (sentence in sentences) {
val corrections = parsed[sentence.swe()]?.problems ?: continue
val start = sentence.start + stripPrefixLength
if (!context.text.hasUnknownFragmentsIn(TextRange.from(start, sentence.text.trimEnd().length))) {
corrections.forEach {
result.add(it.withOffset(start))
}
}
}
return result
}
val ProblemHighlighting.underline: TextRange?
get() = GrazieTextRange.coveringIde(this.always)
@@ -31,7 +31,8 @@ internal class ChangeTo @JvmOverloads constructor(
element: PsiElement,
private val range: TextRange,
private val tracker: SpellcheckerRateTracker? = null,
) : DefaultIntentionActionWithChoice, LazySuggestions(typo) {
suggestions: Set<String>? = null,
) : DefaultIntentionActionWithChoice, LazySuggestions(typo, suggestions) {
private val pointer = SmartPointerManager.getInstance(element.project).createSmartPsiElementPointer(element, element.containingFile)
@@ -4,16 +4,22 @@ package com.intellij.spellchecker.quickfixes;
import com.intellij.openapi.project.Project;
import com.intellij.spellchecker.SpellCheckerManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
public abstract class LazySuggestions {
private List<String> suggestions;
private boolean processed;
protected final String typo;
public LazySuggestions(String typo) {
public LazySuggestions(String typo, @Nullable Set<String> suggestions) {
this.typo = typo;
if (suggestions != null) {
this.suggestions = List.copyOf(suggestions);
this.processed = !this.suggestions.isEmpty();
}
}
public @NotNull List<String> getSuggestions(Project project) {
@@ -25,9 +25,15 @@ abstract class SpellCheckerQuickFixFactory {
@JvmStatic
@JvmOverloads
fun changeToVariants(element: PsiElement, rangeInElement: TextRange, word: String, tracker: SpellcheckerRateTracker? = null): List<LocalQuickFix> {
fun changeToVariants(
element: PsiElement,
rangeInElement: TextRange,
word: String,
tracker: SpellcheckerRateTracker? = null,
suggestions: Set<String>? = null,
): List<LocalQuickFix> {
return EP_NAME.extensionList.firstNotNullOfOrNull { it.createChangeToVariantsFixes(element, rangeInElement, word) }
?: ChangeTo(word, element, rangeInElement, tracker).getAllAsFixes()
?: ChangeTo(word, element, rangeInElement, tracker, suggestions).getAllAsFixes()
}
@JvmStatic
@@ -26,10 +26,7 @@ import com.intellij.util.KeyedLazyInstance;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.regex.Pattern;
/**
@@ -144,11 +141,21 @@ public class SpellcheckingStrategy implements PossiblyDumbAware {
&& InjectedLanguageUtil.hasInjections((PsiLanguageInjectionHost)element);
}
// Used by 3rd party plugins
@SuppressWarnings("unused")
public LocalQuickFix[] getRegularFixes(@NotNull PsiElement element,
@NotNull TextRange textRange,
boolean useRename,
String typo) {
return getDefaultRegularFixes(useRename, typo, element, textRange);
return getDefaultRegularFixes(useRename, typo, element, textRange, null);
}
public LocalQuickFix[] getRegularFixes(@NotNull PsiElement element,
@NotNull TextRange textRange,
boolean useRename,
String typo,
@Nullable Set<String> suggestions) {
return getDefaultRegularFixes(useRename, typo, element, textRange, suggestions);
}
public static SpellcheckingStrategy getSpellcheckingStrategy(@NotNull PsiElement element) {
@@ -164,14 +171,15 @@ public class SpellcheckingStrategy implements PossiblyDumbAware {
public static LocalQuickFix[] getDefaultRegularFixes(boolean useRename,
String typo,
@NotNull PsiElement element,
@NotNull TextRange range) {
@NotNull TextRange range,
@Nullable Set<String> suggestions) {
ArrayList<LocalQuickFix> result = new ArrayList<>();
SpellcheckerRateTracker tracker = new SpellcheckerRateTracker(element);
if (useRename && PsiTreeUtil.getNonStrictParentOfType(element, PsiNamedElement.class) != null) {
result.add(SpellCheckerQuickFixFactory.rename(typo, range, element, tracker));
} else {
List<LocalQuickFix> fixes = SpellCheckerQuickFixFactory.changeToVariants(element, range, typo, tracker);
List<LocalQuickFix> fixes = SpellCheckerQuickFixFactory.changeToVariants(element, range, typo, tracker, suggestions);
result.addAll(fixes);
}