feat: add streamd lsp subcommand with LSP server #89

Closed
opened 2026-04-13 20:10:32 +02:00 by kfickel · 3 comments
Owner

Summary

Add a streamd lsp subcommand that starts a Language Server Protocol server for streamd repositories. The LSP server re-uses the existing parsing pipeline (extract::parserlocalize::shard) and .streamd.toml config 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, per R2/R3), tags after content begins.
  • Conditional marker suggestions: If @Task is already on the line, suggest @Done / @Waiting based on if_with relationships in the config (R11).
  • Temporal marker snippets: When the user types @ followed by a digit, offer @YYYYMMDD and @HHMMSS format snippets (R16).

2. Diagnostics — file name format

Validate the open file's name against the pattern from R15:

YYYYMMDD-HHMMSS[_file_type] [markers].md

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:

  • A day whose last entry is not @Break (error)
  • Overlapping timecards (R18c)
  • Work outside all configured periods (R18c)

4. Document symbols

Run the shard parser on save/open and expose the resulting Shard tree as LSP DocumentSymbol nodes. 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 @Done immediately after @Task (same logic as streamd todo N done, R21).

6. Workspace / cross-file features

  • Workspace symbols: workspace/symbol request returns all shards across all .md files in the base folder that match the query (by marker or heading text).
  • References: textDocument/references on an @Marker returns all positions across the workspace where that marker appears.
  • Rename: textDocument/rename on an @Marker renames it across all files in the base folder.

Implementation plan

Step 1 — Add streamd lsp CLI subcommand

Extend the Clap CLI in src/cli/ with an lsp subcommand (no arguments; communicates over stdin/stdout, standard LSP transport). Wire it up in main.rs alongside the existing commands.

Dependency: add tower-lsp (or lsp-server + lsp-types) to Cargo.toml.

Step 2 — LSP backend struct

Create src/cli/lsp.rs. Implement a Backend struct that holds:

  • The loaded StreamdConfig (read from .streamd.toml in the base folder)
  • A DashMap<Url, Vec<LocalizedShard>> cache of per-file parsed shard trees
  • A file watcher or re-parse on didOpen/didChange/didSave

Step 3 — Completion provider

Implement textDocument/completion:

  1. Determine cursor position relative to line content.
  2. Apply R2/R3 marker-boundary logic to decide marker vs. tag context.
  3. Return CompletionItem list from config.markers keys, filtered by context.
  4. For conditional completions, inspect the current line for existing markers and cross-reference if_with sets.

Step 4 — Diagnostic provider

Implement textDocument/publishDiagnostics on file open/save:

  • File name: check filename via the R15 regex; emit diagnostic if invalid.
  • Timesheet: run the existing timesheet state machine over the file's LocalizedShard tree; map each violation to an LSP Diagnostic with the shard's start_line.

Step 5 — Document symbols

Implement textDocument/documentSymbol. Walk the cached Shard tree recursively, converting each node to a DocumentSymbol (kind: String for headings, Array for list items). Use start_line/end_line for ranges.

Step 6 — Code actions

Implement textDocument/codeAction. For each @Task on the requested range's line, offer a WorkspaceEdit that inserts @Done immediately after @Task (reuse the string-manipulation logic from todo done).

Step 7 — Workspace / cross-file

Implement:

  • workspace/symbol — iterate all .md files 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, emit WorkspaceEdit replacing each occurrence.

Step 8 — Tests & documentation

  • Unit-test the completion context logic (marker vs. tag boundary detection).
  • Integration-test diagnostics with fixture markdown files.
  • Add streamd lsp to R20 in REQUIREMENTS.md and document usage in README.md.

Notes

  • The LSP server communicates over stdin/stdout (standard for editor integrations via neovim, VS Code lspconfig, etc.).
  • The base folder is resolved the same way as all other commands: STREAMD_BASE_FOLDER env var → ~/.config/streamd/config.toml (R22/R23).
  • .streamd.toml should be watched for changes and the config reloaded without restarting the server.
