diff --git a/src/rich-text/commands/index.ts b/src/rich-text/commands/index.ts index d0597549..e5c94e0d 100644 --- a/src/rich-text/commands/index.ts +++ b/src/rich-text/commands/index.ts @@ -1,4 +1,9 @@ -import { setBlockType, toggleMark, wrapIn } from "prosemirror-commands"; +import { + setBlockType, + splitBlock, + toggleMark, + wrapIn, +} from "prosemirror-commands"; import { Mark, MarkType, NodeType, Schema } from "prosemirror-model"; import { Command, @@ -581,3 +586,26 @@ export function exitInclusiveMarkCommand( return true; } + +export function splitCodeBlockAtStartOfDoc( + state: EditorState, + dispatch: (tr: Transaction) => void +) { + const { $from } = state.selection; + const parent = $from.parent; + + if (parent.type.name !== "code_block") { + return false; + } + + if ($from.parentOffset !== 0) { + return false; + } + + // Is this code block the first child of the doc (i.e. no other nodes above it)? + if ($from.depth !== 1 || $from.index(0) !== 0) { + return false; + } + + return splitBlock(state, dispatch); +} diff --git a/src/rich-text/key-bindings.ts b/src/rich-text/key-bindings.ts index 962b4f41..a3422186 100644 --- a/src/rich-text/key-bindings.ts +++ b/src/rich-text/key-bindings.ts @@ -30,6 +30,7 @@ import { toggleHeadingLevel, toggleTagLinkCommand, toggleList, + splitCodeBlockAtStartOfDoc, exitInclusiveMarkCommand, } from "./commands"; @@ -42,6 +43,7 @@ export function allKeymaps( "Shift-Tab": unindentCodeBlockLinesCommand, "Mod-]": indentCodeBlockLinesCommand, "Mod-[": unindentCodeBlockLinesCommand, + "Enter": splitCodeBlockAtStartOfDoc, }); const tableKeymap = caseNormalizeKeymap({ @@ -91,8 +93,8 @@ export function allKeymaps( const keymaps = [ richTextKeymap, - caseNormalizeKeymap(baseKeymap), codeBlockKeymap, + caseNormalizeKeymap(baseKeymap), ]; if (parserFeatures.tables) { diff --git a/test/rich-text/commands/index.test.ts b/test/rich-text/commands/index.test.ts index bf31c10b..647296aa 100644 --- a/test/rich-text/commands/index.test.ts +++ b/test/rich-text/commands/index.test.ts @@ -7,6 +7,7 @@ import { toggleHeadingLevel, toggleTagLinkCommand, toggleWrapIn, + splitCodeBlockAtStartOfDoc, } from "../../../src/rich-text/commands"; import { applyNodeSelection, @@ -1255,4 +1256,92 @@ describe("commands", () => { }); }); }); + + describe("splitCodeBlockAtStartOfDoc", () => { + it("splits if the code block is first in the doc and the selection is at offset 0", () => { + // A doc where the *very first* node is a code block with text "Some code". + const state = createState("
Some code", []);
+
+ // We want to place the cursor at the very start of the code block.
+ const selectionAtStart = applySelection(state, 0, 0);
+
+ const { newState, isValid } = executeTransaction(
+ selectionAtStart,
+ splitCodeBlockAtStartOfDoc
+ );
+
+ // Because it's the first child + offset 0 in code_block, the command should handle it:
+ expect(isValid).toBe(true);
+
+ // The doc should now have an empty paragraph inserted above the code block.
+ expect(newState.doc.childCount).toBe(2);
+
+ // The first node should be an empty paragraph:
+ expect(newState.doc.firstChild.type.name).toBe("paragraph");
+ expect(newState.doc.firstChild.textContent).toBe("");
+
+ // The second node should be the code block with the original text:
+ expect(newState.doc.lastChild.type.name).toBe("code_block");
+ expect(newState.doc.lastChild.textContent).toBe("Some code");
+ });
+
+ it("returns false if selection is NOT at offset 0 (even if code block is first)", () => {
+ // Same doc as above - code block is first child.
+ const state = createState("Some code", []);
+
+ // Place the cursor in the middle of the code block this time.
+ const selectionInMiddle = applySelection(state, 5, 5);
+
+ const { newState, isValid } = executeTransaction(
+ selectionInMiddle,
+ splitCodeBlockAtStartOfDoc
+ );
+
+ // Should not handle it:
+ expect(isValid).toBe(false);
+ // Doc should remain unchanged.
+ expect(newState.doc.toString()).toEqual(state.doc.toString());
+ });
+
+ it("returns false if the code block is NOT the first child in the doc", () => {
+ // A doc with a paragraph first, THEN a code block.
+ const state = createState(
+ "Intro
Some code",
+ []
+ );
+
+ // Even if we place the cursor at offset 0 of the code block, it’s not the doc’s first child.
+ const selectionAtStartOfBlock = applySelection(state, 7, 7);
+
+ // First, verify that this test is actually valid - we want to be in the code_block at offset 0.
+ const sel = selectionAtStartOfBlock.selection;
+ expect(sel.$from.parent.type.name).toBe("code_block");
+ expect(sel.$from.parentOffset).toBe(0);
+
+ const { newState, isValid } = executeTransaction(
+ selectionAtStartOfBlock,
+ splitCodeBlockAtStartOfDoc
+ );
+
+ // Should not handle it because the code block isn’t the doc's first node:
+ expect(isValid).toBe(false);
+ expect(newState.doc.toString()).toEqual(state.doc.toString());
+ });
+
+ it("returns false if the parent node is not a code_block", () => {
+ // If the first node in the doc is a paragraph instead.
+ const state = createState("First paragraph
", []); + // Cursor at the start of that paragraph + const selectionInParagraph = applySelection(state, 0, 0); + + const { newState, isValid } = executeTransaction( + selectionInParagraph, + splitCodeBlockAtStartOfDoc + ); + + // Not a code block, so do nothing: + expect(isValid).toBe(false); + expect(newState.doc.toString()).toEqual(state.doc.toString()); + }); + }); });