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}