From ec28b219f3947a61ba641eb783202fc6b07938b5 Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 14 Jan 2026 14:28:50 +0300 Subject: [PATCH] plugin: another attempt to fix spell checker --- .../idea/annotators/LyngExternalAnnotator.kt | 120 +-------------- .../idea/grazie/AddToLyngDictionaryFix.kt | 43 ------ .../lyng/idea/grazie/LyngGrazieStrategy.kt | 137 ------------------ .../lyng/idea/grazie/LyngTextExtractor.kt | 85 ++--------- .../lyng/idea/grazie/ReplaceWordFix.kt | 86 ----------- .../lyng/idea/psi/LyngElementTypes.kt | 27 ++++ .../lyng/idea/psi/LyngParserDefinition.kt | 65 ++++++++- .../idea/settings/LyngFormatterSettings.kt | 56 +------ .../LyngFormatterSettingsConfigurable.kt | 57 +------- .../lyng/idea/spell/LyngSpellIndex.kt | 50 ------- .../idea/spell/LyngSpellcheckingStrategy.kt | 21 +-- .../resources/META-INF/grazie-bundled.xml | 4 +- .../main/resources/META-INF/grazie-lite.xml | 4 +- 13 files changed, 116 insertions(+), 639 deletions(-) delete mode 100644 lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/AddToLyngDictionaryFix.kt delete mode 100644 lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieStrategy.kt delete mode 100644 lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/ReplaceWordFix.kt create mode 100644 lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/psi/LyngElementTypes.kt delete mode 100644 lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellIndex.kt diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt index 3634e1e..bb2a1ab 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/annotators/LyngExternalAnnotator.kt @@ -44,10 +44,7 @@ class LyngExternalAnnotator : ExternalAnnotator, val error: Error? = null, - val spellIdentifiers: List = emptyList(), - val spellComments: List = emptyList(), - val spellStrings: List = emptyList()) + data class Result(val modStamp: Long, val spans: List, val error: Error? = null) override fun collectInformation(file: PsiFile): Input? { val doc: Document = file.viewProvider.document ?: return null @@ -318,62 +315,6 @@ class LyngExternalAnnotator : ExternalAnnotator() - fun addSpellId(pos: net.sergeych.lyng.Pos, name: String) { - if (pos.source == source) { - val s = source.offsetOf(pos) - val e = (s + name.length).coerceAtMost(text.length) - if (s < e) spellIds.add(s until e) - } - } - - // Add declarations from MiniAst - mini.declarations.forEach { d -> - addSpellId(d.nameStart, d.name) - when (d) { - is MiniFunDecl -> { - d.params.forEach { addSpellId(it.nameStart, it.name) } - addTypeNames(d.returnType, ::addSpellId) - addTypeNames(d.receiver, ::addSpellId) - } - is MiniValDecl -> { - addTypeNames(d.type, ::addSpellId) - addTypeNames(d.receiver, ::addSpellId) - } - is MiniEnumDecl -> { - if (d.entries.size == d.entryPositions.size) { - for (i in d.entries.indices) { - addSpellId(d.entryPositions[i], d.entries[i]) - } - } - } - is MiniClassDecl -> { - d.ctorFields.forEach { addSpellId(it.nameStart, it.name) } - d.classFields.forEach { addSpellId(it.nameStart, it.name) } - d.members.forEach { m -> - when (m) { - is MiniMemberFunDecl -> { - addSpellId(m.nameStart, m.name) - m.params.forEach { addSpellId(it.nameStart, it.name) } - addTypeNames(m.returnType, ::addSpellId) - } - is MiniMemberValDecl -> { - addSpellId(m.nameStart, m.name) - addTypeNames(m.type, ::addSpellId) - } - else -> {} - } - } - } - else -> {} - } - } - - // Map Enum constants from token highlighter for highlighting only. - // We do NOT add them to spellIds here because they might be usages, - // and declarations are already handled via MiniEnumDecl above. tokens.forEach { s -> if (s.kind == HighlightKind.EnumConstant) { val start = s.range.start @@ -384,40 +325,9 @@ class LyngExternalAnnotator : ExternalAnnotator Unit) { - when (t) { - is MiniTypeName -> t.segments.forEach { add(it.range.start, it.name) } - is MiniGenericType -> { - addTypeNames(t.base, add) - t.args.forEach { addTypeNames(it, add) } - } - - is MiniFunctionType -> { - addTypeNames(t.receiver, add) - t.params.forEach { addTypeNames(it, add) } - addTypeNames(t.returnType, add) - } - - is MiniTypeVar -> { - // Type variables are declarations too - add(t.range.start, t.name) - } - - null -> {} - } - } override fun apply(file: PsiFile, annotationResult: Result?, holder: AnnotationHolder) { if (annotationResult == null) return @@ -429,32 +339,6 @@ class LyngExternalAnnotator : ExternalAnnotator = java.util.Collections.synchronizedSet(mutableSetOf()) - - private fun legacySpellcheckerInstalled(): Boolean = - PluginManagerCore.isPluginInstalled(PluginId.getId("com.intellij.spellchecker")) - - // Regex for printf-style specifiers: %[flags][width][.precision][length]type - private val spec = Regex("%(?:[-+ #0]*(?:\\d+)?(?:\\.\\d+)?[a-zA-Z%])") - - override fun isMyContextRoot(element: PsiElement): Boolean { - val type = element.node?.elementType - val settings = LyngFormatterSettings.getInstance(element.project) - val legacyPresent = legacySpellcheckerInstalled() - if (type != null && seenTypes.size < 10) { - val name = type.toString() - if (seenTypes.add(name)) { - log.info("LyngGrazieStrategy: saw PSI type=$name") - } - } - if (!loggedOnce) { - loggedOnce = true - log.info("LyngGrazieStrategy activated: legacyPresent=$legacyPresent, preferGrazieForCommentsAndLiterals=${settings.preferGrazieForCommentsAndLiterals}, spellCheckStringLiterals=${settings.spellCheckStringLiterals}, grazieChecksIdentifiers=${settings.grazieChecksIdentifiers}") - } - - val file = element.containingFile ?: return false - val index = LyngSpellIndex.getUpToDate(file) ?: return false // Suspend until ready - // To ensure Grazie asks TextExtractor for all leafs, accept any Lyng element once index is ready. - // The extractor will decide per-range/domain what to actually provide. - if (!loggedFirstMatch) { - loggedFirstMatch = true - log.info("LyngGrazieStrategy: enabling Grazie on all Lyng elements (index ready)") - } - return true - } - - override fun getContextRootTextDomain(root: PsiElement): TextDomain { - val type = root.node?.elementType - val settings = LyngFormatterSettings.getInstance(root.project) - val file = root.containingFile - val index = if (file != null) LyngSpellIndex.getUpToDate(file) else null - val r = root.textRange - - return when (type) { - LyngTokenTypes.LINE_COMMENT, LyngTokenTypes.BLOCK_COMMENT -> TextDomain.COMMENTS - LyngTokenTypes.STRING -> if (settings.grazieTreatLiteralsAsComments) TextDomain.COMMENTS else TextDomain.LITERALS - LyngTokenTypes.IDENTIFIER -> { - // For Grazie-only reliability in 243+, route identifiers via COMMENTS when configured - if (settings.grazieTreatIdentifiersAsComments && index != null && r != null && index.identifiers.any { it.contains(r) }) - TextDomain.COMMENTS - else TextDomain.PLAIN_TEXT - } - - else -> TextDomain.PLAIN_TEXT - } - } - - // Note: do not override getLanguageSupport to keep compatibility with 243 API - - override fun getStealthyRanges(root: PsiElement, text: CharSequence): java.util.LinkedHashSet { - val result = LinkedHashSet() - val type = root.node?.elementType - if (type == LyngTokenTypes.STRING) { - if (!shouldCheckLiterals(root)) { - // Hide the entire string when literals checking is disabled by settings - result += (0 until text.length) - return result - } - // Hide printf-like specifiers in strings - val (start, end) = stripQuotesBounds(text) - if (end > start) { - val content = text.subSequence(start, end) - for (m in spec.findAll(content)) { - val ms = start + m.range.first - val me = start + m.range.last - result += (ms..me) - } - if (result.isNotEmpty()) { - log.debug("LyngGrazieStrategy: hidden ${result.size} printf specifier ranges in string literal") - } - } - } - return result - } - - override fun isEnabledByDefault(): Boolean = true - - private fun shouldCheckLiterals(root: PsiElement): Boolean = - LyngFormatterSettings.getInstance(root.project).spellCheckStringLiterals - - private fun stripQuotesBounds(text: CharSequence): Pair { - if (text.length < 2) return 0 to text.length - val first = text.first() - val last = text.last() - return if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) - 1 to (text.length - 1) else (0 to text.length) - } -} diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngTextExtractor.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngTextExtractor.kt index ef08fdf..6346d1a 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngTextExtractor.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngTextExtractor.kt @@ -19,84 +19,29 @@ package net.sergeych.lyng.idea.grazie import com.intellij.grazie.text.TextContent import com.intellij.grazie.text.TextContent.TextDomain import com.intellij.grazie.text.TextExtractor -import com.intellij.openapi.diagnostic.Logger import com.intellij.psi.PsiElement import net.sergeych.lyng.idea.highlight.LyngTokenTypes -import net.sergeych.lyng.idea.settings.LyngFormatterSettings -import net.sergeych.lyng.idea.spell.LyngSpellIndex +import net.sergeych.lyng.idea.psi.LyngElementTypes /** - * Provides Grazie with extractable text for Lyng PSI elements. - * We return text for identifiers, comments, and (optionally) string literals. - * printf-like specifiers are filtered by the Grammar strategy via stealth ranges. + * Simplified TextExtractor for Lyng. + * Designates areas for Natural Languages (Grazie) to check. */ class LyngTextExtractor : TextExtractor() { - private val log = Logger.getInstance(LyngTextExtractor::class.java) - @Volatile private var loggedOnce = false - private val seen: MutableSet = java.util.Collections.synchronizedSet(mutableSetOf()) - override fun buildTextContent(element: PsiElement, allowedDomains: Set): TextContent? { val type = element.node?.elementType ?: return null - if (!loggedOnce) { - loggedOnce = true - log.info("LyngTextExtractor active; allowedDomains=${allowedDomains.joinToString()}") - } - val settings = LyngFormatterSettings.getInstance(element.project) - val file = element.containingFile - val index = if (file != null) LyngSpellIndex.getUpToDate(file) else null - val r = element.textRange - - // Decide target domain by intersection with our MiniAst-driven index; prefer comments > strings > identifiers - var domain: TextDomain? = null - if (index != null && r != null) { - if (index.comments.any { it.intersects(r) }) domain = TextDomain.COMMENTS - else if (index.strings.any { it.intersects(r) } && settings.spellCheckStringLiterals) domain = TextDomain.LITERALS - else if (index.identifiers.any { it.contains(r) }) domain = if (settings.grazieTreatIdentifiersAsComments) TextDomain.COMMENTS else TextDomain.DOCUMENTATION - } else { - // Fallback to token type if index is not ready (rare timing), mostly for comments - domain = when (type) { - LyngTokenTypes.LINE_COMMENT, LyngTokenTypes.BLOCK_COMMENT -> TextDomain.COMMENTS - else -> null - } - } - if (domain == null) return null - - // If literals aren't requested but fallback is enabled, route strings as COMMENTS - if (domain == TextDomain.LITERALS && !allowedDomains.contains(TextDomain.LITERALS) && settings.grazieTreatLiteralsAsComments) { - domain = TextDomain.COMMENTS - } - if (!allowedDomains.contains(domain)) { - if (seen.add("deny-${domain.name}")) { - log.info("LyngTextExtractor: domain ${domain.name} not in allowedDomains; skipping") - } - return null - } - return try { - // Try common factory names across versions - val methods = TextContent::class.java.methods.filter { it.name == "psiFragment" } - val built: TextContent? = when { - // Try psiFragment(PsiElement, TextDomain) - methods.any { it.parameterCount == 2 && it.parameterTypes[0].name.contains("PsiElement") } -> { - val m = methods.first { it.parameterCount == 2 && it.parameterTypes[0].name.contains("PsiElement") } - @Suppress("UNCHECKED_CAST") - (m.invoke(null, element, domain) as? TextContent)?.also { - if (seen.add("ok-${domain.name}")) log.info("LyngTextExtractor: provided ${domain.name} for ${type} via psiFragment(element, domain)") - } - } - // Try psiFragment(TextDomain, PsiElement) - methods.any { it.parameterCount == 2 && it.parameterTypes[0].name.endsWith("TextDomain") } -> { - val m = methods.first { it.parameterCount == 2 && it.parameterTypes[0].name.endsWith("TextDomain") } - @Suppress("UNCHECKED_CAST") - (m.invoke(null, domain, element) as? TextContent)?.also { - if (seen.add("ok-${domain.name}")) log.info("LyngTextExtractor: provided ${domain.name} for ${type} via psiFragment(domain, element)") - } - } - else -> null - } - built - } catch (e: Throwable) { - log.info("LyngTextExtractor: failed to build TextContent: ${e.javaClass.simpleName}: ${e.message}") - null + + val domain = when (type) { + LyngTokenTypes.LINE_COMMENT, LyngTokenTypes.BLOCK_COMMENT -> TextDomain.COMMENTS + LyngTokenTypes.STRING -> TextDomain.LITERALS + LyngElementTypes.NAME_IDENTIFIER, + LyngElementTypes.PARAMETER_NAME, + LyngElementTypes.ENUM_CONSTANT_NAME -> TextDomain.COMMENTS + else -> return null } + + if (!allowedDomains.contains(domain)) return null + + return TextContent.psiFragment(domain, element) } } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/ReplaceWordFix.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/ReplaceWordFix.kt deleted file mode 100644 index d9382a5..0000000 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/ReplaceWordFix.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package net.sergeych.lyng.idea.grazie - -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer -import com.intellij.codeInsight.intention.IntentionAction -import com.intellij.openapi.command.WriteCommandAction -import com.intellij.openapi.editor.CaretModel -import com.intellij.openapi.editor.Document -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiFile - -/** - * Lightweight quick-fix to replace a misspelled word (subrange) with a suggested alternative. - * Works without the legacy Spell Checker. The replacement is applied directly to the file text. - */ -class ReplaceWordFix( - private val range: TextRange, - private val original: String, - private val replacementRaw: String -) : IntentionAction { - - override fun getText(): String = "Replace '$original' with '$replacementRaw'" - override fun getFamilyName(): String = "Lyng Spelling" - override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = - editor != null && file != null && range.startOffset in 0..range.endOffset - - override fun startInWriteAction(): Boolean = true - - override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { - if (editor == null) return - val doc: Document = editor.document - val safeRange = range.constrainTo(doc) - val current = doc.getText(safeRange) - // Preserve basic case style based on the original token - val replacement = adaptCaseStyle(current, replacementRaw) - WriteCommandAction.runWriteCommandAction(project, "Replace word", null, Runnable { - doc.replaceString(safeRange.startOffset, safeRange.endOffset, replacement) - }, file) - // Move caret to end of replacement for convenience - try { - val caret: CaretModel = editor.caretModel - caret.moveToOffset(safeRange.startOffset + replacement.length) - } catch (_: Throwable) {} - // Restart daemon to refresh highlights - if (file != null) DaemonCodeAnalyzer.getInstance(project).restart(file) - } - - private fun TextRange.constrainTo(doc: Document): TextRange { - val start = startOffset.coerceIn(0, doc.textLength) - val end = endOffset.coerceIn(start, doc.textLength) - return TextRange(start, end) - } - - private fun adaptCaseStyle(sample: String, suggestion: String): String { - if (suggestion.isEmpty()) return suggestion - return when { - sample.all { it.isUpperCase() } -> suggestion.uppercase() - // PascalCase / Capitalized single word - sample.firstOrNull()?.isUpperCase() == true && sample.drop(1).any { it.isLowerCase() } -> - suggestion.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } - // snake_case -> lower - sample.contains('_') -> suggestion.lowercase() - // camelCase -> lower first - sample.firstOrNull()?.isLowerCase() == true && sample.any { it.isUpperCase() } -> - suggestion.replaceFirstChar { it.lowercase() } - else -> suggestion - } - } -} diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/psi/LyngElementTypes.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/psi/LyngElementTypes.kt new file mode 100644 index 0000000..caa2584 --- /dev/null +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/psi/LyngElementTypes.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package net.sergeych.lyng.idea.psi + +import com.intellij.psi.tree.IElementType +import net.sergeych.lyng.idea.LyngLanguage + +object LyngElementTypes { + val NAME_IDENTIFIER = IElementType("NAME_IDENTIFIER", LyngLanguage) + val PARAMETER_NAME = IElementType("PARAMETER_NAME", LyngLanguage) + val ENUM_CONSTANT_NAME = IElementType("ENUM_CONSTANT_NAME", LyngLanguage) +} diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/psi/LyngParserDefinition.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/psi/LyngParserDefinition.kt index 3787f25..0e67e75 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/psi/LyngParserDefinition.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/psi/LyngParserDefinition.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,7 +45,68 @@ class LyngParserDefinition : ParserDefinition { override fun createParser(project: Project?): PsiParser = PsiParser { root, builder -> val mark: PsiBuilder.Marker = builder.mark() - while (!builder.eof()) builder.advanceLexer() + var lastKeyword: String? = null + var inEnum = false + var inParams = false + var parenDepth = 0 + var braceDepth = 0 + + while (!builder.eof()) { + val type = builder.tokenType + val text = builder.tokenText + + when (type) { + LyngTokenTypes.KEYWORD -> { + lastKeyword = text + if (text == "enum") inEnum = true + } + LyngTokenTypes.PUNCT -> { + if (text == "(") { + parenDepth++ + if (lastKeyword == "fun" || lastKeyword == "constructor" || lastKeyword == "init") inParams = true + } else if (text == ")") { + parenDepth-- + if (parenDepth == 0) inParams = false + } else if (text == "{") { + braceDepth++ + } else if (text == "}") { + braceDepth-- + if (braceDepth == 0) inEnum = false + } + if (text != ".") lastKeyword = null + } + LyngTokenTypes.IDENTIFIER -> { + val m = builder.mark() + builder.advanceLexer() + val nextType = builder.tokenType + val isQualified = nextType == LyngTokenTypes.PUNCT && builder.tokenText == "." + + if (!isQualified) { + when { + lastKeyword in setOf("fun", "val", "var", "class", "enum", "object", "interface", "type", "property") -> { + m.done(LyngElementTypes.NAME_IDENTIFIER) + } + inParams && parenDepth > 0 -> { + m.done(LyngElementTypes.PARAMETER_NAME) + } + inEnum && braceDepth > 0 && parenDepth == 0 -> { + m.done(LyngElementTypes.ENUM_CONSTANT_NAME) + } + else -> m.drop() + } + } else { + m.drop() + } + lastKeyword = null + continue + } + LyngTokenTypes.WHITESPACE, LyngTokenTypes.LINE_COMMENT, LyngTokenTypes.BLOCK_COMMENT -> { + // keep lastKeyword + } + else -> lastKeyword = null + } + builder.advanceLexer() + } mark.done(root) builder.treeBuilt } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/settings/LyngFormatterSettings.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/settings/LyngFormatterSettings.kt index 48416b1..b823735 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/settings/LyngFormatterSettings.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/settings/LyngFormatterSettings.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,24 +32,6 @@ class LyngFormatterSettings(private val project: Project) : PersistentStateCompo var reindentClosedBlockOnEnter: Boolean = true, var reindentPastedBlocks: Boolean = true, var normalizeBlockCommentIndent: Boolean = false, - var spellCheckStringLiterals: Boolean = true, - // When Grazie/Natural Languages is present, prefer it for comments and literals (avoid legacy duplicates) - var preferGrazieForCommentsAndLiterals: Boolean = true, - // When Grazie is available, also check identifiers via Grazie. - // Default OFF because Grazie typically doesn't flag code identifiers; legacy Spellchecker is better for code. - var grazieChecksIdentifiers: Boolean = false, - // Grazie-only fallback: treat identifiers as comments domain so Grazie applies spelling rules - var grazieTreatIdentifiersAsComments: Boolean = true, - // Grazie-only fallback: treat string literals as comments domain when LITERALS domain is not requested - var grazieTreatLiteralsAsComments: Boolean = true, - // Debug helper: show the exact ranges we feed to Grazie/legacy as weak warnings - var debugShowSpellFeed: Boolean = false, - // Visuals: render Lyng typos using the standard Typo green underline styling - var showTyposWithGreenUnderline: Boolean = true, - // Enable lightweight quick-fixes (Replace..., Add to dictionary) without legacy Spell Checker - var offerLyngTypoQuickFixes: Boolean = true, - // Per-project learned words (do not flag again) - var learnedWords: MutableSet = mutableSetOf(), // Experimental: enable Lyng autocompletion (can be disabled if needed) var enableLyngCompletionExperimental: Boolean = true, ) @@ -82,42 +64,6 @@ class LyngFormatterSettings(private val project: Project) : PersistentStateCompo get() = myState.normalizeBlockCommentIndent set(value) { myState.normalizeBlockCommentIndent = value } - var spellCheckStringLiterals: Boolean - get() = myState.spellCheckStringLiterals - set(value) { myState.spellCheckStringLiterals = value } - - var preferGrazieForCommentsAndLiterals: Boolean - get() = myState.preferGrazieForCommentsAndLiterals - set(value) { myState.preferGrazieForCommentsAndLiterals = value } - - var grazieChecksIdentifiers: Boolean - get() = myState.grazieChecksIdentifiers - set(value) { myState.grazieChecksIdentifiers = value } - - var grazieTreatIdentifiersAsComments: Boolean - get() = myState.grazieTreatIdentifiersAsComments - set(value) { myState.grazieTreatIdentifiersAsComments = value } - - var grazieTreatLiteralsAsComments: Boolean - get() = myState.grazieTreatLiteralsAsComments - set(value) { myState.grazieTreatLiteralsAsComments = value } - - var debugShowSpellFeed: Boolean - get() = myState.debugShowSpellFeed - set(value) { myState.debugShowSpellFeed = value } - - var showTyposWithGreenUnderline: Boolean - get() = myState.showTyposWithGreenUnderline - set(value) { myState.showTyposWithGreenUnderline = value } - - var offerLyngTypoQuickFixes: Boolean - get() = myState.offerLyngTypoQuickFixes - set(value) { myState.offerLyngTypoQuickFixes = value } - - var learnedWords: MutableSet - get() = myState.learnedWords - set(value) { myState.learnedWords = value } - var enableLyngCompletionExperimental: Boolean get() = myState.enableLyngCompletionExperimental set(value) { myState.enableLyngCompletionExperimental = value } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/settings/LyngFormatterSettingsConfigurable.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/settings/LyngFormatterSettingsConfigurable.kt index da46b79..e4ad197 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/settings/LyngFormatterSettingsConfigurable.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/settings/LyngFormatterSettingsConfigurable.kt @@ -1,5 +1,5 @@ /* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com + * Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,14 +30,6 @@ class LyngFormatterSettingsConfigurable(private val project: Project) : Configur private var reindentClosedBlockCb: JCheckBox? = null private var reindentPasteCb: JCheckBox? = null private var normalizeBlockCommentIndentCb: JCheckBox? = null - private var spellCheckLiteralsCb: JCheckBox? = null - private var preferGrazieCommentsLiteralsCb: JCheckBox? = null - private var grazieChecksIdentifiersCb: JCheckBox? = null - private var grazieIdsAsCommentsCb: JCheckBox? = null - private var grazieLiteralsAsCommentsCb: JCheckBox? = null - private var debugShowSpellFeedCb: JCheckBox? = null - private var showTyposGreenCb: JCheckBox? = null - private var offerQuickFixesCb: JCheckBox? = null private var enableCompletionCb: JCheckBox? = null override fun getDisplayName(): String = "Lyng Formatter" @@ -50,14 +42,6 @@ class LyngFormatterSettingsConfigurable(private val project: Project) : Configur reindentClosedBlockCb = JCheckBox("Reindent enclosed block on Enter after '}'") reindentPasteCb = JCheckBox("Reindent pasted blocks (align pasted code to current indent)") normalizeBlockCommentIndentCb = JCheckBox("Normalize block comment indentation [experimental]") - spellCheckLiteralsCb = JCheckBox("Spell check string literals (skip % specifiers like %s, %d, %-12s)") - preferGrazieCommentsLiteralsCb = JCheckBox("Prefer Natural Languages/Grazie for comments and string literals (avoid duplicates)") - grazieChecksIdentifiersCb = JCheckBox("Check identifiers via Natural Languages/Grazie when available") - grazieIdsAsCommentsCb = JCheckBox("Natural Languages/Grazie: treat identifiers as comments (forces spelling checks in 2024.3)") - grazieLiteralsAsCommentsCb = JCheckBox("Natural Languages/Grazie: treat string literals as comments when literals are not processed") - debugShowSpellFeedCb = JCheckBox("Debug: show spell-feed ranges (weak warnings)") - showTyposGreenCb = JCheckBox("Show Lyng typos with green underline (TYPO styling)") - offerQuickFixesCb = JCheckBox("Offer Lyng typo quick fixes (Replace…, Add to dictionary) without Spell Checker") enableCompletionCb = JCheckBox("Enable Lyng autocompletion (experimental)") // Tooltips / short help @@ -66,27 +50,12 @@ class LyngFormatterSettingsConfigurable(private val project: Project) : Configur reindentClosedBlockCb?.toolTipText = "On Enter after a closing '}', reindent the just-closed {…} block using formatter rules." reindentPasteCb?.toolTipText = "When caret is in leading whitespace, reindent the pasted text and align it to the caret's indent." normalizeBlockCommentIndentCb?.toolTipText = "Experimental: normalize indentation inside /* … */ comments (code is not modified)." - preferGrazieCommentsLiteralsCb?.toolTipText = "When ON and Natural Languages/Grazie is installed, comments and string literals are checked by Grazie. Turn OFF to force legacy Spellchecker to check them." - grazieChecksIdentifiersCb?.toolTipText = "When ON and Natural Languages/Grazie is installed, identifiers (non-keywords) are checked by Grazie too." - grazieIdsAsCommentsCb?.toolTipText = "Grazie-only fallback: route identifiers as COMMENTS domain so Grazie applies spelling in 2024.3." - grazieLiteralsAsCommentsCb?.toolTipText = "Grazie-only fallback: when Grammar doesn't process literals, route strings as COMMENTS so they are checked." - debugShowSpellFeedCb?.toolTipText = "Show the exact ranges we feed to spellcheckers (ids/comments/strings) as weak warnings." - showTyposGreenCb?.toolTipText = "Render Lyng typos using the platform's green TYPO underline instead of generic warnings." - offerQuickFixesCb?.toolTipText = "Provide lightweight Replace… and Add to dictionary quick-fixes without requiring the legacy Spell Checker." enableCompletionCb?.toolTipText = "Turn on/off the lightweight Lyng code completion (BASIC)." p.add(spacingCb) p.add(wrappingCb) p.add(reindentClosedBlockCb) p.add(reindentPasteCb) p.add(normalizeBlockCommentIndentCb) - p.add(spellCheckLiteralsCb) - p.add(preferGrazieCommentsLiteralsCb) - p.add(grazieChecksIdentifiersCb) - p.add(grazieIdsAsCommentsCb) - p.add(grazieLiteralsAsCommentsCb) - p.add(debugShowSpellFeedCb) - p.add(showTyposGreenCb) - p.add(offerQuickFixesCb) p.add(enableCompletionCb) panel = p reset() @@ -100,14 +69,6 @@ class LyngFormatterSettingsConfigurable(private val project: Project) : Configur reindentClosedBlockCb?.isSelected != s.reindentClosedBlockOnEnter || reindentPasteCb?.isSelected != s.reindentPastedBlocks || normalizeBlockCommentIndentCb?.isSelected != s.normalizeBlockCommentIndent || - spellCheckLiteralsCb?.isSelected != s.spellCheckStringLiterals || - preferGrazieCommentsLiteralsCb?.isSelected != s.preferGrazieForCommentsAndLiterals || - grazieChecksIdentifiersCb?.isSelected != s.grazieChecksIdentifiers || - grazieIdsAsCommentsCb?.isSelected != s.grazieTreatIdentifiersAsComments || - grazieLiteralsAsCommentsCb?.isSelected != s.grazieTreatLiteralsAsComments || - debugShowSpellFeedCb?.isSelected != s.debugShowSpellFeed || - showTyposGreenCb?.isSelected != s.showTyposWithGreenUnderline || - offerQuickFixesCb?.isSelected != s.offerLyngTypoQuickFixes || enableCompletionCb?.isSelected != s.enableLyngCompletionExperimental } @@ -118,14 +79,6 @@ class LyngFormatterSettingsConfigurable(private val project: Project) : Configur s.reindentClosedBlockOnEnter = reindentClosedBlockCb?.isSelected == true s.reindentPastedBlocks = reindentPasteCb?.isSelected == true s.normalizeBlockCommentIndent = normalizeBlockCommentIndentCb?.isSelected == true - s.spellCheckStringLiterals = spellCheckLiteralsCb?.isSelected == true - s.preferGrazieForCommentsAndLiterals = preferGrazieCommentsLiteralsCb?.isSelected == true - s.grazieChecksIdentifiers = grazieChecksIdentifiersCb?.isSelected == true - s.grazieTreatIdentifiersAsComments = grazieIdsAsCommentsCb?.isSelected == true - s.grazieTreatLiteralsAsComments = grazieLiteralsAsCommentsCb?.isSelected == true - s.debugShowSpellFeed = debugShowSpellFeedCb?.isSelected == true - s.showTyposWithGreenUnderline = showTyposGreenCb?.isSelected == true - s.offerLyngTypoQuickFixes = offerQuickFixesCb?.isSelected == true s.enableLyngCompletionExperimental = enableCompletionCb?.isSelected == true } @@ -136,14 +89,6 @@ class LyngFormatterSettingsConfigurable(private val project: Project) : Configur reindentClosedBlockCb?.isSelected = s.reindentClosedBlockOnEnter reindentPasteCb?.isSelected = s.reindentPastedBlocks normalizeBlockCommentIndentCb?.isSelected = s.normalizeBlockCommentIndent - spellCheckLiteralsCb?.isSelected = s.spellCheckStringLiterals - preferGrazieCommentsLiteralsCb?.isSelected = s.preferGrazieForCommentsAndLiterals - grazieChecksIdentifiersCb?.isSelected = s.grazieChecksIdentifiers - grazieIdsAsCommentsCb?.isSelected = s.grazieTreatIdentifiersAsComments - grazieLiteralsAsCommentsCb?.isSelected = s.grazieTreatLiteralsAsComments - debugShowSpellFeedCb?.isSelected = s.debugShowSpellFeed - showTyposGreenCb?.isSelected = s.showTyposWithGreenUnderline - offerQuickFixesCb?.isSelected = s.offerLyngTypoQuickFixes enableCompletionCb?.isSelected = s.enableLyngCompletionExperimental } } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellIndex.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellIndex.kt deleted file mode 100644 index d417dcf..0000000 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellIndex.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package net.sergeych.lyng.idea.spell - -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.util.Key -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiFile - -/** - * Per-file cached spellcheck index built from MiniAst-based highlighting and the lynglib highlighter. - * It exposes identifier, comment, and string literal ranges. Strategies should suspend until data is ready. - */ -object LyngSpellIndex { - private val LOG = Logger.getInstance(LyngSpellIndex::class.java) - - data class Data( - val modStamp: Long, - val identifiers: List, - val comments: List, - val strings: List, - ) - - private val KEY: Key = Key.create("LYNG_SPELL_INDEX") - - fun getUpToDate(file: PsiFile): Data? { - val doc = file.viewProvider.document ?: return null - val d = file.getUserData(KEY) ?: return null - return if (d.modStamp == doc.modificationStamp) d else null - } - - fun store(file: PsiFile, data: Data) { - file.putUserData(KEY, data) - LOG.info("LyngSpellIndex built: ids=${data.identifiers.size}, comments=${data.comments.size}, strings=${data.strings.size}") - } -} diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellcheckingStrategy.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellcheckingStrategy.kt index 86084e4..304e91b 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellcheckingStrategy.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/spell/LyngSpellcheckingStrategy.kt @@ -20,10 +20,11 @@ import com.intellij.psi.PsiElement import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy import com.intellij.spellchecker.tokenizer.Tokenizer import net.sergeych.lyng.idea.highlight.LyngTokenTypes +import net.sergeych.lyng.idea.psi.LyngElementTypes /** * Standard IntelliJ spellchecking strategy for Lyng. - * It uses the MiniAst-driven [LyngSpellIndex] to limit identifier checks to declarations only. + * Uses the simplified PSI structure to identify declarations. */ class LyngSpellcheckingStrategy : SpellcheckingStrategy() { override fun getTokenizer(element: PsiElement?): Tokenizer<*> { @@ -31,21 +32,9 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() { return when (type) { LyngTokenTypes.LINE_COMMENT, LyngTokenTypes.BLOCK_COMMENT -> TEXT_TOKENIZER LyngTokenTypes.STRING -> TEXT_TOKENIZER - LyngTokenTypes.IDENTIFIER -> { - // We use standard NameIdentifierOwner/PsiNamedElement-based logic - // if it's a declaration. Argument names, class names, etc. are PSI-based. - // However, our PSI is currently very minimal (ASTWrapperPsiElement). - // So we stick to the index but ensure it is robustly filled. - val file = element.containingFile - val index = LyngSpellIndex.getUpToDate(file) - if (index != null) { - val range = element.textRange - if (index.identifiers.any { it.contains(range) }) { - return TEXT_TOKENIZER - } - } - EMPTY_TOKENIZER - } + LyngElementTypes.NAME_IDENTIFIER, + LyngElementTypes.PARAMETER_NAME, + LyngElementTypes.ENUM_CONSTANT_NAME -> TEXT_TOKENIZER else -> super.getTokenizer(element) } } diff --git a/lyng-idea/src/main/resources/META-INF/grazie-bundled.xml b/lyng-idea/src/main/resources/META-INF/grazie-bundled.xml index 774b605..dcb198e 100644 --- a/lyng-idea/src/main/resources/META-INF/grazie-bundled.xml +++ b/lyng-idea/src/main/resources/META-INF/grazie-bundled.xml @@ -1,5 +1,5 @@ - diff --git a/lyng-idea/src/main/resources/META-INF/grazie-lite.xml b/lyng-idea/src/main/resources/META-INF/grazie-lite.xml index f38993f..3289b96 100644 --- a/lyng-idea/src/main/resources/META-INF/grazie-lite.xml +++ b/lyng-idea/src/main/resources/META-INF/grazie-lite.xml @@ -1,5 +1,5 @@ -