mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 08:36:25 +03:00
fix(live): expandReplaceRange handles multi-line self-closing JSX <div />
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 `<div\n className="spacer"\n/>` causes openRe
> to match the opener line but selfCloseRe fails on both lines because
> `/<div\b[^>]*\/\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 `<div … />` increments depth
at the `<div` line and never decrements.
- Forward walk's depth never returns to 0 → end stays at block.end (the
inner marker comment) → replace range stops there.
- Wrapper's outer `</div>` is left orphaned in the file after
accept/discard, breaking the JSX. Worse: an unrelated subsequent
`<div className="next-card">…</div>` sibling gets its `</div>`
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\b[^>]*?(\/?)>|<\/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 <div />` test
in live-accept.test.mjs constructs the exact Bugbot scenario: a
multi-line `<div\n className="spacer"\n/>` inside the picked
element AND an unrelated `<div className="next-card">After</div>`
sibling right after. Asserts the discard removes ALL impeccable
markers / wrapper attrs, preserves the next-card sibling intact, and
the multi-line `<div />` survives inside the restored content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
8660d3aa22
commit
1f760aff61
@@ -263,20 +263,32 @@ function expandReplaceRange(block, lines, isJsx) {
|
||||
}
|
||||
|
||||
// Walk forward to the matching `</div>` by div-depth tracking from the
|
||||
// wrapper opener. Self-closing `<div … />` doesn't contribute depth.
|
||||
const openRe = /<div\b/g;
|
||||
const selfCloseRe = /<div\b[^>]*\/\s*>/g;
|
||||
const closeRe = /<\/div\s*>/g;
|
||||
// wrapper opener. Operate on JOINED text instead of per-line: a
|
||||
// multi-line self-closing JSX `<div\n className="spacer"\n/>` would
|
||||
// fool per-line regex tracking (the `<div` line matches openRe but the
|
||||
// `/>` line never matches selfCloseRe since it needs `<div` on the same
|
||||
// line). That left depth permanently over-counted and the wrapper's
|
||||
// outer `</div>` 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 `<div … />` (self-close, group 1 is `/`), `<div … >`
|
||||
// (open, group 1 is empty), or `</div>`.
|
||||
const tagRe = /<div\b[^>]*?(\/?)>|<\/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('</');
|
||||
const isSelfClose = !isClose && m[1] === '/';
|
||||
if (isClose) depth--;
|
||||
else if (!isSelfClose) depth++;
|
||||
if (depth <= 0) {
|
||||
// m.index is offset within `joined`; convert back to a file line.
|
||||
const linesBefore = joined.slice(0, m.index + m[0].length).split('\n').length - 1;
|
||||
const candidateEnd = start + linesBefore;
|
||||
if (candidateEnd >= end) {
|
||||
end = candidateEnd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user