Compare commits
2 commits
036d57176f
...
50cdc14c06
| Author | SHA1 | Date | |
|---|---|---|---|
| 50cdc14c06 | |||
| 0444574ea1 |
2 changed files with 50 additions and 25 deletions
19
README.md
19
README.md
|
|
@ -155,6 +155,8 @@ The `"..."` keeps Zed's default Markdown servers (e.g. `marksman`) active alongs
|
||||||
|
|
||||||
### Neovim (nvim-lspconfig)
|
### Neovim (nvim-lspconfig)
|
||||||
|
|
||||||
|
**1. Register the server** — add to your Neovim config (e.g. `~/.config/nvim/init.lua` or a plugin file):
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local lspconfig = require('lspconfig')
|
local lspconfig = require('lspconfig')
|
||||||
local configs = require('lspconfig.configs')
|
local configs = require('lspconfig.configs')
|
||||||
|
|
@ -173,6 +175,23 @@ end
|
||||||
lspconfig.streamd.setup {}
|
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 | `<C-x><C-o>` (insert mode) | Or via your completion plugin (`nvim-cmp`, `blink.cmp`, …) |
|
||||||
|
| Show diagnostics for current line | `<C-w>d` / `gl` | File-name format warnings, timesheet errors |
|
||||||
|
| Jump to next / previous diagnostic | `]d` / `[d` | Navigate between warnings |
|
||||||
|
| Code actions (mark task as done) | `<leader>ca` (Neovim ≥ 0.10) | Place cursor on a line with `@Task` |
|
||||||
|
| Rename marker across all files | `<leader>cr` / `grn` | Renames the `@Marker` under the cursor everywhere |
|
||||||
|
| Find all references to a marker | `grr` / `<leader>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`, `<C-w>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)
|
### VS Code (tasks.json / manual)
|
||||||
|
|
||||||
Use any extension that lets you configure custom LSP servers, pointing `cmd` to `streamd lsp`.
|
Use any extension that lets you configure custom LSP servers, pointing `cmd` to `streamd lsp`.
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ pub fn completions_for_line(
|
||||||
line: &str,
|
line: &str,
|
||||||
cursor_col: u32,
|
cursor_col: u32,
|
||||||
config: &RepositoryConfiguration,
|
config: &RepositoryConfiguration,
|
||||||
|
tz: Tz,
|
||||||
) -> Vec<CompletionItem> {
|
) -> Vec<CompletionItem> {
|
||||||
let col = (cursor_col as usize).min(line.len());
|
let col = (cursor_col as usize).min(line.len());
|
||||||
let line_prefix = &line[..col];
|
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.
|
// 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()) {
|
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 {
|
items.push(CompletionItem {
|
||||||
label: "@YYYYMMDD".to_string(),
|
label: format!("@{}", date_str),
|
||||||
kind: Some(CompletionItemKind::SNIPPET),
|
kind: Some(CompletionItemKind::VALUE),
|
||||||
detail: Some("Date marker (R16)".to_string()),
|
detail: Some("Current date (R16)".to_string()),
|
||||||
insert_text: Some("${1:YYYYMMDD}".to_string()),
|
insert_text: Some(date_str),
|
||||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
|
||||||
sort_text: Some("2_date".to_string()),
|
sort_text: Some("2_date".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
items.push(CompletionItem {
|
items.push(CompletionItem {
|
||||||
label: "@HHMMSS".to_string(),
|
label: format!("@{}", time_str),
|
||||||
kind: Some(CompletionItemKind::SNIPPET),
|
kind: Some(CompletionItemKind::VALUE),
|
||||||
detail: Some("Time marker (R16)".to_string()),
|
detail: Some("Current time (R16)".to_string()),
|
||||||
insert_text: Some("${1:HHMMSS}".to_string()),
|
insert_text: Some(time_str),
|
||||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
|
||||||
sort_text: Some("2_time".to_string()),
|
sort_text: Some("2_time".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
@ -636,6 +638,7 @@ impl LanguageServer for Backend {
|
||||||
line,
|
line,
|
||||||
position.character,
|
position.character,
|
||||||
&state.config,
|
&state.config,
|
||||||
|
state.tz,
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -994,14 +997,14 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_completions_no_at_sign_returns_empty() {
|
fn test_completions_no_at_sign_returns_empty() {
|
||||||
let config = make_config();
|
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());
|
assert!(items.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_completions_at_sign_returns_all_markers() {
|
fn test_completions_at_sign_returns_all_markers() {
|
||||||
let config = make_config();
|
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());
|
assert!(!items.is_empty());
|
||||||
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
|
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
|
||||||
assert!(labels.contains(&"@Task"));
|
assert!(labels.contains(&"@Task"));
|
||||||
|
|
@ -1011,7 +1014,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_completions_prefix_filters() {
|
fn test_completions_prefix_filters() {
|
||||||
let config = make_config();
|
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
|
assert!(items
|
||||||
.iter()
|
.iter()
|
||||||
.all(|i| i.label.starts_with("@Ta") || i.label.starts_with("@ta")));
|
.all(|i| i.label.starts_with("@Ta") || i.label.starts_with("@ta")));
|
||||||
|
|
@ -1022,7 +1025,7 @@ mod tests {
|
||||||
fn test_completions_conditional_sorted_first() {
|
fn test_completions_conditional_sorted_first() {
|
||||||
let config = make_config();
|
let config = make_config();
|
||||||
// @Task is on the line → Done and Waiting should be conditional suggestions
|
// @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 done = items.iter().find(|i| i.label == "@Done");
|
||||||
let waiting = items.iter().find(|i| i.label == "@Waiting");
|
let waiting = items.iter().find(|i| i.label == "@Waiting");
|
||||||
assert!(done.is_some());
|
assert!(done.is_some());
|
||||||
|
|
@ -1040,32 +1043,35 @@ mod tests {
|
||||||
fn test_completions_temporal_snippet_at_sign() {
|
fn test_completions_temporal_snippet_at_sign() {
|
||||||
let config = make_config();
|
let config = make_config();
|
||||||
// Timestamps suggested when prefix is empty (just @)
|
// Timestamps suggested when prefix is empty (just @)
|
||||||
let items = completions_for_line("@", 1, &config);
|
let items = completions_for_line("@", 1, &config, chrono_tz::UTC);
|
||||||
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
|
// Labels contain actual timestamps (8-digit date and 6-digit time)
|
||||||
assert!(labels.contains(&"@YYYYMMDD"));
|
|
||||||
assert!(labels.contains(&"@HHMMSS"));
|
|
||||||
assert!(items
|
assert!(items
|
||||||
.iter()
|
.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]
|
#[test]
|
||||||
fn test_completions_temporal_snippet_digit_after_at() {
|
fn test_completions_temporal_snippet_digit_after_at() {
|
||||||
let config = make_config();
|
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());
|
assert!(!items.is_empty());
|
||||||
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
|
// Should contain VALUE kind timestamp items
|
||||||
assert!(labels.contains(&"@YYYYMMDD"));
|
|
||||||
assert!(labels.contains(&"@HHMMSS"));
|
|
||||||
assert!(items
|
assert!(items
|
||||||
.iter()
|
.iter()
|
||||||
.any(|i| i.kind == Some(CompletionItemKind::SNIPPET)));
|
.any(|i| i.kind == Some(CompletionItemKind::VALUE)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_completions_no_double_at() {
|
fn test_completions_no_double_at() {
|
||||||
let config = make_config();
|
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();
|
let break_item = items.iter().find(|i| i.label == "@Break").unwrap();
|
||||||
assert_eq!(break_item.insert_text.as_deref(), Some("Break"));
|
assert_eq!(break_item.insert_text.as_deref(), Some("Break"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue