fix(live): wrap preserves relative indent of multi-line picked elements

Companion to the prior outer-indent fix. live-wrap.mjs's
`originalLines.map(l => indent + '    ' + l.trimStart())` calls
`trimStart()` on every line, which strips ALL leading whitespace and
collapses multi-line picked elements to a uniform indent. So a 6/8/6
shape like

    <aside className="card">
      <h1 className="hero-title">Hero</h1>
    </aside>

was being reindented to 10/10/10 inside the wrapper, and on
accept/discard the round-trip restored 6/6/6 — the <h1> ended up at
its parent's depth instead of nested inside it.

Fix: extract `minLeadingSpaces(lines)` and strip only the COMMON
minimum across the picked lines before reindenting under the wrapper.
That mirrors how `deindentContent` on the accept side already works,
so wrap+accept now form a clean round-trip.

Test coverage:
- Expanded the indent regression test in live-accept.test.mjs to
  also assert the inner `<h1>` at 8-space indent and the closing
  `</aside>` at 6 — proving the relative depth survives wrap and
  discard end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-04-28 15:59:07 -07:00
co-authored by Claude Opus 4.7
parent 99e68370b9
commit a701ee613a
14 changed files with 387 additions and 43 deletions
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
+29 -3
View File
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
+29 -3
View File
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
+29 -3
View File
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
+29 -3
View File
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
+29 -3
View File
@@ -188,9 +188,19 @@ The agent should insert variant HTML at insertLine.`);
const isJsx = commentSyntax.open === '{/*';
const indent = lines[startLine].match(/^(\s*)/)[1];
// Extract the original element
// Extract the original element. Reindent under the wrapper while preserving
// the relative depth between lines — `l.trimStart()` would strip ALL leading
// whitespace and collapse e.g. `<aside>`/` <h1>`/`</aside>` (6/8/6 spaces)
// to a single uniform indent, so on accept/discard the round-trip restores
// the inner element at its parent's depth instead of nested inside it.
// Strip only the COMMON minimum leading whitespace across the picked lines;
// `deindentContent` on the accept side already mirrors this convention.
const originalLines = lines.slice(startLine, endLine + 1);
const originalIndented = originalLines.map(l => indent + ' ' + l.trimStart()).join('\n');
const originalBaseIndent = minLeadingSpaces(originalLines);
const reindentOriginal = (extra) => originalLines
.map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
.join('\n');
const originalIndented = reindentOriginal(' ');
// Wrapper attributes differ by syntax. HTML allows plain string attrs;
// JSX requires object-literal style and parses string attrs as HTML (which
@@ -214,7 +224,7 @@ The agent should insert variant HTML at insertLine.`);
indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' Original ' + commentSyntax.close,
indent + ' <div data-impeccable-variant="original">',
originalLines.map(l => indent + ' ' + l.trimStart()).join('\n'),
reindentOriginal(' '),
indent + ' </div>',
indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close,
indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close,
@@ -392,6 +402,22 @@ const OPENER_RE = /<([A-Za-z][A-Za-z0-9]*)(?=[\s/>]|$)/;
* line to find the actual tag opener. When `tag` is provided, opener candidates
* must match that tag name.
*/
/**
* Return the smallest leading-whitespace count across a set of lines,
* ignoring blank lines (whose indent isn't load-bearing). Used to compute
* the common base indent of a multi-line picked element so reindenting
* under the wrapper preserves the relative depth between lines.
*/
function minLeadingSpaces(lines) {
let min = Infinity;
for (const l of lines) {
if (l.trim() === '') continue;
const m = l.match(/^(\s*)/);
if (m && m[1].length < min) min = m[1].length;
}
return min === Infinity ? 0 : min;
}
function findElement(lines, query, tag = null) {
// Iterate all matches — the first substring hit isn't always the right one.
for (let i = 0; i < lines.length; i++) {
+10 -4
View File
@@ -235,11 +235,17 @@ describe('live-accept — style-element edge cases', () => {
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.)
// original — and the <h1> child at 8 (preserved relative depth).
// The earlier 6/6/6 collapse was caused by `originalLines.map(l =>
// indent + ' ' + l.trimStart())` in live-wrap stripping ALL
// leading whitespace before reindenting; the fix strips only the
// COMMON minimum so the relative structure is preserved.
assert.match(after, /^ <aside className="card">$/m,
`<aside> opener must be at 6-space indent (was 8 before fix), got:\n${after}`);
`<aside> opener must be at 6-space indent (was 8 before outer-indent fix), got:\n${after}`);
assert.match(after, /^ <h1 className="hero-title">Hero<\/h1>$/m,
`<h1> child must be at 8-space indent — relative depth preserved through wrap+discard. Got:\n${after}`);
assert.match(after, /^ <\/aside>$/m,
`</aside> closer must be back at 6-space indent. Got:\n${after}`);
});
it('accept (no carbonize, raw HTML) restores at the original indent on JSX', () => {