Commit Graph

4 Commits

Author SHA1 Message Date
gffranco e75ad6ca89 phase 16: diary path conventions + nav commands
CI / cargo fmt --check (push) Successful in 27s
CI / cargo clippy (push) Successful in 1m14s
CI / cargo test (push) Successful in 1m20s
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>
2026-05-11 21:04:55 +00:00
gffranco ce3ac8c4f8 phase 15: link health + TOC/links/orphans
CI / cargo fmt --check (push) Successful in 23s
CI / cargo clippy (push) Successful in 1m29s
CI / cargo test (push) Successful in 1m40s
New diagnostics module hosts the `nuwiki.link` source: walks every
`WikiLinkNode` and emits a diagnostic per broken target. Severity is
gated by `cfg.link_severity` (off | hint | warn | error). Wired into
`collect_diagnostics` so didOpen/didChange publish link diagnostics
alongside parse errors.

Classification (`BrokenLinkKind`):
- `Wiki` target missing from the workspace index → MissingPage.
- `Wiki`/`AnchorOnly` target with an anchor that matches neither a
  heading nor a `:tag:` on the resolved page → MissingAnchor.
- `file:` / `local:` target whose resolved path isn't on disk →
  MissingFile. Absolute paths are used as-is; relative paths resolve
  against the source URI's parent directory, falling back to the wiki
  root.
- Interwiki/Diary/Raw/external — never diagnosed.

`classify_link` works off a `&WikiLinkNode`; `classify_outgoing` does
the same job from a cached `IndexedPage.outgoing` entry so the
workspace-wide checker doesn't need to re-parse every page.

New commands:
- `nuwiki.toc.generate` — generate `= Contents =` heading + nested
  list of `[[#anchor|Title]]` entries from current headings. Replaces
  any existing h1 "Contents" + its immediate following list
  case-insensitively; inserts at line 0 otherwise. Skips the TOC's
  own heading so re-generation is idempotent.
- `nuwiki.links.generate` — equivalent of `:VimwikiGenerateLinks`.
  Flat alphabetical list under `= Generated Links =`, excludes the
  current page.
- `nuwiki.workspace.checkLinks` — returns `Vec<BrokenLinkEntry>` with
  `{ uri, range, kind, message }` per broken link across the wiki.
  Open documents use live text for range conversion; closed pages
  fall back to the stored span coords. Accepts an optional `uri` to
  pick a specific wiki; defaults to the first registered one.
- `nuwiki.workspace.findOrphans` — returns `Vec<{ uri, name }>` for
  every indexed page with no incoming links. Sorted alphabetically.

Pure ops (`commands::ops`):
- `build_toc_text(items, heading_name)` — formats the TOC body with
  2-space indent per level, anchors via `index::slugify`.
- `build_links_text(pages, heading_name, exclude)` — formats the flat
  list.
- `find_section_range(ast, heading_name)` — locates an existing h1
  section (heading + immediate following list) by case-insensitive
  title match.
- `toc_edit` / `links_edit` — full edit producers.
- `collect_workspace_broken_links` / `find_orphans`.

Plumbing:
- `collect_diagnostics` gains a `uri: Option<&Url>` parameter so
  link-health can resolve `file:` / `local:` relatives. `ast_diagnostics`
  back-compat wrapper preserved.
- `Backend::default_wiki` is no longer `#[allow(dead_code)]`.

Tests: 31 new in `phase15_link_health.rs` covering severity mapping,
classification on every kind/anchor case, TOC nesting, links exclusion,
section-range case-insensitivity, idempotent re-generation, orphan
detection, and the COMMANDS list. The Phase 11 stub test was rewritten
into a real "no index = no diagnostics" assertion. All 274 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:49:32 +00:00
gffranco f1d046fad1 phase 14: list/heading edit commands + CommandOutcome split
CI / cargo fmt --check (push) Successful in 20s
CI / cargo clippy (push) Successful in 1m11s
CI / cargo test (push) Successful in 1m11s
Six new executeCommand entries — the cheap surgical edits that account
for the bulk of vimwiki users' daily keymap traffic. Heavier commands
(table.align/moveColumn/insert, list.changeSymbol/changeLevel/renumber/
removeDone, link.normalize, colorize) are deferred for a follow-up
since each needs its own structural rewriter.

CommandOutcome split:
- `Edit(WorkspaceEdit)` — server applies via apply_edit.
- `Value(Value)` — returned verbatim to the client (used by
  read-only queries like nextTask).