## Summary Add a `streamd lsp` subcommand 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.toml` config 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, per `R2`/`R3`), tags after content begins. - **Conditional marker suggestions**: If `@Task` is already on the line, suggest `@Done` / `@Waiting` based on `if_with` relationships in the config (`R11`). - **Temporal marker snippets**: When the user types `@` followed by a digit, offer `@YYYYMMDD` and `@HHMMSS` format snippets (`R16`). ### 2. Diagnostics — file name format Validate the open file's name against the pattern from `R15`: ``` YYYYMMDD-HHMMSS[_file_type] [markers].md ``` 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: - A day whose last entry is not `@Break` (error) - Overlapping timecards (`R18c`) - Work outside all configured periods (`R18c`) ### 4. Document symbols Run the shard parser on save/open and expose the resulting `Shard` tree as LSP `DocumentSymbol` nodes. 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 ` @Done` immediately after `@Task` (same logic as `streamd todo N done`, `R21`). ### 6. Workspace / cross-file features - **Workspace symbols**: `workspace/symbol` request returns all shards across all `.md` files in the base folder that match the query (by marker or heading text). - **References**: `textDocument/references` on an `@Marker` returns all positions across the workspace where that marker appears. - **Rename**: `textDocument/rename` on an `@Marker` renames it across all files in the base folder. --- ## Implementation plan ### Step 1 — Add `streamd lsp` CLI subcommand Extend the Clap CLI in `src/cli/` with an `lsp` subcommand (no arguments; communicates over stdin/stdout, standard LSP transport). Wire it up in `main.rs` alongside the existing commands. **Dependency:** add [`tower-lsp`](https://crates.io/crates/tower-lsp) (or `lsp-server` + `lsp-types`) to `Cargo.toml`. ### Step 2 — LSP backend struct Create `src/cli/lsp.rs`. Implement a `Backend` struct that holds: - The loaded `StreamdConfig` (read from `.streamd.toml` in the base folder) - A `DashMap<Url, Vec<LocalizedShard>>` cache of per-file parsed shard trees - A file watcher or re-parse on `didOpen`/`didChange`/`didSave` ### Step 3 — Completion provider Implement `textDocument/completion`: 1. Determine cursor position relative to line content. 2. Apply `R2`/`R3` marker-boundary logic to decide marker vs. tag context. 3. Return `CompletionItem` list from `config.markers` keys, filtered by context. 4. For conditional completions, inspect the current line for existing markers and cross-reference `if_with` sets. ### Step 4 — Diagnostic provider Implement `textDocument/publishDiagnostics` on file open/save: - **File name**: check filename via the `R15` regex; emit diagnostic if invalid. - **Timesheet**: run the existing timesheet state machine over the file's `LocalizedShard` tree; map each violation to an LSP `Diagnostic` with the shard's `start_line`. ### Step 5 — Document symbols Implement `textDocument/documentSymbol`. Walk the cached `Shard` tree recursively, converting each node to a `DocumentSymbol` (kind: `String` for headings, `Array` for list items). Use `start_line`/`end_line` for ranges. ### Step 6 — Code actions Implement `textDocument/codeAction`. For each `@Task` on the requested range's line, offer a `WorkspaceEdit` that inserts ` @Done` immediately after `@Task` (reuse the string-manipulation logic from `todo done`). ### Step 7 — Workspace / cross-file Implement: - `workspace/symbol` — iterate all `.md` files 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, emit `WorkspaceEdit` replacing each occurrence. ### Step 8 — Tests & documentation - Unit-test the completion context logic (marker vs. tag boundary detection). - Integration-test diagnostics with fixture markdown files. - Add `streamd lsp` to `R20` in `REQUIREMENTS.md` and document usage in `README.md`. --- ## Notes - The LSP server communicates over **stdin/stdout** (standard for editor integrations via `neovim`, VS Code `lspconfig`, etc.). - The base folder is resolved the same way as all other commands: `STREAMD_BASE_FOLDER` env var → `~/.config/streamd/config.toml` (`R22`/`R23`). - `.streamd.toml` should be watched for changes and the config reloaded without restarting the server.
Author
Owner

