1use super::HistoryItemId;
2use crate::{core_editor::LineBuffer, HistoryItem, HistorySessionId, Result};
3use chrono::Utc;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum HistoryNavigationQuery {
8 Normal(LineBuffer),
10 PrefixSearch(String),
12 SubstringSearch(String),
14 }
17
18pub enum CommandLineSearch {
21 Prefix(String),
23 Substring(String),
25 Exact(String),
29}
30
31#[derive(Clone, Copy, PartialEq, Eq)]
33pub enum SearchDirection {
34 Backward,
36 Forward,
38}
39
40pub struct SearchFilter {
42 pub command_line: Option<CommandLineSearch>,
44 pub(crate) not_command_line: Option<String>, pub hostname: Option<String>,
48 pub cwd_exact: Option<String>,
50 pub cwd_prefix: Option<String>,
52 pub exit_successful: Option<bool>,
54 pub session: Option<HistorySessionId>,
56}
57
58impl SearchFilter {
59 pub fn from_text_search(
61 cmd: CommandLineSearch,
62 session: Option<HistorySessionId>,
63 ) -> SearchFilter {
64 let mut s = SearchFilter::anything(session);
65 s.command_line = Some(cmd);
66 s
67 }
68
69 pub fn from_text_search_cwd(
71 cwd: String,
72 cmd: CommandLineSearch,
73 session: Option<HistorySessionId>,
74 ) -> SearchFilter {
75 let mut s = SearchFilter::anything(session);
76 s.command_line = Some(cmd);
77 s.cwd_exact = Some(cwd);
78 s
79 }
80
81 pub fn anything(session: Option<HistorySessionId>) -> SearchFilter {
83 SearchFilter {
84 command_line: None,
85 not_command_line: None,
86 hostname: None,
87 cwd_exact: None,
88 cwd_prefix: None,
89 exit_successful: None,
90 session,
91 }
92 }
93}
94
95pub struct SearchQuery {
97 pub direction: SearchDirection,
99 pub start_time: Option<chrono::DateTime<Utc>>,
101 pub end_time: Option<chrono::DateTime<Utc>>,
103 pub start_id: Option<HistoryItemId>,
105 pub end_id: Option<HistoryItemId>,
107 pub limit: Option<i64>,
109 pub filter: SearchFilter,
111}
112
113impl SearchQuery {
115 pub fn all_that_contain_rev(contains: String) -> SearchQuery {
117 SearchQuery {
118 direction: SearchDirection::Backward,
119 start_time: None,
120 end_time: None,
121 start_id: None,
122 end_id: None,
123 limit: None,
124 filter: SearchFilter::from_text_search(CommandLineSearch::Substring(contains), None),
125 }
126 }
127
128 pub const fn last_with_search(filter: SearchFilter) -> SearchQuery {
130 SearchQuery {
131 direction: SearchDirection::Backward,
132 start_time: None,
133 end_time: None,
134 start_id: None,
135 end_id: None,
136 limit: Some(1),
137 filter,
138 }
139 }
140
141 pub fn last_with_prefix(prefix: String, session: Option<HistorySessionId>) -> SearchQuery {
143 SearchQuery::last_with_search(SearchFilter::from_text_search(
144 CommandLineSearch::Prefix(prefix),
145 session,
146 ))
147 }
148
149 pub fn last_with_prefix_and_cwd(
151 prefix: String,
152 session: Option<HistorySessionId>,
153 ) -> SearchQuery {
154 let cwd = std::env::current_dir();
155 if let Ok(cwd) = cwd {
156 SearchQuery::last_with_search(SearchFilter::from_text_search_cwd(
157 cwd.to_string_lossy().to_string(),
158 CommandLineSearch::Prefix(prefix),
159 session,
160 ))
161 } else {
162 SearchQuery::last_with_search(SearchFilter::from_text_search(
163 CommandLineSearch::Prefix(prefix),
164 session,
165 ))
166 }
167 }
168
169 pub fn everything(
171 direction: SearchDirection,
172 session: Option<HistorySessionId>,
173 ) -> SearchQuery {
174 SearchQuery {
175 direction,
176 start_time: None,
177 end_time: None,
178 start_id: None,
179 end_id: None,
180 limit: None,
181 filter: SearchFilter::anything(session),
182 }
183 }
184}
185
186pub trait History: Send {
189 fn save(&mut self, h: HistoryItem) -> Result<HistoryItem>;
193 fn load(&self, id: HistoryItemId) -> Result<HistoryItem>;
195
196 fn count(&self, query: SearchQuery) -> Result<i64>;
200 fn count_all(&self) -> Result<i64> {
202 self.count(SearchQuery::everything(SearchDirection::Forward, None))
203 }
204 fn search(&self, query: SearchQuery) -> Result<Vec<HistoryItem>>;
206
207 fn update(
209 &mut self,
210 id: HistoryItemId,
211 updater: &dyn Fn(HistoryItem) -> HistoryItem,
212 ) -> Result<()>;
213 fn clear(&mut self) -> Result<()>;
215 fn delete(&mut self, h: HistoryItemId) -> Result<()>;
217 fn sync(&mut self) -> std::io::Result<()>;
219 fn session(&self) -> Option<HistorySessionId>;
221}
222
223#[cfg(test)]
224mod test {
225 #[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
226 const IS_FILE_BASED: bool = false;
227 #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
228 const IS_FILE_BASED: bool = true;
229
230 use crate::HistorySessionId;
231
232 fn create_item(session: i64, cwd: &str, cmd: &str, exit_status: i64) -> HistoryItem {
233 HistoryItem {
234 id: None,
235 start_timestamp: None,
236 command_line: cmd.to_string(),
237 session_id: Some(HistorySessionId::new(session)),
238 hostname: Some("foohost".to_string()),
239 cwd: Some(cwd.to_string()),
240 duration: Some(Duration::from_millis(1000)),
241 exit_status: Some(exit_status),
242 more_info: None,
243 }
244 }
245 use std::time::Duration;
246
247 use super::*;
248 fn create_filled_example_history() -> Result<Box<dyn History>> {
249 #[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
250 let mut history = crate::SqliteBackedHistory::in_memory()?;
251 #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
252 let mut history = crate::FileBackedHistory::default();
253 #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
254 history.save(create_item(1, "/", "dummy", 0))?; history.save(create_item(1, "/home/me", "cd ~/Downloads", 0))?; history.save(create_item(1, "/home/me/Downloads", "unzp foo.zip", 1))?; history.save(create_item(1, "/home/me/Downloads", "unzip foo.zip", 0))?; history.save(create_item(1, "/home/me/Downloads", "cd foo", 0))?; history.save(create_item(1, "/home/me/Downloads/foo", "ls", 0))?; history.save(create_item(1, "/home/me/Downloads/foo", "ls -alh", 0))?; history.save(create_item(1, "/home/me/Downloads/foo", "cat x.txt", 0))?; history.save(create_item(1, "/home/me", "cd /etc/nginx", 0))?; history.save(create_item(1, "/etc/nginx", "ls -l", 0))?; history.save(create_item(1, "/etc/nginx", "vim nginx.conf", 0))?; history.save(create_item(1, "/etc/nginx", "vim htpasswd", 0))?; history.save(create_item(1, "/etc/nginx", "cat nginx.conf", 0))?; Ok(Box::new(history))
269 }
270
271 #[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
272 #[test]
273 fn update_item() -> Result<()> {
274 let mut history = create_filled_example_history()?;
275 let id = HistoryItemId::new(2);
276 let before = history.load(id)?;
277 history.update(id, &|mut e| {
278 e.exit_status = Some(1);
279 e
280 })?;
281 let after = history.load(id)?;
282 assert_eq!(
283 after,
284 HistoryItem {
285 exit_status: Some(1),
286 ..before
287 }
288 );
289 Ok(())
290 }
291
292 fn search_returned(
293 history: &dyn History,
294 res: Vec<HistoryItem>,
295 wanted: Vec<i64>,
296 ) -> Result<()> {
297 let wanted = wanted
298 .iter()
299 .map(|id| history.load(HistoryItemId::new(*id)))
300 .collect::<Result<Vec<HistoryItem>>>()?;
301 assert_eq!(res, wanted);
302 Ok(())
303 }
304
305 #[test]
306 fn count_all() -> Result<()> {
307 let history = create_filled_example_history()?;
308 println!(
309 "{:#?}",
310 history.search(SearchQuery::everything(SearchDirection::Forward, None))
311 );
312
313 assert_eq!(history.count_all()?, if IS_FILE_BASED { 13 } else { 12 });
314 Ok(())
315 }
316
317 #[test]
318 fn get_latest() -> Result<()> {
319 let history = create_filled_example_history()?;
320 let res = history.search(SearchQuery::last_with_search(SearchFilter::anything(None)))?;
321
322 search_returned(&*history, res, vec![12])?;
323 Ok(())
324 }
325
326 #[test]
327 fn get_earliest() -> Result<()> {
328 let history = create_filled_example_history()?;
329 let res = history.search(SearchQuery {
330 limit: Some(1),
331 ..SearchQuery::everything(SearchDirection::Forward, None)
332 })?;
333 search_returned(&*history, res, vec![if IS_FILE_BASED { 0 } else { 1 }])?;
334 Ok(())
335 }
336
337 #[test]
338 fn search_prefix() -> Result<()> {
339 let history = create_filled_example_history()?;
340 let res = history.search(SearchQuery {
341 filter: SearchFilter::from_text_search(
342 CommandLineSearch::Prefix("ls ".to_string()),
343 None,
344 ),
345 ..SearchQuery::everything(SearchDirection::Backward, None)
346 })?;
347 search_returned(&*history, res, vec![9, 6])?;
348
349 Ok(())
350 }
351
352 #[test]
353 fn search_includes() -> Result<()> {
354 let history = create_filled_example_history()?;
355 let res = history.search(SearchQuery {
356 filter: SearchFilter::from_text_search(
357 CommandLineSearch::Substring("foo.zip".to_string()),
358 None,
359 ),
360 ..SearchQuery::everything(SearchDirection::Forward, None)
361 })?;
362 search_returned(&*history, res, vec![2, 3])?;
363 Ok(())
364 }
365
366 #[test]
367 fn search_includes_limit() -> Result<()> {
368 let history = create_filled_example_history()?;
369 let res = history.search(SearchQuery {
370 filter: SearchFilter::from_text_search(
371 CommandLineSearch::Substring("c".to_string()),
372 None,
373 ),
374 limit: Some(2),
375 ..SearchQuery::everything(SearchDirection::Forward, None)
376 })?;
377 search_returned(&*history, res, vec![1, 4])?;
378
379 Ok(())
380 }
381
382 #[test]
383 fn clear_history() -> Result<()> {
384 let mut history = create_filled_example_history()?;
385 assert_ne!(history.count_all()?, 0);
386 history.clear().unwrap();
387 assert_eq!(history.count_all()?, 0);
388
389 Ok(())
390 }
391
392 #[test]
394 fn clear_history_with_backing_file() -> Result<()> {
395 #[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
396 fn open_history() -> Box<dyn History> {
397 Box::new(
398 crate::SqliteBackedHistory::with_file("target/test-history.db".into(), None, None)
399 .unwrap(),
400 )
401 }
402
403 #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
404 fn open_history() -> Box<dyn History> {
405 Box::new(
406 crate::FileBackedHistory::with_file(100, "target/test-history.txt".into()).unwrap(),
407 )
408 }
409
410 let mut history = open_history();
412 history.save(create_item(1, "/home/me", "cd ~/Downloads", 0))?; history.save(create_item(1, "/home/me/Downloads", "unzp foo.zip", 1))?; assert_eq!(history.count_all()?, 2);
415 drop(history);
416
417 let mut history = open_history();
419 assert_eq!(history.count_all()?, 2);
420 history.clear().unwrap();
421 assert_eq!(history.count_all()?, 0);
422 drop(history);
423
424 let history = open_history();
426 assert_eq!(history.count_all()?, 0);
427
428 Ok(())
429 }
430}