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>
This commit is contained in:
@@ -32,10 +32,16 @@ pub struct IndexedPage {
|
||||
/// Phase 12: every `TagNode` on the page. `tags_by_name` on the
|
||||
/// containing `WorkspaceIndex` is the reverse map.
|
||||
pub tags: Vec<TagInfo>,
|
||||
/// Phase 16: `Some(date)` when this page is a diary entry — i.e. its
|
||||
/// file lives under `<root>/<diary_rel_path>/` and its stem parses as
|
||||
/// `YYYY-MM-DD`. Used by the diary navigation commands.
|
||||
/// Phase 16: `Some(date)` when this page is a *daily* diary entry —
|
||||
/// stem parses as `YYYY-MM-DD`. Equivalent to
|
||||
/// `diary_period.and_then(|p| if let DiaryPeriod::Day(d) = p { Some(d) } else { None })`.
|
||||
/// Kept as its own field for back-compat with prev/next/list callers
|
||||
/// that pre-date the multi-frequency upgrade.
|
||||
pub diary_date: Option<DiaryDate>,
|
||||
/// Cluster 4 (vimwiki parity): `Some(period)` for any diary entry
|
||||
/// (daily, weekly, monthly, yearly). The stem must match the
|
||||
/// canonical format for one of the four frequencies.
|
||||
pub diary_period: Option<nuwiki_core::date::DiaryPeriod>,
|
||||
}
|
||||
|
||||
/// One tag occurrence on a page. `name` is the bare tag string (no
|
||||
@@ -118,8 +124,12 @@ impl WorkspaceIndex {
|
||||
pub fn upsert(&mut self, uri: Url, ast: &DocumentNode) {
|
||||
self.remove(&uri);
|
||||
let name = page_name_from_uri(&uri, self.root.as_deref());
|
||||
let diary_date =
|
||||
diary_date_for_uri(&uri, self.root.as_deref(), self.diary_rel_path.as_deref());
|
||||
let diary_period =
|
||||
diary_period_for_uri(&uri, self.root.as_deref(), self.diary_rel_path.as_deref());
|
||||
let diary_date = diary_period.and_then(|p| match p {
|
||||
nuwiki_core::date::DiaryPeriod::Day(d) => Some(d),
|
||||
_ => None,
|
||||
});
|
||||
let mut page = IndexedPage {
|
||||
uri: uri.clone(),
|
||||
name: name.clone(),
|
||||
@@ -128,6 +138,7 @@ impl WorkspaceIndex {
|
||||
outgoing: Vec::new(),
|
||||
tags: Vec::new(),
|
||||
diary_date,
|
||||
diary_period,
|
||||
};
|
||||
index_blocks(&ast.children, &mut page);
|
||||
|
||||
@@ -224,19 +235,40 @@ impl WorkspaceIndex {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a parsed [`DiaryDate`] when `uri` is a diary entry — its file
|
||||
/// path is under `<root>/<diary_rel_path>/` and its stem parses as
|
||||
/// `YYYY-MM-DD`. Anything else returns `None`.
|
||||
/// Return a parsed [`DiaryDate`] when `uri` is a daily diary entry —
|
||||
/// the path lives under `<root>/<diary_rel_path>/` and its stem parses
|
||||
/// as `YYYY-MM-DD`. For weekly/monthly/yearly entries see
|
||||
/// [`diary_period_for_uri`].
|
||||
pub fn diary_date_for_uri(
|
||||
uri: &Url,
|
||||
root: Option<&Path>,
|
||||
diary_rel_path: Option<&str>,
|
||||
) -> Option<DiaryDate> {
|
||||
match diary_period_for_uri(uri, root, diary_rel_path)? {
|
||||
nuwiki_core::date::DiaryPeriod::Day(d) => Some(d),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a parsed [`DiaryPeriod`] when `uri` is a diary entry at any of
|
||||
/// the four supported frequencies. Recognises:
|
||||
///
|
||||
/// `YYYY-MM-DD` → Day
|
||||
/// `YYYY-Www` → Week (ISO)
|
||||
/// `YYYY-MM` → Month
|
||||
/// `YYYY` → Year
|
||||
///
|
||||
/// As with `diary_date_for_uri`, the path must sit under
|
||||
/// `<root>/<diary_rel_path>/` to be accepted; without a `root` we accept
|
||||
/// any stem (so ad-hoc test setups still work).
|
||||
pub fn diary_period_for_uri(
|
||||
uri: &Url,
|
||||
root: Option<&Path>,
|
||||
diary_rel_path: Option<&str>,
|
||||
) -> Option<nuwiki_core::date::DiaryPeriod> {
|
||||
let path = uri.to_file_path().ok()?;
|
||||
let stem = path.file_stem()?.to_str()?;
|
||||
let parsed = DiaryDate::parse(stem)?;
|
||||
// Without a root we can't enforce the subdir requirement — accept any
|
||||
// file whose stem looks like a date so that ad-hoc test setups work.
|
||||
let parsed = nuwiki_core::date::DiaryPeriod::parse(stem)?;
|
||||
let Some(root) = root else {
|
||||
return Some(parsed);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user