[spellchecker] allow SpellCheckerStrategy to decide whether psi element fits or doesn't spell checker scope, pass this scope to getTokenizer

Some backend-driven languages in Rider don't have detailed frontend lexer/parser, so the only node visible to spellchecker will be root PsiFile. We pass range-scope information from backend, and then rider's spell checking strategy will handle it itself.

GitOrigin-RevId: b97c9eaaed30d6a0e09d69b13cf4cd5ad1b5d364
This commit is contained in:
Den Mukhametianov
2024-01-10 10:49:24 +01:00
committed by intellij-monorepo-bot
parent 86c8fa3545
commit 75284da775
3 changed files with 69 additions and 24 deletions

View File

@@ -185,7 +185,7 @@ public abstract class SpellCheckerDictionaryGenerator {
addSeenWord(seenNames, word, language);
});
}
});
}, null);
}
protected void addSeenWord(HashSet<String> seenNames, String word, Language language) {

View File

@@ -12,7 +12,6 @@ import com.intellij.openapi.util.registry.Registry;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.psi.tree.IElementType;
import com.intellij.spellchecker.SpellCheckerManager;
import com.intellij.spellchecker.quickfixes.SpellCheckerQuickFix;
import com.intellij.spellchecker.tokenizer.*;
@@ -23,6 +22,7 @@ import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
import static com.intellij.codeInspection.options.OptPane.checkbox;
@@ -73,6 +73,7 @@ public final class SpellCheckingInspection extends LocalInspectionTool {
return PsiElementVisitor.EMPTY_VISITOR;
}
final SpellCheckerManager manager = SpellCheckerManager.getInstance(holder.getProject());
var scope = buildAllowedScopes();
return new PsiElementVisitor() {
@Override
@@ -84,51 +85,55 @@ public final class SpellCheckingInspection extends LocalInspectionTool {
return;
}
// Extract parser definition from element
final Language language = element.getLanguage();
final IElementType elementType = node.getElementType();
final ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(language);
var strategy = getSpellcheckingStrategy(element, language);
if(strategy == null)
return;
// Handle selected options
if (parserDefinition != null) {
if (parserDefinition.getStringLiteralElements().contains(elementType)) {
if (!processLiterals) {
return;
}
}
else if (parserDefinition.getCommentTokens().contains(elementType)) {
if (!processComments) {
return;
}
}
else if (!processCode) {
return;
}
}
if(!strategy.elementFitsScope(element, scope))
return;
PsiFile containingFile = holder.getFile();
if (Boolean.TRUE.equals(containingFile.getUserData(InjectedLanguageManager.FRANKENSTEIN_INJECTION))) {
return;
}
tokenize(element, language, new MyTokenConsumer(manager, holder, LanguageNamesValidation.INSTANCE.forLanguage(language)));
tokenize(element, language, new MyTokenConsumer(manager, holder, LanguageNamesValidation.INSTANCE.forLanguage(language)), scope);
}
};
}
private Set<SpellCheckingScope> buildAllowedScopes() {
var result = new HashSet<SpellCheckingScope>();
if(processLiterals)
result.add(SpellCheckingScope.Literals);
if(processComments)
result.add(SpellCheckingScope.Comments);
if(processCode)
result.add(SpellCheckingScope.Code);
return result;
}
/**
* Splits element text in tokens according to spell checker strategy of given language
*
* @param element Psi element
* @param allowedScopes
* @param language Usually element.getLanguage()
* @param consumer the consumer of tokens
*/
public static void tokenize(final @NotNull PsiElement element, final @NotNull Language language, TokenConsumer consumer) {
public static void tokenize(@NotNull final PsiElement element,
@NotNull final Language language,
TokenConsumer consumer, Set<SpellCheckingScope> allowedScopes) {
SpellcheckingStrategy factoryByLanguage = getSpellcheckingStrategy(element, language);
if (factoryByLanguage == null) {
return;
}
Tokenizer tokenizer = factoryByLanguage.getTokenizer(element);
tokenize(factoryByLanguage, element, consumer, allowedScopes);
}
private static void tokenize(SpellcheckingStrategy strategy, PsiElement element, TokenConsumer consumer, Set<SpellCheckingScope> allowedScopes) {
var tokenizer = strategy.getTokenizer(element, allowedScopes);
//noinspection unchecked
tokenizer.tokenize(element, consumer);
}
@@ -239,4 +244,10 @@ public final class SpellCheckingInspection extends LocalInspectionTool {
}
}
}
public enum SpellCheckingScope {
Comments,
Literals,
Code,
}
}

View File

@@ -3,15 +3,20 @@ package com.intellij.spellchecker.tokenizer;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.SuppressionUtil;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageParserDefinitions;
import com.intellij.lang.ParserDefinition;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.impl.CustomSyntaxTableFileType;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.spellchecker.DictionaryLevel;
import com.intellij.spellchecker.inspections.PlainTextSplitter;
import com.intellij.spellchecker.inspections.SpellCheckingInspection;
import com.intellij.spellchecker.quickfixes.ChangeTo;
import com.intellij.spellchecker.quickfixes.RenameTo;
import com.intellij.spellchecker.quickfixes.SaveTo;
@@ -22,6 +27,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Set;
/**
* Defines spellchecking support for a custom language.
@@ -50,6 +56,10 @@ public class SpellcheckingStrategy {
private static final SpellCheckerQuickFix[] BATCH_FIXES =
new SpellCheckerQuickFix[]{SaveTo.getSaveToLevelFix(DictionaryLevel.APP), SaveTo.getSaveToLevelFix(DictionaryLevel.PROJECT)};
public Tokenizer getTokenizer(PsiElement element, Set<SpellCheckingInspection.SpellCheckingScope> scope) {
return getTokenizer(element);
}
/**
* @return {@link #EMPTY_TOKENIZER} to skip spellchecking, {@link #TEXT_TOKENIZER} for full element text or custom Tokenizer implementation.
*/
@@ -82,6 +92,30 @@ public class SpellcheckingStrategy {
return EMPTY_TOKENIZER;
}
public boolean elementFitsScope(@NotNull PsiElement element, Set<SpellCheckingInspection.SpellCheckingScope> scope) {
final Language language = element.getLanguage();
final IElementType elementType = element.getNode().getElementType();
final ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(language);
if (parserDefinition != null) {
if (parserDefinition.getStringLiteralElements().contains(elementType)) {
if (!scope.contains(SpellCheckingInspection.SpellCheckingScope.Literals)) {
return false;
}
}
else if (parserDefinition.getCommentTokens().contains(elementType)) {
if (!scope.contains(SpellCheckingInspection.SpellCheckingScope.Comments)) {
return false;
}
}
else if (!scope.contains(SpellCheckingInspection.SpellCheckingScope.Code)) {
return false;
}
}
return true;
}
protected static boolean isInjectedLanguageFragment(@Nullable PsiElement element) {
return element instanceof PsiLanguageInjectionHost
&& InjectedLanguageUtil.hasInjections((PsiLanguageInjectionHost)element);