another attempt to fix plugin spellchecker
This commit is contained in:
parent
d2632cb99e
commit
017111827d
@ -1,9 +1,17 @@
|
||||
# Lyng Language AI Specification (V1.2)
|
||||
# Lyng Language AI Specification (V1.3)
|
||||
|
||||
High-density specification for LLMs. Reference this for all Lyng code generation.
|
||||
|
||||
## 1. Core Philosophy & Syntax
|
||||
- **Everything is an Expression**: Blocks, `if`, `when`, `for`, `while` return their last expression (or `void`).
|
||||
- **Everything is an Expression**: Blocks, `if`, `when`, `for`, `while`, `do-while` return their last expression (or `void`).
|
||||
- **Loops with `else`**: `for`, `while`, and `do-while` support an optional `else` block.
|
||||
- `else` executes **only if** the loop finishes normally (without a `break`).
|
||||
- `break <value>` exits the loop and sets its return value.
|
||||
- Loop Return Value:
|
||||
1. Value from `break <value>`.
|
||||
2. Result of `else` block (if loop finished normally and `else` exists).
|
||||
3. Result of the last iteration (if loop finished normally and no `else`).
|
||||
4. `void` (if loop body never executed and no `else`).
|
||||
- **Implicit Coroutines**: All functions are coroutines. No `async/await`. Use `launch { ... }` (returns `Deferred`) or `flow { ... }`.
|
||||
- **Variables**: `val` (read-only), `var` (mutable). Supports late-init `val` in classes (must be assigned in `init` or body).
|
||||
- **Null Safety**: `?` (nullable type), `?.` (safe access), `?( )` (safe invoke), `?{ }` (safe block invoke), `?[ ]` (safe index), `?:` or `??` (elvis), `?=` (assign-if-null).
|
||||
@ -87,3 +95,11 @@ val [first, middle..., last] = [1, 2, 3, 4, 5]
|
||||
// Safe Navigation and Elvis
|
||||
val companyName = person?.job?.company?.name ?? "Freelancer"
|
||||
```
|
||||
|
||||
## 8. Standard Library Discovery
|
||||
To collect data on the standard library and available APIs, AI should inspect:
|
||||
- **Global Symbols**: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/Script.kt` (root functions like `println`, `sqrt`, `assert`).
|
||||
- **Core Type Members**: `lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/*.kt` (e.g., `ObjList.kt`, `ObjString.kt`, `ObjMap.kt`) for methods on built-in types.
|
||||
- **Lyng-side Extensions**: `lynglib/stdlib/lyng/root.lyng` for high-level functional APIs (e.g., `map`, `filter`, `any`, `lazy`).
|
||||
- **I/O & Processes**: `lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/` for `fs` and `process` modules.
|
||||
- **Documentation**: `docs/*.md` (e.g., `tutorial.md`, `lyngio.md`) for high-level usage and module overviews.
|
||||
|
||||
@ -62,6 +62,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
if (collectedInfo == null) return null
|
||||
ProgressManager.checkCanceled()
|
||||
val text = collectedInfo.text
|
||||
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
||||
|
||||
// Use LyngAstManager to get the (potentially merged) Mini-AST
|
||||
val mini = LyngAstManager.getMiniAst(collectedInfo.file)
|
||||
@ -224,8 +225,6 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
// Heuristics on top of binder: function call-sites and simple name-based roles
|
||||
ProgressManager.checkCanceled()
|
||||
|
||||
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
||||
|
||||
// Build simple name -> role map for top-level vals/vars and parameters
|
||||
val nameRole = HashMap<String, com.intellij.openapi.editor.colors.TextAttributesKey>(8)
|
||||
mini.declarations.forEach { d ->
|
||||
@ -280,7 +279,6 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
|
||||
// Add annotation/label coloring using token highlighter
|
||||
run {
|
||||
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
||||
tokens.forEach { s ->
|
||||
if (s.kind == HighlightKind.Label) {
|
||||
val start = s.range.start
|
||||
@ -322,7 +320,6 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
|
||||
// Map Enum constants from token highlighter to IDEA enum constant color
|
||||
run {
|
||||
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
||||
tokens.forEach { s ->
|
||||
if (s.kind == HighlightKind.EnumConstant) {
|
||||
val start = s.range.start
|
||||
@ -334,24 +331,10 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
||||
}
|
||||
}
|
||||
|
||||
// Build spell index payload: identifiers from symbols + references; comments/strings from simple highlighter
|
||||
val idRanges = mutableSetOf<IntRange>()
|
||||
try {
|
||||
val binding = Binder.bind(text, mini)
|
||||
binding.symbols.forEach { sym ->
|
||||
val s = sym.declStart
|
||||
val e = sym.declEnd
|
||||
if (s in 0..e && e <= text.length && s < e) idRanges += (s until e)
|
||||
}
|
||||
binding.references.forEach { ref ->
|
||||
val s = ref.start
|
||||
val e = ref.end
|
||||
if (s in 0..e && e <= text.length && s < e) idRanges += (s until e)
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
// Best-effort; no identifiers if binder fails
|
||||
}
|
||||
val tokens = try { SimpleLyngHighlighter().highlight(text) } catch (_: Throwable) { emptyList() }
|
||||
// Build spell index payload: identifiers + comments/strings from simple highlighter.
|
||||
// We use the highlighter as the source of truth for all "words" to check, including
|
||||
// identifiers that might not be bound by the Binder.
|
||||
val idRanges = tokens.filter { it.kind == HighlightKind.Identifier }.map { it.range.start until it.range.endExclusive }
|
||||
val commentRanges = tokens.filter { it.kind == HighlightKind.Comment }.map { it.range.start until it.range.endExclusive }
|
||||
val stringRanges = tokens.filter { it.kind == HighlightKind.String }.map { it.range.start until it.range.endExclusive }
|
||||
|
||||
|
||||
@ -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.
|
||||
@ -150,16 +150,18 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
|
||||
}
|
||||
log.info("LyngGrazieAnnotator.apply: used=${chosenEntry ?: "<none>"}, totalFindings=$totalReturned, painting=${findings.size}")
|
||||
|
||||
// IMPORTANT: Do NOT fallback to the tiny bundled vocabulary on modern IDEs.
|
||||
// If Grazie/Natural Languages processing returned nothing, we simply exit here
|
||||
// to avoid low‑quality results from the legacy dictionary.
|
||||
if (findings.isEmpty()) return
|
||||
|
||||
for (f in findings) {
|
||||
val ab = holder.newAnnotation(HighlightSeverity.INFORMATION, f.message).range(f.range)
|
||||
applyTypoStyleIfRequested(file, ab)
|
||||
ab.create()
|
||||
}
|
||||
|
||||
// SUPPLEMENT: Always run the fallback spellchecker to ensure spelling errors are not ignored.
|
||||
// It will avoid duplicating findings already reported by Grazie.
|
||||
val painted = fallbackWithLegacySpellcheckerIfAvailable(file, fragments, holder, findings)
|
||||
if (painted > 0) {
|
||||
log.info("LyngGrazieAnnotator.apply: supplemented with $painted typos from legacy engine")
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleOneShotRestart(file: PsiFile, modStamp: Long) {
|
||||
@ -381,7 +383,8 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
|
||||
private fun fallbackWithLegacySpellcheckerIfAvailable(
|
||||
file: PsiFile,
|
||||
fragments: List<Pair<TextContent, TextRange>>,
|
||||
holder: AnnotationHolder
|
||||
holder: AnnotationHolder,
|
||||
existingFindings: List<Finding>
|
||||
): Int {
|
||||
return try {
|
||||
val mgrCls = Class.forName("com.intellij.spellchecker.SpellCheckerManager")
|
||||
@ -389,12 +392,12 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
|
||||
val isCorrect = mgrCls.methods.firstOrNull { it.name == "isCorrect" && it.parameterCount == 1 && it.parameterTypes[0] == String::class.java }
|
||||
if (getInstance == null || isCorrect == null) {
|
||||
// No legacy spellchecker API available — fall back to naive painter
|
||||
return naiveFallbackPaint(file, fragments, holder)
|
||||
return naiveFallbackPaint(file, fragments, holder, existingFindings)
|
||||
}
|
||||
val mgr = getInstance.invoke(null, file.project)
|
||||
if (mgr == null) {
|
||||
// Legacy manager not present for this project — use naive fallback
|
||||
return naiveFallbackPaint(file, fragments, holder)
|
||||
return naiveFallbackPaint(file, fragments, holder, existingFindings)
|
||||
}
|
||||
var painted = 0
|
||||
val docText = file.viewProvider.document?.text ?: return 0
|
||||
@ -411,13 +414,18 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
|
||||
for (part in parts) {
|
||||
if (part.length <= 2) continue
|
||||
if (isAllowedWord(part)) continue
|
||||
|
||||
// Map part back to original token occurrence within this hostRange
|
||||
val localStart = m.range.first + token.indexOf(part)
|
||||
val localEnd = localStart + part.length
|
||||
val abs = TextRange(hostRange.startOffset + localStart, hostRange.startOffset + localEnd)
|
||||
|
||||
// Avoid duplicating findings from Grazie
|
||||
if (existingFindings.any { it.range.intersects(abs) }) continue
|
||||
|
||||
// Quick allowlist for very common words to reduce noise if dictionaries differ
|
||||
val ok = try { isCorrect.invoke(mgr, part) as? Boolean } catch (_: Throwable) { null }
|
||||
if (ok == false) {
|
||||
// Map part back to original token occurrence within this hostRange
|
||||
val localStart = m.range.first + token.indexOf(part)
|
||||
val localEnd = localStart + part.length
|
||||
val abs = TextRange(hostRange.startOffset + localStart, hostRange.startOffset + localEnd)
|
||||
paintTypoAnnotation(file, holder, abs, part)
|
||||
painted++
|
||||
flagged++
|
||||
@ -430,14 +438,15 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
|
||||
painted
|
||||
} catch (_: Throwable) {
|
||||
// If legacy manager is not available, fall back to a very naive heuristic (no external deps)
|
||||
return naiveFallbackPaint(file, fragments, holder)
|
||||
return naiveFallbackPaint(file, fragments, holder, existingFindings)
|
||||
}
|
||||
}
|
||||
|
||||
private fun naiveFallbackPaint(
|
||||
file: PsiFile,
|
||||
fragments: List<Pair<TextContent, TextRange>>,
|
||||
holder: AnnotationHolder
|
||||
holder: AnnotationHolder,
|
||||
existingFindings: List<Finding>
|
||||
): Int {
|
||||
var painted = 0
|
||||
val docText = file.viewProvider.document?.text
|
||||
@ -461,6 +470,14 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
|
||||
seen++
|
||||
val lower = part.lowercase()
|
||||
if (lower.length <= 2 || isAllowedWord(part)) continue
|
||||
|
||||
val localStart = m.range.first + token.indexOf(part)
|
||||
val localEnd = localStart + part.length
|
||||
val abs = TextRange(hostRange.startOffset + localStart, hostRange.startOffset + localEnd)
|
||||
|
||||
// Avoid duplicating findings from Grazie
|
||||
if (existingFindings.any { it.range.intersects(abs) }) continue
|
||||
|
||||
// Heuristic: no vowels OR 3 repeated chars OR ends with unlikely double consonants
|
||||
val noVowel = lower.none { it in "aeiouy" }
|
||||
val triple = Regex("(.)\\1\\1").containsMatchIn(lower)
|
||||
@ -480,9 +497,6 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
|
||||
}
|
||||
}
|
||||
if (looksWrong) {
|
||||
val localStart = m.range.first + token.indexOf(part)
|
||||
val localEnd = localStart + part.length
|
||||
val abs = TextRange(hostRange.startOffset + localStart, hostRange.startOffset + localEnd)
|
||||
paintTypoAnnotation(file, holder, abs, part)
|
||||
painted++
|
||||
flagged++
|
||||
|
||||
@ -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.
|
||||
@ -101,8 +101,9 @@ class LyngLexer : LexerBase() {
|
||||
return
|
||||
}
|
||||
|
||||
// String "..." with simple escape handling
|
||||
if (ch == '"') {
|
||||
// String "..." or '...' with simple escape handling
|
||||
if (ch == '"' || ch == '\'') {
|
||||
val quote = ch
|
||||
i++
|
||||
while (i < endOffset) {
|
||||
val c = buffer[i]
|
||||
@ -110,7 +111,7 @@ class LyngLexer : LexerBase() {
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (c == '"') { i++; break }
|
||||
if (c == quote) { i++; break }
|
||||
i++
|
||||
}
|
||||
myTokenEnd = i
|
||||
|
||||
@ -17,9 +17,6 @@
|
||||
package net.sergeych.lyng.idea.spell
|
||||
|
||||
// Avoid Tokenizers helper to keep compatibility; implement our own tokenizers
|
||||
import com.intellij.ide.plugins.PluginManagerCore
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.extensions.PluginId
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.spellchecker.inspections.PlainTextSplitter
|
||||
@ -38,67 +35,21 @@ import net.sergeych.lyng.idea.settings.LyngFormatterSettings
|
||||
*/
|
||||
class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
|
||||
|
||||
private val log = Logger.getInstance(LyngSpellcheckingStrategy::class.java)
|
||||
@Volatile private var loggedOnce = false
|
||||
|
||||
private fun grazieInstalled(): Boolean {
|
||||
// Support both historical and bundled IDs
|
||||
return PluginManagerCore.isPluginInstalled(PluginId.getId("com.intellij.grazie")) ||
|
||||
PluginManagerCore.isPluginInstalled(PluginId.getId("tanvd.grazi"))
|
||||
}
|
||||
|
||||
private fun grazieApiAvailable(): Boolean = try {
|
||||
// If this class is absent (as in IC-243), third-party plugins can't run Grazie programmatically
|
||||
Class.forName("com.intellij.grazie.grammar.GrammarChecker")
|
||||
true
|
||||
} catch (_: Throwable) { false }
|
||||
|
||||
override fun getTokenizer(element: PsiElement): Tokenizer<*> {
|
||||
if (element is com.intellij.psi.PsiFile) return EMPTY_TOKENIZER
|
||||
|
||||
val hasGrazie = grazieInstalled()
|
||||
val hasGrazieApi = grazieApiAvailable()
|
||||
val settings = LyngFormatterSettings.getInstance(element.project)
|
||||
if (!loggedOnce) {
|
||||
loggedOnce = true
|
||||
log.info("LyngSpellcheckingStrategy activated: hasGrazie=$hasGrazie, grazieApi=$hasGrazieApi, preferGrazieForCommentsAndLiterals=${settings.preferGrazieForCommentsAndLiterals}, spellCheckStringLiterals=${settings.spellCheckStringLiterals}, grazieChecksIdentifiers=${settings.grazieChecksIdentifiers}")
|
||||
}
|
||||
|
||||
val file = element.containingFile ?: return EMPTY_TOKENIZER
|
||||
val et = element.node?.elementType
|
||||
val index = LyngSpellIndex.getUpToDate(file)
|
||||
|
||||
// Decide responsibility per settings
|
||||
// If Grazie is present but its public API is not available (IC-243), do NOT delegate to it.
|
||||
val preferGrazie = hasGrazie && hasGrazieApi && settings.preferGrazieForCommentsAndLiterals
|
||||
val grazieIds = hasGrazie && hasGrazieApi && settings.grazieChecksIdentifiers
|
||||
|
||||
if (index == null) {
|
||||
// Index not ready: fall back to Lexer-based token types.
|
||||
// Identifiers are safe because LyngLexer separates keywords from identifiers.
|
||||
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.IDENTIFIER) {
|
||||
return if (grazieIds) EMPTY_TOKENIZER else IDENTIFIER_TOKENIZER
|
||||
}
|
||||
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.LINE_COMMENT || et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.BLOCK_COMMENT) {
|
||||
return if (preferGrazie) EMPTY_TOKENIZER else COMMENT_TEXT_TOKENIZER
|
||||
}
|
||||
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.STRING && settings.spellCheckStringLiterals) {
|
||||
return if (preferGrazie) EMPTY_TOKENIZER else STRING_WITH_PRINTF_EXCLUDES
|
||||
}
|
||||
return EMPTY_TOKENIZER
|
||||
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.IDENTIFIER || et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.LABEL) {
|
||||
return IDENTIFIER_TOKENIZER
|
||||
}
|
||||
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.LINE_COMMENT || et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.BLOCK_COMMENT) {
|
||||
return COMMENT_TEXT_TOKENIZER
|
||||
}
|
||||
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.STRING && settings.spellCheckStringLiterals) {
|
||||
return STRING_WITH_PRINTF_EXCLUDES
|
||||
}
|
||||
|
||||
val elRange = element.textRange ?: return EMPTY_TOKENIZER
|
||||
fun overlaps(list: List<TextRange>) = list.any { it.intersects(elRange) }
|
||||
|
||||
// Identifiers: only if range is within identifiers index and not delegated to Grazie
|
||||
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.IDENTIFIER && overlaps(index.identifiers) && !grazieIds) return IDENTIFIER_TOKENIZER
|
||||
|
||||
// Comments: only if not delegated to Grazie and overlapping indexed comments
|
||||
if ((et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.LINE_COMMENT || et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.BLOCK_COMMENT) && overlaps(index.comments) && !preferGrazie) return COMMENT_TEXT_TOKENIZER
|
||||
|
||||
// Strings: only if not delegated to Grazie, literals checking enabled, and overlapping indexed strings
|
||||
if (et == net.sergeych.lyng.idea.highlight.LyngTokenTypes.STRING && settings.spellCheckStringLiterals && overlaps(index.strings) && !preferGrazie) return STRING_WITH_PRINTF_EXCLUDES
|
||||
|
||||
return EMPTY_TOKENIZER
|
||||
}
|
||||
|
||||
@ -4796,5 +4796,20 @@ class ScriptTest {
|
||||
assertEquals("r:r", T().l("r"))
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTypedArgsWithInitializers() = runTest {
|
||||
eval("""
|
||||
fun f(a: String = "foo") = a + "!"
|
||||
fun g(a: String? = null) = a ?: "!!"
|
||||
assertEquals(f(), "foo!")
|
||||
assertEquals(f(), "!!")
|
||||
assertEquals(f("bar"), "bar!")
|
||||
class T(b: Int=42,c: String?=null)
|
||||
assertEquals(42, T().b)
|
||||
assertEquals(null, T().c)
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user