Make tabs flexible in code (#4520)

This commit is contained in:
Gerard Rovira
2023-05-19 16:19:17 +01:00
committed by GitHub
parent 434af9e046
commit 665ed38b64
12 changed files with 108 additions and 225 deletions

View File

@ -18,22 +18,23 @@ import type {
LineBreakNode, LineBreakNode,
SerializedElementNode, SerializedElementNode,
SerializedTabNode, SerializedTabNode,
TabNode,
} from 'lexical'; } from 'lexical';
import {ElementNode, TextNode, TabNode} from 'lexical'; import {ElementNode, TextNode} from 'lexical';
/** /**
* CodeHighlighter * CodeHighlighter
*/ */
declare export function getEndOfCodeInLine( declare export function getEndOfCodeInLine(
anchor: CodeHighlightNode | CodeTabNode, anchor: CodeHighlightNode | TabNode,
): CodeHighlightNode | CodeTabNode; ): CodeHighlightNode | TabNode;
declare export function getStartOfCodeInLine( declare export function getStartOfCodeInLine(
anchor: CodeHighlightNode | CodeTabNode, anchor: CodeHighlightNode | TabNode,
offset: number, offset: number,
): null | { ): null | {
node: CodeHighlightNode | CodeTabNode | LineBreakNode, node: CodeHighlightNode | TabNode | LineBreakNode,
offset: number, offset: number,
}; };
@ -91,14 +92,14 @@ declare export var getCodeLanguages: () => Array<string>;
declare export var getDefaultCodeLanguage: () => string; declare export var getDefaultCodeLanguage: () => string;
declare export function getFirstCodeNodeOfLine( declare export function getFirstCodeNodeOfLine(
anchor: CodeHighlightNode | CodeTabNode | LineBreakNode, anchor: CodeHighlightNode | TabNode | LineBreakNode,
): null | CodeHighlightNode | CodeTabNode | LineBreakNode; ): null | CodeHighlightNode | TabNode | LineBreakNode;
declare export function getLanguageFriendlyName(lang: string): string; declare export function getLanguageFriendlyName(lang: string): string;
declare export function getLastCodeNodeOfLine( declare export function getLastCodeNodeOfLine(
anchor: CodeHighlightNode | CodeTabNode | LineBreakNode, anchor: CodeHighlightNode | TabNode | LineBreakNode,
): CodeHighlightNode | CodeTabNode | LineBreakNode; ): CodeHighlightNode | TabNode | LineBreakNode;
declare export function normalizeCodeLang(lang: string): string; declare export function normalizeCodeLang(lang: string): string;
@ -127,34 +128,8 @@ declare export class CodeNode extends ElementNode {
insertNewAfter( insertNewAfter(
selection: RangeSelection, selection: RangeSelection,
restoreSelection?: boolean, restoreSelection?: boolean,
): null | ParagraphNode | CodeHighlightNode | CodeTabNode; ): null | ParagraphNode | CodeHighlightNode | TabNode;
collapseAtStart(): true; collapseAtStart(): true;
setLanguage(language: string): void; setLanguage(language: string): void;
getLanguage(): string | void; getLanguage(): string | void;
} }
/**
* CodeTabNode
*/
export type SerializedCodeTabNode = SerializedTabNode;
declare export function $createCodeTabNode(): CodeTabNode;
declare export function $isCodeTabNode(
node: LexicalNode | null | void,
): boolean %checks(node instanceof CodeTabNode);
declare export class CodeTabNode extends TabNode {
static getType(): string;
// $FlowFixMe
static clone(node: CodeTabNode): CodeTabNode;
static importJSON(_serializedTabNode: SerializedCodeTabNode): CodeTabNode;
exportJSON(): SerializedTabNode;
createDOM(config: EditorConfig): HTMLElement;
updateDOM(
prevNode: TextNode,
dom: HTMLElement,
config: EditorConfig,
): boolean;
}

View File

