IJPL-199764 Performance improvements for spellchecking with diacritics

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

GitOrigin-RevId: ee95020c702b3d51fffadd340403025ce8464385
This commit is contained in:
Ilia Permiashkin
2025-08-20 10:22:51 +00:00
committed by intellij-monorepo-bot
parent d4ed7bca2c
commit d9d1d93e69
9 changed files with 1191 additions and 3 deletions

View File

@@ -40,6 +40,7 @@
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="com.intellij.grazie.GrazieConfig"/>
<spellchecker.languages implementation="com.intellij.grazie.spellcheck.GrazieConfigNaturalLanguagesProvider"/>
<backgroundPostStartupActivity implementation="com.intellij.grazie.ide.notification.GrazieNotificationComponent"/>

View File

@@ -0,0 +1,12 @@
package com.intellij.grazie.spellcheck
import ai.grazie.nlp.langs.Language
import com.intellij.grazie.GrazieConfig
import com.intellij.spellchecker.grazie.NaturalLanguagesProvider
class GrazieConfigNaturalLanguagesProvider : NaturalLanguagesProvider {
override fun getEnabledLanguages(): Set<Language> =
GrazieConfig.get().enabledLanguages
.mapNotNull { lang -> Language.entries.find { lang.iso == it.iso } }
.toSet()
}

View File

