Skip to main content

promkit_widgets/text_editor/
history.rs

1use std::{
2    collections::VecDeque,
3    fs::File,
4    io::{BufRead, BufReader, Write},
5    path::Path,
6};
7
8use crate::cursor::Cursor;
9
10/// Manages the history of user inputs for a text editor.
11/// This structure allows for the storage,
12/// retrieval, and navigation through past inputs.
13/// It supports adding new entries,
14/// checking for the existence of specific entries,
15/// and moving through the history in both forward and backward directions.
16/// Additionally, it can limit the number of entries stored in the history
17/// to a specified maximum size.
18#[derive(Clone)]
19pub struct History {
20    /// Buffer storing the history of inputs as strings.
21    cursor: Cursor<VecDeque<String>>,
22
23    /// Optional limit on the number of entries in the history.
24    /// If set, the history will not exceed this number of entries,
25    /// and older entries will be removed to make room for new ones.
26    pub limit_size: Option<usize>,
27}
28
29impl Default for History {
30    /// Creates a new `History` instance with a single empty string in the buffer
31    /// and initializes the position at 0.
32    fn default() -> Self {
33        Self {
34            cursor: Cursor::new(VecDeque::from([String::new()]), 0, false),
35            limit_size: None,
36        }
37    }
38}
39
40impl History {
41    /// Saves the current history items to a file in reverse order (newest first),
42    /// respecting the optional limit on the number of entries if provided.
43    ///
44    /// # Arguments
45    ///
46    /// * `path` - The path to the file where the history should be saved.
47    ///
48    /// # Returns
49    ///
50    /// Returns `Ok(())` if the history was successfully saved, or an `io::Error` otherwise.
51    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
52        let mut file = File::create(path)?;
53        let mut contents = self.cursor.contents().clone();
54
55        // Note: The initial empty string ("") in the history
56        // is not included in the save operation.
57        contents.pop_back();
58
59        let items_to_save: Vec<_> = if let Some(limit) = self.limit_size {
60            contents.iter().rev().take(limit).collect()
61        } else {
62            contents.iter().rev().collect()
63        };
64
65        for item in items_to_save {
66            writeln!(file, "{}", item)?;
67        }
68        Ok(())
69    }
70
71    /// Loads history items from a file into a new `History` instance.
72    ///
73    /// This function reads the specified file line by line, adding each non-empty line
74    /// to the history. It respects the optional limit on the number of entries if provided.
75    /// An empty string is always added at the end of the history to represent a new input line.
76    /// After loading, the cursor is moved to the end of the history.
77    ///
78    /// # Arguments
79    ///
80    /// * `path` - The path to the file from which the history should be loaded.
81    /// * `limit_size` - An optional limit on the number of entries in the history.
82    ///
83    /// # Returns
84    ///
85    /// Returns `Ok(History)` with the loaded history if successful, or an `io::Error` otherwise.
86    pub fn load_from_file<P: AsRef<Path>>(
87        path: P,
88        limit_size: Option<usize>,
89    ) -> anyhow::Result<Self> {
90        let file = File::open(path)?;
91        let reader = BufReader::new(file);
92
93        let mut ret = Self {
94            limit_size,
95            ..Default::default()
96        };
97
98        for line in reader.lines() {
99            let line = line?;
100            if !line.is_empty() {
101                // Avoid adding empty lines
102                ret.cursor.contents_mut().push_back(line);
103            }
104        }
105        // Ensure there's always an empty string at the end of the buffer
106        ret.cursor.contents_mut().push_back(String::new());
107        ret.move_to_tail(); // Move cursor to the end after loading
108        Ok(ret)
109    }
110
111    /// Inserts a new item into the history.
112    ///
113    /// If the item does not already exist in the buffer,
114    /// it is inserted just before the last item.
115    /// This method ensures there is always an empty string
116    /// at the end of the buffer to represent
117    /// a new input line. After insertion,
118    /// the current position is moved to the end of the buffer.
119    ///
120    /// # Arguments
121    ///
122    /// * `item` - The item to be inserted into the history.
123    ///
124    /// # Examples
125    ///
126    /// - Initial state: `items = [""]`
127    /// - After inserting "abc": `items = ["abc", ""]`
128    /// - After inserting "xyz": `items = ["abc", "xyz", ""]`
129    pub fn insert<T: AsRef<str>>(&mut self, item: T) {
130        let item = item.as_ref().to_string();
131        if !self.exists(&item) {
132            let init_state = self.cursor.contents_mut().pop_back().unwrap();
133            self.cursor.contents_mut().push_back(item);
134            if let Some(limit) = self.limit_size
135                && limit < self.cursor.contents_mut().len()
136            {
137                self.cursor.contents_mut().pop_front();
138            }
139            self.cursor.contents_mut().push_back(init_state);
140        }
141        self.move_to_tail();
142    }
143
144    /// Retrieves the current item from the history
145    /// based on the current position.
146    /// Returns an empty string if the position is out of bounds.
147    pub fn get(&self) -> String {
148        self.cursor
149            .contents()
150            .get(self.cursor.position())
151            .unwrap_or(&String::new())
152            .to_string()
153    }
154
155    /// Checks whether a specific item exists in the history.
156    ///
157    /// # Arguments
158    ///
159    /// * `item` - The item to check for existence in the history.
160    ///
161    /// # Returns
162    ///
163    /// Returns `true` if the item exists in the history, `false` otherwise.
164    fn exists<T: AsRef<str>>(&self, item: T) -> bool {
165        self.cursor.contents().iter().any(|i| i == item.as_ref())
166    }
167
168    /// Moves the current position backward in the history, if possible.
169    /// Returns `true` if the position was successfully moved backward, `false` otherwise.
170    pub fn backward(&mut self) -> bool {
171        self.cursor.backward()
172    }
173
174    /// Moves the current position forward in the history, if possible.
175    /// Returns `true` if the position was successfully moved forward, `false` otherwise.
176    pub fn forward(&mut self) -> bool {
177        self.cursor.forward()
178    }
179
180    /// Moves the current position to the tail (end) of the history buffer.
181    pub fn move_to_tail(&mut self) {
182        self.cursor.move_to_tail()
183    }
184}
185
186#[cfg(test)]
187mod test {
188    mod insert {
189        use super::super::*;
190
191        #[test]
192        fn test() {
193            let mut h = History::default();
194            h.insert("item");
195            assert_eq!(
196                VecDeque::from([String::from("item"), String::new()]),
197                *h.cursor.contents()
198            );
199        }
200
201        #[test]
202        fn test_with_multiple_items() {
203            let mut h = History::default();
204            h.insert("item1");
205            h.insert("item2");
206            assert_eq!(
207                VecDeque::from([String::from("item1"), String::from("item2"), String::new()]),
208                *h.cursor.contents()
209            );
210        }
211
212        #[test]
213        fn test_with_limit_size() {
214            let mut h = History {
215                limit_size: Some(2),
216                ..Default::default()
217            };
218            h.insert("item1");
219            h.insert("item2");
220            h.insert("item3");
221            assert_eq!(
222                VecDeque::from([String::from("item2"), String::from("item3"), String::new()]),
223                *h.cursor.contents()
224            );
225        }
226    }
227
228    mod exists {
229        use super::super::*;
230
231        #[test]
232        fn test() {
233            let mut h = History::default();
234            h.insert("existed");
235            assert!(h.exists("existed"));
236            assert!(!h.exists("not_found"));
237        }
238    }
239}