diff --git a/CHANGELOG.md b/CHANGELOG.md index 3104e0f..1ecffaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. - 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. diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/EnglishDictionary.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/EnglishDictionary.kt deleted file mode 100644 index 0789ad4..0000000 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/EnglishDictionary.kt +++ /dev/null @@ -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 = 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(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 { - ensureLoaded() - return words - } -} diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt index 993a1cc..a9a14ef 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/LyngGrazieAnnotator.kt @@ -402,6 +402,8 @@ class LyngGrazieAnnotator : ExternalAnnotator { val lower = word.lowercase() val fromProject = collectProjectWords(file) - val fromTech = TechDictionary.allWords() - val fromEnglish = EnglishDictionary.allWords() - // Merge with priority: project (p=0), tech (p=1), english (p=2) + + 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 + } else null + } catch (_: Throwable) { + null + } ?: emptyList() + + // Merge with priority: project (p=0), spellchecker (p=1) val all = LinkedHashSet() - all.addAll(fromProject) - all.addAll(fromTech) - all.addAll(fromEnglish) - data class Cand(val w: String, val d: Int, val p: Int) - val cands = ArrayList(32) - for (w in all) { + // Add project words that are close enough + for (w in fromProject) { if (w == lower) continue - 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 + if (kotlin.math.abs(w.length - lower.length) <= 2 && editDistance(lower, w) <= 2) { + all.add(w) } - cands += Cand(w, d, p) } - cands.sortWith(compareBy { 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 } + all.addAll(fromSpellChecker) + + return all.take(16).toList() } private fun collectProjectWords(file: PsiFile): Set { @@ -589,14 +596,19 @@ class LyngGrazieAnnotator : ExternalAnnotator = emptySet()): 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" + "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" ) } diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/TechDictionary.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/TechDictionary.kt deleted file mode 100644 index b06cb60..0000000 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/grazie/TechDictionary.kt +++ /dev/null @@ -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 = 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(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 { - ensureLoaded() - return words - } -} diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngGotoDeclarationHandler.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngGotoDeclarationHandler.kt index 3121793..e4656f8 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngGotoDeclarationHandler.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngGotoDeclarationHandler.kt @@ -20,13 +20,14 @@ 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? { - if (sourceElement == null) return null + if (sourceElement == null || sourceElement.language != LyngLanguage) return null val allTargets = mutableListOf() diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt index 75aaf81..95ea04e 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/navigation/LyngPsiReference.kt @@ -38,16 +38,18 @@ class LyngPsiReference(element: PsiElement) : PsiPolyVariantReferenceBase { + private fun resolveGlobally(project: Project, name: String, membersOnly: Boolean = false, allowedPackages: Set? = null): List { val results = mutableListOf() 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 diff --git a/lyng-idea/src/main/resources/dictionaries/en-basic.txt b/lyng-idea/src/main/resources/dictionaries/en-basic.txt deleted file mode 100644 index cb80058..0000000 --- a/lyng-idea/src/main/resources/dictionaries/en-basic.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/lyng-idea/src/main/resources/dictionaries/tech-lyng.txt b/lyng-idea/src/main/resources/dictionaries/tech-lyng.txt deleted file mode 100644 index f55a4fc..0000000 --- a/lyng-idea/src/main/resources/dictionaries/tech-lyng.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt index b9427da..9023b26 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/format/LyngFormatter.kt @@ -100,11 +100,16 @@ object LyngFormatter { fun isControlHeaderNoBrace(s: String): Boolean { val t = s.trim() if (t.isEmpty()) return false - // 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 + // 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 // property accessors ending with ) or = if (isAccessorRelated(t)) { @@ -113,11 +118,23 @@ 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() @@ -138,9 +155,15 @@ 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("else") && !trimmedStart.startsWith("}") - if (applyAwaiting) effectiveLevel += awaitingExtraIndent + !trimmedStart.startsWith("}") + var actualAwaiting = 0 + if (applyAwaiting) { + actualAwaiting = awaitingExtraIndent + if (isContinuation) actualAwaiting = (actualAwaiting - 1).coerceAtLeast(0) + effectiveLevel += actualAwaiting + } val firstChar = trimmedStart.firstOrNull() // While inside parentheses, continuation applies scaled by nesting level @@ -194,7 +217,7 @@ object LyngFormatter { val newBlockLevel = blockLevel if (newBlockLevel > oldBlockLevel) { 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) { extraIndents.add(addedThisLine) } @@ -207,23 +230,29 @@ 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(code)) { + if (!endsWithBrace && isControlHeaderNoBrace(trimmedCodeAndStrings)) { // It's another header, increment - val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code)) - awaitingExtraIndent += if (isAccessorRelatedLine) 2 else 1 - } else { - // It's the body, reset + 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 awaitingExtraIndent = 0 } } else { // start awaiting if current line is a control header without '{' val endsWithBrace = code.trimEnd().endsWith("{") - if (!endsWithBrace && isControlHeaderNoBrace(code)) { - val isAccessorRelatedLine = isAccessor || (!inBlockComment && isAccessorRelated(code)) - awaitingExtraIndent = if (isAccessorRelatedLine) 2 else 1 + if (!endsWithBrace && isControlHeaderNoBrace(trimmedCodeAndStrings)) { + val isAccessorOrDouble = isAccessor || (!inBlockComment && isAccessorRelated(code)) || isDoubleIndentHeader(trimmedCodeAndStrings) + awaitingExtraIndent = if (isAccessorOrDouble) 2 else 1 } } diff --git a/lynglib/src/commonTest/kotlin/ScriptTest.kt b/lynglib/src/commonTest/kotlin/ScriptTest.kt index 34a71ca..08d8b88 100644 --- a/lynglib/src/commonTest/kotlin/ScriptTest.kt +++ b/lynglib/src/commonTest/kotlin/ScriptTest.kt @@ -4811,5 +4811,18 @@ 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")) + """) + } }