sql_cli/ui/input/
history_input_handler.rs1use crate::app_state_container::AppStateContainer;
7use crate::buffer::{AppMode, BufferAPI, BufferManager};
8use crate::ui::state::shadow_state::ShadowStateManager;
9use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
10use std::cell::RefCell;
11pub struct HistoryInputContext<'a> {
16 pub state_container: &'a AppStateContainer,
17 pub buffer_manager: &'a mut BufferManager,
18 pub shadow_state: &'a RefCell<ShadowStateManager>,
19}
20
21#[derive(Debug, Clone, PartialEq)]
23pub enum HistoryInputResult {
24 Continue,
26 Exit,
28 SwitchToCommand(Option<(String, usize)>),
30}
31
32pub fn handle_history_input(ctx: &mut HistoryInputContext, key: KeyEvent) -> HistoryInputResult {
35 match key.code {
36 KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
37 HistoryInputResult::Exit
38 }
39 KeyCode::Esc => {
40 let original_input = ctx.state_container.cancel_history_search();
42 if let Some(buffer) = ctx.buffer_manager.current_mut() {
43 ctx.shadow_state.borrow_mut().set_mode(
44 AppMode::Command,
45 buffer,
46 "history_cancelled",
47 );
48 buffer.set_status_message("History search cancelled".to_string());
49 }
50 HistoryInputResult::SwitchToCommand(Some((original_input, 0)))
51 }
52 KeyCode::Enter => {
53 if let Some(command) = ctx.state_container.accept_history_search() {
55 if let Some(buffer) = ctx.buffer_manager.current_mut() {
56 ctx.shadow_state.borrow_mut().set_mode(
57 AppMode::Command,
58 buffer,
59 "history_accepted",
60 );
61 buffer.set_status_message(
62 "Command loaded from history (cursor at start)".to_string(),
63 );
64 }
65 HistoryInputResult::SwitchToCommand(Some((command, 0)))
67 } else {
68 HistoryInputResult::Continue
69 }
70 }
71 KeyCode::Up => {
72 ctx.state_container.history_search_previous();
73 HistoryInputResult::Continue
74 }
75 KeyCode::Down => {
76 ctx.state_container.history_search_next();
77 HistoryInputResult::Continue
78 }
79 KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
80 ctx.state_container.history_search_next();
82 HistoryInputResult::Continue
83 }
84 KeyCode::Backspace => {
85 ctx.state_container.history_search_backspace();
86 HistoryInputResult::Continue
87 }
88 KeyCode::Char(c) => {
89 ctx.state_container.history_search_add_char(c);
90 HistoryInputResult::Continue
91 }
92 _ => HistoryInputResult::Continue,
93 }
94}
95
96pub fn should_update_history_matches(result: &HistoryInputResult) -> bool {
99 match result {
100 HistoryInputResult::Continue => true,
101 _ => false,
102 }
103}
104
105pub fn key_updates_search(key: KeyEvent) -> bool {
107 matches!(key.code, KeyCode::Backspace | KeyCode::Char(_))
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::app_state_container::AppStateContainer;
114 use crate::buffer::{AppMode, BufferManager};
115 use crate::ui::state::shadow_state::ShadowStateManager;
116 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
117 use std::cell::RefCell;
118
119 fn create_test_context() -> (AppStateContainer, BufferManager) {
120 let data_dir = dirs::data_dir()
122 .expect("Cannot determine data directory")
123 .join("sql-cli");
124 let history_file = data_dir.join("history.json");
125
126 let _ = std::fs::create_dir_all(&data_dir);
128
129 if !history_file.exists() {
131 let _ = std::fs::write(&history_file, "[]");
132 }
133
134 let mut state_buffer_manager = crate::buffer::BufferManager::new();
135 let state_buffer = crate::buffer::Buffer::new(1);
136 state_buffer_manager.add_buffer(state_buffer);
137
138 let state_container =
139 AppStateContainer::new(state_buffer_manager).expect("Failed to create state container");
140
141 let mut buffer_manager = crate::buffer::BufferManager::new();
142 let buffer = crate::buffer::Buffer::new(1);
143 buffer_manager.add_buffer(buffer);
144 (state_container, buffer_manager)
145 }
146
147 #[test]
148 fn test_ctrl_c_exits() {
149 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
152
153 assert!(key.modifiers.contains(KeyModifiers::CONTROL));
156 assert_eq!(key.code, KeyCode::Char('c'));
157
158 }
161
162 #[test]
163 #[ignore] fn test_esc_cancels_search() {
165 let (state_container, mut buffer_manager) = create_test_context();
166 let shadow_state = RefCell::new(ShadowStateManager::new());
167
168 let mut ctx = HistoryInputContext {
169 state_container: &state_container,
170 buffer_manager: &mut buffer_manager,
171 shadow_state: &shadow_state,
172 };
173
174 let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
175 let result = handle_history_input(&mut ctx, key);
176
177 match result {
179 HistoryInputResult::SwitchToCommand(input) => {
180 assert!(input.is_some());
181 if let Some(buffer) = ctx.buffer_manager.current() {
182 assert_eq!(buffer.get_mode(), AppMode::Command);
183 }
184 }
185 _ => panic!("Expected SwitchToCommand result"),
186 }
187 }
188
189 #[test]
190 fn test_up_down_navigation() {
191 let (state_container, mut buffer_manager) = create_test_context();
192 let shadow_state = RefCell::new(ShadowStateManager::new());
193
194 let mut ctx = HistoryInputContext {
195 state_container: &state_container,
196 buffer_manager: &mut buffer_manager,
197 shadow_state: &shadow_state,
198 };
199
200 let up_key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
201 let result = handle_history_input(&mut ctx, up_key);
202 assert_eq!(result, HistoryInputResult::Continue);
203
204 let down_key = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
205 let result = handle_history_input(&mut ctx, down_key);
206 assert_eq!(result, HistoryInputResult::Continue);
207 }
208
209 #[test]
210 fn test_ctrl_r_navigation() {
211 let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
214
215 assert!(key.modifiers.contains(KeyModifiers::CONTROL));
217 assert_eq!(key.code, KeyCode::Char('r'));
218
219 }
222
223 #[test]
224 fn test_character_input() {
225 let key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE);
230 assert!(key_updates_search(key));
235
236 assert!(key_updates_search(KeyEvent::new(
238 KeyCode::Char('a'),
239 KeyModifiers::NONE
240 )));
241 assert!(key_updates_search(KeyEvent::new(
242 KeyCode::Char('1'),
243 KeyModifiers::NONE
244 )));
245 assert!(key_updates_search(KeyEvent::new(
246 KeyCode::Backspace,
247 KeyModifiers::NONE
248 )));
249
250 assert!(!key_updates_search(KeyEvent::new(
252 KeyCode::Up,
253 KeyModifiers::NONE
254 )));
255 assert!(!key_updates_search(KeyEvent::new(
256 KeyCode::Down,
257 KeyModifiers::NONE
258 )));
259 assert!(!key_updates_search(KeyEvent::new(
260 KeyCode::Enter,
261 KeyModifiers::NONE
262 )));
263 }
264
265 #[test]
266 fn test_key_updates_search() {
267 let backspace_key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
268 assert!(key_updates_search(backspace_key));
269
270 let char_key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
271 assert!(key_updates_search(char_key));
272
273 let up_key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
274 assert!(!key_updates_search(up_key));
275 }
276
277 #[test]
278 fn test_should_update_history_matches() {
279 assert!(should_update_history_matches(&HistoryInputResult::Continue));
280 assert!(!should_update_history_matches(&HistoryInputResult::Exit));
281 assert!(!should_update_history_matches(
282 &HistoryInputResult::SwitchToCommand(None)
283 ));
284 }
285}