diff --git a/plugins/grazie/resources/META-INF/plugin.xml b/plugins/grazie/resources/META-INF/plugin.xml
index 31e105abef90..03202d97ef58 100644
--- a/plugins/grazie/resources/META-INF/plugin.xml
+++ b/plugins/grazie/resources/META-INF/plugin.xml
@@ -111,6 +111,7 @@
+
diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/cloud/DependencyParser.kt b/plugins/grazie/src/main/kotlin/com/intellij/grazie/cloud/DependencyParser.kt
index d7519e65eff8..ba80a30f4273 100644
--- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/cloud/DependencyParser.kt
+++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/cloud/DependencyParser.kt
@@ -144,7 +144,7 @@ object DependencyParser {
private class Batcher(language: Language): SentenceBatcher(language, TreeSupport.CLOUD_BATCH_SIZE, quoteMarkup = true) {
override suspend fun parse(sentences: List, project: Project): Map {
- if (GrazieCloudConnector.EP_NAME.extensionList.any { it.isAfterRecentGecError() }) {
+ if (GrazieCloudConnector.isAfterRecentGecError()) {
return emptyMap()
}
val support = obtainSupport(language) ?: return emptyMap()
diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/cloud/GrazieCloudConnector.kt b/plugins/grazie/src/main/kotlin/com/intellij/grazie/cloud/GrazieCloudConnector.kt
index 80ec987a3b8e..01038fe9ec1e 100644
--- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/cloud/GrazieCloudConnector.kt
+++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/cloud/GrazieCloudConnector.kt
@@ -46,7 +46,12 @@ interface GrazieCloudConnector {
/**
* Returns machine learning errors for the given [sentences] in the given [language].
*/
- suspend fun mlec(sentences: List, lang: Language, project: Project): List?
+ suspend fun mlec(sentences: List, language: Language, project: Project): List?
+
+ /**
+ * Returns spelling errors for the given [text] in the given [language].
+ */
+ suspend fun spell(sentences: List, language: Language, project: Project): List?
/**
* Subscribe to authorization state change events.
@@ -57,5 +62,7 @@ interface GrazieCloudConnector {
val EP_NAME = ExtensionPointName("com.intellij.grazie.cloudConnector")
fun seemsCloudConnected(): Boolean = EP_NAME.extensionList.any { it.seemsCloudConnected() }
+
+ fun isAfterRecentGecError(): Boolean = EP_NAME.extensionList.any { it.isAfterRecentGecError() }
}
}
\ No newline at end of file
diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/mlec/MlecChecker.kt b/plugins/grazie/src/main/kotlin/com/intellij/grazie/mlec/MlecChecker.kt
index 7bc49de08f51..9e83d2085ef3 100644
--- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/mlec/MlecChecker.kt
+++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/mlec/MlecChecker.kt
@@ -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 {
- val subText = text.subText(TextRange(stripPrefixLength, text.length)) ?: return emptyList()
- val sentences = tokenize(subText)
- val parsed: Map? = runMlec(sentences, language, text.containingFile.viewProvider)
- if (parsed.isNullOrEmpty()) return emptyList()
-
- val result = ArrayList()
- 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, language: Language,
- vp: FileViewProvider,
- ): Map? {
- val queries = sentences.map { it.swe() }
- if (!GrazieCloudConnector.seemsCloudConnected()) {
- return LinkedHashMap()
- }
-
- return try {
- val parser = service().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>() {
+ private class MlecServerBatcherHolder : LanguageHolder>() {
private class ServerBatcher(
language: Language,
) : SentenceBatcher(language, 32), Disposable {
override suspend fun parse(sentences: List, project: Project): Map {
- if (GrazieCloudConnector.EP_NAME.extensionList.any { it.isAfterRecentGecError() }) {
+ if (GrazieCloudConnector.isAfterRecentGecError()) {
return emptyMap()
}
return GrazieCloudConnector.EP_NAME.extensionList
diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/rule/SentenceBatcher.kt b/plugins/grazie/src/main/kotlin/com/intellij/grazie/rule/SentenceBatcher.kt
index 21efed6e496d..6885acb1b5d2 100644
--- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/rule/SentenceBatcher.kt
+++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/rule/SentenceBatcher.kt
@@ -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(val language: Language, private val batchSize: Int, private val quoteMarkup: Boolean = false) : Disposable {
@Volatile
@@ -33,7 +39,7 @@ abstract class SentenceBatcher(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, project: Project): Map
+ protected abstract suspend fun parse(sentences: List, project: Project): Map?
override fun dispose() {
executorScope.cancel()
@@ -131,7 +137,8 @@ abstract class SentenceBatcher(val language: Language, private val batchSize:
}
private suspend fun parseAndCache(batch: LinkedHashSet): Map {
- val parsed: Map = 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(val language: Language, private val batchSize:
fun findInstalledLTLanguage(language: Language): org.languagetool.Language? {
return HighlightingUtil.findInstalledLang(language)?.jLanguage
}
+
+ @JvmStatic
+ suspend fun >> runWithSentenceBatcher(
+ sentences: List,
+ language: Language,
+ vp: FileViewProvider,
+ parserClass: Class
+ ): Map? {
+ 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()
+ }
+ }
}
}
\ No newline at end of file
diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/spellcheck/GrazieSpellCheckingInspection.java b/plugins/grazie/src/main/kotlin/com/intellij/grazie/spellcheck/GrazieSpellCheckingInspection.java
index e2a9af8ab675..bbf77fff8c58 100644
--- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/spellcheck/GrazieSpellCheckingInspection.java
+++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/spellcheck/GrazieSpellCheckingInspection.java
@@ -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 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 suggestions) {
if (holder.isOnTheFly()) {
- addRegularDescriptor(element, range, holder, useRename, word);
+ addRegularDescriptor(element, range, holder, useRename, word, suggestions);
}
else {
addBatchDescriptor(element, range, word, holder);
diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/spellcheck/GrazieTextLevelSpellCheckingExtension.kt b/plugins/grazie/src/main/kotlin/com/intellij/grazie/spellcheck/GrazieTextLevelSpellCheckingExtension.kt
index fef1dd101545..2b16ea1165a4 100644
--- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/spellcheck/GrazieTextLevelSpellCheckingExtension.kt
+++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/spellcheck/GrazieTextLevelSpellCheckingExtension.kt
@@ -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, element: PsiElement): List {
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 {
+ private fun findTypos(text: TextContent, session: LocalInspectionToolSession, textSpeller: TextSpeller, project: Project): List {
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, project: Project): List {
+ 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()
+ .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) = object : SpellingTypo {
override val word: String = word
override val range: TextRange = range
override val element: PsiElement = element
+ override val fixes: LinkedSet = lazyFixes()
+ }
+
+ private class CloudTypo(override val word: String, override val range: ai.grazie.text.TextRange, override val fixes: LinkedSet): Typo
+
+ @Service
+ private class SpellServerBatcherHolder : LanguageHolder>() {
+ private class ServerBatcher(
+ language: Language,
+ ) : SentenceBatcher(language, 32), Disposable {
+ override suspend fun parse(sentences: List, project: Project): Map? {
+ 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
}
enum class SpellCheckingResult { Checked, Ignored }
diff --git a/plugins/grazie/src/main/kotlin/com/intellij/grazie/utils/GrazieUtils.kt b/plugins/grazie/src/main/kotlin/com/intellij/grazie/utils/GrazieUtils.kt
index c1a28f6c2cc5..756532b35a0f 100644
--- a/plugins/grazie/src/main/kotlin/com/intellij/grazie/utils/GrazieUtils.kt
+++ b/plugins/grazie/src/main/kotlin/com/intellij/grazie/utils/GrazieUtils.kt
@@ -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 >> getProblems(context: ProofreadingContext, parserClass: Class): List? {
+ 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()
+ 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)
diff --git a/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.kt b/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.kt
index 03d2be6f79ff..45b5a064995c 100644
--- a/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.kt
+++ b/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.kt
@@ -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? = null,
+) : DefaultIntentionActionWithChoice, LazySuggestions(typo, suggestions) {
private val pointer = SmartPointerManager.getInstance(element.project).createSmartPsiElementPointer(element, element.containingFile)
diff --git a/spellchecker/src/com/intellij/spellchecker/quickfixes/LazySuggestions.java b/spellchecker/src/com/intellij/spellchecker/quickfixes/LazySuggestions.java
index 53b1db1a5308..bf42bb560a0c 100644
--- a/spellchecker/src/com/intellij/spellchecker/quickfixes/LazySuggestions.java
+++ b/spellchecker/src/com/intellij/spellchecker/quickfixes/LazySuggestions.java
@@ -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 suggestions;
private boolean processed;
protected final String typo;
- public LazySuggestions(String typo) {
+ public LazySuggestions(String typo, @Nullable Set suggestions) {
this.typo = typo;
+ if (suggestions != null) {
+ this.suggestions = List.copyOf(suggestions);
+ this.processed = !this.suggestions.isEmpty();
+ }
}
public @NotNull List getSuggestions(Project project) {
diff --git a/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFixFactory.kt b/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFixFactory.kt
index cad40d6730e3..f5061f4c6b16 100644
--- a/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFixFactory.kt
+++ b/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFixFactory.kt
@@ -25,9 +25,15 @@ abstract class SpellCheckerQuickFixFactory {
@JvmStatic
@JvmOverloads
- fun changeToVariants(element: PsiElement, rangeInElement: TextRange, word: String, tracker: SpellcheckerRateTracker? = null): List {
+ fun changeToVariants(
+ element: PsiElement,
+ rangeInElement: TextRange,
+ word: String,
+ tracker: SpellcheckerRateTracker? = null,
+ suggestions: Set? = null,
+ ): List {
return EP_NAME.extensionList.firstNotNullOfOrNull { it.createChangeToVariantsFixes(element, rangeInElement, word) }
- ?: ChangeTo(word, element, rangeInElement, tracker).getAllAsFixes()
+ ?: ChangeTo(word, element, rangeInElement, tracker, suggestions).getAllAsFixes()
}
@JvmStatic
diff --git a/spellchecker/src/com/intellij/spellchecker/tokenizer/SpellcheckingStrategy.java b/spellchecker/src/com/intellij/spellchecker/tokenizer/SpellcheckingStrategy.java
index d3c6b30ec1de..96c1897e7458 100644
--- a/spellchecker/src/com/intellij/spellchecker/tokenizer/SpellcheckingStrategy.java
+++ b/spellchecker/src/com/intellij/spellchecker/tokenizer/SpellcheckingStrategy.java
@@ -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 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 suggestions) {
ArrayList 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 fixes = SpellCheckerQuickFixFactory.changeToVariants(element, range, typo, tracker);
+ List fixes = SpellCheckerQuickFixFactory.changeToVariants(element, range, typo, tracker, suggestions);
result.addAll(fixes);
}