From bd2b8e62414f40fcc6a43cc217095ec07de821b1 Mon Sep 17 00:00:00 2001 From: Senad Date: Thu, 23 Jul 2026 00:04:24 +0200 Subject: [PATCH] Fix Android IME typing: editor clobbered its own autosave echo The $: block applying note changes to CodeMirror listed the autosave bookkeeping vars (saveTimer, lastKnown) as dependencies, so the 400 ms autosave re-ran it before App delivered the updated note prop. That stale run misread the save echo as an external sync change and replaced the whole doc mid-typing: on Android this aborted every IME composition and threw the cursor to the top of the note. Apply note/query via $: fn(prop) so the statements re-run only when the prop itself changes. Co-Authored-By: Claude Fable 5 --- frontend/src/ui/Editor.svelte | 37 +++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/frontend/src/ui/Editor.svelte b/frontend/src/ui/Editor.svelte index 11a4218..01b4229 100644 --- a/frontend/src/ui/Editor.svelte +++ b/frontend/src/ui/Editor.svelte @@ -106,39 +106,52 @@ // Swap document when a different note is opened; apply external (sync) changes // without clobbering in-flight typing. - $: if (view && note) { - if (note.id !== currentId) { + function applyNote(n: Note) { + if (!view || !n) return; + if (n.id !== currentId) { if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; flushSave(currentId, view.state.doc.toString()); } - currentId = note.id; - lastKnown = note.content; - view.setState(makeState(note.content)); - } else if (note.content !== lastKnown) { + currentId = n.id; + lastKnown = n.content; + view.setState(makeState(n.content)); + } else if (n.content !== lastKnown) { // Store changed underneath us (sync pull / conflict replacement) — not an // echo of our own autosave. Replace the doc. - lastKnown = note.content; + lastKnown = n.content; if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; } - if (note.content !== view.state.doc.toString()) { - view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: note.content } }); + if (n.content !== view.state.doc.toString()) { + view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: n.content } }); } } } let lastQuery = ''; - $: if (view && query !== lastQuery) { - lastQuery = query; - view.dispatch({ effects: searchMark.reconfigure(searchExtension(query)) }); + function applyQuery(q: string) { + if (!view || q === lastQuery) return; + lastQuery = q; + view.dispatch({ effects: searchMark.reconfigure(searchExtension(q)) }); } + // Deliberately `$: fn(prop)` instead of inlining the logic in a reactive + // block: these must re-run ONLY when the prop itself changes. Svelte tracks + // every variable a `$:` statement mentions (including writes like + // `saveTimer = null` deep in the autosave path), and a re-run triggered by + // our own bookkeeping sees a stale `note`, misreads the autosave echo as an + // external sync change, and replaces the doc mid-typing — which on Android + // aborts IME composition and throws the cursor to the top of the note. + $: applyNote(note); + $: applyQuery(query); + onMount(() => { currentId = note.id; lastKnown = note.content; + lastQuery = query; // makeState below bakes in the current query view = new EditorView({ state: makeState(note.content), parent: host }); });