From 541156d95aaa1545a4f460b1f4ea331a5dca4122 Mon Sep 17 00:00:00 2001 From: "charles.h" <1376246506@qq.com> Date: Sat, 19 Sep 2026 16:42:00 +0800 Subject: [PATCH 1/2] fix: don't advance the speculative diff position past the last line Inserting text with a line break into an empty editor threw "TypeError: Cannot read properties of undefined (reading 'children')", so the overlay was left untouched and the new text only showed up once the highlight debounce expired (~250ms later). The overlay of an empty document is a single `.line` without spans. checkPosition() treats that line as exhausted and wraps to `line + 1`, which is already past the end of `lines`. An added newline then derives its writing position from it (`readingPosition.line + 1`) and writes into `lines[n] === undefined`. Only wrap when the next line exists. --- packages/carta-md/src/lib/internal/speculative.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/carta-md/src/lib/internal/speculative.ts b/packages/carta-md/src/lib/internal/speculative.ts index 5102f272..fd1f5a07 100644 --- a/packages/carta-md/src/lib/internal/speculative.ts +++ b/packages/carta-md/src/lib/internal/speculative.ts @@ -223,7 +223,12 @@ function checkPosition(position: Position, lines: Element[]): Position { if (nextPosition.span >= lineElement.children.length) { nextPosition.char = 0; nextPosition.span = 0; - nextPosition.line = line + 1; + // Only advance to the next line if there is one. The overlay of an empty document is a + // single line without spans (``), so wrapping past it would + // leave `line` at `lines.length`; writing there reads `children` of `undefined`. + if (line + 1 < lines.length) { + nextPosition.line = line + 1; + } } return nextPosition; From eba72f94968abc10d5f3d185efbe76ccb6b7df55 Mon Sep 17 00:00:00 2001 From: "charles.h" <1376246506@qq.com> Date: Sat, 19 Sep 2026 16:42:12 +0800 Subject: [PATCH 2/2] fix: keep the speculative diff baseline in sync when an update throws MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `currentlyHighlightedValue` is only advanced on the success path, so when speculativeHighlightUpdate() throws, the baseline keeps the pre-change text while the debounced highlight still re-renders the overlay with the new value. The baseline then no longer describes the overlay: the next edit diffs from the stale text, the result does not line up with the overlay's content, and the whole document ends up treated as "added" and replayed into the existing overlay HTML. The text is drawn twice (new value followed by the previous one) until the next debounce lands, and — since a successful patch is what advances the baseline — the state does not heal on its own. The failed patch leaves the overlay untouched and the debounced highlight is already on its way to render `value`, so sync the baseline there too. --- packages/carta-md/src/lib/internal/components/Input.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/carta-md/src/lib/internal/components/Input.svelte b/packages/carta-md/src/lib/internal/components/Input.svelte index f6037a6a..87257f52 100644 --- a/packages/carta-md/src/lib/internal/components/Input.svelte +++ b/packages/carta-md/src/lib/internal/components/Input.svelte @@ -146,6 +146,11 @@ return { html, timestamp }; } catch (e) { console.error(`Error executing speculative update: ${e}.`); + // The overlay still holds the previous text, but the debounced highlight is already + // on its way and will render `value`. Keep the baseline in sync with it, otherwise it + // stays at the pre-change text and the next patch sees the whole document as added, + // replaying it into the overlay (the text is duplicated until the debounce lands). + currentlyHighlightedValue = value; } }