CodeMirror 6 lets you listen for doc changes in the form of transactions, but every change is fired off immediately: if a user types function cakeBaker(type="extreme") at a normal typing rate, that’s 30+ transactions for one letter each, instead of a single transaction for the single stretch of text typed.
Is there a way to set a debounce interval (“keep aggregating changes until there’s no new change for X milliseconds”) or even debounce with explicit “ignore debounce if …” rule (e.g. don’t finalize the current transaction unless the change was a boundary character), or even a way to bundle/merge transactions into one transaction after the fact, so that someone can roll their own debounce logic that lets them send a single object to their OT server ? (e.g. const bundle = Transaction.merge(t1,t2,t3,t4,t5,...)).
You cannot merge transactions, but you can merge change set with ChangeSet.compose. You could set up a thing that accumulates changes and runs some code on the result only after a given period of inactivity.
Won’t work for everyone but I get an effect like this by calling code from the linter callback.
import { linter, lintGutter } from "@codemirror/lint";
// other eslint stuff I guess not strictly necessary
// but see https://codemirror.net/examples/lint/
// in extensions:
linter(
view => {
let o = esLint(new eslint.Linter(), config)(view).filter(d => !(d.source == 'eslint:no-undef' && d.message.includes("_in'")));
if (o.length == 0) {
update(view); // update is my custom function
}
return o;
}),
I think I read that the linter pass gets skipped if it interferes with editor responsiveness, so not quite a simple debounce, but it might have interesting code to look into.
My “solution” was to have a debounce listener on document changes and just literally diff the document with its previous version. That’s probably a really bad plan for huge documents, but for my use-case it works quite well:
async function syncContent(filename) {
const entry = cmInstances[filename];
// get the old and new content
const currentContent = entry.content;
const newContent = entry.view.state.doc.toString();
// form a diff and send that over to the server
const changes = createPatch(filename, currentContent, newContent);
const response = await fetch(`/sync/${filename}`, {
headers: { "Content-Type": `text/plain` },
method: `post`,
body: changes,
});
// verify both client and server have the same content now
const responseHash = parseFloat(await response.text());
if (responseHash === getFileSum(newContent)) {
entry.content = newContent;
updatePreview();
} else {
// This should, if I did everything right, never happen.
}
entry.debounce = false;
}
(with createPatch being the standard “get me the diff between these two strings” that you can find loads of libraries for)