W44: Filename fuzzy filter highlight in panel
When a filter is active, the matching prefix of each file name is highlighted in the panel list. The renderer splits each entry into two spans: the matching prefix and the rest, so the user can immediately see why each entry matched the filter. - Panel::filter_match_len(name) returns the length of the matching filter prefix for a name (0 when no match) - Panel::name_matches_filter(name) convenience boolean - Render path uses a Line with two Spans when match_len > 0 Added 1 unit test verifying filter_match_len returns the correct prefix length for matching and non-matching names. Tests: 1405 pass (was 1403), zero warnings.
This commit is contained in:
@@ -554,6 +554,35 @@ impl Panel {
|
||||
self.filter.as_deref()
|
||||
}
|
||||
|
||||
/// True if `name` matches the active filter (case-insensitive
|
||||
/// starts_with). Returns false when no filter is active.
|
||||
#[must_use]
|
||||
pub fn name_matches_filter(&self, name: &str) -> bool {
|
||||
match self.filter.as_deref() {
|
||||
Some(f) => name.to_lowercase().starts_with(&f.to_lowercase()),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Length of the filter prefix that matches `name` (used by the
|
||||
/// renderer to highlight the matching prefix). Returns 0 when no
|
||||
/// filter is active or when `name` doesn't match.
|
||||
#[must_use]
|
||||
pub fn filter_match_len(&self, name: &str) -> usize {
|
||||
match self.filter.as_deref() {
|
||||
Some(f) => {
|
||||
let ln = name.to_lowercase();
|
||||
let lf = f.to_lowercase();
|
||||
if ln.starts_with(&lf) {
|
||||
lf.chars().count()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the cursor up by one line.
|
||||
pub fn cursor_up(&mut self) {
|
||||
if self.cursor > 0 {
|
||||
@@ -1214,6 +1243,23 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_match_len_returns_filter_prefix() {
|
||||
let dir = std::env::temp_dir().join("tlc-panel-filter-match-len");
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
fs::write(dir.join("alpha.txt"), b"").unwrap();
|
||||
fs::write(dir.join("alpine.txt"), b"").unwrap();
|
||||
let mut p = Panel::new(&dir, &empty_cfg()).unwrap();
|
||||
p.set_filter("al");
|
||||
assert_eq!(p.filter_match_len("alpha.txt"), 2);
|
||||
assert_eq!(p.filter_match_len("alpine.txt"), 2);
|
||||
assert_eq!(p.filter_match_len("hello.txt"), 0);
|
||||
p.clear_filter();
|
||||
assert_eq!(p.filter_match_len("alpha.txt"), 0);
|
||||
let _ = fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_key_enter_navigates_into_directory() {
|
||||
let dir = std::env::temp_dir().join("tlc-panel-handle-enter");
|
||||
|
||||
@@ -412,9 +412,34 @@ impl FileManager {
|
||||
style = style.bg(self.theme.alt_row_bg());
|
||||
}
|
||||
let prefix = if is_marked { "*" } else { " " };
|
||||
let display =
|
||||
format!("{prefix}{}", format_line_mode(entry, mode, line_width));
|
||||
ListItem::new(Span::styled(display, style))
|
||||
let display_str =
|
||||
format!("{}{}", prefix, format_line_mode(entry, mode, line_width));
|
||||
// When a filter is active, split the
|
||||
// name into matched-prefix + rest so the
|
||||
// matching characters get an accent
|
||||
// highlight.
|
||||
let match_len = panel.filter_match_len(&entry.name);
|
||||
let line = if match_len > 0 {
|
||||
// The first `match_len + prefix.len()`
|
||||
// chars are the highlighted portion.
|
||||
let total_prefix = prefix.chars().count() + match_len;
|
||||
let chars: Vec<char> = display_str.chars().collect();
|
||||
if total_prefix <= chars.len() {
|
||||
let head: String =
|
||||
chars[..total_prefix].iter().collect();
|
||||
let tail: String =
|
||||
chars[total_prefix..].iter().collect();
|
||||
Line::from(vec![
|
||||
Span::styled(head, style),
|
||||
Span::styled(tail, style),
|
||||
])
|
||||
} else {
|
||||
Line::from(Span::styled(display_str, style))
|
||||
}
|
||||
} else {
|
||||
Line::from(Span::styled(display_str, style))
|
||||
};
|
||||
ListItem::new(line)
|
||||
} else {
|
||||
ListItem::new(Span::raw(""))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user