From 255677d98ecc7e99bc9ebcec2ab1a866b39a00f6 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:23:16 +0500 Subject: [PATCH] editor: improve headingUpdatePlugin performance Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- .../editor/src/extensions/heading/heading.ts | 11 ++--- .../table/prosemirror-tables/fixtables.ts | 33 +-------------- packages/editor/src/utils/prosemirror.ts | 42 ++++++++++++++++++- 3 files changed, 48 insertions(+), 38 deletions(-) diff --git a/packages/editor/src/extensions/heading/heading.ts b/packages/editor/src/extensions/heading/heading.ts index 5ac71eec6..a9e7ac4b7 100644 --- a/packages/editor/src/extensions/heading/heading.ts +++ b/packages/editor/src/extensions/heading/heading.ts @@ -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; } diff --git a/packages/editor/src/extensions/table/prosemirror-tables/fixtables.ts b/packages/editor/src/extensions/table/prosemirror-tables/fixtables.ts index 848be68ac..920ba1e5b 100644 --- a/packages/editor/src/extensions/table/prosemirror-tables/fixtables.ts +++ b/packages/editor/src/extensions/table/prosemirror-tables/fixtables.ts @@ -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 diff --git a/packages/editor/src/utils/prosemirror.ts b/packages/editor/src/utils/prosemirror.ts index ff2068dd7..a10801f21 100644 --- a/packages/editor/src/utils/prosemirror.ts +++ b/packages/editor/src/utils/prosemirror.ts @@ -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; + } +}