@@ -2,6 +2,7 @@
package com.intellij.grazie.ide.language
import com.intellij.grazie.GrazieTestBase
import com.intellij.grazie.jlanguage.Lang
import com.intellij.openapi.components.service
import com.intellij.spellchecker.grazie.GrazieSpellCheckerEngine
import com.intellij.tools.ide.metrics.benchmark.Benchmark
@@ -11,7 +12,7 @@ class JSONSupportTest : GrazieTestBase() {
runHighlightTestForFile("ide/language/json/Example.json")
}
fun `test json typos spellcheck performance`() {
fun `test code-like and non-english words in json spellcheck performance`() {
Benchmark.newBenchmark("Highlight typos in i18n.json file") {
runHighlightTestForFile("ide/language/json/i18n.json")
}.setup {
@@ -19,4 +20,14 @@ class JSONSupportTest : GrazieTestBase() {
project.service<GrazieSpellCheckerEngine>().dropSuggestionCache()
}.start()
}
fun `test json spellcheck performance with English only`() {
configureGrazieSettings(enabledLanguages = setOf(Lang.AMERICAN_ENGLISH))
Benchmark.newBenchmark("Highlight typos without suggestions in i18n.json file") {
runHighlightTestForFile("ide/language/json/i18n_eng.json")
}.setup {
psiManager.dropPsiCaches()
project.service<GrazieSpellCheckerEngine>().dropSuggestionCache()
}.start()
}
}

View File

@@ -2,6 +2,7 @@
package com.intellij.grazie.ide.language
import com.intellij.grazie.GrazieTestBase
import com.intellij.grazie.jlanguage.Lang
import com.intellij.openapi.components.service
import com.intellij.openapi.vfs.encoding.EncodingProjectManager
import com.intellij.spellchecker.grazie.GrazieSpellCheckerEngine
@@ -20,6 +21,7 @@ class PropertiesSupportTest : GrazieTestBase() {
}
fun `test properties typos spellcheck performance`() {
configureGrazieSettings(enabledLanguages = setOf(Lang.AMERICAN_ENGLISH))
Benchmark.newBenchmark("Highlight typos in i18n.properties file") {
runHighlightTestForFile("ide/language/properties/i18n.properties")
}.setup {

View File

@@ -2,6 +2,7 @@
package com.intellij.grazie.ide.language
import com.intellij.grazie.GrazieTestBase
import com.intellij.grazie.jlanguage.Lang
import com.intellij.grazie.text.TextContent
import com.intellij.grazie.text.TextExtractor
import com.intellij.openapi.components.service
@@ -19,6 +20,7 @@ class YamlSupportTest : GrazieTestBase() {
}
fun `test yaml typos spellcheck performance`() {
configureGrazieSettings(setOf(Lang.AMERICAN_ENGLISH))
Benchmark.newBenchmark("Highlight typos in i18n.yaml file") {
runHighlightTestForFile("ide/language/yaml/i18n.yaml")
}.setup {

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@
<extensionPoint name="spellchecker.dictionaryLayersProvider" interface="com.intellij.spellchecker.DictionaryLayersProvider" dynamic="true"/>
<extensionPoint name="spellchecker.quickFixFactory" interface="com.intellij.spellchecker.quickfixes.SpellCheckerQuickFixFactory" dynamic="true"/>
<extensionPoint name="spellchecker.lifecycle" interface="com.intellij.spellchecker.grazie.SpellcheckerLifecycle" dynamic="true"/>
<extensionPoint name="spellchecker.languages" interface="com.intellij.spellchecker.grazie.NaturalLanguagesProvider" dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">

View File

@@ -0,0 +1,28 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.spellchecker.grazie
import ai.grazie.nlp.langs.Language
import com.intellij.openapi.extensions.ExtensionPointName
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
@ApiStatus.ScheduledForRemoval
@Deprecated("This interface won't be present in 2025.3")
interface NaturalLanguagesProvider {
/**
* Returns the set of enabled languages.
*/
fun getEnabledLanguages(): Set<Language>
companion object {
private val EP_NAME = ExtensionPointName<NaturalLanguagesProvider>("com.intellij.spellchecker.languages")
fun getEnabledLanguages(): Set<Language> = EP_NAME.extensionList
.asSequence()
.map { it.getEnabledLanguages() }
.filter { it.isNotEmpty() }
.flatten()
.toSet()
}
}

View File

@@ -1,10 +1,10 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.spellchecker.inspections;
import ai.grazie.nlp.langs.Language;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageNamesValidation;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.lang.refactoring.NamesValidator;
@@ -22,7 +22,10 @@ import com.intellij.psi.PsiReference;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.refactoring.rename.RenameUtil;
import com.intellij.spellchecker.SpellCheckerManager;
import com.intellij.spellchecker.grazie.GrazieSpellCheckerEngine;
import com.intellij.spellchecker.grazie.NaturalLanguagesProvider;
import com.intellij.spellchecker.grazie.diacritic.Diacritics;
import com.intellij.spellchecker.settings.SpellCheckerSettings;
import com.intellij.spellchecker.tokenizer.LanguageSpellchecking;
import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy;
import com.intellij.spellchecker.tokenizer.SuppressibleSpellcheckingStrategy;
@@ -30,12 +33,14 @@ import com.intellij.spellchecker.tokenizer.TokenConsumer;
import com.intellij.spellchecker.util.SpellCheckerBundle;
import com.intellij.util.Consumer;
import com.intellij.util.containers.CollectionFactory;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.StringSearcher;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -308,7 +313,7 @@ public final class SpellCheckingInspection extends LocalInspectionTool implement
// If the word isn't "code" or contains letters outside the English alphabet,
// then diacritic check should be skipped
if (!myCodeLike || NON_ENGLISH_LETTERS.matcher(word).matches()) {
if (!myCodeLike || NON_ENGLISH_LETTERS.matcher(word).matches() || isOnlyEnglishDictionaryEnabled(myElement.getProject())) {
return true;
}
@@ -318,6 +323,21 @@ public final class SpellCheckingInspection extends LocalInspectionTool implement
.filter(suggestion -> RenameUtil.isValidName(project, myElement, suggestion))
.noneMatch(suggestion -> Diacritics.equalsIgnoringDiacritics(word, suggestion));
}
private static boolean isOnlyEnglishDictionaryEnabled(Project project) {
Set<Language> languages = NaturalLanguagesProvider.Companion.getEnabledLanguages();
if (languages.size() != 1 || !languages.contains(Language.ENGLISH)) {
return false;
}
SpellCheckerSettings settings = SpellCheckerSettings.getInstance(project);
List<String> paths = settings.getCustomDictionariesPaths();
if (paths != null && !paths.isEmpty()) {
GrazieSpellCheckerEngine engine = project.getService(GrazieSpellCheckerEngine.class);
return !ContainerUtil.exists(paths, dictionaryName -> engine.isDictionaryLoad(dictionaryName));
}
return true;
}
}
private static void registerProblem(@NotNull ProblemsHolder holder,