diff --git a/packages/lexical-playground/__tests__/e2e/CodeActionMenu.spec.mjs b/packages/lexical-playground/__tests__/e2e/CodeActionMenu.spec.mjs index 926e842da5e..cc67bb9eb22 100644 --- a/packages/lexical-playground/__tests__/e2e/CodeActionMenu.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CodeActionMenu.spec.mjs @@ -195,6 +195,7 @@ test.describe('CodeActionMenu', () => { await assertHTML( page, ` +


{ 'Hello World' +


`, ); @@ -226,6 +228,7 @@ test.describe('CodeActionMenu', () => { await assertHTML( page, ` +


{

+


`, ); }); @@ -271,6 +275,7 @@ test.describe('CodeActionMenu', () => { await assertHTML( page, ` +


{ 'Hello World' +


`, ); diff --git a/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs b/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs index 150f5cbc43e..ac0db5f5965 100644 --- a/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/CodeBlock.spec.mjs @@ -38,13 +38,14 @@ test.describe('CodeBlock', () => { if (isRichText) { await assertSelection(page, { anchorOffset: 1, - anchorPath: [0, 4, 0], + anchorPath: [1, 4, 0], focusOffset: 1, - focusPath: [0, 4, 0], + focusPath: [1, 4, 0], }); await assertHTML( page, html` +


{ ; +


`, ); // Remove code block (back to a normal paragraph) and check that highlights are converted into regular text await moveToEditorBeginning(page); + await page.keyboard.press('ArrowRight'); await page.keyboard.press('Backspace'); await assertHTML( page, @@ -90,6 +93,7 @@ test.describe('CodeBlock', () => {

alert(1);

+


`, ); } else { @@ -115,13 +119,14 @@ test.describe('CodeBlock', () => { if (isRichText) { await assertSelection(page, { anchorOffset: 0, - anchorPath: [0, 0, 0], + anchorPath: [1, 0, 0], focusOffset: 0, - focusPath: [0, 0, 0], + focusPath: [1, 0, 0], }); await assertHTML( page, html` +


{ ; +


`, ); } else { @@ -210,6 +216,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{
meh
+


`, ); }); @@ -292,6 +300,7 @@ test.describe('CodeBlock', () => {
meh +


`, ); }); @@ -306,6 +315,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ from users +


`, ); await click(page, '.toolbar-item.code-language'); @@ -328,6 +339,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ users +


`, ); } else { @@ -385,6 +398,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ ; +


`, ); }); @@ -481,6 +496,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ ; +


`, ); }); @@ -566,6 +583,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ } +


`, ); await page.keyboard.down('Shift'); @@ -640,6 +659,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ } +


`, ); await page.keyboard.down('Shift'); @@ -728,6 +749,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ } +


`, ); await click(page, '.toolbar-item.alignment'); @@ -807,6 +830,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ } +


`, ); }); @@ -875,6 +900,7 @@ test.describe('CodeBlock', () => { }) => { test.skip(isPlainText); const abcHTML = html` +


