Skip to main content

fresh/view/file_tree/
search.rs

1//! File explorer search functionality
2//!
3//! Provides fuzzy search for quick navigation in the file explorer.
4//! Users can type characters to filter files/directories, with matching
5//! characters highlighted in the results.
6
7use crate::input::fuzzy::{fuzzy_match, FuzzyMatch};
8
9/// Search state for file explorer
10#[derive(Debug, Default, Clone)]
11pub struct FileExplorerSearch {
12    /// Current search query
13    query: String,
14}
15
16impl FileExplorerSearch {
17    /// Create a new empty search state
18    pub fn new() -> Self {
19        Self {
20            query: String::new(),
21        }
22    }
23
24    /// Get the current search query
25    pub fn query(&self) -> &str {
26        &self.query
27    }
28
29    /// Check if search is active (has query text)
30    pub fn is_active(&self) -> bool {
31        !self.query.is_empty()
32    }
33
34    /// Add a character to the search query
35    pub fn push_char(&mut self, c: char) {
36        self.query.push(c);
37    }
38
39    /// Remove the last character from the search query
40    pub fn pop_char(&mut self) {
41        self.query.pop();
42    }
43
44    /// Clear the search query
45    pub fn clear(&mut self) {
46        self.query.clear();
47    }
48
49    /// Match a file/directory name against the current query
50    ///
51    /// Returns Some(FuzzyMatch) if the name matches, None if search is inactive.
52    /// The FuzzyMatch contains match positions for highlighting.
53    pub fn match_name(&self, name: &str) -> Option<FuzzyMatch> {
54        if self.query.is_empty() {
55            return None;
56        }
57        let result = fuzzy_match(&self.query, name);
58        if result.matched {
59            Some(result)
60        } else {
61            None
62        }
63    }
64
65    /// Check if a name matches the current search query
66    pub fn matches(&self, name: &str) -> bool {
67        if self.query.is_empty() {
68            true // Empty query matches everything
69        } else {
70            fuzzy_match(&self.query, name).matched
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_empty_search_matches_all() {
81        let search = FileExplorerSearch::new();
82        assert!(!search.is_active());
83        assert!(search.matches("anything"));
84        assert!(search.matches("file.txt"));
85    }
86
87    #[test]
88    fn test_search_query_operations() {
89        let mut search = FileExplorerSearch::new();
90
91        search.push_char('f');
92        assert_eq!(search.query(), "f");
93        assert!(search.is_active());
94
95        search.push_char('o');
96        search.push_char('o');
97        assert_eq!(search.query(), "foo");
98
99        search.pop_char();
100        assert_eq!(search.query(), "fo");
101
102        search.clear();
103        assert_eq!(search.query(), "");
104        assert!(!search.is_active());
105    }
106
107    #[test]
108    fn test_fuzzy_matching() {
109        let mut search = FileExplorerSearch::new();
110        search.push_char('m');
111        search.push_char('r');
112        search.push_char('s');
113
114        // "mrs" should match "main.rs" (m...r.s)
115        assert!(search.matches("main.rs"));
116
117        // Should not match something without these chars in order
118        assert!(!search.matches("test.txt"));
119    }
120
121    #[test]
122    fn test_match_positions() {
123        let mut search = FileExplorerSearch::new();
124        search.push_char('m');
125        search.push_char('r');
126
127        let result = search.match_name("main.rs");
128        assert!(result.is_some());
129
130        let m = result.unwrap();
131        assert_eq!(m.match_positions.len(), 2);
132        assert_eq!(m.match_positions[0], 0); // 'm' at position 0
133        assert_eq!(m.match_positions[1], 5); // 'r' at position 5
134    }
135}