Files
nuwiki/crates/nuwiki-lsp/src/wiki.rs
T
gffranco e75ad6ca89
CI / cargo fmt --check (push) Successful in 27s
CI / cargo clippy (push) Successful in 1m14s
CI / cargo test (push) Successful in 1m20s
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>
2026-05-11 21:04:55 +00:00

73 lines
2.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `Wiki` aggregate — per-wiki state, even when there's only one.
//!
//! v1.0 shipped with a single `Arc<RwLock<WorkspaceIndex>>` on `Backend`.
//! Phase 11 lifts that into a `Wiki` aggregate so Phase 18 (multi-wiki)
//! only changes config shape, not data flow. Phases 1217 always operate
//! on a `Wiki`; in practice the `wikis` `Vec` has one entry until 18.
use std::path::Path;
use std::sync::{Arc, RwLock};
use tower_lsp::lsp_types::Url;
use crate::config::WikiConfig;
use crate::index::WorkspaceIndex;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
pub struct WikiId(pub u32);
#[derive(Clone)]
pub struct Wiki {
pub id: WikiId,
pub config: WikiConfig,
pub index: Arc<RwLock<WorkspaceIndex>>,
}
impl Wiki {
pub fn new(id: WikiId, config: WikiConfig) -> Self {
let index = WorkspaceIndex::new(Some(config.root.clone()))
.with_diary_rel_path(Some(config.diary_rel_path.clone()));
let index = Arc::new(RwLock::new(index));
Self { id, config, index }
}
/// True when `uri` resolves to a file under this wiki's root.
pub fn contains(&self, uri: &Url) -> bool {
let Ok(path) = uri.to_file_path() else {
return false;
};
path.starts_with(&self.config.root)
}
/// Length of the wiki's root path (component count) — used to break
/// ties in nested-root layouts.
pub fn root_depth(&self) -> usize {
self.config.root.components().count()
}
pub fn root(&self) -> &Path {
&self.config.root
}
}
/// Pick the wiki whose root is the longest prefix of `uri`. Returns the
/// wiki id, not a guard, so callers can release the read lock before
/// awaiting.
pub fn resolve_uri_to_wiki(wikis: &[Wiki], uri: &Url) -> Option<WikiId> {
wikis
.iter()
.filter(|w| w.contains(uri))
.max_by_key(|w| w.root_depth())
.map(|w| w.id)
}
/// Build the initial `Vec<Wiki>` from a list of `WikiConfig`s. Always
/// assigns ids by position so `WikiId(0)` is the default wiki.
pub fn build_wikis(configs: &[WikiConfig]) -> Vec<Wiki> {
configs
.iter()
.enumerate()
.map(|(i, cfg)| Wiki::new(WikiId(i as u32), cfg.clone()))
.collect()
}