Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/rich-text/commands/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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);
}
4 changes: 3 additions & 1 deletion src/rich-text/key-bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
toggleHeadingLevel,
toggleTagLinkCommand,
toggleList,
splitCodeBlockAtStartOfDoc,
exitInclusiveMarkCommand,
} from "./commands";

Expand All @@ -42,6 +43,7 @@ export function allKeymaps(
"Shift-Tab": unindentCodeBlockLinesCommand,
"Mod-]": indentCodeBlockLinesCommand,
"Mod-[": unindentCodeBlockLinesCommand,
"Enter": splitCodeBlockAtStartOfDoc,
});

const tableKeymap = caseNormalizeKeymap({
Expand Down Expand Up @@ -91,8 +93,8 @@ export function allKeymaps(

const keymaps = [
richTextKeymap,
caseNormalizeKeymap(baseKeymap),
codeBlockKeymap,
caseNormalizeKeymap(baseKeymap),
];

if (parserFeatures.tables) {
Expand Down
89 changes: 89 additions & 0 deletions test/rich-text/commands/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
toggleHeadingLevel,
toggleTagLinkCommand,
toggleWrapIn,
splitCodeBlockAtStartOfDoc,
} from "../../../src/rich-text/commands";
import {
applyNodeSelection,
Expand Down Expand Up @@ -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("<pre><code>Some code</code></pre>", []);

// 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("<pre><code>Some code</code></pre>", []);

// 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(
"<p>Intro</p><pre><code>Some code</code></pre>",
[]
);

// 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("<p>First paragraph</p>", []);
// 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());
});
});
});