Improve property accessor indentation handling, add tests, and enhance nested control flow formatting. Fix enum and property keyword highlighting.
This commit is contained in:
parent
eca451b5a3
commit
abb262d9cf
@ -98,6 +98,8 @@ All notable changes to this project will be documented in this file.
|
||||
- Mutually exclusive: `--check` and `--in-place` together now produce an error and exit with code 1.
|
||||
- Multi-file stdout prints headers `--- <path> ---` per file.
|
||||
- `lyng --help` shows `fmt`; `lyng fmt --help` displays dedicated help.
|
||||
- Fix: Property accessors (`get`, `set`, `private set`, `protected set`) are now correctly indented relative to the property declaration.
|
||||
- Fix: Indentation now correctly carries over into blocks that start on extra‑indented lines (e.g., nested `if` statements or property accessor bodies).
|
||||
|
||||
- CLI: Preserved legacy script invocation fast-paths:
|
||||
- `lyng script.lyng [args...]` executes the script directly.
|
||||
|
||||
@ -113,6 +113,15 @@ object Binder {
|
||||
symbols += fieldSym
|
||||
classes.last().fields += fieldSym.id
|
||||
}
|
||||
// Class fields (val/var in class body, if any are reported here)
|
||||
for (cf in d.classFields) {
|
||||
val fs = source.offsetOf(cf.nameStart)
|
||||
val fe = fs + cf.name.length
|
||||
val kind = if (cf.mutable) SymbolKind.Variable else SymbolKind.Value
|
||||
val fieldSym = Symbol(nextId++, cf.name, kind, fs, fe, containerId = sym.id)
|
||||
symbols += fieldSym
|
||||
classes.last().fields += fieldSym.id
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: functions and top-level/class vals/vars
|
||||
|
||||
@ -24,6 +24,24 @@ package net.sergeych.lyng.format
|
||||
*/
|
||||
object LyngFormatter {
|
||||
|
||||
private fun startsWithWord(s: String, w: String): Boolean =
|
||||
s.startsWith(w) && s.getOrNull(w.length)?.let { !it.isLetterOrDigit() && it != '_' } != false
|
||||
|
||||
private fun isPropertyAccessor(s: String): Boolean {
|
||||
val t = s.trim()
|
||||
if (t.isEmpty()) return false
|
||||
if (startsWithWord(t, "get") || startsWithWord(t, "set")) return true
|
||||
if (startsWithWord(t, "private")) {
|
||||
val rest = t.substring("private".length).trimStart()
|
||||
if (startsWithWord(rest, "set")) return true
|
||||
}
|
||||
if (startsWithWord(t, "protected")) {
|
||||
val rest = t.substring("protected".length).trimStart()
|
||||
if (startsWithWord(rest, "set")) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Returns the input with indentation recomputed from scratch, line by line. */
|
||||
fun reindent(text: String, config: LyngFormatConfig = LyngFormatConfig()): String {
|
||||
// Normalize tabs to spaces globally before any transformation; results must contain no tabs
|
||||
@ -35,6 +53,7 @@ object LyngFormatter {
|
||||
var bracketBalance = 0
|
||||
var prevBracketContinuation = false
|
||||
var inBlockComment = false
|
||||
val extraIndents = mutableListOf<Int>()
|
||||
// We don't keep per-"[" base alignment; continuation rules define alignment.
|
||||
|
||||
fun updateBalances(ch: Char) {
|
||||
@ -52,7 +71,7 @@ object LyngFormatter {
|
||||
// Always produce spaces; tabs are not allowed in resulting code
|
||||
" ".repeat(level * config.indentSize + continuation)
|
||||
|
||||
var awaitingSingleIndent = false
|
||||
var awaitingExtraIndent = 0
|
||||
fun isControlHeaderNoBrace(s: String): Boolean {
|
||||
val t = s.trim()
|
||||
if (t.isEmpty()) return false
|
||||
@ -60,7 +79,13 @@ object LyngFormatter {
|
||||
val isIf = Regex("^if\\s*\\(.*\\)\\s*$").matches(t)
|
||||
val isElseIf = Regex("^else\\s+if\\s*\\(.*\\)\\s*$").matches(t)
|
||||
val isElse = t == "else"
|
||||
return isIf || isElseIf || isElse
|
||||
if (isIf || isElseIf || isElse) return true
|
||||
|
||||
// property accessors ending with ) or =
|
||||
if (isPropertyAccessor(t)) {
|
||||
return t.endsWith(")") || t.endsWith("=")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for ((i, rawLine) in lines.withIndex()) {
|
||||
@ -70,7 +95,12 @@ object LyngFormatter {
|
||||
val trimmedLine = rawLine.trim()
|
||||
|
||||
// Compute effective indent level for this line
|
||||
var effectiveLevel = blockLevel
|
||||
val currentExtraIndent = extraIndents.sum()
|
||||
var effectiveLevel = blockLevel + currentExtraIndent
|
||||
|
||||
val isAccessor = !inBlockComment && isPropertyAccessor(trimmedStart)
|
||||
if (isAccessor) effectiveLevel += 1
|
||||
|
||||
if (inBlockComment) {
|
||||
if (!trimmedLine.startsWith("*/")) {
|
||||
effectiveLevel += 1
|
||||
@ -83,9 +113,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 applyAwaiting = awaitingSingleIndent && trimmedStart.isNotEmpty() &&
|
||||
val applyAwaiting = awaitingExtraIndent > 0 && trimmedStart.isNotEmpty() &&
|
||||
!trimmedStart.startsWith("else") && !trimmedStart.startsWith("}")
|
||||
if (applyAwaiting) effectiveLevel += 1
|
||||
if (applyAwaiting) effectiveLevel += awaitingExtraIndent
|
||||
|
||||
val firstChar = trimmedStart.firstOrNull()
|
||||
// While inside parentheses, continuation applies scaled by nesting level
|
||||
@ -129,23 +159,43 @@ object LyngFormatter {
|
||||
// New line (keep EOF semantics similar to input)
|
||||
if (i < lines.lastIndex) sb.append('\n')
|
||||
|
||||
val oldBlockLevel = blockLevel
|
||||
// Update balances using this line's code content
|
||||
for (part in parts) {
|
||||
if (part.type == PartType.Code) {
|
||||
for (ch in part.text) updateBalances(ch)
|
||||
}
|
||||
}
|
||||
val newBlockLevel = blockLevel
|
||||
if (newBlockLevel > oldBlockLevel) {
|
||||
val addedThisLine = (if (applyAwaiting) awaitingExtraIndent else 0) + (if (isAccessor) 1 else 0)
|
||||
repeat(newBlockLevel - oldBlockLevel) {
|
||||
extraIndents.add(addedThisLine)
|
||||
}
|
||||
} else if (newBlockLevel < oldBlockLevel) {
|
||||
repeat(oldBlockLevel - newBlockLevel) {
|
||||
if (extraIndents.isNotEmpty()) extraIndents.removeAt(extraIndents.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
inBlockComment = nextInBlockComment
|
||||
|
||||
// Update awaitingSingleIndent based on current line
|
||||
// Update awaitingExtraIndent based on current line
|
||||
if (applyAwaiting && trimmedStart.isNotEmpty()) {
|
||||
// we have just consumed the body line
|
||||
awaitingSingleIndent = false
|
||||
// we have just applied it.
|
||||
val endsWithBrace = code.trimEnd().endsWith("{")
|
||||
if (!endsWithBrace && isControlHeaderNoBrace(code)) {
|
||||
// It's another header, increment
|
||||
awaitingExtraIndent += 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(code)) {
|
||||
awaitingSingleIndent = true
|
||||
awaitingExtraIndent = 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,9 +246,6 @@ object LyngFormatter {
|
||||
return applyControlledWrapping(spacedText, config)
|
||||
}
|
||||
|
||||
private fun startsWithWord(s: String, w: String): Boolean =
|
||||
s.startsWith(w) && s.getOrNull(w.length)?.let { !it.isLetterOrDigit() && it != '_' } != false
|
||||
|
||||
/**
|
||||
* Reindents a slice of [text] specified by [range] and returns a new string with that slice replaced.
|
||||
* By default, preserves the base indent of the first line in the slice (so the block stays at
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -42,10 +42,10 @@ private val fallbackKeywordIds = setOf(
|
||||
"and", "or", "not",
|
||||
// declarations & modifiers
|
||||
"fun", "fn", "class", "enum", "val", "var", "import", "package",
|
||||
"private", "protected", "static", "open", "extern",
|
||||
"private", "protected", "static", "open", "extern", "init", "get", "set", "by",
|
||||
// control flow and misc
|
||||
"if", "else", "when", "while", "do", "for", "try", "catch", "finally",
|
||||
"throw", "return", "break", "continue", "this", "null", "true", "false"
|
||||
"throw", "return", "break", "continue", "this", "null", "true", "false", "unset"
|
||||
)
|
||||
|
||||
/** Maps lexer token type (and sometimes value) to a [HighlightKind]. */
|
||||
|
||||
@ -730,4 +730,76 @@ class LyngFormatterTest {
|
||||
|
||||
assertEquals(expected, formatted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun privateSet_indentation() {
|
||||
val src = """
|
||||
class A {
|
||||
var x = 1
|
||||
private set
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
// What we expect: private set should be indented relative to the property
|
||||
val expected = """
|
||||
class A {
|
||||
var x = 1
|
||||
private set
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
|
||||
val out = LyngFormatter.reindent(src, cfg)
|
||||
assertEquals(expected, out)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun propertyAccessors_indentation() {
|
||||
val src = """
|
||||
class A {
|
||||
var x
|
||||
get() = 1
|
||||
private set(v) {
|
||||
_x = v
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val expected = """
|
||||
class A {
|
||||
var x
|
||||
get() = 1
|
||||
private set(v) {
|
||||
_x = v
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
|
||||
val out = LyngFormatter.reindent(src, cfg)
|
||||
assertEquals(expected, out)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nestedAwaitingIndentation() {
|
||||
val src = """
|
||||
if (cond)
|
||||
if (other)
|
||||
fun f() {
|
||||
stmt
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val expected = """
|
||||
if (cond)
|
||||
if (other)
|
||||
fun f() {
|
||||
stmt
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val cfg = LyngFormatConfig(indentSize = 4, continuationIndentSize = 4)
|
||||
val out = LyngFormatter.reindent(src, cfg)
|
||||
assertEquals(expected, out)
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,10 +28,7 @@ import net.sergeych.lyng.binding.SymbolKind
|
||||
import net.sergeych.lyng.highlight.HighlightKind
|
||||
import net.sergeych.lyng.highlight.SimpleLyngHighlighter
|
||||
import net.sergeych.lyng.highlight.offsetOf
|
||||
import net.sergeych.lyng.miniast.MiniAstBuilder
|
||||
import net.sergeych.lyng.miniast.MiniClassDecl
|
||||
import net.sergeych.lyng.miniast.MiniFunDecl
|
||||
import net.sergeych.lyng.miniast.MiniTypeName
|
||||
import net.sergeych.lyng.miniast.*
|
||||
import org.w3c.dom.HTMLStyleElement
|
||||
|
||||
fun ensureBootstrapCodeBlocks(html: String): String {
|
||||
@ -367,6 +364,7 @@ suspend fun applyLyngHighlightToTextAst(text: String): String {
|
||||
is MiniFunDecl -> putName(d.nameStart, d.name, "hl-fn")
|
||||
is MiniClassDecl -> putName(d.nameStart, d.name, "hl-class")
|
||||
is net.sergeych.lyng.miniast.MiniValDecl -> putName(d.nameStart, d.name, if (d.mutable) "hl-var" else "hl-val")
|
||||
is MiniEnumDecl -> putName(d.nameStart, d.name, "hl-class")
|
||||
}
|
||||
}
|
||||
// Imports: color each segment as directive/path
|
||||
@ -404,6 +402,7 @@ suspend fun applyLyngHighlightToTextAst(text: String): String {
|
||||
}
|
||||
is net.sergeych.lyng.miniast.MiniValDecl -> addTypeSegments(d.type)
|
||||
is MiniClassDecl -> {}
|
||||
is MiniEnumDecl -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -534,7 +533,8 @@ private fun detectDeclarationAndParamOverrides(text: String): Map<Pair<Int, Int>
|
||||
"package", "import", "fun", "fn", "class", "enum", "val", "var",
|
||||
"if", "else", "while", "do", "for", "when", "try", "catch", "finally",
|
||||
"throw", "return", "break", "continue", "in", "is", "as", "as?", "not",
|
||||
"true", "false", "null", "private", "protected", "open", "extern", "static"
|
||||
"true", "false", "null", "private", "protected", "open", "extern", "static",
|
||||
"init", "get", "set", "Unset", "by"
|
||||
)
|
||||
fun skipWs(idx0: Int): Int {
|
||||
var idx = idx0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user