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
87 changes: 87 additions & 0 deletions src/renderer/editor/codemirror/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,48 @@ 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 previousNonBlankLineText = (
doc: import("@codemirror/state").Text,
lineNumber: number
): string | null => {
for (let currentLine = lineNumber - 1; currentLine >= 1; currentLine -= 1) {
const text = doc.line(currentLine).text;
if (text.trim() !== "") {
return text;
}
}
return null;
};

const isLikelyIndentedCodeLine = (
doc: import("@codemirror/state").Text,
lineNumber: number,
indent: string
): boolean => {
if (indent.length < 4) {
return false;
}
const previousLine = previousNonBlankLineText(doc, lineNumber);
return previousLine === null || !/^\s*(?:[-*+]|\d+[.)])\s+/.test(previousLine);
};

const getSelectedLineNumbers = (
view: import("@codemirror/view").EditorView
): { start: number; end: number } => {
Expand Down Expand Up @@ -456,6 +498,51 @@ export const toggleUnorderedListCommand = (
prefixForLine: () => "- ",
});

export const continueUnorderedListCommand = (
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) {
Comment on lines +510 to +511

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict Enter continuation to real list syntax

This handler treats any line matching ^\s*[-*+]\s+ as a list item without checking Markdown parse context, so pressing Enter inside fenced/indented code blocks (or other non-list contexts) that contain lines like - flag will incorrectly inject another bullet marker instead of a normal newline. That is a behavior regression introduced by this keybinding because it now rewrites non-list content based on plain text shape alone.

Useful? React with 👍 / 👎.

return false;
}

Comment on lines +510 to +514
const [, indent, marker, content] = match;
if (
/^(\s*)[-*+]\s+\[[ xX]\]\s+/.test(line.text) ||
isInsideFencedCode(view.state.doc, line.number) ||
isLikelyIndentedCodeLine(view.state.doc, line.number, indent)
) {
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${indent}${marker} `;
view.dispatch({
changes: { from, to, insert },
selection: { anchor: from + insert.length },
scrollIntoView: true,
});
return true;
};

export const toggleOrderedListCommand = (
view: import("@codemirror/view").EditorView
): boolean =>
Expand Down
11 changes: 9 additions & 2 deletions 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 { continueUnorderedListCommand } from "./commands";

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

export const buildBaseKeymap =
(): import("@codemirror/view").KeyBinding[] => [...searchKeymap];
(): import("@codemirror/view").KeyBinding[] => [
{
key: "Enter",
run: continueUnorderedListCommand,
},
...searchKeymap,
];
Comment on lines 40 to +47

export const renderModeExtension = (mode: RenderMode): Extension => {
if (mode === "hybrid") {
Expand All @@ -49,7 +56,7 @@ export const renderModeExtension = (mode: RenderMode): Extension => {
export const keymapExtension = (
bindings: import("@codemirror/view").KeyBinding[],
base: import("@codemirror/view").KeyBinding[]
): Extension => keymap.of([...bindings, ...base]);
): Extension => keymap.of([...base, ...bindings]);

export const createCmExtensions = (
options: ExtensionOptions
Expand Down
58 changes: 58 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 {
continueUnorderedListCommand,
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 @@ -147,6 +149,62 @@ runTest("unordered list command toggles selected lines", () => {
assert.equal(view.text, "one\ntwo");
});

runTest("enter continues unordered list markers", () => {
const view = new FakeView("- first", { from: 7, to: 7 });

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

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

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

const handled = continueUnorderedListCommand(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 outside unordered lists", () => {
const view = new FakeView("plain", { from: 5, to: 5 });

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

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

runTest("enter falls through for task list items", () => {
const view = new FakeView("- [ ] task", { from: 10, to: 10 });

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

assert.equal(handled, false);
assert.equal(view.text, "- [ ] task");
});

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

const handled = continueUnorderedListCommand(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 bullets", () => {
const view = new FakeView(" - flag", { from: 10, to: 10 });

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

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

runTest("line prefix commands preserve indentation when toggled off", () => {
const view = new FakeView(" - nested", { from: 0, to: 10 });

Expand Down
Loading