- execute_command handler in lib.rs dispatches both cases.

New commands:
- nuwiki.list.toggleCheckbox — [ ] ↔ [X]; mid-states snap to [X];
  rejected reverts to [ ].
- nuwiki.list.cycleCheckbox — [ ] → [.] → [o] → [O] → [X] → [ ];
  rejected resets to [ ].
- nuwiki.list.rejectCheckbox — toggle the [-] rejected marker.
- nuwiki.list.nextTask — returns Location of the first unfinished
  task after the cursor; wraps to the start of the doc when none
  found below. Only items with checkboxes count.
- nuwiki.heading.addLevel — = → ==; clamps at 6.
- nuwiki.heading.removeLevel — == → =; clamps at 1 (no-op on h1).

Pure ops:
- ops::toggle_state / cycle_state / reject_state — checkbox state
  transitions.
- ops::checkbox_edit / next_task / heading_change_level —
  end-to-end edit producers. Tests drive these directly without
  spinning up a Backend.
- ops::rewrite_heading_with_level — text-level rewrite preserving
  the leading whitespace that marks a centered heading (lives
  outside the heading span, so the edit doesn't touch it).
- ops::find_list_item_at / find_checkbox_span / item_in_list —
  AST + source-text helpers, pub for downstream tooling.

Tests (21 new):
- State transitions: round-trip, progression, dash toggle.
- checkbox_edit: empty → done, partial → cycle, reject path,
  no-op on plain item, no-op off list, nested-sublist resolution.
- heading_change_level: h2→h3, level-6 cap, h3→h2, h1 no-op,
  off-heading no-op, span boundary semantics.
- next_task: first-unfinished, wrap-around, all-done returns
  None, plain items ignored.
- COMMANDS list ↔ handler match.

All 211 prior tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:41:11 +00:00
gffranco cb4bfae5e7 phase 13: workspace/rename + executeCommand router + nuwiki.file.delete
CI / cargo fmt --check (push) Successful in 29s
CI / cargo clippy (push) Successful in 1m15s
CI / cargo test (push) Successful in 1m15s
Server-side rename with cross-document link rewriting (P11's
read_or_open delivers on its promise) and the first executeCommand
entry. Plus the routing infrastructure that Phases 14-17 will hang
their own commands off of.

commands.rs:
- commands::execute(backend, name, args) dispatches by command name
  and returns the WorkspaceEdit to apply.
- COMMANDS const slice is the source of truth for what's advertised
  in ExecuteCommandOptions.commands.
- First entry: nuwiki.file.delete with args { uri: <Url> } returning
  a WorkspaceEdit with a DeleteFile op.

rename.rs:
- compute_rename(backend, target_uri, new_name, utf8):
  1. Resolve the wiki via wiki_for_uri.
  2. Build new URI from wiki.root + new_name + extension.
  3. Snapshot the index's backlinks for the old page name before
     releasing the read lock so we don't hold it across awaits.
  4. For each backlink, read_or_open the source (live state or
     disk + reparse) and validate the cached span against current
     text.
  5. Rewrite the [[...]] segment with rewrite_wikilink_target,
     preserving anchor and description.
  6. Add the RenameFile op so the builder emits it before the text
     edits in documentChanges ordering.
- build_new_uri handles subdirectory paths and avoids double-
  appending the file extension.
- rewrite_wikilink_target is pub so other tooling can call it.

lib.rs:
- Capabilities: rename_provider + execute_command_provider listing
  commands::COMMANDS.
- New rename handler: cursor-on-wikilink renames the linked page,
  else the buffer's file. Delegates to rename::compute_rename.
- New execute_command handler dispatches via commands::execute, then
  pushes the resulting WorkspaceEdit through client.apply_edit. On
  unknown command or bad args, logs and returns null.
- read_or_open loses its allow(dead_code) attribute now that Phase
  13 calls it.
- rename_target_uri helper shares index-resolution with goto_def.

Tests (11 new in phase13_rename_commands.rs):
- rewrite_wikilink_target: plain, with description, with anchor,
  with both, subdirectory path, non-wikilink-text rejection.
- build_new_uri: under root, with subdirectory, extension dedup,
  empty path segments skipped.
- nuwiki.file.delete builder smoke test (DocumentChanges + DeleteFile).

All 211 v1.0+Phase 11+12 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:27:39 +00:00