use std::{ffi::OsStr, path::Path};
use crate::{
models::{self},
resolver::{constant::DEFAULT_SCORE, utils, weight},
};
pub fn fuzzy_match(
query: &str,
symbol: &models::resolved::ResolvedSymbol,
config: &frizbee::Config,
) -> Vec<frizbee::Match> {
let path = symbol.path.to_str().unwrap_or_default();
let path_symbol_prefix = format!("{path}:{}", symbol.name.as_str());
frizbee::match_list(
query,
&[path_symbol_prefix.as_str(), symbol.name.as_str()],
config,
)
}
pub fn calculate_score<'a, 'b>(
symbol: &models::resolved::ResolvedSymbol,
fuzzy_matches: impl Iterator<Item = &'a frizbee::Match>,
current_file: Option<&'b Path>,
) -> i64 {
let filename = if let Some(Some(filename)) = symbol.path.file_name().map(OsStr::to_str) {
Some(filename)
} else {
None
};
let entrypoint_file_penalty = if let Some(filename) = filename
&& utils::is_entrypoint_file(filename)
{
weight::ENTRYPOINT_FILE_SCORE_PENALTY
} else {
0
};
let fuzzy_match_bonus: i64 = fuzzy_matches.map(weight::calculate_fuzzy_match_bonus).sum();
let symbol_kind_bonus = match symbol.kind {
models::parsed::SymbolKind::Function
| models::parsed::SymbolKind::Method
| models::parsed::SymbolKind::Struct
| models::parsed::SymbolKind::Type
| models::parsed::SymbolKind::TypeAlias
| models::parsed::SymbolKind::Class
| models::parsed::SymbolKind::Constant
| models::parsed::SymbolKind::Enum
| models::parsed::SymbolKind::EnumMember
| models::parsed::SymbolKind::Interface => weight::COMMON_SYMBOL_KINDS_SCORE_BONUS,
models::parsed::SymbolKind::Variable => weight::INFREQUENT_SYMBOL_KINDS_SCORE_BONUS,
models::parsed::SymbolKind::Package
| models::parsed::SymbolKind::Module
| models::parsed::SymbolKind::SelfParameter => weight::UNCOMMON_SYMBOL_KINDS_SCORE_PENALTY,
_ => 0,
};
let test_harness_penalty = if utils::is_part_of_test_harness(symbol.path.as_path()) {
weight::TEST_HARNESS_SCORE_PENALTY
} else {
0
};
let distance_penalty = current_file.map_or(0, |current_file| {
weight::calculate_distance_score_penalty(utils::get_path_distance(
current_file,
symbol.path.as_path(),
))
});
DEFAULT_SCORE
.saturating_add(entrypoint_file_penalty)
.saturating_add(fuzzy_match_bonus)
.saturating_add(symbol_kind_bonus)
.saturating_add(test_harness_penalty)
.saturating_add(distance_penalty)
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::{
models::{
parsed::SymbolKind,
resolved::{ResolvedSymbol, Score},
},
resolver::scoring::DEFAULT_SCORE,
};
#[test]
pub fn test_scoring_struct_in_entrypoint_file() {
let symbol = ResolvedSymbol {
id: 1,
name: "ResolvedSymbol".to_string(),
kind: SymbolKind::Struct,
path: PathBuf::from("/some/file/mod.rs"),
score: Score::default(),
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 14,
};
let score = super::calculate_score(&symbol, Vec::new().iter(), None);
let mut target_score = DEFAULT_SCORE;
target_score += 35; target_score -= 10;
assert_eq!(target_score, score);
}
#[test]
pub fn test_scoring_struct_where_path_has_no_filename() {
let symbol = ResolvedSymbol {
id: 1,
name: "ResolvedSymbol".to_string(),
kind: SymbolKind::Struct,
path: PathBuf::from("/some/file"),
score: Score::default(),
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 14,
};
let score = super::calculate_score(&symbol, Vec::new().iter(), None);
let mut target_score = DEFAULT_SCORE;
target_score += 35;
assert_eq!(target_score, score);
}
#[test]
pub fn test_scoring_variable_in_far_away_file() {
let symbol = ResolvedSymbol {
id: 1,
name: "ResolvedSymbol".to_string(),
kind: SymbolKind::Variable,
path: PathBuf::from_iter(["", "some", "file", "over", "here", "file.rs"]),
score: Score::default(),
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 14,
};
let score = super::calculate_score(
&symbol,
Vec::new().iter(),
Some(&PathBuf::from_iter([
"a",
"totally",
"different",
"file",
"over",
"there",
"file.ts",
])),
);
let mut target_score = DEFAULT_SCORE;
target_score += 5; target_score -= 6;
assert_eq!(target_score, score);
}
#[test]
pub fn test_scoring_module_symbol() {
let symbol = ResolvedSymbol {
id: 1,
name: "tests".to_string(),
kind: SymbolKind::Module,
path: PathBuf::from("some_module.rs"),
score: Score::default(),
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 14,
};
let score = super::calculate_score(&symbol, Vec::new().iter(), None);
let mut target_score = DEFAULT_SCORE;
target_score -= 15;
assert_eq!(target_score, score);
}
#[test]
pub fn test_scoring_class_in_test_file() {
let symbol = ResolvedSymbol {
id: 1,
name: "TestClass".to_string(),
kind: SymbolKind::Class,
path: PathBuf::from("some_file.test.ts"),
score: Score::default(),
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 9,
};
let score = super::calculate_score(&symbol, Vec::new().iter(), None);
let mut target_score = DEFAULT_SCORE;
target_score += 35; target_score -= 5;
assert_eq!(target_score, score);
}
#[test]
pub fn test_scoring_fuzzy_matched_symbol() {
let query = "Lem";
let name = "TestLemma".to_string();
let path = PathBuf::from_iter(["some", "file", "over", "there.ts"]);
let symbol = ResolvedSymbol {
id: 1,
name: name.clone(),
kind: SymbolKind::Lemma,
path: path.clone(),
score: Score::default(),
start_line: 1,
start_column: 1,
end_line: 1,
end_column: 9,
};
let config = frizbee::Config {
max_typos: Some(1),
sort: false,
scoring: frizbee::Scoring::default(),
};
let fuzzy_matches = frizbee::match_list(
query,
&[
format!("{}:{name}", path.to_str().unwrap()).as_str(),
name.as_str(),
],
&config,
);
let score = super::calculate_score(&symbol, fuzzy_matches.iter(), None);
let mut target_score = DEFAULT_SCORE;
target_score += 26;
assert_eq!(target_score, score);
}
}