use super::target;
use super::tree::{Node, NodeId};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Jump {
pub query: String,
pub cursor: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Candidate {
pub id: NodeId,
pub name: String,
}
pub fn candidates(roots: &[Node]) -> Vec<Candidate> {
let mut out = Vec::new();
for node in roots {
collect(node, &mut out);
}
out
}
fn collect(node: &Node, out: &mut Vec<Candidate>) {
for id in &node.ids {
let name = match id {
NodeId::Stack { project, .. } => format!("{project} {}", target::label(id)),
_ => target::label(id),
};
if !out.iter().any(|c| c.id == *id) {
out.push(Candidate {
id: id.clone(),
name,
});
}
}
for child in &node.children {
collect(child, out);
}
}
pub fn matches<'a>(candidates: &'a [Candidate], query: &str) -> Vec<&'a Candidate> {
let query: Vec<char> = query.to_lowercase().chars().collect();
let mut scored: Vec<(usize, usize, usize, &Candidate)> = candidates
.iter()
.filter_map(|c| {
let (gaps, start) = fit(&c.name.to_lowercase(), &query)?;
Some((gaps, start, c.name.len(), c))
})
.collect();
scored.sort_by_key(|(gaps, start, len, _)| (*gaps, *start, *len));
scored.into_iter().map(|(.., c)| c).collect()
}
fn fit(name: &str, query: &[char]) -> Option<(usize, usize)> {
if query.is_empty() {
return Some((0, 0));
}
let chars: Vec<char> = name.chars().collect();
(0..chars.len())
.filter(|&start| chars[start] == query[0])
.filter_map(|start| {
let mut at = start;
let mut gaps = 0;
for q in &query[1..] {
let next = chars[at + 1..].iter().position(|c| c == q)?;
gaps += next;
at += next + 1;
}
Some((gaps, start))
})
.min()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::fixtures::sample;
use crate::tui::tree;
fn names(found: Vec<&Candidate>) -> Vec<&str> {
found.iter().map(|c| c.name.as_str()).collect()
}
#[test]
fn lists_every_level() {
let all = candidates(&tree::build(&sample()));
assert_eq!(
names(all.iter().collect()),
[
"nova",
"nova @main",
"nova-billing@main",
"nova-billing@main/web",
"nova-billing@main/worker",
"nova-frontend@main",
"nova @feature-x",
"nova-billing@feature-x",
"nova-frontend@feature-x",
]
);
}
#[test]
fn ranks_tight_matches_first() {
let all = candidates(&tree::build(&sample()));
assert_eq!(names(matches(&all, "frontfx")), ["nova-frontend@feature-x"]);
assert_eq!(
names(matches(&all, "bill")),
[
"nova-billing@main",
"nova-billing@main/web",
"nova-billing@feature-x",
"nova-billing@main/worker",
]
);
assert_eq!(names(matches(&all, "BWork")), ["nova-billing@main/worker"]);
assert!(matches(&all, "zzz").is_empty());
assert_eq!(matches(&all, "").len(), all.len());
}
}