this repo has no description
1use std::collections::VecDeque;
2use std::fs;
3use std::io::{BufRead, BufReader, Write};
4use std::path::Path;
5use serde::{Serialize, Deserialize};
6
7/// Command history management with persistence and search
8#[derive(Debug)]
9pub struct CommandHistory {
10 commands: VecDeque<HistoryEntry>,
11 max_size: usize,
12 current_index: Option<usize>,
13 search_mode: bool,
14 search_query: String,
15 filtered_commands: Vec<usize>,
16}
17
18impl CommandHistory {
19 pub fn new(max_size: usize) -> Self {
20 Self {
21 commands: VecDeque::with_capacity(max_size),
22 max_size,
23 current_index: None,
24 search_mode: false,
25 search_query: String::new(),
26 filtered_commands: Vec::new(),
27 }
28 }
29
30 /// Add a command to history
31 pub fn add_command(&mut self, command: String) {
32 // Don't add empty commands or duplicates
33 if command.trim().is_empty() {
34 return;
35 }
36
37 // Don't add if it's the same as the last command
38 if let Some(last) = self.commands.back() {
39 if last.command == command {
40 return;
41 }
42 }
43
44 let entry = HistoryEntry {
45 command,
46 timestamp: chrono::Utc::now(),
47 success: None, // Will be updated later
48 duration: None,
49 };
50
51 self.commands.push_back(entry);
52
53 // Maintain max size
54 if self.commands.len() > self.max_size {
55 self.commands.pop_front();
56 }
57
58 // Reset navigation
59 self.current_index = None;
60 self.exit_search_mode();
61 }
62
63 /// Update the last command's execution result
64 pub fn update_last_result(&mut self, success: bool, duration: std::time::Duration) {
65 if let Some(last) = self.commands.back_mut() {
66 last.success = Some(success);
67 last.duration = Some(duration);
68 }
69 }
70
71 /// Get previous command in history
72 pub fn get_previous(&mut self) -> Option<String> {
73 if self.commands.is_empty() {
74 return None;
75 }
76
77 if self.search_mode {
78 return self.get_previous_filtered();
79 }
80
81 let new_index = match self.current_index {
82 None => self.commands.len() - 1,
83 Some(0) => return None, // Already at the beginning
84 Some(i) => i - 1,
85 };
86
87 self.current_index = Some(new_index);
88 Some(self.commands[new_index].command.clone())
89 }
90
91 /// Get next command in history
92 pub fn get_next(&mut self) -> Option<String> {
93 if self.commands.is_empty() {
94 return None;
95 }
96
97 if self.search_mode {
98 return self.get_next_filtered();
99 }
100
101 let new_index = match self.current_index {
102 None => return None,
103 Some(i) if i >= self.commands.len() - 1 => {
104 self.current_index = None;
105 return Some(String::new()); // Return empty string to clear input
106 }
107 Some(i) => i + 1,
108 };
109
110 self.current_index = Some(new_index);
111 Some(self.commands[new_index].command.clone())
112 }
113
114 /// Search history by substring
115 pub fn search(&mut self, query: &str) -> Vec<String> {
116 let query_lower = query.to_lowercase();
117 let mut results = Vec::new();
118
119 for entry in self.commands.iter().rev() {
120 if entry.command.to_lowercase().contains(&query_lower) {
121 results.push(entry.command.clone());
122 if results.len() >= 20 {
123 break;
124 }
125 }
126 }
127
128 results
129 }
130
131 /// Enter search mode
132 pub fn enter_search_mode(&mut self, query: String) {
133 self.search_mode = true;
134 self.search_query = query.to_lowercase();
135 self.update_filtered_commands();
136 self.current_index = None;
137 }
138
139 /// Exit search mode
140 pub fn exit_search_mode(&mut self) {
141 self.search_mode = false;
142 self.search_query.clear();
143 self.filtered_commands.clear();
144 self.current_index = None;
145 }
146
147 /// Update search query
148 pub fn update_search_query(&mut self, query: String) {
149 self.search_query = query.to_lowercase();
150 self.update_filtered_commands();
151 self.current_index = None;
152 }
153
154 /// Get recent commands
155 pub fn get_recent_commands(&self, count: usize) -> Vec<String> {
156 self.commands
157 .iter()
158 .rev()
159 .take(count)
160 .map(|entry| entry.command.clone())
161 .collect()
162 }
163
164 /// Get all commands
165 pub fn get_all_commands(&self) -> Vec<&HistoryEntry> {
166 self.commands.iter().collect()
167 }
168
169 /// Get commands by pattern
170 pub fn get_commands_by_pattern(&self, pattern: &str) -> Vec<String> {
171 let pattern_lower = pattern.to_lowercase();
172 let mut results = Vec::new();
173
174 for entry in &self.commands {
175 if entry.command.to_lowercase().contains(&pattern_lower) {
176 results.push(entry.command.clone());
177 }
178 }
179
180 results
181 }
182
183 /// Get command statistics
184 pub fn get_statistics(&self) -> HistoryStatistics {
185 let total_commands = self.commands.len();
186 let successful_commands = self.commands.iter()
187 .filter(|entry| entry.success == Some(true))
188 .count();
189 let failed_commands = self.commands.iter()
190 .filter(|entry| entry.success == Some(false))
191 .count();
192
193 let total_duration: std::time::Duration = self.commands.iter()
194 .filter_map(|entry| entry.duration)
195 .sum();
196
197 let average_duration = if total_commands > 0 {
198 total_duration / total_commands as u32
199 } else {
200 std::time::Duration::ZERO
201 };
202
203 // Find most common commands
204 let mut command_counts = std::collections::HashMap::new();
205 for entry in &self.commands {
206 let words: Vec<&str> = entry.command.split_whitespace().collect();
207 if let Some(first_word) = words.first() {
208 *command_counts.entry(first_word.to_uppercase()).or_insert(0) += 1;
209 }
210 }
211
212 let mut most_common: Vec<_> = command_counts.into_iter().collect();
213 most_common.sort_by(|a, b| b.1.cmp(&a.1));
214 most_common.truncate(5);
215
216 HistoryStatistics {
217 total_commands,
218 successful_commands,
219 failed_commands,
220 average_duration,
221 most_common_commands: most_common.into_iter().map(|(cmd, count)| (cmd.to_string(), count)).collect(),
222 }
223 }
224
225 /// Clear all history
226 pub fn clear(&mut self) {
227 self.commands.clear();
228 self.current_index = None;
229 self.exit_search_mode();
230 }
231
232 /// Save history to file
233 pub fn save_to_file(&self, filename: &str) -> Result<(), std::io::Error> {
234 let mut file = std::fs::File::create(filename)?;
235
236 for entry in &self.commands {
237 let line = format!("{}\n", entry.command);
238 file.write_all(line.as_bytes())?;
239 }
240
241 Ok(())
242 }
243
244 /// Load history from file
245 pub fn load_from_file(&mut self, filename: &str) -> Result<(), std::io::Error> {
246 if !Path::new(filename).exists() {
247 return Ok(()); // File doesn't exist, that's fine
248 }
249
250 let file = std::fs::File::open(filename)?;
251 let reader = BufReader::new(file);
252
253 for line in reader.lines() {
254 let command = line?.trim().to_string();
255 if !command.is_empty() {
256 self.add_command(command);
257 }
258 }
259
260 Ok(())
261 }
262
263 /// Export history as JSON
264 pub fn export_json(&self, filename: &str) -> Result<(), Box<dyn std::error::Error>> {
265 let json_data = serde_json::to_string_pretty(&self.commands.iter().collect::<Vec<_>>())?;
266 fs::write(filename, json_data)?;
267 Ok(())
268 }
269
270 /// Import history from JSON
271 pub fn import_json(&mut self, filename: &str) -> Result<usize, Box<dyn std::error::Error>> {
272 let content = fs::read_to_string(filename)?;
273 let entries: Vec<HistoryEntry> = serde_json::from_str(&content)?;
274
275 let imported_count = entries.len();
276 for entry in entries {
277 self.commands.push_back(entry);
278 }
279
280 // Maintain max size
281 while self.commands.len() > self.max_size {
282 self.commands.pop_front();
283 }
284
285 Ok(imported_count)
286 }
287
288 // Private helper methods
289
290 fn update_filtered_commands(&mut self) {
291 self.filtered_commands.clear();
292
293 for (index, entry) in self.commands.iter().enumerate() {
294 if entry.command.to_lowercase().contains(&self.search_query) {
295 self.filtered_commands.push(index);
296 }
297 }
298 }
299
300 fn get_previous_filtered(&mut self) -> Option<String> {
301 if self.filtered_commands.is_empty() {
302 return None;
303 }
304
305 let filtered_index = match self.current_index {
306 None => self.filtered_commands.len() - 1,
307 Some(current_real_index) => {
308 // Find current position in filtered list
309 let current_filtered_pos = self.filtered_commands.iter()
310 .position(|&idx| idx == current_real_index)?;
311
312 if current_filtered_pos == 0 {
313 return None; // Already at beginning
314 }
315
316 current_filtered_pos - 1
317 }
318 };
319
320 let real_index = self.filtered_commands[filtered_index];
321 self.current_index = Some(real_index);
322 Some(self.commands[real_index].command.clone())
323 }
324
325 fn get_next_filtered(&mut self) -> Option<String> {
326 if self.filtered_commands.is_empty() {
327 return None;
328 }
329
330 let filtered_index = match self.current_index {
331 None => return None,
332 Some(current_real_index) => {
333 // Find current position in filtered list
334 let current_filtered_pos = self.filtered_commands.iter()
335 .position(|&idx| idx == current_real_index)?;
336
337 if current_filtered_pos >= self.filtered_commands.len() - 1 {
338 self.current_index = None;
339 return Some(String::new()); // Clear input
340 }
341
342 current_filtered_pos + 1
343 }
344 };
345
346 let real_index = self.filtered_commands[filtered_index];
347 self.current_index = Some(real_index);
348 Some(self.commands[real_index].command.clone())
349 }
350}
351
352/// History entry with metadata
353#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct HistoryEntry {
355 pub command: String,
356 pub timestamp: chrono::DateTime<chrono::Utc>,
357 pub success: Option<bool>,
358 pub duration: Option<std::time::Duration>,
359}
360
361/// History statistics
362#[derive(Debug)]
363pub struct HistoryStatistics {
364 pub total_commands: usize,
365 pub successful_commands: usize,
366 pub failed_commands: usize,
367 pub average_duration: std::time::Duration,
368 pub most_common_commands: Vec<(String, usize)>,
369}
370
371impl std::fmt::Display for HistoryStatistics {
372 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373 writeln!(f, "History Statistics:")?;
374 writeln!(f, " Total commands: {}", self.total_commands)?;
375 writeln!(f, " Successful: {}", self.successful_commands)?;
376 writeln!(f, " Failed: {}", self.failed_commands)?;
377 writeln!(f, " Average duration: {:?}", self.average_duration)?;
378 writeln!(f, " Most common commands:")?;
379 for (cmd, count) in &self.most_common_commands {
380 writeln!(f, " {}: {} times", cmd, count)?;
381 }
382 Ok(())
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use std::time::Duration;
390
391 #[test]
392 fn test_history_basic_operations() {
393 let mut history = CommandHistory::new(10);
394
395 history.add_command("MATCH (n) RETURN n".to_string());
396 history.add_command("CREATE (n:Person)".to_string());
397
398 assert_eq!(history.commands.len(), 2);
399
400 let prev = history.get_previous();
401 assert_eq!(prev, Some("CREATE (n:Person)".to_string()));
402
403 let prev2 = history.get_previous();
404 assert_eq!(prev2, Some("MATCH (n) RETURN n".to_string()));
405 }
406
407 #[test]
408 fn test_history_search() {
409 let mut history = CommandHistory::new(10);
410
411 history.add_command("MATCH (n) RETURN n".to_string());
412 history.add_command("CREATE (n:Person)".to_string());
413 history.add_command("MATCH (p:Person) RETURN p".to_string());
414
415 let results = history.search("MATCH");
416 assert_eq!(results.len(), 2);
417 assert!(results.contains(&"MATCH (p:Person) RETURN p".to_string()));
418 assert!(results.contains(&"MATCH (n) RETURN n".to_string()));
419 }
420
421 #[test]
422 fn test_history_deduplication() {
423 let mut history = CommandHistory::new(10);
424
425 history.add_command("MATCH (n) RETURN n".to_string());
426 history.add_command("MATCH (n) RETURN n".to_string()); // Duplicate
427
428 assert_eq!(history.commands.len(), 1);
429 }
430
431 #[test]
432 fn test_history_max_size() {
433 let mut history = CommandHistory::new(3);
434
435 history.add_command("command1".to_string());
436 history.add_command("command2".to_string());
437 history.add_command("command3".to_string());
438 history.add_command("command4".to_string()); // Should remove command1
439
440 assert_eq!(history.commands.len(), 3);
441 assert!(!history.commands.iter().any(|e| e.command == "command1"));
442 assert!(history.commands.iter().any(|e| e.command == "command4"));
443 }
444
445 #[test]
446 fn test_history_statistics() {
447 let mut history = CommandHistory::new(10);
448
449 history.add_command("MATCH (n) RETURN n".to_string());
450 history.update_last_result(true, Duration::from_millis(100));
451
452 history.add_command("CREATE (n:Person)".to_string());
453 history.update_last_result(false, Duration::from_millis(50));
454
455 let stats = history.get_statistics();
456 assert_eq!(stats.total_commands, 2);
457 assert_eq!(stats.successful_commands, 1);
458 assert_eq!(stats.failed_commands, 1);
459 }
460
461 #[test]
462 fn test_search_mode() {
463 let mut history = CommandHistory::new(10);
464
465 history.add_command("MATCH (n) RETURN n".to_string());
466 history.add_command("CREATE (n:Person)".to_string());
467 history.add_command("MATCH (p:Person) RETURN p".to_string());
468
469 history.enter_search_mode("MATCH".to_string());
470 assert!(history.search_mode);
471 assert_eq!(history.filtered_commands.len(), 2);
472
473 let prev = history.get_previous();
474 assert!(prev.is_some());
475 assert!(prev.unwrap().contains("MATCH"));
476 }
477}