Skip to content

Commit 5878f4d

Browse files
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 <[email protected]> Co-authored-by: Adam Perkowski <[email protected]>
1 parent 58f3433 commit 5878f4d

File tree

1 file changed

+66
-4
lines changed

1 file changed

+66
-4
lines changed

tui/src/filter.rs

+66-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use ego_tree::NodeId;
44
use linutil_core::Tab;
55
use ratatui::{
66
layout::{Position, Rect},
7-
style::Style,
7+
style::{Color, Style},
88
text::Span,
99
widgets::{Block, Borders, Paragraph},
1010
Frame,
@@ -22,6 +22,7 @@ pub struct Filter {
2222
in_search_mode: bool,
2323
input_position: usize,
2424
items: Vec<ListEntry>,
25+
completion_preview: Option<String>,
2526
}
2627

2728
impl Filter {
@@ -31,17 +32,23 @@ impl Filter {
3132
in_search_mode: false,
3233
input_position: 0,
3334
items: vec![],
35+
completion_preview: None,
3436
}
3537
}
38+
3639
pub fn item_list(&self) -> &[ListEntry] {
3740
&self.items
3841
}
42+
3943
pub fn activate_search(&mut self) {
4044
self.in_search_mode = true;
4145
}
46+
4247
pub fn deactivate_search(&mut self) {
4348
self.in_search_mode = false;
49+
self.completion_preview = None;
4450
}
51+
4552
pub fn update_items(&mut self, tabs: &[Tab], current_tab: usize, node: NodeId) {
4653
if self.search_input.is_empty() {
4754
let curr = tabs[current_tab].tree.get(node).unwrap();
@@ -78,13 +85,34 @@ impl Filter {
7885
}
7986
self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name));
8087
}
88+
89+
self.update_completion_preview();
90+
}
91+
92+
fn update_completion_preview(&mut self) {
93+
if self.search_input.is_empty() {
94+
self.completion_preview = None;
95+
return;
96+
}
97+
98+
let input = self.search_input.iter().collect::<String>().to_lowercase();
99+
self.completion_preview = self.items.iter().find_map(|item| {
100+
let item_name_lower = item.node.name.to_lowercase();
101+
if item_name_lower.starts_with(&input) {
102+
Some(item_name_lower[input.len()..].to_string())
103+
} else {
104+
None
105+
}
106+
});
81107
}
108+
82109
pub fn draw_searchbar(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
83110
//Set the search bar text (If empty use the placeholder)
84111
let display_text = if !self.in_search_mode && self.search_input.is_empty() {
85112
Span::raw("Press / to search")
86113
} else {
87-
Span::raw(self.search_input.iter().collect::<String>())
114+
let input_text = self.search_input.iter().collect::<String>();
115+
Span::styled(input_text, Style::default().fg(theme.focused_color()))
88116
};
89117

90118
let search_color = if self.in_search_mode {
@@ -110,11 +138,22 @@ impl Filter {
110138
let x = area.x + cursor_position as u16 + 1;
111139
let y = area.y + 1;
112140
frame.set_cursor_position(Position::new(x, y));
141+
142+
if let Some(preview) = &self.completion_preview {
143+
let preview_span = Span::styled(preview, Style::default().fg(Color::DarkGray));
144+
let preview_paragraph = Paragraph::new(preview_span).style(Style::default());
145+
let preview_area = Rect::new(
146+
x,
147+
y,
148+
(preview.len() as u16).min(area.width - cursor_position as u16 - 1),
149+
1,
150+
);
151+
frame.render_widget(preview_paragraph, preview_area);
152+
}
113153
}
114154
}
115155
// Handles key events. Returns true if search must be exited
116156
pub fn handle_key(&mut self, event: &KeyEvent) -> SearchAction {
117-
//Insert user input into the search bar
118157
match event.code {
119158
KeyCode::Char('c') if event.modifiers.contains(KeyModifiers::CONTROL) => {
120159
return self.exit_search()
@@ -124,10 +163,17 @@ impl Filter {
124163
KeyCode::Delete => self.remove_next(),
125164
KeyCode::Left => return self.cursor_left(),
126165
KeyCode::Right => return self.cursor_right(),
166+
KeyCode::Tab => return self.complete_search(),
167+
KeyCode::Esc => {
168+
self.input_position = 0;
169+
self.search_input.clear();
170+
self.completion_preview = None;
171+
return SearchAction::Exit;
172+
}
127173
KeyCode::Enter => return SearchAction::Exit,
128-
KeyCode::Esc => return self.exit_search(),
129174
_ => return SearchAction::None,
130175
};
176+
self.update_completion_preview();
131177
SearchAction::Update
132178
}
133179

@@ -141,29 +187,45 @@ impl Filter {
141187
self.input_position = self.input_position.saturating_sub(1);
142188
SearchAction::None
143189
}
190+
144191
fn cursor_right(&mut self) -> SearchAction {
145192
if self.input_position < self.search_input.len() {
146193
self.input_position += 1;
147194
}
148195
SearchAction::None
149196
}
197+
150198
fn insert_char(&mut self, input: char) {
151199
self.search_input.insert(self.input_position, input);
152200
self.cursor_right();
153201
}
202+
154203
fn remove_previous(&mut self) {
155204
let current = self.input_position;
156205
if current > 0 {
157206
self.search_input.remove(current - 1);
158207
self.cursor_left();
159208
}
160209
}
210+
161211
fn remove_next(&mut self) {
162212
let current = self.input_position;
163213
if current < self.search_input.len() {
164214
self.search_input.remove(current);
165215
}
166216
}
217+
218+
fn complete_search(&mut self) -> SearchAction {
219+
if let Some(completion) = self.completion_preview.take() {
220+
self.search_input.extend(completion.chars());
221+
self.input_position = self.search_input.len();
222+
self.update_completion_preview();
223+
SearchAction::Update
224+
} else {
225+
SearchAction::None
226+
}
227+
}
228+
167229
pub fn clear_search(&mut self) {
168230
self.search_input.clear();
169231
self.input_position = 0;

0 commit comments

Comments
 (0)