fm/modes/menu/
flagged.rs

1use std::{
2    cmp::min,
3    path::{Path, PathBuf},
4};
5
6use crate::common::tilde;
7use crate::io::{DrawMenu, Extension};
8use crate::modes::extract_extension;
9use crate::{impl_content, impl_selectable};
10
11/// The flagged files and an index, allowing navigation when the flagged files are displayed.
12/// We record here every flagged file by its path, allowing deletion, renaming, copying, moving and other actions.
13#[derive(Default)]
14pub struct Flagged {
15    /// Contains the different flagged files.
16    /// It's basically a `Set` (of whatever kind) and insertion would be faster
17    /// using a set.
18    /// Iteration is faster with a vector and we need a vector to use the common trait
19    /// `SelectableContent` which can be implemented with a macro.
20    /// We use binary search in every non standard method (insertion, removal, search).
21    pub content: Vec<PathBuf>,
22    /// The index of the selected file. Used to jump.
23    pub index: usize,
24}
25
26impl Flagged {
27    pub fn update(&mut self, content: Vec<PathBuf>) {
28        self.content = content;
29        self.content.sort();
30        self.index = 0;
31    }
32
33    pub fn extend(&mut self, mut content: Vec<PathBuf>) {
34        self.content.append(&mut content);
35        self.content.sort();
36        self.index = 0;
37    }
38
39    pub fn clear(&mut self) {
40        self.content = vec![];
41        self.index = 0;
42    }
43
44    pub fn remove_selected(&mut self) {
45        self.content.remove(self.index);
46        self.index = self.index.saturating_sub(1);
47    }
48
49    /// Push a new path into the content.
50    /// We maintain the content sorted and it's used to make `contains` faster.
51    pub fn push(&mut self, path: PathBuf) {
52        let Err(pos) = self.content.binary_search(&path) else {
53            return;
54        };
55        self.content.insert(pos, path);
56    }
57
58    /// Toggle the flagged status of a path.
59    /// Remove the path from the content if it's flagged, flag it if it's not.
60    /// The implantation assumes the content to be sorted.
61    pub fn toggle(&mut self, path: &Path) {
62        let path = path.to_path_buf();
63        match self.content.binary_search(&path) {
64            Ok(pos) => self.remove_index(pos),
65            Err(pos) => self.content.insert(pos, path),
66        }
67    }
68
69    fn remove_index(&mut self, index: usize) {
70        self.content.remove(index);
71        if self.index >= self.len() {
72            self.index = self.index.saturating_sub(1);
73        }
74    }
75
76    /// True if the `path` is flagged.
77    /// Since we maintain the content sorted, we can use a binary search and
78    /// compensate a little bit with using a vector instead of a set.
79    #[inline]
80    #[must_use]
81    pub fn contains(&self, path: &Path) -> bool {
82        self.content.binary_search(&path.to_path_buf()).is_ok()
83    }
84
85    /// Returns a vector of path which are present in the current directory.
86    #[inline]
87    #[must_use]
88    pub fn in_dir(&self, dir: &Path) -> Vec<PathBuf> {
89        self.content
90            .iter()
91            .filter(|p| p.starts_with(dir))
92            .map(|p| p.to_owned())
93            .collect()
94    }
95
96    /// Returns a string with every path in content on a separate line.
97    pub fn content_to_string(&self) -> String {
98        self.content()
99            .iter()
100            .map(|path| path.to_string_lossy().into_owned())
101            .collect::<Vec<String>>()
102            .join("\n")
103    }
104
105    pub fn replace_by_string(&mut self, files: String) {
106        self.clear();
107        files.lines().for_each(|f| {
108            let p = PathBuf::from(tilde(f).as_ref());
109            if p.exists() {
110                self.push(p);
111            }
112        });
113    }
114
115    /// Returns the flagged files as a vector of strings
116    pub fn as_strings(&self) -> Vec<String> {
117        self.content
118            .iter()
119            .map(|p| p.to_string_lossy().to_string())
120            .collect()
121    }
122
123    fn should_this_file_be_opened_in_neovim(&self, path: &Path) -> bool {
124        matches!(Extension::matcher(extract_extension(path)), Extension::Text)
125    }
126
127    pub fn should_all_be_opened_in_neovim(&self) -> bool {
128        self.content()
129            .iter()
130            .all(|path| self.should_this_file_be_opened_in_neovim(path))
131    }
132
133    /// Remove all files from flagged which doesn't exists.
134    pub fn remove_non_existant(&mut self) {
135        let non_existant_indices: Vec<usize> = self
136            .content
137            .iter()
138            .enumerate()
139            .filter(|(_index, path)| !path.exists())
140            .map(|(index, _path)| index)
141            .rev()
142            .collect();
143        for index in non_existant_indices.iter() {
144            self.content.remove(*index);
145        }
146        self.index = min(self.index, self.len().saturating_sub(1))
147    }
148}
149
150impl_selectable!(Flagged);
151impl_content!(Flagged, PathBuf);
152
153impl DrawMenu<PathBuf> for Flagged {}