Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions packages/lang-core/src/parser/__tests__/serialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,41 @@ describe("jsonToOpenUI", () => {
const children = result.root!.props.children as ElementNode[];
expect(children[0].props.text).toBe("New");
});

// A multi-line ternary is part of the accepted grammar (parse() keeps it),
// so merging a patch that doesn't touch it must leave it intact (#821).
it("keeps a multi-line ternary in an untouched statement", () => {
const existing = [
"root = Stack([a, b])",
"a = $ok",
' ? Title("Yes")',
' : Title("No")',
'b = Title("Footer")',
].join("\n");

const merged = mergeStatements(existing, 'b = Title("Updated")');

expect(merged).toBe(
[
"root = Stack([a, b])",
"a = $ok",
' ? Title("Yes")',
' : Title("No")',
'b = Title("Updated")',
].join("\n"),
);
});

it("replaces a multi-line ternary when the patch targets it", () => {
const existing = ["root = Stack([a])", "a = $ok", ' ? Title("Yes")', ' : Title("No")'].join(
"\n",
);

const merged = mergeStatements(existing, 'a = Title("Changed")');

expect(merged).toBe(["root = Stack([a])", 'a = Title("Changed")'].join("\n"));
expect(merged).not.toContain('? Title("Yes")');
});
});

// ── Edge cases ─────────────────────────────────────────────────────────
Expand Down
12 changes: 12 additions & 0 deletions packages/lang-core/src/parser/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface ParsedStatement {
function splitStatementSource(input: string): string[] {
const stmts: string[] = [];
let depth = 0;
let ternaryDepth = 0;
let inStr: false | '"' | "'" = false;
let esc = false;
let start = 0;
Expand All @@ -44,7 +45,18 @@ function splitStatementSource(input: string): string[] {

if (c === "(" || c === "[" || c === "{") depth++;
else if (c === ")" || c === "]" || c === "}") depth = Math.max(0, depth - 1);
// Track ternary `?`/`:` at bracket depth 0 so a multi-line ternary
// (condition on one line, `?`/`:` continuation on the next) stays a single
// statement — mirrors split() in statements.ts. Without this the char-level
// splitter disagrees with the parser and drops the branches (#821).
else if (c === "?" && depth === 0) ternaryDepth++;
else if (c === ":" && depth === 0 && ternaryDepth > 0) ternaryDepth--;
else if (c === "\n" && depth <= 0) {
if (ternaryDepth > 0) continue; // mid-ternary, awaiting the `:` branch
// Peek past whitespace/newlines: a `?` continuation keeps the statement.
let j = i + 1;
while (j < input.length && /\s/.test(input[j])) j++;
if (input[j] === "?") continue;
const stmt = input.slice(start, i).trim();
if (stmt) stmts.push(stmt);
start = i + 1;
Expand Down