Skip to content

Commit 0b4f33c

Browse files
Implement case insensitive, fix word disappearing bug
Use regex for case insesitive finding, implement String instead of char<Vec>, fix word disappearing by recalculating the render x for preview text
1 parent 190c26c commit 0b4f33c

File tree

3 files changed

+68
-56
lines changed

3 files changed

+68
-56
lines changed

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tui/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ ratatui = "0.29.0"
2121
tui-term = "0.2.0"
2222
temp-dir = "0.1.14"
2323
time = { version = "0.3.36", features = ["local-offset", "macros", "formatting"] }
24-
unicode-width = "0.2.0"
2524
rand = { version = "0.8.5", optional = true }
2625
linutil_core = { path = "../core", version = "24.9.28" }
2726
tree-sitter-highlight = "0.24.3"
@@ -30,6 +29,7 @@ textwrap = "0.16.1"
3029
anstyle = "1.0.8"
3130
ansi-to-tui = "7.0.0"
3231
zips = "0.1.7"
32+
regex = { version = "1.3", default-features = false, features = ["std"] }
3333

3434
[[bin]]
3535
name = "linutil"

tui/src/filter.rs

+66-54
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use ratatui::{
88
widgets::{Block, Borders, Paragraph},
99
Frame,
1010
};
11-
use unicode_width::UnicodeWidthChar;
11+
use regex::RegexBuilder;
1212

