plugin: another attempt to fix spell checker
This commit is contained in:
parent
6fa57c8197
commit
ec28b219f3
@ -44,10 +44,7 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
|
|
||||||
data class Span(val start: Int, val end: Int, val key: com.intellij.openapi.editor.colors.TextAttributesKey)
|
data class Span(val start: Int, val end: Int, val key: com.intellij.openapi.editor.colors.TextAttributesKey)
|
||||||
data class Error(val start: Int, val end: Int, val message: String)
|
data class Error(val start: Int, val end: Int, val message: String)
|
||||||
data class Result(val modStamp: Long, val spans: List<Span>, val error: Error? = null,
|
data class Result(val modStamp: Long, val spans: List<Span>, val error: Error? = null)
|
||||||
val spellIdentifiers: List<IntRange> = emptyList(),
|
|
||||||
val spellComments: List<IntRange> = emptyList(),
|
|
||||||
val spellStrings: List<IntRange> = emptyList())
|
|
||||||
|
|
||||||
override fun collectInformation(file: PsiFile): Input? {
|
override fun collectInformation(file: PsiFile): Input? {
|
||||||
val doc: Document = file.viewProvider.document ?: return null
|
val doc: Document = file.viewProvider.document ?: return null
|
||||||
@ -318,62 +315,6 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build spell index payload: identifiers + comments/strings from simple highlighter.
|
|
||||||
// We limit identifier checks to declarations (val, var, fun, class, enum) and enum constants.
|
|
||||||
val spellIds = ArrayList<IntRange>()
|
|
||||||
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 ->
|
tokens.forEach { s ->
|
||||||
if (s.kind == HighlightKind.EnumConstant) {
|
if (s.kind == HighlightKind.EnumConstant) {
|
||||||
val start = s.range.start
|
val start = s.range.start
|
||||||
@ -384,40 +325,9 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val commentRanges = tokens.filter { it.kind == HighlightKind.Comment }.map { it.range.start until it.range.endExclusive }
|
return Result(collectedInfo.modStamp, out, null)
|
||||||
val stringRanges = tokens.filter { it.kind == HighlightKind.String }.map { it.range.start until it.range.endExclusive }
|
|
||||||
|
|
||||||
return Result(collectedInfo.modStamp, out, null,
|
|
||||||
spellIdentifiers = spellIds,
|
|
||||||
spellComments = commentRanges,
|
|
||||||
spellStrings = stringRanges)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to add all segments of a type name to the spell index.
|
|
||||||
*/
|
|
||||||
private fun addTypeNames(t: MiniTypeRef?, add: (net.sergeych.lyng.Pos, String) -> 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) {
|
override fun apply(file: PsiFile, annotationResult: Result?, holder: AnnotationHolder) {
|
||||||
if (annotationResult == null) return
|
if (annotationResult == null) return
|
||||||
@ -429,32 +339,6 @@ class LyngExternalAnnotator : ExternalAnnotator<LyngExternalAnnotator.Input, Lyn
|
|||||||
|
|
||||||
val doc = file.viewProvider.document
|
val doc = file.viewProvider.document
|
||||||
|
|
||||||
// Store spell index for spell/grammar engines to consume (suspend until ready)
|
|
||||||
val ids = result.spellIdentifiers.map { TextRange(it.first, it.last + 1) }
|
|
||||||
val coms = result.spellComments.map { TextRange(it.first, it.last + 1) }
|
|
||||||
val strs = result.spellStrings.map { TextRange(it.first, it.last + 1) }
|
|
||||||
net.sergeych.lyng.idea.spell.LyngSpellIndex.store(file,
|
|
||||||
net.sergeych.lyng.idea.spell.LyngSpellIndex.Data(
|
|
||||||
modStamp = result.modStamp,
|
|
||||||
identifiers = ids,
|
|
||||||
comments = coms,
|
|
||||||
strings = strs
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Optional diagnostic overlay: visualize the ranges we will feed to spellcheckers
|
|
||||||
val settings = net.sergeych.lyng.idea.settings.LyngFormatterSettings.getInstance(file.project)
|
|
||||||
if (settings.debugShowSpellFeed) {
|
|
||||||
fun paint(r: TextRange, label: String) {
|
|
||||||
holder.newAnnotation(HighlightSeverity.WEAK_WARNING, "spell-feed: $label")
|
|
||||||
.range(r)
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
ids.forEach { paint(it, "id") }
|
|
||||||
coms.forEach { paint(it, "comment") }
|
|
||||||
if (settings.spellCheckStringLiterals) strs.forEach { paint(it, "string") }
|
|
||||||
}
|
|
||||||
|
|
||||||
for (s in result.spans) {
|
for (s in result.spans) {
|
||||||
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
|
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
|
||||||
.range(TextRange(s.start, s.end))
|
.range(TextRange(s.start, s.end))
|
||||||
|
|||||||
@ -1,43 +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.editor.Editor
|
|
||||||
import com.intellij.openapi.project.Project
|
|
||||||
import com.intellij.psi.PsiFile
|
|
||||||
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lightweight quick-fix that adds a word to the per-project Lyng dictionary.
|
|
||||||
*/
|
|
||||||
class AddToLyngDictionaryFix(private val word: String) : IntentionAction {
|
|
||||||
override fun getText(): String = "Add '$word' to Lyng dictionary"
|
|
||||||
override fun getFamilyName(): String = "Lyng Spelling"
|
|
||||||
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = word.isNotBlank()
|
|
||||||
override fun startInWriteAction(): Boolean = true
|
|
||||||
|
|
||||||
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
|
|
||||||
val settings = LyngFormatterSettings.getInstance(project)
|
|
||||||
val learned = settings.learnedWords
|
|
||||||
learned.add(word.lowercase())
|
|
||||||
settings.learnedWords = learned
|
|
||||||
// Restart daemon to refresh highlights
|
|
||||||
if (file != null) DaemonCodeAnalyzer.getInstance(project).restart(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,137 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.grazie
|
|
||||||
|
|
||||||
import com.intellij.grazie.grammar.strategy.GrammarCheckingStrategy
|
|
||||||
import com.intellij.grazie.grammar.strategy.GrammarCheckingStrategy.TextDomain
|
|
||||||
import com.intellij.ide.plugins.PluginManagerCore
|
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
|
||||||
import com.intellij.openapi.extensions.PluginId
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grazie/Natural Languages strategy for Lyng.
|
|
||||||
*
|
|
||||||
* - Comments: checked as natural language (TextDomain.COMMENTS)
|
|
||||||
* - String literals: optionally checked (setting), skipping printf-like specifiers via stealth ranges (TextDomain.LITERALS)
|
|
||||||
* - Identifiers (non-keywords): checked under TextDomain.CODE so "Process code" controls apply
|
|
||||||
* - Keywords: skipped
|
|
||||||
*/
|
|
||||||
class LyngGrazieStrategy : GrammarCheckingStrategy {
|
|
||||||
|
|
||||||
private val log = Logger.getInstance(LyngGrazieStrategy::class.java)
|
|
||||||
@Volatile private var loggedOnce = false
|
|
||||||
@Volatile private var loggedFirstMatch = false
|
|
||||||
private val seenTypes: MutableSet<String> = 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<IntRange> {
|
|
||||||
val result = LinkedHashSet<IntRange>()
|
|
||||||
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<Int, Int> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -19,84 +19,29 @@ package net.sergeych.lyng.idea.grazie
|
|||||||
import com.intellij.grazie.text.TextContent
|
import com.intellij.grazie.text.TextContent
|
||||||
import com.intellij.grazie.text.TextContent.TextDomain
|
import com.intellij.grazie.text.TextContent.TextDomain
|
||||||
import com.intellij.grazie.text.TextExtractor
|
import com.intellij.grazie.text.TextExtractor
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
|
||||||
import com.intellij.psi.PsiElement
|
import com.intellij.psi.PsiElement
|
||||||
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
||||||
import net.sergeych.lyng.idea.settings.LyngFormatterSettings
|
import net.sergeych.lyng.idea.psi.LyngElementTypes
|
||||||
import net.sergeych.lyng.idea.spell.LyngSpellIndex
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides Grazie with extractable text for Lyng PSI elements.
|
* Simplified TextExtractor for Lyng.
|
||||||
* We return text for identifiers, comments, and (optionally) string literals.
|
* Designates areas for Natural Languages (Grazie) to check.
|
||||||
* printf-like specifiers are filtered by the Grammar strategy via stealth ranges.
|
|
||||||
*/
|
*/
|
||||||
class LyngTextExtractor : TextExtractor() {
|
class LyngTextExtractor : TextExtractor() {
|
||||||
private val log = Logger.getInstance(LyngTextExtractor::class.java)
|
|
||||||
@Volatile private var loggedOnce = false
|
|
||||||
private val seen: MutableSet<String> = java.util.Collections.synchronizedSet(mutableSetOf())
|
|
||||||
|
|
||||||
override fun buildTextContent(element: PsiElement, allowedDomains: Set<TextDomain>): TextContent? {
|
override fun buildTextContent(element: PsiElement, allowedDomains: Set<TextDomain>): TextContent? {
|
||||||
val type = element.node?.elementType ?: return null
|
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
|
val domain = when (type) {
|
||||||
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
|
LyngTokenTypes.LINE_COMMENT, LyngTokenTypes.BLOCK_COMMENT -> TextDomain.COMMENTS
|
||||||
else -> null
|
LyngTokenTypes.STRING -> TextDomain.LITERALS
|
||||||
|
LyngElementTypes.NAME_IDENTIFIER,
|
||||||
|
LyngElementTypes.PARAMETER_NAME,
|
||||||
|
LyngElementTypes.ENUM_CONSTANT_NAME -> TextDomain.COMMENTS
|
||||||
|
else -> return null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (domain == null) return null
|
|
||||||
|
|
||||||
// If literals aren't requested but fallback is enabled, route strings as COMMENTS
|
if (!allowedDomains.contains(domain)) return null
|
||||||
if (domain == TextDomain.LITERALS && !allowedDomains.contains(TextDomain.LITERALS) && settings.grazieTreatLiteralsAsComments) {
|
|
||||||
domain = TextDomain.COMMENTS
|
return TextContent.psiFragment(domain, element)
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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)
|
||||||
|
}
|
||||||
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 ->
|
override fun createParser(project: Project?): PsiParser = PsiParser { root, builder ->
|
||||||
val mark: PsiBuilder.Marker = builder.mark()
|
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)
|
mark.done(root)
|
||||||
builder.treeBuilt
|
builder.treeBuilt
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 reindentClosedBlockOnEnter: Boolean = true,
|
||||||
var reindentPastedBlocks: Boolean = true,
|
var reindentPastedBlocks: Boolean = true,
|
||||||
var normalizeBlockCommentIndent: Boolean = false,
|
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<String> = mutableSetOf(),
|
|
||||||
// Experimental: enable Lyng autocompletion (can be disabled if needed)
|
// Experimental: enable Lyng autocompletion (can be disabled if needed)
|
||||||
var enableLyngCompletionExperimental: Boolean = true,
|
var enableLyngCompletionExperimental: Boolean = true,
|
||||||
)
|
)
|
||||||
@ -82,42 +64,6 @@ class LyngFormatterSettings(private val project: Project) : PersistentStateCompo
|
|||||||
get() = myState.normalizeBlockCommentIndent
|
get() = myState.normalizeBlockCommentIndent
|
||||||
set(value) { myState.normalizeBlockCommentIndent = value }
|
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<String>
|
|
||||||
get() = myState.learnedWords
|
|
||||||
set(value) { myState.learnedWords = value }
|
|
||||||
|
|
||||||
var enableLyngCompletionExperimental: Boolean
|
var enableLyngCompletionExperimental: Boolean
|
||||||
get() = myState.enableLyngCompletionExperimental
|
get() = myState.enableLyngCompletionExperimental
|
||||||
set(value) { myState.enableLyngCompletionExperimental = value }
|
set(value) { myState.enableLyngCompletionExperimental = value }
|
||||||
|
|||||||
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 reindentClosedBlockCb: JCheckBox? = null
|
||||||
private var reindentPasteCb: JCheckBox? = null
|
private var reindentPasteCb: JCheckBox? = null
|
||||||
private var normalizeBlockCommentIndentCb: 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
|
private var enableCompletionCb: JCheckBox? = null
|
||||||
|
|
||||||
override fun getDisplayName(): String = "Lyng Formatter"
|
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 '}'")
|
reindentClosedBlockCb = JCheckBox("Reindent enclosed block on Enter after '}'")
|
||||||
reindentPasteCb = JCheckBox("Reindent pasted blocks (align pasted code to current indent)")
|
reindentPasteCb = JCheckBox("Reindent pasted blocks (align pasted code to current indent)")
|
||||||
normalizeBlockCommentIndentCb = JCheckBox("Normalize block comment indentation [experimental]")
|
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)")
|
enableCompletionCb = JCheckBox("Enable Lyng autocompletion (experimental)")
|
||||||
|
|
||||||
// Tooltips / short help
|
// 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."
|
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."
|
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)."
|
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)."
|
enableCompletionCb?.toolTipText = "Turn on/off the lightweight Lyng code completion (BASIC)."
|
||||||
p.add(spacingCb)
|
p.add(spacingCb)
|
||||||
p.add(wrappingCb)
|
p.add(wrappingCb)
|
||||||
p.add(reindentClosedBlockCb)
|
p.add(reindentClosedBlockCb)
|
||||||
p.add(reindentPasteCb)
|
p.add(reindentPasteCb)
|
||||||
p.add(normalizeBlockCommentIndentCb)
|
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)
|
p.add(enableCompletionCb)
|
||||||
panel = p
|
panel = p
|
||||||
reset()
|
reset()
|
||||||
@ -100,14 +69,6 @@ class LyngFormatterSettingsConfigurable(private val project: Project) : Configur
|
|||||||
reindentClosedBlockCb?.isSelected != s.reindentClosedBlockOnEnter ||
|
reindentClosedBlockCb?.isSelected != s.reindentClosedBlockOnEnter ||
|
||||||
reindentPasteCb?.isSelected != s.reindentPastedBlocks ||
|
reindentPasteCb?.isSelected != s.reindentPastedBlocks ||
|
||||||
normalizeBlockCommentIndentCb?.isSelected != s.normalizeBlockCommentIndent ||
|
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
|
enableCompletionCb?.isSelected != s.enableLyngCompletionExperimental
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,14 +79,6 @@ class LyngFormatterSettingsConfigurable(private val project: Project) : Configur
|
|||||||
s.reindentClosedBlockOnEnter = reindentClosedBlockCb?.isSelected == true
|
s.reindentClosedBlockOnEnter = reindentClosedBlockCb?.isSelected == true
|
||||||
s.reindentPastedBlocks = reindentPasteCb?.isSelected == true
|
s.reindentPastedBlocks = reindentPasteCb?.isSelected == true
|
||||||
s.normalizeBlockCommentIndent = normalizeBlockCommentIndentCb?.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
|
s.enableLyngCompletionExperimental = enableCompletionCb?.isSelected == true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,14 +89,6 @@ class LyngFormatterSettingsConfigurable(private val project: Project) : Configur
|
|||||||
reindentClosedBlockCb?.isSelected = s.reindentClosedBlockOnEnter
|
reindentClosedBlockCb?.isSelected = s.reindentClosedBlockOnEnter
|
||||||
reindentPasteCb?.isSelected = s.reindentPastedBlocks
|
reindentPasteCb?.isSelected = s.reindentPastedBlocks
|
||||||
normalizeBlockCommentIndentCb?.isSelected = s.normalizeBlockCommentIndent
|
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
|
enableCompletionCb?.isSelected = s.enableLyngCompletionExperimental
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<TextRange>,
|
|
||||||
val comments: List<TextRange>,
|
|
||||||
val strings: List<TextRange>,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val KEY: Key<Data> = 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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -20,10 +20,11 @@ import com.intellij.psi.PsiElement
|
|||||||
import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy
|
import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy
|
||||||
import com.intellij.spellchecker.tokenizer.Tokenizer
|
import com.intellij.spellchecker.tokenizer.Tokenizer
|
||||||
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
import net.sergeych.lyng.idea.highlight.LyngTokenTypes
|
||||||
|
import net.sergeych.lyng.idea.psi.LyngElementTypes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard IntelliJ spellchecking strategy for Lyng.
|
* 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() {
|
class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
|
||||||
override fun getTokenizer(element: PsiElement?): Tokenizer<*> {
|
override fun getTokenizer(element: PsiElement?): Tokenizer<*> {
|
||||||
@ -31,21 +32,9 @@ class LyngSpellcheckingStrategy : SpellcheckingStrategy() {
|
|||||||
return when (type) {
|
return when (type) {
|
||||||
LyngTokenTypes.LINE_COMMENT, LyngTokenTypes.BLOCK_COMMENT -> TEXT_TOKENIZER
|
LyngTokenTypes.LINE_COMMENT, LyngTokenTypes.BLOCK_COMMENT -> TEXT_TOKENIZER
|
||||||
LyngTokenTypes.STRING -> TEXT_TOKENIZER
|
LyngTokenTypes.STRING -> TEXT_TOKENIZER
|
||||||
LyngTokenTypes.IDENTIFIER -> {
|
LyngElementTypes.NAME_IDENTIFIER,
|
||||||
// We use standard NameIdentifierOwner/PsiNamedElement-based logic
|
LyngElementTypes.PARAMETER_NAME,
|
||||||
// if it's a declaration. Argument names, class names, etc. are PSI-based.
|
LyngElementTypes.ENUM_CONSTANT_NAME -> TEXT_TOKENIZER
|
||||||
// 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
|
|
||||||
}
|
|
||||||
else -> super.getTokenizer(element)
|
else -> super.getTokenizer(element)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
~ you may not use this file except in compliance with the License.
|
~ you may not use this file except in compliance with the License.
|
||||||
@ -20,8 +20,6 @@
|
|||||||
-->
|
-->
|
||||||
<idea-plugin>
|
<idea-plugin>
|
||||||
<extensions defaultExtensionNs="com.intellij">
|
<extensions defaultExtensionNs="com.intellij">
|
||||||
<grazie.grammar.strategy language="Lyng"
|
|
||||||
implementationClass="net.sergeych.lyng.idea.grazie.LyngGrazieStrategy"/>
|
|
||||||
<!-- Provide text extraction for Lyng PSI so Grazie (bundled Natural Languages) can check content -->
|
<!-- Provide text extraction for Lyng PSI so Grazie (bundled Natural Languages) can check content -->
|
||||||
<grazie.textExtractor language="Lyng"
|
<grazie.textExtractor language="Lyng"
|
||||||
implementationClass="net.sergeych.lyng.idea.grazie.LyngTextExtractor"/>
|
implementationClass="net.sergeych.lyng.idea.grazie.LyngTextExtractor"/>
|
||||||
|
|||||||
@ -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");
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
~ you may not use this file except in compliance with the License.
|
~ you may not use this file except in compliance with the License.
|
||||||
@ -21,8 +21,6 @@
|
|||||||
-->
|
-->
|
||||||
<idea-plugin>
|
<idea-plugin>
|
||||||
<extensions defaultExtensionNs="com.intellij">
|
<extensions defaultExtensionNs="com.intellij">
|
||||||
<grazie.grammar.strategy language="Lyng"
|
|
||||||
implementationClass="net.sergeych.lyng.idea.grazie.LyngGrazieStrategy"/>
|
|
||||||
<!-- Provide text extraction for Lyng PSI so Grazie can actually check content -->
|
<!-- Provide text extraction for Lyng PSI so Grazie can actually check content -->
|
||||||
<grazie.textExtractor language="Lyng"
|
<grazie.textExtractor language="Lyng"
|
||||||
implementationClass="net.sergeych.lyng.idea.grazie.LyngTextExtractor"/>
|
implementationClass="net.sergeych.lyng.idea.grazie.LyngTextExtractor"/>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user