Zed editor integration notes

Once implemented, the server can be wired into Zed via ~/.config/zed/settings.json:

{
  "languages": {
    "Markdown": {
      "language_servers": ["streamd-lsp", "..."]
    }
  },
  "lsp": {
    "streamd-lsp": {
      "binary": {
        "path": "streamd",
        "arguments": ["lsp"]
      }
    }
  }
}

The "..." keeps Zed's default Markdown servers (e.g. marksman) active alongside the streamd one.

Scope — only attach when .streamd.toml is present

The server should only activate features for a workspace if a .streamd.toml file exists in the workspace root directory. If no .streamd.toml is 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 lsp mode

In LSP mode the base folder must be resolved from the workspace root supplied in the initialize request (initializeParams.rootUri / rootPath), rather than from the global config (~/.config/streamd/config.toml / STREAMD_BASE_FOLDER). The .streamd.toml at that root is then the authoritative configuration for markers, dimensions, timesheet periods, and timezone.

This means the lsp subcommand effectively bypasses the R22/R23 config resolution and treats the workspace root as the base folder directly.

## Zed editor integration notes Once implemented, the server can be wired into [Zed](https://zed.dev) via `~/.config/zed/settings.json`: ```json { "languages": { "Markdown": { "language_servers": ["streamd-lsp", "..."] } }, "lsp": { "streamd-lsp": { "binary": { "path": "streamd", "arguments": ["lsp"] } } } } ``` The `"..."` keeps Zed's default Markdown servers (e.g. `marksman`) active alongside the streamd one. ### Scope — only attach when `.streamd.toml` is present The server should only activate features for a workspace if a `.streamd.toml` file exists **in the workspace root directory**. If no `.streamd.toml` is 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 `lsp` mode In LSP mode the base folder must be resolved from the **workspace root** supplied in the `initialize` request (`initializeParams.rootUri` / `rootPath`), rather than from the global config (`~/.config/streamd/config.toml` / `STREAMD_BASE_FOLDER`). The `.streamd.toml` at that root is then the authoritative configuration for markers, dimensions, timesheet periods, and timezone. This means the `lsp` subcommand effectively bypasses the `R22`/`R23` config resolution and treats the workspace root as the base folder directly.
Author
Owner

Refined Implementation Plan

This plan incorporates the clarifications from the Zed integration notes (workspace-root resolution, passive mode when .streamd.toml is absent).


Step 1 — Add tower-lsp dependency and CLI subcommand

Add to Cargo.toml:

tower-lsp = "0.20"
tokio = { version = "1", features = ["full"] }

Extend src/cli/args.rs with a new Lsp variant in Commands:

/// Start LSP server (communicates over stdin/stdout)
Lsp,

Wire it up in src/main.rs:

Some(Commands::Lsp) => streamd::cli::commands::lsp::run()?,

The lsp subcommand 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 Backend struct implementing tower_lsp::LanguageServer:

struct Backend {
    client: Client,
    // Set after initialize; None = passive mode (no .streamd.toml found)
    state: RwLock<Option<LspState>>,
}

struct LspState {
    config: RepositoryConfiguration,
    tz: Tz,
    base_folder: PathBuf,
    // Per-file cache of parsed shard trees
    file_cache: DashMap<Url, Vec<LocalizedShard>>,
}

Workspace root resolution (important deviation from normal commands):

  • In initialize, read params.root_uri (fallback: params.root_path) to determine the workspace root.
  • Check for <workspace_root>/.streamd.toml. If absent, leave state as None (passive mode) and respond to all requests with empty/no-op results. Log a trace message via client.log_message.
  • If present, parse .streamd.toml into RepositoryConfiguration and store in LspState. Do not use Settings::load() or STREAMD_BASE_FOLDER; the LSP workspace root is the base folder directly.