@ -14,6 +14,7 @@ import type {
NodeKey, NodeKey,
SerializedTextNode, SerializedTextNode,
Spread, Spread,
TabNode,
} from 'lexical'; } from 'lexical';
import 'prismjs/components/prism-clike'; import 'prismjs/components/prism-clike';
@ -35,11 +36,15 @@ import {
addClassNamesToElement, addClassNamesToElement,
removeClassNamesFromElement, removeClassNamesFromElement,
} from '@lexical/utils'; } from '@lexical/utils';
import {$applyNodeReplacement, ElementNode, TextNode} from 'lexical'; import {
$applyNodeReplacement,
$isTabNode,
ElementNode,
TextNode,
} from 'lexical';
import * as Prism from 'prismjs'; import * as Prism from 'prismjs';
import {$createCodeNode} from './CodeNode'; import {$createCodeNode} from './CodeNode';
import {$isCodeTabNode, CodeTabNode} from './CodeTabNode';
export const DEFAULT_CODE_LANGUAGE = 'javascript'; export const DEFAULT_CODE_LANGUAGE = 'javascript';
@ -229,11 +234,11 @@ export function $isCodeHighlightNode(
} }
export function getFirstCodeNodeOfLine( export function getFirstCodeNodeOfLine(
anchor: CodeHighlightNode | CodeTabNode | LineBreakNode, anchor: CodeHighlightNode | TabNode | LineBreakNode,
): null | CodeHighlightNode | CodeTabNode | LineBreakNode { ): null | CodeHighlightNode | TabNode | LineBreakNode {
let previousNode = anchor; let previousNode = anchor;
let node: null | LexicalNode = anchor; let node: null | LexicalNode = anchor;
while ($isCodeHighlightNode(node) || $isCodeTabNode(node)) { while ($isCodeHighlightNode(node) || $isTabNode(node)) {
previousNode = node; previousNode = node;
node = node.getPreviousSibling(); node = node.getPreviousSibling();
} }
@ -241,11 +246,11 @@ export function getFirstCodeNodeOfLine(
} }
export function getLastCodeNodeOfLine( export function getLastCodeNodeOfLine(
anchor: CodeHighlightNode | CodeTabNode | LineBreakNode, anchor: CodeHighlightNode | TabNode | LineBreakNode,
): CodeHighlightNode | CodeTabNode | LineBreakNode { ): CodeHighlightNode | TabNode | LineBreakNode {
let nextNode = anchor; let nextNode = anchor;
let node: null | LexicalNode = anchor; let node: null | LexicalNode = anchor;
while ($isCodeHighlightNode(node) || $isCodeTabNode(node)) { while ($isCodeHighlightNode(node) || $isTabNode(node)) {
nextNode = node; nextNode = node;
node = node.getNextSibling(); node = node.getNextSibling();
} }

View File

@ -50,13 +50,14 @@ import {
INDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND,
KEY_ARROW_DOWN_COMMAND, KEY_ARROW_DOWN_COMMAND,
KEY_ARROW_UP_COMMAND, KEY_ARROW_UP_COMMAND,
TabNode,
MOVE_TO_END, MOVE_TO_END,
MOVE_TO_START, MOVE_TO_START,
$insertNodes, $insertNodes,
OUTDENT_CONTENT_COMMAND, OUTDENT_CONTENT_COMMAND,
KEY_TAB_COMMAND, KEY_TAB_COMMAND,
TextNode, TextNode,
$isTabNode,
TabNode,
} from 'lexical'; } from 'lexical';
import { import {
@ -70,7 +71,6 @@ import {
import {$isCodeNode, CodeNode} from './CodeNode'; import {$isCodeNode, CodeNode} from './CodeNode';
import invariant from 'shared/invariant'; import invariant from 'shared/invariant';
import {CodeTabNode, $createCodeTabNode, $isCodeTabNode} from './CodeTabNode';
type TokenContent = string | Token | (string | Token)[]; type TokenContent = string | Token | (string | Token)[];
@ -95,18 +95,18 @@ export const PrismTokenizer: Tokenizer = {
}; };
export function getStartOfCodeInLine( export function getStartOfCodeInLine(
anchor: CodeHighlightNode | CodeTabNode, anchor: CodeHighlightNode | TabNode,
offset: number, offset: number,
): null | { ): null | {
node: CodeHighlightNode | CodeTabNode | LineBreakNode; node: CodeHighlightNode | TabNode | LineBreakNode;
offset: number; offset: number;
} { } {
let last: null | { let last: null | {
node: CodeHighlightNode | CodeTabNode | LineBreakNode; node: CodeHighlightNode | TabNode | LineBreakNode;
offset: number; offset: number;
} = null; } = null;
let lastNonBlank: null | {node: CodeHighlightNode; offset: number} = null; let lastNonBlank: null | {node: CodeHighlightNode; offset: number} = null;
let node: null | CodeHighlightNode | CodeTabNode | LineBreakNode = anchor; let node: null | CodeHighlightNode | TabNode | LineBreakNode = anchor;
let nodeOffset = offset; let nodeOffset = offset;
let nodeTextContent = anchor.getTextContent(); let nodeTextContent = anchor.getTextContent();
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
@ -118,9 +118,9 @@ export function getStartOfCodeInLine(
} }
invariant( invariant(
$isCodeHighlightNode(node) || $isCodeHighlightNode(node) ||
$isCodeTabNode(node) || $isTabNode(node) ||
$isLineBreakNode(node), $isLineBreakNode(node),
'Expected a valid Code Node: CodeHighlightNode, CodeTabNode, LineBreakNode', 'Expected a valid Code Node: CodeHighlightNode, TabNode, LineBreakNode',
); );
if ($isLineBreakNode(node)) { if ($isLineBreakNode(node)) {
last = { last = {
@ -208,8 +208,8 @@ function findNextNonBlankInLine(
} }
export function getEndOfCodeInLine( export function getEndOfCodeInLine(
anchor: CodeHighlightNode | CodeTabNode, anchor: CodeHighlightNode | TabNode,
): CodeHighlightNode | CodeTabNode { ): CodeHighlightNode | TabNode {
const lastNode = getLastCodeNodeOfLine(anchor); const lastNode = getLastCodeNodeOfLine(anchor);
invariant( invariant(
!$isLineBreakNode(lastNode), !$isLineBreakNode(lastNode),
@ -340,7 +340,7 @@ function getHighlightNodes(tokens: (string | Token)[]): LexicalNode[] {
if (part === '\n' || part === '\r\n') { if (part === '\n' || part === '\r\n') {
nodes.push($createLineBreakNode()); nodes.push($createLineBreakNode());
} else if (part === '\t') { } else if (part === '\t') {
nodes.push($createCodeTabNode()); nodes.push($createTabNode());
} else if (part.length > 0) { } else if (part.length > 0) {
nodes.push($createCodeHighlightNode(part)); nodes.push($createCodeHighlightNode(part));
} }
@ -485,7 +485,7 @@ function isEqual(nodeA: LexicalNode, nodeB: LexicalNode): boolean {
$isCodeHighlightNode(nodeB) && $isCodeHighlightNode(nodeB) &&
nodeA.__text === nodeB.__text && nodeA.__text === nodeB.__text &&
nodeA.__highlightType === nodeB.__highlightType) || nodeA.__highlightType === nodeB.__highlightType) ||
($isCodeTabNode(nodeA) && $isCodeTabNode(nodeB)) || ($isTabNode(nodeA) && $isTabNode(nodeB)) ||
($isLineBreakNode(nodeA) && $isLineBreakNode(nodeB)) ($isLineBreakNode(nodeA) && $isLineBreakNode(nodeB))
); );
} }
@ -507,20 +507,18 @@ function $isSelectionInCode(
function $getCodeLines( function $getCodeLines(
selection: RangeSelection, selection: RangeSelection,
): Array<Array<CodeHighlightNode | CodeTabNode>> { ): Array<Array<CodeHighlightNode | TabNode>> {
const nodes = selection.getNodes(); const nodes = selection.getNodes();
const lines: Array<Array<CodeHighlightNode | CodeTabNode>> = [[]]; const lines: Array<Array<CodeHighlightNode | TabNode>> = [[]];
if (nodes.length === 1 && $isCodeNode(nodes[0])) { if (nodes.length === 1 && $isCodeNode(nodes[0])) {
return lines; return lines;
} }
let lastLine: Array<CodeHighlightNode | CodeTabNode> = lines[0]; let lastLine: Array<CodeHighlightNode | TabNode> = lines[0];
for (let i = 0; i < nodes.length; i++) { for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]; const node = nodes[i];
invariant( invariant(
$isCodeHighlightNode(node) || $isCodeHighlightNode(node) || $isTabNode(node) || $isLineBreakNode(node),
$isCodeTabNode(node) || 'Expected selection to be inside CodeBlock and consisting of CodeHighlightNode, TabNode and LineBreakNode',
$isLineBreakNode(node),
'Expected selection to be inside CodeBlock and consisting of CodeHighlightNode, CodeTabNode and LineBreakNode',
); );
if ($isLineBreakNode(node)) { if ($isLineBreakNode(node)) {
if (i !== 0 && lastLine.length > 0) { if (i !== 0 && lastLine.length > 0) {
@ -554,9 +552,9 @@ function handleTab(shiftKey: boolean): null | LexicalCommand<void> {
invariant( invariant(
$isCodeNode(firstNode) || $isCodeNode(firstNode) ||
$isCodeHighlightNode(firstNode) || $isCodeHighlightNode(firstNode) ||
$isCodeTabNode(firstNode) || $isTabNode(firstNode) ||
$isLineBreakNode(firstNode), $isLineBreakNode(firstNode),
'Expected selection firstNode to be CodeHighlightNode or CodeTabNode', 'Expected selection firstNode to be CodeHighlightNode or TabNode',
); );
if ($isCodeNode(firstNode)) { if ($isCodeNode(firstNode)) {
return indentOrOutdent; return indentOrOutdent;
@ -600,19 +598,16 @@ function handleMultilineIndent(type: LexicalCommand<void>): boolean {
for (let i = 0; i < codeLinesLength; i++) { for (let i = 0; i < codeLinesLength; i++) {
const line = codeLines[i]; const line = codeLines[i];
if (line.length > 0) { if (line.length > 0) {
let firstOfLine: let firstOfLine: null | CodeHighlightNode | TabNode | LineBreakNode =
| null line[0];
| CodeHighlightNode
| CodeTabNode
| LineBreakNode = line[0];
// First and last lines might not be complete // First and last lines might not be complete
if (i === 0) { if (i === 0) {
firstOfLine = getFirstCodeNodeOfLine(firstOfLine); firstOfLine = getFirstCodeNodeOfLine(firstOfLine);
} }
if (firstOfLine !== null) { if (firstOfLine !== null) {
if (type === INDENT_CONTENT_COMMAND) { if (type === INDENT_CONTENT_COMMAND) {
firstOfLine.insertBefore($createCodeTabNode()); firstOfLine.insertBefore($createTabNode());
} else if ($isCodeTabNode(firstOfLine)) { } else if ($isTabNode(firstOfLine)) {
firstOfLine.remove(); firstOfLine.remove();
} }
} }
@ -626,14 +621,14 @@ function handleMultilineIndent(type: LexicalCommand<void>): boolean {
invariant( invariant(
$isCodeNode(firstNode) || $isCodeNode(firstNode) ||
$isCodeHighlightNode(firstNode) || $isCodeHighlightNode(firstNode) ||
$isCodeTabNode(firstNode) || $isTabNode(firstNode) ||
$isLineBreakNode(firstNode), $isLineBreakNode(firstNode),
'Expected selection firstNode to be CodeHighlightNode or CodeTabNode', 'Expected selection firstNode to be CodeHighlightNode or CodeTabNode',
); );
if ($isCodeNode(firstNode)) { if ($isCodeNode(firstNode)) {
// CodeNode is empty // CodeNode is empty
if (type === INDENT_CONTENT_COMMAND) { if (type === INDENT_CONTENT_COMMAND) {
selection.insertNodes([$createCodeTabNode()]); selection.insertNodes([$createTabNode()]);
} }
return true; return true;
} }
@ -644,11 +639,11 @@ function handleMultilineIndent(type: LexicalCommand<void>): boolean {
); );
if (type === INDENT_CONTENT_COMMAND) { if (type === INDENT_CONTENT_COMMAND) {
if ($isLineBreakNode(firstOfLine)) { if ($isLineBreakNode(firstOfLine)) {
firstOfLine.insertAfter($createCodeTabNode()); firstOfLine.insertAfter($createTabNode());
} else { } else {
firstOfLine.insertBefore($createCodeTabNode()); firstOfLine.insertBefore($createTabNode());
} }
} else if ($isCodeTabNode(firstOfLine)) { } else if ($isTabNode(firstOfLine)) {
firstOfLine.remove(); firstOfLine.remove();
} }
return true; return true;
@ -676,8 +671,8 @@ function handleShiftLines(
// Ensure the selection is within the codeblock // Ensure the selection is within the codeblock
if ( if (
!$isSelectionInCode(selection) || !$isSelectionInCode(selection) ||
!($isCodeHighlightNode(anchorNode) || $isCodeTabNode(anchorNode)) || !($isCodeHighlightNode(anchorNode) || $isTabNode(anchorNode)) ||
!($isCodeHighlightNode(focusNode) || $isCodeTabNode(focusNode)) !($isCodeHighlightNode(focusNode) || $isTabNode(focusNode))
) { ) {
return false; return false;
} }
@ -731,7 +726,7 @@ function handleShiftLines(
const node = range[i]; const node = range[i];
if ( if (
!$isCodeHighlightNode(node) && !$isCodeHighlightNode(node) &&
!$isCodeTabNode(node) && !$isTabNode(node) &&
!$isLineBreakNode(node) !$isLineBreakNode(node)
) { ) {
return false; return false;
@ -759,7 +754,7 @@ function handleShiftLines(
const maybeInsertionPoint = const maybeInsertionPoint =
$isCodeHighlightNode(sibling) || $isCodeHighlightNode(sibling) ||
$isCodeTabNode(sibling) || $isTabNode(sibling) ||
$isLineBreakNode(sibling) $isLineBreakNode(sibling)
? arrowIsUp ? arrowIsUp
? getFirstCodeNodeOfLine(sibling) ? getFirstCodeNodeOfLine(sibling)
@ -801,8 +796,8 @@ function handleMoveTo(
const isMoveToStart = type === MOVE_TO_START; const isMoveToStart = type === MOVE_TO_START;
if ( if (
!($isCodeHighlightNode(anchorNode) || $isCodeTabNode(anchorNode)) || !($isCodeHighlightNode(anchorNode) || $isTabNode(anchorNode)) ||
!($isCodeHighlightNode(focusNode) || $isCodeTabNode(focusNode)) !($isCodeHighlightNode(focusNode) || $isTabNode(focusNode))
) { ) {
return false; return false;
} }
@ -830,18 +825,6 @@ function handleMoveTo(
return true; return true;
} }
function tabNodeTransform(node: TabNode): void {
if ($isCodeNode(node.getParent())) {
node.replace($createCodeTabNode());
}
}
function codeTabNodeTransform(node: CodeTabNode): void {
if (!$isCodeNode(node.getParent())) {
node.replace($createTabNode());
}
}
export function registerCodeHighlighting( export function registerCodeHighlighting(
editor: LexicalEditor, editor: LexicalEditor,
tokenizer?: Tokenizer, tokenizer?: Tokenizer,
@ -878,12 +861,6 @@ export function registerCodeHighlighting(
editor.registerNodeTransform(CodeHighlightNode, (node) => editor.registerNodeTransform(CodeHighlightNode, (node) =>
textNodeTransform(node, editor, tokenizer as Tokenizer), textNodeTransform(node, editor, tokenizer as Tokenizer),
), ),
editor.registerNodeTransform(TabNode, (node) => {
tabNodeTransform(node);
}),
editor.registerNodeTransform(CodeTabNode, (node) => {
codeTabNodeTransform(node);
}),
editor.registerCommand( editor.registerCommand(
KEY_TAB_COMMAND, KEY_TAB_COMMAND,
(event) => { (event) => {
@ -904,7 +881,7 @@ export function registerCodeHighlighting(
if (!$isSelectionInCode(selection)) { if (!$isSelectionInCode(selection)) {
return false; return false;
} }
$insertNodes([$createCodeTabNode()]); $insertNodes([$createTabNode()]);
return true; return true;
}, },
COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_LOW,

View File

@ -17,13 +17,9 @@ import type {
RangeSelection, RangeSelection,
SerializedElementNode, SerializedElementNode,
Spread, Spread,
TabNode,
} from 'lexical'; } from 'lexical';
import type {CodeHighlightNode, CodeTabNode} from '@lexical/code'; import type {CodeHighlightNode} from '@lexical/code';
import {
$isCodeHighlightNode,
$isCodeTabNode,
$createCodeTabNode,
} from '@lexical/code';
import 'prismjs/components/prism-clike'; import 'prismjs/components/prism-clike';
import 'prismjs/components/prism-javascript'; import 'prismjs/components/prism-javascript';
@ -46,8 +42,11 @@ import {
$createLineBreakNode, $createLineBreakNode,
$createParagraphNode, $createParagraphNode,
ElementNode, ElementNode,
$isTabNode,
$createTabNode,
} from 'lexical'; } from 'lexical';
import { import {
$isCodeHighlightNode,
$createCodeHighlightNode, $createCodeHighlightNode,
getFirstCodeNodeOfLine, getFirstCodeNodeOfLine,
} from './CodeHighlightNode'; } from './CodeHighlightNode';
@ -222,7 +221,7 @@ export class CodeNode extends ElementNode {
insertNewAfter( insertNewAfter(
selection: RangeSelection, selection: RangeSelection,
restoreSelection = true, restoreSelection = true,
): null | ParagraphNode | CodeHighlightNode | CodeTabNode { ): null | ParagraphNode | CodeHighlightNode | TabNode {
const children = this.getChildren(); const children = this.getChildren();
const childrenLength = children.length; const childrenLength = children.length;
@ -250,14 +249,14 @@ export class CodeNode extends ElementNode {
const firstSelectionNode = firstPoint.getNode(); const firstSelectionNode = firstPoint.getNode();
if ( if (
$isCodeHighlightNode(firstSelectionNode) || $isCodeHighlightNode(firstSelectionNode) ||
$isCodeTabNode(firstSelectionNode) $isTabNode(firstSelectionNode)
) { ) {
let node = getFirstCodeNodeOfLine(firstSelectionNode); let node = getFirstCodeNodeOfLine(firstSelectionNode);
const insertNodes = []; const insertNodes = [];
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
if ($isCodeTabNode(node)) { if ($isTabNode(node)) {
insertNodes.push($createCodeTabNode()); insertNodes.push($createTabNode());
node = node.getNextSibling(); node = node.getNextSibling();
} else if ($isCodeHighlightNode(node)) { } else if ($isCodeHighlightNode(node)) {
let spaces = 0; let spaces = 0;

View File

@ -1,70 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {
EditorConfig,
LexicalNode,
SerializedTabNode,
TextNode,
} from 'lexical';
import {$applyNodeReplacement, TabNode} from 'lexical';
export type SerializedCodeTabNode = SerializedTabNode;
/** @noInheritDoc */
export class CodeTabNode extends TabNode {
static getType(): string {
return 'code-tab';
}
static clone(node: CodeTabNode): CodeTabNode {
return new CodeTabNode(node.__key);
}
static importJSON(_serializedTabNode: SerializedCodeTabNode): CodeTabNode {
return $createCodeTabNode();
}
exportJSON(): SerializedTabNode {
return {
...super.exportJSON(),
type: 'code-tab',
version: 1,
};
}
createDOM(config: EditorConfig): HTMLElement {
const span = super.createDOM(config);
// TODO pass through theme
span.style.letterSpacing = '15px';
const text = span.firstChild;
if (text !== null) {
span.replaceChild(document.createTextNode(' '), text);
}
return span;
}
updateDOM(
prevNode: TextNode,
dom: HTMLElement,
config: EditorConfig,
): boolean {
return true;
}
}
export function $createCodeTabNode(): CodeTabNode {
return $applyNodeReplacement(new CodeTabNode());
}
export function $isCodeTabNode(
node: LexicalNode | null | undefined,
): node is CodeTabNode {
return node instanceof CodeTabNode;
}

View File

@ -9,7 +9,6 @@
import { import {
$createCodeNode, $createCodeNode,
$isCodeHighlightNode, $isCodeHighlightNode,
$isCodeTabNode,
registerCodeHighlighting, registerCodeHighlighting,
} from '@lexical/code'; } from '@lexical/code';
import {registerTabIndentation} from '@lexical/react/LexicalTabIndentationPlugin'; import {registerTabIndentation} from '@lexical/react/LexicalTabIndentationPlugin';
@ -24,6 +23,7 @@ import {
$getSelection, $getSelection,
$isLineBreakNode, $isLineBreakNode,
$isRangeSelection, $isRangeSelection,
$isTabNode,
$setSelection, $setSelection,
KEY_ARROW_DOWN_COMMAND, KEY_ARROW_DOWN_COMMAND,
KEY_ARROW_UP_COMMAND, KEY_ARROW_UP_COMMAND,
@ -180,15 +180,15 @@ describe('LexicalCodeNode tests', () => {
}); });
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent()); await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
expect(testEnv.innerHTML).toBe( expect(testEnv.innerHTML).toBe(
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span></code>', '<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span><span data-lexical-text="true">\t</span></code>',
); );
// CodeNode should only render diffs, make sure that the CodeTabNode is not cloned when // CodeNode should only render diffs, make sure that the TabNode is not cloned when
// appending more text // appending more text
let tabKey; let tabKey;
await editor.update(() => { await editor.update(() => {
tabKey = $dfs() tabKey = $dfs()
.find(({node}) => $isCodeTabNode(node)) .find(({node}) => $isTabNode(node))
.node.getKey(); .node.getKey();
$getSelection().insertText('foo'); $getSelection().insertText('foo');
}); });
@ -198,7 +198,7 @@ describe('LexicalCodeNode tests', () => {
}), }),
); );
expect(testEnv.innerHTML).toBe( expect(testEnv.innerHTML).toBe(
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">foo</span></code>', '<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">function</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">foo</span></code>',
); );
}); });
@ -221,7 +221,7 @@ describe('LexicalCodeNode tests', () => {
}); });
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent()); await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
expect(testEnv.innerHTML).toBe( expect(testEnv.innerHTML).toBe(
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">f</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span></code>', '<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">f</span><span data-lexical-text="true">\t</span></code>',
); );
}); });
@ -244,7 +244,7 @@ describe('LexicalCodeNode tests', () => {
}); });
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent()); await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
expect(testEnv.innerHTML).toBe( expect(testEnv.innerHTML).toBe(
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">function</span></code>', '<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">\t</span><span data-lexical-text="true">function</span></code>',
); );
await editor.update(() => { await editor.update(() => {
@ -280,7 +280,7 @@ describe('LexicalCodeNode tests', () => {
}); });
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent()); await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
expect(testEnv.innerHTML).toBe( expect(testEnv.innerHTML).toBe(
'<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">function</span></code>', '<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1"><span data-lexical-text="true">\t</span><span data-lexical-text="true">function</span></code>',
); );
await editor.update(() => { await editor.update(() => {
@ -322,13 +322,13 @@ describe('LexicalCodeNode tests', () => {
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent()); await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
expect(testEnv.innerHTML).toBe( expect(testEnv.innerHTML).toBe(
`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1 `<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1
2"><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">hello</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">world</span><br><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">hello</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">world</span></code>`, 2"><span data-lexical-text="true">\t</span><span data-lexical-text="true">hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span><br><span data-lexical-text="true">\t</span><span data-lexical-text="true">hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span></code>`,
); );
await editor.dispatchCommand(KEY_TAB_COMMAND, shiftTabKeyboardEvent()); await editor.dispatchCommand(KEY_TAB_COMMAND, shiftTabKeyboardEvent());
expect(testEnv.innerHTML).toBe( expect(testEnv.innerHTML).toBe(
`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1 `<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1
2"><span data-lexical-text="true">hello</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">world</span><br><span data-lexical-text="true">hello</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">world</span></code>`, 2"><span data-lexical-text="true">hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span><br><span data-lexical-text="true">hello</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">world</span></code>`,
); );
}); });
@ -347,7 +347,7 @@ describe('LexicalCodeNode tests', () => {
await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent()); await editor.dispatchCommand(KEY_TAB_COMMAND, tabKeyboardEvent());
expect(testEnv.innerHTML) expect(testEnv.innerHTML)
.toBe(`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1 .toBe(`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1
2"><span data-lexical-text="true">hello</span><br><span style="letter-spacing: 15px;" data-lexical-text="true"> </span></code>`); 2"><span data-lexical-text="true">hello</span><br><span data-lexical-text="true">\t</span></code>`);
}); });
test('can outdent at arbitrary points in the line (with tabs)', async () => { test('can outdent at arbitrary points in the line (with tabs)', async () => {
@ -390,7 +390,7 @@ describe('LexicalCodeNode tests', () => {
await editor.dispatchCommand(KEY_ARROW_UP_COMMAND, keyEvent); await editor.dispatchCommand(KEY_ARROW_UP_COMMAND, keyEvent);
expect(testEnv.innerHTML) expect(testEnv.innerHTML)
.toBe(`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1 .toBe(`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1
2"><span data-lexical-text="true">ghi</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">jkl</span><br><span data-lexical-text="true">abc</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">def</span></code>`); 2"><span data-lexical-text="true">ghi</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">jkl</span><br><span data-lexical-text="true">abc</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">def</span></code>`);
}); });
test('code blocks can shift multiple lines (with tab)', async () => { test('code blocks can shift multiple lines (with tab)', async () => {
@ -424,7 +424,7 @@ describe('LexicalCodeNode tests', () => {
expect(testEnv.innerHTML) expect(testEnv.innerHTML)
.toBe(`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1 .toBe(`<code spellcheck="false" data-highlight-language="javascript" dir="ltr" data-gutter="1
2 2
3"><span data-lexical-text="true">mno</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">pqr</span><br><span data-lexical-text="true">abc</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">def</span><br><span data-lexical-text="true">ghi</span><span style="letter-spacing: 15px;" data-lexical-text="true"> </span><span data-lexical-text="true">jkl</span></code>`); 3"><span data-lexical-text="true">mno</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">pqr</span><br><span data-lexical-text="true">abc</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">def</span><br><span data-lexical-text="true">ghi</span><span data-lexical-text="true">\t</span><span data-lexical-text="true">jkl</span></code>`);
}); });
describe('arrows', () => { describe('arrows', () => {
@ -493,7 +493,7 @@ describe('LexicalCodeNode tests', () => {
expect(selection.isCollapsed()).toBe(true); expect(selection.isCollapsed()).toBe(true);
if (moveTo === 'start') { if (moveTo === 'start') {
if (tabOrSpaces === 'tab') { if (tabOrSpaces === 'tab') {
expect($isCodeTabNode(selection.anchor.getNode())).toBe(true); expect($isTabNode(selection.anchor.getNode())).toBe(true);
expect( expect(
$isCodeHighlightNode( $isCodeHighlightNode(
selection.anchor.getNode().getNextSibling(), selection.anchor.getNode().getNextSibling(),
@ -533,7 +533,7 @@ describe('LexicalCodeNode tests', () => {
expect(selection.isCollapsed()).toBe(true); expect(selection.isCollapsed()).toBe(true);
if (moveTo === 'start') { if (moveTo === 'start') {
if (tabOrSpaces === 'tab') { if (tabOrSpaces === 'tab') {
expect($isCodeTabNode(selection.anchor.getNode())).toBe(true); expect($isTabNode(selection.anchor.getNode())).toBe(true);
expect( expect(
$isCodeHighlightNode( $isCodeHighlightNode(
selection.anchor.getNode().getNextSibling(), selection.anchor.getNode().getNextSibling(),
@ -645,7 +645,7 @@ describe('LexicalCodeNode tests', () => {
expect(selection.isCollapsed()).toBe(true); expect(selection.isCollapsed()).toBe(true);
if (moveTo === 'start') { if (moveTo === 'start') {
if (tabOrSpaces === 'tab') { if (tabOrSpaces === 'tab') {
expect($isCodeTabNode(selection.anchor.getNode())).toBe(true); expect($isTabNode(selection.anchor.getNode())).toBe(true);
expect( expect(
$isCodeHighlightNode( $isCodeHighlightNode(
selection.anchor.getNode().getNextSibling(), selection.anchor.getNode().getNextSibling(),
@ -690,7 +690,7 @@ describe('LexicalCodeNode tests', () => {
expect(selection.isCollapsed()).toBe(true); expect(selection.isCollapsed()).toBe(true);
if (moveTo === 'start') { if (moveTo === 'start') {
if (tabOrSpaces === 'tab') { if (tabOrSpaces === 'tab') {
expect($isCodeTabNode(selection.anchor.getNode())).toBe(true); expect($isTabNode(selection.anchor.getNode())).toBe(true);
expect( expect(
$isCodeHighlightNode( $isCodeHighlightNode(
selection.anchor.getNode().getNextSibling(), selection.anchor.getNode().getNextSibling(),
@ -731,7 +731,7 @@ describe('LexicalCodeNode tests', () => {
expect(selection.isCollapsed()).toBe(true); expect(selection.isCollapsed()).toBe(true);
if (moveTo === 'start') { if (moveTo === 'start') {
if (tabOrSpaces === 'tab') { if (tabOrSpaces === 'tab') {
expect($isCodeTabNode(selection.anchor.getNode())).toBe(true); expect($isTabNode(selection.anchor.getNode())).toBe(true);
expect( expect(
$isCodeHighlightNode( $isCodeHighlightNode(
selection.anchor.getNode().getNextSibling(), selection.anchor.getNode().getNextSibling(),
@ -772,7 +772,7 @@ describe('LexicalCodeNode tests', () => {
expect(selection.isCollapsed()).toBe(true); expect(selection.isCollapsed()).toBe(true);
if (moveTo === 'start') { if (moveTo === 'start') {
if (tabOrSpaces === 'tab') { if (tabOrSpaces === 'tab') {
expect($isCodeTabNode(selection.anchor.getNode())).toBe(true); expect($isTabNode(selection.anchor.getNode())).toBe(true);
expect( expect(
$isCodeHighlightNode( $isCodeHighlightNode(
selection.anchor.getNode().getNextSibling(), selection.anchor.getNode().getNextSibling(),
@ -818,7 +818,7 @@ describe('LexicalCodeNode tests', () => {
expect(selection.isCollapsed()).toBe(true); expect(selection.isCollapsed()).toBe(true);
if (moveTo === 'start') { if (moveTo === 'start') {
if (tabOrSpaces === 'tab') { if (tabOrSpaces === 'tab') {
expect($isCodeTabNode(selection.anchor.getNode())).toBe(true); expect($isTabNode(selection.anchor.getNode())).toBe(true);
expect( expect(
$isCodeHighlightNode( $isCodeHighlightNode(
selection.anchor.getNode().getNextSibling(), selection.anchor.getNode().getNextSibling(),

View File

@ -30,5 +30,3 @@ export {
} from './CodeHighlightNode'; } from './CodeHighlightNode';
export type {SerializedCodeNode} from './CodeNode'; export type {SerializedCodeNode} from './CodeNode';
export {$createCodeNode, $isCodeNode, CodeNode} from './CodeNode'; export {$createCodeNode, $isCodeNode, CodeNode} from './CodeNode';
export type {SerializedCodeTabNode} from './CodeTabNode';
export {$createCodeTabNode, $isCodeTabNode, CodeTabNode} from './CodeTabNode';

View File

@ -361,7 +361,7 @@ test.describe('CodeBlock', () => {
; ;
</span> </span>
<br /> <br />
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span <span
class="PlaygroundEditorTheme__tokenFunction" class="PlaygroundEditorTheme__tokenFunction"
data-lexical-text="true"> data-lexical-text="true">
@ -388,7 +388,7 @@ test.describe('CodeBlock', () => {
; ;
</span> </span>
<br /> <br />
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span <span
class="PlaygroundEditorTheme__tokenPunctuation" class="PlaygroundEditorTheme__tokenPunctuation"
data-lexical-text="true"> data-lexical-text="true">
@ -447,7 +447,7 @@ test.describe('CodeBlock', () => {
{ {
</span> </span>
<br /> <br />
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span <span
class="PlaygroundEditorTheme__tokenFunction" class="PlaygroundEditorTheme__tokenFunction"
data-lexical-text="true"> data-lexical-text="true">
@ -494,8 +494,8 @@ test.describe('CodeBlock', () => {
spellcheck="false" spellcheck="false"
data-gutter="123" data-gutter="123"
data-highlight-language="javascript"> data-highlight-language="javascript">
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span <span
class="PlaygroundEditorTheme__tokenAttr" class="PlaygroundEditorTheme__tokenAttr"
data-lexical-text="true"> data-lexical-text="true">
@ -520,9 +520,9 @@ test.describe('CodeBlock', () => {
{ {
</span> </span>
<br /> <br />
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span <span
class="PlaygroundEditorTheme__tokenFunction" class="PlaygroundEditorTheme__tokenFunction"
data-lexical-text="true"> data-lexical-text="true">
@ -544,8 +544,8 @@ test.describe('CodeBlock', () => {
; ;
</span> </span>
<br /> <br />
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span <span
class="PlaygroundEditorTheme__tokenPunctuation" class="PlaygroundEditorTheme__tokenPunctuation"
data-lexical-text="true"> data-lexical-text="true">
@ -567,7 +567,7 @@ test.describe('CodeBlock', () => {
spellcheck="false" spellcheck="false"
data-gutter="123" data-gutter="123"
data-highlight-language="javascript"> data-highlight-language="javascript">
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span <span
class="PlaygroundEditorTheme__tokenAttr" class="PlaygroundEditorTheme__tokenAttr"
data-lexical-text="true"> data-lexical-text="true">
@ -592,8 +592,8 @@ test.describe('CodeBlock', () => {
{ {
</span> </span>
<br /> <br />
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span <span
class="PlaygroundEditorTheme__tokenFunction" class="PlaygroundEditorTheme__tokenFunction"
data-lexical-text="true"> data-lexical-text="true">
@ -615,7 +615,7 @@ test.describe('CodeBlock', () => {
; ;
</span> </span>
<br /> <br />
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span <span
class="PlaygroundEditorTheme__tokenPunctuation" class="PlaygroundEditorTheme__tokenPunctuation"
data-lexical-text="true"> data-lexical-text="true">
@ -1091,10 +1091,10 @@ test.describe('CodeBlock', () => {
spellcheck="false" spellcheck="false"
data-gutter="12" data-gutter="12"
data-highlight-language="javascript"> data-highlight-language="javascript">
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span data-lexical-text="true">a b</span> <span data-lexical-text="true">a b</span>
<br /> <br />
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span data-lexical-text="true">c d</span> <span data-lexical-text="true">c d</span>
</code> </code>
`, `,

View File

@ -66,7 +66,7 @@ test.describe('Tab', () => {
spellcheck="false" spellcheck="false"
data-gutter="1" data-gutter="1"
data-highlight-language="javascript"> data-highlight-language="javascript">
<span style="letter-spacing: 15px" data-lexical-text="true"></span> <span data-lexical-text="true"></span>
<span <span
class="PlaygroundEditorTheme__tokenAttr" class="PlaygroundEditorTheme__tokenAttr"
data-lexical-text="true"> data-lexical-text="true">

View File

@ -8,7 +8,7 @@
import type {Klass, LexicalNode} from 'lexical'; import type {Klass, LexicalNode} from 'lexical';
import {CodeHighlightNode, CodeNode, CodeTabNode} from '@lexical/code'; import {CodeHighlightNode, CodeNode} from '@lexical/code';
import {HashtagNode} from '@lexical/hashtag'; import {HashtagNode} from '@lexical/hashtag';
import {AutoLinkNode, LinkNode} from '@lexical/link'; import {AutoLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list'; import {ListItemNode, ListNode} from '@lexical/list';
@ -67,7 +67,6 @@ const PlaygroundNodes: Array<Klass<LexicalNode>> = [
CollapsibleContainerNode, CollapsibleContainerNode,
CollapsibleContentNode, CollapsibleContentNode,
CollapsibleTitleNode, CollapsibleTitleNode,
CodeTabNode,
]; ];
export default PlaygroundNodes; export default PlaygroundNodes;

View File

@ -101,6 +101,7 @@
margin-bottom: 8px; margin-bottom: 8px;
overflow-x: auto; overflow-x: auto;
position: relative; position: relative;
tab-size: 2;
} }
.PlaygroundEditorTheme__code:before { .PlaygroundEditorTheme__code:before {
content: attr(data-gutter); content: attr(data-gutter);

View File

@ -18,7 +18,7 @@ import type {
SerializedTextNode, SerializedTextNode,
} from 'lexical'; } from 'lexical';
import {CodeHighlightNode, CodeNode, CodeTabNode} from '@lexical/code'; import {CodeHighlightNode, CodeNode} from '@lexical/code';
import {HashtagNode} from '@lexical/hashtag'; import {HashtagNode} from '@lexical/hashtag';
import {AutoLinkNode, LinkNode} from '@lexical/link'; import {AutoLinkNode, LinkNode} from '@lexical/link';
import {ListItemNode, ListNode} from '@lexical/list'; import {ListItemNode, ListNode} from '@lexical/list';
@ -422,7 +422,6 @@ const DEFAULT_NODES = [
ListItemNode, ListItemNode,
QuoteNode, QuoteNode,
CodeNode, CodeNode,
CodeTabNode,
TableNode, TableNode,
TableCellNode, TableCellNode,
TableRowNode, TableRowNode,