Custom task statuses plus various fixes
This commit is contained in:
parent
9b5fa44fb6
commit
2ee20af5c8
@ -42,8 +42,6 @@ export async function serveCommand(
|
||||
|
||||
const app = new Application();
|
||||
|
||||
console.log(options);
|
||||
|
||||
if (!folder) {
|
||||
folder = Deno.env.get("SB_FOLDER");
|
||||
if (!folder) {
|
||||
|
@ -5,8 +5,6 @@ export const CommandLinkNameTag = Tag.define();
|
||||
export const WikiLinkTag = Tag.define();
|
||||
export const WikiLinkPageTag = Tag.define();
|
||||
export const CodeInfoTag = Tag.define();
|
||||
export const TaskTag = Tag.define();
|
||||
export const TaskMarkerTag = Tag.define();
|
||||
export const CommentTag = Tag.define();
|
||||
export const CommentMarkerTag = Tag.define();
|
||||
export const BulletList = Tag.define();
|
||||
@ -22,3 +20,7 @@ export const DirectiveProgramTag = Tag.define();
|
||||
export const AttributeTag = Tag.define();
|
||||
export const AttributeNameTag = Tag.define();
|
||||
export const AttributeValueTag = Tag.define();
|
||||
|
||||
export const TaskTag = Tag.define();
|
||||
export const TaskMarkerTag = Tag.define();
|
||||
export const TaskStateTag = Tag.define();
|
||||
|
60
common/markdown_parser/extended_task.ts
Normal file
60
common/markdown_parser/extended_task.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import {
|
||||
BlockContext,
|
||||
LeafBlock,
|
||||
LeafBlockParser,
|
||||
MarkdownConfig,
|
||||
} from "../deps.ts";
|
||||
|
||||
import { tags as t } from "@lezer/highlight";
|
||||
import { TaskStateTag } from "./customtags.ts";
|
||||
|
||||
// Taken from https://github.com/lezer-parser/markdown/blob/main/src/extension.ts and adapted
|
||||
|
||||
class MultiStatusTaskParser implements LeafBlockParser {
|
||||
constructor(private status: string) {
|
||||
}
|
||||
|
||||
nextLine() {
|
||||
return false;
|
||||
}
|
||||
|
||||
finish(cx: BlockContext, leaf: LeafBlock) {
|
||||
cx.addLeafElement(
|
||||
leaf,
|
||||
cx.elt("Task", leaf.start, leaf.start + leaf.content.length, [
|
||||
cx.elt("TaskState", leaf.start, leaf.start + 2 + this.status.length, [
|
||||
cx.elt("TaskMarker", leaf.start, leaf.start + 1),
|
||||
cx.elt(
|
||||
"TaskMarker",
|
||||
leaf.start + 1 + this.status.length,
|
||||
leaf.start + 2 + this.status.length,
|
||||
),
|
||||
]),
|
||||
...cx.parser.parseInline(
|
||||
leaf.content.slice(this.status.length + 2),
|
||||
leaf.start + this.status.length + 2,
|
||||
),
|
||||
]),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const TaskList: MarkdownConfig = {
|
||||
defineNodes: [
|
||||
{ name: "Task", block: true, style: t.list },
|
||||
{ name: "TaskMarker", style: t.atom },
|
||||
{ name: "TaskState", style: TaskStateTag },
|
||||
],
|
||||
parseBlock: [{
|
||||
name: "TaskList",
|
||||
leaf(cx, leaf) {
|
||||
const match = /^\[([^\]]+)\][ \t]/.exec(leaf.content);
|
||||
return match &&
|
||||
cx.parentType().name == "ListItem"
|
||||
? new MultiStatusTaskParser(match[1])
|
||||
: null;
|
||||
},
|
||||
after: "SetextHeading",
|
||||
}],
|
||||
};
|
@ -103,7 +103,7 @@ And one with nested brackets: [array: [1, 2, 3]]
|
||||
Deno.test("Test inline attribute syntax", () => {
|
||||
const lang = buildMarkdown([]);
|
||||
const tree = parse(lang, inlineAttributeSample);
|
||||
console.log("Attribute parsed", JSON.stringify(tree, null, 2));
|
||||
// console.log("Attribute parsed", JSON.stringify(tree, null, 2));
|
||||
const attributes = collectNodesOfType(tree, "Attribute");
|
||||
let nameNode = findNodeOfType(attributes[0], "AttributeName");
|
||||
assertEquals(nameNode?.children![0].text, "age");
|
||||
@ -120,3 +120,21 @@ Deno.test("Test inline attribute syntax", () => {
|
||||
valueNode = findNodeOfType(attributes[2], "AttributeValue");
|
||||
assertEquals(valueNode?.children![0].text, "[1, 2, 3]");
|
||||
});
|
||||
|
||||
const multiStatusTaskExample = `
|
||||
* [ ] Task 1
|
||||
- [x] Task 2
|
||||
* [TODO] Task 3
|
||||
`;
|
||||
|
||||
Deno.test("Test multi-status tasks", () => {
|
||||
const lang = buildMarkdown([]);
|
||||
const tree = parse(lang, multiStatusTaskExample);
|
||||
// console.log("Tasks parsed", JSON.stringify(tree, null, 2));
|
||||
const tasks = collectNodesOfType(tree, "Task");
|
||||
assertEquals(tasks.length, 3);
|
||||
// Check " " checkbox state parsing
|
||||
assertEquals(tasks[0].children![0].children![1].text, " ");
|
||||
assertEquals(tasks[1].children![0].children![1].text, "x");
|
||||
assertEquals(tasks[2].children![0].children![1].text, "TODO");
|
||||
});
|
||||
|
@ -10,10 +10,10 @@ import {
|
||||
Strikethrough,
|
||||
styleTags,
|
||||
tags as t,
|
||||
TaskList,
|
||||
yamlLanguage,
|
||||
} from "../deps.ts";
|
||||
import * as ct from "./customtags.ts";
|
||||
import { TaskList } from "./extended_task.ts";
|
||||
import {
|
||||
MDExt,
|
||||
mdExtensionStyleTags,
|
||||
|
@ -115,9 +115,6 @@ export class EventedSpacePrimitives implements SpacePrimitives {
|
||||
console.error("Error dispatching page:saved event", e);
|
||||
});
|
||||
}
|
||||
// if (name.startsWith("_plug/") && name.endsWith(".plug.js")) {
|
||||
// await this.dispatchEvent("plug:changed", name);
|
||||
// }
|
||||
return newMeta;
|
||||
}
|
||||
|
||||
|
47
plugs/editor/complete.ts
Normal file
47
plugs/editor/complete.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { CompleteEvent } from "$sb/app_event.ts";
|
||||
import { space } from "$sb/syscalls.ts";
|
||||
import { PageMeta } from "../../web/types.ts";
|
||||
import { cacheFileListing } from "../federation/federation.ts";
|
||||
|
||||
// Completion
|
||||
export async function pageComplete(completeEvent: CompleteEvent) {
|
||||
const match = /\[\[([^\]@:\{}]*)$/.exec(completeEvent.linePrefix);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
let allPages: PageMeta[] = await space.listPages();
|
||||
const prefix = match[1];
|
||||
if (prefix.startsWith("!")) {
|
||||
// Federation prefix, let's first see if we're matching anything from federation that is locally synced
|
||||
const prefixMatches = allPages.filter((pageMeta) =>
|
||||
pageMeta.name.startsWith(prefix)
|
||||
);
|
||||
if (prefixMatches.length === 0) {
|
||||
// Ok, nothing synced in via federation, let's see if this URI is complete enough to try to fetch index.json
|
||||
if (prefix.includes("/")) {
|
||||
// Yep
|
||||
const domain = prefix.split("/")[0];
|
||||
// Cached listing
|
||||
const federationPages = (await cacheFileListing(domain)).filter((fm) =>
|
||||
fm.name.endsWith(".md")
|
||||
).map((fm) => ({
|
||||
...fm,
|
||||
name: fm.name.slice(0, -3),
|
||||
}));
|
||||
if (federationPages.length > 0) {
|
||||
allPages = allPages.concat(federationPages);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
from: completeEvent.pos - match[1].length,
|
||||
options: allPages.map((pageMeta) => {
|
||||
return {
|
||||
label: pageMeta.name,
|
||||
boost: pageMeta.lastModified,
|
||||
type: "page",
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
@ -32,7 +32,7 @@ functions:
|
||||
|
||||
# Completion
|
||||
pageComplete:
|
||||
path: "./page.ts:pageComplete"
|
||||
path: "./complete.ts:pageComplete"
|
||||
events:
|
||||
- editor:complete
|
||||
commandComplete:
|
||||
|
@ -60,43 +60,3 @@ export async function newPageCommand() {
|
||||
}
|
||||
await editor.navigate(pageName);
|
||||
}
|
||||
|
||||
// Completion
|
||||
export async function pageComplete(completeEvent: CompleteEvent) {
|
||||
const match = /\[\[([^\]@:\{}]*)$/.exec(completeEvent.linePrefix);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
let allPages: PageMeta[] = await space.listPages();
|
||||
const prefix = match[1];
|
||||
if (prefix.startsWith("!")) {
|
||||
// Federation prefix, let's first see if we're matching anything from federation that is locally synced
|
||||
const prefixMatches = allPages.filter((pageMeta) =>
|
||||
pageMeta.name.startsWith(prefix)
|
||||
);
|
||||
if (prefixMatches.length === 0) {
|
||||
// Ok, nothing synced in via federation, let's see if this URI is complete enough to try to fetch index.json
|
||||
if (prefix.includes("/")) {
|
||||
// Yep
|
||||
const domain = prefix.split("/")[0];
|
||||
// Cached listing
|
||||
allPages = (await cacheFileListing(domain)).filter((fm) =>
|
||||
fm.name.endsWith(".md")
|
||||
).map((fm) => ({
|
||||
...fm,
|
||||
name: fm.name.slice(0, -3),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
from: completeEvent.pos - match[1].length,
|
||||
options: allPages.map((pageMeta) => {
|
||||
return {
|
||||
label: pageMeta.name,
|
||||
boost: pageMeta.lastModified,
|
||||
type: "page",
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ const builtinAttributes: Record<string, Record<string, string>> = {
|
||||
name: "string",
|
||||
done: "boolean",
|
||||
page: "string",
|
||||
state: "string",
|
||||
deadline: "string",
|
||||
pos: "number",
|
||||
tags: "array",
|
||||
@ -126,6 +127,10 @@ export function builtinAttributeCompleter(
|
||||
}
|
||||
|
||||
export async function attributeComplete(completeEvent: CompleteEvent) {
|
||||
if (/([\-\*]\s+\[)([^\]]+)$/.test(completeEvent.linePrefix)) {
|
||||
// Don't match task states, which look similar
|
||||
return null;
|
||||
}
|
||||
const inlineAttributeMatch = /([^\[\{}]|^)\[(\w+)$/.exec(
|
||||
completeEvent.linePrefix,
|
||||
);
|
||||
|
@ -260,7 +260,11 @@ function render(
|
||||
name: "span",
|
||||
body: cleanTags(mapRender(t.children!)),
|
||||
};
|
||||
case "TaskMarker":
|
||||
|
||||
case "TaskState": {
|
||||
// child[0] = marker, child[1] = state, child[2] = marker
|
||||
const stateText = t.children![1].text!;
|
||||
if ([" ", "x", "X"].includes(stateText)) {
|
||||
return {
|
||||
name: "input",
|
||||
attrs: {
|
||||
@ -270,6 +274,16 @@ function render(
|
||||
},
|
||||
body: "",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: "span",
|
||||
attrs: {
|
||||
class: "task-state",
|
||||
},
|
||||
body: stateText,
|
||||
};
|
||||
}
|
||||
}
|
||||
case "NamedAnchor":
|
||||
return {
|
||||
name: "a",
|
||||
|
20
plugs/tasks/complete.ts
Normal file
20
plugs/tasks/complete.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { CompleteEvent } from "$sb/app_event.ts";
|
||||
import { index } from "$sb/syscalls.ts";
|
||||
|
||||
export async function completeTaskState(completeEvent: CompleteEvent) {
|
||||
const taskMatch = /([\-\*]\s+\[)([^\[\]]+)$/.exec(
|
||||
completeEvent.linePrefix,
|
||||
);
|
||||
if (!taskMatch) {
|
||||
return null;
|
||||
}
|
||||
const allStates = await index.queryPrefix("taskState:");
|
||||
const states = [...new Set(allStates.map((s) => s.key.split(":")[1]))];
|
||||
|
||||
return {
|
||||
from: completeEvent.pos - taskMatch[2].length,
|
||||
options: states.map((state) => ({
|
||||
label: state,
|
||||
})),
|
||||
};
|
||||
}
|
@ -10,6 +10,7 @@ import {
|
||||
addParentPointers,
|
||||
collectNodesMatching,
|
||||
findNodeOfType,
|
||||
findParentMatching,
|
||||
nodeAtPos,
|
||||
ParseTree,
|
||||
renderToText,
|
||||
@ -25,6 +26,7 @@ import { indexAttributes } from "../index/attributes.ts";
|
||||
export type Task = {
|
||||
name: string;
|
||||
done: boolean;
|
||||
state: string;
|
||||
deadline?: string;
|
||||
tags?: string[];
|
||||
nested?: string;
|
||||
@ -37,8 +39,12 @@ function getDeadline(deadlineNode: ParseTree): string {
|
||||
return deadlineNode.children![0].text!.replace(/📅\s*/, "");
|
||||
}
|
||||
|
||||
const completeStates = ["x", "X"];
|
||||
const incompleteStates = [" "];
|
||||
|
||||
export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
||||
const tasks: { key: string; value: Task }[] = [];
|
||||
const taskStates = new Map<string, number>();
|
||||
removeQueries(tree);
|
||||
addParentPointers(tree);
|
||||
const allAttributes: Record<string, any> = {};
|
||||
@ -46,10 +52,19 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
||||
if (n.type !== "Task") {
|
||||
return false;
|
||||
}
|
||||
const complete = n.children![0].children![0].text! !== "[ ]";
|
||||
const state = n.children![0].children![1].text!;
|
||||
if (!incompleteStates.includes(state) && !completeStates.includes(state)) {
|
||||
if (!taskStates.has(state)) {
|
||||
taskStates.set(state, 1);
|
||||
} else {
|
||||
taskStates.set(state, taskStates.get(state)! + 1);
|
||||
}
|
||||
}
|
||||
const complete = completeStates.includes(state);
|
||||
const task: Task = {
|
||||
name: "",
|
||||
done: complete,
|
||||
state,
|
||||
};
|
||||
|
||||
rewritePageRefs(n, name);
|
||||
@ -95,31 +110,59 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
||||
// console.log("Found", tasks, "task(s)");
|
||||
await index.batchSet(name, tasks);
|
||||
await indexAttributes(name, allAttributes, "task");
|
||||
await index.batchSet(
|
||||
name,
|
||||
Array.from(taskStates.entries()).map(([state, count]) => ({
|
||||
key: `taskState:${state}`,
|
||||
value: count,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export function taskToggle(event: ClickEvent) {
|
||||
return taskToggleAtPos(event.page, event.pos);
|
||||
if (event.altKey) {
|
||||
return;
|
||||
}
|
||||
return taskCycleAtPos(event.page, event.pos);
|
||||
}
|
||||
|
||||
export async function previewTaskToggle(eventString: string) {
|
||||
const [eventName, pos] = JSON.parse(eventString);
|
||||
if (eventName === "task") {
|
||||
return taskToggleAtPos(await editor.getCurrentPage(), +pos);
|
||||
return taskCycleAtPos(await editor.getCurrentPage(), +pos);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTaskMarker(
|
||||
async function cycleTaskState(
|
||||
pageName: string,
|
||||
node: ParseTree,
|
||||
) {
|
||||
let changeTo = "[x]";
|
||||
if (node.children![0].text === "[x]" || node.children![0].text === "[X]") {
|
||||
changeTo = "[ ]";
|
||||
const stateText = node.children![1].text!;
|
||||
let changeTo: string | undefined;
|
||||
if (completeStates.includes(stateText)) {
|
||||
changeTo = " ";
|
||||
} else if (incompleteStates.includes(stateText)) {
|
||||
changeTo = "x";
|
||||
} else {
|
||||
// Not a checkbox, but a custom state
|
||||
const allStates = await index.queryPrefix("taskState:");
|
||||
const states = [...new Set(allStates.map((s) => s.key.split(":")[1]))];
|
||||
states.sort();
|
||||
// Select a next state
|
||||
const currentStateIndex = states.indexOf(stateText);
|
||||
if (currentStateIndex === -1) {
|
||||
console.error("Unknown state", stateText);
|
||||
return;
|
||||
}
|
||||
const nextStateIndex = (currentStateIndex + 1) % states.length;
|
||||
changeTo = states[nextStateIndex];
|
||||
// console.log("All possible states", states);
|
||||
// return;
|
||||
}
|
||||
await editor.dispatch({
|
||||
changes: {
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
from: node.children![1].from,
|
||||
to: node.children![1].to,
|
||||
insert: changeTo,
|
||||
},
|
||||
});
|
||||
@ -135,10 +178,23 @@ async function toggleTaskMarker(
|
||||
const pos = +posS;
|
||||
if (page === pageName) {
|
||||
// In current page, just update the task marker with dispatch
|
||||
const editorText = await editor.getText();
|
||||
// Check if the task state marker is still there
|
||||
const targetText = editorText.substring(
|
||||
pos + 1,
|
||||
pos + 1 + stateText.length,
|
||||
);
|
||||
if (targetText !== stateText) {
|
||||
console.error(
|
||||
"Reference not a task marker, out of date?",
|
||||
targetText,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await editor.dispatch({
|
||||
changes: {
|
||||
from: pos,
|
||||
to: pos + changeTo.length,
|
||||
from: pos + 1,
|
||||
to: pos + 1 + stateText.length,
|
||||
insert: changeTo,
|
||||
},
|
||||
});
|
||||
@ -146,17 +202,16 @@ async function toggleTaskMarker(
|
||||
let text = await space.readPage(page);
|
||||
|
||||
const referenceMdTree = await markdown.parseMarkdown(text);
|
||||
// Adding +1 to immediately hit the task marker
|
||||
const taskMarkerNode = nodeAtPos(referenceMdTree, pos + 1);
|
||||
|
||||
if (!taskMarkerNode || taskMarkerNode.type !== "TaskMarker") {
|
||||
// Adding +1 to immediately hit the task state node
|
||||
const taskStateNode = nodeAtPos(referenceMdTree, pos + 1);
|
||||
if (!taskStateNode || taskStateNode.type !== "TaskState") {
|
||||
console.error(
|
||||
"Reference not a task marker, out of date?",
|
||||
taskMarkerNode,
|
||||
taskStateNode,
|
||||
);
|
||||
return;
|
||||
}
|
||||
taskMarkerNode.children![0].text = changeTo;
|
||||
taskStateNode.children![1].text = changeTo;
|
||||
text = renderToText(referenceMdTree);
|
||||
await space.writePage(page, text);
|
||||
sync.scheduleFileSync(`${page}.md`);
|
||||
@ -165,27 +220,53 @@ async function toggleTaskMarker(
|
||||
}
|
||||
}
|
||||
|
||||
export async function taskToggleAtPos(pageName: string, pos: number) {
|
||||
export async function taskCycleAtPos(pageName: string, pos: number) {
|
||||
const text = await editor.getText();
|
||||
const mdTree = await markdown.parseMarkdown(text);
|
||||
addParentPointers(mdTree);
|
||||
|
||||
const node = nodeAtPos(mdTree, pos);
|
||||
if (node && node.type === "TaskMarker") {
|
||||
await toggleTaskMarker(pageName, node);
|
||||
let node = nodeAtPos(mdTree, pos);
|
||||
if (node) {
|
||||
if (node.type === "TaskMarker") {
|
||||
node = node.parent!;
|
||||
}
|
||||
if (node.type === "TaskState") {
|
||||
await cycleTaskState(pageName, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function taskToggleCommand() {
|
||||
export async function taskCycleCommand() {
|
||||
const text = await editor.getText();
|
||||
const pos = await editor.getCursor();
|
||||
const tree = await markdown.parseMarkdown(text);
|
||||
addParentPointers(tree);
|
||||
|
||||
const node = nodeAtPos(tree, pos);
|
||||
// We kwow node.type === Task (due to the task context)
|
||||
const taskMarker = findNodeOfType(node!, "TaskMarker");
|
||||
await toggleTaskMarker(await editor.getCurrentPage(), taskMarker!);
|
||||
let node = nodeAtPos(tree, pos);
|
||||
if (!node) {
|
||||
await editor.flashNotification("No task at cursor");
|
||||
return;
|
||||
}
|
||||
if (["BulletList", "Document"].includes(node.type!)) {
|
||||
// Likely at the end of the line, let's back up a position
|
||||
node = nodeAtPos(tree, pos - 1);
|
||||
}
|
||||
if (!node) {
|
||||
await editor.flashNotification("No task at cursor");
|
||||
return;
|
||||
}
|
||||
console.log("Node", node);
|
||||
const taskNode = node.type === "Task"
|
||||
? node
|
||||
: findParentMatching(node!, (n) => n.type === "Task");
|
||||
if (!taskNode) {
|
||||
await editor.flashNotification("No task at cursor");
|
||||
return;
|
||||
}
|
||||
const taskState = findNodeOfType(taskNode!, "TaskState");
|
||||
if (taskState) {
|
||||
await cycleTaskState(await editor.getCurrentPage(), taskState);
|
||||
}
|
||||
}
|
||||
|
||||
export async function postponeCommand() {
|
||||
@ -210,7 +291,7 @@ export async function postponeCommand() {
|
||||
return;
|
||||
}
|
||||
// Parse "naive" due date
|
||||
let [yyyy, mm, dd] = date.split("-").map(Number);
|
||||
const [yyyy, mm, dd] = date.split("-").map(Number);
|
||||
// Create new naive Date object.
|
||||
// `monthIndex` parameter is zero-based, so subtract 1 from parsed month.
|
||||
const d = new Date(yyyy, mm - 1, dd);
|
||||
@ -236,7 +317,6 @@ export async function postponeCommand() {
|
||||
anchor: pos,
|
||||
},
|
||||
});
|
||||
// await toggleTaskMarker(taskMarker!, pos);
|
||||
}
|
||||
|
||||
export async function queryProvider({
|
||||
|
@ -40,12 +40,10 @@ functions:
|
||||
events:
|
||||
- query:task
|
||||
taskToggleCommand:
|
||||
path: ./task.ts:taskToggleCommand
|
||||
path: ./task.ts:taskCycleCommand
|
||||
command:
|
||||
name: "Task: Toggle"
|
||||
name: "Task: Cycle State"
|
||||
key: Alt-t
|
||||
contexts:
|
||||
- Task
|
||||
taskPostponeCommand:
|
||||
path: ./task.ts:postponeCommand
|
||||
command:
|
||||
@ -57,3 +55,8 @@ functions:
|
||||
path: ./task.ts:previewTaskToggle
|
||||
events:
|
||||
- preview:click
|
||||
|
||||
taskComplete:
|
||||
path: ./complete.ts:completeTaskState
|
||||
events:
|
||||
- editor:complete
|
@ -96,6 +96,9 @@ export class Client {
|
||||
parent: Element,
|
||||
public syncMode = false,
|
||||
) {
|
||||
if (!syncMode) {
|
||||
this.fullSyncCompleted = true;
|
||||
}
|
||||
// Generate a semi-unique prefix for the database so not to reuse databases for different space paths
|
||||
this.dbPrefix = "" + simpleHash(window.silverBulletConfig.spaceFolderPath);
|
||||
|
||||
|
@ -37,12 +37,13 @@ export function taskListPlugin(
|
||||
syntaxTree(state).iterate({
|
||||
enter({ type, from, to, node }) {
|
||||
if (type.name !== "Task") return;
|
||||
let checked = false;
|
||||
// true/false if this is a checkbox, undefined when it's a custom-status task
|
||||
let checkboxStatus: boolean | undefined;
|
||||
// Iterate inside the task node to find the checkbox
|
||||
node.toTree().iterate({
|
||||
enter: (ref) => iterateInner(ref.type, ref.from, ref.to),
|
||||
});
|
||||
if (checked) {
|
||||
if (checkboxStatus === true) {
|
||||
widgets.push(
|
||||
Decoration.mark({
|
||||
tagName: "span",
|
||||
@ -52,14 +53,22 @@ export function taskListPlugin(
|
||||
}
|
||||
|
||||
function iterateInner(type: NodeType, nfrom: number, nto: number) {
|
||||
if (type.name !== "TaskMarker") return;
|
||||
if (type.name !== "TaskState") return;
|
||||
if (isCursorInRange(state, [from + nfrom, from + nto])) return;
|
||||
const checkbox = state.sliceDoc(from + nfrom, from + nto);
|
||||
// Checkbox is checked if it has a 'x' in between the []
|
||||
if ("xX".includes(checkbox[1])) checked = true;
|
||||
if (checkbox === "[x]" || checkbox === "[X]") {
|
||||
checkboxStatus = true;
|
||||
} else if (checkbox === "[ ]") {
|
||||
checkboxStatus = false;
|
||||
}
|
||||
if (checkboxStatus === undefined) {
|
||||
// Not replacing it with a widget
|
||||
return;
|
||||
}
|
||||
const dec = Decoration.replace({
|
||||
widget: new CheckboxWidget(
|
||||
checked,
|
||||
checkboxStatus,
|
||||
from + nfrom + 1,
|
||||
onCheckboxClick,
|
||||
),
|
||||
|
@ -22,6 +22,7 @@ export default function highlightStyles(mdExtension: MDExt[]) {
|
||||
{ tag: ct.AttributeNameTag, class: "sb-atom" },
|
||||
{ tag: ct.TaskTag, class: "sb-task" },
|
||||
{ tag: ct.TaskMarkerTag, class: "sb-task-marker" },
|
||||
{ tag: ct.TaskStateTag, class: "sb-task-state" },
|
||||
{ tag: ct.CodeInfoTag, class: "sb-code-info" },
|
||||
{ tag: ct.CommentTag, class: "sb-comment" },
|
||||
{ tag: ct.CommentMarkerTag, class: "sb-comment-marker" },
|
||||
|
@ -381,6 +381,10 @@
|
||||
color: var(--editor-task-marker-color);
|
||||
}
|
||||
|
||||
.sb-task-state {
|
||||
color: var(--editor-task-state-color);
|
||||
}
|
||||
|
||||
.sb-line-comment {
|
||||
background-color: var(--editor-code-comment-color); // rgba(255, 255, 0, 0.5);
|
||||
}
|
||||
|
@ -312,6 +312,10 @@
|
||||
font-size: 91%;
|
||||
}
|
||||
|
||||
.sb-task-state {
|
||||
font-size: 91%;
|
||||
}
|
||||
|
||||
.sb-directive-start {
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
|
@ -105,6 +105,8 @@ html {
|
||||
--editor-directive-color: #5b5b5b;
|
||||
--editor-directive-info-color: var(--subtle-color);
|
||||
--editor-task-marker-color: var(--subtle-color);
|
||||
--editor-task-state-color: var(--subtle-color);
|
||||
|
||||
|
||||
--ui-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
|
||||
|
@ -8,6 +8,8 @@ This release brings a new default [[Client Modes|client mode]] to SilverBullet:
|
||||
|
||||
Other notable changes:
|
||||
* Massive reshuffling of built-in [[🔌 Plugs]], splitting the old “core” plug into [[🔌 Editor]], [[🔌 Template]] and [[🔌 Index]].
|
||||
* [[🔌 Tasks]] now support custom states (not just `[x]` and `[ ]`), for example:
|
||||
* [IN PROGRESS] An in progress task
|
||||
* Removed [[Cloud Links]] support in favor of [[Federation]]
|
||||
|
||||
---
|
||||
|
@ -83,15 +83,7 @@ docker run -p 3000:3000 -v myspace:/space -d -e SB_USER=me:letmein zefhemel/silv
|
||||
|
||||
To build your own version of the docker image, run `./scripts/build_docker.sh`.
|
||||
|
||||
You can also use docker-compose if you prefer. From a silverbullet check-out run:
|
||||
|
||||
```shell
|
||||
PORT=3000 docker-compose up
|
||||
```
|
||||
|
||||
or similar.
|
||||
|
||||
To upgrade, simply pull the latest docker image (rebuilt and pushed after every commit to "main") and start the new container.
|
||||
To upgrade, simply pull the latest docker image and start the new container.
|
||||
|
||||
```shell
|
||||
docker pull zefhemel/silverbullet
|
||||
|
@ -1 +1 @@
|
||||
* [{{#if done}}x{{else}} {{/if}}] [[{{page}}@{{pos}}]] {{name}}
|
||||
* [{{state}}] [[{{page}}@{{pos}}]] {{name}}
|
@ -3,17 +3,33 @@ type: plug
|
||||
repo: https://github.com/silverbulletmd/silverbullet
|
||||
---
|
||||
|
||||
The Tasks plug implements a lot of the task support in SilverBullet.
|
||||
The Tasks plug implements task support in SilverBullet.
|
||||
|
||||
Tasks in SilverBullet are written using semi-standard task syntax:
|
||||
## Task states
|
||||
The tasks plug support the standard “done” and “not done” states via `[x]` and `[ ]` notation in the context of a list (this is fairly widely accepted [[Markdown]] syntax):
|
||||
|
||||
* [ ] This is a task
|
||||
* [ ] This is a task (toggle me!)
|
||||
|
||||
However, custom states can also be used for extra flexibility:
|
||||
|
||||
* [TODO] This task is still to do
|
||||
* [IN PROGRESS] In progress task
|
||||
* [RESOLVED] A resolved task
|
||||
* [-] Whatever this state means
|
||||
* [/] Or this one
|
||||
|
||||
You can cycle through the states by clicking on the status or by running the {[Task: Cycle State]} command while on a task. There is also auto complete for all known custom task states in a space.
|
||||
|
||||
## Annotations
|
||||
Tasks can also be annotated with [[Tags]]:
|
||||
|
||||
* [ ] This is a tagged task #my-tag
|
||||
|
||||
You can _toggle_ a task state either by putting in an `x` or `X` inside the box or by simply clicking/tapping on the box. Alternatively, you can use the {[Task: Toggle]} command to toggle the task you’re currently in.
|
||||
As well as [[Attributes]]:
|
||||
|
||||
* [ ] This is a task with attributes [taskAttribute: true]
|
||||
|
||||
## Deadlines
|
||||
|
||||
Tasks can specify deadlines:
|
||||
|
||||
@ -21,12 +37,31 @@ Tasks can specify deadlines:
|
||||
|
||||
When the cursor is positioned inside of a due date, the {[Task: Postpone]} command can be used to postpone the task for a certain period.
|
||||
|
||||
This metadata is extracted and available via the `task` query source to [[🔌 Directive/Query]]:
|
||||
## Querying
|
||||
All meta data (`done` status, `state`, `tags`, `deadline` and custom attributes) is extracted and available via the `task` query source to [[🔌 Directive/Query]]:
|
||||
|
||||
<!-- #query task where page = "{{@page.name}}" -->
|
||||
|name |done |page |pos|tags |deadline |
|
||||
|-----------------------------|-----|--------|---|------|----------|
|
||||
|This is a task |false|🔌 Tasks|213| | |
|
||||
|This is a tagged task #my-tag|false|🔌 Tasks|279|my-tag| |
|
||||
|This is due |false|🔌 Tasks|565| |2022-11-26|
|
||||
|name |done |state |page |pos |tags |taskAttribute|deadline |
|
||||
|--|--|--|--|--|--|--|--|
|
||||
|Remote toggle me |false| |🔌 Tasks|3056| | | |
|
||||
|This is a task (toggle me!) |false| |🔌 Tasks|321 | | | |
|
||||
|This task is still to do |false|TODO |🔌 Tasks|420 | | | |
|
||||
|In progress task |false|IN PROGRESS|🔌 Tasks|454 | | | |
|
||||
|A resolved task |false|RESOLVED |🔌 Tasks|487 | | | |
|
||||
|Whatever this state means |false|- |🔌 Tasks|516 | | | |
|
||||
|Or this one |false|/ |🔌 Tasks|548 | | | |
|
||||
|This is a tagged task #my-tag |false| |🔌 Tasks|824 |my-tag| | |
|
||||
|This is a task with attributes|false| |🔌 Tasks|889 | |true| |
|
||||
|This is due |false| |🔌 Tasks|993 | | |2022-11-26|
|
||||
<!-- /query -->
|
||||
|
||||
## Rendering
|
||||
There is a [[!silverbullet.md/template/task]] template you can use to render tasks nicely rather than using the default table (as demonstrated above). When you use this template, you can even cycle through the states of the task by click on its state _inside_ the rendered query, and it will update the state of the _original_ task automatically (although not yet in reverse) — this works across pages.
|
||||
|
||||
Try it (by clicking on the checkbox inside of the directive):
|
||||
|
||||
<!-- #query task where page = "{{@page.name}}" and name = "Remote toggle me" render [[template/task]] -->
|
||||
* [ ] [[🔌 Tasks@3056]] Remote toggle me
|
||||
<!-- /query -->
|
||||
|
||||
* [ ] Remote toggle me
|
||||
|
Loading…
Reference in New Issue
Block a user