Step 3 — Config file watching

On initialized (after initialize handshake), register a workspace/didChangeWatchedFiles watcher for .streamd.toml in 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 diagnostics
  • did_change → update cache for incremental edits (full-document sync is sufficient for now), publish diagnostics
  • did_save → re-parse from disk, publish diagnostics

Helper parse_and_cache(uri, text, state):

  1. Call parse_markdown_file(&path_str, text) from extract::parser.
  2. Call localize_stream_file(&shard, &state.config, state.tz).
  3. Store result in state.file_cache.
  4. Return the LocalizedShard tree for diagnostics.

Step 5 — Completion provider (textDocument/completion)

Apply R2/R3 marker-boundary logic:

  1. Fetch the line text at the cursor position.
  2. Determine if the cursor is in marker context (position is before the first non-whitespace, non-marker character — i.e., line starts with optional whitespace followed only by @Marker tokens so far) or tag context (content has begun).
  3. If the cursor is immediately after @ (trigger character), return CompletionItems sourced from state.config.markers.keys().
  4. Conditional completions: scan the current line for already-present marker names. For each present marker, look up its placements' if_with sets. Any marker that appears in an if_with set alongside an already-present marker is offered as a conditional suggestion (e.g. @Done/@Waiting when @Task is present).
  5. Temporal snippets: if the typed trigger is @ followed by a digit, offer @YYYYMMDD and @HHMMSS as snippet completion items (per R16).

Declare trigger_characters: Some(vec!["@".to_string()]) in ServerCapabilities.


Step 6 — Diagnostic provider

Publish diagnostics on every parse_and_cache call:

6a. File-name format (R15):

  • Extract the file name from the URI.
  • Match against ^(\d{8})-(\d{6})(_[^. ]+)?( [^.]+)?\.md$.
  • If it does not match, emit one Diagnostic at range (0,0)-(0,0), severity Warning, message "File name does not match streamd format YYYYMMDD-HHMMSS[_type] [markers].md".

6b. Timesheet violations (R18):

  • Run the existing timesheet state machine (timesheet::generator) over the file's LocalizedShard tree.
  • Map each violation (unclosed day, overlap, work outside periods per R18c) to an LSP Diagnostic using the shard's start_line/end_line for the range.
  • Severity: Error for unclosed day; Warning for overlaps and out-of-period work.

If the backend is in passive mode (state is None), publish an empty diagnostics list.


Step 7 — Document symbols (textDocument/documentSymbol)

Walk the cached LocalizedShard tree (after parse_and_cache) recursively:

  • Each LocalizedShardDocumentSymbol with:
    • name: the shard's heading text or first marker name
    • kind: SymbolKind::STRING for heading shards, SymbolKind::ARRAY for list-item shards
    • range / selection_range: from start_line/end_line
    • children: recursive

Step 8 — Code action: "Mark task as done" (textDocument/codeAction)

For each line in the requested range:

  1. Check if the line contains @Task (and not already @Done).
  2. If yes, offer a CodeAction titled "Mark task as done" with kind quickfix.
  3. The action's WorkspaceEdit inserts @Done immediately after the first @Task on that line — identical logic to run_done in todo.rs (reuse line.replacen("@Task", "@Task @Done", 1)).

Step 9 — Workspace / cross-file features

9a. Workspace symbols (workspace/symbol):

  • Iterate all .md files in state.base_folder (depth 1, same as load_markdown_shards).
  • For each, use the cached shard tree (parse on demand if not cached).
  • Filter shards whose heading or marker names contain the query string.
  • Return as WorkspaceSymbol list.

