plugin: remove flaky spell checking for 2024.*

This commit is contained in:
Sergey Chernov 2026-01-13 07:10:03 +01:00
parent 80933c287d
commit 2d2a74656c
10 changed files with 127 additions and 955 deletions

View File

@ -21,6 +21,12 @@
- MI Satisfaction: Abstract requirements are automatically satisfied by matching concrete members found later in the C3 MRO chain without requiring explicit proxy methods. - MI Satisfaction: Abstract requirements are automatically satisfied by matching concrete members found later in the C3 MRO chain without requiring explicit proxy methods.
- Integration: Updated highlighters (lynglib, lyngweb, IDEA plugin), IDEA completion, and Grazie grammar checking. - Integration: Updated highlighters (lynglib, lyngweb, IDEA plugin), IDEA completion, and Grazie grammar checking.
- Documentation: Updated `docs/OOP.md` with sections on "Abstract Classes and Members", "Interfaces", and "Overriding and Virtual Dispatch". - Documentation: Updated `docs/OOP.md` with sections on "Abstract Classes and Members", "Interfaces", and "Overriding and Virtual Dispatch".
- IDEA plugin: Improved natural language support and spellchecking
- Disabled the limited built-in English and Technical dictionaries.
- Enforced usage of the platform's standard Natural Languages (Grazie) and Spellchecker components.
- Integrated `SpellCheckerManager` for word suggestions and validation, respecting users' personal and project dictionaries.
- Added project-specific "learned words" support via `Lyng Formatter` settings and quick-fixes.
- Enhanced fallback spellchecker for technical terms and Lyng-specific vocabulary.
- Language: Class properties with accessors - Language: Class properties with accessors
- Support for `val` (read-only) and `var` (read-write) properties in classes. - Support for `val` (read-only) and `var` (read-write) properties in classes.

View File

@ -1,83 +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.openapi.diagnostic.Logger
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.zip.GZIPInputStream
/**
* Very simple English dictionary loader for offline suggestions on IC-243.
* It loads a word list from classpath resources. Supports plain text (one word per line)
* and gzipped text if the resource ends with .gz.
*/
object EnglishDictionary {
private val log = Logger.getInstance(EnglishDictionary::class.java)
@Volatile private var loaded = false
@Volatile private var words: Set<String> = emptySet()
/**
* Load dictionary from bundled resources (once).
* If multiple candidates exist, the first found is used.
*/
private fun ensureLoaded() {
if (loaded) return
synchronized(this) {
if (loaded) return
val candidates = listOf(
// preferred large bundles first (add en-basic.txt.gz ~3–5MB here)
"/dictionaries/en-basic.txt.gz",
"/dictionaries/en-large.txt.gz",
// plain text fallbacks
"/dictionaries/en-basic.txt",
"/dictionaries/en-large.txt",
)
val merged = HashSet<String>(128_000)
for (res in candidates) {
try {
val stream = javaClass.getResourceAsStream(res) ?: continue
val reader = if (res.endsWith(".gz"))
BufferedReader(InputStreamReader(GZIPInputStream(stream)))
else
BufferedReader(InputStreamReader(stream))
var loadedCount = 0
reader.useLines { seq -> seq.forEach { line ->
val w = line.trim()
if (w.isNotEmpty() && !w.startsWith("#")) { merged += w.lowercase(); loadedCount++ }
} }
log.info("EnglishDictionary: loaded $loadedCount words from $res (total=${merged.size})")
} catch (t: Throwable) {
log.info("EnglishDictionary: failed to load $res: ${t.javaClass.simpleName}: ${t.message}")
}
}
if (merged.isEmpty()) {
// Fallback minimal set
merged += setOf("comment","comments","error","errors","found","file","not","word","words","count","value","name","class","function","string")
log.info("EnglishDictionary: using minimal built-in set (${merged.size})")
}
words = merged
loaded = true
}
}
fun allWords(): Set<String> {
ensureLoaded()
return words
}
}

View File

