use std::path::Path;
use crate::config::{CategoryConfig, PriorityRule, PriorityWeights};
use crate::scanner::entry::FileEntry;
pub fn calculate_priority(
entry: &FileEntry,
rules: &[PriorityRule],
categories: &CategoryConfig,
weights: &PriorityWeights,
) -> i32 {
let mut priority = None;
for rule in rules {
if matches_pattern(&entry.relative_path, &rule.pattern) {
priority = Some(rule.priority);
break;
}
}
if priority.is_none() && !entry.extension.is_empty() {
priority = category_priority(&entry.extension, categories);
}
let mut score = priority.unwrap_or(weights.default_priority);
score += path_adjustment(entry, categories, weights);
score.max(0)
}
fn matches_pattern(path: &Path, pattern: &str) -> bool {
let path_str = path.to_string_lossy();
if pattern.starts_with("**/") {
let suffix = &pattern[3..];
return matches_simple(&path_str, suffix)
|| path_str
.split('/')
.any(|part| matches_simple(part, suffix));
}
matches_simple(&path_str, pattern)
}
fn matches_simple(text: &str, pattern: &str) -> bool {
if pattern == "*" {
return true;
}
if !pattern.contains('*') {
return text == pattern || text.ends_with(&format!("/{}", pattern));
}
if pattern.ends_with('*') {
let prefix = &pattern[..pattern.len() - 1];
let file_name = text.rsplit('/').next().unwrap_or(text);
return file_name.starts_with(prefix);
}
if pattern.starts_with('*') {
let suffix = &pattern[1..];
return text.ends_with(suffix);
}
if let Some(star_pos) = pattern.find('*') {
let prefix = &pattern[..star_pos];
let suffix = &pattern[star_pos + 1..];
return text.starts_with(prefix) && text.ends_with(suffix);
}
false
}
fn category_priority(extension: &str, categories: &CategoryConfig) -> Option<i32> {
let ext_lower = extension.to_lowercase();
if categories.code.extensions.iter().any(|e| e == &ext_lower) {
return Some(categories.code.priority);
}
if categories.docs.extensions.iter().any(|e| e == &ext_lower) {
return Some(categories.docs.priority);
}
if categories
.config
.extensions
.iter()
.any(|e| e == &ext_lower)
{
return Some(categories.config.priority);
}
if categories.build.extensions.iter().any(|e| e == &ext_lower) {
return Some(categories.build.priority);
}
if categories.data.extensions.iter().any(|e| e == &ext_lower) {
return Some(categories.data.priority);
}
None
}
fn path_adjustment(entry: &FileEntry, categories: &CategoryConfig, weights: &PriorityWeights) -> i32 {
let mut delta = 0;
let file_name = entry.file_name().to_ascii_lowercase();
let components = path_components_lower(&entry.relative_path);
let is_code = !entry.extension.is_empty()
&& categories
.code
.extensions
.iter()
.any(|ext| ext.eq_ignore_ascii_case(&entry.extension));
let is_entrypoint = is_entrypoint_name(&file_name);
if is_entrypoint {
delta += weights.entrypoint_boost;
}
if is_code && entry.depth == 0 && is_entrypoint {
delta += weights.root_code_boost;
}
if is_code
&& components
.iter()
.any(|c| matches!(c.as_str(), "src" | "cmd" | "lib" | "pkg" | "internal"))
{
delta += weights.focus_dir_boost;
}
if is_test_like(&file_name, &components) {
delta += weights.test_penalty;
}
if is_fixture_like(&file_name, &components) {
delta += weights.fixture_penalty;
}
if entry.depth > 2 {
let penalty = ((entry.depth - 2) as i32) * weights.depth_penalty_step;
delta += penalty.max(weights.max_depth_penalty);
}
delta
}
fn path_components_lower(path: &Path) -> Vec<String> {
path.components()
.filter_map(|component| component.as_os_str().to_str())
.filter(|part| !part.is_empty() && *part != ".")
.map(|part| part.to_ascii_lowercase())
.collect()
}
fn is_entrypoint_name(file_name: &str) -> bool {
matches!(
file_name,
"main.rs"
| "lib.rs"
| "main.go"
| "app.go"
| "server.go"
| "main.py"
| "app.py"
| "__init__.py"
| "index.ts"
| "index.tsx"
| "main.ts"
| "main.js"
| "index.js"
| "cli.js"
)
}
fn is_test_like(file_name: &str, components: &[String]) -> bool {
if file_name.starts_with("test_")
|| file_name.contains("_test.")
|| file_name.ends_with("_test")
{
return true;
}
components.iter().any(|part| {
matches!(
part.as_str(),
"test" | "tests" | "testing" | "spec" | "specs" | "e2e" | "integration"
)
})
}
fn is_fixture_like(file_name: &str, components: &[String]) -> bool {
if file_name.contains("fixture") || file_name.contains("mock") {
return true;
}
components.iter().any(|part| {
matches!(
part.as_str(),
"fixtures" | "fixture" | "mocks" | "mock" | "testdata"
)
})
}
pub fn sort_by_priority(
entries: &mut [FileEntry],
rules: &[PriorityRule],
categories: &CategoryConfig,
weights: &PriorityWeights,
) {
entries.sort_by(|a, b| {
let pa = calculate_priority(a, rules, categories, weights);
let pb = calculate_priority(b, rules, categories, weights);
pb.cmp(&pa)
.then_with(|| a.depth.cmp(&b.depth))
.then_with(|| a.relative_path.cmp(&b.relative_path))
});
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn make_entry(path: &str) -> FileEntry {
FileEntry {
path: PathBuf::from(path),
relative_path: PathBuf::from(path),
is_dir: false,
size: 100,
extension: path.rsplit('.').next().unwrap_or("").to_string(),
depth: 0,
representation: crate::scanner::entry::Representation::NameOnly,
}
}
#[test]
fn test_readme_priority() {
let rules = vec![PriorityRule {
pattern: "README*".to_string(),
priority: 200,
}];
let categories = CategoryConfig::default();
let weights = PriorityWeights::default();
let entry = make_entry("README.md");
assert_eq!(calculate_priority(&entry, &rules, &categories, &weights), 200);
}
#[test]
fn test_glob_pattern() {
let rules = vec![PriorityRule {
pattern: "**/mod.rs".to_string(),
priority: 130,
}];
let categories = CategoryConfig::default();
let weights = PriorityWeights::default();
let entry = make_entry("src/scanner/mod.rs");
assert!(calculate_priority(&entry, &rules, &categories, &weights) >= 130);
}
#[test]
fn test_extension_priority() {
let rules = vec![];
let categories = CategoryConfig::default();
let weights = PriorityWeights::default();
let entry = make_entry("main.rs");
assert!(calculate_priority(&entry, &rules, &categories, &weights) >= 100);
}
#[test]
fn test_root_code_boost_over_nested() {
let rules = vec![];
let categories = CategoryConfig::default();
let weights = PriorityWeights::default();
let root = make_entry("main.rs");
let nested = make_entry("src/helper.rs");
assert!(calculate_priority(&root, &rules, &categories, &weights) > calculate_priority(&nested, &rules, &categories, &weights));
}
#[test]
fn test_test_file_penalty() {
let rules = vec![];
let categories = CategoryConfig::default();
let weights = PriorityWeights::default();
let code = make_entry("cmd/bd/agent.go");
let test = make_entry("cmd/bd/agent_test.go");
assert!(calculate_priority(&code, &rules, &categories, &weights) > calculate_priority(&test, &rules, &categories, &weights));
}
#[test]
fn test_custom_weights() {
let rules = vec![];
let categories = CategoryConfig::default();
let mut weights = PriorityWeights::default();
weights.test_penalty = -100;
let code = make_entry("src/lib.rs");
let test = make_entry("tests/test_lib.rs");
let code_priority = calculate_priority(&code, &rules, &categories, &weights);
let test_priority = calculate_priority(&test, &rules, &categories, &weights);
assert!(code_priority - test_priority > 50);
}
}