Files
Marcos Paulo 5e8020aa38
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
feat: Obsidian basics, editor previews, graph rework, query blocks, stacked tabs, pop-out windows
Editor previews (colour swatches, arrows, embeds), the 18 obsidian-basics tasks (find and replace
with regex, back/forward, auto-pair, footnotes, folding, search operators, split view, PDF/audio/
video embeds, spelling, drag and drop, pinned tabs, settings) and round 2: ```query blocks, stacked
tabs, pop-out windows, tasks ticked inside embeds, text around an embed, embeds that follow edits,
block menu shortcut. New crates: notescraft-regex (linear-time engine) and notescraft-syntax.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 11:21:19 -03:00

171 lines
7.4 KiB
Rust

//! Lazy open: names first, notes indexed in time-boxed steps, the same index in the end.
mod common;
use std::time::Duration;
use common::{linked_vault, seed, temp_vault};
use notescraft_engine::{IndexProgress, Registry, Session, register_core};
use serde_json::Value;
const GENEROUS: Duration = Duration::from_secs(30);
fn run_to_end(session: &mut Session) -> IndexProgress {
let mut progress = session.index_progress();
while !progress.finished {
progress = session.index_step(GENEROUS);
}
progress
}
fn run(session: &mut Session, id: &str) {
let mut registry = Registry::new();
register_core(&mut registry).expect("register");
registry.run(id, session, &Value::Null).unwrap_or_else(|error| panic!("{id}: {error:?}"));
}
#[test]
fn a_lazy_session_has_the_file_list_but_no_parsed_notes() {
let vault = linked_vault("lazy_empty", 5);
seed(vault.path(), &[("pic.png", "x")]);
let session = Session::open_lazy(vault.path()).expect("lazy");
let progress = session.index_progress();
assert_eq!((progress.total, progress.done, progress.finished), (5, 0, false));
assert!(session.index().is_empty(), "no note is parsed before the first step");
assert!(session.index().files().contains(&"pic.png"), "attachments are registered at open");
}
#[test]
fn a_zero_budget_still_indexes_one_note_per_step() {
let vault = linked_vault("lazy_budget", 4);
let mut session = Session::open_lazy(vault.path()).expect("lazy");
for expected in 1..=3 {
let progress = session.index_step(Duration::ZERO);
assert_eq!((progress.done, progress.finished), (expected, false));
assert_eq!(session.index().len(), expected);
}
let progress = session.index_step(Duration::ZERO);
assert_eq!((progress.done, progress.total), (4, 4));
assert!(progress.finished);
}
#[test]
fn steps_after_the_end_change_nothing() {
let vault = linked_vault("lazy_idempotent", 3);
let mut session = Session::open_lazy(vault.path()).expect("lazy");
let end = run_to_end(&mut session);
assert_eq!(session.index_step(GENEROUS), end);
assert_eq!(session.index_step(Duration::ZERO), end);
}
#[test]
fn lazy_open_plus_all_steps_builds_the_same_index_as_eager_open() {
let vault = linked_vault("lazy_same", 12);
seed(
vault.path(),
&[("sub/Extra.md", "---\naliases: [Spark]\n---\n# H\n[[Missing]] [[spark]] #deep/tag zebra word\n"), ("pic.png", "x"), ("notes/bad-utf8.md", "ok")],
);
std::fs::write(vault.path().join("notes/bad-utf8.md"), [0xFF, 0xFE, 0x00]).expect("write bad bytes");
let eager = Session::open(vault.path()).expect("eager");
drop(eager);
let mut lazy = Session::open_lazy(vault.path()).expect("lazy");
let progress = run_to_end(&mut lazy);
let eager = Session::open_read_only(vault.path()).expect("eager again");
assert_eq!(progress.done, progress.total);
assert_eq!(lazy.index().notes(), eager.index().notes());
assert_eq!(lazy.index().files(), eager.index().files());
assert_eq!(lazy.index().tags().len(), eager.index().tags().len());
assert_eq!(lazy.index().unresolved().len(), eager.index().unresolved().len());
assert_eq!(lazy.index().backlinks("note-03.md").len(), eager.index().backlinks("note-03.md").len());
assert_eq!(lazy.diagnostics().len(), eager.diagnostics().len(), "the unreadable note is a diagnostic in both");
let hits = |session: &Session| session.search("zebra", 10).expect("search").into_iter().map(|hit| hit.path).collect::<Vec<_>>();
assert_eq!(hits(&lazy), hits(&eager));
assert_eq!(hits(&lazy), vec!["sub/Extra.md".to_owned()]);
}
#[test]
fn an_empty_vault_finishes_on_the_first_step() {
let vault = temp_vault("lazy_empty_vault");
let mut session = Session::open_lazy(vault.path()).expect("lazy");
let progress = session.index_step(GENEROUS);
assert_eq!((progress.total, progress.done, progress.finished), (0, 0, true));
}
#[test]
fn pause_stops_the_steps_and_resume_continues_without_rereading() {
let vault = linked_vault("lazy_pause", 6);
let mut session = Session::open_lazy(vault.path()).expect("lazy");
session.index_step(Duration::ZERO);
run(&mut session, "index.pause");
let paused = session.index_step(GENEROUS);
assert!(paused.paused && !paused.finished);
assert_eq!(paused.done, 1);
assert_eq!(session.index().len(), 1);
run(&mut session, "index.resume");
let end = run_to_end(&mut session);
assert_eq!((end.done, end.total, end.paused), (6, 6, false));
assert_eq!(session.index().len(), 6);
}
#[test]
fn the_newest_notes_are_read_first() {
let vault = temp_vault("lazy_order");
seed(vault.path(), &[("a-old.md", "old"), ("b-new.md", "new"), ("c-mid.md", "mid")]);
let now = std::time::SystemTime::now();
for (name, age) in [("a-old.md", 300), ("c-mid.md", 200), ("b-new.md", 100)] {
let file = std::fs::File::options().write(true).open(vault.path().join(name)).expect("open");
file.set_modified(now - Duration::from_secs(age)).expect("set mtime");
}
let mut session = Session::open_lazy(vault.path()).expect("lazy");
session.index_step(Duration::ZERO);
assert_eq!(session.index().notes(), vec!["b-new.md"]);
session.index_step(Duration::ZERO);
assert_eq!(session.index().notes(), vec!["b-new.md", "c-mid.md"]);
}
#[test]
fn opening_a_buffer_indexes_that_note_at_once() {
let vault = linked_vault("lazy_open_buffer", 5);
let mut session = Session::open_lazy(vault.path()).expect("lazy");
session.open_buffer("note-03.md").expect("buffer");
assert!(session.index().note("note-03.md").is_some());
let progress = session.index_progress();
assert_eq!((progress.done, progress.finished), (1, false));
let end = run_to_end(&mut session);
assert_eq!((end.done, end.total), (5, 5), "the note is not counted twice");
assert_eq!(session.index().len(), 5);
}
#[test]
fn a_note_deleted_before_its_turn_is_skipped_quietly() {
let vault = linked_vault("lazy_deleted", 3);
let mut session = Session::open_lazy(vault.path()).expect("lazy");
std::fs::remove_file(vault.path().join("note-01.md")).expect("remove");
let end = run_to_end(&mut session);
assert_eq!(end.done, 3);
assert!(session.diagnostics().is_empty(), "{:?}", session.diagnostics());
assert!(session.index().note("note-01.md").is_none());
}
#[test]
fn renames_and_moves_are_refused_until_the_index_is_complete() {
let vault = linked_vault("lazy_gate", 4);
seed(vault.path(), &[("dir/inner.md", "[[note-00]]")]);
let mut session = Session::open_lazy(vault.path()).expect("lazy");
let error = session.rename_note("note-00.md", "renamed.md", true).expect_err("refused");
assert_eq!(error.code(), "indexing");
assert!(error.to_string().contains("still indexing"), "{error}");
assert_eq!(session.rename_folder("dir", "moved", true).expect_err("refused").code(), "indexing");
assert!(vault.path().join("note-00.md").exists(), "nothing was renamed");
run_to_end(&mut session);
session.rename_note("note-00.md", "renamed.md", true).expect("rename after the scan");
session.rename_folder("dir", "moved", true).expect("move after the scan");
}
#[test]
fn search_reports_partial_results_until_the_scan_ends() {
let vault = linked_vault("lazy_partial", 3);
let mut session = Session::open_lazy(vault.path()).expect("lazy");
assert!(session.search_partial());
run_to_end(&mut session);
assert!(!session.search_partial());
}