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 <noreply@anthropic.com>
This commit is contained in:
@@ -106,39 +106,52 @@
|
|||||||
|
|
||||||
// Swap document when a different note is opened; apply external (sync) changes
|
// Swap document when a different note is opened; apply external (sync) changes
|
||||||
// without clobbering in-flight typing.
|
// without clobbering in-flight typing.
|
||||||
$: if (view && note) {
|
function applyNote(n: Note) {
|
||||||
if (note.id !== currentId) {
|
if (!view || !n) return;
|
||||||
|
if (n.id !== currentId) {
|
||||||
if (saveTimer) {
|
if (saveTimer) {
|
||||||
clearTimeout(saveTimer);
|
clearTimeout(saveTimer);
|
||||||
saveTimer = null;
|
saveTimer = null;
|
||||||
flushSave(currentId, view.state.doc.toString());
|
flushSave(currentId, view.state.doc.toString());
|
||||||
}
|
}
|
||||||
currentId = note.id;
|
currentId = n.id;
|
||||||
lastKnown = note.content;
|
lastKnown = n.content;
|
||||||
view.setState(makeState(note.content));
|
view.setState(makeState(n.content));
|
||||||
} else if (note.content !== lastKnown) {
|
} else if (n.content !== lastKnown) {
|
||||||
// Store changed underneath us (sync pull / conflict replacement) — not an
|
// Store changed underneath us (sync pull / conflict replacement) — not an
|
||||||
// echo of our own autosave. Replace the doc.
|
// echo of our own autosave. Replace the doc.
|
||||||
lastKnown = note.content;
|
lastKnown = n.content;
|
||||||
if (saveTimer) {
|
if (saveTimer) {
|
||||||
clearTimeout(saveTimer);
|
clearTimeout(saveTimer);
|
||||||
saveTimer = null;
|
saveTimer = null;
|
||||||
}
|
}
|
||||||
if (note.content !== view.state.doc.toString()) {
|
if (n.content !== view.state.doc.toString()) {
|
||||||
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: note.content } });
|
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: n.content } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastQuery = '';
|
let lastQuery = '';
|
||||||
$: if (view && query !== lastQuery) {
|
function applyQuery(q: string) {
|
||||||
lastQuery = query;
|
if (!view || q === lastQuery) return;
|
||||||
view.dispatch({ effects: searchMark.reconfigure(searchExtension(query)) });
|
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(() => {
|
onMount(() => {
|
||||||
currentId = note.id;
|
currentId = note.id;
|
||||||
lastKnown = note.content;
|
lastKnown = note.content;
|
||||||
|
lastQuery = query; // makeState below bakes in the current query
|
||||||
view = new EditorView({ state: makeState(note.content), parent: host });
|
view = new EditorView({ state: makeState(note.content), parent: host });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user