From 0444574ea13ac11609b430da874c087360613199 Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sun, 19 Apr 2026 20:54:11 +0200 Subject: [PATCH 1/2] docs: expand Neovim LSP setup with step-by-step guide and keymaps --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index a04e0ce..ccc00c5 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,8 @@ The `"..."` keeps Zed's default Markdown servers (e.g. `marksman`) active alongs ### Neovim (nvim-lspconfig) +**1. Register the server** — add to your Neovim config (e.g. `~/.config/nvim/init.lua` or a plugin file): + ```lua local lspconfig = require('lspconfig') local configs = require('lspconfig.configs') @@ -173,6 +175,23 @@ end lspconfig.streamd.setup {} ``` +The server activates automatically when Neovim opens a Markdown file inside a directory that contains a `.streamd.toml` file. + +**2. Using LSP features** — standard Neovim LSP keymaps apply (`:help lsp`): + +| Action | Default keymap | Notes | +|---|---|---| +| Trigger `@` completions | `` (insert mode) | Or via your completion plugin (`nvim-cmp`, `blink.cmp`, …) | +| Show diagnostics for current line | `d` / `gl` | File-name format warnings, timesheet errors | +| Jump to next / previous diagnostic | `]d` / `[d` | Navigate between warnings | +| Code actions (mark task as done) | `ca` (Neovim ≥ 0.10) | Place cursor on a line with `@Task` | +| Rename marker across all files | `cr` / `grn` | Renames the `@Marker` under the cursor everywhere | +| Find all references to a marker | `grr` / `fr` | Lists every occurrence of `@Marker` across the workspace | +| Document outline (shard tree) | `:lua vim.lsp.buf.document_symbol()` | Or via Telescope: `:Telescope lsp_document_symbols` | +| Workspace symbol search | `:lua vim.lsp.buf.workspace_symbol()` | Or via Telescope: `:Telescope lsp_workspace_symbols` | + +> **Note:** default keymaps (`grn`, `grr`, `d`, `]d`/`[d`) are available from Neovim 0.10+. On older versions use `:lua vim.lsp.buf.*` commands or set up keymaps manually in your `on_attach` callback. + ### VS Code (tasks.json / manual) Use any extension that lets you configure custom LSP servers, pointing `cmd` to `streamd lsp`. From 50cdc14c0694dd3378f7cd2f9d4d35099582640f Mon Sep 17 00:00:00 2001 From: Konstantin Fickel Date: Sun, 19 Apr 2026 20:56:34 +0200 Subject: [PATCH 2/2] fix: use actual timestamps for completion --- src/cli/commands/lsp.rs | 56 +++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/cli/commands/lsp.rs b/src/cli/commands/lsp.rs index 2c6a5cf..8a7b4ff 100644 --- a/src/cli/commands/lsp.rs +++ b/src/cli/commands/lsp.rs @@ -101,6 +101,7 @@ pub fn completions_for_line( line: &str, cursor_col: u32, config: &RepositoryConfiguration, + tz: Tz, ) -> Vec { let col = (cursor_col as usize).min(line.len()); let line_prefix = &line[..col]; @@ -158,21 +159,22 @@ pub fn completions_for_line( // Offer date/time snippets when prefix is empty or starts with a digit. if prefix.is_empty() || prefix.starts_with(|c: char| c.is_ascii_digit()) { + let now = Utc::now().with_timezone(&tz); + let date_str = now.format("%Y%m%d").to_string(); + let time_str = now.format("%H%M%S").to_string(); items.push(CompletionItem { - label: "@YYYYMMDD".to_string(), - kind: Some(CompletionItemKind::SNIPPET), - detail: Some("Date marker (R16)".to_string()), - insert_text: Some("${1:YYYYMMDD}".to_string()), - insert_text_format: Some(InsertTextFormat::SNIPPET), + label: format!("@{}", date_str), + kind: Some(CompletionItemKind::VALUE), + detail: Some("Current date (R16)".to_string()), + insert_text: Some(date_str), sort_text: Some("2_date".to_string()), ..Default::default() }); items.push(CompletionItem { - label: "@HHMMSS".to_string(), - kind: Some(CompletionItemKind::SNIPPET), - detail: Some("Time marker (R16)".to_string()), - insert_text: Some("${1:HHMMSS}".to_string()), - insert_text_format: Some(InsertTextFormat::SNIPPET), + label: format!("@{}", time_str), + kind: Some(CompletionItemKind::VALUE), + detail: Some("Current time (R16)".to_string()), + insert_text: Some(time_str), sort_text: Some("2_time".to_string()), ..Default::default() }); @@ -636,6 +638,7 @@ impl LanguageServer for Backend { line, position.character, &state.config, + state.tz, )))) } @@ -994,14 +997,14 @@ mod tests { #[test] fn test_completions_no_at_sign_returns_empty() { let config = make_config(); - let items = completions_for_line("hello world", 5, &config); + let items = completions_for_line("hello world", 5, &config, chrono_tz::UTC); assert!(items.is_empty()); } #[test] fn test_completions_at_sign_returns_all_markers() { let config = make_config(); - let items = completions_for_line("@", 1, &config); + let items = completions_for_line("@", 1, &config, chrono_tz::UTC); assert!(!items.is_empty()); let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect(); assert!(labels.contains(&"@Task")); @@ -1011,7 +1014,7 @@ mod tests { #[test] fn test_completions_prefix_filters() { let config = make_config(); - let items = completions_for_line("@Ta", 3, &config); + let items = completions_for_line("@Ta", 3, &config, chrono_tz::UTC); assert!(items .iter() .all(|i| i.label.starts_with("@Ta") || i.label.starts_with("@ta"))); @@ -1022,7 +1025,7 @@ mod tests { fn test_completions_conditional_sorted_first() { let config = make_config(); // @Task is on the line → Done and Waiting should be conditional suggestions - let items = completions_for_line("@Task @", 7, &config); + let items = completions_for_line("@Task @", 7, &config, chrono_tz::UTC); let done = items.iter().find(|i| i.label == "@Done"); let waiting = items.iter().find(|i| i.label == "@Waiting"); assert!(done.is_some()); @@ -1040,32 +1043,35 @@ mod tests { fn test_completions_temporal_snippet_at_sign() { let config = make_config(); // Timestamps suggested when prefix is empty (just @) - let items = completions_for_line("@", 1, &config); - let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect(); - assert!(labels.contains(&"@YYYYMMDD")); - assert!(labels.contains(&"@HHMMSS")); + let items = completions_for_line("@", 1, &config, chrono_tz::UTC); + // Labels contain actual timestamps (8-digit date and 6-digit time) assert!(items .iter() - .any(|i| i.kind == Some(CompletionItemKind::SNIPPET))); + .any(|i| i.kind == Some(CompletionItemKind::VALUE) + && i.insert_text.as_deref().map_or(false, |t| t.len() == 8 + && t.chars().all(|c| c.is_ascii_digit())))); + assert!(items + .iter() + .any(|i| i.kind == Some(CompletionItemKind::VALUE) + && i.insert_text.as_deref().map_or(false, |t| t.len() == 6 + && t.chars().all(|c| c.is_ascii_digit())))); } #[test] fn test_completions_temporal_snippet_digit_after_at() { let config = make_config(); - let items = completions_for_line("@2", 2, &config); + let items = completions_for_line("@2", 2, &config, chrono_tz::UTC); assert!(!items.is_empty()); - let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect(); - assert!(labels.contains(&"@YYYYMMDD")); - assert!(labels.contains(&"@HHMMSS")); + // Should contain VALUE kind timestamp items assert!(items .iter() - .any(|i| i.kind == Some(CompletionItemKind::SNIPPET))); + .any(|i| i.kind == Some(CompletionItemKind::VALUE))); } #[test] fn test_completions_no_double_at() { let config = make_config(); - let items = completions_for_line("@Br", 3, &config); + let items = completions_for_line("@Br", 3, &config, chrono_tz::UTC); let break_item = items.iter().find(|i| i.label == "@Break").unwrap(); assert_eq!(break_item.insert_text.as_deref(), Some("Break")); }