Fix Android IME typing: editor clobbered its own autosave echo
Some checks failed
ci / test (push) Has been cancelled
ci / release (push) Has been cancelled

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 00:04:24 +02:00
parent 4328bbaeda
commit bd2b8e6241

View File

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