diff --git a/apps/web/src/components/ComposerPromptEditorTiptap.tsx b/apps/web/src/components/ComposerPromptEditorTiptap.tsx index abc9549ee3d00..911674fe91c00 100644 --- a/apps/web/src/components/ComposerPromptEditorTiptap.tsx +++ b/apps/web/src/components/ComposerPromptEditorTiptap.tsx @@ -1013,7 +1013,7 @@ function ComposerPromptEditorTiptapInner( const handleEditorChange = (updated: TiptapEditor) => { const map = serializeEditorDoc(updated.state.doc); - const contextIds = literalText ? [] : map.contextIds; + const contextIds = literalText ? [] : [...map.contextIds]; const { from, to } = updated.state.selection; const fromFlat = pmToFlat(map, from); const toFlat = pmToFlat(map, to); @@ -1468,7 +1468,7 @@ function ComposerPromptEditorTiptapInner( value: map.value, cursor: collapseExpandedComposerCursor(map.value, expandedCursor, literalText), expandedCursor, - contextIds: literalText ? [] : map.contextIds, + contextIds: literalText ? [] : [...map.contextIds], }; const toFlat = pmToFlat(map, to); selectionRangeRef.current = { diff --git a/apps/web/src/composer-rich-text-doc.ts b/apps/web/src/composer-rich-text-doc.ts index c0e0a653016e3..2a2a3d3b9af42 100644 --- a/apps/web/src/composer-rich-text-doc.ts +++ b/apps/web/src/composer-rich-text-doc.ts @@ -811,11 +811,12 @@ export interface RichRun { nodeName?: string; } +// Shared by every caller that serializes the same doc, so it must not be mutated. export interface RichDocMap { - value: string; - runs: RichRun[]; - docLength: number; - contextIds: string[]; + readonly value: string; + readonly runs: ReadonlyArray>; + readonly docLength: number; + readonly contextIds: ReadonlyArray; } function readAtomSource(node: ProseMirrorNode): string { @@ -1285,7 +1286,20 @@ export function serializeSelection(doc: ProseMirrorNode, from: number, to: numbe return serializeEditorDoc(doc.type.create(null, content)).value; } +// Docs are immutable, and one keystroke serializes the same doc from the marker +// plugin, onUpdate, and onSelectionUpdate. +const serializedDocs = new WeakMap(); + export function serializeEditorDoc(doc: ProseMirrorNode): RichDocMap { + let map = serializedDocs.get(doc); + if (!map) { + map = serializeDoc(doc); + serializedDocs.set(doc, map); + } + return map; +} + +function serializeDoc(doc: ProseMirrorNode): RichDocMap { const acc: RichAccumulator = { runs: [], value: "", flat: 0, md: 0 }; const blocks: ProseMirrorNode[] = []; doc.content.forEach((node) => {