66964be393e9fe661a9b083d541fb7ff175a04fa
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
66964be393 |
chore(fmt): apply cargo fmt to recent LSP changes
Pure rustfmt churn — the anchor-match, source-relative resolution, and config-unwrap commits introduced lines that needed rustfmt reformatting. CI's `cargo fmt --check` job was blocking on this. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
f477f6e6a7 |
fix(lsp): resolve wikilinks source-relative first, with .. collapsing
`[[llm-wiki-pattern]]` in `tips/index.wiki` should resolve to
`tips/llm-wiki-pattern.wiki` — vimwiki's default is source-relative
for non-absolute targets. The index keyed every page by its
workspace-relative path, so the link looked up `llm-wiki-pattern`
(root-relative) and missed the sibling. Goto-definition then
synthesised a non-existent root URL, and `nuwiki.link` diagnostics
flagged every such link as missing-page.
- `WorkspaceIndex::resolve_wiki_path` and `page_for_wiki_target`
centralise the lookup: try `{source_dir}/{path}` first, fall back
to root-relative. Used by goto-definition, diagnostics, and the
workspace `checkLinks` walker.
- `collapse_dots` resolves `.` / `..` segments before lookup so
`[[../Other]]` from `posts/foo` correctly hits `Other` at the
wiki root (and is reported broken if no such page exists, rather
than silently going to nowhere).
- `canonical_link_name` applies the same rule when keying the
backlinks index, so backlinks queried by the resolved page name
pick up references that were written source-relative.
- `OutgoingLink` carries `is_absolute` now so `classify_outgoing`
(driven from cached index data, not the live AST) gets the same
treatment without re-parsing the source.
- `classify_outgoing`'s File/Local branch now reuses
`resolve_file_path` instead of duplicating the `//absolute` /
relative-to-source logic inline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
859d69e4fe |
fix(lsp): match wikilink anchors against raw heading text
`[[Page#Heading text]]` parsed the anchor as the literal string after `#`, but the index stored each heading's anchor as `slugify(title)`. The two never compared equal, so every TOC-style anchor link (`[[#1. Topologia atual]]`, etc.) was reported as a broken anchor and goto-definition couldn't jump to the right heading either. Slugify the request before comparing — `slugify` is idempotent on valid-slug input, so the same code path now accepts both `[[Page#some-heading]]` (slug form) and `[[Page#Some Heading]]` (raw text). Applied to diagnostics, navigation, and hover preview so all three agree on what "an anchor matches a heading" means. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
2aec51274e |
fix(lsp): resolve wikilinks that include the .wiki file extension
`[[page.wiki]]` should resolve identically to `[[page]]`. The index keyed by stem, so links with the explicit extension fell through to the broken-link diagnostic and to-page navigation failed. Strip the configured extension before every page lookup (resolve, definition, backlinks, classify_link, classify_outgoing) and stop appending the extension in synthesise_page_uri when it's already present. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
d8fa59a63a |
parity(4): diary weekly / monthly / yearly frequency support
vimwiki's `diary_frequency` config has been honoured by the existing
`nuwiki.diary.openToday` / `openYesterday` / `openTomorrow` commands
end-to-end. Setting `diary_frequency = 'weekly'` (or `monthly` /
`yearly`) now creates and addresses entries at that cadence — file
stems follow the canonical formats:
daily YYYY-MM-DD 2026-05-12.wiki
weekly YYYY-Www 2026-W19.wiki (ISO 8601 week)
monthly YYYY-MM 2026-05.wiki
yearly YYYY 2026.wiki
Core additions (nuwiki_core::date):
- DiaryFrequency enum + permissive `parse` (unknown → Daily)
- DiaryPeriod enum unifying Day / Week / Month / Year
- format / parse / next / prev / today_utc + first_day, with
proper ISO-week math (Thursday rule, year-boundary handling)
LSP wiring:
- WikiConfig::frequency() and diary_path_for_period()
- crate::diary::uri_for_period
- Index now records `diary_period` for any of the four flavours;
`diary_date` is preserved (filtered to Day-only) for back-compat
with existing daily-only callers
- `nuwiki.diary.next` / `nuwiki.diary.prev` now navigate at the
*same flavour* as the current entry (or at the wiki's configured
frequency when off a diary page), via new
`crate::diary::next_period` / `prev_period` helpers
- `nuwiki.diary.openToday` returns the period stem + frequency in
its response payload alongside the URI
Tests:
- 12 in crates/nuwiki-core/tests/diary_period.rs covering the date
math (ISO week boundaries, format round-trips, next/prev across
year edges, etc.)
- 9 in crates/nuwiki-lsp/tests/diary_frequency.rs covering
WikiConfig path computation, index recognition for all four
stems, period navigation, and diary-dir filtering
Gates: 450 Rust / 39 Neovim / 12 Vim.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e75ad6ca89 |
phase 16: diary path conventions + nav commands
Hand-rolled date primitive (no chrono/time dep) plus the diary
command surface that closes the `:VimwikiMakeDiaryNote` ergonomics gap.
nuwiki-core/src/date.rs:
- `DiaryDate { year, month, day }` with strict `YYYY-MM-DD` parsing
(rejects missing zero-padding, alternate separators, whitespace,
impossible calendar dates including the leap-year exceptions).
- `next_day` / `prev_day` via Howard Hinnant's days-from-civil
algorithm — proleptic Gregorian, walks across month/year/leap-day
boundaries.
- `today_utc()` from `SystemTime::now()`. UTC-only by design: a
local-tz implementation would need a tz database we don't ship.
- `Ord` by epoch days, `Display`/`format` round-trip with `parse`.
WikiConfig (P15 resolution):
- `diary_rel_path: String` (default `"diary"`) — mirrors vimwiki's
`g:vimwiki_diary_rel_path`.
- `diary_index: String` (default `"diary"`) — the stem inside the
diary subdir.
- `diary_dir()`, `diary_index_path()`, `diary_path_for(date)` —
centralise the path math so commands stay declarative.
- `RawWiki` (init-options wire format) accepts both fields, so users
with `g:vimwiki_diary_rel_path = "journal"` migrate cleanly.
WorkspaceIndex:
- New `diary_rel_path: Option<String>` field set by `Wiki::new`.
- `IndexedPage.diary_date: Option<DiaryDate>` — populated in `upsert`
when the URI is under `<root>/<diary_rel_path>/` and the stem
parses. `diary_date_for_uri` is the standalone classifier.
- The classifier *requires* the URI to be inside the diary subdir
when a root is known; with no root it accepts any file whose stem
is a date (kept for the ad-hoc test/scratch case).
nuwiki-lsp/src/diary.rs:
- `uri_for_date` / `index_uri` — bridge from `DiaryDate` and config
to `file://` URIs.
- `list_entries(index) → Vec<DiaryEntry>` — ascending, plus
`list_entries_filtered(year, month)` for calendar hooks.
- `next_entry` / `prev_entry` — strict less-than/greater-than
comparison so navigating from a non-diary page (uses today as
pivot) still moves to the right entry.
- `build_index_body` — newest-first, grouped under `== YYYY ==` /
`=== Month ===` subheadings, emits `- [[diary/YYYY-MM-DD]]`. Matches
`:VimwikiDiaryGenerateLinks`.
- `DiaryEntry` JSON shape: `{ date: "YYYY-MM-DD", uri }`. Manual
serde impl so DiaryDate stays serde-free in nuwiki-core.
Commands (9 new):
- `nuwiki.diary.openToday` / `openYesterday` / `openTomorrow` →
`{ uri, date }`.
- `nuwiki.diary.openIndex` → `{ uri }`.
- `nuwiki.diary.generateIndex` → WorkspaceEdit. Three paths:
live document in `backend.documents` → full-doc replace; on-disk
index → read + full-doc replace; missing → CreateFile + insert.
- `nuwiki.diary.next` / `prev` → pivots on the current page's
`diary_date`, falls back to today.
- `nuwiki.diary.listEntries` → optional `year` / `month` filter.
- `nuwiki.diary.openForDate` → strict `YYYY-MM-DD` arg.
All commands accept an optional `uri` to scope to a particular wiki
when multi-wiki lands; the dispatcher falls back to `default_wiki()`.
Tests: 35 new in `phase16_diary.rs` (date parser exhaustively,
calendar arithmetic across boundaries + leaps, path conventions,
upsert classification including the "date filename outside diary dir"
no-op case, list/filter/nav, index body grouping, serde shape, command
list completeness). Total 309 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8b29d4e9b9 |
phase 12: tags — lexer, AST, index, anchor resolution, ws/symbol
Vimwiki `:tag1:tag2:` syntax across the full stack. P11 resolved as
strict-vimwiki — no markdown `#tag` alias.
AST (nuwiki-core):
- `TagScope { File, Heading(usize), Standalone }` mirrors vimwiki's
three placement-determined meanings: lines 0-1 → File; one or two
lines below the most recent heading → Heading(idx); otherwise
Standalone.
- `TagNode { span, tags: Vec<String>, scope }` + `BlockNode::Tag`.
- `PageMetadata.tags: Vec<String>` accumulates the file-scope tag
names — convenience projection of the Tag blocks whose scope is
File. New field is additive (Default derive) so existing consumers
using `..Default::default()` keep working.
- `Visitor::visit_tag` lands with a default body and `walk_block`
dispatches the new arm.
Lexer (`syntax/vimwiki/lexer.rs`):
- `VimwikiTokenKind::Tag(Vec<String>)` block-level token.
- `try_lex_tag` runs in the normal-line dispatcher right after heading
recognition; demands the *whole trimmed line* match `:name:name…:`,
with non-empty whitespace-free names between adjacent colons. Empty
segments (`::tag::`), whitespace inside a name, or single-colon
lines are all rejected. Doesn't fight `Term::` (definition-list
marker) because that pattern requires text before the `::`.
- Indentation is tolerated; the span covers the trimmed run.
Parser (`syntax/vimwiki/parser.rs`):
- `ParseState` gains `headings_seen` + `last_heading_line`.
- `parse_heading` bumps both on every closed heading so the tag
parser can detect the heading window.
- `parse_tag` reads the Tag token, computes the scope via
`scope_for_tag(tag_line)`, eats the trailing newline, and emits the
TagNode.
- `parse_document` post-processes each block: when a Tag with
`scope == File` is emitted, its names are pushed (de-duped) into
`metadata.tags`.
Renderer (`render/html.rs`):
- `render_tag` emits `<div class="tags"><span class="tag"
id="tag-name">name</span>...</div>`. The `id` is what makes
`[[Page#tag-name]]` jump to the rendered tag.
Semantic tokens (`semantic_tokens.rs`):
- New `TOKEN_TAG = 19` / `vimwikiTag` so editors highlight tag lines
distinctly. `emit_block` adds the Tag arm.
Workspace index (`index.rs`):
- `IndexedPage.tags: Vec<TagInfo>` records each tag occurrence on a
page with its span + scope.
- `WorkspaceIndex.tags_by_name: HashMap<String, Vec<TagOccurrence>>`
is the reverse map for cross-page anchor resolution + the Phase 13
`nuwiki.tags.search` command.
- `upsert` populates both; `remove` scrubs the reverse map and drops
empty entries; `tags_for(name)` is the public lookup.
LSP handlers (`lib.rs`):
- `heading_range_in` falls back to tag matching when the anchor
isn't a heading slug — so `[[Page#some-tag]]` lands on the matching
TagNode. Heading anchors win over tag anchors when both match (the
vimwiki precedence).
- `workspace/symbol` includes tags alongside headings, named `:tag:`
and kinded `PROPERTY` so editor UIs can distinguish them.
Tests (20 new):
- Syntax (15 in `vimwiki_tags.rs`):
lexer recognition + rejection of `::tag::`, whitespace-in-name,
Term::-conflict; parser scope paths (file, heading-1-below,
heading-2-below, standalone, no-heading, line-1 priority);
metadata dedup; renderer HTML + escaping.
- LSP index (5 in `tags_index_and_lsp.rs`):
per-page tag population, cross-page `tags_by_name` aggregation,
re-upsert replaces stale tags, `remove` cleans the reverse map,
heading-scope tag indexed but not in file metadata.
All 191 v1.0 + Phase 11 tests still pass — backward compatible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4d461d2425 |
phase 8: navigation — definition, refs, hover, completion, ws/symbol
P8 resolved (SPEC §11): lazy + background indexing with progress. Initial scan runs in a tokio task spawned from `initialized`; the server stays responsive immediately and nav features answer with partial data until the index is complete. WorkspaceIndex (`index.rs`): - Per-URI `IndexedPage` keeps name, title, headings, outgoing links. - Reverse maps: `pages_by_name`, `backlinks: HashMap<page, …>`. - `upsert` scrubs stale entries first so re-parses don't leak. - `resolve` handles Wiki + AnchorOnly link kinds against the index; other kinds (Interwiki/Diary/File/Local/Raw) fall through. - `slugify` for anchors (lowercases, collapses runs, drops trailing dashes, preserves Unicode), `page_name_from_uri` for workspace- rooted names, `walk_wiki_files` for the background scanner. Backend wiring (`lib.rs`): - `initialize` captures workspace root from `workspaceFolders` or the deprecated `root_uri`; advertises definition/references/hover/ completion (trigger `[`)/workspace_symbol providers. - `initialized` spawns the indexer, wrapped in `window/workDoneProgress` begin/end notifications. - `did_open`/`did_change` synchronously update the index alongside publishing diagnostics; `did_close` keeps the indexed projection (file may still exist on disk and other pages link to it) and only clears diagnostics + drops the live document state. Position lookup (`nav.rs`): - `lsp_to_byte_pos` translates between LSP encoding (UTF-8 or UTF-16) and our byte columns. - `find_inline_at` descends blocks → inlines → inline containers, returning the most specific inline whose span covers the cursor. Handlers: - definition: wikilinks resolve to a target URI; anchored links return the range at the matching heading. - references: backlinks of the cursor's link target, or of the current page when the cursor isn't on a link. Ranges fall back to byte-column LSP positions when the source doc isn't open. - hover: markdown preview with page title + outline (first 8 headings), or a "not in index" fallback. - completion: fires only inside an unterminated `[[ … ` on the current line; suggests every indexed page name. - workspace/symbol: case-insensitive substring over every indexed heading. Tests (20 new): slugify (basic / punctuation / Unicode / trailing dash), page-name derivation under various roots, upsert / replace / remove, resolve for Wiki + AnchorOnly, sorted `page_names`, UTF-8 passthrough + UTF-16 conversion, position lookup hits and misses. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |