editor: improve headingUpdatePlugin performance

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2025-12-29 14:23:16 +05:00
parent 2a92ea4efd
commit 255677d98e
3 changed files with 48 additions and 38 deletions

View File

@@ -26,6 +26,7 @@ import { Heading as TiptapHeading } from "@tiptap/extension-heading";
import { Node } from "@tiptap/pm/model";
import { Plugin, PluginKey, Selection, Transaction } from "@tiptap/pm/state";
import { Callout } from "../callout/callout.js";
import { changedDescendants } from "../../utils/prosemirror.js";
const COLLAPSIBLE_BLOCK_TYPES = [
"paragraph",
@@ -408,12 +409,10 @@ const headingUpdatePlugin = new Plugin({
const newDoc = newState.doc;
let modified = false;
newDoc.descendants((newNode, pos) => {
if (pos >= oldDoc.content.size) return;
function check(newNode: Node, pos: number, oldNode?: Node) {
if (!oldNode) return;
const oldNode = oldDoc.nodeAt(pos);
if (
oldNode &&
oldNode.type.name === "heading" &&
oldNode.attrs.level !== newNode.attrs.level
) {
@@ -432,7 +431,9 @@ const headingUpdatePlugin = new Plugin({
modified = true;
}
}
});
}
changedDescendants(oldDoc, newDoc, 0, check);
return modified ? tr : null;
}

View File

@@ -8,44 +8,13 @@ import { EditorState, PluginKey, Transaction } from "prosemirror-state";
import { tableNodeTypes, TableRole } from "./schema.js";
import { TableMap } from "./tablemap.js";
import { CellAttrs, removeColSpan } from "./util.js";
import { changedDescendants } from "../../../utils/prosemirror.js";
/**
* @public
*/
export const fixTablesKey = new PluginKey<{ fixTables: boolean }>("fix-tables");
/**
* Helper for iterating through the nodes in a document that changed
* compared to the given previous document. Useful for avoiding
* duplicate work on each transaction.
*
* @public
*/
function changedDescendants(
old: Node,
cur: Node,
offset: number,
f: (node: Node, pos: number) => void
): void {
const oldSize = old.childCount,
curSize = cur.childCount;
outer: for (let i = 0, j = 0; i < curSize; i++) {
const child = cur.child(i);
for (let scan = j, e = Math.min(oldSize, i + 3); scan < e; scan++) {
if (old.child(scan) == child) {
j = scan + 1;
offset += child.nodeSize;
continue outer;
}
}
f(child, offset);
if (j < oldSize && old.child(j).sameMarkup(child))
changedDescendants(old.child(j), child, offset + 1, f);
else child.nodesBetween(0, child.content.size, f, offset + 1);
offset += child.nodeSize;
}
}
/**
* Inspect all tables in the given state's document and return a
* transaction that fixes them, if necessary. If `oldState` was

View File

@@ -34,7 +34,8 @@ import {
Slice,
DOMParser,
Schema,
Fragment
Fragment,
Node
} from "prosemirror-model";
import { EditorState, Selection, Transaction } from "prosemirror-state";
import TextStyle from "@tiptap/extension-text-style";
@@ -410,3 +411,42 @@ export function ensureLeadingParagraph(node: Node, schema: Schema): Fragment {
return fragment;
}
/**
* Helper for iterating through the nodes in a document that changed
* compared to the given previous document. Useful for avoiding
* duplicate work on each transaction.
*
* @public
*/
export function changedDescendants(
old: Node,
cur: Node,
offset: number,
f: (newNode: Node, pos: number, oldNode?: Node) => void
): void {
const oldSize = old.childCount,
curSize = cur.childCount;
outer: for (let i = 0, j = 0; i < curSize; i++) {
const child = cur.child(i);
for (let scan = j, e = Math.min(oldSize, i + 3); scan < e; scan++) {
if (old.child(scan) == child) {
j = scan + 1;
offset += child.nodeSize;
continue outer;
}
}
f(child, offset, i < oldSize ? old.child(i) : undefined);
if (j < oldSize && old.child(j).sameMarkup(child)) {
changedDescendants(old.child(j), child, offset + 1, f);
} else {
child.nodesBetween(
0,
child.content.size,
f as (node: Node, pos: number) => void,
offset + 1
);
}
offset += child.nodeSize;
}
}