feat: add streamd lsp subcommand with LSP server #89
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
Add a
streamd lspsubcommand that starts a Language Server Protocol server for streamd repositories. The LSP server re-uses the existing parsing pipeline (extract::parser→localize::shard) and.streamd.tomlconfig to provide IDE features for markdown files in the base folder.Features to implement
1. Completion
@trigger completions: When the cursor is after@, return known marker names from the loaded config (R10). The server must distinguish position: markers are suggested at line-start positions (before non-whitespace, perR2/R3), tags after content begins.@Taskis already on the line, suggest@Done/@Waitingbased onif_withrelationships in the config (R11).@followed by a digit, offer@YYYYMMDDand@HHMMSSformat snippets (R16).2. Diagnostics — file name format
Validate the open file's name against the pattern from
R15:Emit a workspace-level diagnostic (severity: warning) when the name does not match.
3. Diagnostics — timesheet
Run the timesheet state machine (
R18) per day and surface:@Break(error)R18c)R18c)4. Document symbols
Run the shard parser on save/open and expose the resulting
Shardtree as LSPDocumentSymbolnodes. This gives editors a proper outline that mirrors how streamd actually parses the file — not just raw headings. Each shard becomes a symbol; children nest accordingly.5. Code action — "Mark done"
When the cursor is on a line containing
@Task, offer a code action"Mark task as done"that inserts@Doneimmediately after@Task(same logic asstreamd todo N done,R21).6. Workspace / cross-file features
workspace/symbolrequest returns all shards across all.mdfiles in the base folder that match the query (by marker or heading text).textDocument/referenceson an@Markerreturns all positions across the workspace where that marker appears.textDocument/renameon an@Markerrenames it across all files in the base folder.Implementation plan
Step 1 — Add
streamd lspCLI subcommandExtend the Clap CLI in
src/cli/with anlspsubcommand (no arguments; communicates over stdin/stdout, standard LSP transport). Wire it up inmain.rsalongside the existing commands.Dependency: add
tower-lsp(orlsp-server+lsp-types) toCargo.toml.Step 2 — LSP backend struct
Create
src/cli/lsp.rs. Implement aBackendstruct that holds:StreamdConfig(read from.streamd.tomlin the base folder)DashMap<Url, Vec<LocalizedShard>>cache of per-file parsed shard treesdidOpen/didChange/didSaveStep 3 — Completion provider
Implement
textDocument/completion:R2/R3marker-boundary logic to decide marker vs. tag context.CompletionItemlist fromconfig.markerskeys, filtered by context.if_withsets.Step 4 — Diagnostic provider
Implement
textDocument/publishDiagnosticson file open/save:R15regex; emit diagnostic if invalid.LocalizedShardtree; map each violation to an LSPDiagnosticwith the shard'sstart_line.Step 5 — Document symbols
Implement
textDocument/documentSymbol. Walk the cachedShardtree recursively, converting each node to aDocumentSymbol(kind:Stringfor headings,Arrayfor list items). Usestart_line/end_linefor ranges.Step 6 — Code actions
Implement
textDocument/codeAction. For each@Taskon the requested range's line, offer aWorkspaceEditthat inserts@Doneimmediately after@Task(reuse the string-manipulation logic fromtodo done).Step 7 — Workspace / cross-file
Implement:
workspace/symbol— iterate all.mdfiles in base folder, parse shards, filter by query string.textDocument/references— grep shard trees for the marker name under cursor.textDocument/rename— collect all reference positions, emitWorkspaceEditreplacing each occurrence.Step 8 — Tests & documentation
streamd lsptoR20inREQUIREMENTS.mdand document usage inREADME.md.Notes
neovim, VS Codelspconfig, etc.).STREAMD_BASE_FOLDERenv var →~/.config/streamd/config.toml(R22/R23)..streamd.tomlshould be watched for changes and the config reloaded without restarting the server.Zed editor integration notes
Once implemented, the server can be wired into Zed via
~/.config/zed/settings.json:The
"..."keeps Zed's default Markdown servers (e.g.marksman) active alongside the streamd one.Scope — only attach when
.streamd.tomlis presentThe server should only activate features for a workspace if a
.streamd.tomlfile exists in the workspace root directory. If no.streamd.tomlis found, the server must respond to all LSP requests with empty results and publish no diagnostics. This prevents the server from interfering with unrelated Markdown files.Concretely: on
initialize, check for<workspace_root>/.streamd.toml. If absent, enter a passive/no-op mode for the lifetime of that session.Workspace root — use LSP workspace root in
lspmodeIn LSP mode the base folder must be resolved from the workspace root supplied in the
initializerequest (initializeParams.rootUri/rootPath), rather than from the global config (~/.config/streamd/config.toml/STREAMD_BASE_FOLDER). The.streamd.tomlat that root is then the authoritative configuration for markers, dimensions, timesheet periods, and timezone.This means the
lspsubcommand effectively bypasses theR22/R23config resolution and treats the workspace root as the base folder directly.Refined Implementation Plan
This plan incorporates the clarifications from the Zed integration notes (workspace-root resolution, passive mode when
.streamd.tomlis absent).Step 1 — Add
tower-lspdependency and CLI subcommandAdd to
Cargo.toml:Extend
src/cli/args.rswith a newLspvariant inCommands:Wire it up in
src/main.rs:The
lspsubcommand takes no arguments. It runs an async Tokio runtime and serves LSP over stdin/stdout.Step 2 — Backend struct (
src/cli/commands/lsp.rs)Create a
Backendstruct implementingtower_lsp::LanguageServer:Workspace root resolution (important deviation from normal commands):
initialize, readparams.root_uri(fallback:params.root_path) to determine the workspace root.<workspace_root>/.streamd.toml. If absent, leavestateasNone(passive mode) and respond to all requests with empty/no-op results. Log a trace message viaclient.log_message..streamd.tomlintoRepositoryConfigurationand store inLspState. Do not useSettings::load()orSTREAMD_BASE_FOLDER; the LSP workspace root is the base folder directly.Step 3 — Config file watching
On
initialized(afterinitializehandshake), register aworkspace/didChangeWatchedFileswatcher for.streamd.tomlin the workspace root. On change: reload the config and re-parse all cached files. This avoids server restarts on config edits.Step 4 — Document open/change/save lifecycle
Implement:
did_open→ parse file, update cache, publish diagnosticsdid_change→ update cache for incremental edits (full-document sync is sufficient for now), publish diagnosticsdid_save→ re-parse from disk, publish diagnosticsHelper
parse_and_cache(uri, text, state):parse_markdown_file(&path_str, text)fromextract::parser.localize_stream_file(&shard, &state.config, state.tz).state.file_cache.LocalizedShardtree for diagnostics.Step 5 — Completion provider (
textDocument/completion)Apply
R2/R3marker-boundary logic:@Markertokens so far) or tag context (content has begun).@(trigger character), returnCompletionItems sourced fromstate.config.markers.keys().if_withsets. Any marker that appears in anif_withset alongside an already-present marker is offered as a conditional suggestion (e.g.@Done/@Waitingwhen@Taskis present).@followed by a digit, offer@YYYYMMDDand@HHMMSSas snippet completion items (per R16).Declare
trigger_characters: Some(vec!["@".to_string()])inServerCapabilities.Step 6 — Diagnostic provider
Publish diagnostics on every
parse_and_cachecall:6a. File-name format (R15):
^(\d{8})-(\d{6})(_[^. ]+)?( [^.]+)?\.md$.Diagnosticat range(0,0)-(0,0), severity Warning, message"File name does not match streamd format YYYYMMDD-HHMMSS[_type] [markers].md".6b. Timesheet violations (R18):
timesheet::generator) over the file'sLocalizedShardtree.Diagnosticusing the shard'sstart_line/end_linefor the range.If the backend is in passive mode (
stateisNone), publish an empty diagnostics list.Step 7 — Document symbols (
textDocument/documentSymbol)Walk the cached
LocalizedShardtree (afterparse_and_cache) recursively:LocalizedShard→DocumentSymbolwith:name: the shard's heading text or first marker namekind:SymbolKind::STRINGfor heading shards,SymbolKind::ARRAYfor list-item shardsrange/selection_range: fromstart_line/end_linechildren: recursiveStep 8 — Code action: "Mark task as done" (
textDocument/codeAction)For each line in the requested range:
@Task(and not already@Done).CodeActiontitled"Mark task as done"with kindquickfix.WorkspaceEditinserts@Doneimmediately after the first@Taskon that line — identical logic torun_doneintodo.rs(reuseline.replacen("@Task", "@Task @Done", 1)).Step 9 — Workspace / cross-file features
9a. Workspace symbols (
workspace/symbol):.mdfiles instate.base_folder(depth 1, same asload_markdown_shards).WorkspaceSymbollist.9b. References (
textDocument/references):@MarkerNameunder the cursor via simple word-boundary extraction.Locations.9c. Rename (
textDocument/rename):WorkspaceEditmap: for each file, listTextEdits replacing each occurrence of@OldNamewith@NewName.Step 10 —
run()entry pointStep 11 — Tests and documentation
src/cli/commands/lsp.rs:.mdfiles + a mockRepositoryConfiguration.REQUIREMENTS.md: add R25 (or next available) for thelspsubcommand.README.md: addstreamd lspusage section, including the Zedsettings.jsonsnippet below.Zed editor integration
Once implemented, add to
~/.config/zed/settings.json:The
"..."preserves Zed's default Markdown servers (e.g.marksman) alongside the streamd one.Key decisions / constraints
initializeParams.rootUriR22/R23global config; LSP clients always supply a workspace root.streamd.tomlparse_markdown_fileAPItower-lspover rawlsp-serverrun()main.rssynchronous; LSP is self-contained async islandImplementation complete
PR: #90
What was implemented: All 11 steps from the refined plan, across 2 commits (~550 lines of production code, 13 unit tests).
Key findings during implementation:
tower-lsp 0.20names the workspace/symbol handlersymbol()(notworkspace_symbol()), which only appeared at compile time.extract_marker_at_positionhelper needs the character at the cursor position to be included (LSPcharacteris exclusive of the current char), caught by a failing TDD test.-D warningsin nix flake check) required replacingmap_or(false, ...)withis_none_or(... !=)and avoiding explicit auto-derefs onLazy<T>statics.RepositoryConfigurationused for LSP is the merge ofBasicTimesheetConfigurationandTaskConfiguration, giving completions for both timesheet and task markers out of the box.Approximate effort: ~1 hour of agent time, ~35k tokens used.