From ac3773a016bbe80576f11fafc6150d84eecb57a8 Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Mon, 27 Jul 2026 03:56:25 +0700 Subject: [PATCH 1/2] fix(lang-core): keep multi-line ternaries intact in mergeStatements splitStatementSource re-split the existing program with a char-level splitter that broke on every depth-0 newline, unlike split()/scanNewCompleted() which keep a ternary's ?/: continuation lines with the statement. So an untouched multi-line ternary lost both branches on merge. Track ternary depth and peek for a ? continuation, mirroring split(). --- packages/lang-core/src/parser/merge.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/lang-core/src/parser/merge.ts b/packages/lang-core/src/parser/merge.ts index 69ba9a093..9c090de20 100644 --- a/packages/lang-core/src/parser/merge.ts +++ b/packages/lang-core/src/parser/merge.ts @@ -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; @@ -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; From 947e32b1a838788f08c8f66625169809203af6e5 Mon Sep 17 00:00:00 2001 From: serhiizghama Date: Mon, 27 Jul 2026 03:56:25 +0700 Subject: [PATCH 2/2] test(lang-core): cover multi-line ternary merge round-trip --- .../src/parser/__tests__/serialize.test.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/lang-core/src/parser/__tests__/serialize.test.ts b/packages/lang-core/src/parser/__tests__/serialize.test.ts index 6652292d6..cb8d81029 100644 --- a/packages/lang-core/src/parser/__tests__/serialize.test.ts +++ b/packages/lang-core/src/parser/__tests__/serialize.test.ts @@ -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 ─────────────────────────────────────────────────────────