]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/core/LexicalSelection.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / wysiwyg / lexical / core / LexicalSelection.ts
1 /**
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE file in the root directory of this source tree.
6  *
7  */
8
9 import type {LexicalEditor} from './LexicalEditor';
10 import type {EditorState} from './LexicalEditorState';
11 import type {NodeKey} from './LexicalNode';
12 import type {ElementNode} from './nodes/LexicalElementNode';
13 import type {TextFormatType} from './nodes/LexicalTextNode';
14
15 import invariant from 'lexical/shared/invariant';
16
17 import {
18   $createLineBreakNode,
19   $createParagraphNode,
20   $createTextNode, $getNearestNodeFromDOMNode,
21   $isDecoratorNode,
22   $isElementNode,
23   $isLineBreakNode,
24   $isRootNode,
25   $isTextNode,
26   $setSelection,
27   SELECTION_CHANGE_COMMAND,
28   TextNode,
29 } from '.';
30 import {DOM_ELEMENT_TYPE, TEXT_TYPE_TO_FORMAT} from './LexicalConstants';
31 import {
32   markCollapsedSelectionFormat,
33   markSelectionChangeFromDOMUpdate,
34 } from './LexicalEvents';
35 import {getIsProcessingMutations} from './LexicalMutations';
36 import {insertRangeAfter, LexicalNode} from './LexicalNode';
37 import {
38   getActiveEditor,
39   getActiveEditorState,
40   isCurrentlyReadOnlyMode,
41 } from './LexicalUpdates';
42 import {
43   $getAdjacentNode,
44   $getAncestor,
45   $getCompositionKey,
46   $getNearestRootOrShadowRoot,
47   $getNodeByKey,
48   $getNodeFromDOM,
49   $getRoot,
50   $hasAncestor,
51   $isTokenOrSegmented,
52   $setCompositionKey,
53   doesContainGrapheme,
54   getDOMSelection,
55   getDOMTextNode,
56   getElementByKeyOrThrow,
57   getTextNodeOffset,
58   INTERNAL_$isBlock,
59   isSelectionCapturedInDecoratorInput,
60   isSelectionWithinEditor,
61   removeDOMBlockCursorElement,
62   scrollIntoViewIfNeeded,
63   toggleTextFormatType,
64 } from './LexicalUtils';
65 import {$createTabNode, $isTabNode} from './nodes/LexicalTabNode';
66 import {$selectSingleNode} from "../../utils/selection";
67
68 export type TextPointType = {
69   _selection: BaseSelection;
70   getNode: () => TextNode;
71   is: (point: PointType) => boolean;
72   isBefore: (point: PointType) => boolean;
73   key: NodeKey;
74   offset: number;
75   set: (key: NodeKey, offset: number, type: 'text' | 'element') => void;
76   type: 'text';
77 };
78
79 export type ElementPointType = {
80   _selection: BaseSelection;
81   getNode: () => ElementNode;
82   is: (point: PointType) => boolean;
83   isBefore: (point: PointType) => boolean;
84   key: NodeKey;
85   offset: number;
86   set: (key: NodeKey, offset: number, type: 'text' | 'element') => void;
87   type: 'element';
88 };
89
90 export type PointType = TextPointType | ElementPointType;
91
92 export class Point {
93   key: NodeKey;
94   offset: number;
95   type: 'text' | 'element';
96   _selection: BaseSelection | null;
97
98   constructor(key: NodeKey, offset: number, type: 'text' | 'element') {
99     this._selection = null;
100     this.key = key;
101     this.offset = offset;
102     this.type = type;
103   }
104
105   is(point: PointType): boolean {
106     return (
107       this.key === point.key &&
108       this.offset === point.offset &&
109       this.type === point.type
110     );
111   }
112
113   isBefore(b: PointType): boolean {
114     let aNode = this.getNode();
115     let bNode = b.getNode();
116     const aOffset = this.offset;
117     const bOffset = b.offset;
118
119     if ($isElementNode(aNode)) {
120       const aNodeDescendant = aNode.getDescendantByIndex<ElementNode>(aOffset);
121       aNode = aNodeDescendant != null ? aNodeDescendant : aNode;
122     }
123     if ($isElementNode(bNode)) {
124       const bNodeDescendant = bNode.getDescendantByIndex<ElementNode>(bOffset);
125       bNode = bNodeDescendant != null ? bNodeDescendant : bNode;
126     }
127     if (aNode === bNode) {
128       return aOffset < bOffset;
129     }
130     return aNode.isBefore(bNode);
131   }
132
133   getNode(): LexicalNode {
134     const key = this.key;
135     const node = $getNodeByKey(key);
136     if (node === null) {
137       invariant(false, 'Point.getNode: node not found');
138     }
139     return node;
140   }
141
142   set(key: NodeKey, offset: number, type: 'text' | 'element'): void {
143     const selection = this._selection;
144     const oldKey = this.key;
145     this.key = key;
146     this.offset = offset;
147     this.type = type;
148     if (!isCurrentlyReadOnlyMode()) {
149       if ($getCompositionKey() === oldKey) {
150         $setCompositionKey(key);
151       }
152       if (selection !== null) {
153         selection.setCachedNodes(null);
154         selection.dirty = true;
155       }
156     }
157   }
158 }
159
160 export function $createPoint(
161   key: NodeKey,
162   offset: number,
163   type: 'text' | 'element',
164 ): PointType {
165   // @ts-expect-error: intentionally cast as we use a class for perf reasons
166   return new Point(key, offset, type);
167 }
168
169 function selectPointOnNode(point: PointType, node: LexicalNode): void {
170   let key = node.__key;
171   let offset = point.offset;
172   let type: 'element' | 'text' = 'element';
173   if ($isTextNode(node)) {
174     type = 'text';
175     const textContentLength = node.getTextContentSize();
176     if (offset > textContentLength) {
177       offset = textContentLength;
178     }
179   } else if (!$isElementNode(node)) {
180     const nextSibling = node.getNextSibling();
181     if ($isTextNode(nextSibling)) {
182       key = nextSibling.__key;
183       offset = 0;
184       type = 'text';
185     } else {
186       const parentNode = node.getParent();
187       if (parentNode) {
188         key = parentNode.__key;
189         offset = node.getIndexWithinParent() + 1;
190       }
191     }
192   }
193   point.set(key, offset, type);
194 }
195
196 export function $moveSelectionPointToEnd(
197   point: PointType,
198   node: LexicalNode,
199 ): void {
200   if ($isElementNode(node)) {
201     const lastNode = node.getLastDescendant();
202     if ($isElementNode(lastNode) || $isTextNode(lastNode)) {
203       selectPointOnNode(point, lastNode);
204     } else {
205       selectPointOnNode(point, node);
206     }
207   } else {
208     selectPointOnNode(point, node);
209   }
210 }
211
212 function $transferStartingElementPointToTextPoint(
213   start: ElementPointType,
214   end: PointType,
215   format: number,
216   style: string,
217 ): void {
218   const element = start.getNode();
219   const placementNode = element.getChildAtIndex(start.offset);
220   const textNode = $createTextNode();
221   const target = $isRootNode(element)
222     ? $createParagraphNode().append(textNode)
223     : textNode;
224   textNode.setFormat(format);
225   textNode.setStyle(style);
226   if (placementNode === null) {
227     element.append(target);
228   } else {
229     placementNode.insertBefore(target);
230   }
231   // Transfer the element point to a text point.
232   if (start.is(end)) {
233     end.set(textNode.__key, 0, 'text');
234   }
235   start.set(textNode.__key, 0, 'text');
236 }
237
238 function $setPointValues(
239   point: PointType,
240   key: NodeKey,
241   offset: number,
242   type: 'text' | 'element',
243 ): void {
244   point.key = key;
245   point.offset = offset;
246   point.type = type;
247 }
248
249 export interface BaseSelection {
250   _cachedNodes: Array<LexicalNode> | null;
251   dirty: boolean;
252
253   clone(): BaseSelection;
254   extract(): Array<LexicalNode>;
255   getNodes(): Array<LexicalNode>;
256   getTextContent(): string;
257   insertText(text: string): void;
258   insertRawText(text: string): void;
259   is(selection: null | BaseSelection): boolean;
260   insertNodes(nodes: Array<LexicalNode>): void;
261   getStartEndPoints(): null | [PointType, PointType];
262   isCollapsed(): boolean;
263   isBackward(): boolean;
264   getCachedNodes(): LexicalNode[] | null;
265   setCachedNodes(nodes: LexicalNode[] | null): void;
266 }
267
268 export class NodeSelection implements BaseSelection {
269   _nodes: Set<NodeKey>;
270   _cachedNodes: Array<LexicalNode> | null;
271   dirty: boolean;
272
273   constructor(objects: Set<NodeKey>) {
274     this._cachedNodes = null;
275     this._nodes = objects;
276     this.dirty = false;
277   }
278
279   getCachedNodes(): LexicalNode[] | null {
280     return this._cachedNodes;
281   }
282
283   setCachedNodes(nodes: LexicalNode[] | null): void {
284     this._cachedNodes = nodes;
285   }
286
287   is(selection: null | BaseSelection): boolean {
288     if (!$isNodeSelection(selection)) {
289       return false;
290     }
291     const a: Set<NodeKey> = this._nodes;
292     const b: Set<NodeKey> = selection._nodes;
293     return a.size === b.size && Array.from(a).every((key) => b.has(key));
294   }
295
296   isCollapsed(): boolean {
297     return false;
298   }
299
300   isBackward(): boolean {
301     return false;
302   }
303
304   getStartEndPoints(): null {
305     return null;
306   }
307
308   add(key: NodeKey): void {
309     this.dirty = true;
310     this._nodes.add(key);
311     this._cachedNodes = null;
312   }
313
314   delete(key: NodeKey): void {
315     this.dirty = true;
316     this._nodes.delete(key);
317     this._cachedNodes = null;
318   }
319
320   clear(): void {
321     this.dirty = true;
322     this._nodes.clear();
323     this._cachedNodes = null;
324   }
325
326   has(key: NodeKey): boolean {
327     return this._nodes.has(key);
328   }
329
330   clone(): NodeSelection {
331     return new NodeSelection(new Set(this._nodes));
332   }
333
334   extract(): Array<LexicalNode> {
335     return this.getNodes();
336   }
337
338   insertRawText(text: string): void {
339     // Do nothing?
340   }
341
342   insertText(): void {
343     // Do nothing?
344   }
345
346   insertNodes(nodes: Array<LexicalNode>) {
347     const selectedNodes = this.getNodes();
348     const selectedNodesLength = selectedNodes.length;
349     const lastSelectedNode = selectedNodes[selectedNodesLength - 1];
350     let selectionAtEnd: RangeSelection;
351     // Insert nodes
352     if ($isTextNode(lastSelectedNode)) {
353       selectionAtEnd = lastSelectedNode.select();
354     } else {
355       const index = lastSelectedNode.getIndexWithinParent() + 1;
356       selectionAtEnd = lastSelectedNode.getParentOrThrow().select(index, index);
357     }
358     selectionAtEnd.insertNodes(nodes);
359     // Remove selected nodes
360     for (let i = 0; i < selectedNodesLength; i++) {
361       selectedNodes[i].remove();
362     }
363   }
364
365   getNodes(): Array<LexicalNode> {
366     const cachedNodes = this._cachedNodes;
367     if (cachedNodes !== null) {
368       return cachedNodes;
369     }
370     const objects = this._nodes;
371     const nodes = [];
372     for (const object of objects) {
373       const node = $getNodeByKey(object);
374       if (node !== null) {
375         nodes.push(node);
376       }
377     }
378     if (!isCurrentlyReadOnlyMode()) {
379       this._cachedNodes = nodes;
380     }
381     return nodes;
382   }
383
384   getTextContent(): string {
385     const nodes = this.getNodes();
386     let textContent = '';
387     for (let i = 0; i < nodes.length; i++) {
388       textContent += nodes[i].getTextContent();
389     }
390     return textContent;
391   }
392 }
393
394 export function $isRangeSelection(x: unknown): x is RangeSelection {
395   return x instanceof RangeSelection;
396 }
397
398 export class RangeSelection implements BaseSelection {
399   format: number;
400   style: string;
401   anchor: PointType;
402   focus: PointType;
403   _cachedNodes: Array<LexicalNode> | null;
404   dirty: boolean;
405
406   constructor(
407     anchor: PointType,
408     focus: PointType,
409     format: number,
410     style: string,
411   ) {
412     this.anchor = anchor;
413     this.focus = focus;
414     anchor._selection = this;
415     focus._selection = this;
416     this._cachedNodes = null;
417     this.format = format;
418     this.style = style;
419     this.dirty = false;
420   }
421
422   getCachedNodes(): LexicalNode[] | null {
423     return this._cachedNodes;
424   }
425
426   setCachedNodes(nodes: LexicalNode[] | null): void {
427     this._cachedNodes = nodes;
428   }
429
430   /**
431    * Used to check if the provided selections is equal to this one by value,
432    * inluding anchor, focus, format, and style properties.
433    * @param selection - the Selection to compare this one to.
434    * @returns true if the Selections are equal, false otherwise.
435    */
436   is(selection: null | BaseSelection): boolean {
437     if (!$isRangeSelection(selection)) {
438       return false;
439     }
440     return (
441       this.anchor.is(selection.anchor) &&
442       this.focus.is(selection.focus) &&
443       this.format === selection.format &&
444       this.style === selection.style
445     );
446   }
447
448   /**
449    * Returns whether the Selection is "collapsed", meaning the anchor and focus are
450    * the same node and have the same offset.
451    *
452    * @returns true if the Selection is collapsed, false otherwise.
453    */
454   isCollapsed(): boolean {
455     return this.anchor.is(this.focus);
456   }
457
458   /**
459    * Gets all the nodes in the Selection. Uses caching to make it generally suitable
460    * for use in hot paths.
461    *
462    * @returns an Array containing all the nodes in the Selection
463    */
464   getNodes(): Array<LexicalNode> {
465     const cachedNodes = this._cachedNodes;
466     if (cachedNodes !== null) {
467       return cachedNodes;
468     }
469     const anchor = this.anchor;
470     const focus = this.focus;
471     const isBefore = anchor.isBefore(focus);
472     const firstPoint = isBefore ? anchor : focus;
473     const lastPoint = isBefore ? focus : anchor;
474     let firstNode = firstPoint.getNode();
475     let lastNode = lastPoint.getNode();
476     const startOffset = firstPoint.offset;
477     const endOffset = lastPoint.offset;
478
479     if ($isElementNode(firstNode)) {
480       const firstNodeDescendant =
481         firstNode.getDescendantByIndex<ElementNode>(startOffset);
482       firstNode = firstNodeDescendant != null ? firstNodeDescendant : firstNode;
483     }
484     if ($isElementNode(lastNode)) {
485       let lastNodeDescendant =
486         lastNode.getDescendantByIndex<ElementNode>(endOffset);
487       // We don't want to over-select, as node selection infers the child before
488       // the last descendant, not including that descendant.
489       if (
490         lastNodeDescendant !== null &&
491         lastNodeDescendant !== firstNode &&
492         lastNode.getChildAtIndex(endOffset) === lastNodeDescendant
493       ) {
494         lastNodeDescendant = lastNodeDescendant.getPreviousSibling();
495       }
496       lastNode = lastNodeDescendant != null ? lastNodeDescendant : lastNode;
497     }
498
499     let nodes: Array<LexicalNode>;
500
501     if (firstNode.is(lastNode)) {
502       if ($isElementNode(firstNode) && firstNode.getChildrenSize() > 0) {
503         nodes = [];
504       } else {
505         nodes = [firstNode];
506       }
507     } else {
508       nodes = firstNode.getNodesBetween(lastNode);
509     }
510     if (!isCurrentlyReadOnlyMode()) {
511       this._cachedNodes = nodes;
512     }
513     return nodes;
514   }
515
516   /**
517    * Sets this Selection to be of type "text" at the provided anchor and focus values.
518    *
519    * @param anchorNode - the anchor node to set on the Selection
520    * @param anchorOffset - the offset to set on the Selection
521    * @param focusNode - the focus node to set on the Selection
522    * @param focusOffset - the focus offset to set on the Selection
523    */
524   setTextNodeRange(
525     anchorNode: TextNode,
526     anchorOffset: number,
527     focusNode: TextNode,
528     focusOffset: number,
529   ): void {
530     $setPointValues(this.anchor, anchorNode.__key, anchorOffset, 'text');
531     $setPointValues(this.focus, focusNode.__key, focusOffset, 'text');
532     this._cachedNodes = null;
533     this.dirty = true;
534   }
535
536   /**
537    * Gets the (plain) text content of all the nodes in the selection.
538    *
539    * @returns a string representing the text content of all the nodes in the Selection
540    */
541   getTextContent(): string {
542     const nodes = this.getNodes();
543     if (nodes.length === 0) {
544       return '';
545     }
546     const firstNode = nodes[0];
547     const lastNode = nodes[nodes.length - 1];
548     const anchor = this.anchor;
549     const focus = this.focus;
550     const isBefore = anchor.isBefore(focus);
551     const [anchorOffset, focusOffset] = $getCharacterOffsets(this);
552     let textContent = '';
553     let prevWasElement = true;
554     for (let i = 0; i < nodes.length; i++) {
555       const node = nodes[i];
556       if ($isElementNode(node) && !node.isInline()) {
557         if (!prevWasElement) {
558           textContent += '\n';
559         }
560         if (node.isEmpty()) {
561           prevWasElement = false;
562         } else {
563           prevWasElement = true;
564         }
565       } else {
566         prevWasElement = false;
567         if ($isTextNode(node)) {
568           let text = node.getTextContent();
569           if (node === firstNode) {
570             if (node === lastNode) {
571               if (
572                 anchor.type !== 'element' ||
573                 focus.type !== 'element' ||
574                 focus.offset === anchor.offset
575               ) {
576                 text =
577                   anchorOffset < focusOffset
578                     ? text.slice(anchorOffset, focusOffset)
579                     : text.slice(focusOffset, anchorOffset);
580               }
581             } else {
582               text = isBefore
583                 ? text.slice(anchorOffset)
584                 : text.slice(focusOffset);
585             }
586           } else if (node === lastNode) {
587             text = isBefore
588               ? text.slice(0, focusOffset)
589               : text.slice(0, anchorOffset);
590           }
591           textContent += text;
592         } else if (
593           ($isDecoratorNode(node) || $isLineBreakNode(node)) &&
594           (node !== lastNode || !this.isCollapsed())
595         ) {
596           textContent += node.getTextContent();
597         }
598       }
599     }
600     return textContent;
601   }
602
603   /**
604    * Attempts to map a DOM selection range onto this Lexical Selection,
605    * setting the anchor, focus, and type accordingly
606    *
607    * @param range a DOM Selection range conforming to the StaticRange interface.
608    */
609   applyDOMRange(range: StaticRange): void {
610     const editor = getActiveEditor();
611     const currentEditorState = editor.getEditorState();
612     const lastSelection = currentEditorState._selection;
613     const resolvedSelectionPoints = $internalResolveSelectionPoints(
614       range.startContainer,
615       range.startOffset,
616       range.endContainer,
617       range.endOffset,
618       editor,
619       lastSelection,
620     );
621     if (resolvedSelectionPoints === null) {
622       return;
623     }
624     const [anchorPoint, focusPoint] = resolvedSelectionPoints;
625     $setPointValues(
626       this.anchor,
627       anchorPoint.key,
628       anchorPoint.offset,
629       anchorPoint.type,
630     );
631     $setPointValues(
632       this.focus,
633       focusPoint.key,
634       focusPoint.offset,
635       focusPoint.type,
636     );
637     this._cachedNodes = null;
638   }
639
640   /**
641    * Creates a new RangeSelection, copying over all the property values from this one.
642    *
643    * @returns a new RangeSelection with the same property values as this one.
644    */
645   clone(): RangeSelection {
646     const anchor = this.anchor;
647     const focus = this.focus;
648     const selection = new RangeSelection(
649       $createPoint(anchor.key, anchor.offset, anchor.type),
650       $createPoint(focus.key, focus.offset, focus.type),
651       this.format,
652       this.style,
653     );
654     return selection;
655   }
656
657   /**
658    * Toggles the provided format on all the TextNodes in the Selection.
659    *
660    * @param format a string TextFormatType to toggle on the TextNodes in the selection
661    */
662   toggleFormat(format: TextFormatType): void {
663     this.format = toggleTextFormatType(this.format, format, null);
664     this.dirty = true;
665   }
666
667   /**
668    * Sets the value of the style property on the Selection
669    *
670    * @param style - the style to set at the value of the style property.
671    */
672   setStyle(style: string): void {
673     this.style = style;
674     this.dirty = true;
675   }
676
677   /**
678    * Returns whether the provided TextFormatType is present on the Selection. This will be true if any node in the Selection
679    * has the specified format.
680    *
681    * @param type the TextFormatType to check for.
682    * @returns true if the provided format is currently toggled on on the Selection, false otherwise.
683    */
684   hasFormat(type: TextFormatType): boolean {
685     const formatFlag = TEXT_TYPE_TO_FORMAT[type];
686     return (this.format & formatFlag) !== 0;
687   }
688
689   /**
690    * Attempts to insert the provided text into the EditorState at the current Selection.
691    * converts tabs, newlines, and carriage returns into LexicalNodes.
692    *
693    * @param text the text to insert into the Selection
694    */
695   insertRawText(text: string): void {
696     const parts = text.split(/(\r?\n|\t)/);
697     const nodes = [];
698     const length = parts.length;
699     for (let i = 0; i < length; i++) {
700       const part = parts[i];
701       if (part === '\n' || part === '\r\n') {
702         nodes.push($createLineBreakNode());
703       } else if (part === '\t') {
704         nodes.push($createTabNode());
705       } else {
706         nodes.push($createTextNode(part));
707       }
708     }
709     this.insertNodes(nodes);
710   }
711
712   /**
713    * Attempts to insert the provided text into the EditorState at the current Selection as a new
714    * Lexical TextNode, according to a series of insertion heuristics based on the selection type and position.
715    *
716    * @param text the text to insert into the Selection
717    */
718   insertText(text: string): void {
719     const anchor = this.anchor;
720     const focus = this.focus;
721     const format = this.format;
722     const style = this.style;
723     let firstPoint = anchor;
724     let endPoint = focus;
725     if (!this.isCollapsed() && focus.isBefore(anchor)) {
726       firstPoint = focus;
727       endPoint = anchor;
728     }
729     if (firstPoint.type === 'element') {
730       $transferStartingElementPointToTextPoint(
731         firstPoint,
732         endPoint,
733         format,
734         style,
735       );
736     }
737     const startOffset = firstPoint.offset;
738     let endOffset = endPoint.offset;
739     const selectedNodes = this.getNodes();
740     const selectedNodesLength = selectedNodes.length;
741     let firstNode: TextNode = selectedNodes[0] as TextNode;
742
743     if (!$isTextNode(firstNode)) {
744       invariant(false, 'insertText: first node is not a text node');
745     }
746     const firstNodeText = firstNode.getTextContent();
747     const firstNodeTextLength = firstNodeText.length;
748     const firstNodeParent = firstNode.getParentOrThrow();
749     const lastIndex = selectedNodesLength - 1;
750     let lastNode = selectedNodes[lastIndex];
751
752     if (selectedNodesLength === 1 && endPoint.type === 'element') {
753       endOffset = firstNodeTextLength;
754       endPoint.set(firstPoint.key, endOffset, 'text');
755     }
756
757     if (
758       this.isCollapsed() &&
759       startOffset === firstNodeTextLength &&
760       (firstNode.isSegmented() ||
761         firstNode.isToken() ||
762         !firstNode.canInsertTextAfter() ||
763         (!firstNodeParent.canInsertTextAfter() &&
764           firstNode.getNextSibling() === null))
765     ) {
766       let nextSibling = firstNode.getNextSibling<TextNode>();
767       if (
768         !$isTextNode(nextSibling) ||
769         !nextSibling.canInsertTextBefore() ||
770         $isTokenOrSegmented(nextSibling)
771       ) {
772         nextSibling = $createTextNode();
773         nextSibling.setFormat(format);
774         nextSibling.setStyle(style);
775         if (!firstNodeParent.canInsertTextAfter()) {
776           firstNodeParent.insertAfter(nextSibling);
777         } else {
778           firstNode.insertAfter(nextSibling);
779         }
780       }
781       nextSibling.select(0, 0);
782       firstNode = nextSibling;
783       if (text !== '') {
784         this.insertText(text);
785         return;
786       }
787     } else if (
788       this.isCollapsed() &&
789       startOffset === 0 &&
790       (firstNode.isSegmented() ||
791         firstNode.isToken() ||
792         !firstNode.canInsertTextBefore() ||
793         (!firstNodeParent.canInsertTextBefore() &&
794           firstNode.getPreviousSibling() === null))
795     ) {
796       let prevSibling = firstNode.getPreviousSibling<TextNode>();
797       if (!$isTextNode(prevSibling) || $isTokenOrSegmented(prevSibling)) {
798         prevSibling = $createTextNode();
799         prevSibling.setFormat(format);
800         if (!firstNodeParent.canInsertTextBefore()) {
801           firstNodeParent.insertBefore(prevSibling);
802         } else {
803           firstNode.insertBefore(prevSibling);
804         }
805       }
806       prevSibling.select();
807       firstNode = prevSibling;
808       if (text !== '') {
809         this.insertText(text);
810         return;
811       }
812     } else if (firstNode.isSegmented() && startOffset !== firstNodeTextLength) {
813       const textNode = $createTextNode(firstNode.getTextContent());
814       textNode.setFormat(format);
815       firstNode.replace(textNode);
816       firstNode = textNode;
817     } else if (!this.isCollapsed() && text !== '') {
818       // When the firstNode or lastNode parents are elements that
819       // do not allow text to be inserted before or after, we first
820       // clear the content. Then we normalize selection, then insert
821       // the new content.
822       const lastNodeParent = lastNode.getParent();
823
824       if (
825         !firstNodeParent.canInsertTextBefore() ||
826         !firstNodeParent.canInsertTextAfter() ||
827         ($isElementNode(lastNodeParent) &&
828           (!lastNodeParent.canInsertTextBefore() ||
829             !lastNodeParent.canInsertTextAfter()))
830       ) {
831         this.insertText('');
832         $normalizeSelectionPointsForBoundaries(this.anchor, this.focus, null);
833         this.insertText(text);
834         return;
835       }
836     }
837
838     if (selectedNodesLength === 1) {
839       if (firstNode.isToken()) {
840         const textNode = $createTextNode(text);
841         textNode.select();
842         firstNode.replace(textNode);
843         return;
844       }
845       const firstNodeFormat = firstNode.getFormat();
846       const firstNodeStyle = firstNode.getStyle();
847
848       if (
849         startOffset === endOffset &&
850         (firstNodeFormat !== format || firstNodeStyle !== style)
851       ) {
852         if (firstNode.getTextContent() === '') {
853           firstNode.setFormat(format);
854           firstNode.setStyle(style);
855         } else {
856           const textNode = $createTextNode(text);
857           textNode.setFormat(format);
858           textNode.setStyle(style);
859           textNode.select();
860           if (startOffset === 0) {
861             firstNode.insertBefore(textNode, false);
862           } else {
863             const [targetNode] = firstNode.splitText(startOffset);
864             targetNode.insertAfter(textNode, false);
865           }
866           // When composing, we need to adjust the anchor offset so that
867           // we correctly replace that right range.
868           if (textNode.isComposing() && this.anchor.type === 'text') {
869             this.anchor.offset -= text.length;
870           }
871           return;
872         }
873       } else if ($isTabNode(firstNode)) {
874         // We don't need to check for delCount because there is only the entire selected node case
875         // that can hit here for content size 1 and with canInsertTextBeforeAfter false
876         const textNode = $createTextNode(text);
877         textNode.setFormat(format);
878         textNode.setStyle(style);
879         textNode.select();
880         firstNode.replace(textNode);
881         return;
882       }
883       const delCount = endOffset - startOffset;
884
885       firstNode = firstNode.spliceText(startOffset, delCount, text, true);
886       if (firstNode.getTextContent() === '') {
887         firstNode.remove();
888       } else if (this.anchor.type === 'text') {
889         if (firstNode.isComposing()) {
890           // When composing, we need to adjust the anchor offset so that
891           // we correctly replace that right range.
892           this.anchor.offset -= text.length;
893         } else {
894           this.format = firstNodeFormat;
895           this.style = firstNodeStyle;
896         }
897       }
898     } else {
899       const markedNodeKeysForKeep = new Set([
900         ...firstNode.getParentKeys(),
901         ...lastNode.getParentKeys(),
902       ]);
903
904       // We have to get the parent elements before the next section,
905       // as in that section we might mutate the lastNode.
906       const firstElement = $isElementNode(firstNode)
907         ? firstNode
908         : firstNode.getParentOrThrow();
909       let lastElement = $isElementNode(lastNode)
910         ? lastNode
911         : lastNode.getParentOrThrow();
912       let lastElementChild = lastNode;
913
914       // If the last element is inline, we should instead look at getting
915       // the nodes of its parent, rather than itself. This behavior will
916       // then better match how text node insertions work. We will need to
917       // also update the last element's child accordingly as we do this.
918       if (!firstElement.is(lastElement) && lastElement.isInline()) {
919         // Keep traversing till we have a non-inline element parent.
920         do {
921           lastElementChild = lastElement;
922           lastElement = lastElement.getParentOrThrow();
923         } while (lastElement.isInline());
924       }
925
926       // Handle mutations to the last node.
927       if (
928         (endPoint.type === 'text' &&
929           (endOffset !== 0 || lastNode.getTextContent() === '')) ||
930         (endPoint.type === 'element' &&
931           lastNode.getIndexWithinParent() < endOffset)
932       ) {
933         if (
934           $isTextNode(lastNode) &&
935           !lastNode.isToken() &&
936           endOffset !== lastNode.getTextContentSize()
937         ) {
938           if (lastNode.isSegmented()) {
939             const textNode = $createTextNode(lastNode.getTextContent());
940             lastNode.replace(textNode);
941             lastNode = textNode;
942           }
943           // root node selections only select whole nodes, so no text splice is necessary
944           if (!$isRootNode(endPoint.getNode()) && endPoint.type === 'text') {
945             lastNode = (lastNode as TextNode).spliceText(0, endOffset, '');
946           }
947           markedNodeKeysForKeep.add(lastNode.__key);
948         } else {
949           const lastNodeParent = lastNode.getParentOrThrow();
950           if (
951             !lastNodeParent.canBeEmpty() &&
952             lastNodeParent.getChildrenSize() === 1
953           ) {
954             lastNodeParent.remove();
955           } else {
956             lastNode.remove();
957           }
958         }
959       } else {
960         markedNodeKeysForKeep.add(lastNode.__key);
961       }
962
963       // Either move the remaining nodes of the last parent to after
964       // the first child, or remove them entirely. If the last parent
965       // is the same as the first parent, this logic also works.
966       const lastNodeChildren = lastElement.getChildren();
967       const selectedNodesSet = new Set(selectedNodes);
968       const firstAndLastElementsAreEqual = firstElement.is(lastElement);
969
970       // We choose a target to insert all nodes after. In the case of having
971       // and inline starting parent element with a starting node that has no
972       // siblings, we should insert after the starting parent element, otherwise
973       // we will incorrectly merge into the starting parent element.
974       // TODO: should we keep on traversing parents if we're inside another
975       // nested inline element?
976       const insertionTarget =
977         firstElement.isInline() && firstNode.getNextSibling() === null
978           ? firstElement
979           : firstNode;
980
981       for (let i = lastNodeChildren.length - 1; i >= 0; i--) {
982         const lastNodeChild = lastNodeChildren[i];
983
984         if (
985           lastNodeChild.is(firstNode) ||
986           ($isElementNode(lastNodeChild) && lastNodeChild.isParentOf(firstNode))
987         ) {
988           break;
989         }
990
991         if (lastNodeChild.isAttached()) {
992           if (
993             !selectedNodesSet.has(lastNodeChild) ||
994             lastNodeChild.is(lastElementChild)
995           ) {
996             if (!firstAndLastElementsAreEqual) {
997               insertionTarget.insertAfter(lastNodeChild, false);
998             }
999           } else {
1000             lastNodeChild.remove();
1001           }
1002         }
1003       }
1004
1005       if (!firstAndLastElementsAreEqual) {
1006         // Check if we have already moved out all the nodes of the
1007         // last parent, and if so, traverse the parent tree and mark
1008         // them all as being able to deleted too.
1009         let parent: ElementNode | null = lastElement;
1010         let lastRemovedParent = null;
1011
1012         while (parent !== null) {
1013           const children = parent.getChildren();
1014           const childrenLength = children.length;
1015           if (
1016             childrenLength === 0 ||
1017             children[childrenLength - 1].is(lastRemovedParent)
1018           ) {
1019             markedNodeKeysForKeep.delete(parent.__key);
1020             lastRemovedParent = parent;
1021           }
1022           parent = parent.getParent();
1023         }
1024       }
1025
1026       // Ensure we do splicing after moving of nodes, as splicing
1027       // can have side-effects (in the case of hashtags).
1028       if (!firstNode.isToken()) {
1029         firstNode = firstNode.spliceText(
1030           startOffset,
1031           firstNodeTextLength - startOffset,
1032           text,
1033           true,
1034         );
1035         if (firstNode.getTextContent() === '') {
1036           firstNode.remove();
1037         } else if (firstNode.isComposing() && this.anchor.type === 'text') {
1038           // When composing, we need to adjust the anchor offset so that
1039           // we correctly replace that right range.
1040           this.anchor.offset -= text.length;
1041         }
1042       } else if (startOffset === firstNodeTextLength) {
1043         firstNode.select();
1044       } else {
1045         const textNode = $createTextNode(text);
1046         textNode.select();
1047         firstNode.replace(textNode);
1048       }
1049
1050       // Remove all selected nodes that haven't already been removed.
1051       for (let i = 1; i < selectedNodesLength; i++) {
1052         const selectedNode = selectedNodes[i];
1053         const key = selectedNode.__key;
1054         if (!markedNodeKeysForKeep.has(key)) {
1055           selectedNode.remove();
1056         }
1057       }
1058     }
1059   }
1060
1061   /**
1062    * Removes the text in the Selection, adjusting the EditorState accordingly.
1063    */
1064   removeText(): void {
1065     this.insertText('');
1066   }
1067
1068   /**
1069    * Applies the provided format to the TextNodes in the Selection, splitting or
1070    * merging nodes as necessary.
1071    *
1072    * @param formatType the format type to apply to the nodes in the Selection.
1073    */
1074   formatText(formatType: TextFormatType): void {
1075     if (this.isCollapsed()) {
1076       this.toggleFormat(formatType);
1077       // When changing format, we should stop composition
1078       $setCompositionKey(null);
1079       return;
1080     }
1081
1082     const selectedNodes = this.getNodes();
1083     const selectedTextNodes: Array<TextNode> = [];
1084     for (const selectedNode of selectedNodes) {
1085       if ($isTextNode(selectedNode)) {
1086         selectedTextNodes.push(selectedNode);
1087       }
1088     }
1089
1090     const selectedTextNodesLength = selectedTextNodes.length;
1091     if (selectedTextNodesLength === 0) {
1092       this.toggleFormat(formatType);
1093       // When changing format, we should stop composition
1094       $setCompositionKey(null);
1095       return;
1096     }
1097
1098     const anchor = this.anchor;
1099     const focus = this.focus;
1100     const isBackward = this.isBackward();
1101     const startPoint = isBackward ? focus : anchor;
1102     const endPoint = isBackward ? anchor : focus;
1103
1104     let firstIndex = 0;
1105     let firstNode = selectedTextNodes[0];
1106     let startOffset = startPoint.type === 'element' ? 0 : startPoint.offset;
1107
1108     // In case selection started at the end of text node use next text node
1109     if (
1110       startPoint.type === 'text' &&
1111       startOffset === firstNode.getTextContentSize()
1112     ) {
1113       firstIndex = 1;
1114       firstNode = selectedTextNodes[1];
1115       startOffset = 0;
1116     }
1117
1118     if (firstNode == null) {
1119       return;
1120     }
1121
1122     const firstNextFormat = firstNode.getFormatFlags(formatType, null);
1123
1124     const lastIndex = selectedTextNodesLength - 1;
1125     let lastNode = selectedTextNodes[lastIndex];
1126     const endOffset =
1127       endPoint.type === 'text'
1128         ? endPoint.offset
1129         : lastNode.getTextContentSize();
1130
1131     // Single node selected
1132     if (firstNode.is(lastNode)) {
1133       // No actual text is selected, so do nothing.
1134       if (startOffset === endOffset) {
1135         return;
1136       }
1137       // The entire node is selected or it is token, so just format it
1138       if (
1139         $isTokenOrSegmented(firstNode) ||
1140         (startOffset === 0 && endOffset === firstNode.getTextContentSize())
1141       ) {
1142         firstNode.setFormat(firstNextFormat);
1143       } else {
1144         // Node is partially selected, so split it into two nodes
1145         // add style the selected one.
1146         const splitNodes = firstNode.splitText(startOffset, endOffset);
1147         const replacement = startOffset === 0 ? splitNodes[0] : splitNodes[1];
1148         replacement.setFormat(firstNextFormat);
1149
1150         // Update selection only if starts/ends on text node
1151         if (startPoint.type === 'text') {
1152           startPoint.set(replacement.__key, 0, 'text');
1153         }
1154         if (endPoint.type === 'text') {
1155           endPoint.set(replacement.__key, endOffset - startOffset, 'text');
1156         }
1157       }
1158
1159       this.format = firstNextFormat;
1160
1161       return;
1162     }
1163     // Multiple nodes selected
1164     // The entire first node isn't selected, so split it
1165     if (startOffset !== 0 && !$isTokenOrSegmented(firstNode)) {
1166       [, firstNode as TextNode] = firstNode.splitText(startOffset);
1167       startOffset = 0;
1168     }
1169     firstNode.setFormat(firstNextFormat);
1170
1171     const lastNextFormat = lastNode.getFormatFlags(formatType, firstNextFormat);
1172     // If the offset is 0, it means no actual characters are selected,
1173     // so we skip formatting the last node altogether.
1174     if (endOffset > 0) {
1175       if (
1176         endOffset !== lastNode.getTextContentSize() &&
1177         !$isTokenOrSegmented(lastNode)
1178       ) {
1179         [lastNode as TextNode] = lastNode.splitText(endOffset);
1180       }
1181       lastNode.setFormat(lastNextFormat);
1182     }
1183
1184     // Process all text nodes in between
1185     for (let i = firstIndex + 1; i < lastIndex; i++) {
1186       const textNode = selectedTextNodes[i];
1187       const nextFormat = textNode.getFormatFlags(formatType, lastNextFormat);
1188       textNode.setFormat(nextFormat);
1189     }
1190
1191     // Update selection only if starts/ends on text node
1192     if (startPoint.type === 'text') {
1193       startPoint.set(firstNode.__key, startOffset, 'text');
1194     }
1195     if (endPoint.type === 'text') {
1196       endPoint.set(lastNode.__key, endOffset, 'text');
1197     }
1198
1199     this.format = firstNextFormat | lastNextFormat;
1200   }
1201
1202   /**
1203    * Attempts to "intelligently" insert an arbitrary list of Lexical nodes into the EditorState at the
1204    * current Selection according to a set of heuristics that determine how surrounding nodes
1205    * should be changed, replaced, or moved to accomodate the incoming ones.
1206    *
1207    * @param nodes - the nodes to insert
1208    */
1209   insertNodes(nodes: Array<LexicalNode>): void {
1210     if (nodes.length === 0) {
1211       return;
1212     }
1213     if (this.anchor.key === 'root') {
1214       this.insertParagraph();
1215       const selection = $getSelection();
1216       invariant(
1217         $isRangeSelection(selection),
1218         'Expected RangeSelection after insertParagraph',
1219       );
1220       return selection.insertNodes(nodes);
1221     }
1222
1223     const firstPoint = this.isBackward() ? this.focus : this.anchor;
1224     const firstBlock = $getAncestor(firstPoint.getNode(), INTERNAL_$isBlock)!;
1225
1226     const last = nodes[nodes.length - 1]!;
1227
1228     // CASE 1: insert inside a code block
1229     if ('__language' in firstBlock && $isElementNode(firstBlock)) {
1230       if ('__language' in nodes[0]) {
1231         this.insertText(nodes[0].getTextContent());
1232       } else {
1233         const index = $removeTextAndSplitBlock(this);
1234         firstBlock.splice(index, 0, nodes);
1235         last.selectEnd();
1236       }
1237       return;
1238     }
1239
1240     // CASE 2: All elements of the array are inline
1241     const notInline = (node: LexicalNode) =>
1242       ($isElementNode(node) || $isDecoratorNode(node)) && !node.isInline();
1243
1244     if (!nodes.some(notInline)) {
1245       invariant(
1246         $isElementNode(firstBlock),
1247         "Expected 'firstBlock' to be an ElementNode",
1248       );
1249       const index = $removeTextAndSplitBlock(this);
1250       firstBlock.splice(index, 0, nodes);
1251       last.selectEnd();
1252       return;
1253     }
1254
1255     // CASE 3: At least 1 element of the array is not inline
1256     const blocksParent = $wrapInlineNodes(nodes);
1257     const nodeToSelect = blocksParent.getLastDescendant()!;
1258     const blocks = blocksParent.getChildren();
1259     const isMergeable = (node: LexicalNode): node is ElementNode =>
1260       $isElementNode(node) &&
1261       INTERNAL_$isBlock(node) &&
1262       !node.isEmpty() &&
1263       $isElementNode(firstBlock) &&
1264       (!firstBlock.isEmpty() || firstBlock.canMergeWhenEmpty());
1265
1266     const shouldInsert = !$isElementNode(firstBlock) || !firstBlock.isEmpty();
1267     const insertedParagraph = shouldInsert ? this.insertParagraph() : null;
1268     const lastToInsert = blocks[blocks.length - 1];
1269     let firstToInsert = blocks[0];
1270     if (isMergeable(firstToInsert)) {
1271       invariant(
1272         $isElementNode(firstBlock),
1273         "Expected 'firstBlock' to be an ElementNode",
1274       );
1275       firstBlock.append(...firstToInsert.getChildren());
1276       firstToInsert = blocks[1];
1277     }
1278     if (firstToInsert) {
1279       insertRangeAfter(firstBlock, firstToInsert);
1280     }
1281     const lastInsertedBlock = $getAncestor(nodeToSelect, INTERNAL_$isBlock)!;
1282
1283     if (
1284       insertedParagraph &&
1285       $isElementNode(lastInsertedBlock) &&
1286       (insertedParagraph.canMergeWhenEmpty() || INTERNAL_$isBlock(lastToInsert))
1287     ) {
1288       lastInsertedBlock.append(...insertedParagraph.getChildren());
1289       insertedParagraph.remove();
1290     }
1291     if ($isElementNode(firstBlock) && firstBlock.isEmpty()) {
1292       firstBlock.remove();
1293     }
1294
1295     nodeToSelect.selectEnd();
1296
1297     // To understand this take a look at the test "can wrap post-linebreak nodes into new element"
1298     const lastChild = $isElementNode(firstBlock)
1299       ? firstBlock.getLastChild()
1300       : null;
1301     if ($isLineBreakNode(lastChild) && lastInsertedBlock !== firstBlock) {
1302       lastChild.remove();
1303     }
1304   }
1305
1306   /**
1307    * Inserts a new ParagraphNode into the EditorState at the current Selection
1308    *
1309    * @returns the newly inserted node.
1310    */
1311   insertParagraph(): ElementNode | null {
1312     if (this.anchor.key === 'root') {
1313       const paragraph = $createParagraphNode();
1314       $getRoot().splice(this.anchor.offset, 0, [paragraph]);
1315       paragraph.select();
1316       return paragraph;
1317     }
1318     const index = $removeTextAndSplitBlock(this);
1319     const block = $getAncestor(this.anchor.getNode(), INTERNAL_$isBlock)!;
1320     invariant($isElementNode(block), 'Expected ancestor to be an ElementNode');
1321     const firstToAppend = block.getChildAtIndex(index);
1322     const nodesToInsert = firstToAppend
1323       ? [firstToAppend, ...firstToAppend.getNextSiblings()]
1324       : [];
1325     const newBlock = block.insertNewAfter(this, false) as ElementNode | null;
1326     if (newBlock) {
1327       newBlock.append(...nodesToInsert);
1328       newBlock.selectStart();
1329       return newBlock;
1330     }
1331     // if newBlock is null, it means that block is of type CodeNode.
1332     return null;
1333   }
1334
1335   /**
1336    * Inserts a logical linebreak, which may be a new LineBreakNode or a new ParagraphNode, into the EditorState at the
1337    * current Selection.
1338    */
1339   insertLineBreak(selectStart?: boolean): void {
1340     const lineBreak = $createLineBreakNode();
1341     this.insertNodes([lineBreak]);
1342     // this is used in MacOS with the command 'ctrl-O' (openLineBreak)
1343     if (selectStart) {
1344       const parent = lineBreak.getParentOrThrow();
1345       const index = lineBreak.getIndexWithinParent();
1346       parent.select(index, index);
1347     }
1348   }
1349
1350   /**
1351    * Extracts the nodes in the Selection, splitting nodes where necessary
1352    * to get offset-level precision.
1353    *
1354    * @returns The nodes in the Selection
1355    */
1356   extract(): Array<LexicalNode> {
1357     const selectedNodes = this.getNodes();
1358     const selectedNodesLength = selectedNodes.length;
1359     const lastIndex = selectedNodesLength - 1;
1360     const anchor = this.anchor;
1361     const focus = this.focus;
1362     let firstNode = selectedNodes[0];
1363     let lastNode = selectedNodes[lastIndex];
1364     const [anchorOffset, focusOffset] = $getCharacterOffsets(this);
1365
1366     if (selectedNodesLength === 0) {
1367       return [];
1368     } else if (selectedNodesLength === 1) {
1369       if ($isTextNode(firstNode) && !this.isCollapsed()) {
1370         const startOffset =
1371           anchorOffset > focusOffset ? focusOffset : anchorOffset;
1372         const endOffset =
1373           anchorOffset > focusOffset ? anchorOffset : focusOffset;
1374         const splitNodes = firstNode.splitText(startOffset, endOffset);
1375         const node = startOffset === 0 ? splitNodes[0] : splitNodes[1];
1376         return node != null ? [node] : [];
1377       }
1378       return [firstNode];
1379     }
1380     const isBefore = anchor.isBefore(focus);
1381
1382     if ($isTextNode(firstNode)) {
1383       const startOffset = isBefore ? anchorOffset : focusOffset;
1384       if (startOffset === firstNode.getTextContentSize()) {
1385         selectedNodes.shift();
1386       } else if (startOffset !== 0) {
1387         [, firstNode] = firstNode.splitText(startOffset);
1388         selectedNodes[0] = firstNode;
1389       }
1390     }
1391     if ($isTextNode(lastNode)) {
1392       const lastNodeText = lastNode.getTextContent();
1393       const lastNodeTextLength = lastNodeText.length;
1394       const endOffset = isBefore ? focusOffset : anchorOffset;
1395       if (endOffset === 0) {
1396         selectedNodes.pop();
1397       } else if (endOffset !== lastNodeTextLength) {
1398         [lastNode] = lastNode.splitText(endOffset);
1399         selectedNodes[lastIndex] = lastNode;
1400       }
1401     }
1402     return selectedNodes;
1403   }
1404
1405   /**
1406    * Modifies the Selection according to the parameters and a set of heuristics that account for
1407    * various node types. Can be used to safely move or extend selection by one logical "unit" without
1408    * dealing explicitly with all the possible node types.
1409    *
1410    * @param alter the type of modification to perform
1411    * @param isBackward whether or not selection is backwards
1412    * @param granularity the granularity at which to apply the modification
1413    */
1414   modify(
1415     alter: 'move' | 'extend',
1416     isBackward: boolean,
1417     granularity: 'character' | 'word' | 'lineboundary',
1418   ): void {
1419     const focus = this.focus;
1420     const anchor = this.anchor;
1421     const collapse = alter === 'move';
1422
1423     // Handle the selection movement around decorators.
1424     const possibleNode = $getAdjacentNode(focus, isBackward);
1425     if ($isDecoratorNode(possibleNode) && !possibleNode.isIsolated()) {
1426       // Make it possible to move selection from range selection to
1427       // node selection on the node.
1428       if (collapse && possibleNode.isKeyboardSelectable()) {
1429         const nodeSelection = $createNodeSelection();
1430         nodeSelection.add(possibleNode.__key);
1431         $setSelection(nodeSelection);
1432         return;
1433       }
1434       const sibling = isBackward
1435         ? possibleNode.getPreviousSibling()
1436         : possibleNode.getNextSibling();
1437
1438       if (!$isTextNode(sibling)) {
1439         const parent = possibleNode.getParentOrThrow();
1440         let offset;
1441         let elementKey;
1442
1443         if ($isElementNode(sibling)) {
1444           elementKey = sibling.__key;
1445           offset = isBackward ? sibling.getChildrenSize() : 0;
1446         } else {
1447           offset = possibleNode.getIndexWithinParent();
1448           elementKey = parent.__key;
1449           if (!isBackward) {
1450             offset++;
1451           }
1452         }
1453         focus.set(elementKey, offset, 'element');
1454         if (collapse) {
1455           anchor.set(elementKey, offset, 'element');
1456         }
1457         return;
1458       } else {
1459         const siblingKey = sibling.__key;
1460         const offset = isBackward ? sibling.getTextContent().length : 0;
1461         focus.set(siblingKey, offset, 'text');
1462         if (collapse) {
1463           anchor.set(siblingKey, offset, 'text');
1464         }
1465         return;
1466       }
1467     }
1468     const editor = getActiveEditor();
1469     const domSelection = getDOMSelection(editor._window);
1470
1471     if (!domSelection) {
1472       return;
1473     }
1474     const blockCursorElement = editor._blockCursorElement;
1475     const rootElement = editor._rootElement;
1476     // Remove the block cursor element if it exists. This will ensure selection
1477     // works as intended. If we leave it in the DOM all sorts of strange bugs
1478     // occur. :/
1479     if (
1480       rootElement !== null &&
1481       blockCursorElement !== null &&
1482       $isElementNode(possibleNode) &&
1483       !possibleNode.isInline() &&
1484       !possibleNode.canBeEmpty()
1485     ) {
1486       removeDOMBlockCursorElement(blockCursorElement, editor, rootElement);
1487     }
1488     // We use the DOM selection.modify API here to "tell" us what the selection
1489     // will be. We then use it to update the Lexical selection accordingly. This
1490     // is much more reliable than waiting for a beforeinput and using the ranges
1491     // from getTargetRanges(), and is also better than trying to do it ourselves
1492     // using Intl.Segmenter or other workarounds that struggle with word segments
1493     // and line segments (especially with word wrapping and non-Roman languages).
1494     moveNativeSelection(
1495       domSelection,
1496       alter,
1497       isBackward ? 'backward' : 'forward',
1498       granularity,
1499     );
1500     // Guard against no ranges
1501     if (domSelection.rangeCount > 0) {
1502       const range = domSelection.getRangeAt(0);
1503       // Apply the DOM selection to our Lexical selection.
1504       const anchorNode = this.anchor.getNode();
1505       const root = $isRootNode(anchorNode)
1506         ? anchorNode
1507         : $getNearestRootOrShadowRoot(anchorNode);
1508       this.applyDOMRange(range);
1509       this.dirty = true;
1510       if (!collapse) {
1511         // Validate selection; make sure that the new extended selection respects shadow roots
1512         const nodes = this.getNodes();
1513         const validNodes = [];
1514         let shrinkSelection = false;
1515         for (let i = 0; i < nodes.length; i++) {
1516           const nextNode = nodes[i];
1517           if ($hasAncestor(nextNode, root)) {
1518             validNodes.push(nextNode);
1519           } else {
1520             shrinkSelection = true;
1521           }
1522         }
1523         if (shrinkSelection && validNodes.length > 0) {
1524           // validNodes length check is a safeguard against an invalid selection; as getNodes()
1525           // will return an empty array in this case
1526           if (isBackward) {
1527             const firstValidNode = validNodes[0];
1528             if ($isElementNode(firstValidNode)) {
1529               firstValidNode.selectStart();
1530             } else {
1531               firstValidNode.getParentOrThrow().selectStart();
1532             }
1533           } else {
1534             const lastValidNode = validNodes[validNodes.length - 1];
1535             if ($isElementNode(lastValidNode)) {
1536               lastValidNode.selectEnd();
1537             } else {
1538               lastValidNode.getParentOrThrow().selectEnd();
1539             }
1540           }
1541         }
1542
1543         // Because a range works on start and end, we might need to flip
1544         // the anchor and focus points to match what the DOM has, not what
1545         // the range has specifically.
1546         if (
1547           domSelection.anchorNode !== range.startContainer ||
1548           domSelection.anchorOffset !== range.startOffset
1549         ) {
1550           $swapPoints(this);
1551         }
1552       }
1553     }
1554   }
1555   /**
1556    * Helper for handling forward character and word deletion that prevents element nodes
1557    * like a table, columns layout being destroyed
1558    *
1559    * @param anchor the anchor
1560    * @param anchorNode the anchor node in the selection
1561    * @param isBackward whether or not selection is backwards
1562    */
1563   forwardDeletion(
1564     anchor: PointType,
1565     anchorNode: TextNode | ElementNode,
1566     isBackward: boolean,
1567   ): boolean {
1568     if (
1569       !isBackward &&
1570       // Delete forward handle case
1571       ((anchor.type === 'element' &&
1572         $isElementNode(anchorNode) &&
1573         anchor.offset === anchorNode.getChildrenSize()) ||
1574         (anchor.type === 'text' &&
1575           anchor.offset === anchorNode.getTextContentSize()))
1576     ) {
1577       const parent = anchorNode.getParent();
1578       const nextSibling =
1579         anchorNode.getNextSibling() ||
1580         (parent === null ? null : parent.getNextSibling());
1581
1582       if ($isElementNode(nextSibling) && nextSibling.isShadowRoot()) {
1583         return true;
1584       }
1585     }
1586     return false;
1587   }
1588
1589   /**
1590    * Performs one logical character deletion operation on the EditorState based on the current Selection.
1591    * Handles different node types.
1592    *
1593    * @param isBackward whether or not the selection is backwards.
1594    */
1595   deleteCharacter(isBackward: boolean): void {
1596     const wasCollapsed = this.isCollapsed();
1597     if (this.isCollapsed()) {
1598       const anchor = this.anchor;
1599       let anchorNode: TextNode | ElementNode | null = anchor.getNode();
1600       if (this.forwardDeletion(anchor, anchorNode, isBackward)) {
1601         return;
1602       }
1603
1604       // Handle the deletion around decorators.
1605       const focus = this.focus;
1606       const possibleNode = $getAdjacentNode(focus, isBackward);
1607       if ($isDecoratorNode(possibleNode) && !possibleNode.isIsolated()) {
1608         // Make it possible to move selection from range selection to
1609         // node selection on the node.
1610         if (
1611           possibleNode.isKeyboardSelectable() &&
1612           $isElementNode(anchorNode) &&
1613           anchorNode.getChildrenSize() === 0
1614         ) {
1615           anchorNode.remove();
1616           const nodeSelection = $createNodeSelection();
1617           nodeSelection.add(possibleNode.__key);
1618           $setSelection(nodeSelection);
1619         } else {
1620           possibleNode.remove();
1621           const editor = getActiveEditor();
1622           editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
1623         }
1624         return;
1625       } else if (
1626         !isBackward &&
1627         $isElementNode(possibleNode) &&
1628         $isElementNode(anchorNode) &&
1629         anchorNode.isEmpty()
1630       ) {
1631         anchorNode.remove();
1632         possibleNode.selectStart();
1633         return;
1634       }
1635       this.modify('extend', isBackward, 'character');
1636
1637       if (!this.isCollapsed()) {
1638         const focusNode = focus.type === 'text' ? focus.getNode() : null;
1639         anchorNode = anchor.type === 'text' ? anchor.getNode() : null;
1640
1641         if (focusNode !== null && focusNode.isSegmented()) {
1642           const offset = focus.offset;
1643           const textContentSize = focusNode.getTextContentSize();
1644           if (
1645             focusNode.is(anchorNode) ||
1646             (isBackward && offset !== textContentSize) ||
1647             (!isBackward && offset !== 0)
1648           ) {
1649             $removeSegment(focusNode, isBackward, offset);
1650             return;
1651           }
1652         } else if (anchorNode !== null && anchorNode.isSegmented()) {
1653           const offset = anchor.offset;
1654           const textContentSize = anchorNode.getTextContentSize();
1655           if (
1656             anchorNode.is(focusNode) ||
1657             (isBackward && offset !== 0) ||
1658             (!isBackward && offset !== textContentSize)
1659           ) {
1660             $removeSegment(anchorNode, isBackward, offset);
1661             return;
1662           }
1663         }
1664         $updateCaretSelectionForUnicodeCharacter(this, isBackward);
1665       } else if (isBackward && anchor.offset === 0) {
1666         // Special handling around rich text nodes
1667         const element =
1668           anchor.type === 'element'
1669             ? anchor.getNode()
1670             : anchor.getNode().getParentOrThrow();
1671         if (element.collapseAtStart(this)) {
1672           return;
1673         }
1674       }
1675     }
1676     this.removeText();
1677     if (
1678       isBackward &&
1679       !wasCollapsed &&
1680       this.isCollapsed() &&
1681       this.anchor.type === 'element' &&
1682       this.anchor.offset === 0
1683     ) {
1684       const anchorNode = this.anchor.getNode();
1685       if (
1686         anchorNode.isEmpty() &&
1687         $isRootNode(anchorNode.getParent()) &&
1688         anchorNode.getIndexWithinParent() === 0
1689       ) {
1690         anchorNode.collapseAtStart(this);
1691       }
1692     }
1693   }
1694
1695   /**
1696    * Performs one logical line deletion operation on the EditorState based on the current Selection.
1697    * Handles different node types.
1698    *
1699    * @param isBackward whether or not the selection is backwards.
1700    */
1701   deleteLine(isBackward: boolean): void {
1702     if (this.isCollapsed()) {
1703       // Since `domSelection.modify('extend', ..., 'lineboundary')` works well for text selections
1704       // but doesn't properly handle selections which end on elements, a space character is added
1705       // for such selections transforming their anchor's type to 'text'
1706       const anchorIsElement = this.anchor.type === 'element';
1707       if (anchorIsElement) {
1708         this.insertText(' ');
1709       }
1710
1711       this.modify('extend', isBackward, 'lineboundary');
1712
1713       // If selection is extended to cover text edge then extend it one character more
1714       // to delete its parent element. Otherwise text content will be deleted but empty
1715       // parent node will remain
1716       const endPoint = isBackward ? this.focus : this.anchor;
1717       if (endPoint.offset === 0) {
1718         this.modify('extend', isBackward, 'character');
1719       }
1720
1721       // Adjusts selection to include an extra character added for element anchors to remove it
1722       if (anchorIsElement) {
1723         const startPoint = isBackward ? this.anchor : this.focus;
1724         startPoint.set(startPoint.key, startPoint.offset + 1, startPoint.type);
1725       }
1726     }
1727     this.removeText();
1728   }
1729
1730   /**
1731    * Performs one logical word deletion operation on the EditorState based on the current Selection.
1732    * Handles different node types.
1733    *
1734    * @param isBackward whether or not the selection is backwards.
1735    */
1736   deleteWord(isBackward: boolean): void {
1737     if (this.isCollapsed()) {
1738       const anchor = this.anchor;
1739       const anchorNode: TextNode | ElementNode | null = anchor.getNode();
1740       if (this.forwardDeletion(anchor, anchorNode, isBackward)) {
1741         return;
1742       }
1743       this.modify('extend', isBackward, 'word');
1744     }
1745     this.removeText();
1746   }
1747
1748   /**
1749    * Returns whether the Selection is "backwards", meaning the focus
1750    * logically precedes the anchor in the EditorState.
1751    * @returns true if the Selection is backwards, false otherwise.
1752    */
1753   isBackward(): boolean {
1754     return this.focus.isBefore(this.anchor);
1755   }
1756
1757   getStartEndPoints(): null | [PointType, PointType] {
1758     return [this.anchor, this.focus];
1759   }
1760 }
1761
1762 export function $isNodeSelection(x: unknown): x is NodeSelection {
1763   return x instanceof NodeSelection;
1764 }
1765
1766 function getCharacterOffset(point: PointType): number {
1767   const offset = point.offset;
1768   if (point.type === 'text') {
1769     return offset;
1770   }
1771
1772   const parent = point.getNode();
1773   return offset === parent.getChildrenSize()
1774     ? parent.getTextContent().length
1775     : 0;
1776 }
1777
1778 export function $getCharacterOffsets(
1779   selection: BaseSelection,
1780 ): [number, number] {
1781   const anchorAndFocus = selection.getStartEndPoints();
1782   if (anchorAndFocus === null) {
1783     return [0, 0];
1784   }
1785   const [anchor, focus] = anchorAndFocus;
1786   if (
1787     anchor.type === 'element' &&
1788     focus.type === 'element' &&
1789     anchor.key === focus.key &&
1790     anchor.offset === focus.offset
1791   ) {
1792     return [0, 0];
1793   }
1794   return [getCharacterOffset(anchor), getCharacterOffset(focus)];
1795 }
1796
1797 function $swapPoints(selection: RangeSelection): void {
1798   const focus = selection.focus;
1799   const anchor = selection.anchor;
1800   const anchorKey = anchor.key;
1801   const anchorOffset = anchor.offset;
1802   const anchorType = anchor.type;
1803
1804   $setPointValues(anchor, focus.key, focus.offset, focus.type);
1805   $setPointValues(focus, anchorKey, anchorOffset, anchorType);
1806   selection._cachedNodes = null;
1807 }
1808
1809 function moveNativeSelection(
1810   domSelection: Selection,
1811   alter: 'move' | 'extend',
1812   direction: 'backward' | 'forward' | 'left' | 'right',
1813   granularity: 'character' | 'word' | 'lineboundary',
1814 ): void {
1815   // Selection.modify() method applies a change to the current selection or cursor position,
1816   // but is still non-standard in some browsers.
1817   domSelection.modify(alter, direction, granularity);
1818 }
1819
1820 function $updateCaretSelectionForUnicodeCharacter(
1821   selection: RangeSelection,
1822   isBackward: boolean,
1823 ): void {
1824   const anchor = selection.anchor;
1825   const focus = selection.focus;
1826   const anchorNode = anchor.getNode();
1827   const focusNode = focus.getNode();
1828
1829   if (
1830     anchorNode === focusNode &&
1831     anchor.type === 'text' &&
1832     focus.type === 'text'
1833   ) {
1834     // Handling of multibyte characters
1835     const anchorOffset = anchor.offset;
1836     const focusOffset = focus.offset;
1837     const isBefore = anchorOffset < focusOffset;
1838     const startOffset = isBefore ? anchorOffset : focusOffset;
1839     const endOffset = isBefore ? focusOffset : anchorOffset;
1840     const characterOffset = endOffset - 1;
1841
1842     if (startOffset !== characterOffset) {
1843       const text = anchorNode.getTextContent().slice(startOffset, endOffset);
1844       if (!doesContainGrapheme(text)) {
1845         if (isBackward) {
1846           focus.offset = characterOffset;
1847         } else {
1848           anchor.offset = characterOffset;
1849         }
1850       }
1851     }
1852   } else {
1853     // TODO Handling of multibyte characters
1854   }
1855 }
1856
1857 function $removeSegment(
1858   node: TextNode,
1859   isBackward: boolean,
1860   offset: number,
1861 ): void {
1862   const textNode = node;
1863   const textContent = textNode.getTextContent();
1864   const split = textContent.split(/(?=\s)/g);
1865   const splitLength = split.length;
1866   let segmentOffset = 0;
1867   let restoreOffset: number | undefined = 0;
1868
1869   for (let i = 0; i < splitLength; i++) {
1870     const text = split[i];
1871     const isLast = i === splitLength - 1;
1872     restoreOffset = segmentOffset;
1873     segmentOffset += text.length;
1874
1875     if (
1876       (isBackward && segmentOffset === offset) ||
1877       segmentOffset > offset ||
1878       isLast
1879     ) {
1880       split.splice(i, 1);
1881       if (isLast) {
1882         restoreOffset = undefined;
1883       }
1884       break;
1885     }
1886   }
1887   const nextTextContent = split.join('').trim();
1888
1889   if (nextTextContent === '') {
1890     textNode.remove();
1891   } else {
1892     textNode.setTextContent(nextTextContent);
1893     textNode.select(restoreOffset, restoreOffset);
1894   }
1895 }
1896
1897 function shouldResolveAncestor(
1898   resolvedElement: ElementNode,
1899   resolvedOffset: number,
1900   lastPoint: null | PointType,
1901 ): boolean {
1902   const parent = resolvedElement.getParent();
1903   return (
1904     lastPoint === null ||
1905     parent === null ||
1906     !parent.canBeEmpty() ||
1907     parent !== lastPoint.getNode()
1908   );
1909 }
1910
1911 function $internalResolveSelectionPoint(
1912   dom: Node,
1913   offset: number,
1914   lastPoint: null | PointType,
1915   editor: LexicalEditor,
1916 ): null | PointType {
1917   let resolvedOffset = offset;
1918   let resolvedNode: TextNode | LexicalNode | null;
1919   // If we have selection on an element, we will
1920   // need to figure out (using the offset) what text
1921   // node should be selected.
1922
1923   if (dom.nodeType === DOM_ELEMENT_TYPE) {
1924     // Resolve element to a ElementNode, or TextNode, or null
1925     let moveSelectionToEnd = false;
1926     // Given we're moving selection to another node, selection is
1927     // definitely dirty.
1928     // We use the anchor to find which child node to select
1929     const childNodes = dom.childNodes;
1930     const childNodesLength = childNodes.length;
1931     const blockCursorElement = editor._blockCursorElement;
1932     // If the anchor is the same as length, then this means we
1933     // need to select the very last text node.
1934     if (resolvedOffset === childNodesLength) {
1935       moveSelectionToEnd = true;
1936       resolvedOffset = childNodesLength - 1;
1937     }
1938     let childDOM = childNodes[resolvedOffset];
1939     let hasBlockCursor = false;
1940     if (childDOM === blockCursorElement) {
1941       childDOM = childNodes[resolvedOffset + 1];
1942       hasBlockCursor = true;
1943     } else if (blockCursorElement !== null) {
1944       const blockCursorElementParent = blockCursorElement.parentNode;
1945       if (dom === blockCursorElementParent) {
1946         const blockCursorOffset = Array.prototype.indexOf.call(
1947           blockCursorElementParent.children,
1948           blockCursorElement,
1949         );
1950         if (offset > blockCursorOffset) {
1951           resolvedOffset--;
1952         }
1953       }
1954     }
1955     resolvedNode = $getNodeFromDOM(childDOM);
1956
1957     if ($isTextNode(resolvedNode)) {
1958       resolvedOffset = getTextNodeOffset(resolvedNode, moveSelectionToEnd);
1959     } else {
1960       let resolvedElement = $getNodeFromDOM(dom);
1961       // Ensure resolvedElement is actually a element.
1962       if (resolvedElement === null) {
1963         return null;
1964       }
1965       if ($isElementNode(resolvedElement)) {
1966         resolvedOffset = Math.min(
1967           resolvedElement.getChildrenSize(),
1968           resolvedOffset,
1969         );
1970         let child = resolvedElement.getChildAtIndex(resolvedOffset);
1971         if (
1972           $isElementNode(child) &&
1973           shouldResolveAncestor(child, resolvedOffset, lastPoint)
1974         ) {
1975           const descendant = moveSelectionToEnd
1976             ? child.getLastDescendant()
1977             : child.getFirstDescendant();
1978           if (descendant === null) {
1979             resolvedElement = child;
1980           } else {
1981             child = descendant;
1982             resolvedElement = $isElementNode(child)
1983               ? child
1984               : child.getParentOrThrow();
1985           }
1986           resolvedOffset = 0;
1987         }
1988         if ($isTextNode(child)) {
1989           resolvedNode = child;
1990           resolvedElement = null;
1991           resolvedOffset = getTextNodeOffset(child, moveSelectionToEnd);
1992         } else if (
1993           child !== resolvedElement &&
1994           moveSelectionToEnd &&
1995           !hasBlockCursor
1996         ) {
1997           resolvedOffset++;
1998         }
1999       } else {
2000         const index = resolvedElement.getIndexWithinParent();
2001         // When selecting decorators, there can be some selection issues when using resolvedOffset,
2002         // and instead we should be checking if we're using the offset
2003         if (
2004           offset === 0 &&
2005           $isDecoratorNode(resolvedElement) &&
2006           $getNodeFromDOM(dom) === resolvedElement
2007         ) {
2008           resolvedOffset = index;
2009         } else {
2010           resolvedOffset = index + 1;
2011         }
2012         resolvedElement = resolvedElement.getParentOrThrow();
2013       }
2014       if ($isElementNode(resolvedElement)) {
2015         return $createPoint(resolvedElement.__key, resolvedOffset, 'element');
2016       }
2017     }
2018   } else {
2019     // TextNode or null
2020     resolvedNode = $getNodeFromDOM(dom);
2021   }
2022   if (!$isTextNode(resolvedNode)) {
2023     return null;
2024   }
2025   return $createPoint(resolvedNode.__key, resolvedOffset, 'text');
2026 }
2027
2028 function resolveSelectionPointOnBoundary(
2029   point: TextPointType,
2030   isBackward: boolean,
2031   isCollapsed: boolean,
2032 ): void {
2033   const offset = point.offset;
2034   const node = point.getNode();
2035
2036   if (offset === 0) {
2037     const prevSibling = node.getPreviousSibling();
2038     const parent = node.getParent();
2039
2040     if (!isBackward) {
2041       if (
2042         $isElementNode(prevSibling) &&
2043         !isCollapsed &&
2044         prevSibling.isInline()
2045       ) {
2046         point.key = prevSibling.__key;
2047         point.offset = prevSibling.getChildrenSize();
2048         // @ts-expect-error: intentional
2049         point.type = 'element';
2050       } else if ($isTextNode(prevSibling)) {
2051         point.key = prevSibling.__key;
2052         point.offset = prevSibling.getTextContent().length;
2053       }
2054     } else if (
2055       (isCollapsed || !isBackward) &&
2056       prevSibling === null &&
2057       $isElementNode(parent) &&
2058       parent.isInline()
2059     ) {
2060       const parentSibling = parent.getPreviousSibling();
2061       if ($isTextNode(parentSibling)) {
2062         point.key = parentSibling.__key;
2063         point.offset = parentSibling.getTextContent().length;
2064       }
2065     }
2066   } else if (offset === node.getTextContent().length) {
2067     const nextSibling = node.getNextSibling();
2068     const parent = node.getParent();
2069
2070     if (isBackward && $isElementNode(nextSibling) && nextSibling.isInline()) {
2071       point.key = nextSibling.__key;
2072       point.offset = 0;
2073       // @ts-expect-error: intentional
2074       point.type = 'element';
2075     } else if (
2076       (isCollapsed || isBackward) &&
2077       nextSibling === null &&
2078       $isElementNode(parent) &&
2079       parent.isInline() &&
2080       !parent.canInsertTextAfter()
2081     ) {
2082       const parentSibling = parent.getNextSibling();
2083       if ($isTextNode(parentSibling)) {
2084         point.key = parentSibling.__key;
2085         point.offset = 0;
2086       }
2087     }
2088   }
2089 }
2090
2091 function $normalizeSelectionPointsForBoundaries(
2092   anchor: PointType,
2093   focus: PointType,
2094   lastSelection: null | BaseSelection,
2095 ): void {
2096   if (anchor.type === 'text' && focus.type === 'text') {
2097     const isBackward = anchor.isBefore(focus);
2098     const isCollapsed = anchor.is(focus);
2099
2100     // Attempt to normalize the offset to the previous sibling if we're at the
2101     // start of a text node and the sibling is a text node or inline element.
2102     resolveSelectionPointOnBoundary(anchor, isBackward, isCollapsed);
2103     resolveSelectionPointOnBoundary(focus, !isBackward, isCollapsed);
2104
2105     if (isCollapsed) {
2106       focus.key = anchor.key;
2107       focus.offset = anchor.offset;
2108       focus.type = anchor.type;
2109     }
2110     const editor = getActiveEditor();
2111
2112     if (
2113       editor.isComposing() &&
2114       editor._compositionKey !== anchor.key &&
2115       $isRangeSelection(lastSelection)
2116     ) {
2117       const lastAnchor = lastSelection.anchor;
2118       const lastFocus = lastSelection.focus;
2119       $setPointValues(
2120         anchor,
2121         lastAnchor.key,
2122         lastAnchor.offset,
2123         lastAnchor.type,
2124       );
2125       $setPointValues(focus, lastFocus.key, lastFocus.offset, lastFocus.type);
2126     }
2127   }
2128 }
2129
2130 function $internalResolveSelectionPoints(
2131   anchorDOM: null | Node,
2132   anchorOffset: number,
2133   focusDOM: null | Node,
2134   focusOffset: number,
2135   editor: LexicalEditor,
2136   lastSelection: null | BaseSelection,
2137 ): null | [PointType, PointType] {
2138   if (
2139     anchorDOM === null ||
2140     focusDOM === null ||
2141     !isSelectionWithinEditor(editor, anchorDOM, focusDOM)
2142   ) {
2143     return null;
2144   }
2145   const resolvedAnchorPoint = $internalResolveSelectionPoint(
2146     anchorDOM,
2147     anchorOffset,
2148     $isRangeSelection(lastSelection) ? lastSelection.anchor : null,
2149     editor,
2150   );
2151   if (resolvedAnchorPoint === null) {
2152     return null;
2153   }
2154   const resolvedFocusPoint = $internalResolveSelectionPoint(
2155     focusDOM,
2156     focusOffset,
2157     $isRangeSelection(lastSelection) ? lastSelection.focus : null,
2158     editor,
2159   );
2160   if (resolvedFocusPoint === null) {
2161     return null;
2162   }
2163   if (
2164     resolvedAnchorPoint.type === 'element' &&
2165     resolvedFocusPoint.type === 'element'
2166   ) {
2167     const anchorNode = $getNodeFromDOM(anchorDOM);
2168     const focusNode = $getNodeFromDOM(focusDOM);
2169     // Ensure if we're selecting the content of a decorator that we
2170     // return null for this point, as it's not in the controlled scope
2171     // of Lexical.
2172     if ($isDecoratorNode(anchorNode) && $isDecoratorNode(focusNode)) {
2173       return null;
2174     }
2175   }
2176
2177   // Handle normalization of selection when it is at the boundaries.
2178   $normalizeSelectionPointsForBoundaries(
2179     resolvedAnchorPoint,
2180     resolvedFocusPoint,
2181     lastSelection,
2182   );
2183
2184   return [resolvedAnchorPoint, resolvedFocusPoint];
2185 }
2186
2187 export function $isBlockElementNode(
2188   node: LexicalNode | null | undefined,
2189 ): node is ElementNode {
2190   return $isElementNode(node) && !node.isInline();
2191 }
2192
2193 // This is used to make a selection when the existing
2194 // selection is null, i.e. forcing selection on the editor
2195 // when it current exists outside the editor.
2196
2197 export function $internalMakeRangeSelection(
2198   anchorKey: NodeKey,
2199   anchorOffset: number,
2200   focusKey: NodeKey,
2201   focusOffset: number,
2202   anchorType: 'text' | 'element',
2203   focusType: 'text' | 'element',
2204 ): RangeSelection {
2205   const editorState = getActiveEditorState();
2206   const selection = new RangeSelection(
2207     $createPoint(anchorKey, anchorOffset, anchorType),
2208     $createPoint(focusKey, focusOffset, focusType),
2209     0,
2210     '',
2211   );
2212   selection.dirty = true;
2213   editorState._selection = selection;
2214   return selection;
2215 }
2216
2217 export function $createRangeSelection(): RangeSelection {
2218   const anchor = $createPoint('root', 0, 'element');
2219   const focus = $createPoint('root', 0, 'element');
2220   return new RangeSelection(anchor, focus, 0, '');
2221 }
2222
2223 export function $createNodeSelection(): NodeSelection {
2224   return new NodeSelection(new Set());
2225 }
2226
2227 export function $internalCreateSelection(
2228   editor: LexicalEditor,
2229 ): null | BaseSelection {
2230   const currentEditorState = editor.getEditorState();
2231   const lastSelection = currentEditorState._selection;
2232   const domSelection = getDOMSelection(editor._window);
2233
2234   if ($isRangeSelection(lastSelection) || lastSelection == null) {
2235     return $internalCreateRangeSelection(
2236       lastSelection,
2237       domSelection,
2238       editor,
2239       null,
2240     );
2241   }
2242   return lastSelection.clone();
2243 }
2244
2245 export function $createRangeSelectionFromDom(
2246   domSelection: Selection | null,
2247   editor: LexicalEditor,
2248 ): null | RangeSelection {
2249   return $internalCreateRangeSelection(null, domSelection, editor, null);
2250 }
2251
2252 export function $internalCreateRangeSelection(
2253   lastSelection: null | BaseSelection,
2254   domSelection: Selection | null,
2255   editor: LexicalEditor,
2256   event: UIEvent | Event | null,
2257 ): null | RangeSelection {
2258   const windowObj = editor._window;
2259   if (windowObj === null) {
2260     return null;
2261   }
2262   // When we create a selection, we try to use the previous
2263   // selection where possible, unless an actual user selection
2264   // change has occurred. When we do need to create a new selection
2265   // we validate we can have text nodes for both anchor and focus
2266   // nodes. If that holds true, we then return that selection
2267   // as a mutable object that we use for the editor state for this
2268   // update cycle. If a selection gets changed, and requires a
2269   // update to native DOM selection, it gets marked as "dirty".
2270   // If the selection changes, but matches with the existing
2271   // DOM selection, then we only need to sync it. Otherwise,
2272   // we generally bail out of doing an update to selection during
2273   // reconciliation unless there are dirty nodes that need
2274   // reconciling.
2275
2276   const windowEvent = event || windowObj.event;
2277   const eventType = windowEvent ? windowEvent.type : undefined;
2278   const isSelectionChange = eventType === 'selectionchange';
2279   const useDOMSelection =
2280     !getIsProcessingMutations() &&
2281     (isSelectionChange ||
2282       eventType === 'beforeinput' ||
2283       eventType === 'compositionstart' ||
2284       eventType === 'compositionend' ||
2285       (eventType === 'click' &&
2286         windowEvent &&
2287         (windowEvent as InputEvent).detail === 3) ||
2288       eventType === 'drop' ||
2289       eventType === undefined);
2290   let anchorDOM, focusDOM, anchorOffset, focusOffset;
2291
2292   if (!$isRangeSelection(lastSelection) || useDOMSelection) {
2293     if (domSelection === null) {
2294       return null;
2295     }
2296     anchorDOM = domSelection.anchorNode;
2297     focusDOM = domSelection.focusNode;
2298     anchorOffset = domSelection.anchorOffset;
2299     focusOffset = domSelection.focusOffset;
2300     if (
2301       isSelectionChange &&
2302       $isRangeSelection(lastSelection) &&
2303       !isSelectionWithinEditor(editor, anchorDOM, focusDOM)
2304     ) {
2305       return lastSelection.clone();
2306     }
2307   } else {
2308     return lastSelection.clone();
2309   }
2310   // Let's resolve the text nodes from the offsets and DOM nodes we have from
2311   // native selection.
2312   const resolvedSelectionPoints = $internalResolveSelectionPoints(
2313     anchorDOM,
2314     anchorOffset,
2315     focusDOM,
2316     focusOffset,
2317     editor,
2318     lastSelection,
2319   );
2320   if (resolvedSelectionPoints === null) {
2321     return null;
2322   }
2323   const [resolvedAnchorPoint, resolvedFocusPoint] = resolvedSelectionPoints;
2324   return new RangeSelection(
2325     resolvedAnchorPoint,
2326     resolvedFocusPoint,
2327     !$isRangeSelection(lastSelection) ? 0 : lastSelection.format,
2328     !$isRangeSelection(lastSelection) ? '' : lastSelection.style,
2329   );
2330 }
2331
2332 export function $getSelection(): null | BaseSelection {
2333   const editorState = getActiveEditorState();
2334   return editorState._selection;
2335 }
2336
2337 export function $getPreviousSelection(): null | BaseSelection {
2338   const editor = getActiveEditor();
2339   return editor._editorState._selection;
2340 }
2341
2342 export function $updateElementSelectionOnCreateDeleteNode(
2343   selection: RangeSelection,
2344   parentNode: LexicalNode,
2345   nodeOffset: number,
2346   times = 1,
2347 ): void {
2348   const anchor = selection.anchor;
2349   const focus = selection.focus;
2350   const anchorNode = anchor.getNode();
2351   const focusNode = focus.getNode();
2352   if (!parentNode.is(anchorNode) && !parentNode.is(focusNode)) {
2353     return;
2354   }
2355   const parentKey = parentNode.__key;
2356   // Single node. We shift selection but never redimension it
2357   if (selection.isCollapsed()) {
2358     const selectionOffset = anchor.offset;
2359     if (
2360       (nodeOffset <= selectionOffset && times > 0) ||
2361       (nodeOffset < selectionOffset && times < 0)
2362     ) {
2363       const newSelectionOffset = Math.max(0, selectionOffset + times);
2364       anchor.set(parentKey, newSelectionOffset, 'element');
2365       focus.set(parentKey, newSelectionOffset, 'element');
2366       // The new selection might point to text nodes, try to resolve them
2367       $updateSelectionResolveTextNodes(selection);
2368     }
2369   } else {
2370     // Multiple nodes selected. We shift or redimension selection
2371     const isBackward = selection.isBackward();
2372     const firstPoint = isBackward ? focus : anchor;
2373     const firstPointNode = firstPoint.getNode();
2374     const lastPoint = isBackward ? anchor : focus;
2375     const lastPointNode = lastPoint.getNode();
2376     if (parentNode.is(firstPointNode)) {
2377       const firstPointOffset = firstPoint.offset;
2378       if (
2379         (nodeOffset <= firstPointOffset && times > 0) ||
2380         (nodeOffset < firstPointOffset && times < 0)
2381       ) {
2382         firstPoint.set(
2383           parentKey,
2384           Math.max(0, firstPointOffset + times),
2385           'element',
2386         );
2387       }
2388     }
2389     if (parentNode.is(lastPointNode)) {
2390       const lastPointOffset = lastPoint.offset;
2391       if (
2392         (nodeOffset <= lastPointOffset && times > 0) ||
2393         (nodeOffset < lastPointOffset && times < 0)
2394       ) {
2395         lastPoint.set(
2396           parentKey,
2397           Math.max(0, lastPointOffset + times),
2398           'element',
2399         );
2400       }
2401     }
2402   }
2403   // The new selection might point to text nodes, try to resolve them
2404   $updateSelectionResolveTextNodes(selection);
2405 }
2406
2407 function $updateSelectionResolveTextNodes(selection: RangeSelection): void {
2408   const anchor = selection.anchor;
2409   const anchorOffset = anchor.offset;
2410   const focus = selection.focus;
2411   const focusOffset = focus.offset;
2412   const anchorNode = anchor.getNode();
2413   const focusNode = focus.getNode();
2414   if (selection.isCollapsed()) {
2415     if (!$isElementNode(anchorNode)) {
2416       return;
2417     }
2418     const childSize = anchorNode.getChildrenSize();
2419     const anchorOffsetAtEnd = anchorOffset >= childSize;
2420     const child = anchorOffsetAtEnd
2421       ? anchorNode.getChildAtIndex(childSize - 1)
2422       : anchorNode.getChildAtIndex(anchorOffset);
2423     if ($isTextNode(child)) {
2424       let newOffset = 0;
2425       if (anchorOffsetAtEnd) {
2426         newOffset = child.getTextContentSize();
2427       }
2428       anchor.set(child.__key, newOffset, 'text');
2429       focus.set(child.__key, newOffset, 'text');
2430     }
2431     return;
2432   }
2433   if ($isElementNode(anchorNode)) {
2434     const childSize = anchorNode.getChildrenSize();
2435     const anchorOffsetAtEnd = anchorOffset >= childSize;
2436     const child = anchorOffsetAtEnd
2437       ? anchorNode.getChildAtIndex(childSize - 1)
2438       : anchorNode.getChildAtIndex(anchorOffset);
2439     if ($isTextNode(child)) {
2440       let newOffset = 0;
2441       if (anchorOffsetAtEnd) {
2442         newOffset = child.getTextContentSize();
2443       }
2444       anchor.set(child.__key, newOffset, 'text');
2445     }
2446   }
2447   if ($isElementNode(focusNode)) {
2448     const childSize = focusNode.getChildrenSize();
2449     const focusOffsetAtEnd = focusOffset >= childSize;
2450     const child = focusOffsetAtEnd
2451       ? focusNode.getChildAtIndex(childSize - 1)
2452       : focusNode.getChildAtIndex(focusOffset);
2453     if ($isTextNode(child)) {
2454       let newOffset = 0;
2455       if (focusOffsetAtEnd) {
2456         newOffset = child.getTextContentSize();
2457       }
2458       focus.set(child.__key, newOffset, 'text');
2459     }
2460   }
2461 }
2462
2463 export function applySelectionTransforms(
2464   nextEditorState: EditorState,
2465   editor: LexicalEditor,
2466 ): void {
2467   const prevEditorState = editor.getEditorState();
2468   const prevSelection = prevEditorState._selection;
2469   const nextSelection = nextEditorState._selection;
2470   if ($isRangeSelection(nextSelection)) {
2471     const anchor = nextSelection.anchor;
2472     const focus = nextSelection.focus;
2473     let anchorNode;
2474
2475     if (anchor.type === 'text') {
2476       anchorNode = anchor.getNode();
2477       anchorNode.selectionTransform(prevSelection, nextSelection);
2478     }
2479     if (focus.type === 'text') {
2480       const focusNode = focus.getNode();
2481       if (anchorNode !== focusNode) {
2482         focusNode.selectionTransform(prevSelection, nextSelection);
2483       }
2484     }
2485   }
2486 }
2487
2488 export function moveSelectionPointToSibling(
2489   point: PointType,
2490   node: LexicalNode,
2491   parent: ElementNode,
2492   prevSibling: LexicalNode | null,
2493   nextSibling: LexicalNode | null,
2494 ): void {
2495   let siblingKey = null;
2496   let offset = 0;
2497   let type: 'text' | 'element' | null = null;
2498   if (prevSibling !== null) {
2499     siblingKey = prevSibling.__key;
2500     if ($isTextNode(prevSibling)) {
2501       offset = prevSibling.getTextContentSize();
2502       type = 'text';
2503     } else if ($isElementNode(prevSibling)) {
2504       offset = prevSibling.getChildrenSize();
2505       type = 'element';
2506     }
2507   } else {
2508     if (nextSibling !== null) {
2509       siblingKey = nextSibling.__key;
2510       if ($isTextNode(nextSibling)) {
2511         type = 'text';
2512       } else if ($isElementNode(nextSibling)) {
2513         type = 'element';
2514       }
2515     }
2516   }
2517   if (siblingKey !== null && type !== null) {
2518     point.set(siblingKey, offset, type);
2519   } else {
2520     offset = node.getIndexWithinParent();
2521     if (offset === -1) {
2522       // Move selection to end of parent
2523       offset = parent.getChildrenSize();
2524     }
2525     point.set(parent.__key, offset, 'element');
2526   }
2527 }
2528
2529 export function adjustPointOffsetForMergedSibling(
2530   point: PointType,
2531   isBefore: boolean,
2532   key: NodeKey,
2533   target: TextNode,
2534   textLength: number,
2535 ): void {
2536   if (point.type === 'text') {
2537     point.key = key;
2538     if (!isBefore) {
2539       point.offset += textLength;
2540     }
2541   } else if (point.offset > target.getIndexWithinParent()) {
2542     point.offset -= 1;
2543   }
2544 }
2545
2546 export function updateDOMSelection(
2547   prevSelection: BaseSelection | null,
2548   nextSelection: BaseSelection | null,
2549   editor: LexicalEditor,
2550   domSelection: Selection,
2551   tags: Set<string>,
2552   rootElement: HTMLElement,
2553   nodeCount: number,
2554 ): void {
2555   const anchorDOMNode = domSelection.anchorNode;
2556   const focusDOMNode = domSelection.focusNode;
2557   const anchorOffset = domSelection.anchorOffset;
2558   const focusOffset = domSelection.focusOffset;
2559   const activeElement = document.activeElement;
2560
2561   // TODO: make this not hard-coded, and add another config option
2562   // that makes this configurable.
2563   if (
2564     (tags.has('collaboration') && activeElement !== rootElement) ||
2565     (activeElement !== null &&
2566       isSelectionCapturedInDecoratorInput(activeElement))
2567   ) {
2568     return;
2569   }
2570
2571   if (!$isRangeSelection(nextSelection)) {
2572
2573     // If the DOM selection enters a decorator node update the selection to a single node selection
2574     if (activeElement !== null && domSelection.isCollapsed && focusDOMNode instanceof Node) {
2575       const node = $getNearestNodeFromDOMNode(focusDOMNode);
2576       if ($isDecoratorNode(node)) {
2577         domSelection.removeAllRanges();
2578         $selectSingleNode(node);
2579         return;
2580       }
2581     }
2582
2583     // We don't remove selection if the prevSelection is null because
2584     // of editor.setRootElement(). If this occurs on init when the
2585     // editor is already focused, then this can cause the editor to
2586     // lose focus.
2587     if (
2588       prevSelection !== null &&
2589       isSelectionWithinEditor(editor, anchorDOMNode, focusDOMNode)
2590     ) {
2591       domSelection.removeAllRanges();
2592     }
2593
2594     return;
2595   }
2596
2597   const anchor = nextSelection.anchor;
2598   const focus = nextSelection.focus;
2599   const anchorKey = anchor.key;
2600   const focusKey = focus.key;
2601   const anchorDOM = getElementByKeyOrThrow(editor, anchorKey);
2602   const focusDOM = getElementByKeyOrThrow(editor, focusKey);
2603   const nextAnchorOffset = anchor.offset;
2604   const nextFocusOffset = focus.offset;
2605   const nextFormat = nextSelection.format;
2606   const nextStyle = nextSelection.style;
2607   const isCollapsed = nextSelection.isCollapsed();
2608   let nextAnchorNode: HTMLElement | Text | null = anchorDOM;
2609   let nextFocusNode: HTMLElement | Text | null = focusDOM;
2610   let anchorFormatOrStyleChanged = false;
2611
2612   if (anchor.type === 'text') {
2613     nextAnchorNode = getDOMTextNode(anchorDOM);
2614     const anchorNode = anchor.getNode();
2615     anchorFormatOrStyleChanged =
2616       anchorNode.getFormat() !== nextFormat ||
2617       anchorNode.getStyle() !== nextStyle;
2618   } else if (
2619     $isRangeSelection(prevSelection) &&
2620     prevSelection.anchor.type === 'text'
2621   ) {
2622     anchorFormatOrStyleChanged = true;
2623   }
2624
2625   if (focus.type === 'text') {
2626     nextFocusNode = getDOMTextNode(focusDOM);
2627   }
2628
2629   // If we can't get an underlying text node for selection, then
2630   // we should avoid setting selection to something incorrect.
2631   if (nextAnchorNode === null || nextFocusNode === null) {
2632     return;
2633   }
2634
2635   if (
2636     isCollapsed &&
2637     (prevSelection === null ||
2638       anchorFormatOrStyleChanged ||
2639       ($isRangeSelection(prevSelection) &&
2640         (prevSelection.format !== nextFormat ||
2641           prevSelection.style !== nextStyle)))
2642   ) {
2643     markCollapsedSelectionFormat(
2644       nextFormat,
2645       nextStyle,
2646       nextAnchorOffset,
2647       anchorKey,
2648       performance.now(),
2649     );
2650   }
2651
2652   // Diff against the native DOM selection to ensure we don't do
2653   // an unnecessary selection update. We also skip this check if
2654   // we're moving selection to within an element, as this can
2655   // sometimes be problematic around scrolling.
2656   if (
2657     anchorOffset === nextAnchorOffset &&
2658     focusOffset === nextFocusOffset &&
2659     anchorDOMNode === nextAnchorNode &&
2660     focusDOMNode === nextFocusNode && // Badly interpreted range selection when collapsed - #1482
2661     !(domSelection.type === 'Range' && isCollapsed)
2662   ) {
2663     // If the root element does not have focus, ensure it has focus
2664     if (activeElement === null || !rootElement.contains(activeElement)) {
2665       rootElement.focus({
2666         preventScroll: true,
2667       });
2668     }
2669     if (anchor.type !== 'element') {
2670       return;
2671     }
2672   }
2673
2674   // Apply the updated selection to the DOM. Note: this will trigger
2675   // a "selectionchange" event, although it will be asynchronous.
2676   try {
2677     domSelection.setBaseAndExtent(
2678       nextAnchorNode,
2679       nextAnchorOffset,
2680       nextFocusNode,
2681       nextFocusOffset,
2682     );
2683   } catch (error) {
2684     // If we encounter an error, continue. This can sometimes
2685     // occur with FF and there's no good reason as to why it
2686     // should happen.
2687     if (__DEV__) {
2688       console.warn(error);
2689     }
2690   }
2691   if (
2692     !tags.has('skip-scroll-into-view') &&
2693     nextSelection.isCollapsed() &&
2694     rootElement !== null &&
2695     rootElement === document.activeElement
2696   ) {
2697     const selectionTarget: null | Range | HTMLElement | Text =
2698       nextSelection instanceof RangeSelection &&
2699       nextSelection.anchor.type === 'element'
2700         ? (nextAnchorNode.childNodes[nextAnchorOffset] as HTMLElement | Text) ||
2701           null
2702         : domSelection.rangeCount > 0
2703         ? domSelection.getRangeAt(0)
2704         : null;
2705     if (selectionTarget !== null) {
2706       let selectionRect: DOMRect;
2707       if (selectionTarget instanceof Text) {
2708         const range = document.createRange();
2709         range.selectNode(selectionTarget);
2710         selectionRect = range.getBoundingClientRect();
2711       } else {
2712         selectionRect = selectionTarget.getBoundingClientRect();
2713       }
2714       scrollIntoViewIfNeeded(editor, selectionRect, rootElement);
2715     }
2716   }
2717
2718   markSelectionChangeFromDOMUpdate();
2719 }
2720
2721 export function $insertNodes(nodes: Array<LexicalNode>) {
2722   let selection = $getSelection() || $getPreviousSelection();
2723
2724   if (selection === null) {
2725     selection = $getRoot().selectEnd();
2726   }
2727   selection.insertNodes(nodes);
2728 }
2729
2730 export function $getTextContent(): string {
2731   const selection = $getSelection();
2732   if (selection === null) {
2733     return '';
2734   }
2735   return selection.getTextContent();
2736 }
2737
2738 function $removeTextAndSplitBlock(selection: RangeSelection): number {
2739   let selection_ = selection;
2740   if (!selection.isCollapsed()) {
2741     selection_.removeText();
2742   }
2743   // A new selection can originate as a result of node replacement, in which case is registered via
2744   // $setSelection
2745   const newSelection = $getSelection();
2746   if ($isRangeSelection(newSelection)) {
2747     selection_ = newSelection;
2748   }
2749
2750   invariant(
2751     $isRangeSelection(selection_),
2752     'Unexpected dirty selection to be null',
2753   );
2754
2755   const anchor = selection_.anchor;
2756   let node = anchor.getNode();
2757   let offset = anchor.offset;
2758
2759   while (!INTERNAL_$isBlock(node)) {
2760     [node, offset] = $splitNodeAtPoint(node, offset);
2761   }
2762
2763   return offset;
2764 }
2765
2766 function $splitNodeAtPoint(
2767   node: LexicalNode,
2768   offset: number,
2769 ): [parent: ElementNode, offset: number] {
2770   const parent = node.getParent();
2771   if (!parent) {
2772     const paragraph = $createParagraphNode();
2773     $getRoot().append(paragraph);
2774     paragraph.select();
2775     return [$getRoot(), 0];
2776   }
2777
2778   if ($isTextNode(node)) {
2779     const split = node.splitText(offset);
2780     if (split.length === 0) {
2781       return [parent, node.getIndexWithinParent()];
2782     }
2783     const x = offset === 0 ? 0 : 1;
2784     const index = split[0].getIndexWithinParent() + x;
2785
2786     return [parent, index];
2787   }
2788
2789   if (!$isElementNode(node) || offset === 0) {
2790     return [parent, node.getIndexWithinParent()];
2791   }
2792
2793   const firstToAppend = node.getChildAtIndex(offset);
2794   if (firstToAppend) {
2795     const insertPoint = new RangeSelection(
2796       $createPoint(node.__key, offset, 'element'),
2797       $createPoint(node.__key, offset, 'element'),
2798       0,
2799       '',
2800     );
2801     const newElement = node.insertNewAfter(insertPoint) as ElementNode | null;
2802     if (newElement) {
2803       newElement.append(firstToAppend, ...firstToAppend.getNextSiblings());
2804     }
2805   }
2806   return [parent, node.getIndexWithinParent() + 1];
2807 }
2808
2809 function $wrapInlineNodes(nodes: LexicalNode[]) {
2810   // We temporarily insert the topLevelNodes into an arbitrary ElementNode,
2811   // since insertAfter does not work on nodes that have no parent (TO-DO: fix that).
2812   const virtualRoot = $createParagraphNode();
2813
2814   let currentBlock = null;
2815   for (let i = 0; i < nodes.length; i++) {
2816     const node = nodes[i];
2817
2818     const isLineBreakNode = $isLineBreakNode(node);
2819
2820     if (
2821       isLineBreakNode ||
2822       ($isDecoratorNode(node) && node.isInline()) ||
2823       ($isElementNode(node) && node.isInline()) ||
2824       $isTextNode(node) ||
2825       node.isParentRequired()
2826     ) {
2827       if (currentBlock === null) {
2828         currentBlock = node.createParentElementNode();
2829         virtualRoot.append(currentBlock);
2830         // In the case of LineBreakNode, we just need to
2831         // add an empty ParagraphNode to the topLevelBlocks.
2832         if (isLineBreakNode) {
2833           continue;
2834         }
2835       }
2836
2837       if (currentBlock !== null) {
2838         currentBlock.append(node);
2839       }
2840     } else {
2841       virtualRoot.append(node);
2842       currentBlock = null;
2843     }
2844   }
2845
2846   return virtualRoot;
2847 }