Files
Marcos Paulo 15d7f5e873 feat: editor blocks and lint, vault knowledge, calendar plugin, macOS menu
- notescraft-lint (rules, safe fixes, learned style) and notescraft-blocks (block model and edits); engine lint.* and block.* commands with CLI and MCP parity
- Lint tab, underlines, slash menu with preview in Source and Visual, block grip and menu, Settings > Editor and lint, Vim block keys
- Visual view: tables, note embeds, %% comments, wrapping property chips
- Vault knowledge (Related tab, assistant sources, Settings) and calendar plugin
- Sidebar top padding fixed, dot grid follows PaperCraft spacing, right-card tabs no longer cover the collapse button

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 12:36:00 -03:00

129 lines
5.3 KiB
Rust

//! Quick-add phrases. Today is Saturday 2026-10-17.
#![allow(clippy::unwrap_used)]
use notescraft_calendar::{Error, Event, parse_quick};
use notescraft_templates::Date;
fn today() -> Date {
Date::new(2026, 10, 17).unwrap()
}
fn ok(text: &str) -> Event {
parse_quick(text, today()).unwrap_or_else(|e| panic!("{text:?}: {e}"))
}
fn when(text: &str) -> (String, String) {
let event = ok(text);
(event.start.to_string(), event.end.map(|e| e.to_string()).unwrap_or_default())
}
fn failing_part(text: &str) -> String {
match parse_quick(text, today()) {
Err(Error::QuickAdd { part, .. }) => part,
other => panic!("{text:?} should fail, got {other:?}"),
}
}
#[test]
fn a_bare_title_is_an_all_day_event_today() {
let event = ok("Buy milk");
assert_eq!((event.title.as_str(), event.start.to_string().as_str()), ("Buy milk", "2026-10-17"));
assert!(event.end.is_none() && event.repeat.is_none() && event.reminders.is_empty());
}
#[test]
fn days() {
assert_eq!(when("Call tomorrow").0, "2026-10-18");
assert_eq!(when("Call today").0, "2026-10-17");
assert_eq!(when("Call monday").0, "2026-10-19");
assert_eq!(when("Call saturday").0, "2026-10-17");
assert_eq!(when("Call next saturday").0, "2026-10-24");
assert_eq!(when("Call on Friday").0, "2026-10-23");
assert_eq!(when("Call 2026-12-25").0, "2026-12-25");
assert_eq!(when("Call oct 25").0, "2026-10-25");
assert_eq!(when("Call 25th October").0, "2026-10-25");
assert_eq!(when("Call on march 3").0, "2027-03-03");
assert_eq!(when("Call 3 jan").0, "2027-01-03");
assert_eq!(ok("Call on").title, "Call on");
assert_eq!(ok("May the force").title, "May the force");
}
#[test]
fn times_ranges_and_lengths() {
assert_eq!(when("Lunch tomorrow at 12:30"), ("2026-10-18T12:30".into(), String::new()));
assert_eq!(when("Lunch at 9").0, "2026-10-17T09:00");
assert_eq!(when("Lunch 3pm").0, "2026-10-17T15:00");
assert_eq!(when("Lunch 12am").0, "2026-10-17T00:00");
assert_eq!(when("Lunch noon").0, "2026-10-17T12:00");
assert_eq!(when("Lunch 14:30").0, "2026-10-17T14:30");
assert_eq!(when("Lunch at 9 for 1h"), ("2026-10-17T09:00".into(), "2026-10-17T10:00".into()));
assert_eq!(when("Lunch at 9 for 90 min").1, "2026-10-17T10:30");
assert_eq!(when("Lunch at 23:30 for 2 hours").1, "2026-10-18T01:30");
assert_eq!(when("Meet 9-10:30"), ("2026-10-17T09:00".into(), "2026-10-17T10:30".into()));
assert_eq!(when("Meet 9-10am").0, "2026-10-17T09:00");
assert_eq!(when("Meet 11-1pm"), ("2026-10-17T11:00".into(), "2026-10-17T13:00".into()));
assert_eq!(when("Meet from 9 to 10"), ("2026-10-17T09:00".into(), "2026-10-17T10:00".into()));
assert_eq!(ok("Lunch tomorrow at 12:30 for 1h").title, "Lunch");
}
#[test]
fn repeats_reminders_tags_and_calendars() {
assert_eq!(ok("Standup every weekday at 9:30").repeat.unwrap(), "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR");
assert_eq!(ok("Gym every mon,wed").repeat.unwrap(), "FREQ=WEEKLY;BYDAY=MO,WE");
assert_eq!(ok("Trash every 2 weeks").repeat.unwrap(), "FREQ=WEEKLY;INTERVAL=2");
assert_eq!(ok("Water plants every day").repeat.unwrap(), "FREQ=DAILY");
assert_eq!(ok("Rent monthly").repeat.unwrap(), "FREQ=MONTHLY");
assert_eq!(ok("Review every friday").repeat.unwrap(), "FREQ=WEEKLY;BYDAY=FR");
let event = ok("Dentist friday 3pm remind 15 min before remind me 1 hour #health @Personal");
assert_eq!(event.reminders.iter().map(|r| r.minutes_before).collect::<Vec<_>>(), [15, 60]);
assert_eq!((event.tags, event.calendar.as_deref(), event.title.as_str()), (vec!["health".to_owned()], Some("Personal"), "Dentist"));
assert_eq!(ok("Dentist remind 10m").reminders[0].minutes_before, 10);
}
#[test]
fn bad_parts_are_named_and_nothing_is_saved() {
assert_eq!(failing_part("Lunch at 25:00"), "25:00");
assert_eq!(failing_part("Lunch at 9:5"), "9:5");
assert_eq!(failing_part("Lunch 13pm"), "13pm");
assert_eq!(failing_part("Lunch at noonish"), "noonish");
assert_eq!(failing_part("Lunch at"), "at");
assert_eq!(failing_part("Lunch 2026-02-30"), "2026-02-30");
assert_eq!(failing_part("Lunch for banana"), "banana");
assert_eq!(failing_part("Lunch at 9 for 5 parsecs"), "5 parsecs");
assert_eq!(failing_part("Lunch every blue moon"), "every blue moon");
assert_eq!(failing_part("Lunch every 0 days"), "every 0 days");
assert_eq!(failing_part("Lunch tomorrow friday"), "friday");
assert_eq!(failing_part("Lunch at 9 at 10"), "10");
assert_eq!(failing_part("Lunch daily weekly"), "weekly");
assert_eq!(failing_part("Lunch for 1h"), "for");
assert_eq!(failing_part("Lunch 10-9"), "");
assert_eq!(failing_part("Lunch #"), "#");
assert_eq!(failing_part("tomorrow at 9"), "");
assert_eq!(failing_part(""), "");
assert_eq!(failing_part("Lunch for 99999 days"), "99999");
}
#[test]
fn hostile_input_is_an_error_not_a_panic() {
for text in [
"x ".repeat(300).as_str(),
&"word ".repeat(150),
"日本語 を 食べる 明日",
"Lunch at 09:00",
"💥 at 💥",
"Lunch every 4294967296 days",
"a\u{0}b at 9",
"-",
"9-",
"-9",
"at -",
"from to",
"remind",
"remind me",
"next",
"on on on",
] {
let _ = parse_quick(text, today());
}
}