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        if !path.exists() {
53            return;
54        }
55        let Err(pos) = self.content.binary_search(&path) else {
56            return;
57        };
58        self.content.insert(pos, path);
59    }
60
61    /// Toggle the flagged status of a path.
62    /// Remove the path from the content if it's flagged, flag it if it's not.
63    /// The implantation assumes the content to be sorted.
64    pub fn toggle(&mut self, path: &Path) {
65        let path = path.to_path_buf();
66        match self.content.binary_search(&path) {
67            Ok(pos) => self.remove_index(pos),
68            Err(pos) => self.content.insert(pos, path),
69        }
70    }
71
72    fn remove_index(&mut self, index: usize) {
73        self.content.remove(index);
74        if self.index >= self.len() {
75            self.index = self.index.saturating_sub(1);
76        }
77    }
78
79    /// True if the `path` is flagged.
80    /// Since we maintain the content sorted, we can use a binary search and
81    /// compensate a little bit with using a vector instead of a set.
82    #[inline]
83    #[must_use]
84    pub fn contains(&self, path: &Path) -> bool {
85        self.content.binary_search(&path.to_path_buf()).is_ok()
86    }
87
88    /// Returns a vector of path which are present in the current directory but ARE NOT the current dir.
89    /// This prevents the current directory (or root path in tree display mode) to be altered by bulk.
90    #[inline]
91    #[must_use]
92    pub fn in_dir(&self, dir: &Path) -> Vec<PathBuf> {
93        self.content
94            .iter()
95            .filter(|p| *p != dir)
96            .filter(|p| p.starts_with(dir))
97            .map(|p| p.to_owned())
98            .collect()
99    }
100
101    /// Returns a string with every path in content on a separate line.
102    pub fn content_to_string(&self) -> String {
103        self.content()
104            .iter()
105            .map(|path| path.to_string_lossy().into_owned())
106            .collect::<Vec<String>>()
107            .join("\n")
108    }
109
110    pub fn replace_by_string(&mut self, files: String) {
111        self.clear();
112        files.lines().for_each(|f| {
113            let p = PathBuf::from(tilde(f).as_ref());
114            if p.exists() {
115                self.push(p);
116            }
117        });
118    }
119
120    /// Returns the flagged files as a vector of strings
121    pub fn as_strings(&self) -> Vec<String> {
122        self.content
123            .iter()
124            .map(|p| p.to_string_lossy().to_string())
125            .collect()
126    }
127
128    fn should_this_file_be_opened_in_neovim(&self, path: &Path) -> bool {
129        matches!(Extension::matcher(extract_extension(path)), Extension::Text)
130    }
131
132    pub fn should_all_be_opened_in_neovim(&self) -> bool {
133        self.content()
134            .iter()
135            .all(|path| self.should_this_file_be_opened_in_neovim(path))
136    }
137
138    /// Remove all files from flagged which doesn't exists.
139    pub fn remove_non_existant(&mut self) {
140        let non_existant_indices: Vec<usize> = self
141            .content
142            .iter()
143            .enumerate()
144            .filter(|(_index, path)| !path.exists())
145            .map(|(index, _path)| index)
146            .rev()
147            .collect();
148        for index in non_existant_indices.iter() {
149            self.content.remove(*index);
150        }
151        self.index = min(self.index, self.len().saturating_sub(1))
152    }
153}
154
155impl_content!(Flagged, PathBuf);
156
157impl DrawMenu<PathBuf> for Flagged {}