Compare commits

..

No commits in common. "2d2a74656c21d35556b020a17fb3d396af902e68" and "017111827d49295bd8ef365b8d4a137074fb0fbf" have entirely different histories.

12 changed files with 964 additions and 150 deletions

View File

@ -21,12 +21,6 @@
- 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.
- 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
- Support for `val` (read-only) and `var` (read-write) properties in classes.

View File

@ -0,0 +1,83 @@
/*
* 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,8 +402,6 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
var painted = 0
val docText = file.viewProvider.document?.text ?: return 0
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) {
val text = try { docText.substring(hostRange.startOffset, hostRange.endOffset) } catch (_: Throwable) { null } ?: continue
var seen = 0
@ -415,7 +413,7 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
val parts = splitIdentifier(token)
for (part in parts) {
if (part.length <= 2) continue
if (isAllowedWord(part, learned)) continue
if (isAllowedWord(part)) continue
// Map part back to original token occurrence within this hostRange
val localStart = m.range.first + token.indexOf(part)
@ -453,8 +451,6 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
var painted = 0
val docText = file.viewProvider.document?.text
val tokenRegex = Regex("[A-Za-z][A-Za-z0-9_']{2,}")
val settings = LyngFormatterSettings.getInstance(file.project)
val learned = settings.learnedWords
val baseWords = setOf(
// 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"
@ -473,7 +469,7 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
for (part in parts) {
seen++
val lower = part.lowercase()
if (lower.length <= 2 || isAllowedWord(part, learned)) continue
if (lower.length <= 2 || isAllowedWord(part)) continue
val localStart = m.range.first + token.indexOf(part)
val localEnd = localStart + part.length
@ -544,32 +540,29 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
private fun suggestReplacements(file: PsiFile, word: String): List<String> {
val lower = word.lowercase()
val fromProject = collectProjectWords(file)
val fromSpellChecker = try {
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 fromTech = TechDictionary.allWords()
val fromEnglish = EnglishDictionary.allWords()
// Merge with priority: project (p=0), tech (p=1), english (p=2)
val all = LinkedHashSet<String>()
// Add project words that are close enough
for (w in fromProject) {
all.addAll(fromProject)
all.addAll(fromTech)
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 (kotlin.math.abs(w.length - lower.length) <= 2 && editDistance(lower, w) <= 2) {
all.add(w)
if (kotlin.math.abs(w.length - lower.length) > 2) continue
val d = editDistance(lower, w)
val p = when {
w in fromProject -> 0
w in fromTech -> 1
else -> 2
}
cands += Cand(w, d, p)
}
all.addAll(fromSpellChecker)
return all.take(16).toList()
cands.sortWith(compareBy<Cand> { it.d }.thenBy { it.p }.thenBy { it.w })
// Return a larger pool so callers can choose desired display count
return cands.take(16).map { it.w }
}
private fun collectProjectWords(file: PsiFile): Set<String> {
@ -596,19 +589,14 @@ class LyngGrazieAnnotator : ExternalAnnotator<LyngGrazieAnnotator.Input, LyngGra
return out
}
private fun isAllowedWord(w: String, learnedWords: Set<String> = emptySet()): Boolean {
private fun isAllowedWord(w: String): Boolean {
val s = w.lowercase()
if (s in learnedWords) return true
return s in setOf(
// 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",
"abstract","closed","override",
// very common English words
"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"
"the","and","or","not","with","from","into","this","that","file","found","count","name","value","object"
)
}

View File

@ -0,0 +1,77 @@
/*
* 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,14 +20,13 @@ package net.sergeych.lyng.idea.navigation
import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler
import com.intellij.openapi.editor.Editor
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.
*/
class LyngGotoDeclarationHandler : GotoDeclarationHandler {
override fun getGotoDeclarationTargets(sourceElement: PsiElement?, offset: Int, editor: Editor?): Array<PsiElement>? {
if (sourceElement == null || sourceElement.language != LyngLanguage) return null
if (sourceElement == null) return null
val allTargets = mutableListOf<PsiElement>()

View File

@ -38,18 +38,16 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
val mini = LyngAstManager.getMiniAst(file) ?: return emptyArray()
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)
val dotPos = TextCtx.findDotLeft(text, offset)
if (dotPos != null) {
val receiverClass = DocLookupUtils.guessReceiverClassViaMini(mini, text, dotPos, imported.toList(), binding)
?: DocLookupUtils.guessReceiverClass(text, dotPos, imported.toList(), mini)
val imported = DocLookupUtils.canonicalImportedModules(mini, text)
val receiverClass = DocLookupUtils.guessReceiverClassViaMini(mini, text, dotPos, imported, binding)
?: DocLookupUtils.guessReceiverClass(text, dotPos, imported, mini)
if (receiverClass != null) {
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported.toList(), receiverClass, name, mini)
val resolved = DocLookupUtils.resolveMemberWithInheritance(imported, receiverClass, name, mini)
if (resolved != null) {
val owner = resolved.first
val member = resolved.second
@ -77,7 +75,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 (results.isEmpty()) {
results.addAll(resolveGlobally(file.project, name, membersOnly = true, allowedPackages = allowedPackages))
results.addAll(resolveGlobally(file.project, name, membersOnly = true))
}
} else {
// 2. Local resolution via Binder
@ -96,7 +94,7 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
// 3. Global project scan
// Only search globally if we haven't found a strong local match
if (results.isEmpty()) {
results.addAll(resolveGlobally(file.project, name, allowedPackages = allowedPackages))
results.addAll(resolveGlobally(file.project, name))
}
}
@ -143,16 +141,6 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
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? {
val results = multiResolve(false)
if (results.isEmpty()) return null
@ -166,20 +154,13 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase<PsiEle
return target
}
private fun resolveGlobally(project: Project, name: String, membersOnly: Boolean = false, allowedPackages: Set<String>? = null): List<ResolveResult> {
private fun resolveGlobally(project: Project, name: String, membersOnly: Boolean = false): List<ResolveResult> {
val results = mutableListOf<ResolveResult>()
val files = FilenameIndex.getAllFilesByExt(project, "lyng", GlobalSearchScope.projectScope(project))
val psiManager = PsiManager.getInstance(project)
for (vFile in files) {
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 src = mini.range.start.source

View File

@ -0,0 +1,466 @@
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

@ -0,0 +1,282 @@
# 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

@ -1048,12 +1048,7 @@ class Compiler(
}
// Nullable suffix after base or generic
val isNullable = if (cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)) {
true
} else if (cc.skipTokenOfType(Token.Type.IFNULLASSIGN, isOptional = true)) {
cc.pushPendingAssign()
true
} else false
val isNullable = cc.skipTokenOfType(Token.Type.QUESTION, isOptional = true)
val endPos = cc.currentPos()
val miniRef = buildBaseRef(if (miniArgs != null) endPos else lastEnd, miniArgs, isNullable)

View File

@ -35,9 +35,8 @@ class CompilerContext(val tokens: List<Token>) {
var currentIndex = 0
private var pendingGT = 0
private var pendingAssign = false
fun hasNext() = currentIndex < tokens.size || pendingGT > 0 || pendingAssign
fun hasNext() = currentIndex < tokens.size || pendingGT > 0
fun hasPrevious() = currentIndex > 0
fun next(): Token {
if (pendingGT > 0) {
@ -45,11 +44,6 @@ class CompilerContext(val tokens: List<Token>) {
val last = tokens[currentIndex - 1]
return Token(">", last.pos.copy(column = last.pos.column + 1), Token.Type.GT)
}
if (pendingAssign) {
pendingAssign = false
val last = tokens[currentIndex - 1]
return Token("=", last.pos.copy(column = last.pos.column + 1), Token.Type.ASSIGN)
}
return if (currentIndex < tokens.size) tokens[currentIndex++]
else Token("", tokens.last().pos, Token.Type.EOF)
}
@ -58,19 +52,16 @@ class CompilerContext(val tokens: List<Token>) {
pendingGT++
}
fun pushPendingAssign() {
pendingAssign = true
}
fun previous() = if (pendingGT > 0 || pendingAssign) {
throw IllegalStateException("previous() not supported after pushPending tokens")
fun previous() = if (pendingGT > 0) {
pendingGT-- // This is wrong, previous should go back.
// But we don't really use previous() in generics parser after splitting.
throw IllegalStateException("previous() not supported after pushPendingGT")
} else if (!hasPrevious()) throw IllegalStateException("No previous token") else tokens[--currentIndex]
fun savePos() = (currentIndex shl 3) or (pendingGT and 3) or (if (pendingAssign) 4 else 0)
fun savePos() = (currentIndex shl 2) or (pendingGT and 3)
fun restorePos(pos: Int) {
currentIndex = pos shr 3
currentIndex = pos shr 2
pendingGT = pos and 3
pendingAssign = (pos and 4) != 0
}
fun ensureLabelIsValid(pos: Pos, label: String) {

View File

@ -100,16 +100,11 @@ object LyngFormatter {
fun isControlHeaderNoBrace(s: String): Boolean {
val t = s.trim()
if (t.isEmpty()) return false
// match: if (...) | else if (...) | else | catch (...) | finally
if (Regex("^if\\s*\\(.*\\)$").matches(t)) return true
if (Regex("^else\\s+if\\s*\\(.*\\)$").matches(t)) return true
if (t == "else") 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
// match: if (...) | else if (...) | else
val isIf = Regex("^if\\s*\\(.*\\)\\s*$").matches(t)
val isElseIf = Regex("^else\\s+if\\s*\\(.*\\)\\s*$").matches(t)
val isElse = t == "else"
if (isIf || isElseIf || isElse) return true
// property accessors ending with ) or =
if (isAccessorRelated(t)) {
@ -118,23 +113,11 @@ object LyngFormatter {
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()) {
val (parts, nextInBlockComment) = splitIntoParts(rawLine, inBlockComment)
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 trimmedLine = rawLine.trim()
val trimmedCodeAndStrings = codeAndStrings.trim()
// Compute effective indent level for this line
val currentExtraIndent = extraIndents.sum()
@ -155,15 +138,9 @@ object LyngFormatter {
// Single-line control header (if/else/else if) without braces: indent the next
// 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() &&
!trimmedStart.startsWith("}")
var actualAwaiting = 0
if (applyAwaiting) {
actualAwaiting = awaitingExtraIndent
if (isContinuation) actualAwaiting = (actualAwaiting - 1).coerceAtLeast(0)
effectiveLevel += actualAwaiting
}
!trimmedStart.startsWith("else") && !trimmedStart.startsWith("}")
if (applyAwaiting) effectiveLevel += awaitingExtraIndent
val firstChar = trimmedStart.firstOrNull()
// While inside parentheses, continuation applies scaled by nesting level
@ -217,7 +194,7 @@ object LyngFormatter {
val newBlockLevel = blockLevel
if (newBlockLevel > oldBlockLevel) {
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code))
val addedThisLine = actualAwaiting + (if (isAccessorRelatedLine) 1 else 0)
val addedThisLine = (if (applyAwaiting) awaitingExtraIndent else 0) + (if (isAccessorRelatedLine) 1 else 0)
repeat(newBlockLevel - oldBlockLevel) {
extraIndents.add(addedThisLine)
}
@ -230,29 +207,23 @@ object LyngFormatter {
inBlockComment = nextInBlockComment
// 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()) {
// we have just applied it.
val endsWithBrace = code.trimEnd().endsWith("{")
if (!endsWithBrace && isControlHeaderNoBrace(trimmedCodeAndStrings)) {
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
// It's another header, increment
val isAccessorOrDouble = isAccessor || (!inBlockComment && isAccessorRelated(code)) || isDoubleIndentHeader(trimmedCodeAndStrings)
val increment = if (isAccessorOrDouble) 2 else 1
awaitingExtraIndent = actualAwaiting + increment
} else if (!nextIsContinuation) {
// It's the body, and no continuation follows, so reset
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code))
awaitingExtraIndent += if (isAccessorRelatedLine) 2 else 1
} else {
// It's the body, reset
awaitingExtraIndent = 0
}
} else {
// start awaiting if current line is a control header without '{'
val endsWithBrace = code.trimEnd().endsWith("{")
if (!endsWithBrace && isControlHeaderNoBrace(trimmedCodeAndStrings)) {
val isAccessorOrDouble = isAccessor || (!inBlockComment && isAccessorRelated(code)) || isDoubleIndentHeader(trimmedCodeAndStrings)
awaitingExtraIndent = if (isAccessorOrDouble) 2 else 1
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code))
awaitingExtraIndent = if (isAccessorRelatedLine) 2 else 1
}
}

View File

@ -4803,7 +4803,7 @@ class ScriptTest {
fun f(a: String = "foo") = a + "!"
fun g(a: String? = null) = a ?: "!!"
assertEquals(f(), "foo!")
assertEquals(g(), "!!")
assertEquals(f(), "!!")
assertEquals(f("bar"), "bar!")
class T(b: Int=42,c: String?=null)
assertEquals(42, T().b)
@ -4811,18 +4811,5 @@ class ScriptTest {
""".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"))
""")
}
}