datui_lib/widgets/text_input/
history.rs1use color_eyre::Result;
13
14use crate::cache::CacheManager;
15
16pub fn push_entry(entries: &mut Vec<String>, entry: String) {
21 if entries.last() == Some(&entry) {
22 return;
23 }
24 entries.push(entry);
25}
26
27const LINE_BREAK_ON_DISK: char = '\u{2028}';
31
32fn to_disk(entry: &str) -> String {
33 entry.replace('\n', &LINE_BREAK_ON_DISK.to_string())
34}
35
36fn from_disk(entry: String) -> String {
37 if entry.contains(LINE_BREAK_ON_DISK) {
38 entry.replace(LINE_BREAK_ON_DISK, "\n")
39 } else {
40 entry
41 }
42}
43
44fn trim(entries: &mut Vec<String>, limit: usize) {
46 let excess = entries.len().saturating_sub(limit);
47 entries.drain(..excess);
48}
49
50#[derive(Debug, Clone, Default)]
53pub(super) struct InputHistory {
54 pub id: Option<String>,
55 entries: Vec<String>,
56 index: Option<usize>,
58 stash: Option<String>,
60 pub limit: usize,
61 loaded: bool,
62}
63
64impl InputHistory {
65 pub fn new(limit: usize) -> Self {
66 Self {
67 limit,
68 ..Self::default()
69 }
70 }
71
72 pub fn is_enabled(&self) -> bool {
73 self.id.is_some()
74 }
75
76 pub fn entries(&self) -> &[String] {
77 &self.entries
78 }
79
80 pub fn ensure_loaded(&mut self, cache: &CacheManager) -> Result<()> {
82 if self.loaded {
83 return Ok(());
84 }
85 if let Some(id) = &self.id {
86 self.entries = cache
87 .load_history_file(id)?
88 .into_iter()
89 .map(from_disk)
90 .collect();
91 self.loaded = true;
92 }
93 Ok(())
94 }
95
96 pub fn remember(&mut self, value: &str, cache: &CacheManager) -> Result<()> {
101 let Some(id) = self.id.clone() else {
102 return Ok(());
103 };
104 if value.is_empty() {
105 return Ok(());
106 }
107 push_entry(&mut self.entries, value.to_string());
108 trim(&mut self.entries, self.limit);
109
110 let entry = to_disk(value);
111 let limit = self.limit;
112 cache.update_history_file(&id, move |entries| {
113 push_entry(entries, entry);
114 trim(entries, limit);
115 })?;
116 Ok(())
117 }
118
119 #[cfg(test)]
121 pub fn seed(&mut self, entry: String) {
122 self.loaded = true;
123 push_entry(&mut self.entries, entry);
124 }
125
126 pub fn reset_position(&mut self) {
128 self.index = None;
129 self.stash = None;
130 }
131
132 pub fn older(&mut self, current: &str, cache: Option<&CacheManager>) -> Option<String> {
137 self.id.as_ref()?;
138 if !self.loaded {
139 let cache = cache?;
140 self.ensure_loaded(cache)
141 .inspect_err(|e| log::warn!(target: "datui", "read input history: {e:#}"))
142 .ok()?;
143 }
144 if self.entries.is_empty() {
145 return None;
146 }
147 if self.index.is_none() {
148 self.stash = Some(current.to_string());
149 }
150 let index = match self.index {
151 Some(current) => current.saturating_sub(1),
152 None => self.entries.len() - 1,
153 };
154 self.index = Some(index);
155 self.entries.get(index).cloned()
156 }
157
158 pub fn newer(&mut self) -> Option<String> {
161 self.id.as_ref()?;
162 let index = self.index?;
163 if index + 1 >= self.entries.len() {
164 let stashed = self.stash.take();
165 self.index = None;
166 return stashed;
167 }
168 self.index = Some(index + 1);
169 self.entries.get(index + 1).cloned()
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 fn history_with(entries: &[&str]) -> InputHistory {
178 let mut history = InputHistory::new(100);
179 history.id = Some("test".to_string());
180 history.entries = entries.iter().map(|s| s.to_string()).collect();
181 history.loaded = true;
182 history
183 }
184
185 #[test]
186 fn push_entry_skips_consecutive_duplicates_only() {
187 let mut entries = Vec::new();
188 push_entry(&mut entries, "query1".to_string());
189 push_entry(&mut entries, "query2".to_string());
190 push_entry(&mut entries, "query2".to_string());
191 push_entry(&mut entries, "query1".to_string());
192 assert_eq!(entries, vec!["query1", "query2", "query1"]);
193 }
194
195 #[test]
196 fn walking_back_and_forward_returns_to_the_draft() {
197 let mut history = history_with(&["one", "two", "three"]);
198 assert_eq!(history.older("draft", None).as_deref(), Some("three"));
199 assert_eq!(history.older("draft", None).as_deref(), Some("two"));
200 assert_eq!(history.older("draft", None).as_deref(), Some("one"));
201 assert_eq!(history.older("draft", None).as_deref(), Some("one"));
203 assert_eq!(history.newer().as_deref(), Some("two"));
204 assert_eq!(history.newer().as_deref(), Some("three"));
205 assert_eq!(history.newer().as_deref(), Some("draft"));
206 assert_eq!(history.newer(), None);
207 }
208
209 #[test]
210 fn a_statement_over_several_lines_is_one_entry_on_disk() {
211 let sql = "SELECT *\nFROM df";
212 assert!(!to_disk(sql).contains('\n'));
213 assert_eq!(from_disk(to_disk(sql)), sql);
214 assert_eq!(from_disk("select a".to_string()), "select a");
215 }
216
217 #[test]
218 fn walking_forward_without_walking_back_does_nothing() {
219 let mut history = history_with(&["one"]);
220 assert_eq!(history.newer(), None);
221 }
222
223 #[test]
224 fn history_is_inert_without_an_id() {
225 let mut history = InputHistory::new(100);
226 assert!(!history.is_enabled());
227 assert_eq!(history.older("draft", None), None);
228 assert_eq!(history.newer(), None);
229 }
230
231 #[test]
232 fn an_empty_history_has_nothing_to_walk() {
233 let mut history = history_with(&[]);
234 assert_eq!(history.older("draft", None), None);
235 }
236
237 #[test]
238 fn unloaded_history_needs_a_cache_to_walk() {
239 let mut history = InputHistory::new(100);
240 history.id = Some("test".to_string());
241 assert_eq!(history.older("draft", None), None);
242 }
243
244 fn temp_cache() -> (tempfile::TempDir, CacheManager) {
245 let dir = tempfile::tempdir().expect("temp dir");
246 let cache = CacheManager::with_dir(dir.path().to_path_buf());
247 (dir, cache)
248 }
249
250 #[test]
251 fn remembered_entries_survive_a_reload() {
252 let (_dir, cache) = temp_cache();
253 let mut history = InputHistory::new(100);
254 history.id = Some("query".to_string());
255 history.remember("select one", &cache).expect("remember");
256 history.remember("select two", &cache).expect("remember");
257
258 let mut reloaded = InputHistory::new(100);
259 reloaded.id = Some("query".to_string());
260 reloaded.ensure_loaded(&cache).expect("load");
261 assert_eq!(reloaded.entries(), ["select one", "select two"]);
262 }
263
264 #[test]
265 fn empty_values_are_not_remembered() {
266 let (_dir, cache) = temp_cache();
267 let mut history = InputHistory::new(100);
268 history.id = Some("query".to_string());
269 history.remember("", &cache).expect("remember");
270 assert!(history.entries().is_empty());
271 }
272
273 #[test]
274 fn the_limit_drops_the_oldest_entries() {
275 let (_dir, cache) = temp_cache();
276 let mut history = InputHistory::new(2);
277 history.id = Some("query".to_string());
278 for entry in ["a", "b", "c"] {
279 history.remember(entry, &cache).expect("remember");
280 }
281 assert_eq!(history.entries(), ["b", "c"]);
282
283 let mut reloaded = InputHistory::new(2);
284 reloaded.id = Some("query".to_string());
285 reloaded.ensure_loaded(&cache).expect("load");
286 assert_eq!(reloaded.entries(), ["b", "c"]);
287 }
288
289 #[test]
290 fn an_entry_written_by_another_instance_is_kept() {
291 let (_dir, cache) = temp_cache();
292 cache
293 .save_history_file("query", &["from elsewhere".to_string()])
294 .expect("seed file");
295
296 let mut history = InputHistory::new(100);
297 history.id = Some("query".to_string());
298 history.remember("mine", &cache).expect("remember");
299
300 let stored = cache.load_history_file("query").expect("load");
301 assert_eq!(stored, ["from elsewhere", "mine"]);
302 }
303
304 #[test]
305 fn a_history_without_an_id_is_never_persisted() {
306 let (_dir, cache) = temp_cache();
307 let mut history = InputHistory::new(100);
308 history.remember("ignored", &cache).expect("remember");
309 assert!(history.entries().is_empty());
310 }
311
312 #[test]
313 fn resetting_the_position_drops_the_stash() {
314 let mut history = history_with(&["one"]);
315 history.older("draft", None);
316 history.reset_position();
317 assert_eq!(history.newer(), None);
318 }
319}