From 1f760aff61a5398025e08ec711b14d887771a06e Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 28 Apr 2026 17:19:07 -0700 Subject: [PATCH] fix(live): expandReplaceRange handles multi-line self-closing JSX
MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cursor Bugbot review on 8660d3a flagged a real corruption bug: > Multi-line self-closing div breaks depth tracking in expandReplaceRange. > The forward div-depth walk applies openRe / selfCloseRe / closeRe > per-line. A multi-line `` causes openRe > to match the opener line but selfCloseRe fails on both lines because > `/]*\/\s*>/` requires the full tag on one line. Depth is > permanently over-counted by 1, so the walk overshoots. Trace on the JSX-marker-inside-wrapper layout: - Inside the wrapped element, a multi-line `
` increments depth at the `` is left orphaned in the file after accept/discard, breaking the JSX. Worse: an unrelated subsequent `
` sibling gets its `
` mis-counted as the wrapper close, and the depth walk corrupts further. Fix: rewrite the forward walk on JOINED text instead of per-line. A single regex `/]*?(\/?)>|<\/div\s*>/g` spans newlines (because `[^>]` matches `\n`), so it correctly identifies multi-line opens, closes, AND self-closes. Convert the match offset back to a file line index to set `end`. Walk-back logic for the wrapper opener is unchanged. Test coverage: - New `expandReplaceRange handles multi-line self-closing
` test in live-accept.test.mjs constructs the exact Bugbot scenario: a multi-line `` inside the picked element AND an unrelated `
After
` sibling right after. Asserts the discard removes ALL impeccable markers / wrapper attrs, preserves the next-card sibling intact, and the multi-line `
` survives inside the restored content. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .pi/skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- .../skills/impeccable/scripts/live-accept.mjs | 38 ++++++++----- tests/live-accept.test.mjs | 54 +++++++++++++++++++ 14 files changed, 379 insertions(+), 169 deletions(-) diff --git a/.agents/skills/impeccable/scripts/live-accept.mjs b/.agents/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/.agents/skills/impeccable/scripts/live-accept.mjs +++ b/.agents/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/.claude/skills/impeccable/scripts/live-accept.mjs b/.claude/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/.claude/skills/impeccable/scripts/live-accept.mjs +++ b/.claude/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/.cursor/skills/impeccable/scripts/live-accept.mjs b/.cursor/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/.cursor/skills/impeccable/scripts/live-accept.mjs +++ b/.cursor/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/.gemini/skills/impeccable/scripts/live-accept.mjs b/.gemini/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/.gemini/skills/impeccable/scripts/live-accept.mjs +++ b/.gemini/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/.github/skills/impeccable/scripts/live-accept.mjs b/.github/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/.github/skills/impeccable/scripts/live-accept.mjs +++ b/.github/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/.kiro/skills/impeccable/scripts/live-accept.mjs b/.kiro/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/.kiro/skills/impeccable/scripts/live-accept.mjs +++ b/.kiro/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/.opencode/skills/impeccable/scripts/live-accept.mjs b/.opencode/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/.opencode/skills/impeccable/scripts/live-accept.mjs +++ b/.opencode/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/.pi/skills/impeccable/scripts/live-accept.mjs b/.pi/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/.pi/skills/impeccable/scripts/live-accept.mjs +++ b/.pi/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/.rovodev/skills/impeccable/scripts/live-accept.mjs b/.rovodev/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/.rovodev/skills/impeccable/scripts/live-accept.mjs +++ b/.rovodev/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/.trae-cn/skills/impeccable/scripts/live-accept.mjs b/.trae-cn/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/.trae-cn/skills/impeccable/scripts/live-accept.mjs +++ b/.trae-cn/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/.trae/skills/impeccable/scripts/live-accept.mjs b/.trae/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/.trae/skills/impeccable/scripts/live-accept.mjs +++ b/.trae/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/plugin/skills/impeccable/scripts/live-accept.mjs b/plugin/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/plugin/skills/impeccable/scripts/live-accept.mjs +++ b/plugin/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/source/skills/impeccable/scripts/live-accept.mjs b/source/skills/impeccable/scripts/live-accept.mjs index b1e0b035c..f3cb1b484 100644 --- a/source/skills/impeccable/scripts/live-accept.mjs +++ b/source/skills/impeccable/scripts/live-accept.mjs @@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) { } // Walk forward to the matching `
` by div-depth tracking from the - // wrapper opener. Self-closing `
` doesn't contribute depth. - const openRe = /]*\/\s*>/g; - const closeRe = /<\/div\s*>/g; + // wrapper opener. Operate on JOINED text instead of per-line: a + // multi-line self-closing JSX `` would + // fool per-line regex tracking (the `` line never matches selfCloseRe since it needs `` orphaned after accept/discard. Single regex with + // `[^>]*?` (which spans newlines in JS) handles either form correctly. + const joined = lines.slice(start).join('\n'); + // Match either `
` (self-close, group 1 is `/`), `
` + // (open, group 1 is empty), or `
`. + const tagRe = /]*?(\/?)>|<\/div\s*>/g; let depth = 0; - for (let i = start; i < lines.length; i++) { - const line = lines[i]; - const opens = (line.match(openRe) || []).length; - const selfCloses = (line.match(selfCloseRe) || []).length; - const closes = (line.match(closeRe) || []).length; - depth += opens - selfCloses - closes; - if (depth <= 0 && i >= end) { - end = i; - break; + let m; + while ((m = tagRe.exec(joined)) !== null) { + const isClose = m[0].startsWith('= end) { + end = candidateEnd; + break; + } } } diff --git a/tests/live-accept.test.mjs b/tests/live-accept.test.mjs index 59dabc670..ea1f1d7ca 100644 --- a/tests/live-accept.test.mjs +++ b/tests/live-accept.test.mjs @@ -248,6 +248,60 @@ describe('live-accept — style-element edge cases', () => { ` closer must be back at 6-space indent. Got:\n${after}`); }); + it('expandReplaceRange handles multi-line self-closing
inside the wrapped element', () => { + // Cursor Bugbot regression: per-line depth tracking in + // `expandReplaceRange` couldn't see across line boundaries, so a + // multi-line self-closing JSX `` got + // counted as +1 with no compensating -1. The wrapper's outer
+ // never matched the depth-zero condition; replace-range stopped at + // block.end (the marker comment), leaving the wrapper's outer
+ // orphaned in the file after accept/discard — and worse, an + // unrelated
right after the wrapper got + // its own
mis-counted as the wrapper close. + const tsx = `export default function App() { + return ( +
+ +
After
+
+ ); +}`; + writeFileSync(join(tmp, 'App.tsx'), tsx); + + execSync( + `node source/skills/impeccable/scripts/live-wrap.mjs --id MULTILINESC --count 3 --classes "card" --tag "aside" --file "${join(tmp, 'App.tsx')}"`, + { cwd: process.cwd(), encoding: 'utf-8' } + ); + + const result = runAccept(tmp, ['--id', 'MULTILINESC', '--discard']); + assert.equal(result.handled, true, `discard should succeed: ${JSON.stringify(result)}`); + + const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8'); + // The wrapper scaffold must be fully gone — no orphan
from + // the outer wrapper, and no impeccable markers/data attributes. + assert.ok(!after.includes('data-impeccable-variants'), + `outer wrapper div must be fully removed; got:\n${after}`); + assert.ok(!after.includes('data-impeccable-variant'), + `original-div wrapper must be fully removed; got:\n${after}`); + assert.ok(!after.includes('impeccable-variants-start'), + `start marker must be removed; got:\n${after}`); + // The unrelated
After
sibling + // must survive intact — Bugbot's worst-case scenario was the depth + // walk eating its
as the wrapper close. + assert.ok(after.includes('
After
'), + `unrelated next-card sibling must be preserved; got:\n${after}`); + // The multi-line self-closing div inside the original element must + // survive too. + assert.match(after, //m, + `multi-line self-closing
inside original must survive; got:\n${after}`); + }); + it('accept (no carbonize, raw HTML) restores at the original indent on JSX', () => { // Manually craft a wrapped file in the JSX-marker-inside layout — this // mirrors what wrap produces, but lets us exercise accept's indent