fixed bug with reformatting code sequences in markdown + lyng

This commit is contained in:
Sergey Chernov 2026-01-04 04:12:15 +01:00
parent 9e138367ef
commit aad7c9619b
6 changed files with 75 additions and 31 deletions

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ xcuserdata
.output*.txt
debug.log
/build.log
/test.md

View File

@ -100,6 +100,7 @@ All notable changes to this project will be documented in this 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).
- Fix: Formatting Markdown files no longer deletes content in `.lyng` code fences and works correctly with injected files (resolves clobbering, `StringIndexOutOfBoundsException`, and `nonempty text is not covered by block` errors).
- CLI: Preserved legacy script invocation fast-paths:
- `lyng script.lyng [args...]` executes the script directly.

View File

@ -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,7 +42,7 @@ private class LineBlocksRootBlock(
private val file: PsiFile,
private val settings: CodeStyleSettings
) : Block {
override fun getTextRange(): TextRange = file.textRange
override fun getTextRange(): TextRange = TextRange(0, file.textLength)
override fun getSubBlocks(): List<Block> = emptyList()
@ -52,7 +52,7 @@ private class LineBlocksRootBlock(
override fun getSpacing(child1: Block?, child2: Block): Spacing? = null
override fun getChildAttributes(newChildIndex: Int): ChildAttributes = ChildAttributes(Indent.getNoneIndent(), null)
override fun isIncomplete(): Boolean = false
override fun isLeaf(): Boolean = false
override fun isLeaf(): Boolean = true
}
// Intentionally no sub-blocks/spacing: indentation is handled by PreFormatProcessor + LineIndentProvider

View File

@ -44,25 +44,67 @@ class LyngPreFormatProcessor : PreFormatProcessor {
// When both spacing and wrapping are OFF, still fix indentation for the whole file to
// guarantee visible changes on Reformat Code.
val runFullFileIndent = !settings.enableSpacing && !settings.enableWrapping
// Maintain a working range and a modification flag to avoid stale offsets after replacements
var modified = false
fun fullRange(): TextRange = TextRange(0, doc.textLength)
var workingRange: TextRange = range.intersection(fullRange()) ?: fullRange()
val startLine = if (runFullFileIndent) 0 else doc.getLineNumber(workingRange.startOffset)
val endLine = if (runFullFileIndent) (doc.lineCount - 1).coerceAtLeast(0)
else doc.getLineNumber(workingRange.endOffset.coerceAtMost(doc.textLength))
val docW = doc as? com.intellij.injected.editor.DocumentWindow
// The host range of the entire injected fragment (or the whole file if not injected).
fun currentHostRange(): TextRange = if (docW != null) {
TextRange(docW.injectedToHost(0), docW.injectedToHost(doc.textLength))
} else {
file.textRange
}
// The range in 'doc' coordinate system (local 0..len for injections, host offsets for normal files).
fun currentLocalRange(): TextRange = if (docW != null) {
TextRange(0, doc.textLength)
} else {
file.textRange
}
val clr = currentLocalRange()
val chr = currentHostRange()
// Convert the input range to the coordinate system of 'doc'
var workingRangeLocal: TextRange = if (docW != null) {
val hostIntersection = range.intersection(chr)
if (hostIntersection != null) {
try {
val start = docW.hostToInjected(hostIntersection.startOffset)
val end = docW.hostToInjected(hostIntersection.endOffset)
TextRange(start.coerceAtMost(end), end.coerceAtLeast(start))
} catch (e: Exception) {
clr
}
} else {
range.intersection(clr) ?: clr
}
} else {
range.intersection(clr) ?: clr
}
val startLine = if (runFullFileIndent) {
doc.getLineNumber(currentLocalRange().startOffset)
} else {
doc.getLineNumber(workingRangeLocal.startOffset)
}
val endLine = if (runFullFileIndent) {
if (clr.endOffset <= clr.startOffset) doc.getLineNumber(clr.startOffset)
else doc.getLineNumber(clr.endOffset)
} else {
doc.getLineNumber(workingRangeLocal.endOffset.coerceAtMost(doc.textLength))
}
fun codePart(s: String): String {
val idx = s.indexOf("//")
return if (idx >= 0) s.substring(0, idx) else s
}
// Pre-scan to compute balances up to startLine
// Pre-scan to compute balances up to startLine.
val fragmentStartLine = doc.getLineNumber(currentLocalRange().startOffset)
var blockLevel = 0
var parenBalance = 0
var bracketBalance = 0
for (ln in 0 until startLine) {
for (ln in fragmentStartLine until startLine) {
val text = doc.getText(TextRange(doc.getLineStartOffset(ln), doc.getLineEndOffset(ln)))
for (ch in codePart(text)) when (ch) {
'{' -> blockLevel++
@ -85,7 +127,6 @@ class LyngPreFormatProcessor : PreFormatProcessor {
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
} catch (e: Exception) {
// Log as debug because this can be called many times during reformat
// and we don't want to spam warnings if it's a known platform issue with injections
}
}
@ -110,15 +151,14 @@ class LyngPreFormatProcessor : PreFormatProcessor {
useTabs = options.USE_TAB_CHARACTER,
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
)
val full = fullRange()
val r = if (runFullFileIndent) full else workingRange.intersection(full) ?: full
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
val text = doc.getText(r)
val formatted = LyngFormatter.reindent(text, cfg)
if (formatted != text) {
doc.replaceString(r.startOffset, r.endOffset, formatted)
modified = true
psiDoc.commitDocument(doc)
workingRange = fullRange()
workingRangeLocal = currentLocalRange()
}
}
@ -131,14 +171,14 @@ class LyngPreFormatProcessor : PreFormatProcessor {
applySpacing = true,
applyWrapping = false,
)
val safe = workingRange.intersection(fullRange()) ?: fullRange()
val text = doc.getText(safe)
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
val text = doc.getText(r)
val formatted = LyngFormatter.format(text, cfg)
if (formatted != text) {
doc.replaceString(safe.startOffset, safe.endOffset, formatted)
doc.replaceString(r.startOffset, r.endOffset, formatted)
modified = true
psiDoc.commitDocument(doc)
workingRange = fullRange()
workingRangeLocal = currentLocalRange()
}
}
// Optionally apply wrapping (after spacing) when enabled
@ -150,17 +190,19 @@ class LyngPreFormatProcessor : PreFormatProcessor {
applySpacing = settings.enableSpacing,
applyWrapping = true,
)
val safe2 = workingRange.intersection(fullRange()) ?: fullRange()
val text2 = doc.getText(safe2)
val wrapped = LyngFormatter.format(text2, cfg)
if (wrapped != text2) {
doc.replaceString(safe2.startOffset, safe2.endOffset, wrapped)
val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
val text = doc.getText(r)
val wrapped = LyngFormatter.format(text, cfg)
if (wrapped != text) {
doc.replaceString(r.startOffset, r.endOffset, wrapped)
modified = true
psiDoc.commitDocument(doc)
workingRange = fullRange()
workingRangeLocal = currentLocalRange()
}
}
// Return a safe range for the formatter to continue with, preventing stale offsets
return if (modified) fullRange() else (range.intersection(fullRange()) ?: fullRange())
// Return a safe range for the formatter to continue with, preventing stale offsets.
// For injected files, ALWAYS return a range in local coordinates.
val finalRange = currentLocalRange()
return if (modified) finalRange else (range.intersection(finalRange) ?: finalRange)
}
}

View File

@ -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.
@ -329,7 +329,7 @@
<!-- Top-left version ribbon -->
<div class="corner-ribbon bg-danger text-white">
<span style="margin-left: -5em">
v1.1.0-beta2
v1.1.0-rc
</span>
</div>
<!-- Fixed top navbar for the whole site -->

View File

@ -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.
@ -60,7 +60,7 @@ class HighlightSmokeTest {
// Spread operator present
assertContains(html, "<span class=\"hl-op\">...</span>")
// String key and identifier key appear
assertContains(html, "<span class=\"hl-str\">\"a\"</span>")
assertContains(html, "<span class=\"hl-str\">&quot;a&quot;</span>")
assertContains(html, "<span class=\"hl-id\">b</span>")
}