diff --git a/packages/lexical-code/src/CodeHighlighter.ts b/packages/lexical-code/src/CodeHighlighter.ts index c023b80b7..9a48a3063 100644 --- a/packages/lexical-code/src/CodeHighlighter.ts +++ b/packages/lexical-code/src/CodeHighlighter.ts @@ -880,22 +880,64 @@ export function registerCodeHighlighting( ), editor.registerCommand( KEY_ARROW_UP_COMMAND, - (payload): boolean => $handleShiftLines(KEY_ARROW_UP_COMMAND, payload), + (event) => { + const selection = $getSelection(); + if (!$isRangeSelection(selection)) { + return false; + } + const {anchor} = selection; + const anchorNode = anchor.getNode(); + if (!$isSelectionInCode(selection)) { + return false; + } + // If at the start of a code block, prevent selection from moving out + if ( + selection.isCollapsed() && + anchor.offset === 0 && + anchorNode.getPreviousSibling() === null && + $isCodeNode(anchorNode.getParentOrThrow()) + ) { + event.preventDefault(); + return true; + } + return $handleShiftLines(KEY_ARROW_UP_COMMAND, event); + }, COMMAND_PRIORITY_LOW, ), editor.registerCommand( KEY_ARROW_DOWN_COMMAND, - (payload): boolean => $handleShiftLines(KEY_ARROW_DOWN_COMMAND, payload), - COMMAND_PRIORITY_LOW, - ), - editor.registerCommand( - MOVE_TO_END, - (payload): boolean => $handleMoveTo(MOVE_TO_END, payload), + (event) => { + const selection = $getSelection(); + if (!$isRangeSelection(selection)) { + return false; + } + const {anchor} = selection; + const anchorNode = anchor.getNode(); + if (!$isSelectionInCode(selection)) { + return false; + } + // If at the end of a code block, prevent selection from moving out + if ( + selection.isCollapsed() && + anchor.offset === anchorNode.getTextContentSize() && + anchorNode.getNextSibling() === null && + $isCodeNode(anchorNode.getParentOrThrow()) + ) { + event.preventDefault(); + return true; + } + return $handleShiftLines(KEY_ARROW_DOWN_COMMAND, event); + }, COMMAND_PRIORITY_LOW, ), editor.registerCommand( MOVE_TO_START, - (payload): boolean => $handleMoveTo(MOVE_TO_START, payload), + (event) => $handleMoveTo(MOVE_TO_START, event as KeyboardEvent), + COMMAND_PRIORITY_LOW, + ), + editor.registerCommand( + MOVE_TO_END, + (event) => $handleMoveTo(MOVE_TO_END, event as KeyboardEvent), COMMAND_PRIORITY_LOW, ), ); diff --git a/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs b/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs index dab99d16c..29cc42775 100644 --- a/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs @@ -17,6 +17,7 @@ import { assertHTML, assertSelection, click, + expect, focusEditor, html, initialize, @@ -916,6 +917,162 @@ test.describe('CodeBlock', () => { await assertHTML(page, bcaHTML); }); + test('prevents selection and typing outside code block boundaries', async ({ + page, + isPlainText, + }) => { + test.skip(isPlainText); + + await focusEditor(page); + await page.keyboard.type('console.log("test");'); + await selectAll(page); + await toggleCodeBlock(page); + + // Test 1: Selection stays at start when pressing up + await moveToStart(page); + await page.keyboard.press('ArrowUp'); + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [0, 0, 0], + focusOffset: 0, + focusPath: [0, 0, 0], + }); + + // Test 2: Typing at start stays within code block + await page.keyboard.type('// start'); + await page.keyboard.press('Enter'); + await assertHTML( + page, + html` + + + // start + +
+ console + + . + + + log + + + ( + + + "test" + + + ) + + + ; + +
+ `, + ); + + // Let's verify the cursor position after typing the start comment + await assertSelection(page, { + anchorOffset: 0, + anchorPath: [0, 2, 0], + focusOffset: 0, + focusPath: [0, 2, 0], + }); + + // Test 3: Selection stays at end when pressing down + await moveToEnd(page); + await page.keyboard.type(' // end'); + await assertHTML( + page, + html` + + + // start + +
+ console + + . + + + log + + + ( + + + "test" + + + ) + + + ; + + + + // end + +
+ `, + ); + + await page.keyboard.press('ArrowDown'); + await assertSelection(page, { + anchorOffset: 6, + anchorPath: [0, 10, 0], + focusOffset: 6, + focusPath: [0, 10, 0], + }); + + // Verify no content escaped the code block + const paragraphs = await page.$$('p'); + expect(paragraphs.length).toBe(0); + }); + test('When pressing CMD/Ctrl + Left, CMD/Ctrl + Right, the cursor should go to the start of the code', async ({ page, isPlainText,