use super::model::{RawHit, SearchKind};
use crate::provider::Provider;
const PREVIEW_CAP: usize = 8;
const HIT_CAP: usize = 25;
fn code_query(kind: SearchKind, query: &str, scope_label: &str, extension: &str) -> String {
let mut q = match kind {
SearchKind::Grep => query.to_string(),
SearchKind::FileFind => format!("path:{query}"),
};
if scope_label != "global" {
q.push(' ');
q.push_str(scope_label); }
let ext = extension.trim_start_matches('.');
if !ext.is_empty() {
q.push_str(&format!(" extension:{ext}"));
}
q
}
pub fn run_view_search(
provider: &dyn Provider,
kind: SearchKind,
query: &str,
scope_label: &str,
extension: &str,
) -> crate::provider::ProviderResult<(Vec<RawHit>, bool)> {
if kind == SearchKind::FileFind && scope_label.starts_with("repo:") {
return tree_file_find(provider, query, &scope_label["repo:".len()..], extension);
}
code_search(provider, kind, query, scope_label, extension)
}
fn tree_file_find(
provider: &dyn Provider,
query: &str,
repo_full: &str,
extension: &str,
) -> crate::provider::ProviderResult<(Vec<RawHit>, bool)> {
let tree = provider.fetch_tree(repo_full)?;
let branch = tree.branch;
let needles: Vec<String> = query
.to_lowercase()
.split_whitespace()
.map(str::to_string)
.collect();
let ext = extension.trim_start_matches('.').to_lowercase();
let mut scored: Vec<(i32, RawHit)> = Vec::new();
for entry in tree.entries {
if entry.is_dir {
continue;
}
let path_lower = entry.path.to_lowercase();
if !ext.is_empty() && !path_lower.ends_with(&format!(".{ext}")) {
continue;
}
let Some(score) = file_find_score(&path_lower, &needles) else {
continue;
};
scored.push((
score,
RawHit {
repo: repo_full.to_string(),
path: entry.path,
sha: entry.sha,
branch: branch.clone(),
line: 1,
preview: vec![],
match_count: 0,
stale: false,
},
));
}
scored.sort_by_key(|(score, _)| std::cmp::Reverse(*score));
let client_capped = scored.len() > HIT_CAP;
let mut hits: Vec<RawHit> = scored.into_iter().take(HIT_CAP).map(|(_, h)| h).collect();
add_blob_heads(provider, &mut hits);
Ok((hits, client_capped))
}
pub(crate) fn file_find_score(path: &str, needles: &[String]) -> Option<i32> {
let file = path.rsplit('/').next().unwrap_or(path);
let mut total = 0;
let mut any = false;
for needle in needles {
if needle.is_empty() {
continue;
}
any = true;
let n = needle.len() as i32;
let score = if file.starts_with(needle.as_str()) {
120 + n
} else if file.contains(needle.as_str()) {
100 + n
} else if path.contains(needle.as_str()) {
50 + n
} else {
subsequence_score(path, needle)?
};
total += score;
}
if !any {
return Some(0);
}
Some(total - path.len() as i32 / 8)
}
fn subsequence_score(path: &str, needle: &str) -> Option<i32> {
let hay: Vec<char> = path.chars().collect();
let mut score = 0;
let mut hi = 0;
let mut prev: Option<usize> = None;
let mut run = 0;
for c in needle.chars() {
let pos = hay[hi..].iter().position(|&h| h == c)? + hi;
run = if prev.is_some_and(|p| pos == p + 1) {
run + 1
} else {
1
};
let boundary = pos == 0 || matches!(hay[pos - 1], '/' | '.' | '_' | '-');
score += 1 + run + i32::from(boundary) * 3;
prev = Some(pos);
hi = pos + 1;
}
Some(score)
}
fn code_search(
provider: &dyn Provider,
kind: SearchKind,
query: &str,
scope_label: &str,
extension: &str,
) -> crate::provider::ProviderResult<(Vec<RawHit>, bool)> {
let q = code_query(kind, query, scope_label, extension);
let result = provider.search_code(&q)?;
let client_capped = result.hits.len() > HIT_CAP;
let mut hits: Vec<RawHit> = Vec::new();
for item in result.hits.into_iter().take(HIT_CAP) {
let needles = item.matches.clone();
let mut hit = RawHit {
repo: item.repo,
path: item.path,
sha: item.sha,
branch: item.branch,
line: 1,
preview: vec![],
match_count: needles.len() as u32,
stale: !item.located,
};
if kind == SearchKind::Grep
&& hits.len() < PREVIEW_CAP
&& let Some((line, preview, count)) =
locate_matches(provider, &hit.repo, &hit.sha, &needles)
{
hit.line = line;
hit.preview = preview;
hit.match_count = count;
hit.stale = false; }
hits.push(hit);
}
if kind == SearchKind::FileFind {
add_blob_heads(provider, &mut hits);
}
Ok((hits, result.truncated || client_capped))
}
pub(crate) type LocatedPreview = (u32, Vec<(u32, String)>, u32);
pub(crate) fn locate_matches(
provider: &dyn Provider,
repo: &str,
sha: &str,
needles: &[String],
) -> Option<LocatedPreview> {
let bytes = provider.fetch_blob(repo, sha).ok()?;
locate_in_blob(&bytes, needles)
}
pub(crate) fn locate_in_blob(bytes: &[u8], needles: &[String]) -> Option<LocatedPreview> {
if crate::sanitize::is_binary(bytes) {
return None;
}
let text = crate::sanitize::sanitize(bytes);
let lines: Vec<&str> = text.lines().collect();
let needles: Vec<String> = needles
.iter()
.map(|n| n.to_lowercase())
.filter(|n| !n.is_empty())
.collect();
if needles.is_empty() {
return None;
}
let matched: Vec<usize> = lines
.iter()
.enumerate()
.filter(|(_, l)| {
let ll = l.to_lowercase();
needles.iter().any(|n| ll.contains(n))
})
.map(|(i, _)| i)
.collect();
if matched.is_empty() {
return None;
}
let mut regions: Vec<(usize, usize)> = Vec::new();
for &m in &matched {
let (start, end) = (m.saturating_sub(1), (m + 2).min(lines.len()));
match regions.last_mut() {
Some((_, e)) if start <= *e => *e = end.max(*e),
_ => regions.push((start, end)),
}
}
let mut preview = Vec::new();
for (start, end) in regions.into_iter().take(2) {
let capped = end.min(start + 5);
for (i, line) in lines.iter().enumerate().take(capped).skip(start) {
preview.push(((i + 1) as u32, line.to_string()));
}
}
Some(((matched[0] + 1) as u32, preview, matched.len() as u32))
}
fn add_blob_heads(provider: &dyn Provider, hits: &mut [RawHit]) {
for hit in hits.iter_mut().take(PREVIEW_CAP) {
let Ok(bytes) = provider.fetch_blob(&hit.repo, &hit.sha) else {
continue;
};
if crate::sanitize::is_binary(&bytes) {
continue;
}
let text = crate::sanitize::sanitize(&bytes);
hit.preview = text
.lines()
.take(3)
.enumerate()
.map(|(i, l)| ((i + 1) as u32, l.to_string()))
.collect();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn needles(q: &str) -> Vec<String> {
q.split_whitespace().map(str::to_string).collect()
}
#[test]
fn file_find_matches_like_github_go_to_file() {
let s = file_find_score("djangosite/urls/default.py", &needles("urldef"));
assert!(s.is_some(), "subsequence across dir + file must match");
assert!(file_find_score("something/term/cargo.toml", &needles("term")).is_some());
assert!(file_find_score("djangosite/urls/default.py", &needles("url def")).is_some());
assert!(file_find_score("djangosite/urls/default.py", &needles("url zzz")).is_none());
assert!(file_find_score("src/main.rs", &needles("msni")).is_none());
assert!(file_find_score("src/main.rs", &needles("mrs")).is_some());
assert_eq!(file_find_score("src/main.rs", &[]), Some(0));
}
#[test]
fn file_find_ranks_filename_above_path_above_scattered() {
let starts = file_find_score("src/terminal.rs", &needles("term")).unwrap();
let inside = file_find_score("src/myterm.rs", &needles("term")).unwrap();
let dir = file_find_score("src/term/cargo.toml", &needles("term")).unwrap();
let scattered = file_find_score("src/test/remote.rs", &needles("term")).unwrap();
assert!(
starts > inside,
"needle starting the file name beats one inside it"
);
assert!(inside > dir, "file-name match beats a directory-only match");
assert!(
dir > scattered,
"contiguous path match beats scattered chars"
);
}
#[test]
fn locate_in_blob_folds_regions_and_counts() {
let text = b"line one\nmatch here\nbetween\nanother match\ntail";
let needles = vec!["match".to_string()];
let (line, preview, count) = locate_in_blob(text, &needles).unwrap();
assert_eq!(line, 2);
assert_eq!(count, 2);
let nos: Vec<u32> = preview.iter().map(|(n, _)| *n).collect();
assert_eq!(nos, vec![1, 2, 3, 4, 5]);
assert!(locate_in_blob(b"\x00\x01match", &needles).is_none());
}
#[test]
fn code_query_maps_scope_and_extension() {
use super::SearchKind;
assert_eq!(
code_query(SearchKind::Grep, "needle", "global", ""),
"needle"
);
assert_eq!(
code_query(SearchKind::FileFind, "main", "repo:o/r", ".rs"),
"path:main repo:o/r extension:rs"
);
assert_eq!(
code_query(SearchKind::Grep, "q", "org:x", "rs"),
"q org:x extension:rs"
);
}
}