fm/modes/menu/
search.rs

1use std::path::PathBuf;
2
3use anyhow::Result;
4
5use crate::app::Tab;
6use crate::modes::{CaseDependantRegex, Display, FileInfo, Go, IndexToIndex, To, ToPath, Tree};
7
8/// The current search term.
9/// it records the regex used, the matched paths and where we are in those pathes.
10/// The pathes are refreshed every time we jump to another match, allowing the
11/// display to stay updated.
12pub struct Search {
13    pub regex: CaseDependantRegex,
14    pub paths: Vec<PathBuf>,
15    pub index: usize,
16}
17
18impl std::fmt::Display for Search {
19    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
20        if self.is_empty() {
21            write!(f, "")
22        } else {
23            write!(
24                f,
25                " Searched: {regex} - {pos} / {len} ",
26                regex = self.regex,
27                pos = self.index + 1 - self.paths.is_empty() as usize,
28                len = self.paths.len()
29            )
30        }
31    }
32}
33
34impl Search {
35    pub fn empty() -> Self {
36        Self {
37            regex: CaseDependantRegex::new("").unwrap(),
38            paths: vec![],
39            index: 0,
40        }
41    }
42
43    pub fn new(searched: &str) -> Result<Self> {
44        Ok(Self {
45            regex: CaseDependantRegex::new(searched)?,
46            paths: vec![],
47            index: 0,
48        })
49    }
50
51    pub fn clone_with_regex(&self) -> Self {
52        Self {
53            regex: self.regex.clone(),
54            paths: vec![],
55            index: 0,
56        }
57    }
58
59    pub fn is_empty(&self) -> bool {
60        self.regex.is_empty()
61    }
62
63    pub fn reset_paths(&mut self) {
64        self.paths = vec![];
65        self.index = 0;
66    }
67
68    pub fn select_next(&mut self) -> Option<PathBuf> {
69        if !self.paths.is_empty() && !self.regex.to_string().is_empty() {
70            self.index = (self.index + 1) % self.paths.len();
71            return Some(self.paths[self.index].to_owned());
72        }
73        None
74    }
75
76    pub fn execute_search(&mut self, tab: &mut Tab) -> Result<()> {
77        match tab.display_mode {
78            Display::Tree => {
79                self.tree(&mut tab.tree);
80            }
81            Display::Directory => {
82                self.directory(tab);
83            }
84            _ => (),
85        };
86        Ok(())
87    }
88
89    /// Search in current directory for an file whose name contains `searched_name`,
90    /// from a starting position `next_index`.
91    /// We search forward from that position and start again from top if nothing is found.
92    /// We move the selection to the first matching file.
93    #[inline]
94    fn directory(&mut self, tab: &mut Tab) {
95        let current_index = tab.directory.index;
96        let mut next_index = current_index;
97        let mut found = false;
98        for (index, file) in tab.directory.enumerate().skip(current_index) {
99            if self.regex.is_match(&file.filename) {
100                (next_index, found) = self.set_found(index, file, next_index, found);
101            }
102        }
103        for (index, file) in tab.directory.enumerate().take(current_index) {
104            if self.regex.is_match(&file.filename) {
105                (next_index, found) = self.set_found(index, file, next_index, found);
106            }
107        }
108        tab.go_to_index(next_index);
109    }
110
111    #[inline]
112    fn set_found(
113        &mut self,
114        index: usize,
115        file: &FileInfo,
116        mut next_index: usize,
117        mut found: bool,
118    ) -> (usize, bool) {
119        if !found {
120            next_index = index;
121            self.index = self.paths.len();
122            found = true;
123        }
124        self.paths.push(file.path.to_path_buf());
125
126        (next_index, found)
127    }
128
129    pub fn directory_search_next<'a>(
130        &mut self,
131        files: impl Iterator<Item = &'a FileInfo>,
132    ) -> Option<PathBuf> {
133        let (paths, Some(next_index), Some(next_path)) = self.directory_update_search(files) else {
134            return None;
135        };
136        self.set_index_paths(next_index, paths);
137        Some(next_path)
138    }
139
140    fn directory_update_search<'a>(
141        &self,
142        files: impl std::iter::Iterator<Item = &'a FileInfo>,
143    ) -> (Vec<PathBuf>, Option<usize>, Option<PathBuf>) {
144        let mut paths = vec![];
145        let mut next_index = None;
146        let mut next_path = None;
147
148        for file in files {
149            if self.regex.is_match(&file.filename) {
150                if next_index.is_none() {
151                    (next_index, next_path) = self.found_first_match(file)
152                }
153                paths.push(file.path.to_path_buf());
154            }
155        }
156        (paths, next_index, next_path)
157    }
158
159    fn found_first_match(&self, file: &FileInfo) -> (Option<usize>, Option<PathBuf>) {
160        (Some(self.paths.len()), Some(file.path.to_path_buf()))
161    }
162
163    pub fn set_index_paths(&mut self, index: usize, paths: Vec<PathBuf>) {
164        self.index = index;
165        self.paths = paths;
166    }
167
168    pub fn tree(&mut self, tree: &mut Tree) {
169        if let Some(path) = &self.tree_find_next_path(tree) {
170            tree.go(To::Path(path));
171        }
172    }
173
174    fn tree_find_next_path(&mut self, tree: &mut Tree) -> Option<PathBuf> {
175        if let Some(path) = self.select_next() {
176            return Some(path);
177        }
178        self.tree_search_again(tree)
179    }
180
181    fn tree_search_again(&mut self, tree: &mut Tree) -> Option<PathBuf> {
182        let mut next_path = None;
183        for line in tree.index_to_index() {
184            let Some(filename) = line.path.file_name() else {
185                continue;
186            };
187            if self.regex.is_match(&filename.to_string_lossy()) {
188                let match_path = line.path.to_path_buf();
189                if next_path.is_none() {
190                    self.index = self.paths.len();
191                    next_path = Some(match_path.clone());
192                }
193                self.paths.push(match_path);
194            }
195        }
196        next_path
197    }
198
199    #[inline]
200    pub fn matches_from(&self, content: &[impl ToPath]) -> Vec<String> {
201        content
202            .iter()
203            .filter_map(|e| e.to_path().file_name())
204            .map(|s| s.to_string_lossy().to_string())
205            .filter(|p| self.regex.is_match(p))
206            .collect()
207    }
208
209    #[inline]
210    pub fn is_match(&self, filename: &str) -> bool {
211        !self.is_empty() && self.regex.is_match(filename)
212    }
213}