mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
co-authored by
Peter Gromov
parent
e42aade206
commit
1dc57b7167
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+15
-6
@@ -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);
|
||||
|
||||
+96
-13
@@ -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) {
|
||||
|
||||
+8
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user