use egaku::picker::{FuzzyPicker, PickerEffect, PickerEvent};
pub use egaku::picker::PickerItem;
use escriba_core::BufferId;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Choice {
Buffer(BufferId),
Command(String),
OpenFile(std::path::PathBuf),
Location { path: std::path::PathBuf, line: u32 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source {
Buffers,
Commands,
Help,
Grep,
Files,
Project,
Findings,
}
impl Source {
#[must_use]
pub const fn title(self) -> &'static str {
match self {
Self::Buffers => "Buffers",
Self::Commands => "Commands",
Self::Help => "Help",
Self::Grep => "Grep",
Self::Files => "Files",
Self::Project => "Project",
Self::Findings => "Diagnostics",
}
}
}
#[derive(Debug)]
pub struct Picker {
inner: FuzzyPicker<Choice>,
source: Source,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Consumed {
NotShowing,
Held,
Dismissed,
Chose(Choice),
}
impl Picker {
#[must_use]
pub fn open(source: Source, items: Vec<PickerItem<Choice>>) -> Self {
let mut inner = FuzzyPicker::new(items);
let _ = inner.on_event(PickerEvent::Open);
Self { inner, source }
}
#[must_use]
pub const fn source(&self) -> Source {
self.source
}
#[must_use]
pub fn query(&self) -> &str {
self.inner.query()
}
#[must_use]
pub fn visible_count(&self) -> usize {
self.inner.visible_count()
}
#[must_use]
pub fn rows(&self) -> Vec<(String, bool)> {
let view = self.inner.view();
let sel = view.selected;
view.rows
.iter()
.enumerate()
.map(|(i, it)| (it.label.clone(), i == sel))
.collect()
}
pub fn on_key(&mut self, key: &escriba_keymap::Key) -> Consumed {
let Some(event) = translate(key) else {
return Consumed::Held;
};
for effect in self.inner.on_event(event) {
match effect {
PickerEffect::Accepted { key } => return Consumed::Chose(key),
PickerEffect::Cancelled => return Consumed::Dismissed,
PickerEffect::Opened
| PickerEffect::Filtered { .. }
| PickerEffect::Moved { .. } => {}
}
}
Consumed::Held
}
}
#[must_use]
pub fn translate(key: &escriba_keymap::Key) -> Option<PickerEvent> {
use escriba_keymap::Key;
Some(match key {
Key::Char(c) => PickerEvent::Type(*c),
Key::Backspace => PickerEvent::Backspace,
Key::Up | Key::Ctrl('p') => PickerEvent::NavUp,
Key::Down | Key::Ctrl('n') => PickerEvent::NavDown,
Key::Enter => PickerEvent::Accept,
Key::Esc | Key::Ctrl('c') => PickerEvent::Cancel,
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn picker() -> Picker {
Picker::open(
Source::Buffers,
vec![
PickerItem::new(Choice::Buffer(BufferId(1)), "alpha.rs"),
PickerItem::new(Choice::Buffer(BufferId(2)), "beta.rs"),
PickerItem::new(Choice::Buffer(BufferId(3)), "gamma.txt"),
],
)
}
#[test]
fn typing_narrows_the_rows() {
let mut p = picker();
assert_eq!(p.visible_count(), 3);
assert_eq!(p.on_key(&escriba_keymap::Key::Char('b')), Consumed::Held);
assert!(p.visible_count() < 3, "typing must filter");
assert_eq!(p.query(), "b");
}
#[test]
fn enter_commits_the_highlighted_row() {
let mut p = picker();
match p.on_key(&escriba_keymap::Key::Enter) {
Consumed::Chose(Choice::Buffer(_)) => {}
other => panic!("Enter must commit, got {other:?}"),
}
}
#[test]
fn esc_dismisses_without_committing() {
let mut p = picker();
assert_eq!(p.on_key(&escriba_keymap::Key::Esc), Consumed::Dismissed);
}
#[test]
fn an_unknown_key_is_held_not_passed_through() {
let mut p = picker();
assert_eq!(
p.on_key(&escriba_keymap::Key::Ctrl('w')),
Consumed::Held,
"an unknown key must be swallowed by the open overlay",
);
}
#[test]
fn navigation_moves_the_selection() {
let mut p = picker();
let first = p.rows().iter().position(|(_, sel)| *sel);
p.on_key(&escriba_keymap::Key::Ctrl('n'));
let second = p.rows().iter().position(|(_, sel)| *sel);
assert_ne!(first, second, "<C-n> must move the highlight");
}
#[test]
fn both_the_arrows_and_the_control_pair_navigate() {
let mut a = picker();
a.on_key(&escriba_keymap::Key::Down);
let via_arrow = a.rows().iter().position(|(_, s)| *s);
let mut b = picker();
b.on_key(&escriba_keymap::Key::Ctrl('n'));
assert_eq!(via_arrow, b.rows().iter().position(|(_, s)| *s));
}
}