diff --git a/src/renderer/commands/commandSystem.ts b/src/renderer/commands/commandSystem.ts
index cbdb55f..1977fe8 100644
--- a/src/renderer/commands/commandSystem.ts
+++ b/src/renderer/commands/commandSystem.ts
@@ -6,6 +6,7 @@ import {
createWrapSelectionOrWordCommand,
insertHorizontalRuleCommand,
toggleBlockquoteCommand,
+ toggleTaskCheckCommand,
toggleFencedCodeBlockCommand,
toggleOrderedListCommand,
toggleTaskListCommand,
@@ -34,6 +35,7 @@ export type CommandId =
| "insert.unorderedList"
| "insert.orderedList"
| "insert.taskList"
+ | "insert.taskCheck"
| "insert.blockquote"
| "insert.codeBlock"
| "theme.set"
@@ -58,6 +60,7 @@ export type CommandArgs = {
"insert.unorderedList": void;
"insert.orderedList": void;
"insert.taskList": void;
+ "insert.taskCheck": void;
"insert.blockquote": void;
"insert.codeBlock": void;
"theme.set": { theme: ThemeName };
@@ -179,6 +182,7 @@ const editorCommands = {
unorderedList: toggleUnorderedListCommand,
orderedList: toggleOrderedListCommand,
taskList: toggleTaskListCommand,
+ taskCheck: toggleTaskCheckCommand,
blockquote: toggleBlockquoteCommand,
codeBlock: toggleFencedCodeBlockCommand,
} as const;
@@ -365,6 +369,17 @@ export const createCommandRegistry = (): CommandRegistry => {
return view ? editorCommands.taskList(view) : false;
},
},
+ {
+ id: "insert.taskCheck",
+ title: "Toggle task check",
+ category: "Insert",
+ description: "Toggle checked state for task checklist items.",
+ requiresEditor: true,
+ run: (ctx) => {
+ const view = ctx.getEditorView();
+ return view ? editorCommands.taskCheck(view) : false;
+ },
+ },
{
id: "insert.blockquote",
title: "Quote",
diff --git a/src/renderer/components/EditorContextMenu.tsx b/src/renderer/components/EditorContextMenu.tsx
index 33f05ef..d43d120 100644
--- a/src/renderer/components/EditorContextMenu.tsx
+++ b/src/renderer/components/EditorContextMenu.tsx
@@ -196,6 +196,12 @@ export function EditorContextMenu({
Task list
{shortcuts.taskList}
+ runCommand("insert.taskCheck")}
+ >
+ Toggle task check
+
runCommand("insert.blockquote")}
diff --git a/src/renderer/editor/codemirror/commands.ts b/src/renderer/editor/codemirror/commands.ts
index cc37066..9d9a04e 100644
--- a/src/renderer/editor/codemirror/commands.ts
+++ b/src/renderer/editor/codemirror/commands.ts
@@ -472,6 +472,43 @@ export const toggleTaskListCommand = (
prefixForLine: () => "- [ ] ",
});
+export const toggleTaskCheckCommand = (
+ view: import("@codemirror/view").EditorView
+): boolean => {
+ const { start, end } = getSelectedLineNumbers(view);
+ const taskLines = [];
+
+ for (let lineNumber = start; lineNumber <= end; lineNumber += 1) {
+ const line = view.state.doc.line(lineNumber);
+ const match = line.text.match(/^(\s*[-*+]\s+\[)([ xX])(\]\s+)/);
+ if (match) {
+ taskLines.push({ line, match });
+ }
+ }
+
+ if (taskLines.length === 0) {
+ return false;
+ }
+
+ const shouldUncheck = taskLines.every(({ match }) =>
+ /[xX]/.test(match[2])
+ );
+ const changes = taskLines.map(({ line, match }) => {
+ const markerOffset = match[1].length;
+ return {
+ from: line.from + markerOffset,
+ to: line.from + markerOffset + 1,
+ insert: shouldUncheck ? " " : "x",
+ };
+ });
+
+ view.dispatch({
+ changes,
+ scrollIntoView: true,
+ });
+ return true;
+};
+
export const toggleBlockquoteCommand = (
view: import("@codemirror/view").EditorView
): boolean =>
diff --git a/tests/controllerShortcuts.test.ts b/tests/controllerShortcuts.test.ts
index 46ff10c..2ef26e6 100644
--- a/tests/controllerShortcuts.test.ts
+++ b/tests/controllerShortcuts.test.ts
@@ -4,6 +4,7 @@ import {
toggleBlockquoteCommand,
toggleFencedCodeBlockCommand,
toggleOrderedListCommand,
+ toggleTaskCheckCommand,
toggleTaskListCommand,
toggleUnorderedListCommand,
} from "../src/renderer/editor/codemirror/commands";
@@ -174,6 +175,33 @@ runTest("task list command toggles checklist markers", () => {
assert.equal(view.text, "one\ntwo");
});
+runTest("task check command toggles unchecked tasks on", () => {
+ const view = new FakeView("- [ ] one\n- [x] two", { from: 0, to: 19 });
+
+ const handled = toggleTaskCheckCommand(view as unknown as EditorView);
+
+ assert.equal(handled, true);
+ assert.equal(view.text, "- [x] one\n- [x] two");
+});
+
+runTest("task check command toggles checked tasks off", () => {
+ const view = new FakeView("- [x] one\n- [X] two", { from: 0, to: 19 });
+
+ const handled = toggleTaskCheckCommand(view as unknown as EditorView);
+
+ assert.equal(handled, true);
+ assert.equal(view.text, "- [ ] one\n- [ ] two");
+});
+
+runTest("task check command falls through outside task lines", () => {
+ const view = new FakeView("- one", { from: 0, to: 5 });
+
+ const handled = toggleTaskCheckCommand(view as unknown as EditorView);
+
+ assert.equal(handled, false);
+ assert.equal(view.text, "- one");
+});
+
runTest("blockquote command toggles selected lines", () => {
const view = new FakeView("one\ntwo", { from: 0, to: 7 });