Skip to content

Commit

Permalink
[lexical][lexical-code] Refactor: Use NodeCaret to implement RangeSel…
Browse files Browse the repository at this point in the history
…ection.getNodes() (facebook#7135)
  • Loading branch information
etrepum authored Feb 24, 2025
1 parent 1c9d39c commit 322870e
Show file tree
Hide file tree
Showing 11 changed files with 767 additions and 199 deletions.
20 changes: 20 additions & 0 deletions packages/lexical-code/flow/LexicalCode.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,22 @@ import {ElementNode, TextNode} from 'lexical';
/**
* CodeHighlighter
*/
declare export function $getEndOfCodeInLine(
anchor: CodeHighlightNode | TabNode,
): CodeHighlightNode | TabNode;
/** @deprecated renamed to {@link $getEndOfCodeInLine} by @lexical/eslint-plugin rules-of-lexical */
declare export function getEndOfCodeInLine(
anchor: CodeHighlightNode | TabNode,
): CodeHighlightNode | TabNode;

declare export function $getStartOfCodeInLine(
anchor: CodeHighlightNode | TabNode,
offset: number,
): null | {
node: CodeHighlightNode | TabNode | LineBreakNode,
offset: number,
};
/** @deprecated renamed to {@link $getStartOfCodeInLine} by @lexical/eslint-plugin rules-of-lexical */
declare export function getStartOfCodeInLine(
anchor: CodeHighlightNode | TabNode,
offset: number,
Expand Down Expand Up @@ -85,12 +97,20 @@ declare export var DEFAULT_CODE_LANGUAGE: string;
declare export var getCodeLanguages: () => Array<string>;
declare export var getDefaultCodeLanguage: () => string;

declare export function $getFirstCodeNodeOfLine(
anchor: CodeHighlightNode | TabNode | LineBreakNode,
): CodeHighlightNode | TabNode | LineBreakNode;
/** @deprecated renamed to {@link $getFirstCodeNodeOfLine} by @lexical/eslint-plugin rules-of-lexical */
declare export function getFirstCodeNodeOfLine(
anchor: CodeHighlightNode | TabNode | LineBreakNode,
): null | CodeHighlightNode | TabNode | LineBreakNode;

declare export function getLanguageFriendlyName(lang: string): string;

declare export function $getLastCodeNodeOfLine(
anchor: CodeHighlightNode | TabNode | LineBreakNode,
): CodeHighlightNode | TabNode | LineBreakNode;
/** @deprecated renamed to {@link $getLastCodeNodeOfLine} by @lexical/eslint-plugin rules-of-lexical */
declare export function getLastCodeNodeOfLine(
anchor: CodeHighlightNode | TabNode | LineBreakNode,
): CodeHighlightNode | TabNode | LineBreakNode;
Expand Down
42 changes: 26 additions & 16 deletions packages/lexical-code/src/CodeHighlightNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,27 @@
*/

import type {
CaretDirection,
EditorConfig,
EditorThemeClasses,
LexicalNode,
LexicalUpdateJSON,
LineBreakNode,
NodeKey,
SerializedTextNode,
SiblingCaret,
Spread,
TabNode,
} from 'lexical';

import {
$getAdjacentCaret,
addClassNamesToElement,
removeClassNamesFromElement,
} from '@lexical/utils';
import {
$applyNodeReplacement,
$getSiblingCaret,
$isTabNode,
ElementNode,
TextNode,
Expand Down Expand Up @@ -224,26 +228,32 @@ export function $isCodeHighlightNode(
return node instanceof CodeHighlightNode;
}

export function getFirstCodeNodeOfLine(
function $getLastMatchingCodeNode<D extends CaretDirection>(
anchor: CodeHighlightNode | TabNode | LineBreakNode,
): null | CodeHighlightNode | TabNode | LineBreakNode {
let previousNode = anchor;
let node: null | LexicalNode = anchor;
while ($isCodeHighlightNode(node) || $isTabNode(node)) {
previousNode = node;
node = node.getPreviousSibling();
direction: D,
): CodeHighlightNode | TabNode | LineBreakNode {
let matchingNode: CodeHighlightNode | TabNode | LineBreakNode = anchor;
for (
let caret: null | SiblingCaret<LexicalNode, D> = $getSiblingCaret(
anchor,
direction,
);
caret && ($isCodeHighlightNode(caret.origin) || $isTabNode(caret.origin));
caret = $getAdjacentCaret(caret)
) {
matchingNode = caret.origin;
}
return previousNode;
return matchingNode;
}

export function getLastCodeNodeOfLine(
export function $getFirstCodeNodeOfLine(
anchor: CodeHighlightNode | TabNode | LineBreakNode,
): CodeHighlightNode | TabNode | LineBreakNode {
let nextNode = anchor;
let node: null | LexicalNode = anchor;
while ($isCodeHighlightNode(node) || $isTabNode(node)) {
nextNode = node;
node = node.getNextSibling();
}
return nextNode;
return $getLastMatchingCodeNode(anchor, 'previous');
}

export function $getLastCodeNodeOfLine(
anchor: CodeHighlightNode | TabNode | LineBreakNode,
): CodeHighlightNode | TabNode | LineBreakNode {
return $getLastMatchingCodeNode(anchor, 'next');
}
38 changes: 17 additions & 21 deletions packages/lexical-code/src/CodeHighlighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ import invariant from 'shared/invariant';
import {Prism} from './CodeHighlighterPrism';
import {
$createCodeHighlightNode,
$getFirstCodeNodeOfLine,
$getLastCodeNodeOfLine,
$isCodeHighlightNode,
CodeHighlightNode,
DEFAULT_CODE_LANGUAGE,
getFirstCodeNodeOfLine,
getLastCodeNodeOfLine,
} from './CodeHighlightNode';
import {$isCodeNode, CodeNode} from './CodeNode';

Expand All @@ -75,7 +75,7 @@ export const PrismTokenizer: Tokenizer = {
},
};

export function getStartOfCodeInLine(
export function $getStartOfCodeInLine(
anchor: CodeHighlightNode | TabNode,
offset: number,
): null | {
Expand Down Expand Up @@ -188,10 +188,10 @@ function findNextNonBlankInLine(
}
}

export function getEndOfCodeInLine(
export function $getEndOfCodeInLine(
anchor: CodeHighlightNode | TabNode,
): CodeHighlightNode | TabNode {
const lastNode = getLastCodeNodeOfLine(anchor);
const lastNode = $getLastCodeNodeOfLine(anchor);
invariant(
!$isLineBreakNode(lastNode),
'Unexpected lineBreakNode in getEndOfCodeInLine',
Expand Down Expand Up @@ -535,8 +535,8 @@ function $handleTab(shiftKey: boolean): null | LexicalCommand<void> {
if ($isCodeNode(firstNode)) {
return indentOrOutdent;
}
const firstOfLine = getFirstCodeNodeOfLine(firstNode);
const lastOfLine = getLastCodeNodeOfLine(firstNode);
const firstOfLine = $getFirstCodeNodeOfLine(firstNode);
const lastOfLine = $getLastCodeNodeOfLine(firstNode);
const anchor = selection.anchor;
const focus = selection.focus;
let selectionFirst;
Expand Down Expand Up @@ -578,7 +578,7 @@ function $handleMultilineIndent(type: LexicalCommand<void>): boolean {
line[0];
// First and last lines might not be complete
if (i === 0) {
firstOfLine = getFirstCodeNodeOfLine(firstOfLine);
firstOfLine = $getFirstCodeNodeOfLine(firstOfLine);
}
if (firstOfLine !== null) {
if (type === INDENT_CONTENT_COMMAND) {
Expand Down Expand Up @@ -608,11 +608,7 @@ function $handleMultilineIndent(type: LexicalCommand<void>): boolean {
}
return true;
}
const firstOfLine = getFirstCodeNodeOfLine(firstNode);
invariant(
firstOfLine !== null,
'Expected getFirstCodeNodeOfLine to return a valid Code Node',
);
const firstOfLine = $getFirstCodeNodeOfLine(firstNode);
if (type === INDENT_CONTENT_COMMAND) {
if ($isLineBreakNode(firstOfLine)) {
firstOfLine.insertAfter($createTabNode());
Expand Down Expand Up @@ -687,11 +683,11 @@ function $handleShiftLines(
let start;
let end;
if (anchorNode.isBefore(focusNode)) {
start = getFirstCodeNodeOfLine(anchorNode);
end = getLastCodeNodeOfLine(focusNode);
start = $getFirstCodeNodeOfLine(anchorNode);
end = $getLastCodeNodeOfLine(focusNode);
} else {
start = getFirstCodeNodeOfLine(focusNode);
end = getLastCodeNodeOfLine(anchorNode);
start = $getFirstCodeNodeOfLine(focusNode);
end = $getLastCodeNodeOfLine(anchorNode);
}
if (start == null || end == null) {
return false;
Expand Down Expand Up @@ -733,8 +729,8 @@ function $handleShiftLines(
$isTabNode(sibling) ||
$isLineBreakNode(sibling)
? arrowIsUp
? getFirstCodeNodeOfLine(sibling)
: getLastCodeNodeOfLine(sibling)
? $getFirstCodeNodeOfLine(sibling)
: $getLastCodeNodeOfLine(sibling)
: null;
let insertionPoint =
maybeInsertionPoint != null ? maybeInsertionPoint : sibling;
Expand Down Expand Up @@ -781,7 +777,7 @@ function $handleMoveTo(
}

if (isMoveToStart) {
const start = getStartOfCodeInLine(focusNode, focus.offset);
const start = $getStartOfCodeInLine(focusNode, focus.offset);
if (start !== null) {
const {node, offset} = start;
if ($isLineBreakNode(node)) {
Expand All @@ -793,7 +789,7 @@ function $handleMoveTo(
focusNode.getParentOrThrow().selectStart();
}
} else {
const node = getEndOfCodeInLine(focusNode);
const node = $getEndOfCodeInLine(focusNode);
node.select();
}

Expand Down
5 changes: 3 additions & 2 deletions packages/lexical-code/src/CodeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ import {
import {Prism} from './CodeHighlighterPrism';
import {
$createCodeHighlightNode,
$getFirstCodeNodeOfLine,
$isCodeHighlightNode,
getFirstCodeNodeOfLine,
} from './CodeHighlightNode';

export type SerializedCodeNode = Spread<
Expand Down Expand Up @@ -259,7 +259,8 @@ export class CodeNode extends ElementNode {
const firstPoint = anchor.isBefore(focus) ? anchor : focus;
const firstSelectionNode = firstPoint.getNode();
if ($isTextNode(firstSelectionNode)) {
let node = getFirstCodeNodeOfLine(firstSelectionNode);
let node: null | LexicalNode =
$getFirstCodeNodeOfLine(firstSelectionNode);
const insertNodes = [];
// eslint-disable-next-line no-constant-condition
while (true) {
Expand Down
23 changes: 19 additions & 4 deletions packages/lexical-code/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,40 @@
*
*/

import {$getEndOfCodeInLine, $getStartOfCodeInLine} from './CodeHighlighter';
import {
$getFirstCodeNodeOfLine,
$getLastCodeNodeOfLine,
} from './CodeHighlightNode';

export {
getEndOfCodeInLine,
getStartOfCodeInLine,
$getEndOfCodeInLine,
$getStartOfCodeInLine,
PrismTokenizer,
registerCodeHighlighting,
} from './CodeHighlighter';
export {
$createCodeHighlightNode,
$getFirstCodeNodeOfLine,
$getLastCodeNodeOfLine,
$isCodeHighlightNode,
CODE_LANGUAGE_FRIENDLY_NAME_MAP,
CODE_LANGUAGE_MAP,
CodeHighlightNode,
DEFAULT_CODE_LANGUAGE,
getCodeLanguages,
getDefaultCodeLanguage,
getFirstCodeNodeOfLine,
getLanguageFriendlyName,
getLastCodeNodeOfLine,
normalizeCodeLang,
} from './CodeHighlightNode';
export type {SerializedCodeNode} from './CodeNode';
export {$createCodeNode, $isCodeNode, CodeNode} from './CodeNode';

/** @deprecated renamed to {@link $getFirstCodeNodeOfLine} by @lexical/eslint-plugin rules-of-lexical */
export const getFirstCodeNodeOfLine = $getFirstCodeNodeOfLine;
/** @deprecated renamed to {@link $getLastCodeNodeOfLine} by @lexical/eslint-plugin rules-of-lexical */
export const getLastCodeNodeOfLine = $getLastCodeNodeOfLine;
/** @deprecated renamed to {@link $getEndOfCodeInLine} by @lexical/eslint-plugin rules-of-lexical */
export const getEndOfCodeInLine = $getEndOfCodeInLine;
/** @deprecated renamed to {@link $getStartOfCodeInLine} by @lexical/eslint-plugin rules-of-lexical */
export const getStartOfCodeInLine = $getStartOfCodeInLine;
42 changes: 42 additions & 0 deletions packages/lexical/flow/Lexical.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -1179,3 +1179,45 @@ declare export function $updateRangeSelectionFromCaretRange(
selection: RangeSelection,
caretRange: CaretRange<CaretDirection>,
): void;

export type CommonAncestorResult<
A: LexicalNode,
B: LexicalNode,
> =
| CommonAncestorResultSame<A>
| CommonAncestorResultAncestor<A & ElementNode>
| CommonAncestorResultDescendant<B & ElementNode>
| CommonAncestorResultBranch<A, B>;
export interface CommonAncestorResultBranch<
A: LexicalNode,
B: LexicalNode,
> {
+type: 'branch';
+commonAncestor: ElementNode;
+a: A | ElementNode;
+b: B | ElementNode;
}
export interface CommonAncestorResultAncestor<A: ElementNode> {
+type: 'ancestor';
+commonAncestor: A;
}
export interface CommonAncestorResultDescendant<B: ElementNode> {
+type: 'descendant';
+commonAncestor: B;
}
export interface CommonAncestorResultSame<A: LexicalNode> {
+type: 'same';
+commonAncestor: A;
}
declare export function $comparePointCaretNext(
a: PointCaret<'next'>,
b: PointCaret<'next'>,
): -1 | 0 | 1;
declare export function $getCommonAncestorResultBranchOrder<
A: LexicalNode,
B: LexicalNode,
>(compare: CommonAncestorResultBranch<A, B>): -1 | 1 ;
declare export function $getCommonAncestor<
A: LexicalNode,
B: LexicalNode,
>(a: A, b: B): null | CommonAncestorResult<A, B>;
Loading

0 comments on commit 322870e

Please sign in to comment.