From 43adb13fb2400ac82b2ef76925936521832b5416 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Fri, 26 Jan 2024 11:10:35 +0100 Subject: [PATCH] Fixes #330 --- plug-api/silverbullet-syscall/editor.ts | 8 +++++++ plugs/tasks/task.ts | 32 +++++++++++++++++++++++++ plugs/tasks/tasks.plug.yaml | 7 +++++- web/cm_plugins/task.ts | 6 +++-- web/syscalls/cm_util.ts | 23 ++++++++++++++++++ web/syscalls/editor.ts | 8 +++++++ 6 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 web/syscalls/cm_util.ts diff --git a/plug-api/silverbullet-syscall/editor.ts b/plug-api/silverbullet-syscall/editor.ts index 5f1b91e1..c80bb0c1 100644 --- a/plug-api/silverbullet-syscall/editor.ts +++ b/plug-api/silverbullet-syscall/editor.ts @@ -15,6 +15,14 @@ export function getText(): Promise { return syscall("editor.getText"); } +/** + * This updates the editor text, but in a minimal-diff way: + * it compares the current editor text with the new text, and only sends the changes to the editor, thereby preserving cursor location + */ +export function setText(newText: string) { + return syscall("editor.setText", newText); +} + export function getCursor(): Promise { return syscall("editor.getCursor"); } diff --git a/plugs/tasks/task.ts b/plugs/tasks/task.ts index f280cb97..3f8b7086 100644 --- a/plugs/tasks/task.ts +++ b/plugs/tasks/task.ts @@ -5,6 +5,7 @@ import { editor, markdown, space, sync } from "$sb/syscalls.ts"; import { addParentPointers, collectNodesMatching, + findNodeMatching, findNodeOfType, findParentMatching, nodeAtPos, @@ -337,3 +338,34 @@ export async function postponeCommand() { }, }); } + +export async function removeCompletedTasksCommand() { + const tree = await markdown.parseMarkdown(await editor.getText()); + addParentPointers(tree); + + // Taking this ugly approach because the tree is modified in place + // Just finding and removing one task at a time and then repeating until nothing changes + while (true) { + const completedTaskNode = findNodeMatching(tree, (node) => { + return node.type === "Task" && + ["x", "X"].includes(node.children![0].children![1].text!); + }); + if (completedTaskNode) { + // Ok got one, let's remove it + const listItemNode = completedTaskNode.parent!; + const bulletListNode = listItemNode.parent!; + // Remove the list item + const listItemIdx = bulletListNode.children!.indexOf(listItemNode); + let removeItems = 1; + if (bulletListNode.children![listItemIdx + 1]?.text === "\n") { + removeItems++; + } + bulletListNode.children!.splice(listItemIdx, removeItems); + } else { + // No completed tasks left, we're done + break; + } + } + + await editor.setText(renderToText(tree)); +} diff --git a/plugs/tasks/tasks.plug.yaml b/plugs/tasks/tasks.plug.yaml index 1b776475..d8ca28fa 100644 --- a/plugs/tasks/tasks.plug.yaml +++ b/plugs/tasks/tasks.plug.yaml @@ -32,4 +32,9 @@ functions: taskComplete: path: ./complete.ts:completeTaskState events: - - editor:complete \ No newline at end of file + - editor:complete + + removeCompletedTasksCommand: + path: task.ts:removeCompletedTasksCommand + command: + name: "Task: Remove Completed" \ No newline at end of file diff --git a/web/cm_plugins/task.ts b/web/cm_plugins/task.ts index 60a2915d..a0cca4ef 100644 --- a/web/cm_plugins/task.ts +++ b/web/cm_plugins/task.ts @@ -19,9 +19,11 @@ class CheckboxWidget extends WidgetType { checkbox.type = "checkbox"; checkbox.checked = this.checked; checkbox.addEventListener("click", (e) => { - // Let the click handler handle this e.stopPropagation(); - + e.preventDefault(); + }); + checkbox.addEventListener("mouseup", (e) => { + e.stopPropagation(); this.clickCallback(this.pos); }); wrap.appendChild(checkbox); diff --git a/web/syscalls/cm_util.ts b/web/syscalls/cm_util.ts new file mode 100644 index 00000000..7c9af6cd --- /dev/null +++ b/web/syscalls/cm_util.ts @@ -0,0 +1,23 @@ +import diff, { DELETE, INSERT } from "https://esm.sh/fast-diff@1.3.0"; +import type { ChangeSpec } from "@codemirror/state"; + +export function diffAndPrepareChanges( + oldString: string, + newString: string, +): ChangeSpec[] { + // Use the fast-diff library to compute the changes + const diffs = diff(oldString, newString); + + // Convert the diffs to CodeMirror transactions + let startIndex = 0; + const changes: ChangeSpec[] = []; + for (const part of diffs) { + if (part[0] === INSERT) { + changes.push({ from: startIndex, insert: part[1] }); + } else if (part[0] === DELETE) { + changes.push({ from: startIndex, to: startIndex + part[1].length }); + } + startIndex += part[1].length; + } + return changes; +} diff --git a/web/syscalls/editor.ts b/web/syscalls/editor.ts index 760cc297..b91a50f7 100644 --- a/web/syscalls/editor.ts +++ b/web/syscalls/editor.ts @@ -15,6 +15,7 @@ import type { FilterOption } from "../types.ts"; import { UploadFile } from "../../plug-api/types.ts"; import { PageRef } from "$sb/lib/page.ts"; import { openSearchPanel } from "../deps.ts"; +import { diffAndPrepareChanges } from "./cm_util.ts"; export function editorSyscalls(client: Client): SysCallMapping { const syscalls: SysCallMapping = { @@ -24,6 +25,13 @@ export function editorSyscalls(client: Client): SysCallMapping { "editor.getText": () => { return client.editorView.state.sliceDoc(); }, + "editor.setText": (_ctx, newText: string) => { + const currentText = client.editorView.state.sliceDoc(); + const allChanges = diffAndPrepareChanges(currentText, newText); + client.editorView.dispatch({ + changes: allChanges, + }); + }, "editor.getCursor": (): number => { return client.editorView.state.selection.main.from; },