Show TODOs across all note files in the editor via LSP #129
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
TODOs are scattered across markdown files in the workspace, but the editor only surfaces what's in the open buffer. The language server should expose all workspace todos so Zed can display and navigate them.
Proposed approach
Primary: implement LSP pull diagnostics (LSP 3.17)
textDocument/diagnostic — todos for a single document
workspace/diagnostic — workspace-wide todos, reported as diagnostics with severity Hint (or Information)
workspace/diagnostic/refresh — invalidate when the note index changes (file watcher / save)
Zed supports both push and pull diagnostics; workspace diagnostics will show up inline, in the scrollbar, and aggregated in the diagnostics: deploy panel — which effectively becomes the global todo list.
Secondary: implement workspace/symbol and emit each TODO as a symbol (name = todo text, location = file + line) so todos are reachable via fuzzy symbol search.
Implementation notes
Advertise diagnosticProvider (with workspaceDiagnostics: true) and workspaceSymbolProvider in server capabilities
Maintain a workspace-wide todo index (scan on init, update on didChange/didSave/file events)
Use resultId/unchanged reports in workspace/diagnostic to avoid re-sending unchanged files
Fallback option if pull diagnostics cause issues: push textDocument/publishDiagnostics for all indexed files, including unopened ones
Out of scope / later
Code lens per-file todo counts (Zed support exists but is off by default via "code_lens" setting)
textDocument/documentSymbol entries for todos in the current file's outline
Todo states (done/open) mapped to diagnostic tags
Acceptance criteria
[ ] Opening the project shows all todos from all files in Zed's diagnostics panel without opening the files
[ ] Editing/removing a todo updates diagnostics within ~1s
[ ] Todos are findable via workspace symbol search
Implementation plan
Current state (relevant to this ticket)
src/cli/commands/lsp.rsalready implements atower-lsp 0.20server (streamd lsp) withtextDocumentSync,completionProvider,documentSymbolProvider,codeActionProvider("Mark task as done"),workspaceSymbolProvider,referencesProvider,renameProvider. Diagnostics today are push-only (publish_diagnosticson open/change/save) and cover only R15 filename format + R18 timesheet overlaps for the currently open file — nothing workspace-wide.Confirmed:
tower-lsp 0.20.0already exposesdiagnostic()(textDocument/diagnostic),workspace_diagnostic()(workspace/diagnostic), andclient.workspace_diagnostic_refresh()— so the ticket's primary (pull-diagnostics) approach needs no new dependency.Todos are not literal "TODO" text — they're
@Taskshards without@Done/@Waiting/@NotDone(open) or with@Waiting(waiting), perTaskConfiguration(src/localize/preconfigured.rs).collect_open_tasks()(src/cli/commands/todo.rs:15) already doesfind_shard_by_position(&all_shards, "task", "open")— same pattern will be reused for"waiting".Gaps: no recursive workspace scan (
symbol/references/renameall useWalkDir::new(...).max_depth(1), so subdirectories are invisible today), no markdown file watcher (only.streamd.tomlis watched), nodiagnosticProvidercapability, no todo-specific diagnostics or symbols.Scope decisions (confirmed with reporter)
.git) and switchsymbol,references,rename, and the new todo index to use it — fixes an existing inconsistency, not just the new feature.task == openandtask == waitingcount as todos (distinguished by message text, e.g. a[waiting]prefix), matchingfind_shard_by_position(..., "task", "open"/"waiting").DiagnosticSeverity::INFORMATION(Hint-level diagnostics are often filtered/underline-only in editors incl. Zed's problems panel).resultId/unchanged-report caching. Acceptable for typical vault sizes and the ~1s acceptance target; can follow up later if profiling shows it's needed.Implementation steps
Shared recursive file walk helper — new fn (e.g.
list_markdown_files(base: &Path) -> Vec<PathBuf>) usingWalkDir::new(base)withoutmax_depth(1), filtering.mdextension and skipping any path component starting with.. Replace the three existingmax_depth(1)call sites (symbol,references,rename) with it.Todo extraction helper — new fn in
lsp.rs(or reuse/extendfind_shard_by_position) that, given a parsed/localized shard tree, returns todo shards for bothopenandwaitingstates with their line ranges and text.textDocument/diagnostic— implementdiagnostic(): run existingcompute_diagnostics(R15/R18) plus new todo diagnostics for the single requested document, returnDocumentDiagnosticReportResult::Report(RelatedFullDocumentDiagnosticReport { .. }).workspace/diagnostic— implementworkspace_diagnostic(): use the recursive walk helper to enumerate all.mdfiles underbase_folder, parse/localize each (reusingfile_cachewhere already populated), compute todo diagnostics per file (R15/R18 stay document-scoped, not included here since they need the open buffer's live content — confirm during implementation whether R15/R18 should also run workspace-wide from disk content), and return aWorkspaceDiagnosticReportResultwith oneWorkspaceFullDocumentDiagnosticReportper file that has todos.Advertise capability — add
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions { workspace_diagnostics: true, inter_file_dependencies: false, ..Default::default() }))toinitialize().Markdown file watcher + refresh — extend
initialized()to also register a**/*.mdwatcher (create/change/delete) alongside the existing.streamd.tomlwatcher. Indid_change_watched_files, on markdown changes: invalidate the affectedfile_cache/file_linesentries and callself.client.workspace_diagnostic_refresh()so Zed re-pulls. Also trigger refresh fromdid_save/did_changefor the open-buffer case (todo added/removed without external save).Workspace symbols for todos — extend
collect_workspace_symbols(or add a parallel collector) so@Taskshards emit aSymbolInformationwhosenameis the todo's text content (not just the marker name"Task"),kinddistinguishing appropriately (e.g.SymbolKind::EVENTor keepSTRING— pick something sensible),location= file + line. Runs over the same recursive file list from step 1.Tests (extending the existing
mod testsblock inlsp.rs, following current conventions):.mdfiles in nested directories, skips.git.workspace/diagnosticover a fixture tree returns one report per file containing a todo, empty otherwise.textDocument/diagnosticincludes todo diagnostics alongside existing R15/R18 ones..streamd.tomland**/*.mdpatterns.Docs — update
README.md/REQUIREMENTS.mdper CLAUDE.md convention: describe the new pull-diagnostics/workspace-symbol todo surfacing under the LSP section.Acceptance criteria mapping
Out of scope (per ticket)
Code lens per-file todo counts,
textDocument/documentSymboltodo entries in current-file outline, done/open state mapped to diagnostic tags,resultId/unchanged-report caching (deferred, see above), fallback push-diagnostics-for-all-files (only needed if pull diagnostics prove unreliable in Zed during implementation/testing).Implementation complete
PR: #130 (branch
129_lsp-workspace-todo-diagnostics)Time: ~25 minutes of active implementation (excluding the earlier
/refineplanning pass on this ticket).Tokens: not something I can introspect precisely from within the session — as a rough proxy for effort: 6 commits, ~440 lines added to
src/cli/commands/lsp.rs(including ~150 lines of new unit tests), plus README/REQUIREMENTS updates.Summary of what was built
textDocument/diagnostic+workspace/diagnostic(LSP 3.17 pull diagnostics) report open/waiting@Taskshards across the whole workspace asInformation-severity diagnostics — this is now effectively the global todo list in Zed's diagnostics panel, not just what's open in a buffer.workspace/symboladditionally emits a symbol per todo whose name is the todo's own text, so todos are fuzzy-findable by content, not just by marker name.**/*.mdfile watcher (new, alongside the existing.streamd.tomlone) invalidates stale cache entries and triggersworkspace/diagnostic/refresh;didSavedoes the same, so edits propagate to the workspace view without relying on the client's own pull heuristics.Notable findings during implementation
workspace/symbol,textDocument/references, andtextDocument/renamewere all silently limited to the top-level directory (WalkDir::max_depth(1)) before this change — notes in subdirectories were invisible to every cross-file LSP feature, not just the new todo index. Fixed as part of this ticket since the new recursive todo scan needed the same capability anyway.tower-lsp 0.20.0(already a dependency) fully supports the ticket's primary pull-diagnostics approach (diagnostic(),workspace_diagnostic(),client.workspace_diagnostic_refresh()) — no new crate dependency was needed, and the "fallback push-diagnostics" option from the ticket wasn't necessary.workspace/diagnostic/refreshon everydidChangekeystroke (only ondidSaveand external file-watcher events) to avoid notification spam during typing, per the "keep it simple" scope decision from refinement — flagging this in case ~1s live-typing responsiveness turns out to matter more than expected in practice.resultId/unchanged-report caching was implemented (per the confirmed scope decision) — each pull request recomputes from scratch. Fine for typical vault sizes; worth revisiting if profiling ever shows it matters.Version bumped 0.2.7 → 0.3.0 per request, so merging this PR triggers a new release.