use super::*;
fn cand(insert: &str, kind: CandidateKind) -> CompletionCandidate {
CompletionCandidate {
insert: insert.to_string(),
label: insert.to_string(),
rel_path: format!("{insert}.x"),
kind,
}
}
fn cand_at(insert: &str, rel_path: &str, kind: CandidateKind) -> CompletionCandidate {
CompletionCandidate {
insert: insert.to_string(),
label: insert.to_string(),
rel_path: rel_path.to_string(),
kind,
}
}
#[test]
fn empty_prefix_returns_all_candidates() {
let cands = vec![
cand("about", CandidateKind::Page),
cand("photo.png", CandidateKind::Asset),
];
let ranked = rank_completions("", &cands, false, "");
assert_eq!(ranked.len(), 2);
assert_eq!(cands[ranked[0]].kind, CandidateKind::Page);
assert_eq!(cands[ranked[1]].kind, CandidateKind::Asset);
}
#[test]
fn prefix_filters_and_starts_with_ranks_first() {
let cands = vec![
cand("changelog", CandidateKind::Page), cand("angle", CandidateKind::Page), cand("about", CandidateKind::Page), ];
let ranked = rank_completions("ang", &cands, false, "");
assert_eq!(ranked.len(), 2);
assert_eq!(cands[ranked[0]].insert, "angle");
assert_eq!(cands[ranked[1]].insert, "changelog");
}
#[test]
fn case_insensitive_match() {
let cands = vec![cand("README", CandidateKind::Page)];
assert_eq!(rank_completions("read", &cands, false, "").len(), 1);
}
#[test]
fn embed_ranks_assets_before_pages() {
let cands = vec![
cand("hero", CandidateKind::Page),
cand("hero.png", CandidateKind::Asset),
];
let ranked = rank_completions("hero", &cands, true, "");
assert_eq!(cands[ranked[0]].kind, CandidateKind::Asset);
let ranked2 = rank_completions("hero", &cands, false, "");
assert_eq!(cands[ranked2[0]].kind, CandidateKind::Page);
}
#[test]
fn cjk_prefix_matches() {
let cands = vec![
cand("刘果的笔记", CandidateKind::Page),
cand("about", CandidateKind::Page),
];
let ranked = rank_completions("刘果", &cands, false, "");
assert_eq!(ranked.len(), 1);
assert_eq!(cands[ranked[0]].insert, "刘果的笔记");
}
#[test]
fn heading_candidates_rank_starts_with_before_contains() {
let cands = vec![
cand("Background and context", CandidateKind::Heading), cand("Context", CandidateKind::Heading), cand("Conclusion", CandidateKind::Heading), ];
let ranked = rank_completions("context", &cands, false, "");
assert_eq!(ranked.len(), 2);
assert_eq!(cands[ranked[0]].insert, "Context");
assert_eq!(cands[ranked[1]].insert, "Background and context");
}
#[test]
fn heading_embed_flag_does_not_reorder_headings() {
let cands = vec![
cand("bbbb", CandidateKind::Heading),
cand("aaaa", CandidateKind::Heading),
];
let with_embed = rank_completions("", &cands, true, "");
let without = rank_completions("", &cands, false, "");
assert_eq!(with_embed, without);
assert_eq!(cands[with_embed[0]].insert, "aaaa");
}
#[test]
fn nfc_and_nfd_forms_match_each_other() {
let nfc = "caf\u{00e9}"; let nfd = "cafe\u{0301}"; assert_ne!(nfc, nfd, "precondition: the two byte-forms differ");
let cands = vec![cand(nfd, CandidateKind::Page)];
assert_eq!(rank_completions(nfc, &cands, false, "").len(), 1);
let cands2 = vec![cand(nfc, CandidateKind::Page)];
assert_eq!(rank_completions(nfd, &cands2, false, "").len(), 1);
}
#[test]
fn same_language_tree_ranks_before_other_language() {
let cands = vec![
cand_at("guide", "en/guide.md", CandidateKind::Page),
cand_at("guide", "zh-hans/guide.md", CandidateKind::Page),
];
let ranked = rank_completions("guide", &cands, false, "zh-hans/about.md");
assert_eq!(cands[ranked[0]].rel_path, "zh-hans/guide.md");
}
#[test]
fn closer_in_tree_ranks_before_farther_in_same_language() {
let cands = vec![
cand_at("note", "zh-hans/note.md", CandidateKind::Page),
cand_at("note", "zh-hans/游记/note.md", CandidateKind::Page),
];
let ranked = rank_completions("note", &cands, false, "zh-hans/游记/index.md");
assert_eq!(cands[ranked[0]].rel_path, "zh-hans/游记/note.md");
}
#[test]
fn match_quality_outranks_language() {
let cands = vec![
cand_at("周报report", "zh-hans/周报report.md", CandidateKind::Page), cand_at("report-en", "en/report-en.md", CandidateKind::Page), ];
let ranked = rank_completions("report", &cands, false, "zh-hans/about.md");
assert_eq!(cands[ranked[0]].rel_path, "en/report-en.md");
}
#[test]
fn root_source_prefers_root_candidate_over_language_tree() {
let cands = vec![
cand_at("about", "zh-hans/about.md", CandidateKind::Page),
cand_at("about", "about.md", CandidateKind::Page),
];
let ranked = rank_completions("about", &cands, false, "index.md");
assert_eq!(cands[ranked[0]].rel_path, "about.md");
}
#[test]
fn ties_break_by_path_deterministically() {
let cands = vec![
cand_at("guide", "zh-hans/b/guide.md", CandidateKind::Page),
cand_at("guide", "zh-hans/a/guide.md", CandidateKind::Page),
];
let ranked = rank_completions("guide", &cands, false, "zh-hans/x.md");
assert_eq!(cands[ranked[0]].rel_path, "zh-hans/a/guide.md");
}
#[test]
fn path_query_matches_across_an_omitted_directory() {
let cands = vec![cand_at(
"頭像-李柏萱.png",
"關於/assets/頭像-李柏萱.png",
CandidateKind::Asset,
)];
let ranked = rank_completions("關於/頭像-李", &cands, true, "在場.md");
assert_eq!(ranked.len(), 1);
}
#[test]
fn path_query_rejects_a_different_directory() {
let cands = vec![cand_at(
"頭像-李柏萱.png",
"關於/assets/頭像-李柏萱.png",
CandidateKind::Asset,
)];
assert!(rank_completions("獎項/頭像-李", &cands, true, "在場.md").is_empty());
}
#[test]
fn partial_directory_segment_still_lists_the_subtree() {
let cands = vec![
cand_at("f99cc68b.png", "關於/assets/f99cc68b.png", CandidateKind::Asset),
cand_at("assembly.png", "關於/assembly.png", CandidateKind::Asset),
];
let ranked = rank_completions("關於/ass", &cands, true, "在場.md");
assert_eq!(ranked.len(), 2);
assert_eq!(cands[ranked[0]].insert, "assembly.png");
}
#[test]
fn bare_query_ordering_is_unchanged_by_path_keys() {
let cands = vec![
cand_at("changelog", "en/notes/changelog.md", CandidateKind::Page),
cand_at("angle", "en/angle.md", CandidateKind::Page),
cand_at("about", "en/about.md", CandidateKind::Page),
];
let ranked = rank_completions("ang", &cands, false, "en/index.md");
assert_eq!(ranked.len(), 2);
assert_eq!(cands[ranked[0]].insert, "angle");
assert_eq!(cands[ranked[1]].insert, "changelog");
}
#[test]
fn contiguous_suffix_dir_match_outranks_a_gapped_one() {
let cands = vec![
cand_at("x.png", "關於/deep/assets/x.png", CandidateKind::Asset), cand_at("x.png", "assets/x.png", CandidateKind::Asset), ];
let ranked = rank_completions("assets/x", &cands, true, "在場.md");
assert_eq!(cands[ranked[0]].rel_path, "assets/x.png");
}
#[test]
fn starts_with_uses_the_final_segment_for_a_path_query() {
let cands = vec![
cand_at("changelog", "dir/changelog.md", CandidateKind::Page),
cand_at("angle", "dir/angle.md", CandidateKind::Page),
];
let ranked = rank_completions("dir/ang", &cands, false, "index.md");
assert_eq!(cands[ranked[0]].insert, "angle");
}
#[test]
fn heading_candidates_ignore_slashes_in_the_query() {
let cands = vec![cand_at("Intro/Setup", "H2", CandidateKind::Heading)];
let ranked = rank_completions("Intro/Set", &cands, false, "notes.md");
assert_eq!(ranked.len(), 1);
assert_eq!(insert_for("Intro/Set", &cands[0], "notes.md"), None);
}
#[test]
fn trailing_slash_lists_only_that_directory() {
let cands = vec![
cand_at("a.png", "關於/assets/a.png", CandidateKind::Asset),
cand_at("b.png", "獎項/b.png", CandidateKind::Asset),
];
let ranked = rank_completions("關於/", &cands, true, "在場.md");
assert_eq!(ranked.len(), 1);
assert_eq!(cands[ranked[0]].rel_path, "關於/assets/a.png");
}
#[test]
fn insert_for_always_round_trips_through_the_resolver() {
use crate::resolve::asset_class::{
resolve_asset_ref, AssetProvenance, AssetResolution, FakeAssetIndex,
};
let paths = [
"關於/assets/頭像-李柏萱.png",
"關於/歷季得獎者.md",
"關於/近照.png",
"assets/首頁hero.png",
"關於/assets/首頁hero.png",
"首頁.png",
];
let idx = FakeAssetIndex::new(&paths);
for from_rel in ["在場.md", "關於/歷季得獎者.md"] {
for rel in [
"關於/assets/頭像-李柏萱.png",
"關於/近照.png",
"assets/首頁hero.png",
"首頁.png",
] {
let c = cand_at(
rel.rsplit('/').next().unwrap(),
rel,
CandidateKind::Asset,
);
let emitted = insert_for("關於/x", &c, from_rel)
.unwrap_or_else(|| panic!("path-qualified query must emit a form for {rel}"));
assert_eq!(
resolve_asset_ref(&emitted, from_rel, &idx),
AssetResolution::Resolved {
root_rel: rel.to_string(),
provenance: AssetProvenance::Literal,
},
"from {from_rel}, candidate {rel}, emitted {emitted}"
);
}
}
let mut b = crate::content_graph::ContentGraphBuilder::new();
b.add_file("notes/ideas.md", "notes/ideas");
b.add_file("關於/歷季得獎者.md", "關於/歷季得獎者");
let graph = b.build();
for from_rel in ["在場.md", "關於/歷季得獎者.md"] {
let c = cand_at("ideas", "notes/ideas.md", CandidateKind::Page);
let emitted = insert_for("notes/id", &c, from_rel).unwrap();
assert_eq!(emitted, "notes/ideas");
assert_eq!(graph.resolve_path(&emitted, from_rel).as_deref(), Some("notes/ideas.md"));
}
}
#[test]
fn insert_for_is_none_without_a_separator() {
let c = cand_at("x.png", "關於/assets/x.png", CandidateKind::Asset);
assert_eq!(insert_for("x", &c, "關於/y.md"), None);
}