1use crate::{History, HistoryNavigationQuery, HistorySessionId};
2
3use super::base::CommandLineSearch;
4use super::base::SearchDirection;
5use super::base::SearchFilter;
6use super::HistoryItem;
7use super::SearchQuery;
8use crate::Result;
9
10#[derive(Debug)]
12pub struct HistoryCursor {
13 query: HistoryNavigationQuery,
14 current: Option<HistoryItem>,
15 skip_dupes: bool,
16 session: Option<HistorySessionId>,
17}
18
19impl HistoryCursor {
20 pub fn new(query: HistoryNavigationQuery, session: Option<HistorySessionId>) -> HistoryCursor {
21 HistoryCursor {
22 query,
23 current: None,
24 skip_dupes: true,
25 session,
26 }
27 }
28
29 pub fn back(&mut self, history: &dyn History) -> Result<()> {
32 self.navigate_in_direction(history, SearchDirection::Backward)
33 }
34
35 pub fn forward(&mut self, history: &dyn History) -> Result<()> {
38 self.navigate_in_direction(history, SearchDirection::Forward)
39 }
40
41 fn get_search_filter(&self) -> SearchFilter {
42 let filter = match self.query.clone() {
43 HistoryNavigationQuery::Normal(_) => SearchFilter::anything(self.session),
44 HistoryNavigationQuery::PrefixSearch(prefix) => {
45 SearchFilter::from_text_search(CommandLineSearch::Prefix(prefix), self.session)
46 }
47 HistoryNavigationQuery::SubstringSearch(substring) => SearchFilter::from_text_search(
48 CommandLineSearch::Substring(substring),
49 self.session,
50 ),
51 };
52 if let (true, Some(current)) = (self.skip_dupes, &self.current) {
53 SearchFilter {
54 not_command_line: Some(current.command_line.clone()),
55 ..filter
56 }
57 } else {
58 filter
59 }
60 }
61 fn navigate_in_direction(
62 &mut self,
63 history: &dyn History,
64 direction: SearchDirection,
65 ) -> Result<()> {
66 if direction == SearchDirection::Forward && self.current.is_none() {
67 return Ok(());
69 }
70 let start_id = self.current.as_ref().and_then(|e| e.id);
71 let mut next = history.search(SearchQuery {
72 start_id,
73 end_id: None,
74 start_time: None,
75 end_time: None,
76 direction,
77 limit: Some(1),
78 filter: self.get_search_filter(),
79 })?;
80 if next.len() == 1 {
81 self.current = Some(next.swap_remove(0));
82 } else if direction == SearchDirection::Forward {
83 self.current = None;
85 }
86 Ok(())
87 }
88
89 pub fn string_at_cursor(&self) -> Option<String> {
91 self.current.as_ref().map(|e| e.command_line.to_string())
92 }
93
94 pub fn get_navigation(&self) -> HistoryNavigationQuery {
96 self.query.clone()
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use std::path::Path;
103
104 use pretty_assertions::assert_eq;
105
106 use crate::LineBuffer;
107
108 use super::super::*;
109 use super::*;
110
111 fn create_history() -> (Box<dyn History>, HistoryCursor) {
112 #[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
113 let hist = Box::new(SqliteBackedHistory::in_memory().unwrap());
114 #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
115 let hist = Box::<FileBackedHistory>::default();
116 (
117 hist,
118 HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default()), None),
119 )
120 }
121 fn create_history_at(cap: usize, path: &Path) -> (Box<dyn History>, HistoryCursor) {
122 let hist = Box::new(FileBackedHistory::with_file(cap, path.to_owned()).unwrap());
123 (
124 hist,
125 HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default()), None),
126 )
127 }
128
129 fn get_all_entry_texts(hist: &dyn History) -> Vec<String> {
130 let res = hist
131 .search(SearchQuery::everything(SearchDirection::Forward, None))
132 .unwrap();
133 let actual: Vec<_> = res.iter().map(|e| e.command_line.to_string()).collect();
134 actual
135 }
136 fn add_text_entries(hist: &mut dyn History, entries: &[impl AsRef<str>]) {
137 entries.iter().for_each(|e| {
138 hist.save(HistoryItem::from_command_line(e.as_ref()))
139 .unwrap();
140 });
141 }
142
143 #[test]
144 fn accessing_empty_history_returns_nothing() -> Result<()> {
145 let (_hist, cursor) = create_history();
146 assert_eq!(cursor.string_at_cursor(), None);
147 Ok(())
148 }
149
150 #[test]
151 fn going_forward_in_empty_history_does_not_error_out() -> Result<()> {
152 let (hist, mut cursor) = create_history();
153 cursor.forward(&*hist)?;
154 assert_eq!(cursor.string_at_cursor(), None);
155 Ok(())
156 }
157
158 #[test]
159 fn going_backwards_in_empty_history_does_not_error_out() -> Result<()> {
160 let (hist, mut cursor) = create_history();
161 cursor.back(&*hist)?;
162 assert_eq!(cursor.string_at_cursor(), None);
163 Ok(())
164 }
165
166 #[test]
167 fn going_backwards_bottoms_out() -> Result<()> {
168 let (mut hist, mut cursor) = create_history();
169 hist.save(HistoryItem::from_command_line("command1"))?;
170 hist.save(HistoryItem::from_command_line("command2"))?;
171 cursor.back(&*hist)?;
172 cursor.back(&*hist)?;
173 cursor.back(&*hist)?;
174 cursor.back(&*hist)?;
175 cursor.back(&*hist)?;
176 assert_eq!(cursor.string_at_cursor(), Some("command1".to_string()));
177 Ok(())
178 }
179
180 #[test]
181 fn going_forwards_bottoms_out() -> Result<()> {
182 let (mut hist, mut cursor) = create_history();
183 hist.save(HistoryItem::from_command_line("command1"))?;
184 hist.save(HistoryItem::from_command_line("command2"))?;
185 cursor.forward(&*hist)?;
186 cursor.forward(&*hist)?;
187 cursor.forward(&*hist)?;
188 cursor.forward(&*hist)?;
189 cursor.forward(&*hist)?;
190 assert_eq!(cursor.string_at_cursor(), None);
191 Ok(())
192 }
193
194 #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
195 #[test]
196 fn appends_only_unique() -> Result<()> {
197 let (mut hist, _) = create_history();
198 hist.save(HistoryItem::from_command_line("unique_old"))?;
199 hist.save(HistoryItem::from_command_line("test"))?;
200 hist.save(HistoryItem::from_command_line("test"))?;
201 hist.save(HistoryItem::from_command_line("unique"))?;
202 assert_eq!(hist.count_all()?, 3);
203 Ok(())
204 }
205
206 #[test]
207 fn prefix_search_works() -> Result<()> {
208 let (mut hist, _) = create_history();
209 hist.save(HistoryItem::from_command_line("find me as well"))?;
210 hist.save(HistoryItem::from_command_line("test"))?;
211 hist.save(HistoryItem::from_command_line("find me"))?;
212
213 let mut cursor = HistoryCursor::new(
214 HistoryNavigationQuery::PrefixSearch("find".to_string()),
215 None,
216 );
217
218 cursor.back(&*hist)?;
219 assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
220 cursor.back(&*hist)?;
221 assert_eq!(
222 cursor.string_at_cursor(),
223 Some("find me as well".to_string())
224 );
225 Ok(())
226 }
227
228 #[test]
229 fn prefix_search_bottoms_out() -> Result<()> {
230 let (mut hist, _) = create_history();
231 hist.save(HistoryItem::from_command_line("find me as well"))?;
232 hist.save(HistoryItem::from_command_line("test"))?;
233 hist.save(HistoryItem::from_command_line("find me"))?;
234
235 let mut cursor = HistoryCursor::new(
236 HistoryNavigationQuery::PrefixSearch("find".to_string()),
237 None,
238 );
239 cursor.back(&*hist)?;
240 assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
241 cursor.back(&*hist)?;
242 assert_eq!(
243 cursor.string_at_cursor(),
244 Some("find me as well".to_string())
245 );
246 cursor.back(&*hist)?;
247 cursor.back(&*hist)?;
248 cursor.back(&*hist)?;
249 cursor.back(&*hist)?;
250 assert_eq!(
251 cursor.string_at_cursor(),
252 Some("find me as well".to_string())
253 );
254 Ok(())
255 }
256 #[test]
257 fn prefix_search_returns_to_none() -> Result<()> {
258 let (mut hist, _) = create_history();
259 hist.save(HistoryItem::from_command_line("find me as well"))?;
260 hist.save(HistoryItem::from_command_line("test"))?;
261 hist.save(HistoryItem::from_command_line("find me"))?;
262
263 let mut cursor = HistoryCursor::new(
264 HistoryNavigationQuery::PrefixSearch("find".to_string()),
265 None,
266 );
267 cursor.back(&*hist)?;
268 assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
269 cursor.back(&*hist)?;
270 assert_eq!(
271 cursor.string_at_cursor(),
272 Some("find me as well".to_string())
273 );
274 cursor.forward(&*hist)?;
275 assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
276 cursor.forward(&*hist)?;
277 assert_eq!(cursor.string_at_cursor(), None);
278 cursor.forward(&*hist)?;
279 assert_eq!(cursor.string_at_cursor(), None);
280 Ok(())
281 }
282
283 #[test]
284 fn prefix_search_ignores_consecutive_equivalent_entries_going_backwards() -> Result<()> {
285 let (mut hist, _) = create_history();
286 hist.save(HistoryItem::from_command_line("find me as well"))?;
287 hist.save(HistoryItem::from_command_line("find me once"))?;
288 hist.save(HistoryItem::from_command_line("test"))?;
289 hist.save(HistoryItem::from_command_line("find me once"))?;
290
291 let mut cursor = HistoryCursor::new(
292 HistoryNavigationQuery::PrefixSearch("find".to_string()),
293 None,
294 );
295 cursor.back(&*hist)?;
296 assert_eq!(cursor.string_at_cursor(), Some("find me once".to_string()));
297 cursor.back(&*hist)?;
298 assert_eq!(
299 cursor.string_at_cursor(),
300 Some("find me as well".to_string())
301 );
302 Ok(())
303 }
304
305 #[test]
306 fn prefix_search_ignores_consecutive_equivalent_entries_going_forwards() -> Result<()> {
307 let (mut hist, _) = create_history();
308 hist.save(HistoryItem::from_command_line("find me once"))?;
309 hist.save(HistoryItem::from_command_line("test"))?;
310 hist.save(HistoryItem::from_command_line("find me once"))?;
311 hist.save(HistoryItem::from_command_line("find me as well"))?;
312
313 let mut cursor = HistoryCursor::new(
314 HistoryNavigationQuery::PrefixSearch("find".to_string()),
315 None,
316 );
317 cursor.back(&*hist)?;
318 assert_eq!(
319 cursor.string_at_cursor(),
320 Some("find me as well".to_string())
321 );
322 cursor.back(&*hist)?;
323 cursor.back(&*hist)?;
324 assert_eq!(cursor.string_at_cursor(), Some("find me once".to_string()));
325 cursor.forward(&*hist)?;
326 assert_eq!(
327 cursor.string_at_cursor(),
328 Some("find me as well".to_string())
329 );
330 cursor.forward(&*hist)?;
331 assert_eq!(cursor.string_at_cursor(), None);
332 Ok(())
333 }
334
335 #[test]
336 fn substring_search_works() -> Result<()> {
337 let (mut hist, _) = create_history();
338 hist.save(HistoryItem::from_command_line("substring"))?;
339 hist.save(HistoryItem::from_command_line("don't find me either"))?;
340 hist.save(HistoryItem::from_command_line("prefix substring"))?;
341 hist.save(HistoryItem::from_command_line("don't find me"))?;
342 hist.save(HistoryItem::from_command_line("prefix substring suffix"))?;
343
344 let mut cursor = HistoryCursor::new(
345 HistoryNavigationQuery::SubstringSearch("substring".to_string()),
346 None,
347 );
348 cursor.back(&*hist)?;
349 assert_eq!(
350 cursor.string_at_cursor(),
351 Some("prefix substring suffix".to_string())
352 );
353 cursor.back(&*hist)?;
354 assert_eq!(
355 cursor.string_at_cursor(),
356 Some("prefix substring".to_string())
357 );
358 cursor.back(&*hist)?;
359 assert_eq!(cursor.string_at_cursor(), Some("substring".to_string()));
360 Ok(())
361 }
362
363 #[test]
364 fn substring_search_with_empty_value_returns_none() -> Result<()> {
365 let (mut hist, _) = create_history();
366 hist.save(HistoryItem::from_command_line("substring"))?;
367
368 let cursor = HistoryCursor::new(
369 HistoryNavigationQuery::SubstringSearch("".to_string()),
370 None,
371 );
372
373 assert_eq!(cursor.string_at_cursor(), None);
374 Ok(())
375 }
376
377 #[test]
378 fn writes_to_new_file() -> Result<()> {
379 use tempfile::tempdir;
380
381 let tmp = tempdir().unwrap();
382 let histfile = tmp.path().join("nested_path").join(".history");
384
385 let entries = vec!["test", "text", "more test text"];
386
387 {
388 let (mut hist, _) = create_history_at(5, &histfile);
389
390 add_text_entries(hist.as_mut(), &entries);
391 }
393
394 let (reading_hist, _) = create_history_at(5, &histfile);
395 let actual = get_all_entry_texts(reading_hist.as_ref());
396 assert_eq!(entries, actual);
397
398 tmp.close().unwrap();
399 Ok(())
400 }
401
402 #[test]
403 fn persists_newlines_in_entries() -> Result<()> {
404 use tempfile::tempdir;
405
406 let tmp = tempdir().unwrap();
407 let histfile = tmp.path().join(".history");
408
409 let entries = vec![
410 "test",
411 "multiline\nentry\nunix",
412 "multiline\r\nentry\r\nwindows",
413 "more test text",
414 ];
415
416 {
417 let (mut writing_hist, _) = create_history_at(5, &histfile);
418 add_text_entries(writing_hist.as_mut(), &entries);
419 }
421
422 let (reading_hist, _) = create_history_at(5, &histfile);
423
424 let actual: Vec<_> = get_all_entry_texts(reading_hist.as_ref());
425 assert_eq!(entries, actual);
426
427 tmp.close().unwrap();
428 Ok(())
429 }
430
431 #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
432 #[test]
433 fn truncates_file_to_capacity() -> Result<()> {
434 use tempfile::tempdir;
435
436 let tmp = tempdir().unwrap();
437 let histfile = tmp.path().join(".history");
438
439 let capacity = 5;
440 let initial_entries = vec!["test 1", "test 2"];
441 let appending_entries = vec!["test 3", "test 4"];
442 let expected_appended_entries = vec!["test 1", "test 2", "test 3", "test 4"];
443 let truncating_entries = vec!["test 5", "test 6", "test 7", "test 8"];
444 let expected_truncated_entries = vec!["test 4", "test 5", "test 6", "test 7", "test 8"];
445
446 {
447 let (mut writing_hist, _) = create_history_at(capacity, &histfile);
448 add_text_entries(writing_hist.as_mut(), &initial_entries);
449 }
451
452 {
453 let (mut appending_hist, _) = create_history_at(capacity, &histfile);
454 add_text_entries(appending_hist.as_mut(), &appending_entries);
455 let actual: Vec<_> = get_all_entry_texts(appending_hist.as_ref());
457 assert_eq!(expected_appended_entries, actual);
458 }
459
460 {
461 let (mut truncating_hist, _) = create_history_at(capacity, &histfile);
462 add_text_entries(truncating_hist.as_mut(), &truncating_entries);
463 let actual: Vec<_> = get_all_entry_texts(truncating_hist.as_ref());
464 assert_eq!(expected_truncated_entries, actual);
465 }
467
468 let (reading_hist, _) = create_history_at(capacity, &histfile);
469
470 let actual: Vec<_> = get_all_entry_texts(reading_hist.as_ref());
471 assert_eq!(expected_truncated_entries, actual);
472
473 tmp.close().unwrap();
474 Ok(())
475 }
476
477 #[test]
478 fn truncates_too_large_file() -> Result<()> {
479 use tempfile::tempdir;
480
481 let tmp = tempdir().unwrap();
482 let histfile = tmp.path().join(".history");
483
484 let overly_large_previous_entries = vec![
485 "test 1", "test 2", "test 3", "test 4", "test 5", "test 6", "test 7", "test 8",
486 ];
487 let expected_truncated_entries = vec!["test 4", "test 5", "test 6", "test 7", "test 8"];
488
489 {
490 let (mut writing_hist, _) = create_history_at(10, &histfile);
491 add_text_entries(writing_hist.as_mut(), &overly_large_previous_entries);
492 }
494
495 {
496 let (truncating_hist, _) = create_history_at(5, &histfile);
497
498 let actual: Vec<_> = get_all_entry_texts(truncating_hist.as_ref());
499 assert_eq!(expected_truncated_entries, actual);
500 }
502
503 let (reading_hist, _) = create_history_at(5, &histfile);
504
505 let actual: Vec<_> = get_all_entry_texts(reading_hist.as_ref());
506 assert_eq!(expected_truncated_entries, actual);
507
508 tmp.close().unwrap();
509 Ok(())
510 }
511
512 #[test]
513 fn concurrent_histories_dont_erase_eachother() -> Result<()> {
514 use tempfile::tempdir;
515
516 let tmp = tempdir().unwrap();
517 let histfile = tmp.path().join(".history");
518
519 let capacity = 7;
520 let initial_entries = vec!["test 1", "test 2", "test 3", "test 4", "test 5"];
521 let entries_a = vec!["A1", "A2", "A3"];
522 let entries_b = vec!["B1", "B2", "B3"];
523 let expected_entries = vec!["test 5", "B1", "B2", "B3", "A1", "A2", "A3"];
524
525 {
526 let (mut writing_hist, _) = create_history_at(capacity, &histfile);
527 add_text_entries(writing_hist.as_mut(), &initial_entries);
528 }
530
531 {
532 let (mut hist_a, _) = create_history_at(capacity, &histfile);
533
534 {
535 let (mut hist_b, _) = create_history_at(capacity, &histfile);
536
537 add_text_entries(hist_b.as_mut(), &entries_b);
538 }
540 add_text_entries(hist_a.as_mut(), &entries_a);
541 }
543
544 let (reading_hist, _) = create_history_at(capacity, &histfile);
545
546 let actual: Vec<_> = get_all_entry_texts(reading_hist.as_ref());
547 assert_eq!(expected_entries, actual);
548
549 tmp.close().unwrap();
550 Ok(())
551 }
552
553 #[test]
554 fn concurrent_histories_are_threadsafe() -> Result<()> {
555 use tempfile::tempdir;
556
557 let tmp = tempdir().unwrap();
558 let histfile = tmp.path().join(".history");
559
560 let num_threads = 16;
561 let capacity = 2 * num_threads + 1;
562
563 let initial_entries: Vec<_> = (0..capacity).map(|i| format!("initial {i}")).collect();
564
565 {
566 let (mut writing_hist, _) = create_history_at(capacity, &histfile);
567 add_text_entries(writing_hist.as_mut(), &initial_entries);
568 }
570
571 let threads = (0..num_threads)
572 .map(|i| {
573 let cap = capacity;
574 let hfile = histfile.clone();
575 std::thread::spawn(move || {
576 let (mut hist, _) = create_history_at(cap, &hfile);
577 hist.save(HistoryItem::from_command_line(format!("A{i}")))
578 .unwrap();
579 hist.sync().unwrap();
580 hist.save(HistoryItem::from_command_line(format!("B{i}")))
581 .unwrap();
582 })
583 })
584 .collect::<Vec<_>>();
585
586 for t in threads {
587 t.join().unwrap();
588 }
589
590 let (reading_hist, _) = create_history_at(capacity, &histfile);
591
592 let actual: Vec<_> = get_all_entry_texts(reading_hist.as_ref());
593
594 assert!(
595 actual.contains(&format!("initial {}", capacity - 1)),
596 "Overwrote entry from before threading test"
597 );
598
599 for i in 0..num_threads {
600 assert!(actual.contains(&format!("A{i}")),);
601 assert!(actual.contains(&format!("B{i}")),);
602 }
603
604 tmp.close().unwrap();
605 Ok(())
606 }
607}