e75ad6ca89
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>
73 lines
2.2 KiB
Rust
73 lines
2.2 KiB
Rust
//! `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 12–17 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()
|
||
}
|