use super::GlobalSearch;
use super::grammar::{ext_lang, lang_exts, path_ext};
use super::model::SearchHit;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum FacetKind {
Repo,
Lang,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct FacetId {
pub kind: FacetKind,
pub name: String,
}
impl FacetId {
pub(super) fn matches(&self, hit: &SearchHit) -> bool {
match self.kind {
FacetKind::Repo => hit.repo == self.name,
FacetKind::Lang => match lang_exts(&self.name) {
Some(exts) => exts.contains(&path_ext(&hit.path).as_str()),
None => path_ext(&hit.path) == self.name,
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Facet {
pub id: FacetId,
pub count: usize,
}
pub(super) fn facets(hits: &[SearchHit]) -> Vec<Facet> {
let mut repos: BTreeMap<String, usize> = BTreeMap::new();
let mut langs: BTreeMap<String, usize> = BTreeMap::new();
for hit in hits {
if !hit.repo.is_empty() {
*repos.entry(hit.repo.clone()).or_insert(0) += 1;
}
if let Some(lang) = lang_chip(&hit.path) {
*langs.entry(lang).or_insert(0) += 1;
}
}
let chips = |map: BTreeMap<String, usize>, kind: FacetKind| {
let mut out: Vec<Facet> = map
.into_iter()
.map(|(name, count)| Facet {
id: FacetId { kind, name },
count,
})
.collect();
out.sort_by(|a, b| {
b.count
.cmp(&a.count)
.then_with(|| a.id.name.cmp(&b.id.name))
});
out
};
let mut out = chips(repos, FacetKind::Repo);
out.extend(chips(langs, FacetKind::Lang));
out
}
fn lang_chip(path: &str) -> Option<String> {
let file = path.rsplit('/').next().unwrap_or(path);
let dot = file.rfind('.')?;
if dot == 0 || dot + 1 >= file.len() {
return None; }
let ext = path_ext(file);
Some(ext_lang(&ext).unwrap_or(ext.as_str()).to_string())
}
impl GlobalSearch {
pub(super) fn facets(&self) -> Vec<Facet> {
facets(&self.hits)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hit(repo: &str, path: &str) -> SearchHit {
SearchHit::plain(repo, path, 1, vec![], 0, String::new())
}
#[test]
fn chips_group_by_repo_and_language_with_counts() {
let hits = vec![
hit("a/one", "src/main.rs"),
hit("a/one", "docs/x.md"),
hit("b/two", "src/lib.rs"),
hit("b/two", "notes.txt"),
];
let chips = facets(&hits);
let names: Vec<(FacetKind, &str, usize)> = chips
.iter()
.map(|c| (c.id.kind, c.id.name.as_str(), c.count))
.collect();
assert_eq!(
names,
vec![
(FacetKind::Repo, "a/one", 2),
(FacetKind::Repo, "b/two", 2),
(FacetKind::Lang, "rust", 2),
(FacetKind::Lang, "markdown", 1),
(FacetKind::Lang, "txt", 1),
]
);
}
#[test]
fn language_facet_matches_whole_extension_family() {
let rust = FacetId {
kind: FacetKind::Lang,
name: "rust".into(),
};
assert!(rust.matches(&hit("a/one", "src/main.rs")));
assert!(!rust.matches(&hit("a/one", "docs/x.md")));
let txt = FacetId {
kind: FacetKind::Lang,
name: "txt".into(),
};
assert!(txt.matches(&hit("a/one", "notes.txt")));
assert!(!txt.matches(&hit("a/one", "src/main.rs")));
let repo = FacetId {
kind: FacetKind::Repo,
name: "a/one".into(),
};
assert!(repo.matches(&hit("a/one", "src/main.rs")));
assert!(!repo.matches(&hit("b/two", "src/main.rs")));
}
#[test]
fn extension_less_paths_get_no_language_chip() {
let chips = facets(&[hit("a/one", "Makefile"), hit("a/one", ".gitignore")]);
assert_eq!(chips.len(), 1);
assert_eq!(chips[0].id.kind, FacetKind::Repo);
}
}