@ -402,6 +402,8 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
var painted = 0 var painted = 0
val docText = file.viewProvider.document?.text ?: return 0 val docText = file.viewProvider.document?.text ?: return 0
val tokenRegex = Regex("[A-Za-z][A-Za-z0-9_']{2,}") val tokenRegex = Regex("[A-Za-z][A-Za-z0-9_']{2,}")
val settings = LyngFormatterSettings.getInstance(file.project)
val learned = settings.learnedWords
for ((content, hostRange) in fragments) { for ((content, hostRange) in fragments) {
val text = try { docText.substring(hostRange.startOffset, hostRange.endOffset) } catch (_: Throwable) { null } ?: continue val text = try { docText.substring(hostRange.startOffset, hostRange.endOffset) } catch (_: Throwable) { null } ?: continue
var seen = 0 var seen = 0
@ -413,7 +415,7 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
val parts = splitIdentifier(token) val parts = splitIdentifier(token)
for (part in parts) { for (part in parts) {
if (part.length <= 2) continue if (part.length <= 2) continue
if (isAllowedWord(part)) continue if (isAllowedWord(part, learned)) continue
// Map part back to original token occurrence within this hostRange // Map part back to original token occurrence within this hostRange
val localStart = m.range.first + token.indexOf(part) val localStart = m.range.first + token.indexOf(part)
@ -451,6 +453,8 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
var painted = 0 var painted = 0
val docText = file.viewProvider.document?.text val docText = file.viewProvider.document?.text
val tokenRegex = Regex("[A-Za-z][A-Za-z0-9_']{2,}") val tokenRegex = Regex("[A-Za-z][A-Za-z0-9_']{2,}")
val settings = LyngFormatterSettings.getInstance(file.project)
val learned = settings.learnedWords
val baseWords = setOf( val baseWords = setOf(
// small, common vocabulary to catch near-miss typos in typical code/comments // small, common vocabulary to catch near-miss typos in typical code/comments
"comment","comments","error","errors","found","file","not","word","words","count","value","name","class","function","string" "comment","comments","error","errors","found","file","not","word","words","count","value","name","class","function","string"
@ -469,7 +473,7 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
for (part in parts) { for (part in parts) {
seen++ seen++
val lower = part.lowercase() val lower = part.lowercase()
if (lower.length <= 2 || isAllowedWord(part)) continue if (lower.length <= 2 || isAllowedWord(part, learned)) continue
val localStart = m.range.first + token.indexOf(part) val localStart = m.range.first + token.indexOf(part)
val localEnd = localStart + part.length val localEnd = localStart + part.length
@ -540,29 +544,32 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
private fun suggestReplacements(file: PsiFile, word: String): List<String> { private fun suggestReplacements(file: PsiFile, word: String): List<String> {
val lower = word.lowercase() val lower = word.lowercase()
val fromProject = collectProjectWords(file) val fromProject = collectProjectWords(file)
val fromTech = TechDictionary.allWords()
val fromEnglish = EnglishDictionary.allWords() val fromSpellChecker = try {
// Merge with priority: project (p=0), tech (p=1), english (p=2) val mgrCls = Class.forName("com.intellij.spellchecker.SpellCheckerManager")
val getInstance = mgrCls.methods.firstOrNull { it.name == "getInstance" && it.parameterCount == 1 }
val getSuggestions = mgrCls.methods.firstOrNull { it.name == "getSuggestions" && it.parameterCount == 1 && it.parameterTypes[0] == String::class.java }
val mgr = getInstance?.invoke(null, file.project)
if (mgr != null && getSuggestions != null) {
@Suppress("UNCHECKED_CAST")
getSuggestions.invoke(mgr, word) as? List<String>
} else null
} catch (_: Throwable) {
null
} ?: emptyList()
// Merge with priority: project (p=0), spellchecker (p=1)
val all = LinkedHashSet<String>() val all = LinkedHashSet<String>()
all.addAll(fromProject) // Add project words that are close enough
all.addAll(fromTech) for (w in fromProject) {
all.addAll(fromEnglish)
data class Cand(val w: String, val d: Int, val p: Int)
val cands = ArrayList<Cand>(32)
for (w in all) {
if (w == lower) continue if (w == lower) continue
if (kotlin.math.abs(w.length - lower.length) > 2) continue if (kotlin.math.abs(w.length - lower.length) <= 2 && editDistance(lower, w) <= 2) {
val d = editDistance(lower, w) all.add(w)
val p = when {
w in fromProject -> 0
w in fromTech -> 1
else -> 2
} }
cands += Cand(w, d, p)
} }
cands.sortWith(compareBy<Cand> { it.d }.thenBy { it.p }.thenBy { it.w }) all.addAll(fromSpellChecker)
// Return a larger pool so callers can choose desired display count
return cands.take(16).map { it.w } return all.take(16).toList()
} }
private fun collectProjectWords(file: PsiFile): Set<String> { private fun collectProjectWords(file: PsiFile): Set<String> {
@ -589,14 +596,19 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
return out return out
} }
private fun isAllowedWord(w: String): Boolean { private fun isAllowedWord(w: String, learnedWords: Set<String> = emptySet()): Boolean {
val s = w.lowercase() val s = w.lowercase()
if (s in learnedWords) return true
return s in setOf( return s in setOf(
// common code words / language keywords to avoid noise // common code words / language keywords to avoid noise
"val","var","fun","class","interface","enum","type","import","package","return","if","else","when","while","for","try","catch","finally","true","false","null", "val","var","fun","class","interface","enum","type","import","package","return","if","else","when","while","for","try","catch","finally","true","false","null",
"abstract","closed","override", "abstract","closed","override",
// very common English words // very common English words
"the","and","or","not","with","from","into","this","that","file","found","count","name","value","object" "the","and","or","not","with","from","into","this","that","file","found","count","name","value","object",
// Lyng technical/vocabulary words formerly in TechDictionary
"lyng","miniast","binder","printf","specifier","specifiers","regex","token","tokens",
"identifier","identifiers","keyword","keywords","comment","comments","string","strings",
"literal","literals","formatting","formatter","grazie","typo","typos","dictionary","dictionaries"
) )
} }

View File

@ -1,77 +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.openapi.diagnostic.Logger
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.zip.GZIPInputStream
/**
* Lightweight technical/Lyng vocabulary dictionary.
* Loaded from classpath resources; supports .txt and .txt.gz. Merged with EnglishDictionary.
*/
object TechDictionary {
private val log = Logger.getInstance(TechDictionary::class.java)
@Volatile private var loaded = false
@Volatile private var words: Set<String> = emptySet()
private fun ensureLoaded() {
if (loaded) return
synchronized(this) {
if (loaded) return
val candidates = listOf(
"/dictionaries/tech-lyng.txt.gz",
"/dictionaries/tech-lyng.txt"
)
val merged = HashSet<String>(8_000)
for (res in candidates) {
try {
val stream = javaClass.getResourceAsStream(res) ?: continue
val reader = if (res.endsWith(".gz"))
BufferedReader(InputStreamReader(GZIPInputStream(stream)))
else
BufferedReader(InputStreamReader(stream))
var n = 0
reader.useLines { seq -> seq.forEach { line ->
val w = line.trim()
if (w.isNotEmpty() && !w.startsWith("#")) { merged += w.lowercase(); n++ }
} }
log.info("TechDictionary: loaded $n words from $res (total=${merged.size})")
} catch (t: Throwable) {
log.info("TechDictionary: failed to load $res: ${t.javaClass.simpleName}: ${t.message}")
}
}
if (merged.isEmpty()) {
merged += setOf(
// minimal Lyng/tech seeding to avoid empty dictionary
"lyng","miniast","binder","printf","specifier","specifiers","regex","token","tokens",
"identifier","identifiers","keyword","keywords","comment","comments","string","strings",
"literal","literals","formatting","formatter","grazie","typo","typos","dictionary","dictionaries"
)
log.info("TechDictionary: using minimal built-in set (${merged.size})")
}
words = merged
loaded = true
}
}
fun allWords(): Set<String> {
ensureLoaded()
return words
}
}

View File

@ -20,13 +20,14 @@ package net.sergeych.lyng.idea.navigation
import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.psi.PsiElement import com.intellij.psi.PsiElement
import net.sergeych.lyng.idea.LyngLanguage
/** /**
* Ensures Ctrl+B (Go to Definition) works on Lyng identifiers by resolving through LyngPsiReference. * Ensures Ctrl+B (Go to Definition) works on Lyng identifiers by resolving through LyngPsiReference.
*/ */
class LyngGotoDeclarationHandler : GotoDeclarationHandler { class LyngGotoDeclarationHandler : GotoDeclarationHandler {
override fun getGotoDeclarationTargets(sourceElement: PsiElement?, offset: Int, editor: Editor?): Array<PsiElement>? { override fun getGotoDeclarationTargets(sourceElement: PsiElement?, offset: Int, editor: Editor?): Array<PsiElement>? {
if (sourceElement == null) return null if (sourceElement == null || sourceElement.language != LyngLanguage) return null
val allTargets = mutableListOf<PsiElement>() val allTargets = mutableListOf<PsiElement>()

View File

@ -38,16 +38,18 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
val mini = LyngAstManager.getMiniAst(file) ?: return emptyArray() val mini = LyngAstManager.getMiniAst(file) ?: return emptyArray()
val binding = LyngAstManager.getBinding(file) val binding = LyngAstManager.getBinding(file)
val imported = DocLookupUtils.canonicalImportedModules(mini, text).toSet()
val currentPackage = getPackageName(file)
val allowedPackages = if (currentPackage != null) imported + currentPackage else imported
// 1. Member resolution (obj.member) // 1. Member resolution (obj.member)
val dotPos = TextCtx.findDotLeft(text, offset) val dotPos = TextCtx.findDotLeft(text, offset)
if (dotPos != null) { if (dotPos != null) {
val imported = DocLookupUtils.canonicalImportedModules(mini, text) val receiverClass = DocLookupUtils.guessReceiverClassViaMini(mini, text, dotPos, imported.toList(), binding)
val receiverClass = DocLookupUtils.guessReceiverClassViaMini(mini, text, dotPos, imported, binding) ?: DocLookupUtils.guessReceiverClass(text, dotPos, imported.toList(), mini)
?: DocLookupUtils.guessReceiverClass(text, dotPos, imported, mini)
if (receiverClass != null) { if (receiverClass != null) {
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, name, mini) val resolved = DocLookupUtils.resolveMemberWithInheritance(imported.toList(), receiverClass, name, mini)
if (resolved != null) { if (resolved != null) {
val owner = resolved.first val owner = resolved.first
val member = resolved.second val member = resolved.second
@ -75,7 +77,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
} }
// If we couldn't resolve exactly, we might still want to search globally but ONLY for members // If we couldn't resolve exactly, we might still want to search globally but ONLY for members
if (results.isEmpty()) { if (results.isEmpty()) {
results.addAll(resolveGlobally(file.project, name, membersOnly = true)) results.addAll(resolveGlobally(file.project, name, membersOnly = true, allowedPackages = allowedPackages))
} }
} else { } else {
// 2. Local resolution via Binder // 2. Local resolution via Binder
@ -94,7 +96,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
// 3. Global project scan // 3. Global project scan
// Only search globally if we haven't found a strong local match // Only search globally if we haven't found a strong local match
if (results.isEmpty()) { if (results.isEmpty()) {
results.addAll(resolveGlobally(file.project, name)) results.addAll(resolveGlobally(file.project, name, allowedPackages = allowedPackages))
} }
} }
@ -141,6 +143,16 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
return null return null
} }
private fun getPackageName(file: PsiFile): String? {
val mini = LyngAstManager.getMiniAst(file) ?: return null
return try {
val pkg = mini.range.start.source.extractPackageName()
if (pkg.startsWith("lyng.")) pkg else "lyng.$pkg"
} catch (e: Exception) {
null
}
}
override fun resolve(): PsiElement? { override fun resolve(): PsiElement? {
val results = multiResolve(false) val results = multiResolve(false)
if (results.isEmpty()) return null if (results.isEmpty()) return null
@ -154,13 +166,20 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
return target return target
} }
private fun resolveGlobally(project: Project, name: String, membersOnly: Boolean = false): List<ResolveResult> { private fun resolveGlobally(project: Project, name: String, membersOnly: Boolean = false, allowedPackages: Set<String>? = null): List<ResolveResult> {
val results = mutableListOf<ResolveResult>() val results = mutableListOf<ResolveResult>()
val files = FilenameIndex.getAllFilesByExt(project, "lyng", GlobalSearchScope.projectScope(project)) val files = FilenameIndex.getAllFilesByExt(project, "lyng", GlobalSearchScope.projectScope(project))
val psiManager = PsiManager.getInstance(project) val psiManager = PsiManager.getInstance(project)
for (vFile in files) { for (vFile in files) {
val file = psiManager.findFile(vFile) ?: continue val file = psiManager.findFile(vFile) ?: continue
// Filter by package if requested
if (allowedPackages != null) {
val pkg = getPackageName(file)
if (pkg == null || pkg !in allowedPackages) continue
}
val mini = LyngAstManager.getMiniAst(file) ?: continue val mini = LyngAstManager.getMiniAst(file) ?: continue
val src = mini.range.start.source val src = mini.range.start.source

View File

@ -1,466 +0,0 @@
the
be
to
of
and
a
in
that
have
I
it
for
not
on
with
he
as
you
do
at
this
but
his
by
from
they
we
say
her
she
or
an
will
my
one
all
would
there
their
what
so
up
out
if
about
who
get
which
go
me
when
make
can
like
time
no
just
him
know
take
people
into
year
your
good
some
could
them
see
other
than
then
now
look
only
come
its
over
think
also
back
after
use
two
how
our
work
first
well
way
even
new
want
because
any
these
give
day
most
us
is
are
was
were
been
being
does
did
done
has
had
having
may
might
must
shall
should
ought
need
used
here
therefore
where
why
while
until
since
before
afterward
between
among
without
within
through
across
against
toward
upon
above
below
under
around
near
far
early
late
often
always
never
seldom
sometimes
usually
really
very
quite
rather
almost
already
again
still
yet
soon
today
tomorrow
yesterday
number
string
boolean
true
false
null
none
file
files
path
paths
line
lines
word
words
count
value
values
name
names
title
text
message
error
errors
warning
warnings
info
information
debug
trace
format
printf
specifier
specifiers
pattern
patterns
match
matches
regex
version
versions
module
modules
package
packages
import
imports
export
exports
class
classes
object
objects
function
functions
method
methods
parameter
parameters
argument
arguments
variable
variables
constant
constants
type
types
generic
generics
map
maps
list
lists
array
arrays
set
sets
queue
stack
graph
tree
node
nodes
edge
edges
pair
pairs
key
keys
value
values
index
indices
length
size
empty
contains
equals
compare
greater
less
minimum
maximum
average
sum
total
random
round
floor
ceil
sin
cos
tan
sqrt
abs
min
max
read
write
open
close
append
create
delete
remove
update
save
load
start
stop
run
execute
return
break
continue
try
catch
finally
throw
throws
if
else
when
while
for
loop
range
case
switch
default
optional
required
enable
disable
enabled
disabled
visible
hidden
public
private
protected
internal
external
inline
override
abstract
sealed
open
final
static
const
lazy
late
init
initialize
configuration
settings
option
options
preference
preferences
project
projects
module
modules
build
builds
compile
compiles
compiler
test
tests
testing
assert
assertion
result
results
success
failure
status
state
context
scope
scopes
token
tokens
identifier
identifiers
keyword
keywords
comment
comments
string
strings
literal
literals
formatting
formatter
spell
spelling
dictionary
dictionaries
language
languages
natural
grazie
typo
typos
suggest
suggestion
suggestions
replace
replacement
replacements
learn
learned
learns
filter
filters
exclude
excludes
include
includes
bundle
bundled
resource
resources
gzipped
plain
text
editor
editors
inspection
inspections
highlight
highlighting
underline
underlines
style
styles
range
ranges
offset
offsets
position
positions
apply
applies
provides
present
absent
available
unavailable
version
build
platform
ide
intellij
plugin
plugins
sandbox
gradle
kotlin
java
linux
macos
windows
unix
system
systems
support
supports
compatible
compatibility
fallback
native
automatic
autoswitch
switch
switches

View File

@ -1,282 +0,0 @@
# Lyng/tech vocabulary – one word per line, lowercase
lyng
miniast
binder
printf
specifier
specifiers
regex
regexp
token
tokens
lexer
parser
syntax
semantic
highlight
highlighting
underline
typo
typos
dictionary
dictionaries
grazie
natural
languages
inspection
inspections
annotation
annotator
annotations
quickfix
quickfixes
intention
intentions
replacement
replacements
identifier
identifiers
keyword
keywords
comment
comments
string
strings
literal
literals
formatting
formatter
splitter
camelcase
snakecase
pascalcase
uppercase
lowercase
titlecase
case
cases
project
module
modules
resource
resources
bundle
bundled
gzipped
plaintext
text
range
ranges
offset
offsets
position
positions
apply
applies
runtime
compile
build
artifact
artifacts
plugin
plugins
intellij
idea
sandbox
gradle
kotlin
java
jvm
coroutines
suspend
scope
scopes
context
contexts
tokenizer
tokenizers
spell
spelling
spellcheck
spellchecker
fallback
native
autoswitch
switch
switching
enable
disable
enabled
disabled
setting
settings
preference
preferences
editor
filetype
filetypes
language
languages
psi
psielement
psifile
textcontent
textdomain
stealth
stealthy
printfspec
format
formats
pattern
patterns
match
matches
group
groups
node
nodes
tree
graph
edge
edges
pair
pairs
map
maps
list
lists
array
arrays
set
sets
queue
stack
index
indices
length
size
empty
contains
equals
compare
greater
less
minimum
maximum
average
sum
total
random
round
floor
ceil
sin
cos
tan
sqrt
abs
min
max
read
write
open
close
append
create
delete
remove
update
save
load
start
stop
run
execute
return
break
continue
try
catch
finally
throw
throws
if
else
when
while
for
loop
rangeop
caseop
switchop
default
optional
required
public
private
protected
internal
external
inline
override
abstract
sealed
open
final
static
const
lazy
late
init
initialize
configuration
option
options
projectwide
workspace
crossplatform
multiplatform
commonmain
jsmain
native
platform
api
implementation
dependency
dependencies
classpath
source
sources
document
documents
logging
logger
info
debug
trace
warning
error
severity
severitylevel
intentionaction
daemon
daemoncodeanalyzer
restart
textattributes
textattributeskey
typostyle
learned
learn
tech
vocabulary
domain
term
terms
us
uk
american
british
colour
color
organisation
organization

View File

@ -100,11 +100,16 @@ object LyngFormatter {
fun isControlHeaderNoBrace(s: String): Boolean { fun isControlHeaderNoBrace(s: String): Boolean {
val t = s.trim() val t = s.trim()
if (t.isEmpty()) return false if (t.isEmpty()) return false
// match: if (...) | else if (...) | else // match: if (...) | else if (...) | else | catch (...) | finally
val isIf = Regex("^if\\s*\\(.*\\)\\s*$").matches(t) if (Regex("^if\\s*\\(.*\\)$").matches(t)) return true
val isElseIf = Regex("^else\\s+if\\s*\\(.*\\)\\s*$").matches(t) if (Regex("^else\\s+if\\s*\\(.*\\)$").matches(t)) return true
val isElse = t == "else" if (t == "else") return true
if (isIf || isElseIf || isElse) return true if (Regex("^catch\\s*\\(.*\\)$").matches(t)) return true
if (t == "finally") return true
// Short definition form: fun x() = or val x =
if (Regex("^(override\\s+)?(fun|fn)\\b.*=\\s*$").matches(t)) return true
if (Regex("^(private\\s+|protected\\s+|public\\s+|override\\s+)?(val|var)\\b.*=\\s*$").matches(t)) return true
// property accessors ending with ) or = // property accessors ending with ) or =
if (isAccessorRelated(t)) { if (isAccessorRelated(t)) {
@ -113,11 +118,23 @@ object LyngFormatter {
return false return false
} }
fun isDoubleIndentHeader(s: String): Boolean {
val t = s.trim()
if (!t.endsWith("=")) return false
// Is it a function or property definition?
if (Regex("\\b(fun|fn|val|var)\\b").containsMatchIn(t)) return true
// Is it an accessor?
if (isPropertyAccessor(t)) return true
return false
}
for ((i, rawLine) in lines.withIndex()) { for ((i, rawLine) in lines.withIndex()) {
val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment) val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment)
val code = parts.filter { it.type == PartType.Code }.joinToString("") { it.text } val code = parts.filter { it.type == PartType.Code }.joinToString("") { it.text }
val codeAndStrings = parts.filter { it.type != PartType.BlockComment && it.type != PartType.LineComment }.joinToString("") { it.text }
val trimmedStart = code.dropWhile { it == ' ' || it == '\t' } val trimmedStart = code.dropWhile { it == ' ' || it == '\t' }
val trimmedLine = rawLine.trim() val trimmedLine = rawLine.trim()
val trimmedCodeAndStrings = codeAndStrings.trim()
// Compute effective indent level for this line // Compute effective indent level for this line
val currentExtraIndent = extraIndents.sum() val currentExtraIndent = extraIndents.sum()
@ -138,9 +155,15 @@ object LyngFormatter {
// Single-line control header (if/else/else if) without braces: indent the next // Single-line control header (if/else/else if) without braces: indent the next
// non-empty, non-'}', non-'else' line by one extra level // non-empty, non-'}', non-'else' line by one extra level
val isContinuation = trimmedStart.startsWith("else") || trimmedStart.startsWith("catch") || trimmedStart.startsWith("finally")
val applyAwaiting = awaitingExtraIndent > 0 && trimmedStart.isNotEmpty() && val applyAwaiting = awaitingExtraIndent > 0 && trimmedStart.isNotEmpty() &&
!trimmedStart.startsWith("else") && !trimmedStart.startsWith("}") !trimmedStart.startsWith("}")
if (applyAwaiting) effectiveLevel += awaitingExtraIndent var actualAwaiting = 0
if (applyAwaiting) {
actualAwaiting = awaitingExtraIndent
if (isContinuation) actualAwaiting = (actualAwaiting - 1).coerceAtLeast(0)
effectiveLevel += actualAwaiting
}
val firstChar = trimmedStart.firstOrNull() val firstChar = trimmedStart.firstOrNull()
// While inside parentheses, continuation applies scaled by nesting level // While inside parentheses, continuation applies scaled by nesting level
@ -194,7 +217,7 @@ object LyngFormatter {
val newBlockLevel = blockLevel val newBlockLevel = blockLevel
if (newBlockLevel > oldBlockLevel) { if (newBlockLevel > oldBlockLevel) {
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code)) val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code))
val addedThisLine = (if (applyAwaiting) awaitingExtraIndent else 0) + (if (isAccessorRelatedLine) 1 else 0) val addedThisLine = actualAwaiting + (if (isAccessorRelatedLine) 1 else 0)
repeat(newBlockLevel - oldBlockLevel) { repeat(newBlockLevel - oldBlockLevel) {
extraIndents.add(addedThisLine) extraIndents.add(addedThisLine)
} }
@ -207,23 +230,29 @@ object LyngFormatter {
inBlockComment = nextInBlockComment inBlockComment = nextInBlockComment
// Update awaitingExtraIndent based on current line // Update awaitingExtraIndent based on current line
val nextLineTrimmed = lines.getOrNull(i + 1)?.trim() ?: ""
val nextIsContinuation = nextLineTrimmed.startsWith("else") ||
nextLineTrimmed.startsWith("catch") ||
nextLineTrimmed.startsWith("finally")
if (applyAwaiting && trimmedStart.isNotEmpty()) { if (applyAwaiting && trimmedStart.isNotEmpty()) {
// we have just applied it. // we have just applied it.
val endsWithBrace = code.trimEnd().endsWith("{") val endsWithBrace = code.trimEnd().endsWith("{")
if (!endsWithBrace && isControlHeaderNoBrace(code)) { if (!endsWithBrace && isControlHeaderNoBrace(trimmedCodeAndStrings)) {
// It's another header, increment // It's another header, increment
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code)) val isAccessorOrDouble = isAccessor || (!inBlockComment && isAccessorRelated(code)) || isDoubleIndentHeader(trimmedCodeAndStrings)
awaitingExtraIndent += if (isAccessorRelatedLine) 2 else 1 val increment = if (isAccessorOrDouble) 2 else 1
} else { awaitingExtraIndent = actualAwaiting + increment
// It's the body, reset } else if (!nextIsContinuation) {
// It's the body, and no continuation follows, so reset
awaitingExtraIndent = 0 awaitingExtraIndent = 0
} }
} else { } else {
// start awaiting if current line is a control header without '{' // start awaiting if current line is a control header without '{'
val endsWithBrace = code.trimEnd().endsWith("{") val endsWithBrace = code.trimEnd().endsWith("{")
if (!endsWithBrace && isControlHeaderNoBrace(code)) { if (!endsWithBrace && isControlHeaderNoBrace(trimmedCodeAndStrings)) {
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code)) val isAccessorOrDouble = isAccessor || (!inBlockComment && isAccessorRelated(code)) || isDoubleIndentHeader(trimmedCodeAndStrings)
awaitingExtraIndent = if (isAccessorRelatedLine) 2 else 1 awaitingExtraIndent = if (isAccessorOrDouble) 2 else 1
} }
} }

View File

@ -4811,5 +4811,18 @@ class ScriptTest {
""".trimIndent()) """.trimIndent())
} }
@Test
fun testArgsPriorityWithSplash() = runTest {
eval("""
class A {
val tags get() = ["foo"]
fun f1(tags...) = tags
fun f2(tags...) = f1(...tags)
}
assertEquals(["bar"], A().f2("bar"))
""")
}
} }