lexical
Version:
Lexical is an extensible text editor framework that provides excellent reliability, accessible and performance.
194 lines (179 loc) • 6.18 kB
text/typescript
/**
* 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 {LexicalEditor} from './LexicalEditor';
import type {EditorState} from './LexicalEditorState';
import type {LexicalNode, NodeKey, NodeMap} from './LexicalNode';
import {$isElementNode} from '.';
import {$isSlotChild, $isSlotHost} from './LexicalSlot';
import {getActiveEditor} from './LexicalUpdates';
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: LexicalNode,
parentKey: NodeKey,
prevNodeMap: NodeMap,
nodeMap: NodeMap,
nodeMapDelete: NodeKey[],
dirtyNodes: Map<NodeKey, IntentionallyMarkedAsDirtyElement> | Set<NodeKey>,
): void {
if ($isElementNode(node)) {
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) ||
($isSlotHost(child) && child.__slots !== null)
) {
$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();
}
}
// Slot nodes are not in the linked-list child channel; reach them through
// the slot map, gating on the slot host the mirror of the __parent check.
// Slots hang off any host (element or decorator), so this runs regardless
// of the host node type.
for (const slotKey of $isSlotHost(node) && node.__slots !== null
? node.__slots.values()
: []) {
const slotNode = nodeMap.get(slotKey);
if (
slotNode !== undefined &&
$isSlotChild(slotNode) &&
slotNode.__slotHost === parentKey
) {
if (
$isElementNode(slotNode) ||
($isSlotHost(slotNode) && slotNode.__slots !== null)
) {
$garbageCollectDetachedDeepChildNodes(
slotNode,
slotKey,
prevNodeMap,
nodeMap,
nodeMapDelete,
dirtyNodes,
);
}
if (!prevNodeMap.has(slotKey)) {
dirtyNodes.delete(slotKey);
}
nodeMapDelete.push(slotKey);
}
}
}
export function $garbageCollectDetachedNodes(
prevEditorState: EditorState,
editorState: EditorState,
dirtyLeaves: Set<NodeKey>,
dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
): 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: NodeKey[] = [];
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 dirtyLeaves) {
const node = nodeMap.get(nodeKey);
if (node !== undefined && !node.isAttached()) {
// A decorator host is a leaf, so the element deep-walk above never
// reaches its slots; collect them here to avoid orphaning the slot
// subtree. Deletion is deferred to the shared queue so the walk can
// still read the slot nodes. When a host is in dirtyElements and one of
// its slot values is also dirty, the two loops can both push the same
// slot subtree key into nodeMapDelete — that redundancy is harmless
// because nodeMap.delete is idempotent and the dirtyNodes.delete calls
// are too.
if ($isSlotHost(node) && node.__slots !== null) {
$garbageCollectDetachedDeepChildNodes(
node,
nodeKey,
prevNodeMap,
nodeMap,
nodeMapDelete,
dirtyLeaves,
);
}
if (!prevNodeMap.has(nodeKey)) {
dirtyLeaves.delete(nodeKey);
}
nodeMapDelete.push(nodeKey);
}
}
for (const nodeKey of nodeMapDelete) {
nodeMap.delete(nodeKey);
}
// Clear the composition key if it points at a node that just got collected.
// Without this, isComposing() keeps reporting true after a remote yjs
// update (or any host removal) drops the composing TextNode — most often
// observable when the composing node sits inside a slot subtree that gets
// collected wholesale via the dual-channel slot GC above.
const editor = getActiveEditor();
const compositionKey = editor._compositionKey;
if (compositionKey !== null && !nodeMap.has(compositionKey)) {
editor._compositionKey = null;
}
}