{ ; +


`; const bcaHTML = html` +


{ ; +


`; const endOfFirstLine = { anchorOffset: 1, - anchorPath: [0, 3, 0], + anchorPath: [1, 3, 0], focusOffset: 1, - focusPath: [0, 3, 0], + focusPath: [1, 3, 0], }; const endOfLastLine = { anchorOffset: 1, - anchorPath: [0, 13, 0], + anchorPath: [1, 13, 0], focusOffset: 1, - focusPath: [0, 13, 0], + focusPath: [1, 13, 0], }; await focusEditor(page); await page.keyboard.type('``` a();\nb();\nc();'); @@ -1072,9 +1101,9 @@ test.describe('CodeBlock', () => { await page.keyboard.press('ArrowUp'); await assertSelection(page, { anchorOffset: 0, - anchorPath: [0, 0, 0], + anchorPath: [1, 0, 0], focusOffset: 0, - focusPath: [0, 0, 0], + focusPath: [1, 0, 0], }); // Test 2: Typing at start stays within code block @@ -1083,6 +1112,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ ; +


`, ); // Let's verify the cursor position after typing the start comment await assertSelection(page, { anchorOffset: 0, - anchorPath: [0, 2, 0], + anchorPath: [1, 2, 0], focusOffset: 0, - focusPath: [0, 2, 0], + focusPath: [1, 2, 0], }); // Test 3: Selection stays at end when pressing down @@ -1145,6 +1176,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ // end +


`, ); await page.keyboard.press('ArrowDown'); await assertSelection(page, { anchorOffset: 6, - anchorPath: [0, 10, 0], + anchorPath: [1, 10, 0], focusOffset: 6, - focusPath: [0, 10, 0], + focusPath: [1, 10, 0], }); // Verify no content escaped the code block const paragraphs = await page.$$('p'); - expect(paragraphs.length).toBe(0); + expect(paragraphs.length).toBe(2); // Should have 2 paragraphs (before and after code block) }); test('When pressing CMD/Ctrl + Left, CMD/Ctrl + Right, the cursor should go to the start of the code', async ({ @@ -1230,6 +1263,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, ` +


{ c d +


`, ); await selectCharacters(page, 'left', 11); await assertSelection(page, { anchorOffset: 5, - anchorPath: [0, 4, 0], + anchorPath: [1, 4, 0], focusOffset: 1, - focusPath: [0, 1, 0], + focusPath: [1, 1, 0], }); await moveToStart(page); await assertSelection(page, { anchorOffset: 0, - anchorPath: [0, 0, 0], + anchorPath: [1, 0, 0], focusOffset: 0, - focusPath: [0, 0, 0], + focusPath: [1, 0, 0], }); await moveToEnd(page); await assertSelection(page, { anchorOffset: 5, - anchorPath: [0, 1, 0], + anchorPath: [1, 1, 0], focusOffset: 5, - focusPath: [0, 1, 0], + focusPath: [1, 1, 0], }); await moveToStart(page); await assertSelection(page, { anchorOffset: 1, - anchorPath: [0, 1, 0], + anchorPath: [1, 1, 0], focusOffset: 1, - focusPath: [0, 1, 0], + focusPath: [1, 1, 0], }); await selectCharacters(page, 'right', 11); await assertSelection(page, { anchorOffset: 1, - anchorPath: [0, 1, 0], + anchorPath: [1, 1, 0], focusOffset: 5, - focusPath: [0, 4, 0], + focusPath: [1, 4, 0], }); await moveToEnd(page); await assertSelection(page, { anchorOffset: 5, - anchorPath: [0, 4, 0], + anchorPath: [1, 4, 0], focusOffset: 5, - focusPath: [0, 4, 0], + focusPath: [1, 4, 0], }); }); @@ -1307,6 +1342,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ data-lexical-text="true"> let d = 4; +


`, ); } else { @@ -1367,6 +1404,7 @@ test.describe('CodeBlock', () => { await assertHTML( page, html` +


{ +


{ code


+


diff --git a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs index 1320f0d655d..ee93eb099e4 100644 --- a/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Markdown.spec.mjs @@ -106,7 +106,7 @@ test.describe.parallel('Markdown', () => { }, { expectation: - '
', + '




', importExpectation: '', isBlockTest: true, markdownImport: '', diff --git a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs index a0513a361f7..06bf48b492a 100644 --- a/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Selection.spec.mjs @@ -156,6 +156,7 @@ test.describe.parallel('Selection', () => {

Line1

+


{ data-language="javascript"> Line2 +


`, ); }); diff --git a/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs b/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs index 200fd9a0652..79deeb8de73 100644 --- a/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs +++ b/packages/lexical-playground/__tests__/e2e/Tab.spec.mjs @@ -101,6 +101,7 @@ test.describe('Tab', () => { await assertHTML( page, html` +


{ function +


`, ); }); diff --git a/packages/lexical-playground/__tests__/regression/1384-insert-nodes.spec.mjs b/packages/lexical-playground/__tests__/regression/1384-insert-nodes.spec.mjs index 3622c1af932..7a02631ceb4 100644 --- a/packages/lexical-playground/__tests__/regression/1384-insert-nodes.spec.mjs +++ b/packages/lexical-playground/__tests__/regression/1384-insert-nodes.spec.mjs @@ -44,6 +44,7 @@ test.describe('Regression test #1384', () => { await assertHTML( page, html` +


{ ; +


`, ); }); diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx index 1284efb2da1..148b93ae809 100644 --- a/packages/lexical-playground/src/Editor.tsx +++ b/packages/lexical-playground/src/Editor.tsx @@ -46,6 +46,7 @@ import AutoLinkPlugin from './plugins/AutoLinkPlugin'; import CodeActionMenuPlugin from './plugins/CodeActionMenuPlugin'; import CodeHighlightPrismPlugin from './plugins/CodeHighlightPrismPlugin'; import CodeHighlightShikiPlugin from './plugins/CodeHighlightShikiPlugin'; +import CodePlugin from './plugins/CodePlugin'; import CollapsiblePlugin from './plugins/CollapsiblePlugin'; import CommentPlugin from './plugins/CommentPlugin'; import ComponentPickerPlugin from './plugins/ComponentPickerPlugin'; @@ -197,6 +198,7 @@ export default function Editor(): JSX.Element { )} {isRichText ? ( <> + {isCollab ? ( useCollabV2 ? ( <> diff --git a/packages/lexical-playground/src/plugins/CodePlugin/index.tsx b/packages/lexical-playground/src/plugins/CodePlugin/index.tsx new file mode 100644 index 00000000000..5280756c918 --- /dev/null +++ b/packages/lexical-playground/src/plugins/CodePlugin/index.tsx @@ -0,0 +1,51 @@ +/** + * 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 {CodeNode} from '@lexical/code'; +import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import {$createParagraphNode, $getRoot} from 'lexical'; +import {useEffect} from 'react'; + +export default function CodePlugin(): null { + const [editor] = useLexicalComposerContext(); + + useEffect(() => { + if (!editor.hasNodes([CodeNode])) { + return; + } + + return editor.registerNodeTransform(CodeNode, (codeNode) => { + // Skip if not a direct child of root + if (codeNode.getParent() !== $getRoot()) { + return codeNode; + } + + // Skip if this is markdown mode (single CodeNode with language 'markdown') + if (codeNode.getLanguage() === 'markdown') { + const root = $getRoot(); + const children = root.getChildren(); + if (children.length === 1) { + return codeNode; + } + } + + // Ensure there is a paragraph node after the code node + if (!codeNode.getNextSibling()) { + codeNode.insertAfter($createParagraphNode()); + } + + // Ensure there is a paragraph node before the code node + if (!codeNode.getPreviousSibling()) { + codeNode.insertBefore($createParagraphNode()); + } + + return codeNode; + }); + }, [editor]); + + return null; +}