#2396·milkdown

[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

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).

bash
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?)