diff --git a/src/renderer/editor/codemirror/commands.ts b/src/renderer/editor/codemirror/commands.ts index cc37066..bbcd95a 100644 --- a/src/renderer/editor/codemirror/commands.ts +++ b/src/renderer/editor/codemirror/commands.ts @@ -464,6 +464,44 @@ export const toggleOrderedListCommand = ( prefixForLine: (lineIndex) => `${lineIndex + 1}. `, }); +export const continueOrderedListCommand = ( + 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*)(\d+)([.)])\s+(.*)$/); + if (!match) { + return false; + } + + const [, indent, numberText, delimiter, content] = match; + 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 nextNumber = Number.parseInt(numberText, 10) + 1; + const insert = `\n${indent}${nextNumber}${delimiter} `; + view.dispatch({ + changes: { from, to, insert }, + selection: { anchor: from + insert.length }, + scrollIntoView: true, + }); + return true; +}; + export const toggleTaskListCommand = ( view: import("@codemirror/view").EditorView ): boolean => diff --git a/src/renderer/editor/codemirror/extensions.ts b/src/renderer/editor/codemirror/extensions.ts index fa0aebd..96d506e 100644 --- a/src/renderer/editor/codemirror/extensions.ts +++ b/src/renderer/editor/codemirror/extensions.ts @@ -16,6 +16,7 @@ import { buildThemeExtension } from "./theme"; import { hybridMarkdown } from "./hybridMarkdown"; import { linkClickHandler } from "./links"; import { createReactSearchPanel } from "./searchPanel"; +import { continueOrderedListCommand } from "./commands"; type ExtensionOptions = { renderMode: RenderMode; @@ -37,7 +38,13 @@ export type ExtensionBundle = { }; export const buildBaseKeymap = - (): import("@codemirror/view").KeyBinding[] => [...searchKeymap]; + (): import("@codemirror/view").KeyBinding[] => [ + { + key: "Enter", + run: continueOrderedListCommand, + }, + ...searchKeymap, + ]; export const renderModeExtension = (mode: RenderMode): Extension => { if (mode === "hybrid") { @@ -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 diff --git a/tests/controllerShortcuts.test.ts b/tests/controllerShortcuts.test.ts index 46ff10c..b008a0d 100644 --- a/tests/controllerShortcuts.test.ts +++ b/tests/controllerShortcuts.test.ts @@ -1,5 +1,6 @@ import { strict as assert } from "assert"; import { + continueOrderedListCommand, createSnippetCommand, toggleBlockquoteCommand, toggleFencedCodeBlockCommand, @@ -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 @@ -163,6 +165,35 @@ runTest("ordered list command numbers selected lines", () => { assert.equal(view.text, "1. one\n2. two"); }); +runTest("enter continues ordered list numbers", () => { + const view = new FakeView("7. first", { from: 8, to: 8 }); + + const handled = continueOrderedListCommand(view as unknown as EditorView); + + assert.equal(handled, true); + assert.equal(view.text, "7. first\n8. "); + assert.deepEqual(view.state.selection.main, { from: 12, to: 12 }); +}); + +runTest("enter preserves ordered list delimiter style", () => { + const view = new FakeView("2) first", { from: 8, to: 8 }); + + const handled = continueOrderedListCommand(view as unknown as EditorView); + + assert.equal(handled, true); + assert.equal(view.text, "2) first\n3) "); +}); + +runTest("enter exits an empty ordered list item", () => { + const view = new FakeView(" 3. ", { from: 7, to: 7 }); + + const handled = continueOrderedListCommand(view as unknown as EditorView); + + assert.equal(handled, true); + assert.equal(view.text, " "); + assert.deepEqual(view.state.selection.main, { from: 2, to: 2 }); +}); + runTest("task list command toggles checklist markers", () => { const view = new FakeView("one\ntwo", { from: 0, to: 7 });