use std::path::PathBuf;
use crate::fuzzy::fuzzy_match;
use super::DocumentSymbol;
pub struct OutlinePane {
pub target: PathBuf,
pub items: Vec<DocumentSymbol>,
pub selected: usize,
pub scroll: usize,
pub query: String,
pub filter_mode: bool,
}
impl OutlinePane {
pub fn new(target: PathBuf, items: Vec<DocumentSymbol>) -> Self {
OutlinePane {
target,
items,
selected: 0,
scroll: 0,
query: String::new(),
filter_mode: false,
}
}
pub fn visible_indices(&self) -> Vec<usize> {
if self.query.is_empty() {
return (0..self.items.len()).collect();
}
self.items
.iter()
.enumerate()
.filter_map(|(i, s)| fuzzy_match(&self.query, &s.name).map(|_| i))
.collect()
}
pub fn selected_filtered_item(&self) -> Option<&DocumentSymbol> {
let v = self.visible_indices();
v.get(self.selected).and_then(|&i| self.items.get(i))
}
pub fn filter_push(&mut self, c: char) {
self.query.push(c);
self.selected = 0;
self.scroll = 0;
}
pub fn filter_pop(&mut self) {
self.query.pop();
self.selected = 0;
self.scroll = 0;
}
pub fn filter_clear_and_exit(&mut self) {
self.query.clear();
self.filter_mode = false;
self.selected = 0;
self.scroll = 0;
}
pub fn tab_title(&self) -> String {
let name = self
.target
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("outline")
.to_string();
let n = self.items.len();
if n == 0 {
format!("{name} ⌥")
} else {
format!("{name} ⌥{n}")
}
}
pub fn clamp(&mut self) {
let n = self.visible_indices().len();
if n == 0 {
self.selected = 0;
} else if self.selected >= n {
self.selected = n - 1;
}
}
pub fn move_selection(&mut self, delta: isize) {
let n = self.visible_indices().len() as isize;
if n == 0 {
return;
}
self.selected = (self.selected as isize + delta).clamp(0, n - 1) as usize;
}
pub fn selected_item(&self) -> Option<&DocumentSymbol> {
self.selected_filtered_item()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sym(name: &str, kind: &'static str, line: u32, depth: u32) -> DocumentSymbol {
DocumentSymbol {
name: name.into(),
kind,
line,
character: 0,
depth,
}
}
#[test]
fn tab_title_includes_count_and_filename() {
let p = OutlinePane::new(
PathBuf::from("/ws/src/main.rs"),
vec![sym("main", "fn", 0, 0), sym("Foo", "struct", 5, 0)],
);
assert_eq!(p.tab_title(), "main.rs ⌥2");
let empty = OutlinePane::new(PathBuf::from("/ws/src/main.rs"), Vec::new());
assert_eq!(empty.tab_title(), "main.rs ⌥");
}
#[test]
fn move_selection_clamps() {
let mut p = OutlinePane::new(
PathBuf::from("/x"),
vec![sym("a", "fn", 0, 0), sym("b", "fn", 1, 0)],
);
p.move_selection(-5);
assert_eq!(p.selected, 0);
p.move_selection(99);
assert_eq!(p.selected, 1);
}
#[test]
fn filter_narrows_visible_indices() {
let mut p = OutlinePane::new(
PathBuf::from("/x"),
vec![
sym("alpha", "fn", 0, 0),
sym("beta", "fn", 1, 0),
sym("alphabet", "fn", 2, 0),
],
);
assert_eq!(p.visible_indices().len(), 3);
p.filter_push('a');
p.filter_push('l');
let v = p.visible_indices();
assert_eq!(v.len(), 2);
assert_eq!(p.items[v[0]].name, "alpha");
assert_eq!(p.items[v[1]].name, "alphabet");
p.filter_clear_and_exit();
assert_eq!(p.query, "");
assert!(!p.filter_mode);
assert_eq!(p.visible_indices().len(), 3);
}
#[test]
fn move_selection_respects_filter() {
let mut p = OutlinePane::new(
PathBuf::from("/x"),
vec![
sym("alpha", "fn", 0, 0),
sym("zeta", "fn", 1, 0),
sym("alphabet", "fn", 2, 0),
],
);
p.filter_push('a');
p.filter_push('l');
assert_eq!(p.visible_indices().len(), 2);
p.move_selection(99);
assert_eq!(p.selected, 1);
assert_eq!(p.selected_item().unwrap().name, "alphabet");
}
}