[Feature] Provide a way to "patch" the current markdown without doing a full replace (to maintain a coherent cursor position)
Author: fabiozCreated Jun 15, 2026Updated Sep 12, 2026
Labelsenhancement
Initial checklist
- I agree to follow the code of conduct
- I searched issues and discussions and couldn’t find anything (or linked relevant results below)
Problem
Doing "replaceAll' the cursor position is lost.
Solution
I'm now using the code below and it seems to be working well for me (so, my suggestion is add that -- or something close to it -- in the same context that replaceAll is added).
import { editorViewCtx, parserCtx } from "@milkdown/kit/core";
import { computeDocDiff } from "@milkdown/plugin-diff";
import type { EditorView } from "@milkdown/prose/view";
/**
* Applies a semantic patch to the current Milkdown document.
*
* Uses Milkdown's block-level LCS diff with per-block ChangeSet merging so
* edits are as small as possible (preserving cursor position when mapping allows).
*
* Returns true if a transaction was dispatched.
*/
export function patchMarkdown(
editor: { action: (action: (ctx: any) => unknown) => unknown },
newMarkdown: string,
): boolean {
return editor.action((ctx) => {
const view: EditorView = ctx.get(editorViewCtx);
const parser = ctx.get(parserCtx);
const oldDoc = view.state.doc;
const newDoc = parser(newMarkdown);
if (oldDoc.eq(newDoc)) {
return false;
}
const changes = computeDocDiff(oldDoc, newDoc);
if (changes.length === 0) {
return false;
}
let tr = view.state.tr;
for (let i = changes.length - 1; i >= 0; i--) {
const change = changes[i]!;
const newContent = newDoc.slice(change.fromB, change.toB);
tr = tr.replace(change.fromA, change.toA, newContent);
}
if (!tr.docChanged) {
return false;
}
tr.setSelection(view.state.selection.map(tr.doc, tr.mapping));
view.dispatch(tr);
return true;
}) as boolean;
}Alternatives
I haven't found a good alternative to "migrate" to a new markdown structure without loosing the cursor (maybe I'm missing something?)
Source: Milkdown/milkdown