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`
+
+
+
+ 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`
+
+
+
+ console
+
+ .
+
+
+ log
+
+
+ (
+
+
+ "test"
+
+
+ )
+
+
+ ;
+
+
+
+
+ `,
+ );
+
+ 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,