From b224620212adae0cd0b135603ac87e38d1da333f Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Mon, 13 Apr 2026 21:42:15 +0200 Subject: [PATCH] fix: resolve clippy warnings in lsp module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace map_or(false, ...) with is_none_or(... !=) - Replace explicit auto-derefs (&*Static) with auto-deref (&Static) - Refactor range-indexed loop to iterator with enumerate + skip/take - Suppress deprecated root_path field with allow(deprecated) docs: update README and REQUIREMENTS for streamd lsp - Add streamd lsp to commands table in README - Add Editor Integration section with Zed, Neovim, VS Code snippets - Add R25 LSP requirements (R25a–R25e) to REQUIREMENTS.md --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++ REQUIREMENTS.md | 51 +++++++++++++++++++++++++++++++ src/cli/commands/lsp.rs | 18 +++++++---- 3 files changed, 129 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index cf40451..a04e0ce 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Within files, `@`-prefixed markers at the beginning of paragraphs or headings de - `streamd todo --show-future` — Include tasks with future dates in the listing - `streamd edit [number]` — Edit a stream file by index (most recent first) - `streamd timesheet` — Generate time reports from `@Timesheet` markers +- `streamd lsp` — Start the LSP server (stdin/stdout transport; see [Editor Integration](#editor-integration) below) ## Configuration @@ -110,3 +111,68 @@ Running `streamd todo` finds all shards marked as open tasks and displays them n You can quickly edit or complete tasks by number: - `streamd todo 1 edit` opens task 1 in your editor at the correct line - `streamd todo 1 done` marks task 1 as done by inserting `@Done` after `@Task` + +## Editor Integration + +`streamd lsp` starts a Language Server Protocol server that provides IDE features for your stream markdown files. The server communicates over **stdin/stdout** and auto-activates only when a `.streamd.toml` file is present in the workspace root. + +### Features + +| Feature | Description | +|---|---| +| `@` completions | Suggests known markers from your config; conditional suggestions (e.g. `@Done` when `@Task` is on the line) | +| Temporal snippets | `@` followed by a digit offers `YYYYMMDD` / `HHMMSS` format snippets | +| Diagnostics | File-name format warnings (R15); timesheet errors (overlapping timecards, unclosed days) | +| Document symbols | Shard tree exposed as outline symbols | +| "Mark task as done" | Quick-fix code action: inserts `@Done` after `@Task` | +| Workspace symbols | Search shards across all `.md` files | +| References | Find all occurrences of an `@Marker` across the workspace | +| Rename | Rename an `@Marker` across all files | + +### Zed + +Add to `~/.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 streamd. + +### Neovim (nvim-lspconfig) + +```lua +local lspconfig = require('lspconfig') +local configs = require('lspconfig.configs') + +if not configs.streamd then + configs.streamd = { + default_config = { + cmd = { 'streamd', 'lsp' }, + filetypes = { 'markdown' }, + root_dir = lspconfig.util.root_pattern('.streamd.toml'), + single_file_support = false, + }, + } +end + +lspconfig.streamd.setup {} +``` + +### VS Code (tasks.json / manual) + +Use any extension that lets you configure custom LSP servers, pointing `cmd` to `streamd lsp`. diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 1c5b8ec..01b555f 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -400,6 +400,7 @@ Provide recursive search through the shard tree: | `streamd edit [n]` | Edit nth file (supports negative indexing for recent files) | | `streamd timesheet` | Generate formatted timesheet report with expected/actual hours | | `streamd completions ` | Generate shell completions (bash, zsh, fish, elvish, powershell) | +| `streamd lsp` | Start Language Server Protocol server over stdin/stdout | ### R21a: Daily Command Behavior @@ -471,3 +472,53 @@ Multiple configurations can be merged: - Dimensions are combined (later configs can add new dimensions) - Markers are combined (later configs can add new markers) - This allows base configuration + domain-specific extensions + +--- + +## LSP Server + +### R25: LSP Subcommand + +`streamd lsp` starts a Language Server Protocol server over stdin/stdout. + +**Workspace root resolution:** +- The base folder is taken from `initializeParams.rootUri` (or `rootPath` as fallback). +- R22/R23 global config resolution is bypassed in LSP mode. + +**Passive mode:** +- If `.streamd.toml` is absent from the workspace root, the server enters passive mode: all requests return empty results and no diagnostics are published. + +**Config watching:** +- The server registers a `workspace/didChangeWatchedFiles` watcher for `.streamd.toml`. +- Config is reloaded without restarting the server when `.streamd.toml` changes. + +**Document sync:** +- Full-document sync (`TextDocumentSyncKind::FULL`). +- Re-parses on `didOpen`, `didChange`, and `didSave`. + +### R25a: LSP Completion + +- Trigger character: `@` +- Returns marker names from the merged config (BasicTimesheetConfiguration + TaskConfiguration). +- Conditional suggestions: if marker A is on the line and A has placements with `if_with: {B}`, B is offered with higher priority. +- Temporal snippets: `@` followed by a digit offers `YYYYMMDD` and `HHMMSS` format snippets (R16). + +### R25b: LSP Diagnostics + +- **File-name format (R15)**: Warning when the file basename does not match `^(\d{8})(?:-(\d{4,6}))?.+\.md$`. +- **Timesheet violations (R18)**: Error when a day ends without a break; Warning for overlapping timecards. + +### R25c: LSP Document Symbols + +- Returns the `LocalizedShard` tree as nested `DocumentSymbol` nodes. +- Symbol names are derived from marker names or tag names. + +### R25d: LSP Code Actions + +- "Mark task as done": offered on any line containing `@Task` without `@Done`; inserts ` @Done` after `@Task`. + +### R25e: LSP Cross-file Features + +- `workspace/symbol`: searches all `.md` files in base folder (depth 1) for shards matching the query. +- `textDocument/references`: finds all occurrences of the `@Marker` under the cursor across the workspace. +- `textDocument/rename`: renames an `@Marker` across all files via `WorkspaceEdit`. diff --git a/src/cli/commands/lsp.rs b/src/cli/commands/lsp.rs index ba3886a..378e7f9 100644 --- a/src/cli/commands/lsp.rs +++ b/src/cli/commands/lsp.rs @@ -418,7 +418,7 @@ impl Backend { .unwrap_or(chrono_tz::UTC); let config = - merge_repository_configuration(&*BasicTimesheetConfiguration, &*TaskConfiguration); + merge_repository_configuration(&BasicTimesheetConfiguration, &TaskConfiguration); Some(Arc::new(LspState { config, @@ -437,6 +437,7 @@ impl Backend { #[tower_lsp::async_trait] impl LanguageServer for Backend { async fn initialize(&self, params: InitializeParams) -> Result { + #[allow(deprecated)] let root: Option = params .root_uri .as_ref() @@ -691,8 +692,13 @@ impl LanguageServer for Backend { let start_line = params.range.start.line as usize; let end_line = params.range.end.line as usize; - for line_idx in start_line..=end_line.min(lines.len().saturating_sub(1)) { - let line = &lines[line_idx]; + let range_end = end_line.min(lines.len().saturating_sub(1)); + for (line_idx, line) in lines + .iter() + .enumerate() + .skip(start_line) + .take(range_end.saturating_sub(start_line) + 1) + { if line.contains("@Task") && !line.contains("@Done") { let new_line = line.replacen("@Task", "@Task @Done", 1); let mut changes: std::collections::HashMap> = @@ -745,7 +751,7 @@ impl LanguageServer for Backend { .filter_map(|e| e.ok()) { let path = entry.path(); - if !path.extension().map_or(false, |e| e == "md") { + if path.extension().is_none_or(|e| e != "md") { continue; } let uri = match Url::from_file_path(path) { @@ -809,7 +815,7 @@ impl LanguageServer for Backend { .filter_map(|e| e.ok()) { let path = entry.path(); - if !path.extension().map_or(false, |e| e == "md") { + if path.extension().is_none_or(|e| e != "md") { continue; } let file_uri = match Url::from_file_path(path) { @@ -893,7 +899,7 @@ impl LanguageServer for Backend { .filter_map(|e| e.ok()) { let path = entry.path(); - if !path.extension().map_or(false, |e| e == "md") { + if path.extension().is_none_or(|e| e != "md") { continue; } let file_uri = match Url::from_file_path(path) {