use crate::store::SymbolRow;
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub(crate) struct Feature {
pub name: &'static str,
pub value: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Scored {
pub total: f64,
pub features: Vec<Feature>,
}
pub(crate) fn match_quality(features: &[Feature]) -> f64 {
for f in features {
match f.name {
"exact" => return 1.0,
"prefix" => return 0.9,
"wildcard" => return 0.7,
"fuzzy" => return (0.30 + 0.35 * (f.value / 600.0)).clamp(0.30, 0.65),
_ => {}
}
}
0.25 }
pub(crate) fn confidence(score: f64, quality: f64, best_other: Option<f64>) -> f64 {
let lead = match best_other {
None => 1.0,
Some(_) if score <= 0.0 => 0.5,
Some(other) => (0.5 + 3.0 * (score - other) / score).clamp(0.0, 1.0),
};
((quality * lead) * 100.0).round() / 100.0
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub(crate) struct Boosts {
pub learned: f64,
pub recency: f64,
pub branch: f64,
}
pub(crate) fn score(
query: &str,
cand: &SymbolRow,
current_repo_id: Option<i64>,
boosts: Boosts,
) -> Option<Scored> {
let (leaf, qualifier) = parse_qualified(query);
let q = leaf.to_ascii_lowercase();
let name_lower = cand.name.to_ascii_lowercase();
let mut features = Vec::new();
let wildcard = has_wildcard(&q);
let name_matched = if wildcard {
if let Some(s) = wildcard_score(&q, &cand.name) {
features.push(Feature {
name: "wildcard",
value: s.min(600.0),
});
true
} else {
false
}
} else if name_lower == q {
features.push(Feature {
name: "exact",
value: 1000.0,
});
if leaf != q && cand.name == leaf {
features.push(Feature {
name: "case",
value: CASE_MATCH,
});
}
true
} else if name_lower.starts_with(&q) {
let tail = cand.name.chars().count().saturating_sub(q.chars().count());
features.push(Feature {
name: "prefix",
value: 700.0 - (tail as f64).min(100.0),
});
true
} else if let Some(s) = subsequence_score(&q, &cand.name) {
features.push(Feature {
name: "fuzzy",
value: s.min(600.0),
});
true
} else {
false
};
let stem = path_stem(&cand.file);
let path_match = if wildcard {
wildcard_score(&q, stem)
} else {
subsequence_score(&q, stem)
};
if name_matched {
if let Some(ps) = path_match {
features.push(Feature {
name: "path",
value: (ps * 0.2).min(50.0),
});
}
} else {
match path_match {
Some(ps)
if matches!(
cand.kind.as_str(),
"class" | "module" | "struct" | "enum" | "trait"
) =>
{
features.push(Feature {
name: "path",
value: (ps * 0.6).min(300.0),
});
}
_ => return None,
}
}
if matches!(
cand.visibility.as_deref(),
Some("private") | Some("protected")
) {
features.push(Feature {
name: "private",
value: -15.0,
});
}
let kind = match cand.kind.as_str() {
"class" | "struct" | "trait" => 15.0,
"module" | "enum" => 12.0,
_ => 0.0,
};
if kind != 0.0 {
features.push(Feature {
name: "kind",
value: kind,
});
}
if let Some(qual) = qualifier
&& let Some(b) = parent_boost(qual, cand.parent.as_deref())
{
features.push(Feature {
name: "parent",
value: b,
});
}
if let Some(cur) = current_repo_id
&& cur == cand.repository_id
{
features.push(Feature {
name: "current_repo",
value: 200.0,
});
}
if boosts.learned > 0.0 {
features.push(Feature {
name: "learned",
value: boosts.learned,
});
}
if boosts.recency > 0.0 {
features.push(Feature {
name: "recency",
value: boosts.recency,
});
}
if boosts.branch > 0.0 {
features.push(Feature {
name: "branch",
value: boosts.branch,
});
}
let total = features.iter().map(|f| f.value).sum();
Some(Scored { total, features })
}
const MAX_NONBOUNDARY_GAP: usize = 2;
const CASE_MATCH: f64 = 150.0;
const GAP_PENALTY: f64 = 3.0;
struct Alignment {
score: f64,
positions: Vec<usize>,
}
fn align(query: &str, name: &str) -> Option<Alignment> {
let q: Vec<char> = query
.chars()
.filter(|c| c.is_alphanumeric())
.map(|c| c.to_ascii_lowercase())
.collect();
if q.is_empty() {
return None;
}
let mut qi = 0;
for c in name.chars() {
if qi < q.len() && c.to_ascii_lowercase() == q[qi] {
qi += 1;
}
}
if qi < q.len() {
return None;
}
let chars: Vec<char> = name.chars().collect();
let n = chars.len();
let lower: Vec<char> = chars.iter().map(|c| c.to_ascii_lowercase()).collect();
let boundary = boundaries(&chars);
let mut bnd_prefix = vec![0usize; n + 1];
for i in 0..n {
bnd_prefix[i + 1] = bnd_prefix[i] + boundary[i] as usize;
}
let mut table: Vec<Vec<Option<(f64, usize)>>> = vec![vec![None; n]; q.len()];
for (i, &c) in lower.iter().enumerate() {
if c == q[0] {
let mut s = 10.0;
if boundary[i] {
s += 15.0;
}
if i == 0 {
s += 20.0; }
table[0][i] = Some((s, i));
}
}
for qi in 1..q.len() {
for i in qi..n {
if lower[i] != q[qi] {
continue;
}
let base = 10.0 + if boundary[i] { 15.0 } else { 0.0 };
let j_start = if boundary[i] {
qi - 1
} else {
(qi - 1).max(i.saturating_sub(MAX_NONBOUNDARY_GAP + 1))
};
let mut best: Option<(f64, usize)> = None;
let prev_row = &table[qi - 1];
for (j, cell) in prev_row.iter().enumerate().take(i).skip(j_start) {
let Some((pscore, _)) = cell else {
continue;
};
let trans = if j + 1 == i {
10.0 } else {
let gap = i - j - 1;
let crossed_word = bnd_prefix[i] - bnd_prefix[j + 1] > 0;
if boundary[i] {
if crossed_word {
continue;
}
} else if gap > MAX_NONBOUNDARY_GAP || crossed_word {
continue;
}
-(gap as f64) * GAP_PENALTY
};
let cand = pscore + trans;
if best.is_none_or(|(b, _)| cand > b) {
best = Some((cand, j));
}
}
if let Some((bscore, j)) = best {
table[qi][i] = Some((bscore + base, j));
}
}
}
let last = q.len() - 1;
let (mut pos, score) = (0..n)
.filter_map(|i| table[last][i].map(|(s, _)| (i, s)))
.max_by(|a, b| a.1.total_cmp(&b.1))?;
let mut positions = Vec::with_capacity(q.len());
for qi in (0..q.len()).rev() {
positions.push(pos);
pos = table[qi][pos].expect("backtrack hits a filled cell").1;
}
positions.reverse();
Some(Alignment {
score: score.max(0.0),
positions,
})
}
pub(crate) fn match_positions(query: &str, name: &str) -> Vec<usize> {
let (leaf, _) = parse_qualified(query);
if has_wildcard(leaf) {
return glob_positions(leaf, name).unwrap_or_default();
}
let positions = align(leaf, name).map(|a| a.positions).unwrap_or_default();
contiguous_highlight(positions, name)
}
pub(crate) fn parse_qualified(query: &str) -> (&str, Option<&str>) {
let sep = query
.rmatch_indices("::")
.map(|(i, _)| (i, 2usize))
.chain(query.rmatch_indices('#').map(|(i, _)| (i, 1usize)))
.max_by_key(|&(i, _)| i);
match sep {
Some((i, len)) if i > 0 && i + len < query.len() => (&query[i + len..], Some(&query[..i])),
_ => (query, None),
}
}
fn segments(s: &str) -> Vec<String> {
s.split("::")
.flat_map(|p| p.split('#'))
.filter(|p| !p.is_empty())
.map(|p| p.to_ascii_lowercase())
.collect()
}
fn parent_boost(qualifier: &str, parent: Option<&str>) -> Option<f64> {
let p = segments(parent?);
let q = segments(qualifier);
if q.is_empty() || q.len() > p.len() {
return None;
}
let off = p.len() - q.len();
(p[off..] == q[..]).then(|| (120.0 + 60.0 * q.len() as f64).min(300.0))
}
fn contiguous_highlight(positions: Vec<usize>, name: &str) -> Vec<usize> {
if positions.is_empty() {
return positions;
}
let boundary = boundaries(&name.chars().collect::<Vec<_>>());
let mut out = Vec::with_capacity(positions.len());
let mut i = 0;
while i < positions.len() {
let mut j = i;
while j + 1 < positions.len() && positions[j + 1] == positions[j] + 1 {
j += 1;
}
if j > i {
out.extend_from_slice(&positions[i..=j]); } else if boundary[positions[i]] {
out.push(positions[i]); }
i = j + 1;
}
out
}
fn subsequence_score(query: &str, name: &str) -> Option<f64> {
align(query, name).map(|a| a.score)
}
pub(crate) fn has_wildcard(query: &str) -> bool {
query.contains(['*', '?', '.'])
}
pub(crate) fn strip_wildcards(query: &str) -> String {
query
.chars()
.filter(|c| !matches!(c, '*' | '?' | '.'))
.collect()
}
enum Glob {
Lit(char), Any, Star, }
fn compile_glob(query: &str) -> Vec<Glob> {
query
.chars()
.filter_map(|c| match c {
'*' => Some(Glob::Star),
'?' | '.' => Some(Glob::Any),
c if c.is_alphanumeric() => Some(Glob::Lit(c.to_ascii_lowercase())),
_ => None,
})
.collect()
}
fn glob_positions(query: &str, name: &str) -> Option<Vec<usize>> {
let mut toks = vec![Glob::Star];
toks.extend(compile_glob(query));
toks.push(Glob::Star);
let lower: Vec<char> = name.chars().map(|c| c.to_ascii_lowercase()).collect();
let mut ti = 0;
let mut ni = 0;
let mut positions: Vec<usize> = Vec::new();
let mut star: Option<(usize, usize, usize)> = None;
while ni < lower.len() {
match toks.get(ti) {
Some(Glob::Lit(c)) if lower[ni] == *c => {
positions.push(ni);
ti += 1;
ni += 1;
}
Some(Glob::Any) => {
ti += 1;
ni += 1;
}
Some(Glob::Star) => {
star = Some((ti + 1, ni, positions.len()));
ti += 1;
}
_ => match star {
Some((sti, sni, plen)) => {
ti = sti;
ni = sni + 1;
star = Some((sti, sni + 1, plen));
positions.truncate(plen);
}
None => return None,
},
}
}
while matches!(toks.get(ti), Some(Glob::Star)) {
ti += 1;
}
(ti == toks.len()).then_some(positions)
}
fn wildcard_score(query: &str, name: &str) -> Option<f64> {
let positions = glob_positions(query, name)?;
if positions.is_empty() {
return None;
}
let chars: Vec<char> = name.chars().collect();
let boundary = boundaries(&chars);
let mut score = 0.0;
let mut prev: Option<usize> = None;
for &i in &positions {
score += 10.0;
if boundary[i] {
score += 15.0;
}
match prev {
Some(p) if p + 1 == i => score += 10.0, None if i == 0 => score += 20.0, _ => {}
}
prev = Some(i);
}
Some(score)
}
pub(crate) fn path_stem(path: &str) -> &str {
let base = path.rsplit(['/', '\\']).next().unwrap_or(path);
match base.rfind('.') {
Some(i) if i > 0 => &base[..i],
_ => base,
}
}
fn boundaries(chars: &[char]) -> Vec<bool> {
let mut out = vec![false; chars.len()];
for i in 0..chars.len() {
let c = chars[i];
out[i] = if i == 0 {
true
} else {
let prev = chars[i - 1];
!prev.is_alphanumeric()
|| (c.is_uppercase() && prev.is_lowercase())
|| (c.is_uppercase()
&& prev.is_uppercase()
&& chars.get(i + 1).is_some_and(|n| n.is_lowercase()))
};
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn row(name: &str, kind: &str, repo: i64) -> SymbolRow {
SymbolRow {
name: name.into(),
kind: kind.into(),
language: "ruby".into(),
file: "f.rb".into(),
line: 1,
end_line: Some(1),
parent: None,
repository_id: repo,
repo_identity: "r".into(),
mtime: None,
git_ts: None,
visibility: None,
}
}
fn total(query: &str, name: &str) -> Option<f64> {
score(query, &row(name, "class", 1), None, Boosts::default()).map(|s| s.total)
}
#[test]
fn a_typed_capital_picks_the_matching_case() {
let upper = total("Symbol", "Symbol").unwrap();
let lower = total("Symbol", "symbol").unwrap();
assert!(upper > lower, "{upper} > {lower}");
assert!(upper - lower > 120.0, "margin {} too small", upper - lower);
}
#[test]
fn a_lowercase_query_stays_case_agnostic() {
assert_eq!(total("symbol", "symbol"), total("symbol", "Symbol"));
assert_eq!(total("user", "User"), total("user", "user"));
}
#[test]
fn private_ranks_below_public_on_an_equal_match() {
let mut public = row("save", "method", 1);
public.visibility = Some("public".into());
let mut private = row("save", "method", 1);
private.visibility = Some("private".into());
let unknown = row("save", "method", 1);
let pub_score = score("save", &public, None, Boosts::default()).unwrap();
let priv_score = score("save", &private, None, Boosts::default()).unwrap();
let unk_score = score("save", &unknown, None, Boosts::default()).unwrap();
assert!(pub_score.total > priv_score.total);
assert_eq!(
pub_score.total, unk_score.total,
"unknown carries no penalty"
);
assert!(priv_score.total > 700.0, "still comfortably above a prefix");
}
#[test]
fn exact_beats_prefix_beats_fuzzy() {
let exact = total("user", "user").unwrap();
let prefix = total("user", "users").unwrap();
let fuzzy = total("usr", "user").unwrap();
assert!(exact > prefix, "{exact} > {prefix}");
assert!(prefix > fuzzy, "{prefix} > {fuzzy}");
}
#[test]
fn abbreviations_match() {
assert!(total("refundproc", "RefundProcessor").is_some());
assert!(total("refproc", "RefundProcessor").is_some());
assert!(total("paymnt", "Payments").is_some());
assert!(total("perf", "perform").is_some());
assert!(total("usr", "User").is_some());
assert!(total("ctrl", "Controller").is_some());
}
#[test]
fn rejects_scattered_midword_matches() {
assert!(total("employeescontroller", "EmployeeXYZsController").is_none());
assert!(total("employeescontroller", "EmployeesController").is_some());
assert!(total("employescontroller", "EmployeesController").is_some());
}
#[test]
fn match_positions_report_what_matched() {
assert_eq!(match_positions("foo", "FooThing"), vec![0, 1, 2]);
assert_eq!(match_positions("ft", "FooThing"), vec![0, 3]); assert_eq!(match_positions("wc", "WidgetController"), vec![0, 6]); assert!(match_positions("xyz", "FooThing").is_empty());
}
#[test]
fn prefers_the_contiguous_run_over_an_earlier_scattered_match() {
assert_eq!(
match_positions("employee", "xxxe_employee"),
vec![5, 6, 7, 8, 9, 10, 11, 12]
);
assert_eq!(
match_positions("controller", "calc_controller"),
(5..15).collect::<Vec<_>>()
);
assert_eq!(
match_positions("widgetcontroller", "WidgetController"),
(0..16).collect::<Vec<_>>()
);
}
#[test]
fn matches_only_span_adjacent_words() {
assert_eq!(
match_positions("employeescontroller", "employees_controller"),
vec![
0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
]
);
assert!(subsequence_score("employees", "employee_x_syy").is_none());
assert!(subsequence_score("rndsvc", "RefundProcessingService").is_none());
assert!(subsequence_score("refproc", "RefundProcessor").is_some());
assert!(subsequence_score("refprocsvc", "RefundProcessingService").is_some());
}
#[test]
fn a_contiguous_match_beats_a_farther_boundary_jump() {
assert_eq!(match_positions("car", "car_r"), vec![0, 1, 2]);
}
#[test]
fn acronyms_highlight_word_initials_across_adjacent_words() {
assert_eq!(match_positions("uc", "UserController"), vec![0, 4]);
assert_eq!(
match_positions("abc", "alpha_bravo_charlie"),
vec![0, 6, 12] );
assert!(subsequence_score("payrollcontroller", "payroll_runs_controller").is_none());
assert!(subsequence_score("apc", "alpha_bravo_charlie").is_none()); }
#[test]
fn a_gap_cannot_cross_a_word_boundary_into_a_mid_word_char() {
assert!(
subsequence_score("employeescontroller", "employee_before_starting_controller")
.is_none()
);
assert!(subsequence_score("employeescontroller", "employees_controller").is_some());
assert!(subsequence_score("usr", "user").is_some());
assert!(subsequence_score("cfg", "config").is_some());
}
#[test]
fn a_contiguous_word_match_outranks_a_scattered_cross_word_one() {
let contiguous = total("test", "test_helper").unwrap(); let scattered = total("test", "the_settings_store");
if let Some(s) = scattered {
assert!(contiguous > s, "contiguous {contiguous} > scattered {s}");
}
}
#[test]
fn score_and_positions_come_from_the_same_alignment() {
assert!(subsequence_score("refproc", "RefundProcessor").is_some());
assert_eq!(match_positions("refproc", "RefundProcessor").len(), 7);
assert!(subsequence_score("xyz", "RefundProcessor").is_none());
assert!(match_positions("xyz", "RefundProcessor").is_empty());
}
#[test]
fn highlights_are_ordered_in_bounds_and_correct_across_varied_inputs() {
let cases = [
("usr", "UserService"),
("paymnt", "Payments"),
("wc", "WidgetController"),
("ctrl", "Controller"),
("gp", "get_post"),
("ab", "alpha_beta"),
("refproc", "RefundProcessor"),
("emp", "EmployeesController"),
("http", "HTTPParser"),
];
for (q, name) in cases {
let nchars: Vec<char> = name.chars().collect();
let qchars: Vec<char> = q.chars().filter(|c| c.is_alphanumeric()).collect();
let boundary = boundaries(&nchars);
let pos = match_positions(q, name);
assert!(
pos.windows(2).all(|w| w[0] < w[1]),
"strictly increasing: {q}/{name} {pos:?}"
);
let mut qi = 0;
for &p in &pos {
assert!(p < nchars.len(), "in bounds: {q}/{name}");
while qi < qchars.len() && !qchars[qi].eq_ignore_ascii_case(&nchars[p]) {
qi += 1;
}
assert!(
qi < qchars.len(),
"highlight maps to a query char: {q}/{name}"
);
qi += 1;
}
for (idx, &p) in pos.iter().enumerate() {
let clumped = (idx > 0 && pos[idx - 1] + 1 == p)
|| (idx + 1 < pos.len() && p + 1 == pos[idx + 1]);
assert!(
clumped || boundary[p],
"no isolated mid-word highlight: {q}/{name} at {p} {pos:?}"
);
}
}
}
#[test]
fn highlights_avoid_isolated_single_chars() {
assert_eq!(
match_positions("paymnt", "Payments"),
vec![0, 1, 2, 3, 5, 6]
);
assert_eq!(match_positions("usr", "UserService"), vec![0, 1]);
assert_eq!(match_positions("ctrl", "Controller"), vec![0, 3, 4]);
assert!(match_positions("rp", "wrapper").is_empty());
assert_eq!(match_positions("uc", "UserController"), vec![0, 4]);
}
#[test]
fn an_acronym_at_boundaries_outranks_a_mid_word_alignment() {
let acronym = subsequence_score("wc", "WidgetController").unwrap();
let midword = subsequence_score("wc", "switchcase").unwrap();
assert!(acronym > midword, "{acronym} > {midword}");
}
#[test]
fn a_far_path_straggler_never_outranks_a_prefix_match() {
let mut straggler = row("Thing", "class", 1);
straggler.file = "app/employee_x_syy.rb".into();
let prefixed = row("EmployeesController", "class", 1);
let pre = score("employees", &prefixed, None, Boosts::default())
.unwrap()
.total;
if let Some(s) = score("employees", &straggler, None, Boosts::default()) {
assert!(pre > s.total, "prefix {pre} > path straggler {}", s.total);
}
}
#[test]
fn snake_case_query_matches_camelcase_name() {
assert!(total("widget_controller", "WidgetsController").is_some());
assert!(total("widget_controller", "WidgetController").is_some());
assert!(total("widget_controller", "AdminController").is_none());
}
#[test]
fn wildcard_star_spans_an_explicit_gap() {
assert!(total("find*controller", "FindController").is_some());
assert!(total("find*controller", "FindUserController").is_some());
assert!(total("find*controller", "FindUserAccountController").is_some());
assert!(total("find*ctrlr", "FindController").is_none());
assert!(total("find*controller", "FindService").is_none());
}
#[test]
fn wildcard_question_mark_matches_one_char() {
assert!(total("find?controller", "FindXController").is_some());
assert!(total("find.controller", "Find1Controller").is_some());
assert!(total("find?controller", "FindController").is_none());
assert!(total("find?controller", "FindXyController").is_none());
}
#[test]
fn wildcard_highlights_only_the_literals() {
assert_eq!(
match_positions("find*er", "FindController"),
vec![0, 1, 2, 3, 12, 13] );
}
#[test]
fn wildcard_prefers_boundary_aligned_matches() {
let boundary = total("a*b", "Alpha_Bravo").unwrap();
let midword = total("a*b", "Alphabet").unwrap();
assert!(boundary > midword, "{boundary} > {midword}");
}
#[test]
fn non_subsequence_does_not_match() {
assert!(total("xyz", "RefundProcessor").is_none());
assert!(total("zzz", "User").is_none());
}
#[test]
fn confidence_reflects_quality_and_dominance() {
let exact = vec![Feature {
name: "exact",
value: 1000.0,
}];
let fuzzy = vec![Feature {
name: "fuzzy",
value: 300.0,
}];
assert_eq!(confidence(1000.0, match_quality(&exact), None), 1.0);
let f = confidence(300.0, match_quality(&fuzzy), None);
assert!(f > 0.3 && f < 0.65, "fuzzy confidence {f}");
let tied = confidence(1000.0, match_quality(&exact), Some(1000.0));
assert!(tied < 0.6, "tied exact confidence {tied}");
let dominant = confidence(1000.0, match_quality(&exact), Some(300.0));
assert!(dominant > 0.9, "dominant confidence {dominant}");
}
#[test]
fn parse_qualified_splits_on_scope_separators() {
assert_eq!(parse_qualified("User"), ("User", None));
assert_eq!(parse_qualified("Foo::Bar"), ("Bar", Some("Foo")));
assert_eq!(parse_qualified("App::Foo::Bar"), ("Bar", Some("App::Foo")));
assert_eq!(parse_qualified("Foo::Bar#baz"), ("baz", Some("Foo::Bar")));
assert_eq!(parse_qualified("::Bar"), ("::Bar", None));
assert_eq!(parse_qualified("Foo::"), ("Foo::", None));
}
#[test]
fn parent_boost_matches_the_innermost_scopes() {
assert!(parent_boost("Foo", Some("Foo")).is_some());
assert!(parent_boost("Foo", Some("App::Foo")).is_some());
assert!(parent_boost("App::Foo", Some("App::Foo")).is_some());
let one = parent_boost("Foo", Some("App::Foo")).unwrap();
let two = parent_boost("App::Foo", Some("App::Foo")).unwrap();
assert!(two > one, "{two} > {one}");
assert!(parent_boost("App", Some("App::Foo")).is_none());
assert!(parent_boost("Foo", Some("Foo::Inner")).is_none());
assert!(parent_boost("Foo", None).is_none());
}
#[test]
fn qualifier_ranks_the_symbol_in_the_named_scope_first() {
let in_foo = SymbolRow {
parent: Some("Foo".into()),
..row("Bar", "class", 1)
};
let in_baz = SymbolRow {
parent: Some("Baz".into()),
..row("Bar", "class", 1)
};
let foo = score("Foo::Bar", &in_foo, None, Boosts::default())
.unwrap()
.total;
let baz = score("Foo::Bar", &in_baz, None, Boosts::default())
.unwrap()
.total;
assert!(foo > baz, "{foo} > {baz}");
assert!(score("Bar", &in_baz, None, Boosts::default()).is_some());
assert!(score("Foo::Zzz", &in_foo, None, Boosts::default()).is_none());
}
#[test]
fn boundary_alignment_outranks_scattered() {
let aligned = total("rp", "RefundProcessor").unwrap();
let scattered = total("rp", "wrapper").unwrap();
assert!(aligned > scattered, "{aligned} > {scattered}");
}
#[test]
fn path_only_match_surfaces_a_class_in_a_named_file() {
let mut cand = row("Invoice", "class", 1);
cand.file = "app/models/billing.rb".into();
let s = score("billing", &cand, None, Boosts::default()).expect("path match");
assert!(s.features.iter().any(|f| f.name == "path"));
let mut method = row("compute", "method", 1);
method.file = "app/models/billing.rb".into();
assert!(score("billing", &method, None, Boosts::default()).is_none());
}
#[test]
fn path_bonus_reinforces_a_name_match() {
let mut named = row("User", "class", 1);
named.file = "app/models/user.rb".into();
let mut elsewhere = row("User", "class", 1);
elsewhere.file = "app/lib/misc.rb".into();
let with_path = score("user", &named, None, Boosts::default())
.unwrap()
.total;
let without = score("user", &elsewhere, None, Boosts::default())
.unwrap()
.total;
assert!(with_path > without, "{with_path} > {without}");
}
#[test]
fn current_repo_boost_applies() {
let cand = row("User", "class", 7);
let in_repo = score("user", &cand, Some(7), Boosts::default())
.unwrap()
.total;
let out_repo = score("user", &cand, Some(99), Boosts::default())
.unwrap()
.total;
assert!(in_repo > out_repo);
assert_eq!(in_repo - out_repo, 200.0);
}
#[test]
fn learned_boost_adds_to_the_score() {
let cand = row("User", "class", 1);
let base = score("user", &cand, None, Boosts::default()).unwrap().total;
let boosted = score(
"user",
&cand,
None,
Boosts {
learned: 150.0,
..Default::default()
},
)
.unwrap();
assert_eq!(boosted.total - base, 150.0);
assert!(boosted.features.iter().any(|f| f.name == "learned"));
}
#[test]
fn recency_boost_adds_to_the_score() {
let cand = row("User", "class", 1);
let base = score("user", &cand, None, Boosts::default()).unwrap().total;
let boosted = score(
"user",
&cand,
None,
Boosts {
recency: 80.0,
..Default::default()
},
)
.unwrap();
assert_eq!(boosted.total - base, 80.0);
assert!(boosted.features.iter().any(|f| f.name == "recency"));
}
#[test]
fn branch_boost_adds_to_the_score() {
let cand = row("User", "class", 1);
let base = score("user", &cand, None, Boosts::default()).unwrap().total;
let boosted = score(
"user",
&cand,
None,
Boosts {
branch: 180.0,
..Default::default()
},
)
.unwrap();
assert_eq!(boosted.total - base, 180.0);
assert!(boosted.features.iter().any(|f| f.name == "branch"));
}
}