implement fish like tab completion (#767)

* implement fish like tab completion

* grey the pv out and convert to lowercase

* run fmt

* do not tabcomplete if the user hits enter

* fix

* fix lints

* run docgen

---------

Co-authored-by: nyx <nnyyxxxx@users.noreply.github.com>
Co-authored-by: Adam Perkowski <adas1per@protonmail.com>
This commit is contained in:
nyx 2024-11-01 17:01:01 -04:00 committed by GitHub
parent 58f3433de6
commit 5878f4dbf0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -4,7 +4,7 @@ use ego_tree::NodeId;
use linutil_core::Tab; use linutil_core::Tab;
use ratatui::{ use ratatui::{
layout::{Position, Rect}, layout::{Position, Rect},
style::Style, style::{Color, Style},
text::Span, text::Span,
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
Frame, Frame,
@ -22,6 +22,7 @@ pub struct Filter {
in_search_mode: bool, in_search_mode: bool,
input_position: usize, input_position: usize,
items: Vec<ListEntry>, items: Vec<ListEntry>,
completion_preview: Option<String>,
} }
impl Filter { impl Filter {
@ -31,17 +32,23 @@ impl Filter {
in_search_mode: false, in_search_mode: false,
input_position: 0, input_position: 0,
items: vec![], items: vec![],
completion_preview: None,
} }
} }
pub fn item_list(&self) -> &[ListEntry] { pub fn item_list(&self) -> &[ListEntry] {
&self.items &self.items
} }
pub fn activate_search(&mut self) { pub fn activate_search(&mut self) {
self.in_search_mode = true; self.in_search_mode = true;
} }
pub fn deactivate_search(&mut self) { pub fn deactivate_search(&mut self) {
self.in_search_mode = false; self.in_search_mode = false;
self.completion_preview = None;
} }
pub fn update_items(&mut self, tabs: &[Tab], current_tab: usize, node: NodeId) { pub fn update_items(&mut self, tabs: &[Tab], current_tab: usize, node: NodeId) {
if self.search_input.is_empty() { if self.search_input.is_empty() {
let curr = tabs[current_tab].tree.get(node).unwrap(); let curr = tabs[current_tab].tree.get(node).unwrap();
@ -78,13 +85,34 @@ impl Filter {
} }
self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name)); self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name));
} }
self.update_completion_preview();
} }
fn update_completion_preview(&mut self) {
if self.search_input.is_empty() {
self.completion_preview = None;
return;
}
let input = self.search_input.iter().collect::<String>().to_lowercase();
self.completion_preview = self.items.iter().find_map(|item| {
let item_name_lower = item.node.name.to_lowercase();
if item_name_lower.starts_with(&input) {
Some(item_name_lower[input.len()..].to_string())
} else {
None
}
});
}
pub fn draw_searchbar(&self, frame: &mut Frame, area: Rect, theme: &Theme) { pub fn draw_searchbar(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
//Set the search bar text (If empty use the placeholder) //Set the search bar text (If empty use the placeholder)
let display_text = if !self.in_search_mode && self.search_input.is_empty() { let display_text = if !self.in_search_mode && self.search_input.is_empty() {
Span::raw("Press / to search") Span::raw("Press / to search")
} else { } else {
Span::raw(self.search_input.iter().collect::<String>()) let input_text = self.search_input.iter().collect::<String>();
Span::styled(input_text, Style::default().fg(theme.focused_color()))
}; };
let search_color = if self.in_search_mode { let search_color = if self.in_search_mode {
@ -110,11 +138,22 @@ impl Filter {
let x = area.x + cursor_position as u16 + 1; let x = area.x + cursor_position as u16 + 1;
let y = area.y + 1; let y = area.y + 1;
frame.set_cursor_position(Position::new(x, y)); frame.set_cursor_position(Position::new(x, y));
if let Some(preview) = &self.completion_preview {
let preview_span = Span::styled(preview, Style::default().fg(Color::DarkGray));
let preview_paragraph = Paragraph::new(preview_span).style(Style::default());
let preview_area = Rect::new(
x,
y,
(preview.len() as u16).min(area.width - cursor_position as u16 - 1),
1,
);
frame.render_widget(preview_paragraph, preview_area);
}
} }
} }
// Handles key events. Returns true if search must be exited // Handles key events. Returns true if search must be exited
pub fn handle_key(&mut self, event: &KeyEvent) -> SearchAction { pub fn handle_key(&mut self, event: &KeyEvent) -> SearchAction {
//Insert user input into the search bar
match event.code { match event.code {
KeyCode::Char('c') if event.modifiers.contains(KeyModifiers::CONTROL) => { KeyCode::Char('c') if event.modifiers.contains(KeyModifiers::CONTROL) => {
return self.exit_search() return self.exit_search()
@ -124,10 +163,17 @@ impl Filter {
KeyCode::Delete => self.remove_next(), KeyCode::Delete => self.remove_next(),
KeyCode::Left => return self.cursor_left(), KeyCode::Left => return self.cursor_left(),
KeyCode::Right => return self.cursor_right(), KeyCode::Right => return self.cursor_right(),
KeyCode::Tab => return self.complete_search(),
KeyCode::Esc => {
self.input_position = 0;
self.search_input.clear();
self.completion_preview = None;
return SearchAction::Exit;
}
KeyCode::Enter => return SearchAction::Exit, KeyCode::Enter => return SearchAction::Exit,
KeyCode::Esc => return self.exit_search(),
_ => return SearchAction::None, _ => return SearchAction::None,
}; };
self.update_completion_preview();
SearchAction::Update SearchAction::Update
} }
@ -141,16 +187,19 @@ impl Filter {
self.input_position = self.input_position.saturating_sub(1); self.input_position = self.input_position.saturating_sub(1);
SearchAction::None SearchAction::None
} }
fn cursor_right(&mut self) -> SearchAction { fn cursor_right(&mut self) -> SearchAction {
if self.input_position < self.search_input.len() { if self.input_position < self.search_input.len() {
self.input_position += 1; self.input_position += 1;
} }
SearchAction::None SearchAction::None
} }
fn insert_char(&mut self, input: char) { fn insert_char(&mut self, input: char) {
self.search_input.insert(self.input_position, input); self.search_input.insert(self.input_position, input);
self.cursor_right(); self.cursor_right();
} }
fn remove_previous(&mut self) { fn remove_previous(&mut self) {
let current = self.input_position; let current = self.input_position;
if current > 0 { if current > 0 {
@ -158,12 +207,25 @@ impl Filter {
self.cursor_left(); self.cursor_left();
} }
} }
fn remove_next(&mut self) { fn remove_next(&mut self) {
let current = self.input_position; let current = self.input_position;
if current < self.search_input.len() { if current < self.search_input.len() {
self.search_input.remove(current); self.search_input.remove(current);
} }
} }
fn complete_search(&mut self) -> SearchAction {
if let Some(completion) = self.completion_preview.take() {
self.search_input.extend(completion.chars());
self.input_position = self.search_input.len();
self.update_completion_preview();
SearchAction::Update
} else {
SearchAction::None
}
}
pub fn clear_search(&mut self) { pub fn clear_search(&mut self) {
self.search_input.clear(); self.search_input.clear();
self.input_position = 0; self.input_position = 0;