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 }); });