1313
pub enum SearchAction {
1414
None,
@@ -17,7 +17,7 @@ pub enum SearchAction {
1717
}
1818

1919
pub struct Filter {
20-
search_input: Vec<char>,
20+
search_input: String,
2121
in_search_mode: bool,
2222
input_position: usize,
2323
items: Vec<ListEntry>,
@@ -27,7 +27,7 @@ pub struct Filter {
2727
impl Filter {
2828
pub fn new() -> Self {
2929
Self {
30-
search_input: vec![],
30+
search_input: String::new(),
3131
in_search_mode: false,
3232
input_position: 0,
3333
items: vec![],
@@ -62,56 +62,56 @@ impl Filter {
6262
.collect();
6363
} else {
6464
self.items.clear();
65-
66-
let query_lower = self.search_input.iter().collect::<String>().to_lowercase();
67-
for tab in tabs.iter() {
68-
let mut stack = vec![tab.tree.root().id()];
69-
while let Some(node_id) = stack.pop() {
70-
let node = tab.tree.get(node_id).unwrap();
71-
72-
if node.value().name.to_lowercase().contains(&query_lower)
73-
&& !node.has_children()
74-
{
75-
self.items.push(ListEntry {
76-
node: node.value().clone(),
77-
id: node.id(),
78-
has_children: false,
79-
});
65+
if let Ok(regex) = self.regex_builder(&regex::escape(&self.search_input)) {
66+
for tab in tabs {
67+
let mut stack = vec![tab.tree.root().id()];
68+
while let Some(node_id) = stack.pop() {
69+
let node = tab.tree.get(node_id).unwrap();
70+
if regex.is_match(&node.value().name) && !node.has_children() {
71+
self.items.push(ListEntry {
72+
node: node.value().clone(),
73+
id: node.id(),
74+
has_children: false,
75+
});
76+
}
77+
stack.extend(node.children().map(|child| child.id()));
8078
}
81-
82-
stack.extend(node.children().map(|child| child.id()));
8379
}
80+
self.items
81+
.sort_unstable_by(|a, b| a.node.name.cmp(&b.node.name));
82+
} else {
83+
self.search_input.clear();
8484
}
85-
self.items.sort_by(|a, b| a.node.name.cmp(&b.node.name));
8685
}
87-
8886
self.update_completion_preview();
8987
}
9088

9189
fn update_completion_preview(&mut self) {
92-
if self.search_input.is_empty() {
93-
self.completion_preview = None;
94-
return;
95-
}
96-
97-
let input = self.search_input.iter().collect::<String>().to_lowercase();
98-
self.completion_preview = self.items.iter().find_map(|item| {
99-
let item_name_lower = item.node.name.to_lowercase();
100-
if item_name_lower.starts_with(&input) {
101-
Some(item_name_lower[input.len()..].to_string())
90+
self.completion_preview = if self.items.is_empty() || self.search_input.is_empty() {
91+
None
92+
} else {
93+
let pattern = format!("(?i)^{}", regex::escape(&self.search_input));
94+
if let Ok(regex) = self.regex_builder(&pattern) {
95+
self.items.iter().find_map(|item| {
96+
regex
97+
.find(&item.node.name)
98+
.map(|mat| item.node.name[mat.end()..].to_string())
99+
})
102100
} else {
103101
None
104102
}
105-
});
103+
}
106104
}
107105

108106
pub fn draw_searchbar(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
109107
//Set the search bar text (If empty use the placeholder)
110108
let display_text = if !self.in_search_mode && self.search_input.is_empty() {
111109
Span::raw("Press / to search")
112110
} else {
113-
let input_text = self.search_input.iter().collect::<String>();
114-
Span::styled(input_text, Style::default().fg(theme.focused_color()))
111+
Span::styled(
112+
&self.search_input,
113+
Style::default().fg(theme.focused_color()),
114+
)
115115
};
116116

117117
let search_color = if self.in_search_mode {
@@ -135,25 +135,16 @@ impl Filter {
135135

136136
// Render cursor in search bar
137137
if self.in_search_mode {
138-
let cursor_position: usize = self.search_input[..self.input_position]
139-
.iter()
140-
.map(|c| c.width().unwrap_or(1))
141-
.sum();
142-
let x = area.x + cursor_position as u16 + 1;
138+
let x = area.x + self.input_position as u16 + 1;
143139
let y = area.y + 1;
144140
frame.set_cursor_position(Position::new(x, y));
145141

146142
if let Some(preview) = &self.completion_preview {
143+
let preview_x = area.x + self.search_input.len() as u16 + 1;
147144
let preview_span =
148145
Span::styled(preview, Style::default().fg(theme.search_preview_color()));
149-
let preview_paragraph = Paragraph::new(preview_span).style(Style::default());
150-
let preview_area = Rect::new(
151-
x,
152-
y,
153-
(preview.len() as u16).min(area.width - cursor_position as u16 - 1),
154-
1,
155-
);
156-
frame.render_widget(preview_paragraph, preview_area);
146+
let preview_area = Rect::new(preview_x, y, preview.len() as u16, 1);
147+
frame.render_widget(Paragraph::new(preview_span), preview_area);
157148
}
158149
}
159150
}
@@ -220,14 +211,35 @@ impl Filter {
220211
}
221212
}
222213

214+
fn regex_builder(&self, pattern: &str) -> Result<regex::Regex, regex::Error> {
215+
RegexBuilder::new(pattern).case_insensitive(true).build()
216+
}
217+
223218
fn complete_search(&mut self) -> SearchAction {
224-
if let Some(completion) = self.completion_preview.take() {
225-
self.search_input.extend(completion.chars());
226-
self.input_position = self.search_input.len();
227-
self.update_completion_preview();
228-
SearchAction::Update
229-
} else {
219+
if self.completion_preview.is_none() {
230220
SearchAction::None
221+
} else {
222+
let pattern = format!("(?i)^{}", self.search_input);
223+
if let Ok(regex) = self.regex_builder(&pattern) {
224+
self.search_input = self
225+
.items
226+
.iter()
227+
.find_map(|item| {
228+
if regex.is_match(&item.node.name) {
229+
Some(item.node.name.clone())
230+
} else {
231+
None
232+
}
233+
})
234+
.unwrap_or_default();
235+
236+
self.completion_preview = None;
237+
self.input_position = self.search_input.len();
238+
239+
SearchAction::Update
240+
} else {
241+
SearchAction::None
242+
}
231243
}
232244
}
233245

0 commit comments

Comments
 (0)