From 8a9b000f6692ab2ecffe96401861289230505dea Mon Sep 17 00:00:00 2001 From: Peter Thomson Date: Sat, 17 Jan 2026 15:20:07 +1300 Subject: [PATCH] fix(light-touch): preserve original blank line counts during merge The mergeWithOriginal function was using blank lines from regenerated markdown instead of preserving original blank lines. This caused intentional double blank lines to collapse to single blank lines whenever any edit was made elsewhere in the file. Changes: - When NO new content in a gap: preserve original gap lines exactly (fixes the double-blank collapse issue) - When new content IS inserted: use Muya's spacing since the document structure has changed (preserves proper spacing around new content) - Apply same logic to tail handling after last matched line This fixes unwanted diffs for files with intentional multiple blank lines used for visual separation, while still allowing Muya to determine appropriate spacing when new content is added. --- docs/todo/save-light-touch.md | 2 +- src/renderer/src/store/editor.js | 58 ++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/docs/todo/save-light-touch.md b/docs/todo/save-light-touch.md index 6acd8f05..16090de2 100644 --- a/docs/todo/save-light-touch.md +++ b/docs/todo/save-light-touch.md @@ -9,7 +9,7 @@ Working notes for Light Touch behavior in `src/renderer/src/store/editor.js`. - Baseline advances to the last saved payload when tracked via `pendingSavedMarkdown` (manual/auto saves to existing paths). ## Known tradeoffs (accepted for now) -- Aggressive normalization: intentional multiple blank lines can be collapsed when nearby edits occur. +- ~~Aggressive normalization: intentional multiple blank lines can be collapsed when nearby edits occur.~~ (FIXED: merge now preserves original blank line counts) - Line-oriented LCS can misalign within fenced code, tables, or reordered lists because block types are ignored. ## Test coverage to add diff --git a/src/renderer/src/store/editor.js b/src/renderer/src/store/editor.js index eb24c004..2a4c5713 100644 --- a/src/renderer/src/store/editor.js +++ b/src/renderer/src/store/editor.js @@ -1602,18 +1602,28 @@ const mergeWithOriginal = (regenerated, original) => { let prevRegen = -1 for (const { orig: oi, regen: rj } of matches) { - // Anything inserted between previous regen match and this regen match - const inserted = regenLines.slice(prevRegen + 1, rj) - if (inserted.length) { - const onlyBlank = inserted.every((l) => normalizeLine(l) === '') - const origGap = oi - prevOrig - 1 // lines between previous orig match and this one - // If original lines were adjacent (no gap), drop purely blank insertions - if (!(onlyBlank && origGap === 0)) { - // If no orig gap, strip blank lines that piggyback on edits - const toInsert = origGap === 0 ? inserted.filter((l) => normalizeLine(l) !== '') : inserted - if (toInsert.length) { - resultLines.push(...toInsert) - } + // Lines from original in the gap (blank lines we want to preserve) + const origGapLines = origLines.slice(prevOrig + 1, oi) + // Lines from regenerated in the gap (may include new content) + const regenGapLines = regenLines.slice(prevRegen + 1, rj) + + if (origGapLines.length > 0) { + // Original had lines in this gap + const newContent = regenGapLines.filter((l) => normalizeLine(l) !== '') + + if (newContent.length > 0) { + // New content is being inserted - use Muya's spacing since structure changed + resultLines.push(...regenGapLines) + } else { + // No new content - preserve original gap lines exactly (keeps double blanks) + resultLines.push(...origGapLines) + } + } else if (regenGapLines.length > 0) { + // Original had no gap but regen does - this is purely new content + // Strip blank-only insertions to avoid adding unwanted spacing + const onlyBlank = regenGapLines.every((l) => normalizeLine(l) === '') + if (!onlyBlank) { + resultLines.push(...regenGapLines.filter((l) => normalizeLine(l) !== '')) } } @@ -1623,12 +1633,26 @@ const mergeWithOriginal = (regenerated, original) => { prevRegen = rj } - // Handle tail insertions after the last match - if (prevRegen < regenLines.length - 1) { - const tail = regenLines.slice(prevRegen + 1) - const onlyBlank = tail.every((l) => normalizeLine(l) === '') + // Handle tail after the last match + const origTail = origLines.slice(prevOrig + 1) + const regenTail = regenLines.slice(prevRegen + 1) + + if (origTail.length > 0) { + // Original had lines after last match + const newContent = regenTail.filter((l) => normalizeLine(l) !== '') + + if (newContent.length > 0) { + // New content at end - use Muya's spacing since structure changed + resultLines.push(...regenTail) + } else { + // No new content - preserve original tail exactly + resultLines.push(...origTail) + } + } else if (regenTail.length > 0) { + // Only regen has tail lines - add non-blank content only + const onlyBlank = regenTail.every((l) => normalizeLine(l) === '') if (!onlyBlank) { - resultLines.push(...tail.filter((l) => normalizeLine(l) !== '')) + resultLines.push(...regenTail.filter((l) => normalizeLine(l) !== '')) } // If only blank, rely on original trailing newlines preservation below }