Skip to content
Open
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
59 changes: 59 additions & 0 deletions src/renderer/editor/codemirror/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,23 @@ type LinePrefixOptions = {
prefixForLine: (lineIndex: number) => string;
};

const isFenceDelimiterLine = (text: string): boolean => {
return /^```/.test(text.trim());
};

const isInsideFencedCode = (
doc: import("@codemirror/state").Text,
lineNumber: number
): boolean => {
let inFence = false;
for (let currentLine = 1; currentLine < lineNumber; currentLine += 1) {
if (isFenceDelimiterLine(doc.line(currentLine).text)) {
inFence = !inFence;
}
}
return inFence;
};

const getSelectedLineNumbers = (
view: import("@codemirror/view").EditorView
): { start: number; end: number } => {
Expand Down Expand Up @@ -480,6 +497,48 @@ export const toggleBlockquoteCommand = (
prefixForLine: () => "> ",
});

export const continueBlockquoteCommand = (
view: import("@codemirror/view").EditorView
): boolean => {
const { from, to } = view.state.selection.main;
if (from !== to) {
return false;
}

const line = view.state.doc.lineAt(from);
const match = line.text.match(/^(\s*(?:>\s*)+)(.*)$/);
if (!match) {
return false;
}
Comment on lines +508 to +512

const [, marker, content] = match;
const indent = line.text.match(/^\s*/)?.[0] ?? "";
if (indent.length >= 4 || isInsideFencedCode(view.state.doc, line.number)) {
return false;
}

if (content.trim() === "") {
view.dispatch({
changes: {
from: line.from,
to: line.to,
insert: indent,
},
selection: { anchor: line.from + indent.length },
scrollIntoView: true,
});
return true;
}

const insert = `\n${marker}`;
view.dispatch({
changes: { from, to, insert },
selection: { anchor: from + insert.length },
scrollIntoView: true,
});
return true;
};

export const toggleFencedCodeBlockCommand = (
view: import("@codemirror/view").EditorView
): boolean => {
Expand Down
10 changes: 9 additions & 1 deletion src/renderer/editor/codemirror/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { buildThemeExtension } from "./theme";
import { hybridMarkdown } from "./hybridMarkdown";
import { linkClickHandler } from "./links";
import { createReactSearchPanel } from "./searchPanel";
import { continueBlockquoteCommand } from "./commands";

type ExtensionOptions = {
renderMode: RenderMode;
Expand All @@ -37,7 +38,14 @@ export type ExtensionBundle = {
};

export const buildBaseKeymap =
(): import("@codemirror/view").KeyBinding[] => [...searchKeymap];
(): import("@codemirror/view").KeyBinding[] => [
{
key: "Enter",
preventDefault: true,
run: continueBlockquoteCommand,
},
...searchKeymap,
];

export const renderModeExtension = (mode: RenderMode): Extension => {
if (mode === "hybrid") {
Expand Down
59 changes: 59 additions & 0 deletions tests/controllerShortcuts.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { strict as assert } from "assert";
import {
continueBlockquoteCommand,
createSnippetCommand,
toggleBlockquoteCommand,
toggleFencedCodeBlockCommand,
Expand Down Expand Up @@ -74,6 +75,7 @@ class FakeView {
| { from: number; to?: number; insert: string }
| Array<{ from: number; to?: number; insert: string }>;
selection?: { anchor: number; head?: number };
scrollIntoView?: boolean;
}): void {
const changes = Array.isArray(spec.changes)
? spec.changes
Expand Down Expand Up @@ -185,6 +187,63 @@ runTest("blockquote command toggles selected lines", () => {
assert.equal(view.text, "one\ntwo");
});

runTest("enter continues blockquotes", () => {
const view = new FakeView("> quote", { from: 7, to: 7 });

const handled = continueBlockquoteCommand(view as unknown as EditorView);

assert.equal(handled, true);
assert.equal(view.text, "> quote\n> ");
assert.deepEqual(view.state.selection.main, { from: 10, to: 10 });
});

runTest("enter preserves spaced nested blockquote markers", () => {
const view = new FakeView("> > nested", { from: 10, to: 10 });

const handled = continueBlockquoteCommand(view as unknown as EditorView);

assert.equal(handled, true);
assert.equal(view.text, "> > nested\n> > ");
assert.deepEqual(view.state.selection.main, { from: 15, to: 15 });
});

runTest("enter exits an empty blockquote", () => {
const view = new FakeView(" > ", { from: 6, to: 6 });

const handled = continueBlockquoteCommand(view as unknown as EditorView);

assert.equal(handled, true);
assert.equal(view.text, " ");
assert.deepEqual(view.state.selection.main, { from: 2, to: 2 });
});

runTest("enter falls through for fenced code lines shaped like quotes", () => {
const view = new FakeView("```\n> flag\n```", { from: 10, to: 10 });

const handled = continueBlockquoteCommand(view as unknown as EditorView);

assert.equal(handled, false);
assert.equal(view.text, "```\n> flag\n```");
});

runTest("enter falls through for indented code lines shaped like quotes", () => {
const view = new FakeView(" > flag", { from: 10, to: 10 });

const handled = continueBlockquoteCommand(view as unknown as EditorView);

assert.equal(handled, false);
assert.equal(view.text, " > flag");
});

runTest("enter falls through outside blockquotes", () => {
const view = new FakeView("plain", { from: 5, to: 5 });

const handled = continueBlockquoteCommand(view as unknown as EditorView);

assert.equal(handled, false);
assert.equal(view.text, "plain");
});

runTest("code block command wraps and unwraps selected lines", () => {
const view = new FakeView("const x = 1;", { from: 0, to: 12 });

Expand Down
Loading