mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-22 02:56:52 +03:00
fix(live): JSX accept/discard restores at original indent (Bugbot review)
Cursor Bugbot caught this on PR #118 review: > JSX discard/accept restores content with wrong indentation. In the JSX > path, `indent` is captured from `lines[block.start]` — the marker comment > line inside the wrapper div, which is indented 2 extra spaces relative > to the original element. But `expandReplaceRange` expands the replacement > to include the outer `<div data-impeccable-variants>` wrapper, which sits > at the original element's indent level. `deindentContent(original, indent)` > restores content to the marker's deeper indent, so all restored lines end > up 2 spaces deeper than the original element was. I'd actually noticed the symptom during the live testing session ("some odd indentation in card-2 after discard") and dismissed it as cosmetic. Bugbot's analysis matches exactly. Fix: anchor the deindent base on `replaceRange.start` instead of `block.start`. For HTML the two are identical (markers sit outside the wrapper), so HTML is unchanged. For JSX `replaceRange.start` is the outer `<div>` at the original element's indent — correct base. Also dropped a duplicate `expandReplaceRange` call in handleAccept that the earlier edit left orphaned. Test coverage: - Two new regression tests in live-accept.test.mjs: - `discard restores JSX content at the original indent` runs the real wrap CLI and asserts the restored <aside> opener lands at its original 6-space indent (was 8 before the fix). - `accept (no carbonize, raw HTML) restores at the original indent on JSX` exercises the same anchor on the accept path. - Inner-element indent loss inside the wrapped content (`<h1>` ending up at the same indent as its parent `<aside>`) is a separate, pre-existing wrap behavior — left for a follow-up; explicitly noted in the test comments. 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
fdb9e7c6f8
commit
99e68370b9
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -105,11 +105,16 @@ function handleDiscard(id, lines, targetFile) {
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const original = extractOriginal(lines, block);
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const isJsx = detectCommentSyntax(targetFile).open === '{/*';
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
|
||||
// De-indent the original content back to the marker's indentation level
|
||||
// Restore at the line we're actually replacing FROM, not the marker line.
|
||||
// For JSX wrappers the marker comments live INSIDE the outer `<div>`, so
|
||||
// `block.start` sits 2 spaces deeper than the original element. Using that
|
||||
// as the deindent base would push the restored content 2 spaces too far
|
||||
// right on every JSX/TSX session. `replaceRange.start` is the outer wrapper
|
||||
// line, which is at the original element's indent for both HTML and JSX.
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
const restored = deindentContent(original, indent);
|
||||
|
||||
const newLines = [
|
||||
@@ -129,9 +134,14 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
const block = findMarkerBlock(id, lines);
|
||||
if (!block) return { handled: false, error: 'Markers not found' };
|
||||
|
||||
const indent = lines[block.start].match(/^(\s*)/)[1];
|
||||
const commentSyntax = detectCommentSyntax(targetFile);
|
||||
const isJsx = commentSyntax.open === '{/*';
|
||||
// Anchor indent on the line we're replacing FROM (the outer wrapper),
|
||||
// not on `block.start` — for JSX that's the marker comment 2 spaces
|
||||
// deeper than the original element. See handleDiscard for the full
|
||||
// rationale.
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const indent = lines[replaceRange.start].match(/^(\s*)/)[1];
|
||||
|
||||
// Extract the chosen variant's inner content
|
||||
const variantContent = extractVariant(lines, block, variantNum);
|
||||
@@ -187,7 +197,6 @@ function handleAccept(id, variantNum, lines, targetFile, paramValues) {
|
||||
replacement.push(...restored);
|
||||
}
|
||||
|
||||
const replaceRange = expandReplaceRange(block, lines, isJsx);
|
||||
const newLines = [
|
||||
...lines.slice(0, replaceRange.start),
|
||||
...replacement,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { execFileSync, execSync } from 'node:child_process';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ACCEPT = resolve(__dirname, '..', 'source/skills/impeccable/scripts/live-accept.mjs');
|
||||
@@ -206,6 +206,74 @@ describe('live-accept — style-element edge cases', () => {
|
||||
assert.ok(inner.includes('@scope ([data-impeccable-variant="1"])'), 'variant-1 scope kept');
|
||||
});
|
||||
|
||||
// Cursor Bugbot regression (PR #118 review): the JSX wrapper places
|
||||
// marker comments INSIDE the outer <div>, so block.start sits 2 spaces
|
||||
// deeper than the original element. Using block.start as the deindent
|
||||
// base on JSX accept/discard pushes every restored line 2 spaces too far
|
||||
// right. The fix anchors the indent on `replaceRange.start` (the outer
|
||||
// wrapper line), which is at the original element's indent level for
|
||||
// both HTML and JSX.
|
||||
it('discard restores JSX content at the original indent (no 2-space drift from marker-inside layout)', () => {
|
||||
// Run the real wrap CLI so we exercise the JSX-marker-inside-wrapper
|
||||
// layout end to end, not a hand-rolled approximation.
|
||||
const tsx = `export default function App() {
|
||||
return (
|
||||
<main>
|
||||
<aside className="card">
|
||||
<h1 className="hero-title">Hero</h1>
|
||||
</aside>
|
||||
</main>
|
||||
);
|
||||
}`;
|
||||
writeFileSync(join(tmp, 'App.tsx'), tsx);
|
||||
|
||||
execSync(
|
||||
`node source/skills/impeccable/scripts/live-wrap.mjs --id INDENTDISC --count 3 --classes "card" --tag "aside" --file "${join(tmp, 'App.tsx')}"`,
|
||||
{ cwd: process.cwd(), encoding: 'utf-8' }
|
||||
);
|
||||
|
||||
runAccept(tmp, ['--id', 'INDENTDISC', '--discard']);
|
||||
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
|
||||
// The aside opener should land at exactly 6 spaces — same as the
|
||||
// original. Any deeper indent is the bug Bugbot flagged. (Inner indent
|
||||
// loss inside the element is a separate, pre-existing wrap behavior;
|
||||
// not asserted here.)
|
||||
assert.match(after, /^ <aside className="card">$/m,
|
||||
`<aside> opener must be at 6-space indent (was 8 before fix), 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
|
||||
// logic without a full live cycle.
|
||||
const tsx = `export default function App() {
|
||||
return (
|
||||
<main>
|
||||
<div data-impeccable-variants="INDENTACC" data-impeccable-variant-count="3" style={{ display: "contents" }}>
|
||||
{/* impeccable-variants-start INDENTACC */}
|
||||
{/* Original */}
|
||||
<div data-impeccable-variant="original">
|
||||
<aside className="card">
|
||||
<h1 className="hero-title">Hero</h1>
|
||||
</aside>
|
||||
</div>
|
||||
{/* Variants: insert below this line */}
|
||||
<div data-impeccable-variant="1"><aside className="card variant-one"><h1 className="hero-title">Hero</h1></aside></div>
|
||||
{/* impeccable-variants-end INDENTACC */}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}`;
|
||||
writeFileSync(join(tmp, 'App.tsx'), tsx);
|
||||
|
||||
runAccept(tmp, ['--id', 'INDENTACC', '--variant', '1']);
|
||||
const after = readFileSync(join(tmp, 'App.tsx'), 'utf-8');
|
||||
// The accepted aside (variant-one) should land at 6-space indent, the
|
||||
// same place the wrapper <div> sat — not 2 spaces deeper.
|
||||
assert.match(after, /^ <aside className="card variant-one">/m,
|
||||
`accepted <aside> must land at 6-space indent (the wrapper's level), got:\n${after}`);
|
||||
});
|
||||
|
||||
// Discard must restore the original element after a self-closing <style />,
|
||||
// proving extractOriginal also survives the style pattern.
|
||||
it('discard restores the original element after a JSX self-closing <style />', () => {
|
||||
|
||||
Reference in New Issue
Block a user