1use std::collections::VecDeque;
2
3/// Determines the behavior to use when inserting a new query into the search history.
4#[derive(Default, Debug, Clone, PartialEq)]
5pub enum QueryInsertionBehavior {
6 #[default]
7 /// Always insert the query to the search history.
8 AlwaysInsert,
9 /// Replace the previous query in the search history, if the new query contains the previous query.
10 ReplacePreviousIfContains,
11}
12
13/// A cursor that stores an index to the currently selected query in the search history.
14/// This can be passed to the search history to update the selection accordingly,
15/// e.g. when using the up and down arrow keys to navigate the search history.
16///
17/// Note: The cursor can point to the wrong query, if the maximum length of the history is exceeded
18/// and the old query is overwritten.
19#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
20pub struct SearchHistoryCursor {
21 selection: Option<usize>,
22 draft: Option<String>,
23}
24
25impl SearchHistoryCursor {
26 /// Resets the selection to `None` and clears the draft.
27 pub fn reset(&mut self) {
28 self.selection = None;
29 self.draft = None;
30 }
31
32 /// Takes the stored draft query, if any.
33 pub fn take_draft(&mut self) -> Option<String> {
34 self.draft.take()
35 }
36}
37
38#[derive(Debug, Clone)]
39pub struct SearchHistory {
40 history: VecDeque<String>,
41 max_history_len: Option<usize>,
42 insertion_behavior: QueryInsertionBehavior,
43}
44
45impl SearchHistory {
46 pub fn new(max_history_len: Option<usize>, insertion_behavior: QueryInsertionBehavior) -> Self {
47 SearchHistory {
48 max_history_len,
49 insertion_behavior,
50 history: VecDeque::new(),
51 }
52 }
53
54 pub fn add(&mut self, cursor: &mut SearchHistoryCursor, search_string: String) {
55 cursor.draft = None;
56
57 if self.insertion_behavior == QueryInsertionBehavior::ReplacePreviousIfContains
58 && let Some(previously_searched) = self.history.back_mut()
59 && search_string.contains(previously_searched.as_str())
60 {
61 *previously_searched = search_string;
62 cursor.selection = Some(self.history.len() - 1);
63 return;
64 }
65
66 if let Some(max_history_len) = self.max_history_len
67 && self.history.len() >= max_history_len
68 {
69 self.history.pop_front();
70 }
71 self.history.push_back(search_string);
72
73 cursor.selection = Some(self.history.len() - 1);
74 }
75
76 pub fn next(&mut self, cursor: &mut SearchHistoryCursor) -> Option<&str> {
77 let selected = cursor.selection?;
78 let next_index = selected + 1;
79
80 let next = self.history.get(next_index)?;
81 cursor.selection = Some(next_index);
82 Some(next)
83 }
84
85 pub fn current(&self, cursor: &SearchHistoryCursor) -> Option<&str> {
86 cursor
87 .selection
88 .and_then(|selected_ix| self.history.get(selected_ix).map(|s| s.as_str()))
89 }
90
91 /// Get the previous history entry using the given `SearchHistoryCursor`.
92 /// Uses the last element in the history when there is no cursor.
93 ///
94 /// `current_query` is the current text in the search editor. If it differs
95 /// from the history entry at the cursor position (or if the cursor has no
96 /// selection), it is saved as a draft so it can be restored later.
97 pub fn previous(
98 &mut self,
99 cursor: &mut SearchHistoryCursor,
100 current_query: &str,
101 ) -> Option<&str> {
102 let matches_history = cursor
103 .selection
104 .and_then(|i| self.history.get(i))
105 .is_some_and(|entry| entry == current_query);
106 if !matches_history {
107 cursor.draft = Some(current_query.to_string());
108 }
109
110 let prev_index = match cursor.selection {
111 Some(index) => index.checked_sub(1)?,
112 None => self.history.len().checked_sub(1)?,
113 };
114
115 let previous = self.history.get(prev_index)?;
116 cursor.selection = Some(prev_index);
117 Some(previous)
118 }
119
120 pub fn len(&self) -> usize {
121 self.history.len()
122 }
123}