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 .output*.txt
debug.log debug.log
/build.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. - `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: 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: 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: - CLI: Preserved legacy script invocation fast-paths:
- `lyng script.lyng [args...]` executes the script directly. - `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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 file: PsiFile,
private val settings: CodeStyleSettings private val settings: CodeStyleSettings
) : Block { ) : Block {
override fun getTextRange(): TextRange = file.textRange override fun getTextRange(): TextRange = TextRange(0, file.textLength)
override fun getSubBlocks(): List<Block> = emptyList() override fun getSubBlocks(): List<Block> = emptyList()
@ -52,7 +52,7 @@ private class LineBlocksRootBlock(
override fun getSpacing(child1: Block?, child2: Block): Spacing? = null override fun getSpacing(child1: Block?, child2: Block): Spacing? = null
override fun getChildAttributes(newChildIndex: Int): ChildAttributes = ChildAttributes(Indent.getNoneIndent(), null) override fun getChildAttributes(newChildIndex: Int): ChildAttributes = ChildAttributes(Indent.getNoneIndent(), null)
override fun isIncomplete(): Boolean = false 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 // 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 // When both spacing and wrapping are OFF, still fix indentation for the whole file to
// guarantee visible changes on Reformat Code. // guarantee visible changes on Reformat Code.
val runFullFileIndent = !settings.enableSpacing && !settings.enableWrapping val runFullFileIndent = !settings.enableSpacing && !settings.enableWrapping
// Maintain a working range and a modification flag to avoid stale offsets after replacements
var modified = false 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 docW = doc as? com.intellij.injected.editor.DocumentWindow
val endLine = if (runFullFileIndent) (doc.lineCount - 1).coerceAtLeast(0) // The host range of the entire injected fragment (or the whole file if not injected).
else doc.getLineNumber(workingRange.endOffset.coerceAtMost(doc.textLength)) 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 { fun codePart(s: String): String {
val idx = s.indexOf("//") val idx = s.indexOf("//")
return if (idx >= 0) s.substring(0, idx) else s 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 blockLevel = 0
var parenBalance = 0 var parenBalance = 0
var bracketBalance = 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))) val text = doc.getText(TextRange(doc.getLineStartOffset(ln), doc.getLineEndOffset(ln)))
for (ch in codePart(text)) when (ch) { for (ch in codePart(text)) when (ch) {
'{' -> blockLevel++ '{' -> blockLevel++
@ -85,7 +127,6 @@ class LyngPreFormatProcessor : PreFormatProcessor {
CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart) CodeStyleManager.getInstance(project).adjustLineIndent(file, lineStart)
} catch (e: Exception) { } catch (e: Exception) {
// Log as debug because this can be called many times during reformat // 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, useTabs = options.USE_TAB_CHARACTER,
continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)), continuationIndentSize = options.CONTINUATION_INDENT_SIZE.coerceAtLeast(options.INDENT_SIZE.coerceAtLeast(1)),
) )
val full = fullRange() val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
val r = if (runFullFileIndent) full else workingRange.intersection(full) ?: full
val text = doc.getText(r) val text = doc.getText(r)
val formatted = LyngFormatter.reindent(text, cfg) val formatted = LyngFormatter.reindent(text, cfg)
if (formatted != text) { if (formatted != text) {
doc.replaceString(r.startOffset, r.endOffset, formatted) doc.replaceString(r.startOffset, r.endOffset, formatted)
modified = true modified = true
psiDoc.commitDocument(doc) psiDoc.commitDocument(doc)
workingRange = fullRange() workingRangeLocal = currentLocalRange()
} }
} }
@ -131,14 +171,14 @@ class LyngPreFormatProcessor : PreFormatProcessor {
applySpacing = true, applySpacing = true,
applyWrapping = false, applyWrapping = false,
) )
val safe = workingRange.intersection(fullRange()) ?: fullRange() val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
val text = doc.getText(safe) val text = doc.getText(r)
val formatted = LyngFormatter.format(text, cfg) val formatted = LyngFormatter.format(text, cfg)
if (formatted != text) { if (formatted != text) {
doc.replaceString(safe.startOffset, safe.endOffset, formatted) doc.replaceString(r.startOffset, r.endOffset, formatted)
modified = true modified = true
psiDoc.commitDocument(doc) psiDoc.commitDocument(doc)
workingRange = fullRange() workingRangeLocal = currentLocalRange()
} }
} }
// Optionally apply wrapping (after spacing) when enabled // Optionally apply wrapping (after spacing) when enabled
@ -150,17 +190,19 @@ class LyngPreFormatProcessor : PreFormatProcessor {
applySpacing = settings.enableSpacing, applySpacing = settings.enableSpacing,
applyWrapping = true, applyWrapping = true,
) )
val safe2 = workingRange.intersection(fullRange()) ?: fullRange() val r = if (runFullFileIndent) currentLocalRange() else workingRangeLocal.intersection(currentLocalRange()) ?: currentLocalRange()
val text2 = doc.getText(safe2) val text = doc.getText(r)
val wrapped = LyngFormatter.format(text2, cfg) val wrapped = LyngFormatter.format(text, cfg)
if (wrapped != text2) { if (wrapped != text) {
doc.replaceString(safe2.startOffset, safe2.endOffset, wrapped) doc.replaceString(r.startOffset, r.endOffset, wrapped)
modified = true modified = true
psiDoc.commitDocument(doc) psiDoc.commitDocument(doc)
workingRange = fullRange() workingRangeLocal = currentLocalRange()
} }
} }
// Return a safe range for the formatter to continue with, preventing stale offsets // Return a safe range for the formatter to continue with, preventing stale offsets.
return if (modified) fullRange() else (range.intersection(fullRange()) ?: fullRange()) // 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"); ~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License. ~ you may not use this file except in compliance with the License.
@ -329,7 +329,7 @@
<!-- Top-left version ribbon --> <!-- Top-left version ribbon -->
<div class="corner-ribbon bg-danger text-white"> <div class="corner-ribbon bg-danger text-white">
<span style="margin-left: -5em"> <span style="margin-left: -5em">
v1.1.0-beta2 v1.1.0-rc
</span> </span>
</div> </div>
<!-- Fixed top navbar for the whole site --> <!-- 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -60,7 +60,7 @@ class HighlightSmokeTest {
// Spread operator present // Spread operator present
assertContains(html, "<span class=\"hl-op\">...</span>") assertContains(html, "<span class=\"hl-op\">...</span>")
// String key and identifier key appear // 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>") assertContains(html, "<span class=\"hl-id\">b</span>")
} }