/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import type {ElementNode} from '.'; import type {LexicalEditor} from './LexicalEditor'; import type {EditorState} from './LexicalEditorState'; import type {NodeKey, NodeMap} from './LexicalNode'; import {$isElementNode} from '.'; import {cloneDecorators} from './LexicalUtils'; export function $garbageCollectDetachedDecorators( editor: LexicalEditor, pendingEditorState: EditorState, ): void { const currentDecorators = editor._decorators; const pendingDecorators = editor._pendingDecorators; let decorators = pendingDecorators || currentDecorators; const nodeMap = pendingEditorState._nodeMap; let key; for (key in decorators) { if (!nodeMap.has(key)) { if (decorators === currentDecorators) { decorators = cloneDecorators(editor); } delete decorators[key]; } } } type IntentionallyMarkedAsDirtyElement = boolean; function $garbageCollectDetachedDeepChildNodes( node: ElementNode, parentKey: NodeKey, prevNodeMap: NodeMap, nodeMap: NodeMap, nodeMapDelete: Array, dirtyNodes: Map, ): void { let child = node.getFirstChild(); while (child !== null) { const childKey = child.__key; // TODO Revise condition below, redundant? LexicalNode already cleans up children when moving Nodes if (child.__parent === parentKey) { if ($isElementNode(child)) { $garbageCollectDetachedDeepChildNodes( child, childKey, prevNodeMap, nodeMap, nodeMapDelete, dirtyNodes, ); } // If we have created a node and it was dereferenced, then also // remove it from out dirty nodes Set. if (!prevNodeMap.has(childKey)) { dirtyNodes.delete(childKey); } nodeMapDelete.push(childKey); } child = child.getNextSibling(); } } export function $garbageCollectDetachedNodes( prevEditorState: EditorState, editorState: EditorState, dirtyLeaves: Set, dirtyElements: Map, ): void { const prevNodeMap = prevEditorState._nodeMap; const nodeMap = editorState._nodeMap; // Store dirtyElements in a queue for later deletion; deleting dirty subtrees too early will // hinder accessing .__next on child nodes const nodeMapDelete: Array = []; for (const [nodeKey] of dirtyElements) { const node = nodeMap.get(nodeKey); if (node !== undefined) { // Garbage collect node and its children if they exist if (!node.isAttached()) { if ($isElementNode(node)) { $garbageCollectDetachedDeepChildNodes( node, nodeKey, prevNodeMap, nodeMap, nodeMapDelete, dirtyElements, ); } // If we have created a node and it was dereferenced, then also // remove it from out dirty nodes Set. if (!prevNodeMap.has(nodeKey)) { dirtyElements.delete(nodeKey); } nodeMapDelete.push(nodeKey); } } } for (const nodeKey of nodeMapDelete) { nodeMap.delete(nodeKey); } for (const nodeKey of dirtyLeaves) { const node = nodeMap.get(nodeKey); if (node !== undefined && !node.isAttached()) { if (!prevNodeMap.has(nodeKey)) { dirtyLeaves.delete(nodeKey); } nodeMap.delete(nodeKey); } } }