9b. References (textDocument/references):

  • Identify the @MarkerName under the cursor via simple word-boundary extraction.
  • Scan all cached shard trees for occurrences of that marker name; return their Locations.

9c. Rename (textDocument/rename):

  • Same scan as references.
  • Build a WorkspaceEdit map: for each file, list TextEdits replacing each occurrence of @OldName with @NewName.

Step 10 — run() entry point

pub fn run() -> Result<(), StreamdError> {
    let rt = tokio::runtime::Runtime::new()?;
    rt.block_on(async {
        let stdin = tokio::io::stdin();
        let stdout = tokio::io::stdout();
        let (service, socket) = LspService::new(|client| Backend {
            client,
            state: RwLock::new(None),
        });
        Server::new(stdin, stdout, socket).serve(service).await;
    });
    Ok(())
}

Step 11 — Tests and documentation

  • Unit tests in src/cli/commands/lsp.rs:
    • Completion context detection: marker vs. tag boundary (given line text + cursor column, assert context type).
    • Conditional completion filtering (given markers on line, assert which completions are offered).
    • File-name diagnostic: valid and invalid name fixtures → assert diagnostics count.
  • Integration tests using fixture .md files + a mock RepositoryConfiguration.
  • Update REQUIREMENTS.md: add R25 (or next available) for the lsp subcommand.
  • Update README.md: add streamd lsp usage section, including the Zed settings.json snippet below.

Zed editor integration

Once implemented, add to ~/.config/zed/settings.json:

{
  "languages": {
    "Markdown": {
      "language_servers": ["streamd-lsp", "..."]
    }
  },
  "lsp": {
    "streamd-lsp": {
      "binary": {
        "path": "streamd",
        "arguments": ["lsp"]
      }
    }
  }
}

The "..." preserves Zed's default Markdown servers (e.g. marksman) alongside the streamd one.


Key decisions / constraints

