From 44eca2fc6ca69b4cea4a09cff1643d7b942a8432 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Wed, 25 Jun 2025 15:27:14 -0700 Subject: [PATCH] Better --- packages/spike/src/discord/cron.ts | 5 - packages/todo/src/dateAutocompletion.ts | 43 ++-- packages/todo/src/main.ts | 3 +- packages/todo/src/todo.test.old.ts | 171 ++++++++++++++ packages/todo/src/todo.test.ts | 213 +++++------------- packages/todo/src/todo.ts | 284 ++++++++++++++---------- packages/todo/src/todoDecorations.ts | 26 ++- packages/todo/src/todoKeymap.ts | 7 +- 8 files changed, 430 insertions(+), 322 deletions(-) create mode 100644 packages/todo/src/todo.test.old.ts diff --git a/packages/spike/src/discord/cron.ts b/packages/spike/src/discord/cron.ts index 5508c2b..cbb0643 100644 --- a/packages/spike/src/discord/cron.ts +++ b/packages/spike/src/discord/cron.ts @@ -21,11 +21,6 @@ export const runCronJobs = async (client: Client) => { return reminderDueDate <= nextDueDate }) - console.log( - `🌭 Checking for reminders due between now and ${nextDueDate.toISO()}, found ${ - reminders.length - } total reminders.` - ) if (upcomingReminders.length > 0) { console.log(`Found ${upcomingReminders.length} upcoming reminders to notify.`) const content = `These reminders are due soon, let them know!: ${JSON.stringify(upcomingReminders)}` diff --git a/packages/todo/src/dateAutocompletion.ts b/packages/todo/src/dateAutocompletion.ts index 1f6e9f2..204df0a 100644 --- a/packages/todo/src/dateAutocompletion.ts +++ b/packages/todo/src/dateAutocompletion.ts @@ -1,47 +1,38 @@ import { autocompletion, type CompletionContext, type CompletionResult } from "@codemirror/autocomplete" import { DateTime } from "luxon" -// Helper function to get date options -function getDateOptions() { +const getDateOptions = () => { const today = DateTime.local() - // Helper to create option with formatted date const createDateOption = (label: string, date: DateTime) => { - const formatted = date.toFormat("M/dd/yyyy") - return { - label, - detail: formatted, - apply: formatted, - } + const detail = date.toFormat("ccc, LLL d") + const apply = date.toFormat("M/dd/yyyy") + return { label: `${label}: `, detail, apply } } - const options = [createDateOption("today", today), createDateOption("tomorrow", today.plus({ days: 1 }))] + const options = [ + createDateOption("today", today), + createDateOption("tomorrow", today.plus({ days: 1 })), + createDateOption("next week", today.plus({ days: 7 })), + createDateOption("next month", today.plus({ months: 1 })), + ] // Add remaining days of the current week (starting from day after tomorrow) const currentWeekday = today.weekday // 1 = Monday, 7 = Sunday const weekdayNames = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] - for (let i = currentWeekday + 2; i <= 7; i++) { - const daysToAdd = i - currentWeekday - const date = today.plus({ days: daysToAdd }) - const weekdayName = weekdayNames[i - 1] - if (weekdayName) { - options.push(createDateOption(weekdayName, date)) - } + // Add the next 5 days of the week + for (let i = 1; i <= 6; i++) { + const nextDay = today.plus({ days: i }) + const nameIndex = (currentWeekday + i - 1) % 7 + options.push(createDateOption(weekdayNames[nameIndex]!, nextDay)) } - // Add next week (next Monday) - const nextMonday = today.plus({ days: 8 - currentWeekday }) - options.push(createDateOption("next week", nextMonday)) - - // Add next month - options.push(createDateOption("next month", today.plus({ months: 1 }))) - return options } // Completion function that triggers after @ -function dateCompletion(context: CompletionContext): CompletionResult | null { +const dateCompletion = (context: CompletionContext) => { const { state, pos } = context const line = state.doc.lineAt(pos) const lineText = line.text @@ -61,7 +52,7 @@ function dateCompletion(context: CompletionContext): CompletionResult | null { // Only complete if we found @ and cursor is right after it or after some text following @ if (atPos === -1) { - return null + return } const textAfterAt = lineText.slice(atPos + 1, linePos) diff --git a/packages/todo/src/main.ts b/packages/todo/src/main.ts index 3599457..374bda1 100644 --- a/packages/todo/src/main.ts +++ b/packages/todo/src/main.ts @@ -1,3 +1,2 @@ export { TodoEditor } from "@/todoEditor" -export { parseTodoLine, parseTodoList } from "@/todo" -export type { Todo } from "@/todo" +export { Todo } from "@/todo" diff --git a/packages/todo/src/todo.test.old.ts b/packages/todo/src/todo.test.old.ts new file mode 100644 index 0000000..6446f5d --- /dev/null +++ b/packages/todo/src/todo.test.old.ts @@ -0,0 +1,171 @@ +import { parseTodoList, parseTodoLine, Todo, TodoMeta } from "./todo" +import { test, expect } from "bun:test" + +test("parse things", () => { + check("- [ ] some words and that is it", { + done: false, + text: "some words and that is it", + tags: [], + children: [], + header: undefined, + }) + + check("- [x] some words", { + done: true, + text: "some words", + tags: [], + children: [], + header: undefined, + }) + + check("- [ ] some words -and - [ ] not another task", { + done: false, + text: "some words -and - [ ] not another task", + tags: [], + children: [], + header: undefined, + }) + + check("- [ ] a task with a #tag", { + done: false, + text: "a task with a #tag", + tags: ["tag"], + children: [], + header: undefined, + }) + + check("- [ ] a #tag in a task", { + done: false, + text: "a #tag in a task", + tags: ["tag"], + children: [], + header: undefined, + }) + + check("- [ ] a #tag and #another.", { + done: false, + text: "a #tag and #another.", + tags: ["tag", "another"], + children: [], + header: undefined, + }) + + check("- [ ] a word with a po#nd sign", { + done: false, + text: "a word with a po#nd sign", + tags: [], + children: [], + header: undefined, + }) + + check("- [ ] a date @1/2/25!", { + done: false, + text: "a date @1/2/25!", + tags: [], + dueDate: "2025-01-02", + children: [], + header: undefined, + }) + + check("- [ ] a date @2-2-25!", { + done: false, + text: "a date @2-2-25!", + tags: [], + dueDate: "2025-02-02", + children: [], + header: undefined, + }) + + check("- [ ] a @1/2/25 date and #tag", { + done: false, + text: "a @1/2/25 date and #tag", + tags: ["tag"], + dueDate: "2025-01-02", + children: [], + header: undefined, + }) + + check(" ", []) + + // Invalid formats + check("- [x status error", []) + check("- [. ] status error", []) + check("just text", []) + + // A full todo list + check( + ` +# Today +- [ ] a task with a #tag +- [x] a #tag and date @2/2/25 in a task + +# Tomorrow +- [ ] another task + - [ ] a subtask + - [x] completed + +- [ ] a task with a @1/2/25 date + +`, + [ + { + done: false, + text: "a task with a #tag", + tags: ["tag"], + dueDate: undefined, + children: [], + header: "Today", + }, + { + done: true, + text: "a #tag and date @2/2/25 in a task", + tags: ["tag"], + dueDate: "2025-02-02", + children: [], + header: "Today", + }, + { + done: false, + text: "another task", + tags: [], + dueDate: undefined, + children: [ + { done: false, text: "a subtask", tags: [], dueDate: undefined, children: [], header: "Tomorrow" }, + { done: true, text: "completed", tags: [], dueDate: undefined, children: [], header: "Tomorrow" }, + ], + header: "Tomorrow", + }, + { + done: false, + text: "a task with a @1/2/25 date", + tags: [], + dueDate: "2025-01-02", + children: [], + header: "Tomorrow", + }, + ] + ) +}) + +test("parse metadata positions", () => { + const line = "- [ ] do #tag @1/2/25" + const parsed = parseTodoLine(line) + expect(parsed, line).toBeDefined() + const { meta } = parsed as { todo: Todo; meta: TodoMeta } + + // '#tag' starts at index 9, ends at 9 + 4 + expect(meta.tags).toEqual([{ start: 7, end: 12 }]) + + // '+1/2/25' starts at index 14, ends at 14 + 6 + 1 = 21 + expect(meta.dates).toEqual([{ start: 12, end: 18 }]) +}) + +const check = (input: string, expectation: Todo | Todo[]) => { + const result = parseTodoList(input) + + if (expectation === undefined) { + expect(result, input).toBeUndefined() + } else { + expect(result, input).toEqual(Array.isArray(expectation) ? expectation : [expectation]) + } +} diff --git a/packages/todo/src/todo.test.ts b/packages/todo/src/todo.test.ts index 6446f5d..6b0081a 100644 --- a/packages/todo/src/todo.test.ts +++ b/packages/todo/src/todo.test.ts @@ -1,171 +1,66 @@ -import { parseTodoList, parseTodoLine, Todo, TodoMeta } from "./todo" +import { DateTime } from "luxon" +import { Todo } from "./todo" import { test, expect } from "bun:test" -test("parse things", () => { - check("- [ ] some words and that is it", { - done: false, - text: "some words and that is it", - tags: [], - children: [], - header: undefined, - }) - - check("- [x] some words", { +test("parsing valid todos", () => { + expectTodo("- [ ] some words and that is it", { done: false, text: "some words and that is it" }) + expectTodo("- [x] has #some #tags and @2/5/25", { done: true, - text: "some words", - tags: [], - children: [], - header: undefined, + text: "has #some #tags and @2/5/25", + tags: ["some", "tags"], + dueDate: DateTime.fromISO("2025-02-05"), }) - - check("- [ ] some words -and - [ ] not another task", { - done: false, - text: "some words -and - [ ] not another task", - tags: [], - children: [], - header: undefined, + expectTodo("- [ ] works with iso dates @2055/01/2", { + text: "works with iso dates @2055/01/2", + dueDate: DateTime.fromISO("2055-01-02"), }) - - check("- [ ] a task with a #tag", { - done: false, - text: "a task with a #tag", - tags: ["tag"], - children: [], - header: undefined, - }) - - check("- [ ] a #tag in a task", { - done: false, - text: "a #tag in a task", - tags: ["tag"], - children: [], - header: undefined, - }) - - check("- [ ] a #tag and #another.", { - done: false, - text: "a #tag and #another.", - tags: ["tag", "another"], - children: [], - header: undefined, - }) - - check("- [ ] a word with a po#nd sign", { - done: false, - text: "a word with a po#nd sign", - tags: [], - children: [], - header: undefined, - }) - - check("- [ ] a date @1/2/25!", { - done: false, - text: "a date @1/2/25!", - tags: [], - dueDate: "2025-01-02", - children: [], - header: undefined, - }) - - check("- [ ] a date @2-2-25!", { - done: false, - text: "a date @2-2-25!", - tags: [], - dueDate: "2025-02-02", - children: [], - header: undefined, - }) - - check("- [ ] a @1/2/25 date and #tag", { - done: false, - text: "a @1/2/25 date and #tag", - tags: ["tag"], - dueDate: "2025-01-02", - children: [], - header: undefined, - }) - - check(" ", []) - - // Invalid formats - check("- [x status error", []) - check("- [. ] status error", []) - check("just text", []) - - // A full todo list - check( - ` -# Today -- [ ] a task with a #tag -- [x] a #tag and date @2/2/25 in a task - -# Tomorrow -- [ ] another task - - [ ] a subtask - - [x] completed - -- [ ] a task with a @1/2/25 date - -`, - [ - { - done: false, - text: "a task with a #tag", - tags: ["tag"], - dueDate: undefined, - children: [], - header: "Today", - }, - { - done: true, - text: "a #tag and date @2/2/25 in a task", - tags: ["tag"], - dueDate: "2025-02-02", - children: [], - header: "Today", - }, - { - done: false, - text: "another task", - tags: [], - dueDate: undefined, - children: [ - { done: false, text: "a subtask", tags: [], dueDate: undefined, children: [], header: "Tomorrow" }, - { done: true, text: "completed", tags: [], dueDate: undefined, children: [], header: "Tomorrow" }, - ], - header: "Tomorrow", - }, - { - done: false, - text: "a task with a @1/2/25 date", - tags: [], - dueDate: "2025-01-02", - children: [], - header: "Tomorrow", - }, - ] - ) + expectTodo(" - [ ] handles nested todos", { text: "handles nested todos", indent: " " }) }) -test("parse metadata positions", () => { - const line = "- [ ] do #tag @1/2/25" - const parsed = parseTodoLine(line) - expect(parsed, line).toBeDefined() - const { meta } = parsed as { todo: Todo; meta: TodoMeta } - - // '#tag' starts at index 9, ends at 9 + 4 - expect(meta.tags).toEqual([{ start: 7, end: 12 }]) - - // '+1/2/25' starts at index 14, ends at 14 + 6 + 1 = 21 - expect(meta.dates).toEqual([{ start: 12, end: 18 }]) +test("todo nodes", () => { + const todo = Todo.parse("- [ ] some #words and @2/2/25") + expect(todo!.nodes).toEqual([ + { type: "text", content: "some", start: 6 }, + { type: "whitespace", content: " ", start: 10 }, + { type: "tag", content: "#words", start: 11 }, + { type: "whitespace", content: " ", start: 17 }, + { type: "text", content: "and", start: 18 }, + { type: "whitespace", content: " ", start: 21 }, + { type: "date", content: "@2/2/25", parsed: DateTime.fromISO("2025-02-02"), start: 22 }, + ]) }) -const check = (input: string, expectation: Todo | Todo[]) => { - const result = parseTodoList(input) +test("parsing invalid todos", () => { + expectInvalidTodo("- [ ]") + expectInvalidTodo(" -[x") + expectInvalidTodo("-[omg] some text") +}) - if (expectation === undefined) { - expect(result, input).toBeUndefined() - } else { - expect(result, input).toEqual(Array.isArray(expectation) ? expectation : [expectation]) +// Helpers +test("todo to string", () => { + const todo = Todo.parse("- [ ] some #words and @2/2/25") + expect(todo!.toString()).toBe("- [ ] some #words and @2/2/25") + + const nestedTodo = Todo.parse(" - [x] more #words and @2/2/25") + expect(nestedTodo!.toString()).toBe(" - [x] more #words and @2/2/25") +}) + +const expectTodo = (input: string, expectation: Partial) => { + const todo = Todo.parse(input) + if (!todo) { + throw new Error(`Failed to parse todo line: "${input}"`) + } + + for (const key in expectation) { + const typedKey = key as keyof Todo + const expectedValue = expectation[typedKey] + expect(todo[typedKey], `Unexpected "${typedKey}" for todo "${input}"`).toEqual(expectedValue!) } } + +const expectInvalidTodo = (input: string) => { + test(`parse invalid todo: ${input}`, () => { + const todo = Todo.parse(input) + expect(todo, `Expected parsing to fail for: "${input}"`).toBeUndefined() + }) +} diff --git a/packages/todo/src/todo.ts b/packages/todo/src/todo.ts index 1165a54..f99cee3 100644 --- a/packages/todo/src/todo.ts +++ b/packages/todo/src/todo.ts @@ -1,137 +1,179 @@ +import { ensure } from "@workshop/shared/utils" import { DateTime } from "luxon" -export type Todo = { +export class Todo { done: boolean - text: string - tags: string[] - dueDate?: string - children: Todo[] - header?: string - nested?: boolean -} + nodes: TodoNode[] + indent = "" -export type TodoMeta = { - type: "tag" | "date" - start: number - end: number -}[] - -export const parseTodoList = (text: string, zone = "America/Los_Angeles") => { - const todos: Todo[] = [] - const lines = text.split("\n") - let currentParent: Todo | undefined - let currentHeader - for (const line of lines) { - const headerMatch = line.match(/^\s*#+\s*(.*)/) - if (headerMatch) { - currentParent = undefined - currentHeader = headerMatch[1]!.trim() - continue - } else if (line.trim() === "") { - continue - } - - const { todo } = parseTodoLine(line, currentHeader, zone) - if (todo) { - if (todo.nested) { - if (currentParent) { - currentParent.children.push(todo) - } else { - todos.push(todo) // If there's no parent, treat it as a top-level todo - } - } else { - todos.push(todo) - currentParent = todo - } - } else { - // console.warn(`❌ Skipping invalid todo line "${line}"`) - } - } - return todos -} - -export const todoToText = (todo: Todo): string => { - let indent = "" - if (todo.nested) { - indent += " " // Indent for nested todos - } - const text = `${indent}- [${todo.done ? "x" : " "}] ${todo.text}` - return text -} - -const invalidLine = { todo: undefined, meta: undefined } as const -export const parseTodoLine = ( - line: string, - header?: string, - zone = "America/Los_Angeles" -): { todo: Todo; meta: TodoMeta } | typeof invalidLine => { - const meta: TodoMeta = [] - let offset = 0 - - const todo: Todo = { - done: false, - text: "", - tags: [], - dueDate: undefined, - children: [], - header, - nested: isSubtodo(line), + static parse(line: string) { + return parseTodo(line) } - offset += line.search(/\S/) - line = line.trimStart() + constructor(nodes: TodoNode[], done: boolean, indent = "") { + this.nodes = nodes + this.done = done + this.indent = indent + } + + toString() { + let string = `${this.indent}- [${this.done ? "x" : " "}] ${this.text}` + return string + } + + get text() { + return this.nodes.map((node) => node.content).join("") + } + + get tags() { + return this.nodes.filter((node) => node.type === "tag").map((node) => node.content.slice(1)) + } + + get dueDate() { + const dateNode = this.nodes.find((node) => node.type === "date") + return dateNode && dateNode.parsed + } +} + +type TodoNode = { content: string; start: number } & ( + | { type: "whitespace" } + | { type: "text" } + | { type: "date"; parsed: DateTime } + | { type: "tag" } +) + +const parseError = (line: string, offset: number, message?: string): string => { + const errorMessage = message + ? `Failed to parse node at offset ${offset}` + : `Failed to parse node at offset ${offset}` + return `${errorMessage} +${line} +${" ".repeat(offset)}^ error occurred here` +} + +const parseTodo = (line: string): Todo | undefined => { + const indentMatch = line.match(/^\s+/) + const indent = indentMatch?.[0] ?? "" + let offset = indent.length // Check for status - const match = line.match(/- \[\s*(\w?)\s*\]\s+/i) - if (!match) return invalidLine // Invalid format, no status found - todo.done = Boolean(match[1]) - offset += match[0].length - line = line.slice(match[0].length).trimStart() + const match = line.match(/\s*-\s*\[\s*(\w?)\s*\]\s+/i) + if (!match) return // Invalid format, no status found + const done = Boolean(match[1]) + offset = match[0].length - // Save text - todo.text = line + const todoNodes: TodoNode[] = [] + let previousOffset = -1 + while (offset < line.length) { + ensure(offset !== previousOffset, parseError(line, offset, `Infinite loop detected.`)) + previousOffset = offset - // Extract tags - todo.text.matchAll(/(?:^|\s)#(\w+)/g).forEach((match) => { - const start = match.index! + offset - const end = start + match[0].length - meta.push({ type: "tag", start, end }) - todo.tags.push(match[1]!) - }) - - // Extract due date in Month/Day/Year - const dateMatches = todo.text.matchAll(/(?:^|\s)@([\d\/\-]{6,})/g) - - for (const match of dateMatches) { - let [month, day, year] = match[1]!.split(/[\/-]/) - if (!month || !day || !year) continue - - if (year.length == 2) year = `20${year}` // Assume 21st century for two-digit years - const dateTime = DateTime.fromObject( - { - year: parseInt(year, 10), - month: parseInt(month, 10), - day: parseInt(day, 10), - }, - { zone } - ) - - if (dateTime.isValid) { - const start = match.index + offset - const end = start + match[0].length - - meta.push({ type: "date", start, end }) - todo.dueDate = dateTime.toISODate() - break - } else { - console.warn(`🕰️ Invalid date format "${match[1]}"`) + // eat whitespace + const whitespaceMatch = line.slice(offset).match(/^\s+/) + if (whitespaceMatch) { + const content = whitespaceMatch[0] + todoNodes.push({ type: "whitespace", content, start: offset }) + offset += content.length + continue } + + // eat text + const isTag = line.slice(offset).startsWith("#") + const isDate = line.slice(offset).startsWith("@") + + let node: TodoNode | undefined + if (isTag) { + node = parseTag(line, offset) + } else if (isDate) { + node = parseDate(line, offset) + } else { + node = parseText(line, offset) + } + + ensure(node, parseError(line, offset)) + + todoNodes.push(node) + offset += node.content.length } - meta.sort((a, b) => a.start - b.start) // CodeMiror expects metadata to be sorted by start position - return { todo, meta } + return new Todo(todoNodes, done, indent) } -const isSubtodo = (line: string) => { - return /^\s+/.test(line) +const parseTag = (line: string, start: number) => { + const tagMatch = line.slice(start).match(/^#(\w+)/) + const content = tagMatch?.[0] + if (!content) { + return parseText(line, start) + } + + const node: TodoNode = { type: "tag", content, start } + + return node } + +const parseDate = (line: string, start: number) => { + const dateMatch = line.slice(start).match(/^@([\d\/\-]{6,})/) + const content = dateMatch?.[0] + + if (!content) { + return parseText(line, start) + } + + let [month, day, year] = content.slice(1).split(/[\/-]/) || [] + if (!month || !day || !year) { + return parseText(line, start) + } + + const date = DateTime.fromMillis(Date.parse(content.slice(1))) + + if (date.isValid) { + const node: TodoNode = { type: "date", content, parsed: date, start } + return node + } else { + return parseText(line, start) + } +} + +const parseText = (line: string, start: number) => { + const textMatch = line.slice(start).match(/^\S+/) + const content = textMatch?.[0] + if (!content) return + + const node: TodoNode = { type: "text", content, start } + + return node +} + +// export const parseTodoList = (text: string, zone = defaultZone) => { +// const todos: Todo[] = [] +// const lines = text.split("\n") +// let currentParent: Todo | undefined +// let currentHeader +// for (const line of lines) { +// const headerMatch = line.match(/^\s*#+\s*(.*)/) +// if (headerMatch) { +// currentParent = undefined +// currentHeader = headerMatch[1]!.trim() +// continue +// } else if (line.trim() === "") { +// continue +// } + +// const { todo } = parseTodoLine(line, currentHeader, zone) +// if (todo) { +// if (todo.nested) { +// if (currentParent) { +// currentParent.children.push(todo) +// } else { +// todos.push(todo) // If there's no parent, treat it as a top-level todo +// } +// } else { +// todos.push(todo) +// currentParent = todo +// } +// } else { +// // console.warn(`❌ Skipping invalid todo line "${line}"`) +// } +// } +// return todos +// } diff --git a/packages/todo/src/todoDecorations.ts b/packages/todo/src/todoDecorations.ts index 23051f8..aca527c 100644 --- a/packages/todo/src/todoDecorations.ts +++ b/packages/todo/src/todoDecorations.ts @@ -1,4 +1,4 @@ -import { parseTodoLine, type Todo } from "@/todo" +import { Todo } from "@/todo" import { RangeSetBuilder, StateEffect } from "@codemirror/state" import { EditorView, Decoration, ViewPlugin, ViewUpdate } from "@codemirror/view" import { type RefObject } from "hono/jsx" @@ -33,7 +33,7 @@ export const todoDecorations = (filterRef: RefObject) => { while (pos <= to) { const line = doc.lineAt(pos) const text = line.text - const { todo, meta } = parseTodoLine(text, "") + const todo = Todo.parse(text) if (todo) { if (filterRef.current && !todo.tags.find((t) => t.startsWith(filterRef.current!))) { @@ -46,8 +46,7 @@ export const todoDecorations = (filterRef: RefObject) => { if (todo.dueDate) { // Get number of days until due date - const dueDate = DateTime.fromISO(todo.dueDate, { zone: "America/Los_Angeles" }) - const daysUntilDue = dueDate.diffNow("days").days + const daysUntilDue = todo.dueDate.diffNow("days").days if (daysUntilDue <= -1) { builder.add(line.from, line.to, Decoration.mark({ class: "todo-overdue" })) } else if (daysUntilDue <= 0) { @@ -55,7 +54,7 @@ export const todoDecorations = (filterRef: RefObject) => { } } - for (const { type, start, end } of meta) { + for (const { type, start, end } of decorationsFor(todo)) { builder.add(line.from + start, line.from + end, Decoration.mark({ class: `todo-${type}` })) } } else if (/^\s*#+\s/.test(text)) { @@ -73,3 +72,20 @@ export const todoDecorations = (filterRef: RefObject) => { { decorations: (v) => v.decorations } ) } + +const decorationsFor = (todo: Todo) => { + const decorations: { type: string; start: number; end: number }[] = [] + + for (const node of todo.nodes) { + const start = node.start + const end = node.start + node.content.length + + if (node.type === "date") { + decorations.push({ type: "date", start, end }) + } else if (node.type === "tag") { + decorations.push({ type: "tag", start, end }) + } + } + + return decorations +} diff --git a/packages/todo/src/todoKeymap.ts b/packages/todo/src/todoKeymap.ts index 2f7b931..cf39cd8 100644 --- a/packages/todo/src/todoKeymap.ts +++ b/packages/todo/src/todoKeymap.ts @@ -1,6 +1,6 @@ import { indentMore, indentLess } from "@codemirror/commands" import { EditorView, keymap } from "@codemirror/view" -import { parseTodoLine, todoToText } from "./todo" +import { Todo } from "./todo" import { type RefObject } from "hono/jsx" export const todoKeymap = (filterElRef: RefObject) => { @@ -21,16 +21,15 @@ export const todoKeymap = (filterElRef: RefObject) => { const { head } = state.selection.main const line = state.doc.lineAt(head) - const { todo } = parseTodoLine(line.text) + const todo = Todo.parse(line.text) if (!todo) return false todo.done = !todo.done - const updatedLine = todoToText(todo) const from = line.from const to = line.to const offset = head - from view.dispatch({ - changes: { from, to, insert: updatedLine }, + changes: { from, to, insert: todo.toString() }, selection: { anchor: from + offset }, userEvent: "input", })