From aad7c9619b88c5b3ece9498194f374cf7f9e0e43 Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 4 Jan 2026 04:12:15 +0100 Subject: [PATCH] fixed bug with reformatting code sequences in markdown + lyng --- .gitignore | 1 + CHANGELOG.md | 1 + .../idea/format/LyngFormattingModelBuilder.kt | 6 +- .../idea/format/LyngPreFormatProcessor.kt | 90 ++++++++++++++----- site/src/jsMain/resources/index.html | 4 +- site/src/jsTest/kotlin/HighlightSmokeTest.kt | 4 +- 6 files changed, 75 insertions(+), 31 deletions(-) diff --git a/.gitignore b/.gitignore index 0a49f4b..4cb1d2c 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ xcuserdata .output*.txt debug.log /build.log +/test.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f4067..ab6fdca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngFormattingModelBuilder.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngFormattingModelBuilder.kt index ecdad57..7d14ef0 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngFormattingModelBuilder.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngFormattingModelBuilder.kt @@ -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 = 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 diff --git a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngPreFormatProcessor.kt b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngPreFormatProcessor.kt index 81f0121..52ebccd 100644 --- a/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngPreFormatProcessor.kt +++ b/lyng-idea/src/main/kotlin/net/sergeych/lyng/idea/format/LyngPreFormatProcessor.kt @@ -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) } } diff --git a/site/src/jsMain/resources/index.html b/site/src/jsMain/resources/index.html index a4879f2..312f952 100644 --- a/site/src/jsMain/resources/index.html +++ b/site/src/jsMain/resources/index.html @@ -1,5 +1,5 @@
- v1.1.0-beta2 + v1.1.0-rc
diff --git a/site/src/jsTest/kotlin/HighlightSmokeTest.kt b/site/src/jsTest/kotlin/HighlightSmokeTest.kt index 01eecdd..62b616c 100644 --- a/site/src/jsTest/kotlin/HighlightSmokeTest.kt +++ b/site/src/jsTest/kotlin/HighlightSmokeTest.kt @@ -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, "...") // String key and identifier key appear - assertContains(html, "\"a\"") + assertContains(html, ""a"") assertContains(html, "b") }