todo.txt integration

This commit is contained in:
2026-07-03 11:08:27 +02:00
parent 8c9b448bdc
commit 5e72b073b8
9 changed files with 458 additions and 36 deletions
+61 -1
View File
@@ -8,7 +8,7 @@ import { EditorState, EditorSelection, Compartment, Prec } from "@codemirror/sta
import { EditorView, keymap, drawSelection } from "@codemirror/view";
import { history, historyKeymap, defaultKeymap, indentWithTab, undo, redo, deleteLine } from "@codemirror/commands";
import { markdown, markdownLanguage, markdownKeymap } from "@codemirror/lang-markdown";
import { syntaxHighlighting, HighlightStyle, indentOnInput } from "@codemirror/language";
import { syntaxHighlighting, HighlightStyle, indentOnInput, StreamLanguage } from "@codemirror/language";
import {
autocompletion,
closeBrackets,
@@ -77,6 +77,63 @@ const highlightStyle = HighlightStyle.define([
{ tag: [tags.processingInstruction, tags.meta], color: "var(--text-muted)" },
]);
// todo.txt language for the left-rail todo widget (assets/todo-rail.js).
// Line-oriented: `x ` done-prefix strikes the whole line; `(A)` priority,
// `+project`, `@context` and ISO dates get their own colors. No parsing into
// task objects — highlighting only.
const todoLanguage = StreamLanguage.define({
token(stream) {
if (stream.sol() && stream.match(/^x\s.*/)) return "todoDone";
if (stream.sol() && stream.match(/^\([A-Z]\)(?=\s|$)/)) return "todoPriority";
// +project / @context / dates only count at word starts (start of line
// or after whitespace), per the todo.txt format.
const atWordStart = stream.start === 0 || /\s/.test(stream.string.charAt(stream.start - 1));
if (atWordStart) {
if (stream.match(/^\d{4}-\d{2}-\d{2}(?=\s|$)/)) return "todoDate";
if (stream.match(/^\+\S+/)) return "todoProject";
if (stream.match(/^@\S+/)) return "todoContext";
}
// Consume whitespace OR one plain word — never both, or the word after
// a space would be swallowed before it can be matched above.
if (!stream.eatSpace()) {
stream.next();
stream.eatWhile(/\S/);
}
return null;
},
tokenTable: {
todoDone: tags.strikethrough,
todoPriority: tags.keyword,
todoProject: tags.typeName,
todoContext: tags.atom,
todoDate: tags.meta,
},
});
const todoHighlightStyle = HighlightStyle.define([
{ tag: tags.strikethrough, textDecoration: "line-through", color: "var(--text-muted)" },
{ tag: tags.keyword, color: "var(--secondary)", fontWeight: "bold" },
{ tag: tags.typeName, color: "var(--link)" },
{ tag: tags.atom, color: "var(--primary-hover)" },
{ tag: tags.meta, color: "var(--text-muted)" },
]);
// Minimal chrome for the rail editor — the full editor `theme` above forces a
// 60vh min-height and page-editor padding that don't fit a sidebar widget.
// Sizing/height caps live in style.css under .todo-rail.
const todoTheme = EditorView.theme(
{
"&": { backgroundColor: "transparent", color: "var(--text)", fontSize: "var(--font-xs)" },
"&.cm-focused": { outline: "none" },
".cm-scroller": { fontFamily: '"Iosevka Slab", monospace', lineHeight: "1.5" },
".cm-content": { caretColor: "var(--text)" },
".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--text)" },
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
{ backgroundColor: "var(--bg-panel-hover)" },
},
{ dark: true }
);
window.CM = {
EditorState,
EditorSelection,
@@ -104,4 +161,7 @@ window.CM = {
startCompletion,
theme,
highlightStyle,
todoLanguage,
todoHighlightStyle,
todoTheme,
};