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
96#[must_use]
99pub fn should_update_history_matches(result: &HistoryInputResult) -> bool {
100 match result {
101 HistoryInputResult::Continue => true,
102 _ => false,
103 }
104}
105
106#[must_use]
108pub fn key_updates_search(key: KeyEvent) -> bool {
109 matches!(key.code, KeyCode::Backspace | KeyCode::Char(_))
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::app_state_container::AppStateContainer;
116 use crate::buffer::{AppMode, BufferManager};
117 use crate::ui::state::shadow_state::ShadowStateManager;
118 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
119 use std::cell::RefCell;
120
121 fn create_test_context() -> (AppStateContainer, BufferManager) {
122 let data_dir = dirs::data_dir()
124 .expect("Cannot determine data directory")
125 .join("sql-cli");
126 let history_file = data_dir.join("history.json");
127
128 let _ = std::fs::create_dir_all(&data_dir);
130
131 if !history_file.exists() {
133 let _ = std::fs::write(&history_file, "[]");
134 }
135
136 let mut state_buffer_manager = crate::buffer::BufferManager::new();
137 let state_buffer = crate::buffer::Buffer::new(1);
138 state_buffer_manager.add_buffer(state_buffer);
139
140 let state_container =
141 AppStateContainer::new(state_buffer_manager).expect("Failed to create state container");
142
143 let mut buffer_manager = crate::buffer::BufferManager::new();
144 let buffer = crate::buffer::Buffer::new(1);
145 buffer_manager.add_buffer(buffer);
146 (state_container, buffer_manager)
147 }
148
149 #[test]
150 fn test_ctrl_c_exits() {
151 let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
154
155 assert!(key.modifiers.contains(KeyModifiers::CONTROL));
158 assert_eq!(key.code, KeyCode::Char('c'));
159
160 }
163
164 #[test]
165 #[ignore] fn test_esc_cancels_search() {
167 let (state_container, mut buffer_manager) = create_test_context();
168 let shadow_state = RefCell::new(ShadowStateManager::new());
169
170 let mut ctx = HistoryInputContext {
171 state_container: &state_container,
172 buffer_manager: &mut buffer_manager,
173 shadow_state: &shadow_state,
174 };
175
176 let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
177 let result = handle_history_input(&mut ctx, key);
178
179 match result {
181 HistoryInputResult::SwitchToCommand(input) => {
182 assert!(input.is_some());
183 if let Some(buffer) = ctx.buffer_manager.current() {
184 assert_eq!(buffer.get_mode(), AppMode::Command);
185 }
186 }
187 _ => panic!("Expected SwitchToCommand result"),
188 }
189 }
190
191 #[test]
192 fn test_up_down_navigation() {
193 let (state_container, mut buffer_manager) = create_test_context();
194 let shadow_state = RefCell::new(ShadowStateManager::new());
195
196 let mut ctx = HistoryInputContext {
197 state_container: &state_container,
198 buffer_manager: &mut buffer_manager,
199 shadow_state: &shadow_state,
200 };
201
202 let up_key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
203 let result = handle_history_input(&mut ctx, up_key);
204 assert_eq!(result, HistoryInputResult::Continue);
205
206 let down_key = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
207 let result = handle_history_input(&mut ctx, down_key);
208 assert_eq!(result, HistoryInputResult::Continue);
209 }
210
211 #[test]
212 fn test_ctrl_r_navigation() {
213 let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
216
217 assert!(key.modifiers.contains(KeyModifiers::CONTROL));
219 assert_eq!(key.code, KeyCode::Char('r'));
220
221 }
224
225 #[test]
226 fn test_character_input() {
227 let key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE);
232 assert!(key_updates_search(key));
237
238 assert!(key_updates_search(KeyEvent::new(
240 KeyCode::Char('a'),
241 KeyModifiers::NONE
242 )));
243 assert!(key_updates_search(KeyEvent::new(
244 KeyCode::Char('1'),
245 KeyModifiers::NONE
246 )));
247 assert!(key_updates_search(KeyEvent::new(
248 KeyCode::Backspace,
249 KeyModifiers::NONE
250 )));
251
252 assert!(!key_updates_search(KeyEvent::new(
254 KeyCode::Up,
255 KeyModifiers::NONE
256 )));
257 assert!(!key_updates_search(KeyEvent::new(
258 KeyCode::Down,
259 KeyModifiers::NONE
260 )));
261 assert!(!key_updates_search(KeyEvent::new(
262 KeyCode::Enter,
263 KeyModifiers::NONE
264 )));
265 }
266
267 #[test]
268 fn test_key_updates_search() {
269 let backspace_key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
270 assert!(key_updates_search(backspace_key));
271
272 let char_key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
273 assert!(key_updates_search(char_key));
274
275 let up_key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
276 assert!(!key_updates_search(up_key));
277 }
278
279 #[test]
280 fn test_should_update_history_matches() {
281 assert!(should_update_history_matches(&HistoryInputResult::Continue));
282 assert!(!should_update_history_matches(&HistoryInputResult::Exit));
283 assert!(!should_update_history_matches(
284 &HistoryInputResult::SwitchToCommand(None)
285 ));
286 }
287}