Decision Rationale
Workspace root from initializeParams.rootUri Bypasses R22/R23 global config; LSP clients always supply a workspace root
Passive mode when no .streamd.toml Prevents interference with unrelated Markdown workspaces
Full-document sync (not incremental) Simplest correct approach given existing parse_markdown_file API
tower-lsp over raw lsp-server Higher-level async abstraction; fewer boilerplate handlers to write
Tokio runtime in run() Keeps main.rs synchronous; LSP is self-contained async island
## Refined Implementation Plan This plan incorporates the clarifications from the Zed integration notes (workspace-root resolution, passive mode when `.streamd.toml` is absent). --- ### Step 1 — Add `tower-lsp` dependency and CLI subcommand Add to `Cargo.toml`: ```toml tower-lsp = "0.20" tokio = { version = "1", features = ["full"] } ``` Extend `src/cli/args.rs` with a new `Lsp` variant in `Commands`: ```rust /// Start LSP server (communicates over stdin/stdout) Lsp, ``` Wire it up in `src/main.rs`: ```rust Some(Commands::Lsp) => streamd::cli::commands::lsp::run()?, ``` The `lsp` subcommand 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 `Backend` struct implementing `tower_lsp::LanguageServer`: ```rust struct Backend { client: Client, // Set after initialize; None = passive mode (no .streamd.toml found) state: RwLock<Option<LspState>>, } struct LspState { config: RepositoryConfiguration, tz: Tz, base_folder: PathBuf, // Per-file cache of parsed shard trees file_cache: DashMap<Url, Vec<LocalizedShard>>, } ``` **Workspace root resolution** (important deviation from normal commands): - In `initialize`, read `params.root_uri` (fallback: `params.root_path`) to determine the workspace root. - Check for `<workspace_root>/.streamd.toml`. If absent, leave `state` as `None` (passive mode) and respond to all requests with empty/no-op results. Log a trace message via `client.log_message`. - If present, parse `.streamd.toml` into `RepositoryConfiguration` and store in `LspState`. Do **not** use `Settings::load()` or `STREAMD_BASE_FOLDER`; the LSP workspace root is the base folder directly. --- ### Step 3 — Config file watching On `initialized` (after `initialize` handshake), register a `workspace/didChangeWatchedFiles` watcher for `.streamd.toml` in 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 diagnostics - `did_change` → update cache for incremental edits (full-document sync is sufficient for now), publish diagnostics - `did_save` → re-parse from disk, publish diagnostics Helper `parse_and_cache(uri, text, state)`: 1. Call `parse_markdown_file(&path_str, text)` from `extract::parser`. 2. Call `localize_stream_file(&shard, &state.config, state.tz)`. 3. Store result in `state.file_cache`. 4. Return the `LocalizedShard` tree for diagnostics. --- ### Step 5 — Completion provider (`textDocument/completion`) Apply `R2`/`R3` marker-boundary logic: 1. Fetch the line text at the cursor position. 2. Determine if the cursor is in *marker context* (position is before the first non-whitespace, non-marker character — i.e., line starts with optional whitespace followed only by `@Marker` tokens so far) or *tag context* (content has begun). 3. If the cursor is immediately after `@` (trigger character), return `CompletionItem`s sourced from `state.config.markers.keys()`. 4. **Conditional completions**: scan the current line for already-present marker names. For each present marker, look up its placements' `if_with` sets. Any marker that appears in an `if_with` set alongside an already-present marker is offered as a conditional suggestion (e.g. `@Done`/`@Waiting` when `@Task` is present). 5. **Temporal snippets**: if the typed trigger is `@` followed by a digit, offer `@YYYYMMDD` and `@HHMMSS` as snippet completion items (per R16). Declare `trigger_characters: Some(vec!["@".to_string()])` in `ServerCapabilities`. --- ### Step 6 — Diagnostic provider Publish diagnostics on every `parse_and_cache` call: **6a. File-name format** (R15): - Extract the file name from the URI. - Match against `^(\d{8})-(\d{6})(_[^. ]+)?( [^.]+)?\.md$`. - If it does not match, emit one `Diagnostic` at range `(0,0)-(0,0)`, severity Warning, message `"File name does not match streamd format YYYYMMDD-HHMMSS[_type] [markers].md"`. **6b. Timesheet violations** (R18): - Run the existing timesheet state machine (`timesheet::generator`) over the file's `LocalizedShard` tree. - Map each violation (unclosed day, overlap, work outside periods per R18c) to an LSP `Diagnostic` using the shard's `start_line`/`end_line` for the range. - Severity: Error for unclosed day; Warning for overlaps and out-of-period work. If the backend is in passive mode (`state` is `None`), publish an empty diagnostics list. --- ### Step 7 — Document symbols (`textDocument/documentSymbol`) Walk the cached `LocalizedShard` tree (after `parse_and_cache`) recursively: - Each `LocalizedShard` → `DocumentSymbol` with: - `name`: the shard's heading text or first marker name - `kind`: `SymbolKind::STRING` for heading shards, `SymbolKind::ARRAY` for list-item shards - `range` / `selection_range`: from `start_line`/`end_line` - `children`: recursive --- ### Step 8 — Code action: "Mark task as done" (`textDocument/codeAction`) For each line in the requested range: 1. Check if the line contains `@Task` (and not already `@Done`). 2. If yes, offer a `CodeAction` titled `"Mark task as done"` with kind `quickfix`. 3. The action's `WorkspaceEdit` inserts ` @Done` immediately after the first `@Task` on that line — identical logic to `run_done` in `todo.rs` (reuse `line.replacen("@Task", "@Task @Done", 1)`). --- ### Step 9 — Workspace / cross-file features **9a. Workspace symbols** (`workspace/symbol`): - Iterate all `.md` files in `state.base_folder` (depth 1, same as `load_markdown_shards`). - For each, use the cached shard tree (parse on demand if not cached). - Filter shards whose heading or marker names contain the query string. - Return as `WorkspaceSymbol` list. **9b. References** (`textDocument/references`): - Identify the `@MarkerName` under the cursor via simple word-boundary extraction. - Scan all cached shard trees for occurrences of that marker name; return their `Location`s. **9c. Rename** (`textDocument/rename`): - Same scan as references. - Build a `WorkspaceEdit` map: for each file, list `TextEdit`s replacing each occurrence of `@OldName` with `@NewName`. --- ### Step 10 — `run()` entry point ```rust pub fn run() -> Result<(), StreamdError> { let rt = tokio::runtime::Runtime::new()?; rt.block_on(async { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); let (service, socket) = LspService::new(|client| Backend { client, state: RwLock::new(None), }); Server::new(stdin, stdout, socket).serve(service).await; }); Ok(()) } ``` --- ### Step 11 — Tests and documentation - **Unit tests** in `src/cli/commands/lsp.rs`: - Completion context detection: marker vs. tag boundary (given line text + cursor column, assert context type). - Conditional completion filtering (given markers on line, assert which completions are offered). - File-name diagnostic: valid and invalid name fixtures → assert diagnostics count. - **Integration tests** using fixture `.md` files + a mock `RepositoryConfiguration`. - Update `REQUIREMENTS.md`: add R25 (or next available) for the `lsp` subcommand. - Update `README.md`: add `streamd lsp` usage section, including the Zed `settings.json` snippet below. --- ### Zed editor integration Once implemented, add to `~/.config/zed/settings.json`: ```json { "languages": { "Markdown": { "language_servers": ["streamd-lsp", "..."] } }, "lsp": { "streamd-lsp": { "binary": { "path": "streamd", "arguments": ["lsp"] } } } } ``` The `"..."` preserves Zed's default Markdown servers (e.g. `marksman`) alongside the streamd one. --- ### Key decisions / constraints | Decision | Rationale | |---|---| | Workspace root from `initializeParams.rootUri` | Bypasses `R22`/`R23` global config; LSP clients always supply a workspace root | | Passive mode when no `.streamd.toml` | Prevents interference with unrelated Markdown workspaces | | Full-document sync (not incremental) | Simplest correct approach given existing `parse_markdown_file` API | | `tower-lsp` over raw `lsp-server` | Higher-level async abstraction; fewer boilerplate handlers to write | | Tokio runtime in `run()` | Keeps `main.rs` synchronous; LSP is self-contained async island |
kfickel added the
planned
label 2026-04-13 21:05:04 +02:00
Author
Owner

