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:
@@ -12,7 +12,7 @@ use std::path::Path;
|
||||
|
||||
use tower_lsp::lsp_types::Url;
|
||||
|
||||
use nuwiki_core::date::DiaryDate;
|
||||
use nuwiki_core::date::{DiaryDate, DiaryPeriod};
|
||||
|
||||
use crate::config::WikiConfig;
|
||||
use crate::index::WorkspaceIndex;
|
||||
@@ -22,6 +22,12 @@ pub fn uri_for_date(cfg: &WikiConfig, date: &DiaryDate) -> Option<Url> {
|
||||
Url::from_file_path(cfg.diary_path_for(date)).ok()
|
||||
}
|
||||
|
||||
/// Resolve a `DiaryPeriod` to its `file://` URI under the given wiki.
|
||||
/// Picks the right stem for the period's flavour (day / week / month / year).
|
||||
pub fn uri_for_period(cfg: &WikiConfig, period: &DiaryPeriod) -> Option<Url> {
|
||||
Url::from_file_path(cfg.diary_path_for_period(period)).ok()
|
||||
}
|
||||
|
||||
/// `file://` URI of the diary index page.
|
||||
pub fn index_uri(cfg: &WikiConfig) -> Option<Url> {
|
||||
Url::from_file_path(cfg.diary_index_path()).ok()
|
||||
@@ -74,6 +80,94 @@ pub fn prev_entry(index: &WorkspaceIndex, from: &DiaryDate) -> Option<DiaryEntry
|
||||
.find(|e| e.date < *from)
|
||||
}
|
||||
|
||||
/// All indexed diary periods of a *specific* flavour (daily / weekly /
|
||||
/// monthly / yearly), sorted ascending. Used by next/prev navigation so
|
||||
/// `<C-Down>` on a weekly page jumps to the next weekly entry, not the
|
||||
/// next daily one.
|
||||
pub fn list_periods_of_kind(index: &WorkspaceIndex, kind: PeriodKind) -> Vec<PeriodEntry> {
|
||||
let mut out: Vec<PeriodEntry> = index
|
||||
.pages_by_uri
|
||||
.values()
|
||||
.filter_map(|p| {
|
||||
let period = p.diary_period?;
|
||||
if PeriodKind::of(&period) != kind {
|
||||
return None;
|
||||
}
|
||||
Some(PeriodEntry {
|
||||
period,
|
||||
uri: p.uri.clone(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
out.sort_by_key(|e| period_first_day(&e.period));
|
||||
out
|
||||
}
|
||||
|
||||
pub fn next_period(index: &WorkspaceIndex, from: &DiaryPeriod) -> Option<PeriodEntry> {
|
||||
let kind = PeriodKind::of(from);
|
||||
let pivot = period_first_day(from);
|
||||
list_periods_of_kind(index, kind)
|
||||
.into_iter()
|
||||
.find(|e| period_first_day(&e.period) > pivot)
|
||||
}
|
||||
|
||||
pub fn prev_period(index: &WorkspaceIndex, from: &DiaryPeriod) -> Option<PeriodEntry> {
|
||||
let kind = PeriodKind::of(from);
|
||||
let pivot = period_first_day(from);
|
||||
list_periods_of_kind(index, kind)
|
||||
.into_iter()
|
||||
.rev()
|
||||
.find(|e| period_first_day(&e.period) < pivot)
|
||||
}
|
||||
|
||||
/// Cluster 4: `(period, uri)` pair for non-daily diary navigation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PeriodEntry {
|
||||
pub period: DiaryPeriod,
|
||||
pub uri: Url,
|
||||
}
|
||||
|
||||
impl serde::Serialize for PeriodEntry {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::SerializeStruct;
|
||||
// For back-compat with daily-only clients, emit `date` AND
|
||||
// `period` — the former is just the first-day string of the
|
||||
// period, the latter is the canonical stem.
|
||||
let mut s = serializer.serialize_struct("PeriodEntry", 3)?;
|
||||
s.serialize_field("date", &self.period.first_day().format())?;
|
||||
s.serialize_field("period", &self.period.format())?;
|
||||
s.serialize_field("uri", &self.uri)?;
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Which flavour of `DiaryPeriod` we're looking at.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PeriodKind {
|
||||
Day,
|
||||
Week,
|
||||
Month,
|
||||
Year,
|
||||
}
|
||||
|
||||
impl PeriodKind {
|
||||
pub fn of(p: &DiaryPeriod) -> Self {
|
||||
match p {
|
||||
DiaryPeriod::Day(_) => Self::Day,
|
||||
DiaryPeriod::Week { .. } => Self::Week,
|
||||
DiaryPeriod::Month { .. } => Self::Month,
|
||||
DiaryPeriod::Year { .. } => Self::Year,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn period_first_day(p: &DiaryPeriod) -> DiaryDate {
|
||||
p.first_day()
|
||||
}
|
||||
|
||||
/// Generate the body of the diary index page — a flat newest-first list
|
||||
/// of `[[diary/YYYY-MM-DD]]` wikilinks, grouped under year and month
|
||||
/// subheadings so the rendered page mirrors `:VimwikiDiaryGenerateLinks`.
|
||||
|
||||
Reference in New Issue
Block a user