9441fe918c
Implements the remaining fourth-pass findings (gap doc updated):
- list_margin: rework to upstream's buffer-side meaning. Drop the
nuwiki-only HTML em-margin (renderer field/method + render_page_html
param removed) and prepend max(0, list_margin) leading spaces to every
generated bullet in build_toc_text / build_links_text /
build_tag_links_text / diary::build_index_body; headings stay at col 0.
From<RawWiki> derives 0 for markdown wikis when unset. Negatives can't
resolve 'shiftwidth' server-side, so they collapse to zero indent
(documented divergence).
- diary_months: per-wiki Vec<String> (default 12 English names), threaded
into the diary-index month labels; missing/empty slots fall back to the
English name.
- diary_caption_level: widen u8 -> i8 so vimwiki's -1 (min: -1) parses;
build_index_body clamps < 0 to base tree level 0.
- VimwikiRemoveDone: regain upstream's -range. All four defs are now
-bang -range, dispatched via remove_done(bang, range, l1, l2) in both
clients: ! -> whole buffer, explicit range -> new list_remove_done_range
({range:[l1-1,l2-1]}), else current list. Server remove_done_edit gained
an Option<(u32,u32)> range that filters whole-doc victims by start line.
markdown_header_style is deferred: the generators emit vimwiki syntax only
(caption_line never writes markdown headers), so there's no markdown header
to attach the style to. Logged as the "generated-content is vimwiki-only"
intentional divergence pending a later markdown generated-content effort.
Tests: list_margin indent, markdown list_margin-0 default, diary_months
custom + fallback, negative caption_level clamp/parse, ranged remove-done
(server + both keymap harnesses). 553 lsp/core tests pass; clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
768 lines
23 KiB
Rust
768 lines
23 KiB
Rust
//! Diary feature tests:
|
||
//! - Date primitives, path conventions, nav helpers, IndexedPage diary
|
||
//! classification.
|
||
//! - Frequency wiring (daily/weekly/monthly/yearly): per-frequency
|
||
//! path/URI shape, index recognition, and `next_period` / `prev_period`
|
||
//! navigation across mixed-frequency entries.
|
||
|
||
use std::path::PathBuf;
|
||
|
||
use nuwiki_core::date::{DiaryDate, DiaryFrequency, DiaryPeriod};
|
||
use nuwiki_core::syntax::vimwiki::VimwikiSyntax;
|
||
use nuwiki_core::syntax::SyntaxPlugin;
|
||
use nuwiki_lsp::config::WikiConfig;
|
||
use nuwiki_lsp::diary;
|
||
use nuwiki_lsp::index::{diary_period_for_uri, WorkspaceIndex};
|
||
use tower_lsp::lsp_types::Url;
|
||
|
||
fn parse(src: &str) -> nuwiki_core::ast::DocumentNode {
|
||
VimwikiSyntax::new().parse(src)
|
||
}
|
||
|
||
fn wiki_cfg(root: &str) -> WikiConfig {
|
||
let mut cfg = WikiConfig::from_root(PathBuf::from(root));
|
||
cfg.diary_rel_path = "diary".into();
|
||
cfg.diary_index = "diary".into();
|
||
cfg
|
||
}
|
||
|
||
fn build_index_with_entries(root: &str, dates: &[&str]) -> WorkspaceIndex {
|
||
let mut idx =
|
||
WorkspaceIndex::new(Some(PathBuf::from(root))).with_diary_rel_path(Some("diary".into()));
|
||
for d in dates {
|
||
let path = format!("{root}/diary/{d}.wiki");
|
||
let uri = Url::from_file_path(&path).unwrap();
|
||
idx.upsert(uri, &parse("= entry =\n"));
|
||
}
|
||
idx
|
||
}
|
||
|
||
// ===== DiaryDate parser =====
|
||
|
||
#[test]
|
||
fn parse_accepts_canonical_iso8601() {
|
||
let d = DiaryDate::parse("2026-05-11").unwrap();
|
||
assert_eq!(d.year, 2026);
|
||
assert_eq!(d.month, 5);
|
||
assert_eq!(d.day, 11);
|
||
}
|
||
|
||
#[test]
|
||
fn parse_rejects_missing_zero_padding() {
|
||
assert!(DiaryDate::parse("2026-5-11").is_none());
|
||
assert!(DiaryDate::parse("2026-05-1").is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn parse_rejects_alternate_separators() {
|
||
assert!(DiaryDate::parse("2026/05/11").is_none());
|
||
assert!(DiaryDate::parse("2026.05.11").is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn parse_rejects_extra_whitespace() {
|
||
assert!(DiaryDate::parse(" 2026-05-11").is_none());
|
||
assert!(DiaryDate::parse("2026-05-11 ").is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn parse_rejects_invalid_calendar_dates() {
|
||
assert!(DiaryDate::parse("2026-13-01").is_none(), "month 13");
|
||
assert!(DiaryDate::parse("2026-02-30").is_none(), "feb 30");
|
||
assert!(DiaryDate::parse("2026-04-31").is_none(), "april 31");
|
||
assert!(DiaryDate::parse("2026-00-10").is_none(), "month 0");
|
||
assert!(DiaryDate::parse("2026-05-00").is_none(), "day 0");
|
||
}
|
||
|
||
#[test]
|
||
fn parse_accepts_leap_day_in_leap_year() {
|
||
assert!(DiaryDate::parse("2024-02-29").is_some(), "2024 is leap");
|
||
assert!(DiaryDate::parse("2023-02-29").is_none(), "2023 not leap");
|
||
assert!(DiaryDate::parse("2000-02-29").is_some(), "div by 400");
|
||
assert!(
|
||
DiaryDate::parse("1900-02-29").is_none(),
|
||
"div by 100 not 400"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn format_round_trip() {
|
||
let d = DiaryDate::from_ymd(2026, 5, 11).unwrap();
|
||
assert_eq!(d.format(), "2026-05-11");
|
||
assert_eq!(DiaryDate::parse(&d.format()), Some(d));
|
||
}
|
||
|
||
// ===== Date arithmetic =====
|
||
|
||
#[test]
|
||
fn next_day_wraps_month_boundary() {
|
||
let last_of_jan = DiaryDate::from_ymd(2026, 1, 31).unwrap();
|
||
assert_eq!(
|
||
last_of_jan.next_day(),
|
||
DiaryDate::from_ymd(2026, 2, 1).unwrap()
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn next_day_wraps_year_boundary() {
|
||
let nye = DiaryDate::from_ymd(2025, 12, 31).unwrap();
|
||
assert_eq!(nye.next_day(), DiaryDate::from_ymd(2026, 1, 1).unwrap());
|
||
}
|
||
|
||
#[test]
|
||
fn prev_day_walks_back_across_month() {
|
||
let first_of_mar = DiaryDate::from_ymd(2026, 3, 1).unwrap();
|
||
assert_eq!(
|
||
first_of_mar.prev_day(),
|
||
DiaryDate::from_ymd(2026, 2, 28).unwrap()
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn prev_day_walks_back_across_leap() {
|
||
let first_of_mar = DiaryDate::from_ymd(2024, 3, 1).unwrap();
|
||
assert_eq!(
|
||
first_of_mar.prev_day(),
|
||
DiaryDate::from_ymd(2024, 2, 29).unwrap()
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn ordering_is_chronological() {
|
||
let a = DiaryDate::from_ymd(2026, 1, 1).unwrap();
|
||
let b = DiaryDate::from_ymd(2026, 1, 2).unwrap();
|
||
let c = DiaryDate::from_ymd(2026, 2, 1).unwrap();
|
||
let d = DiaryDate::from_ymd(2027, 1, 1).unwrap();
|
||
assert!(a < b);
|
||
assert!(b < c);
|
||
assert!(c < d);
|
||
}
|
||
|
||
#[test]
|
||
fn today_utc_is_a_real_calendar_date() {
|
||
let t = DiaryDate::today_utc();
|
||
assert!(t.year >= 2020 && t.year <= 2100);
|
||
assert!((1..=12).contains(&t.month));
|
||
assert!((1..=31).contains(&t.day));
|
||
}
|
||
|
||
// ===== WikiConfig diary paths =====
|
||
|
||
#[test]
|
||
fn diary_dir_joins_root_and_rel_path() {
|
||
let cfg = wiki_cfg("/tmp/wiki");
|
||
assert_eq!(cfg.diary_dir(), PathBuf::from("/tmp/wiki/diary"));
|
||
}
|
||
|
||
#[test]
|
||
fn diary_index_path_uses_diary_index_filename_and_extension() {
|
||
let cfg = wiki_cfg("/tmp/wiki");
|
||
assert_eq!(
|
||
cfg.diary_index_path(),
|
||
PathBuf::from("/tmp/wiki/diary/diary.wiki")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn diary_path_for_uses_iso_date_as_stem() {
|
||
let cfg = wiki_cfg("/tmp/wiki");
|
||
let date = DiaryDate::from_ymd(2026, 5, 11).unwrap();
|
||
assert_eq!(
|
||
cfg.diary_path_for(&date),
|
||
PathBuf::from("/tmp/wiki/diary/2026-05-11.wiki")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn diary_uri_for_date_is_file_url() {
|
||
let cfg = wiki_cfg("/tmp/wiki");
|
||
let date = DiaryDate::from_ymd(2026, 5, 11).unwrap();
|
||
let uri = diary::uri_for_date(&cfg, &date).unwrap();
|
||
assert!(uri.as_str().ends_with("/diary/2026-05-11.wiki"));
|
||
}
|
||
|
||
#[test]
|
||
fn diary_index_uri_resolves() {
|
||
let cfg = wiki_cfg("/tmp/wiki");
|
||
let uri = diary::index_uri(&cfg).unwrap();
|
||
assert!(uri.as_str().ends_with("/diary/diary.wiki"));
|
||
}
|
||
|
||
// ===== Index integration: diary_date on IndexedPage =====
|
||
|
||
#[test]
|
||
fn upsert_marks_diary_entries() {
|
||
let root = "/tmp/diary1";
|
||
let idx = build_index_with_entries(root, &["2026-05-11"]);
|
||
let uri = Url::from_file_path(format!("{root}/diary/2026-05-11.wiki")).unwrap();
|
||
let page = idx.page(&uri).expect("page");
|
||
let date = page.diary_date.expect("diary_date");
|
||
assert_eq!(date.format(), "2026-05-11");
|
||
}
|
||
|
||
#[test]
|
||
fn upsert_does_not_mark_non_diary_files() {
|
||
let mut idx = WorkspaceIndex::new(Some(PathBuf::from("/tmp/diary2")))
|
||
.with_diary_rel_path(Some("diary".into()));
|
||
let uri = Url::from_file_path("/tmp/diary2/Home.wiki").unwrap();
|
||
idx.upsert(uri.clone(), &parse("= Home =\n"));
|
||
assert!(idx.page(&uri).unwrap().diary_date.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn upsert_does_not_mark_dates_outside_diary_subdir() {
|
||
// A file at root named like a date but not under diary/ → not a
|
||
// diary entry.
|
||
let mut idx = WorkspaceIndex::new(Some(PathBuf::from("/tmp/diary3")))
|
||
.with_diary_rel_path(Some("diary".into()));
|
||
let uri = Url::from_file_path("/tmp/diary3/2026-05-11.wiki").unwrap();
|
||
idx.upsert(uri.clone(), &parse("= note =\n"));
|
||
assert!(idx.page(&uri).unwrap().diary_date.is_none());
|
||
}
|
||
|
||
// ===== Diary listing + navigation =====
|
||
|
||
#[test]
|
||
fn list_entries_returns_sorted_ascending() {
|
||
let idx = build_index_with_entries("/tmp/diary4", &["2026-05-11", "2026-04-30", "2026-05-12"]);
|
||
let entries = diary::list_entries(&idx);
|
||
let dates: Vec<String> = entries.iter().map(|e| e.date.format()).collect();
|
||
assert_eq!(dates, vec!["2026-04-30", "2026-05-11", "2026-05-12"]);
|
||
}
|
||
|
||
#[test]
|
||
fn list_entries_filtered_by_year_and_month() {
|
||
let idx = build_index_with_entries(
|
||
"/tmp/diary5",
|
||
&["2025-12-31", "2026-01-01", "2026-05-11", "2026-05-12"],
|
||
);
|
||
let may = diary::list_entries_filtered(&idx, Some(2026), Some(5));
|
||
assert_eq!(may.len(), 2);
|
||
let y_only = diary::list_entries_filtered(&idx, Some(2026), None);
|
||
assert_eq!(y_only.len(), 3);
|
||
}
|
||
|
||
#[test]
|
||
fn next_entry_finds_strictly_greater() {
|
||
let idx = build_index_with_entries("/tmp/diary6", &["2026-05-10", "2026-05-12", "2026-05-15"]);
|
||
let from = DiaryDate::from_ymd(2026, 5, 11).unwrap();
|
||
let next = diary::next_entry(&idx, &from).unwrap();
|
||
assert_eq!(next.date.format(), "2026-05-12");
|
||
}
|
||
|
||
#[test]
|
||
fn next_entry_none_when_no_future_entries() {
|
||
let idx = build_index_with_entries("/tmp/diary7", &["2026-05-10"]);
|
||
let from = DiaryDate::from_ymd(2026, 5, 11).unwrap();
|
||
assert!(diary::next_entry(&idx, &from).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn prev_entry_finds_strictly_lesser() {
|
||
let idx = build_index_with_entries("/tmp/diary8", &["2026-05-10", "2026-05-12", "2026-05-15"]);
|
||
let from = DiaryDate::from_ymd(2026, 5, 13).unwrap();
|
||
let prev = diary::prev_entry(&idx, &from).unwrap();
|
||
assert_eq!(prev.date.format(), "2026-05-12");
|
||
}
|
||
|
||
// ===== Index body generation =====
|
||
|
||
#[test]
|
||
fn build_index_body_is_newest_first_grouped_by_year_month() {
|
||
let idx = build_index_with_entries(
|
||
"/tmp/diary9",
|
||
&["2025-12-31", "2026-05-11", "2026-05-12", "2026-04-30"],
|
||
);
|
||
let entries = diary::list_entries(&idx);
|
||
let body = diary::build_index_body(
|
||
&entries,
|
||
"diary",
|
||
"Diary",
|
||
diary::DiarySort::Desc,
|
||
1,
|
||
&[],
|
||
0,
|
||
);
|
||
let lines: Vec<&str> = body.lines().collect();
|
||
assert_eq!(lines[0], "= Diary =");
|
||
// First date in output should be the latest: 2026-05-12
|
||
let earliest_2026_pos = body.find("2026-05-12").unwrap();
|
||
let latest_2025_pos = body.find("2025-12-31").unwrap();
|
||
assert!(earliest_2026_pos < latest_2025_pos, "newer entries first");
|
||
// Year headings appear
|
||
assert!(body.contains("== 2026 =="));
|
||
assert!(body.contains("== 2025 =="));
|
||
// Month headings appear
|
||
assert!(body.contains("=== May ==="));
|
||
assert!(body.contains("=== December ==="));
|
||
assert!(body.contains("=== April ==="));
|
||
}
|
||
|
||
#[test]
|
||
fn build_index_body_empty_when_no_entries() {
|
||
let body = diary::build_index_body(&[], "diary", "Diary", diary::DiarySort::Desc, 1, &[], 0);
|
||
assert_eq!(body, "= Diary =\n");
|
||
}
|
||
|
||
#[test]
|
||
fn build_index_body_ascending_sort_lists_oldest_first() {
|
||
let idx = build_index_with_entries("/tmp/diarySort", &["2026-05-11", "2026-05-12"]);
|
||
let entries = diary::list_entries(&idx);
|
||
let body =
|
||
diary::build_index_body(&entries, "diary", "Diary", diary::DiarySort::Asc, 1, &[], 0);
|
||
let older = body.find("2026-05-11").unwrap();
|
||
let newer = body.find("2026-05-12").unwrap();
|
||
assert!(older < newer, "ascending sort lists oldest first");
|
||
}
|
||
|
||
#[test]
|
||
fn build_index_body_caption_level_shifts_all_headings() {
|
||
let idx = build_index_with_entries("/tmp/diaryCap", &["2026-05-11"]);
|
||
let entries = diary::list_entries(&idx);
|
||
let body = diary::build_index_body(
|
||
&entries,
|
||
"diary",
|
||
"Diary",
|
||
diary::DiarySort::Desc,
|
||
2,
|
||
&[],
|
||
0,
|
||
);
|
||
assert!(body.contains("== Diary =="), "caption at level 2");
|
||
assert!(
|
||
body.contains("=== 2026 ==="),
|
||
"year nests one below caption"
|
||
);
|
||
assert!(
|
||
body.contains("==== May ===="),
|
||
"month nests two below caption"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn build_index_body_clamps_caption_level_to_six() {
|
||
let idx = build_index_with_entries("/tmp/diaryClamp", &["2026-05-11"]);
|
||
let entries = diary::list_entries(&idx);
|
||
// caption_level 6 → year/month would be 7/8 but clamp to 6.
|
||
let body = diary::build_index_body(
|
||
&entries,
|
||
"diary",
|
||
"Diary",
|
||
diary::DiarySort::Desc,
|
||
6,
|
||
&[],
|
||
0,
|
||
);
|
||
assert!(body.contains("====== Diary ======"));
|
||
assert!(!body.contains("======="), "no heading exceeds level 6");
|
||
}
|
||
|
||
#[test]
|
||
fn build_index_body_uses_custom_diary_months() {
|
||
let idx = build_index_with_entries("/tmp/diaryMonths", &["2026-05-11", "2026-12-01"]);
|
||
let entries = diary::list_entries(&idx);
|
||
// A localized month table (Portuguese) replaces the English labels.
|
||
let months: Vec<String> = [
|
||
"Janeiro",
|
||
"Fevereiro",
|
||
"Março",
|
||
"Abril",
|
||
"Maio",
|
||
"Junho",
|
||
"Julho",
|
||
"Agosto",
|
||
"Setembro",
|
||
"Outubro",
|
||
"Novembro",
|
||
"Dezembro",
|
||
]
|
||
.iter()
|
||
.map(|s| s.to_string())
|
||
.collect();
|
||
let body = diary::build_index_body(
|
||
&entries,
|
||
"diary",
|
||
"Diary",
|
||
diary::DiarySort::Desc,
|
||
1,
|
||
&months,
|
||
0,
|
||
);
|
||
assert!(body.contains("=== Maio ==="), "May → Maio: {body}");
|
||
assert!(
|
||
body.contains("=== Dezembro ==="),
|
||
"December → Dezembro: {body}"
|
||
);
|
||
assert!(!body.contains("May"), "English label leaked: {body}");
|
||
}
|
||
|
||
#[test]
|
||
fn build_index_body_falls_back_per_missing_month_slot() {
|
||
let idx = build_index_with_entries("/tmp/diaryShort", &["2026-05-11"]);
|
||
let entries = diary::list_entries(&idx);
|
||
// A too-short table only covers Jan–Mar; May falls back to English.
|
||
let months: Vec<String> = ["Jan", "Feb", "Mar"]
|
||
.iter()
|
||
.map(|s| s.to_string())
|
||
.collect();
|
||
let body = diary::build_index_body(
|
||
&entries,
|
||
"diary",
|
||
"Diary",
|
||
diary::DiarySort::Desc,
|
||
1,
|
||
&months,
|
||
0,
|
||
);
|
||
assert!(
|
||
body.contains("=== May ==="),
|
||
"missing slot → English: {body}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn build_index_body_clamps_negative_caption_level_to_zero() {
|
||
let idx = build_index_with_entries("/tmp/diaryNeg", &["2026-05-11"]);
|
||
let entries = diary::list_entries(&idx);
|
||
// vimwiki allows diary_caption_level = -1; nuwiki clamps it to 0 (same as
|
||
// a caption_level of 0: caption h1, year h1, month h2).
|
||
let body = diary::build_index_body(
|
||
&entries,
|
||
"diary",
|
||
"Diary",
|
||
diary::DiarySort::Desc,
|
||
-1,
|
||
&[],
|
||
0,
|
||
);
|
||
assert!(body.contains("= Diary ="), "caption at level 1: {body}");
|
||
assert!(body.contains("= 2026 ="), "year at level 1: {body}");
|
||
assert!(body.contains("== May =="), "month at level 2: {body}");
|
||
}
|
||
|
||
#[test]
|
||
fn render_index_body_round_trip() {
|
||
let cfg = wiki_cfg("/tmp/diaryA");
|
||
let idx = build_index_with_entries("/tmp/diaryA", &["2026-05-11"]);
|
||
let body = diary::render_index_body(&cfg, &idx);
|
||
assert!(body.contains("= Diary ="));
|
||
assert!(body.contains("[[diary/2026-05-11]]"));
|
||
}
|
||
|
||
#[test]
|
||
fn render_index_body_honours_custom_header_sort_and_caption() {
|
||
let mut cfg = wiki_cfg("/tmp/diaryCustom");
|
||
cfg.diary_header = "Journal".into();
|
||
cfg.diary_sort = "asc".into();
|
||
cfg.diary_caption_level = 2;
|
||
let idx = build_index_with_entries("/tmp/diaryCustom", &["2026-05-11", "2026-05-12"]);
|
||
let body = diary::render_index_body(&cfg, &idx);
|
||
assert!(
|
||
body.contains("== Journal =="),
|
||
"custom header at caption level 2"
|
||
);
|
||
let older = body.find("2026-05-11").unwrap();
|
||
let newer = body.find("2026-05-12").unwrap();
|
||
assert!(older < newer, "asc sort honoured");
|
||
}
|
||
|
||
// ===== is_in_diary =====
|
||
|
||
#[test]
|
||
fn is_in_diary_recognises_subdir_paths() {
|
||
let cfg = wiki_cfg("/tmp/wiki");
|
||
assert!(diary::is_in_diary(
|
||
&cfg,
|
||
&PathBuf::from("/tmp/wiki/diary/2026-05-11.wiki")
|
||
));
|
||
assert!(!diary::is_in_diary(
|
||
&cfg,
|
||
&PathBuf::from("/tmp/wiki/Home.wiki")
|
||
));
|
||
}
|
||
|
||
// ===== Serde =====
|
||
|
||
#[test]
|
||
fn diary_entry_serializes_to_date_string_and_uri() {
|
||
let uri = Url::parse("file:///tmp/wiki/diary/2026-05-11.wiki").unwrap();
|
||
let entry = diary::DiaryEntry {
|
||
date: DiaryDate::from_ymd(2026, 5, 11).unwrap(),
|
||
uri,
|
||
};
|
||
let v = serde_json::to_value(&entry).unwrap();
|
||
assert_eq!(v["date"], "2026-05-11");
|
||
assert!(v["uri"].as_str().unwrap().contains("2026-05-11.wiki"));
|
||
}
|
||
|
||
// ===== COMMANDS list completeness =====
|
||
|
||
#[test]
|
||
fn commands_list_includes_diary_entries() {
|
||
let names: Vec<&str> = nuwiki_lsp::commands::COMMANDS.to_vec();
|
||
for name in [
|
||
"nuwiki.diary.openToday",
|
||
"nuwiki.diary.openYesterday",
|
||
"nuwiki.diary.openTomorrow",
|
||
"nuwiki.diary.openIndex",
|
||
"nuwiki.diary.generateIndex",
|
||
"nuwiki.diary.next",
|
||
"nuwiki.diary.prev",
|
||
"nuwiki.diary.listEntries",
|
||
"nuwiki.diary.openForDate",
|
||
] {
|
||
assert!(names.contains(&name), "missing: {name}");
|
||
}
|
||
}
|
||
|
||
// ===== Config wire format =====
|
||
|
||
#[test]
|
||
fn wiki_config_defaults_match_vimwiki() {
|
||
let cfg = WikiConfig::empty();
|
||
assert_eq!(cfg.diary_rel_path, "diary");
|
||
assert_eq!(cfg.diary_index, "diary");
|
||
}
|
||
|
||
#[test]
|
||
fn wiki_config_from_root_uses_default_diary_paths() {
|
||
let cfg = WikiConfig::from_root(PathBuf::from("/tmp/x"));
|
||
assert_eq!(cfg.diary_rel_path, "diary");
|
||
}
|
||
|
||
#[test]
|
||
fn config_from_json_accepts_custom_diary_paths() {
|
||
use nuwiki_lsp::config::config_from_json;
|
||
let cfg = config_from_json(serde_json::json!({
|
||
"wikis": [
|
||
{ "root": "/tmp/p", "diary_rel_path": "journal", "diary_index": "index" },
|
||
],
|
||
}));
|
||
assert_eq!(cfg.wikis[0].diary_rel_path, "journal");
|
||
assert_eq!(cfg.wikis[0].diary_index, "index");
|
||
}
|
||
|
||
// ===== Frequency wiring (daily / weekly / monthly / yearly) =====
|
||
|
||
fn cfg(root: &str, frequency: &str) -> WikiConfig {
|
||
let mut c = WikiConfig::from_root(PathBuf::from(root));
|
||
c.diary_rel_path = "diary".into();
|
||
c.diary_frequency = frequency.into();
|
||
c
|
||
}
|
||
|
||
#[test]
|
||
fn frequency_helper_picks_up_config_value() {
|
||
assert_eq!(cfg("/tmp", "daily").frequency(), DiaryFrequency::Daily);
|
||
assert_eq!(cfg("/tmp", "weekly").frequency(), DiaryFrequency::Weekly);
|
||
assert_eq!(cfg("/tmp", "monthly").frequency(), DiaryFrequency::Monthly);
|
||
assert_eq!(cfg("/tmp", "yearly").frequency(), DiaryFrequency::Yearly);
|
||
}
|
||
|
||
#[test]
|
||
fn diary_path_for_period_picks_right_stem_per_frequency() {
|
||
let c = cfg("/wiki", "daily");
|
||
let day = DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 12).unwrap());
|
||
let wk = DiaryPeriod::Week {
|
||
iso_year: 2026,
|
||
iso_week: 19,
|
||
};
|
||
let mo = DiaryPeriod::Month {
|
||
year: 2026,
|
||
month: 5,
|
||
};
|
||
let yr = DiaryPeriod::Year { year: 2026 };
|
||
assert_eq!(
|
||
c.diary_path_for_period(&day),
|
||
PathBuf::from("/wiki/diary/2026-05-12.wiki")
|
||
);
|
||
assert_eq!(
|
||
c.diary_path_for_period(&wk),
|
||
PathBuf::from("/wiki/diary/2026-W19.wiki")
|
||
);
|
||
assert_eq!(
|
||
c.diary_path_for_period(&mo),
|
||
PathBuf::from("/wiki/diary/2026-05.wiki")
|
||
);
|
||
assert_eq!(
|
||
c.diary_path_for_period(&yr),
|
||
PathBuf::from("/wiki/diary/2026.wiki")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn uri_for_period_produces_file_url_per_frequency() {
|
||
let c = cfg("/wiki", "weekly");
|
||
let wk = DiaryPeriod::Week {
|
||
iso_year: 2026,
|
||
iso_week: 19,
|
||
};
|
||
let u = diary::uri_for_period(&c, &wk).unwrap();
|
||
assert!(
|
||
u.as_str().ends_with("/wiki/diary/2026-W19.wiki"),
|
||
"got: {u}"
|
||
);
|
||
}
|
||
|
||
fn make_index_with(entries: &[&str]) -> WorkspaceIndex {
|
||
let root = PathBuf::from("/wiki");
|
||
let mut idx = WorkspaceIndex::new(Some(root.clone())).with_diary_rel_path(Some("diary".into()));
|
||
for stem in entries {
|
||
let path = format!("/wiki/diary/{stem}.wiki");
|
||
let uri = Url::from_file_path(&path).unwrap();
|
||
let ast = VimwikiSyntax::new().parse("= entry =\n");
|
||
idx.upsert(uri, &ast);
|
||
}
|
||
idx
|
||
}
|
||
|
||
#[test]
|
||
fn index_recognises_all_four_frequency_stems() {
|
||
let idx = make_index_with(&["2026-05-12", "2026-W19", "2026-05", "2026"]);
|
||
for (stem, expected) in [
|
||
(
|
||
"2026-05-12",
|
||
DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 12).unwrap()),
|
||
),
|
||
(
|
||
"2026-W19",
|
||
DiaryPeriod::Week {
|
||
iso_year: 2026,
|
||
iso_week: 19,
|
||
},
|
||
),
|
||
(
|
||
"2026-05",
|
||
DiaryPeriod::Month {
|
||
year: 2026,
|
||
month: 5,
|
||
},
|
||
),
|
||
("2026", DiaryPeriod::Year { year: 2026 }),
|
||
] {
|
||
let uri = Url::from_file_path(format!("/wiki/diary/{stem}.wiki")).unwrap();
|
||
let page = idx.page(&uri).expect(stem);
|
||
assert_eq!(
|
||
page.diary_period,
|
||
Some(expected),
|
||
"stem {stem} should index as {expected:?}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn index_only_sets_diary_date_for_daily_entries() {
|
||
let idx = make_index_with(&["2026-05-12", "2026-W19"]);
|
||
let daily = Url::from_file_path("/wiki/diary/2026-05-12.wiki").unwrap();
|
||
let weekly = Url::from_file_path("/wiki/diary/2026-W19.wiki").unwrap();
|
||
assert!(idx.page(&daily).unwrap().diary_date.is_some());
|
||
assert!(idx.page(&weekly).unwrap().diary_date.is_none());
|
||
assert!(idx.page(&weekly).unwrap().diary_period.is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn diary_period_for_uri_rejects_paths_outside_diary_dir() {
|
||
// Non-diary path with a date-looking stem — should NOT count.
|
||
let uri = Url::from_file_path("/wiki/notes/2026-05-12.wiki").unwrap();
|
||
let root = PathBuf::from("/wiki");
|
||
assert!(diary_period_for_uri(&uri, Some(&root), Some("diary")).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn next_prev_period_navigate_at_the_same_frequency() {
|
||
// Mix entries at different frequencies. Navigation should stay
|
||
// within the same flavour as the pivot.
|
||
let idx = make_index_with(&[
|
||
"2026-W18",
|
||
"2026-W19",
|
||
"2026-W20", // weekly
|
||
"2026-04",
|
||
"2026-05",
|
||
"2026-06", // monthly
|
||
"2026-05-10",
|
||
"2026-05-12", // daily
|
||
]);
|
||
let pivot_w = DiaryPeriod::Week {
|
||
iso_year: 2026,
|
||
iso_week: 19,
|
||
};
|
||
let next_w = diary::next_period(&idx, &pivot_w).unwrap();
|
||
assert_eq!(
|
||
next_w.period,
|
||
DiaryPeriod::Week {
|
||
iso_year: 2026,
|
||
iso_week: 20
|
||
}
|
||
);
|
||
let prev_w = diary::prev_period(&idx, &pivot_w).unwrap();
|
||
assert_eq!(
|
||
prev_w.period,
|
||
DiaryPeriod::Week {
|
||
iso_year: 2026,
|
||
iso_week: 18
|
||
}
|
||
);
|
||
|
||
let pivot_m = DiaryPeriod::Month {
|
||
year: 2026,
|
||
month: 5,
|
||
};
|
||
let next_m = diary::next_period(&idx, &pivot_m).unwrap();
|
||
assert_eq!(
|
||
next_m.period,
|
||
DiaryPeriod::Month {
|
||
year: 2026,
|
||
month: 6
|
||
}
|
||
);
|
||
let prev_m = diary::prev_period(&idx, &pivot_m).unwrap();
|
||
assert_eq!(
|
||
prev_m.period,
|
||
DiaryPeriod::Month {
|
||
year: 2026,
|
||
month: 4
|
||
}
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn next_prev_period_returns_none_past_the_edge() {
|
||
let idx = make_index_with(&["2026-W19"]);
|
||
let only = DiaryPeriod::Week {
|
||
iso_year: 2026,
|
||
iso_week: 19,
|
||
};
|
||
assert!(diary::next_period(&idx, &only).is_none());
|
||
assert!(diary::prev_period(&idx, &only).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn diary_period_for_uri_handles_each_frequency_under_diary_dir() {
|
||
let root = PathBuf::from("/wiki");
|
||
for (stem, expected) in [
|
||
(
|
||
"2026-05-12",
|
||
DiaryPeriod::Day(DiaryDate::from_ymd(2026, 5, 12).unwrap()),
|
||
),
|
||
(
|
||
"2026-W19",
|
||
DiaryPeriod::Week {
|
||
iso_year: 2026,
|
||
iso_week: 19,
|
||
},
|
||
),
|
||
(
|
||
"2026-05",
|
||
DiaryPeriod::Month {
|
||
year: 2026,
|
||
month: 5,
|
||
},
|
||
),
|
||
("2026", DiaryPeriod::Year { year: 2026 }),
|
||
] {
|
||
let uri = Url::from_file_path(format!("/wiki/diary/{stem}.wiki")).unwrap();
|
||
let got = diary_period_for_uri(&uri, Some(&root), Some("diary"));
|
||
assert_eq!(got, Some(expected), "stem {stem}");
|
||
}
|
||
}
|