Skip to content

[Feature] Provide a way to "patch" the current markdown without doing a full replace (to maintain a coherent cursor position) #2396

Description

@fabioz

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions