use crate::app_state_container::AppStateContainer;
use crate::buffer::{AppMode, BufferAPI, BufferManager};
use crate::ui::state::shadow_state::ShadowStateManager;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::cell::RefCell;
pub struct HistoryInputContext<'a> {
pub state_container: &'a AppStateContainer,
pub buffer_manager: &'a mut BufferManager,
pub shadow_state: &'a RefCell<ShadowStateManager>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HistoryInputResult {
Continue,
Exit,
SwitchToCommand(Option<(String, usize)>),
}
pub fn handle_history_input(ctx: &mut HistoryInputContext, key: KeyEvent) -> HistoryInputResult {
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
HistoryInputResult::Exit
}
KeyCode::Esc => {
let original_input = ctx.state_container.cancel_history_search();
if let Some(buffer) = ctx.buffer_manager.current_mut() {
ctx.shadow_state.borrow_mut().set_mode(
AppMode::Command,
buffer,
"history_cancelled",
);
buffer.set_status_message("History search cancelled".to_string());
}
HistoryInputResult::SwitchToCommand(Some((original_input, 0)))
}
KeyCode::Enter => {
if let Some(command) = ctx.state_container.accept_history_search() {
if let Some(buffer) = ctx.buffer_manager.current_mut() {
ctx.shadow_state.borrow_mut().set_mode(
AppMode::Command,
buffer,
"history_accepted",
);
buffer.set_status_message(
"Command loaded from history (cursor at start)".to_string(),
);
}
HistoryInputResult::SwitchToCommand(Some((command, 0)))
} else {
HistoryInputResult::Continue
}
}
KeyCode::Up => {
ctx.state_container.history_search_previous();
HistoryInputResult::Continue
}
KeyCode::Down => {
ctx.state_container.history_search_next();
HistoryInputResult::Continue
}
KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
ctx.state_container.history_search_next();
HistoryInputResult::Continue
}
KeyCode::Backspace => {
ctx.state_container.history_search_backspace();
HistoryInputResult::Continue
}
KeyCode::Char(c) => {
ctx.state_container.history_search_add_char(c);
HistoryInputResult::Continue
}
_ => HistoryInputResult::Continue,
}
}
#[must_use]
pub fn should_update_history_matches(result: &HistoryInputResult) -> bool {
match result {
HistoryInputResult::Continue => true,
_ => false,
}
}
#[must_use]
pub fn key_updates_search(key: KeyEvent) -> bool {
matches!(key.code, KeyCode::Backspace | KeyCode::Char(_))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app_state_container::AppStateContainer;
use crate::buffer::{AppMode, BufferManager};
use crate::ui::state::shadow_state::ShadowStateManager;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::cell::RefCell;
fn create_test_context() -> (AppStateContainer, BufferManager) {
let data_dir = dirs::data_dir()
.expect("Cannot determine data directory")
.join("sql-cli");
let history_file = data_dir.join("history.json");
let _ = std::fs::create_dir_all(&data_dir);
if !history_file.exists() {
let _ = std::fs::write(&history_file, "[]");
}
let mut state_buffer_manager = crate::buffer::BufferManager::new();
let state_buffer = crate::buffer::Buffer::new(1);
state_buffer_manager.add_buffer(state_buffer);
let state_container =
AppStateContainer::new(state_buffer_manager).expect("Failed to create state container");
let mut buffer_manager = crate::buffer::BufferManager::new();
let buffer = crate::buffer::Buffer::new(1);
buffer_manager.add_buffer(buffer);
(state_container, buffer_manager)
}
#[test]
fn test_ctrl_c_exits() {
let key = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL);
assert!(key.modifiers.contains(KeyModifiers::CONTROL));
assert_eq!(key.code, KeyCode::Char('c'));
}
#[test]
#[ignore] fn test_esc_cancels_search() {
let (state_container, mut buffer_manager) = create_test_context();
let shadow_state = RefCell::new(ShadowStateManager::new());
let mut ctx = HistoryInputContext {
state_container: &state_container,
buffer_manager: &mut buffer_manager,
shadow_state: &shadow_state,
};
let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
let result = handle_history_input(&mut ctx, key);
match result {
HistoryInputResult::SwitchToCommand(input) => {
assert!(input.is_some());
if let Some(buffer) = ctx.buffer_manager.current() {
assert_eq!(buffer.get_mode(), AppMode::Command);
}
}
_ => panic!("Expected SwitchToCommand result"),
}
}
#[test]
fn test_up_down_navigation() {
let (state_container, mut buffer_manager) = create_test_context();
let shadow_state = RefCell::new(ShadowStateManager::new());
let mut ctx = HistoryInputContext {
state_container: &state_container,
buffer_manager: &mut buffer_manager,
shadow_state: &shadow_state,
};
let up_key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
let result = handle_history_input(&mut ctx, up_key);
assert_eq!(result, HistoryInputResult::Continue);
let down_key = KeyEvent::new(KeyCode::Down, KeyModifiers::NONE);
let result = handle_history_input(&mut ctx, down_key);
assert_eq!(result, HistoryInputResult::Continue);
}
#[test]
fn test_ctrl_r_navigation() {
let key = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL);
assert!(key.modifiers.contains(KeyModifiers::CONTROL));
assert_eq!(key.code, KeyCode::Char('r'));
}
#[test]
fn test_character_input() {
let key = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE);
assert!(key_updates_search(key));
assert!(key_updates_search(KeyEvent::new(
KeyCode::Char('a'),
KeyModifiers::NONE
)));
assert!(key_updates_search(KeyEvent::new(
KeyCode::Char('1'),
KeyModifiers::NONE
)));
assert!(key_updates_search(KeyEvent::new(
KeyCode::Backspace,
KeyModifiers::NONE
)));
assert!(!key_updates_search(KeyEvent::new(
KeyCode::Up,
KeyModifiers::NONE
)));
assert!(!key_updates_search(KeyEvent::new(
KeyCode::Down,
KeyModifiers::NONE
)));
assert!(!key_updates_search(KeyEvent::new(
KeyCode::Enter,
KeyModifiers::NONE
)));
}
#[test]
fn test_key_updates_search() {
let backspace_key = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
assert!(key_updates_search(backspace_key));
let char_key = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
assert!(key_updates_search(char_key));
let up_key = KeyEvent::new(KeyCode::Up, KeyModifiers::NONE);
assert!(!key_updates_search(up_key));
}
#[test]
fn test_should_update_history_matches() {
assert!(should_update_history_matches(&HistoryInputResult::Continue));
assert!(!should_update_history_matches(&HistoryInputResult::Exit));
assert!(!should_update_history_matches(
&HistoryInputResult::SwitchToCommand(None)
));
}
}