use std::ops::Range;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Widget};
use crate::ui::controller::settings::theme::groups::Groups;
pub struct Completion {
range: Range<usize>,
candidates: Vec<String>,
chosen: usize,
taken: bool,
listed: bool,
}
impl Completion {
pub fn new(text: &str, range: Range<usize>, candidates: Vec<String>) -> Option<Self> {
let typed = &text[range.clone()];
let candidates: Vec<String> = candidates
.into_iter()
.filter(|candidate| candidate.len() > typed.len() && candidate.starts_with(typed))
.collect();
(!candidates.is_empty()).then_some(Self {
range,
candidates,
chosen: 0,
taken: false,
listed: false,
})
}
pub fn chosen(&self) -> &str {
&self.candidates[self.chosen]
}
pub fn hinted(&self, below: u16) -> bool {
!self.taken && self.candidates.len() < usize::from(below)
}
pub fn reveal(&mut self, below: u16) -> bool {
if self.listed {
return false;
}
let reveal_only = !self.taken && !self.hinted(below);
self.listed = true;
reveal_only
}
pub fn list_height(&self) -> u16 {
if self.listed {
u16::try_from(self.candidates.len())
.unwrap_or(u16::MAX)
.saturating_add(2)
} else {
0
}
}
pub fn render_list(&self, area: Rect, buffer: &mut Buffer, groups: &Groups) {
let height = usize::from(area.height.saturating_sub(2));
let first = (self.chosen + 1).saturating_sub(height);
let lines: Vec<_> = self
.candidates
.iter()
.enumerate()
.skip(first)
.take(height)
.map(|(index, name)| {
let selected = index == self.chosen;
let marker = if selected { "> " } else { " " };
Line::from(vec![
Span::styled(marker, groups.picker_selected_marker()),
Span::raw(name.clone()),
])
.style(if selected {
groups.picker().patch(groups.picker_selected())
} else {
groups.picker()
})
})
.collect();
Paragraph::new(lines)
.style(groups.picker())
.block(
Block::default()
.borders(Borders::TOP | Borders::BOTTOM)
.border_style(groups.picker_border()),
)
.render(area, buffer);
}
pub fn ghost(&self) -> &str {
&self.chosen()[self.range.len()..]
}
pub fn take(&mut self, forward: bool) -> (Range<usize>, &str) {
let count = self.candidates.len();
if self.taken || !forward {
self.chosen = if forward {
(self.chosen + 1) % count
} else {
(self.chosen + count - 1) % count
};
}
self.taken = true;
let replaced = self.range.clone();
self.range = replaced.start..replaced.start + self.chosen().len();
(replaced, &self.candidates[self.chosen])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hint_threshold_is_strict_and_a_hint_inserts_as_the_list_opens() {
let mut completion =
Completion::new("i", 0..1, vec!["ideal".into(), "image".into()]).unwrap();
assert!(!completion.hinted(0));
assert!(!completion.hinted(2));
assert!(completion.hinted(3));
assert!(!completion.reveal(3));
assert_eq!(completion.list_height(), 4);
assert_eq!(completion.take(true).1, "ideal");
assert!(!completion.hinted(3));
assert_eq!(completion.take(false).1, "image");
}
#[test]
fn a_short_candidate_list_keeps_the_cycled_candidate_visible() {
let mut completion = Completion::new(
"i",
0..1,
vec!["ideal".into(), "image".into(), "identity".into()],
)
.unwrap();
assert!(completion.reveal(0));
completion.take(true);
completion.take(false);
let area = Rect::new(0, 0, 20, 3);
let mut buffer = Buffer::empty(area);
completion.render_list(area, &mut buffer, &Groups::default());
let row: String = (0..area.width).map(|x| buffer[(x, 1)].symbol()).collect();
assert!(row.starts_with("> identity"), "{row}");
}
#[test]
fn only_candidates_extending_the_typed_text_are_offered() {
let candidates = ["ideal", "id", "identity", "image"]
.map(String::from)
.to_vec();
let mut completion = Completion::new("x = id", 4..6, candidates).unwrap();
assert_eq!(completion.ghost(), "eal");
let (replaced, taken) = completion.take(true);
assert_eq!((replaced, taken), (4..6, "ideal"));
let (replaced, taken) = completion.take(true);
assert_eq!((replaced, taken), (4..9, "identity"));
let (_, taken) = completion.take(false);
assert_eq!(taken, "ideal");
assert!(Completion::new("id", 0..2, vec!["id".to_string()]).is_none());
}
}