This commit is contained in:
Corey Johnson 2025-06-26 14:03:06 -07:00
parent fcc221945b
commit 24c5ac6dc5
8 changed files with 100 additions and 11 deletions

View File

@ -25,7 +25,6 @@ export const runCronJobs = async (client: Client) => {
console.log(`Found ${upcomingReminders.length} upcoming reminders to notify.`)
const content = `These reminders are due soon, let them know!: ${JSON.stringify(upcomingReminders)}`
console.log(`🌭`, { content })
const output = await respondToSystemMessage(content, channelId)
ensure(output, "The response to reminders should not be undefined")

View File

@ -8,7 +8,7 @@
color: #3B82F6;
}
.todo-completed .todo-tag, .todo-completed .todo-date {
.todo-completed .todo-tag, .todo-completed .todo-date, .todo-completed .todo-time-estimate {
color: #6B7280;
}
@ -16,6 +16,10 @@
color: #10B981;
}
.todo-time-estimate {
color: #aBb2d0;
}
.todo-overdue .todo-date {
color: #EF4444;
}
@ -35,6 +39,10 @@
font-weight: 700;
}
.todo-checkbox {
cursor: pointer;
}
.todo-filtered {
background-color: greenyellow;
}

View File

@ -4,10 +4,11 @@ import { test, expect } from "bun:test"
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", {
expectTodo("- [x] has #some #tags and @2/5/25 5m", {
done: true,
text: "has #some #tags and @2/5/25",
text: "has #some #tags and @2/5/25 5m",
tags: ["some", "tags"],
timeEstimate: 300,
dueDate: DateTime.fromISO("2025-02-05"),
})
expectTodo("- [ ] works with iso dates @2055/01/2", {

View File

@ -41,6 +41,10 @@ export class Todo {
return this.nodes.map((node) => node.content).join("")
}
get timeEstimate() {
return this.nodes.find((node) => node.type === "time-estimate")?.seconds
}
get tags() {
return this.nodes.filter((node) => node.type === "tag").map((node) => node.content.slice(1))
}
@ -56,6 +60,7 @@ type TodoNode = { content: string; start: number } & (
| { type: "text" }
| { type: "date"; parsed: DateTime }
| { type: "tag" }
| { type: "time-estimate"; seconds: number }
)
const parseError = (line: string, offset: number, message?: string): string => {
@ -96,12 +101,15 @@ const parseTodo = (line: string): Todo | undefined => {
// eat text
const isTag = line.slice(offset).startsWith("#")
const isDate = line.slice(offset).startsWith("@")
const isTimeEstimate = line.slice(offset).match(/^[\d\.]+[mh]$/)
let node: TodoNode | undefined
if (isTag) {
node = parseTag(line, offset)
} else if (isDate) {
node = parseDate(line, offset)
} else if (isTimeEstimate) {
node = parseTimeEstimate(line, offset)
} else {
node = parseText(line, offset)
}
@ -115,6 +123,29 @@ const parseTodo = (line: string): Todo | undefined => {
return new Todo(todoNodes, done, indent)
}
const parseTimeEstimate = (line: string, start: number) => {
const timeMatch = line.slice(start).match(/^([\d\.]+)([mh])($|\s+)/)
const [_, numberString, unitString] = timeMatch ?? []
if (!numberString || !unitString) {
return parseText(line, start)
}
const value = parseInt(numberString, 10)
const unit = unitString.toLowerCase()
let seconds: number
if (unit === "m") {
seconds = value * 60
} else if (unit === "h") {
seconds = value * 3600
} else {
return parseText(line, start)
}
const node: TodoNode = { type: "time-estimate", content: timeMatch![0], seconds, start }
return node
}
const parseTag = (line: string, start: number) => {
const tagMatch = line.slice(start).match(/^#(\w+)/)
const content = tagMatch?.[0]

View File

@ -0,0 +1,46 @@
import { EditorView } from "@codemirror/view"
import { Todo } from "@/todo"
export const todoClickHandler = EditorView.domEventHandlers({
click(event, view) {
const pos = view.posAtCoords({ x: event.clientX, y: event.clientY })
if (pos === null) return false
const line = view.state.doc.lineAt(pos)
const todo = Todo.parse(line.text)
if (!todo) return false
// Find the checkbox position in the line
const checkboxMatch = line.text.match(/^(\s*-\s*\[)\s*(\w?)\s*(\]\s+)/)
if (!checkboxMatch) return false
const [fullMatch, beforeCheckbox, currentState, afterCheckbox] = checkboxMatch
if (!beforeCheckbox || !afterCheckbox) return false
const checkboxStart = line.from + beforeCheckbox.length
const checkboxEnd = checkboxStart + 1
// Check if the click was within the checkbox area (including some padding)
const clickOffset = pos - line.from
// Allow clicking anywhere in the "- [ ]" or "- [x]" part
if (clickOffset >= 0 && clickOffset <= fullMatch.length) {
// Toggle the checkbox state
const newState = todo.done ? " " : "x"
view.dispatch({
changes: {
from: checkboxStart,
to: checkboxEnd,
insert: newState,
},
})
event.preventDefault()
return true
}
return false
},
})

View File

@ -54,6 +54,13 @@ export const todoDecorations = (filterRef: RefObject<string>) => {
}
}
// Add checkbox decoration for cursor styling
const checkboxMatch = text.match(/^(\s*-\s*\[)\s*(\w?)\s*(\]\s+)/)
if (checkboxMatch) {
const checkboxEnd = checkboxMatch[0].length
builder.add(line.from, line.from + checkboxEnd, Decoration.mark({ class: "todo-checkbox" }))
}
for (const { type, start, end } of decorationsFor(todo)) {
builder.add(line.from + start, line.from + end, Decoration.mark({ class: `todo-${type}` }))
}
@ -77,14 +84,10 @@ const decorationsFor = (todo: Todo) => {
const decorations: { type: string; start: number; end: number }[] = []
for (const node of todo.nodes) {
const start = node.start
const { type, start } = node
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 })
}
decorations.push({ type, start, end })
}
return decorations

View File

@ -7,6 +7,7 @@ import { foldGutter, foldKeymap } from "@codemirror/language"
import { refreshFilterEffect, todoDecorations } from "@/todoDecorations"
import { autoTodoOnNewline } from "@/autoTodoOnNewline"
import { todoKeymap } from "@/todoKeymap"
import { todoClickHandler } from "@/todoClickHandler"
import { dateAutocompletion } from "@/dateAutocompletion"
import { DateTime } from "luxon"
@ -50,6 +51,7 @@ export const TodoEditor = ({ defaultValue, onChange }: TodoEditorProps) => {
changeListener,
autoTodoOnNewline,
todoKeymap(filterElRef),
todoClickHandler,
dateAutocompletion,
keymap.of(historyKeymap),
keymap.of(defaultKeymap),

View File

@ -24,7 +24,6 @@ export const todoListToString = (todoList: TodoList): string => {
}
const todoJsonToString = (todoOrString: TodoOrString): string => {
console.log(`🌭`, { todoOrString })
if (typeof todoOrString === "string") {
return `${todoOrString}\n`
}