Implementation 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.20 names the workspace/symbol handler symbol() (not workspace_symbol()), which only appeared at compile time.
  • The extract_marker_at_position helper needs the character at the cursor position to be included (LSP character is exclusive of the current char), caught by a failing TDD test.
  • Clippy (-D warnings in nix flake check) required replacing map_or(false, ...) with is_none_or(... !=) and avoiding explicit auto-derefs on Lazy<T> statics.
  • The RepositoryConfiguration used for LSP is the merge of BasicTimesheetConfiguration and TaskConfiguration, giving completions for both timesheet and task markers out of the box.

Approximate effort: ~1 hour of agent time, ~35k tokens used.

## Implementation 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.20` names the workspace/symbol handler `symbol()` (not `workspace_symbol()`), which only appeared at compile time. - The `extract_marker_at_position` helper needs the character *at* the cursor position to be included (LSP `character` is exclusive of the current char), caught by a failing TDD test. - Clippy (`-D warnings` in nix flake check) required replacing `map_or(false, ...)` with `is_none_or(... !=)` and avoiding explicit auto-derefs on `Lazy<T>` statics. - The `RepositoryConfiguration` used for LSP is the merge of `BasicTimesheetConfiguration` and `TaskConfiguration`, giving completions for both timesheet and task markers out of the box. **Approximate effort:** ~1 hour of agent time, ~35k tokens used.
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#89
No description provided.