Show TODOs across all note files in the editor via LSP #129

Closed
opened 2026-08-06 19:27:59 +02:00 by kfickel · 2 comments
Owner

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

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
Author
Owner

Implementation plan

Current state (relevant to this ticket)

src/cli/commands/lsp.rs already implements a tower-lsp 0.20 server (streamd lsp) with textDocumentSync, completionProvider, documentSymbolProvider, codeActionProvider ("Mark task as done"), workspaceSymbolProvider, referencesProvider, renameProvider. Diagnostics today are push-only (publish_diagnostics on 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.0 already exposes diagnostic() (textDocument/diagnostic), workspace_diagnostic() (workspace/diagnostic), and client.workspace_diagnostic_refresh() — so the ticket's primary (pull-diagnostics) approach needs no new dependency.

Todos are not literal "TODO" text — they're @Task shards without @Done/@Waiting/@NotDone (open) or with @Waiting (waiting), per TaskConfiguration (src/localize/preconfigured.rs). collect_open_tasks() (src/cli/commands/todo.rs:15) already does find_shard_by_position(&all_shards, "task", "open") — same pattern will be reused for "waiting".

Gaps: no recursive workspace scan (symbol/references/rename all use WalkDir::new(...).max_depth(1), so subdirectories are invisible today), no markdown file watcher (only .streamd.toml is watched), no diagnosticProvider capability, no todo-specific diagnostics or symbols.

Scope decisions (confirmed with reporter)

  • Recursive walks: introduce one shared recursive file-listing helper (skip dotdirs like .git) and switch symbol, references, rename, and the new todo index to use it — fixes an existing inconsistency, not just the new feature.
  • Todo scope: both task == open and task == waiting count as todos (distinguished by message text, e.g. a [waiting] prefix), matching find_shard_by_position(..., "task", "open"/"waiting").
  • Severity: DiagnosticSeverity::INFORMATION (Hint-level diagnostics are often filtered/underline-only in editors incl. Zed's problems panel).
  • Caching: first pass recomputes full workspace diagnostics per request, no 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

  1. Shared recursive file walk helper — new fn (e.g. list_markdown_files(base: &Path) -> Vec<PathBuf>) using WalkDir::new(base) without max_depth(1), filtering .md extension and skipping any path component starting with .. Replace the three existing max_depth(1) call sites (symbol, references, rename) with it.

  2. Todo extraction helper — new fn in lsp.rs (or reuse/extend find_shard_by_position) that, given a parsed/localized shard tree, returns todo shards for both open and waiting states with their line ranges and text.

  3. textDocument/diagnostic — implement diagnostic(): run existing compute_diagnostics (R15/R18) plus new todo diagnostics for the single requested document, return DocumentDiagnosticReportResult::Report(RelatedFullDocumentDiagnosticReport { .. }).

  4. workspace/diagnostic — implement workspace_diagnostic(): use the recursive walk helper to enumerate all .md files under base_folder, parse/localize each (reusing file_cache where 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 a WorkspaceDiagnosticReportResult with one WorkspaceFullDocumentDiagnosticReport per file that has todos.

  5. Advertise capability — add diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions { workspace_diagnostics: true, inter_file_dependencies: false, ..Default::default() })) to initialize().

  6. Markdown file watcher + refresh — extend initialized() to also register a **/*.md watcher (create/change/delete) alongside the existing .streamd.toml watcher. In did_change_watched_files, on markdown changes: invalidate the affected file_cache/file_lines entries and call self.client.workspace_diagnostic_refresh() so Zed re-pulls. Also trigger refresh from did_save/did_change for the open-buffer case (todo added/removed without external save).

  7. Workspace symbols for todos — extend collect_workspace_symbols (or add a parallel collector) so @Task shards emit a SymbolInformation whose name is the todo's text content (not just the marker name "Task"), kind distinguishing appropriately (e.g. SymbolKind::EVENT or keep STRING — pick something sensible), location = file + line. Runs over the same recursive file list from step 1.

  8. Tests (extending the existing mod tests block in lsp.rs, following current conventions):

    • Recursive walk helper finds .md files in nested directories, skips .git.
    • Todo extraction returns both open and waiting shards with correct text/state prefix.
    • workspace/diagnostic over a fixture tree returns one report per file containing a todo, empty otherwise.
    • textDocument/diagnostic includes todo diagnostics alongside existing R15/R18 ones.
    • Workspace symbol search finds a todo by its text content in a nested file.
    • Watcher registration includes both .streamd.toml and **/*.md patterns.
  9. Docs — update README.md / REQUIREMENTS.md per CLAUDE.md convention: describe the new pull-diagnostics/workspace-symbol todo surfacing under the LSP section.

Acceptance criteria mapping

  • "Opening the project shows all todos... without opening the files" → steps 4–6.
  • "Editing/removing a todo updates diagnostics within ~1s" → step 6 (watcher + refresh), step 4 recompute is fast enough without caching for typical vaults.
  • "Todos are findable via workspace symbol search" → step 7 + step 1 (recursive).

Out of scope (per ticket)

Code lens per-file todo counts, textDocument/documentSymbol todo 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 plan ### Current state (relevant to this ticket) `src/cli/commands/lsp.rs` already implements a `tower-lsp 0.20` server (`streamd lsp`) with `textDocumentSync`, `completionProvider`, `documentSymbolProvider`, `codeActionProvider` ("Mark task as done"), `workspaceSymbolProvider`, `referencesProvider`, `renameProvider`. Diagnostics today are **push-only** (`publish_diagnostics` on 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.0` already exposes `diagnostic()` (`textDocument/diagnostic`), `workspace_diagnostic()` (`workspace/diagnostic`), and `client.workspace_diagnostic_refresh()` — so the ticket's primary (pull-diagnostics) approach needs no new dependency. Todos are not literal "TODO" text — they're `@Task` shards without `@Done`/`@Waiting`/`@NotDone` (open) or with `@Waiting` (waiting), per `TaskConfiguration` (`src/localize/preconfigured.rs`). `collect_open_tasks()` (`src/cli/commands/todo.rs:15`) already does `find_shard_by_position(&all_shards, "task", "open")` — same pattern will be reused for `"waiting"`. Gaps: no recursive workspace scan (`symbol`/`references`/`rename` all use `WalkDir::new(...).max_depth(1)`, so subdirectories are invisible today), no markdown file watcher (only `.streamd.toml` is watched), no `diagnosticProvider` capability, no todo-specific diagnostics or symbols. ### Scope decisions (confirmed with reporter) - **Recursive walks**: introduce one shared recursive file-listing helper (skip dotdirs like `.git`) and switch `symbol`, `references`, `rename`, and the new todo index to use it — fixes an existing inconsistency, not just the new feature. - **Todo scope**: both `task == open` and `task == waiting` count as todos (distinguished by message text, e.g. a `[waiting]` prefix), matching `find_shard_by_position(..., "task", "open"/"waiting")`. - **Severity**: `DiagnosticSeverity::INFORMATION` (Hint-level diagnostics are often filtered/underline-only in editors incl. Zed's problems panel). - **Caching**: first pass recomputes full workspace diagnostics per request, no `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 1. **Shared recursive file walk helper** — new fn (e.g. `list_markdown_files(base: &Path) -> Vec<PathBuf>`) using `WalkDir::new(base)` without `max_depth(1)`, filtering `.md` extension and skipping any path component starting with `.`. Replace the three existing `max_depth(1)` call sites (`symbol`, `references`, `rename`) with it. 2. **Todo extraction helper** — new fn in `lsp.rs` (or reuse/extend `find_shard_by_position`) that, given a parsed/localized shard tree, returns todo shards for both `open` and `waiting` states with their line ranges and text. 3. **`textDocument/diagnostic`** — implement `diagnostic()`: run existing `compute_diagnostics` (R15/R18) plus new todo diagnostics for the single requested document, return `DocumentDiagnosticReportResult::Report(RelatedFullDocumentDiagnosticReport { .. })`. 4. **`workspace/diagnostic`** — implement `workspace_diagnostic()`: use the recursive walk helper to enumerate all `.md` files under `base_folder`, parse/localize each (reusing `file_cache` where 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 a `WorkspaceDiagnosticReportResult` with one `WorkspaceFullDocumentDiagnosticReport` per file that has todos. 5. **Advertise capability** — add `diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions { workspace_diagnostics: true, inter_file_dependencies: false, ..Default::default() }))` to `initialize()`. 6. **Markdown file watcher + refresh** — extend `initialized()` to also register a `**/*.md` watcher (create/change/delete) alongside the existing `.streamd.toml` watcher. In `did_change_watched_files`, on markdown changes: invalidate the affected `file_cache`/`file_lines` entries and call `self.client.workspace_diagnostic_refresh()` so Zed re-pulls. Also trigger refresh from `did_save`/`did_change` for the open-buffer case (todo added/removed without external save). 7. **Workspace symbols for todos** — extend `collect_workspace_symbols` (or add a parallel collector) so `@Task` shards emit a `SymbolInformation` whose `name` is the todo's text content (not just the marker name `"Task"`), `kind` distinguishing appropriately (e.g. `SymbolKind::EVENT` or keep `STRING` — pick something sensible), `location` = file + line. Runs over the same recursive file list from step 1. 8. **Tests** (extending the existing `mod tests` block in `lsp.rs`, following current conventions): - Recursive walk helper finds `.md` files in nested directories, skips `.git`. - Todo extraction returns both open and waiting shards with correct text/state prefix. - `workspace/diagnostic` over a fixture tree returns one report per file containing a todo, empty otherwise. - `textDocument/diagnostic` includes todo diagnostics alongside existing R15/R18 ones. - Workspace symbol search finds a todo by its text content in a nested file. - Watcher registration includes both `.streamd.toml` and `**/*.md` patterns. 9. **Docs** — update `README.md` / `REQUIREMENTS.md` per CLAUDE.md convention: describe the new pull-diagnostics/workspace-symbol todo surfacing under the LSP section. ### Acceptance criteria mapping - "Opening the project shows all todos... without opening the files" → steps 4–6. - "Editing/removing a todo updates diagnostics within ~1s" → step 6 (watcher + refresh), step 4 recompute is fast enough without caching for typical vaults. - "Todos are findable via workspace symbol search" → step 7 + step 1 (recursive). ### Out of scope (per ticket) Code lens per-file todo counts, `textDocument/documentSymbol` todo 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).
Author
Owner

Implementation complete

PR: #130 (branch 129_lsp-workspace-todo-diagnostics)

Time: ~25 minutes of active implementation (excluding the earlier /refine planning 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 @Task shards across the whole workspace as Information-severity diagnostics — this is now effectively the global todo list in Zed's diagnostics panel, not just what's open in a buffer.
  • workspace/symbol additionally 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.
  • A **/*.md file watcher (new, alongside the existing .streamd.toml one) invalidates stale cache entries and triggers workspace/diagnostic/refresh; didSave does 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, and textDocument/rename were 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.
  • Confirmed 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.
  • Deliberately did not send workspace/diagnostic/refresh on every didChange keystroke (only on didSave and 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.
  • No 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.
  • Manual verification in the actual Zed editor was not performed in this session (no interactive Zed environment available) — the PR test plan flags this as an open item for the reviewer/reporter.

Version bumped 0.2.7 → 0.3.0 per request, so merging this PR triggers a new release.

## Implementation complete PR: #130 (branch `129_lsp-workspace-todo-diagnostics`) **Time:** ~25 minutes of active implementation (excluding the earlier `/refine` planning 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 `@Task` shards across the whole workspace as `Information`-severity diagnostics — this is now effectively the global todo list in Zed's diagnostics panel, not just what's open in a buffer. - `workspace/symbol` additionally 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. - A `**/*.md` file watcher (new, alongside the existing `.streamd.toml` one) invalidates stale cache entries and triggers `workspace/diagnostic/refresh`; `didSave` does 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`, and `textDocument/rename` were 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. - Confirmed `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. - Deliberately did **not** send `workspace/diagnostic/refresh` on every `didChange` keystroke (only on `didSave` and 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. - No `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. - Manual verification in the actual Zed editor was **not** performed in this session (no interactive Zed environment available) — the PR test plan flags this as an open item for the reviewer/reporter. Version bumped 0.2.7 → 0.3.0 per request, so merging this PR triggers a new release.
Sign in to join this conversation.
No labels
planned
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
kfickel/streamd#129
No description provided.