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

109 lines
4.3 KiB
Rust

//! Content search over the files on disk: works with no index, stops on request, bounded.
mod common;
use std::time::{Duration, Instant};
use common::{seed, temp_vault};
use notescraft_engine::{ContentHit, ContentSearch};
fn collect(mut search: ContentSearch) -> Vec<ContentHit> {
let deadline = Instant::now() + Duration::from_secs(30);
let mut hits = Vec::new();
loop {
let poll = search.poll();
hits.extend(poll.hits);
if poll.done {
return hits;
}
assert!(Instant::now() < deadline, "the search never finished");
std::thread::sleep(Duration::from_millis(5));
}
}
fn paths(hits: &[ContentHit]) -> Vec<&str> {
let mut paths: Vec<&str> = hits.iter().map(|hit| hit.path.as_str()).collect();
paths.dedup();
paths.sort_unstable();
paths
}
#[test]
fn finds_notes_with_every_term_on_one_line_ignoring_case() {
let vault = temp_vault("cs_basic");
seed(
vault.path(),
&[
("a.md", "first line\nThe Zebra crossed the road\n"),
("sub/b.md", "zebra only\nroad only\n"),
("c.md", "nothing here"),
("pic.png", "zebra road"),
(".hidden/d.md", "zebra road"),
],
);
let hits = collect(ContentSearch::start(vault.path(), "zebra ROAD", 10).expect("start"));
assert_eq!(paths(&hits), vec!["a.md"]);
assert_eq!((hits[0].line, hits[0].text.as_str()), (2, "The Zebra crossed the road"));
}
#[test]
fn an_empty_query_is_refused() {
let vault = temp_vault("cs_empty");
assert!(ContentSearch::start(vault.path(), " ", 10).is_err());
}
#[test]
fn the_file_limit_stops_the_search() {
let vault = temp_vault("cs_limit");
let files: Vec<(String, String)> = (0..30).map(|n| (format!("n{n:02}.md"), "needle".to_owned())).collect();
let borrowed: Vec<(&str, &str)> = files.iter().map(|(p, t)| (p.as_str(), t.as_str())).collect();
seed(vault.path(), &borrowed);
let hits = collect(ContentSearch::start(vault.path(), "needle", 5).expect("start"));
assert_eq!(paths(&hits).len(), 5);
}
#[test]
fn hostile_files_are_skipped_without_panic() {
let vault = temp_vault("cs_hostile");
seed(vault.path(), &[("ok.md", "needle in a good note\n")]);
std::fs::write(vault.path().join("binary.md"), b"needle\0\0\x01\x02 binary").expect("write");
std::fs::write(vault.path().join("latin1.md"), b"caf\xE9 needle \xFF\xFE\n").expect("write");
let huge = vec![b'x'; 9 * 1024 * 1024];
std::fs::write(vault.path().join("huge.md"), [b"needle ".as_slice(), &huge].concat()).expect("write");
let long_line = format!("needle {}", "y".repeat(100_000));
std::fs::write(vault.path().join("long.md"), long_line).expect("write");
let hits = collect(ContentSearch::start(vault.path(), "needle", 50).expect("start"));
let found = paths(&hits);
assert!(found.contains(&"ok.md") && found.contains(&"latin1.md"), "{found:?}");
assert!(!found.contains(&"binary.md") && !found.contains(&"huge.md"), "{found:?}");
let long = hits.iter().find(|hit| hit.path == "long.md").expect("the long line is found");
assert!(long.text.chars().count() <= 241, "the snippet is cut: {}", long.text.len());
}
#[test]
fn cancel_stops_the_search_and_drops_late_hits() {
let vault = temp_vault("cs_cancel");
let files: Vec<(String, String)> = (0..400).map(|n| (format!("d{}/n{n}.md", n % 20), "needle\n".to_owned())).collect();
let borrowed: Vec<(&str, &str)> = files.iter().map(|(p, t)| (p.as_str(), t.as_str())).collect();
seed(vault.path(), &borrowed);
let mut search = ContentSearch::start(vault.path(), "needle", 1000).expect("start");
search.cancel();
let deadline = Instant::now() + Duration::from_secs(30);
loop {
let poll = search.poll();
assert!(poll.hits.is_empty() || !poll.done || poll.hits.len() < 400, "cancelled search kept going");
if poll.done {
break;
}
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(5));
}
assert!(search.poll().hits.is_empty(), "nothing arrives after done");
}
#[test]
fn dropping_a_search_cancels_it() {
let vault = temp_vault("cs_drop");
seed(vault.path(), &[("a.md", "needle")]);
let search = ContentSearch::start(vault.path(), "needle", 10).expect("start");
drop(search);
}