@lexical/code-core 0.44.1-nightly.20260519.0 → 0.45.1-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,654 @@
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 {CodeHighlightNode} from './CodeHighlightNode';
10
+ import type {
11
+ BaseSelection,
12
+ LexicalCommand,
13
+ LexicalEditor,
14
+ LineBreakNode,
15
+ RangeSelection,
16
+ TabNode,
17
+ } from 'lexical';
18
+
19
+ import {effect, namedSignals} from '@lexical/extension';
20
+ import invariant from '@lexical/internal/invariant';
21
+ import {
22
+ $createLineBreakNode,
23
+ $createPoint,
24
+ $createTabNode,
25
+ $getCaretRange,
26
+ $getCaretRangeInDirection,
27
+ $getSelection,
28
+ $getSiblingCaret,
29
+ $getTextPointCaret,
30
+ $insertNodes,
31
+ $isLineBreakNode,
32
+ $isRangeSelection,
33
+ $isTabNode,
34
+ $normalizeCaret,
35
+ $setSelectionFromCaretRange,
36
+ COMMAND_PRIORITY_LOW,
37
+ defineExtension,
38
+ INDENT_CONTENT_COMMAND,
39
+ INSERT_TAB_COMMAND,
40
+ KEY_ARROW_DOWN_COMMAND,
41
+ KEY_ARROW_UP_COMMAND,
42
+ KEY_TAB_COMMAND,
43
+ mergeRegister,
44
+ MOVE_TO_END,
45
+ MOVE_TO_START,
46
+ OUTDENT_CONTENT_COMMAND,
47
+ safeCast,
48
+ } from 'lexical';
49
+
50
+ import {CodeExtension} from './CodeExtension';
51
+ import {$isCodeHighlightNode} from './CodeHighlightNode';
52
+ import {$isCodeNode} from './CodeNode';
53
+ import {
54
+ $getCodeLineDirection,
55
+ $getEndOfCodeInLine,
56
+ $getFirstCodeNodeOfLine,
57
+ $getLastCodeNodeOfLine,
58
+ $getStartOfCodeInLine,
59
+ $outdentLeadingSpaces,
60
+ } from './FlatStructureUtils';
61
+
62
+ function $isSelectionInCode(selection: null | BaseSelection): boolean {
63
+ if (!$isRangeSelection(selection)) {
64
+ return false;
65
+ }
66
+ const anchorNode = selection.anchor.getNode();
67
+ const maybeAnchorCodeNode = $isCodeNode(anchorNode)
68
+ ? anchorNode
69
+ : anchorNode.getParent();
70
+ const focusNode = selection.focus.getNode();
71
+ const maybeFocusCodeNode = $isCodeNode(focusNode)
72
+ ? focusNode
73
+ : focusNode.getParent();
74
+
75
+ return (
76
+ $isCodeNode(maybeAnchorCodeNode) &&
77
+ maybeAnchorCodeNode.is(maybeFocusCodeNode)
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Returns an Array of code lines
83
+ * Take the sequence of LineBreakNode | TabNode | CodeHighlightNode forming
84
+ * the selection and split it by LineBreakNode.
85
+ * If the selection ends at the start of the last line, it is considered empty.
86
+ * Empty lines are discarded.
87
+ */
88
+ function $getCodeLines(
89
+ selection: RangeSelection,
90
+ ): (CodeHighlightNode | TabNode)[][] {
91
+ const nodes = selection.getNodes();
92
+ const lines: (CodeHighlightNode | TabNode)[][] = [];
93
+ if (nodes.length === 1 && $isCodeNode(nodes[0])) {
94
+ return lines;
95
+ }
96
+ let lastLine: (CodeHighlightNode | TabNode)[] = [];
97
+ for (let i = 0; i < nodes.length; i++) {
98
+ const node = nodes[i];
99
+ invariant(
100
+ $isCodeHighlightNode(node) || $isTabNode(node) || $isLineBreakNode(node),
101
+ 'Expected selection to be inside CodeBlock and consisting of CodeHighlightNode, TabNode and LineBreakNode',
102
+ );
103
+ if ($isLineBreakNode(node)) {
104
+ if (lastLine.length > 0) {
105
+ lines.push(lastLine);
106
+ lastLine = [];
107
+ }
108
+ } else {
109
+ lastLine.push(node);
110
+ }
111
+ }
112
+ if (lastLine.length > 0) {
113
+ const selectionEnd = selection.isBackward()
114
+ ? selection.anchor
115
+ : selection.focus;
116
+
117
+ // Discard the last line if the selection ends exactly at the
118
+ // start of the line (no real selection)
119
+ const lastPoint = $createPoint(lastLine[0].getKey(), 0, 'text');
120
+ if (!selectionEnd.is(lastPoint)) {
121
+ lines.push(lastLine);
122
+ }
123
+ }
124
+
125
+ return lines;
126
+ }
127
+
128
+ function $handleTab(shiftKey: boolean): null | LexicalCommand<void> {
129
+ const selection = $getSelection();
130
+ if (!$isRangeSelection(selection) || !$isSelectionInCode(selection)) {
131
+ return null;
132
+ }
133
+ const indentOrOutdent = !shiftKey
134
+ ? INDENT_CONTENT_COMMAND
135
+ : OUTDENT_CONTENT_COMMAND;
136
+ const tabOrOutdent = !shiftKey ? INSERT_TAB_COMMAND : OUTDENT_CONTENT_COMMAND;
137
+
138
+ const anchor = selection.anchor;
139
+ const focus = selection.focus;
140
+
141
+ // 1. early decision when there is no real selection
142
+ if (anchor.is(focus)) {
143
+ return tabOrOutdent;
144
+ }
145
+
146
+ // 2. If only empty lines or multiple non-empty lines are selected: indent/outdent
147
+ const codeLines = $getCodeLines(selection);
148
+ if (codeLines.length !== 1) {
149
+ return indentOrOutdent;
150
+ }
151
+
152
+ const codeLine: (CodeHighlightNode | TabNode)[] = codeLines[0];
153
+ const codeLineLength = codeLine.length;
154
+
155
+ invariant(
156
+ codeLineLength !== 0,
157
+ '$getCodeLines only extracts non-empty lines',
158
+ );
159
+
160
+ // Take into account the direction of the selection
161
+ let selectionFirst;
162
+ let selectionLast;
163
+ if (selection.isBackward()) {
164
+ selectionFirst = focus;
165
+ selectionLast = anchor;
166
+ } else {
167
+ selectionFirst = anchor;
168
+ selectionLast = focus;
169
+ }
170
+
171
+ // find boundary elements of the line
172
+ // since codeLine only contains TabNode | CodeHighlightNode
173
+ // the result of these functions should is of Type TabNode | CodeHighlightNode
174
+ const firstOfLine = $getFirstCodeNodeOfLine(codeLine[0]);
175
+ const lastOfLine = $getLastCodeNodeOfLine(codeLine[0]);
176
+
177
+ const anchorOfLine = $createPoint(firstOfLine.getKey(), 0, 'text');
178
+ const focusOfLine = $createPoint(
179
+ lastOfLine.getKey(),
180
+ lastOfLine.getTextContentSize(),
181
+ 'text',
182
+ );
183
+
184
+ // 3. multiline because selection started strictly before the line
185
+ if (selectionFirst.isBefore(anchorOfLine)) {
186
+ return indentOrOutdent;
187
+ }
188
+
189
+ // 4. multiline because the selection stops strictly after the line
190
+ if (focusOfLine.isBefore(selectionLast)) {
191
+ return indentOrOutdent;
192
+ }
193
+
194
+ // The selection if within the line.
195
+ // 4. If it does not touch both borders, it needs a tab
196
+ if (
197
+ anchorOfLine.isBefore(selectionFirst) ||
198
+ selectionLast.isBefore(focusOfLine)
199
+ ) {
200
+ return tabOrOutdent;
201
+ }
202
+
203
+ // 5. Selection is matching a full line on non-empty code
204
+ return indentOrOutdent;
205
+ }
206
+
207
+ function $handleMultilineIndent(
208
+ type: LexicalCommand<void>,
209
+ tabSize?: number,
210
+ ): boolean {
211
+ const selection = $getSelection();
212
+ if (!$isRangeSelection(selection) || !$isSelectionInCode(selection)) {
213
+ return false;
214
+ }
215
+
216
+ const codeLines = $getCodeLines(selection);
217
+ const codeLinesLength = codeLines.length;
218
+
219
+ // Special Indent case
220
+ // Selection is collapsed at the beginning of a line
221
+ if (codeLinesLength === 0 && selection.isCollapsed()) {
222
+ if (type === INDENT_CONTENT_COMMAND) {
223
+ selection.insertNodes([$createTabNode()]);
224
+ }
225
+ return true;
226
+ }
227
+
228
+ // Special Indent case
229
+ // Selection is matching only one LineBreak
230
+ if (
231
+ codeLinesLength === 0 &&
232
+ type === INDENT_CONTENT_COMMAND &&
233
+ selection.getTextContent() === '\n'
234
+ ) {
235
+ const tabNode = $createTabNode();
236
+ const lineBreakNode = $createLineBreakNode();
237
+ const direction = selection.isBackward() ? 'previous' : 'next';
238
+ selection.insertNodes([tabNode, lineBreakNode]);
239
+ $setSelectionFromCaretRange(
240
+ $getCaretRangeInDirection(
241
+ $getCaretRange(
242
+ $getTextPointCaret(tabNode, 'next', 0),
243
+ $normalizeCaret($getSiblingCaret(lineBreakNode, 'next')),
244
+ ),
245
+ direction,
246
+ ),
247
+ );
248
+
249
+ return true;
250
+ }
251
+
252
+ // Indent Non Empty Lines
253
+ for (let i = 0; i < codeLinesLength; i++) {
254
+ const line = codeLines[i];
255
+ // a line here is never empty
256
+ if (line.length > 0) {
257
+ let firstOfLine: null | CodeHighlightNode | TabNode | LineBreakNode =
258
+ line[0];
259
+
260
+ // make sure to consider the first node on the first line
261
+ // because the line might not be fully selected
262
+ if (i === 0) {
263
+ firstOfLine = $getFirstCodeNodeOfLine(firstOfLine);
264
+ }
265
+
266
+ if (type === INDENT_CONTENT_COMMAND) {
267
+ const tabNode = $createTabNode();
268
+ firstOfLine.insertBefore(tabNode);
269
+ // First real code line may need selection adjustment
270
+ // when firstOfLine is at the selection boundary
271
+ if (i === 0) {
272
+ const anchorKey = selection.isBackward() ? 'focus' : 'anchor';
273
+ const anchorLine = $createPoint(firstOfLine.getKey(), 0, 'text');
274
+
275
+ if (selection[anchorKey].is(anchorLine)) {
276
+ selection[anchorKey].set(tabNode.getKey(), 0, 'text');
277
+ }
278
+ }
279
+ } else if ($isTabNode(firstOfLine)) {
280
+ firstOfLine.remove();
281
+ } else if (tabSize !== undefined && $isCodeHighlightNode(firstOfLine)) {
282
+ // Outdent space-indented lines (e.g. code formatted with prettier).
283
+ $outdentLeadingSpaces(firstOfLine, tabSize, selection);
284
+ }
285
+ }
286
+ }
287
+ return true;
288
+ }
289
+
290
+ function $handleShiftLines(
291
+ type: LexicalCommand<KeyboardEvent>,
292
+ event: KeyboardEvent,
293
+ ): boolean {
294
+ // We only care about the alt+arrow keys
295
+ const selection = $getSelection();
296
+ if (!$isRangeSelection(selection)) {
297
+ return false;
298
+ }
299
+
300
+ // I'm not quite sure why, but it seems like calling anchor.getNode() collapses the selection here
301
+ // So first, get the anchor and the focus, then get their nodes
302
+ const {anchor, focus} = selection;
303
+ const anchorOffset = anchor.offset;
304
+ const focusOffset = focus.offset;
305
+ const anchorNode = anchor.getNode();
306
+ const focusNode = focus.getNode();
307
+ const arrowIsUp = type === KEY_ARROW_UP_COMMAND;
308
+
309
+ // Ensure the selection is within the codeblock
310
+ if (
311
+ !$isSelectionInCode(selection) ||
312
+ !($isCodeHighlightNode(anchorNode) || $isTabNode(anchorNode)) ||
313
+ !($isCodeHighlightNode(focusNode) || $isTabNode(focusNode))
314
+ ) {
315
+ return false;
316
+ }
317
+ if (!event.altKey) {
318
+ // Handle moving selection out of the code block, given there are no
319
+ // siblings that can natively take the selection.
320
+ if (selection.isCollapsed()) {
321
+ const codeNode = anchorNode.getParentOrThrow();
322
+ if (
323
+ arrowIsUp &&
324
+ anchorOffset === 0 &&
325
+ anchorNode.getPreviousSibling() === null
326
+ ) {
327
+ const codeNodeSibling = codeNode.getPreviousSibling();
328
+ if (codeNodeSibling === null) {
329
+ codeNode.selectPrevious();
330
+ event.preventDefault();
331
+ return true;
332
+ }
333
+ } else if (
334
+ !arrowIsUp &&
335
+ anchorOffset === anchorNode.getTextContentSize() &&
336
+ anchorNode.getNextSibling() === null
337
+ ) {
338
+ const codeNodeSibling = codeNode.getNextSibling();
339
+ if (codeNodeSibling === null) {
340
+ codeNode.selectNext();
341
+ event.preventDefault();
342
+ return true;
343
+ }
344
+ }
345
+ }
346
+ return false;
347
+ }
348
+
349
+ let start;
350
+ let end;
351
+ if (anchorNode.isBefore(focusNode)) {
352
+ start = $getFirstCodeNodeOfLine(anchorNode);
353
+ end = $getLastCodeNodeOfLine(focusNode);
354
+ } else {
355
+ start = $getFirstCodeNodeOfLine(focusNode);
356
+ end = $getLastCodeNodeOfLine(anchorNode);
357
+ }
358
+ if (start == null || end == null) {
359
+ return false;
360
+ }
361
+
362
+ const range = start.getNodesBetween(end);
363
+ for (let i = 0; i < range.length; i++) {
364
+ const node = range[i];
365
+ if (
366
+ !$isCodeHighlightNode(node) &&
367
+ !$isTabNode(node) &&
368
+ !$isLineBreakNode(node)
369
+ ) {
370
+ return false;
371
+ }
372
+ }
373
+
374
+ // After this point, we know the selection is within the codeblock. We may not be able to
375
+ // actually move the lines around, but we want to return true either way to prevent
376
+ // the event's default behavior
377
+ event.preventDefault();
378
+ event.stopPropagation(); // required to stop cursor movement under Firefox
379
+
380
+ const linebreak = arrowIsUp
381
+ ? start.getPreviousSibling()
382
+ : end.getNextSibling();
383
+ if (!$isLineBreakNode(linebreak)) {
384
+ return true;
385
+ }
386
+ const sibling = arrowIsUp
387
+ ? linebreak.getPreviousSibling()
388
+ : linebreak.getNextSibling();
389
+ if (sibling == null) {
390
+ return true;
391
+ }
392
+
393
+ const maybeInsertionPoint =
394
+ $isCodeHighlightNode(sibling) ||
395
+ $isTabNode(sibling) ||
396
+ $isLineBreakNode(sibling)
397
+ ? arrowIsUp
398
+ ? $getFirstCodeNodeOfLine(sibling)
399
+ : $getLastCodeNodeOfLine(sibling)
400
+ : null;
401
+ let insertionPoint =
402
+ maybeInsertionPoint != null ? maybeInsertionPoint : sibling;
403
+ linebreak.remove();
404
+ range.forEach(node => node.remove());
405
+ if (type === KEY_ARROW_UP_COMMAND) {
406
+ range.forEach(node => insertionPoint.insertBefore(node));
407
+ insertionPoint.insertBefore(linebreak);
408
+ } else {
409
+ insertionPoint.insertAfter(linebreak);
410
+ insertionPoint = linebreak;
411
+ range.forEach(node => {
412
+ insertionPoint.insertAfter(node);
413
+ insertionPoint = node;
414
+ });
415
+ }
416
+
417
+ selection.setTextNodeRange(anchorNode, anchorOffset, focusNode, focusOffset);
418
+
419
+ return true;
420
+ }
421
+
422
+ function $handleMoveTo(
423
+ type: LexicalCommand<KeyboardEvent>,
424
+ event: KeyboardEvent,
425
+ ): boolean {
426
+ const selection = $getSelection();
427
+ if (!$isRangeSelection(selection)) {
428
+ return false;
429
+ }
430
+
431
+ const {anchor, focus} = selection;
432
+ const anchorNode = anchor.getNode();
433
+ const focusNode = focus.getNode();
434
+ const isMoveToStart = type === MOVE_TO_START;
435
+
436
+ // Ensure the selection is within the codeblock
437
+ if (
438
+ !$isSelectionInCode(selection) ||
439
+ !($isCodeHighlightNode(anchorNode) || $isTabNode(anchorNode)) ||
440
+ !($isCodeHighlightNode(focusNode) || $isTabNode(focusNode))
441
+ ) {
442
+ return false;
443
+ }
444
+
445
+ const focusLineNode = focusNode as CodeHighlightNode | TabNode;
446
+ const direction = $getCodeLineDirection(focusLineNode);
447
+ const moveToStart = direction === 'rtl' ? !isMoveToStart : isMoveToStart;
448
+
449
+ // Shift variant: let the non-shift branches resolve the target via
450
+ // framework helpers (`selectNext` / `selectStart` / `setTextNodeRange` /
451
+ // `node.select`), then restore the original anchor so we end up with an
452
+ // extended selection rather than a collapsed caret. This keeps point
453
+ // shapes (text vs. element) consistent between shift and non-shift.
454
+ const originalAnchorKey = anchor.key;
455
+ const originalAnchorOffset = anchor.offset;
456
+ const originalAnchorType = anchor.type;
457
+ if (moveToStart) {
458
+ const start = $getStartOfCodeInLine(focusLineNode, focus.offset);
459
+ if (start !== null) {
460
+ const {node, offset} = start;
461
+ if ($isLineBreakNode(node)) {
462
+ node.selectNext(0, 0);
463
+ } else {
464
+ selection.setTextNodeRange(node, offset, node, offset);
465
+ }
466
+ } else {
467
+ focusLineNode.getParentOrThrow().selectStart();
468
+ }
469
+ } else {
470
+ const node = $getEndOfCodeInLine(focusLineNode);
471
+ node.select();
472
+ }
473
+ if (event.shiftKey) {
474
+ selection.anchor.set(
475
+ originalAnchorKey,
476
+ originalAnchorOffset,
477
+ originalAnchorType,
478
+ );
479
+ }
480
+
481
+ event.preventDefault();
482
+ event.stopPropagation();
483
+
484
+ return true;
485
+ }
486
+
487
+ /**
488
+ * @internal
489
+ * Register the keyboard and command handlers that drive code-block
490
+ * indentation: Tab / Shift+Tab, INDENT/OUTDENT_CONTENT_COMMAND,
491
+ * INSERT_TAB_COMMAND, alt+arrow line shifting, and Home/End movement.
492
+ *
493
+ * Both `@lexical/code-shiki` and `@lexical/code-prism` use this via
494
+ * {@link CodeIndentExtension}; callers using `registerCodeHighlighting`
495
+ * will implicitly call this with tabSize of `undefined`.
496
+ *
497
+ * @param editor The editor to register on.
498
+ * @param tabSize When set, OUTDENT_CONTENT_COMMAND (Shift+Tab) also strips
499
+ * up to that many leading spaces from a code line. See
500
+ * {@link $outdentLeadingSpaces}.
501
+ */
502
+ export function registerCodeIndentation(
503
+ editor: LexicalEditor,
504
+ tabSize?: number,
505
+ ): () => void {
506
+ return mergeRegister(
507
+ editor.registerCommand(
508
+ KEY_TAB_COMMAND,
509
+ event => {
510
+ const command = $handleTab(event.shiftKey);
511
+ if (command === null) {
512
+ return false;
513
+ }
514
+ event.preventDefault();
515
+ editor.dispatchCommand(command, undefined);
516
+ return true;
517
+ },
518
+ COMMAND_PRIORITY_LOW,
519
+ ),
520
+ editor.registerCommand(
521
+ INSERT_TAB_COMMAND,
522
+ () => {
523
+ const selection = $getSelection();
524
+ if (!$isSelectionInCode(selection)) {
525
+ return false;
526
+ }
527
+ $insertNodes([$createTabNode()]);
528
+ return true;
529
+ },
530
+ COMMAND_PRIORITY_LOW,
531
+ ),
532
+ editor.registerCommand(
533
+ INDENT_CONTENT_COMMAND,
534
+ (): boolean => $handleMultilineIndent(INDENT_CONTENT_COMMAND),
535
+ COMMAND_PRIORITY_LOW,
536
+ ),
537
+ editor.registerCommand(
538
+ OUTDENT_CONTENT_COMMAND,
539
+ (): boolean => $handleMultilineIndent(OUTDENT_CONTENT_COMMAND, tabSize),
540
+ COMMAND_PRIORITY_LOW,
541
+ ),
542
+ editor.registerCommand(
543
+ KEY_ARROW_UP_COMMAND,
544
+ event => {
545
+ const selection = $getSelection();
546
+ if (!$isRangeSelection(selection)) {
547
+ return false;
548
+ }
549
+ const {anchor} = selection;
550
+ const anchorNode = anchor.getNode();
551
+ if (!$isSelectionInCode(selection)) {
552
+ return false;
553
+ }
554
+ // If at the start of a code block, prevent selection from moving out
555
+ if (
556
+ selection.isCollapsed() &&
557
+ anchor.offset === 0 &&
558
+ anchorNode.getPreviousSibling() === null &&
559
+ $isCodeNode(anchorNode.getParentOrThrow())
560
+ ) {
561
+ event.preventDefault();
562
+ return true;
563
+ }
564
+ return $handleShiftLines(KEY_ARROW_UP_COMMAND, event);
565
+ },
566
+ COMMAND_PRIORITY_LOW,
567
+ ),
568
+ editor.registerCommand(
569
+ KEY_ARROW_DOWN_COMMAND,
570
+ event => {
571
+ const selection = $getSelection();
572
+ if (!$isRangeSelection(selection)) {
573
+ return false;
574
+ }
575
+ const {anchor} = selection;
576
+ const anchorNode = anchor.getNode();
577
+ if (!$isSelectionInCode(selection)) {
578
+ return false;
579
+ }
580
+ // If at the end of a code block, prevent selection from moving out
581
+ if (
582
+ selection.isCollapsed() &&
583
+ anchor.offset === anchorNode.getTextContentSize() &&
584
+ anchorNode.getNextSibling() === null &&
585
+ $isCodeNode(anchorNode.getParentOrThrow())
586
+ ) {
587
+ event.preventDefault();
588
+ return true;
589
+ }
590
+ return $handleShiftLines(KEY_ARROW_DOWN_COMMAND, event);
591
+ },
592
+ COMMAND_PRIORITY_LOW,
593
+ ),
594
+ editor.registerCommand(
595
+ MOVE_TO_START,
596
+ event => $handleMoveTo(MOVE_TO_START, event),
597
+ COMMAND_PRIORITY_LOW,
598
+ ),
599
+ editor.registerCommand(
600
+ MOVE_TO_END,
601
+ event => $handleMoveTo(MOVE_TO_END, event),
602
+ COMMAND_PRIORITY_LOW,
603
+ ),
604
+ );
605
+ }
606
+
607
+ export interface CodeIndentConfig {
608
+ /**
609
+ * When true, the indent commands are not registered on the editor.
610
+ * This signal can be flipped at runtime to enable or disable indent
611
+ * handling without rebuilding the editor.
612
+ */
613
+ disabled: boolean;
614
+ /**
615
+ * When set, treats that many leading spaces on a code line as one indent
616
+ * level for the OUTDENT_CONTENT_COMMAND (Shift+Tab). See
617
+ * {@link registerCodeIndentation}. When undefined (the default), only
618
+ * TabNode removal is supported on outdent.
619
+ *
620
+ * Tab and INSERT_TAB_COMMAND continue to insert a TabNode regardless of
621
+ * this option.
622
+ */
623
+ tabSize: number | undefined;
624
+ }
625
+
626
+ /**
627
+ * Adds keyboard-driven indentation to code blocks (Tab / Shift+Tab,
628
+ * alt+arrow line shifts, Home/End within a line). Both
629
+ * {@link "@lexical/code-shiki".CodeShikiExtension} and
630
+ * {@link "@lexical/code-prism".CodePrismExtension} declare this as a
631
+ * dependency, so it is activated automatically alongside either
632
+ * highlighter.
633
+ *
634
+ * Code blocks without syntax highlighting can use this extension on its
635
+ * own.
636
+ */
637
+ export const CodeIndentExtension = defineExtension({
638
+ build: (editor, config) => namedSignals(config),
639
+ config: safeCast<CodeIndentConfig>({
640
+ disabled: false,
641
+ tabSize: undefined,
642
+ }),
643
+ dependencies: [CodeExtension],
644
+ name: '@lexical/code-indent',
645
+ register: (editor, config, state) => {
646
+ const stores = state.getOutput();
647
+ return effect(() => {
648
+ if (stores.disabled.value) {
649
+ return;
650
+ }
651
+ return registerCodeIndentation(editor, stores.tabSize.value);
652
+ });
653
+ },
654
+ });