use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::super::app_state::{App, Focus, OutputMode};
fn accept_autocomplete_suggestion(app: &mut App) -> bool {
if app.focus == Focus::InputField && app.autocomplete.is_visible() {
if let Some(suggestion) = app.autocomplete.selected() {
let suggestion_clone = suggestion.clone();
app.insert_autocomplete_suggestion(&suggestion_clone);
app.debouncer.mark_executed();
app.update_tooltip();
}
return true;
}
false
}
pub fn handle_global_keys(app: &mut App, key: KeyEvent) -> bool {
if app.history.is_visible() && key.code != KeyCode::BackTab {
return false;
}
if app.help.visible {
match key.code {
KeyCode::Esc | KeyCode::F(1) => {
app.help.visible = false;
app.help.scroll.reset();
return true;
}
KeyCode::Char('q') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
app.help.visible = false;
app.help.scroll.reset();
return true;
}
KeyCode::Char('?') => {
app.help.visible = false;
app.help.scroll.reset();
return true;
}
KeyCode::Char('j') | KeyCode::Down => {
app.help.scroll.scroll_down(1);
return true;
}
KeyCode::Char('J') => {
app.help.scroll.scroll_down(10);
return true;
}
KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.help.scroll.scroll_down(10);
return true;
}
KeyCode::PageDown => {
app.help.scroll.scroll_down(10);
return true;
}
KeyCode::Char('k') | KeyCode::Up => {
app.help.scroll.scroll_up(1);
return true;
}
KeyCode::Char('K') => {
app.help.scroll.scroll_up(10);
return true;
}
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.help.scroll.scroll_up(10);
return true;
}
KeyCode::PageUp => {
app.help.scroll.scroll_up(10);
return true;
}
KeyCode::Char('g') | KeyCode::Home => {
app.help.scroll.jump_to_top();
return true;
}
KeyCode::Char('G') | KeyCode::End => {
app.help.scroll.jump_to_bottom();
return true;
}
_ => {
return true;
}
}
}
match key.code {
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.should_quit = true;
true
}
KeyCode::Char('q') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
match app.focus {
Focus::ResultsPane => {
app.should_quit = true;
true
}
Focus::InputField => {
if app.input.editor_mode == crate::editor::EditorMode::Normal {
app.should_quit = true;
true
} else {
false }
}
}
}
KeyCode::Char('q') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if app.debouncer.has_pending() {
crate::editor::editor_events::execute_query(app);
app.debouncer.mark_executed();
}
if app.query.result.is_ok() && !app.query().is_empty() {
let query = app.query().to_string();
app.history.add_entry(&query);
}
app.output_mode = Some(OutputMode::Query);
app.should_quit = true;
true
}
KeyCode::Enter if key.modifiers.contains(KeyModifiers::SHIFT) => {
if app.debouncer.has_pending() {
crate::editor::editor_events::execute_query(app);
app.debouncer.mark_executed();
}
if app.query.result.is_ok() && !app.query().is_empty() {
let query = app.query().to_string();
app.history.add_entry(&query);
}
app.output_mode = Some(OutputMode::Query);
app.should_quit = true;
true
}
KeyCode::Enter if key.modifiers.contains(KeyModifiers::ALT) => {
if app.debouncer.has_pending() {
crate::editor::editor_events::execute_query(app);
app.debouncer.mark_executed();
}
if app.query.result.is_ok() && !app.query().is_empty() {
let query = app.query().to_string();
app.history.add_entry(&query);
}
app.output_mode = Some(OutputMode::Query);
app.should_quit = true;
true
}
KeyCode::Enter => {
if accept_autocomplete_suggestion(app) {
return true;
}
if app.debouncer.has_pending() {
crate::editor::editor_events::execute_query(app);
app.debouncer.mark_executed();
}
if app.query.result.is_ok() && !app.query().is_empty() {
let query = app.query().to_string();
app.history.add_entry(&query);
}
app.output_mode = Some(OutputMode::Results);
app.should_quit = true;
true
}
KeyCode::Tab if !key.modifiers.contains(KeyModifiers::CONTROL) => {
accept_autocomplete_suggestion(app)
}
KeyCode::BackTab => {
if app.history.is_visible() {
app.history.close();
}
app.focus = match app.focus {
Focus::InputField => Focus::ResultsPane,
Focus::ResultsPane => Focus::InputField,
};
true
}
KeyCode::F(1) => {
app.help.visible = !app.help.visible;
true
}
KeyCode::Char('?') => {
if app.input.editor_mode == crate::editor::EditorMode::Normal
|| app.focus == Focus::ResultsPane
{
app.help.visible = !app.help.visible;
true
} else {
false
}
}
KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if app.query.result.is_err() {
app.error_overlay_visible = !app.error_overlay_visible;
}
true
}
KeyCode::Char('t') if key.modifiers.contains(KeyModifiers::CONTROL) => {
crate::tooltip::tooltip_events::handle_tooltip_toggle(&mut app.tooltip);
true
}
KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::CONTROL) => {
crate::search::search_events::open_search(app);
true
}
_ => false, }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::editor::EditorMode;
use crate::history::HistoryState;
use crate::test_utils::test_helpers::{
TEST_JSON, app_with_query, key, key_with_mods, test_app,
};
use tui_textarea::CursorMove;
fn flush_debounced_query(app: &mut App) {
if app.debouncer.has_pending() {
crate::editor::editor_events::execute_query(app);
app.debouncer.mark_executed();
}
}
#[test]
fn test_error_overlay_initializes_hidden() {
let app = test_app(TEST_JSON);
assert!(!app.error_overlay_visible);
}
#[test]
fn test_ctrl_e_toggles_error_overlay_when_error_exists() {
let mut app = test_app(TEST_JSON);
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::Char('|')));
flush_debounced_query(&mut app);
assert!(app.query.result.is_err());
assert!(!app.error_overlay_visible);
app.handle_key_event(key_with_mods(KeyCode::Char('e'), KeyModifiers::CONTROL));
assert!(app.error_overlay_visible);
app.handle_key_event(key_with_mods(KeyCode::Char('e'), KeyModifiers::CONTROL));
assert!(!app.error_overlay_visible);
}
#[test]
fn test_ctrl_e_does_nothing_when_no_error() {
let mut app = test_app(TEST_JSON);
assert!(app.query.result.is_ok());
assert!(!app.error_overlay_visible);
app.handle_key_event(key_with_mods(KeyCode::Char('e'), KeyModifiers::CONTROL));
assert!(!app.error_overlay_visible); }
#[test]
fn test_error_overlay_hides_on_query_change() {
let mut app = test_app(TEST_JSON);
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::Char('|')));
flush_debounced_query(&mut app);
assert!(app.query.result.is_err());
app.handle_key_event(key_with_mods(KeyCode::Char('e'), KeyModifiers::CONTROL));
assert!(app.error_overlay_visible);
app.handle_key_event(key(KeyCode::Backspace));
assert!(!app.error_overlay_visible);
}
#[test]
fn test_error_overlay_hides_on_query_change_in_normal_mode() {
let mut app = test_app(TEST_JSON);
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::Char('|')));
flush_debounced_query(&mut app);
assert!(app.query.result.is_err());
app.handle_key_event(key_with_mods(KeyCode::Char('e'), KeyModifiers::CONTROL));
assert!(app.error_overlay_visible);
app.handle_key_event(key(KeyCode::Esc));
app.input.textarea.move_cursor(CursorMove::Head);
app.handle_key_event(key(KeyCode::Char('x')));
assert!(!app.error_overlay_visible);
}
#[test]
fn test_ctrl_e_works_in_normal_mode() {
let mut app = test_app(TEST_JSON);
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::Char('|')));
flush_debounced_query(&mut app);
assert!(app.query.result.is_err());
app.handle_key_event(key(KeyCode::Esc));
assert_eq!(app.input.editor_mode, EditorMode::Normal);
app.handle_key_event(key_with_mods(KeyCode::Char('e'), KeyModifiers::CONTROL));
assert!(app.error_overlay_visible);
}
#[test]
fn test_ctrl_e_works_when_results_pane_focused() {
let mut app = test_app(TEST_JSON);
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::Char('|')));
flush_debounced_query(&mut app);
assert!(app.query.result.is_err());
app.handle_key_event(key(KeyCode::BackTab));
assert_eq!(app.focus, Focus::ResultsPane);
app.handle_key_event(key_with_mods(KeyCode::Char('e'), KeyModifiers::CONTROL));
assert!(app.error_overlay_visible);
}
#[test]
fn test_ctrl_c_sets_quit_flag() {
let mut app = app_with_query(".");
app.handle_key_event(key_with_mods(KeyCode::Char('c'), KeyModifiers::CONTROL));
assert!(app.should_quit);
}
#[test]
fn test_q_sets_quit_flag_in_normal_mode() {
let mut app = app_with_query(".");
app.input.editor_mode = EditorMode::Normal;
app.handle_key_event(key(KeyCode::Char('q')));
assert!(app.should_quit);
}
#[test]
fn test_q_does_not_quit_in_insert_mode() {
let mut app = app_with_query(".");
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::Char('q')));
assert!(!app.should_quit);
assert_eq!(app.query(), ".q");
}
#[test]
fn test_enter_sets_results_output_mode() {
let mut app = app_with_query(".");
app.handle_key_event(key(KeyCode::Enter));
assert_eq!(app.output_mode, Some(OutputMode::Results));
assert!(app.should_quit);
}
#[test]
fn test_enter_saves_successful_query_to_history() {
let mut app = app_with_query(".name");
let initial_count = app.history.total_count();
assert!(app.query.result.is_ok());
app.handle_key_event(key(KeyCode::Enter));
assert_eq!(app.history.total_count(), initial_count + 1);
assert!(app.should_quit);
}
#[test]
fn test_enter_does_not_save_failed_query_to_history() {
let mut app = test_app(r#"{"name": "test"}"#);
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::Char('|')));
flush_debounced_query(&mut app);
let initial_count = app.history.total_count();
assert!(app.query.result.is_err());
app.handle_key_event(key(KeyCode::Enter));
assert_eq!(app.history.total_count(), initial_count);
assert!(app.should_quit);
}
#[test]
fn test_enter_does_not_save_empty_query_to_history() {
let mut app = app_with_query("");
let initial_count = app.history.total_count();
app.handle_key_event(key(KeyCode::Enter));
assert_eq!(app.history.total_count(), initial_count);
assert!(app.should_quit);
}
#[test]
fn test_ctrl_q_outputs_query_and_saves_successful_query() {
let mut app = app_with_query(".name");
let initial_count = app.history.total_count();
app.handle_key_event(key_with_mods(KeyCode::Char('q'), KeyModifiers::CONTROL));
assert_eq!(app.history.total_count(), initial_count + 1);
assert_eq!(app.output_mode, Some(OutputMode::Query));
assert!(app.should_quit);
}
#[test]
fn test_ctrl_q_does_not_save_failed_query() {
let mut app = test_app(TEST_JSON);
app.input.editor_mode = EditorMode::Insert;
app.history = HistoryState::empty();
app.handle_key_event(key(KeyCode::Char('|')));
flush_debounced_query(&mut app);
let initial_count = app.history.total_count();
assert!(app.query.result.is_err());
app.handle_key_event(key_with_mods(KeyCode::Char('q'), KeyModifiers::CONTROL));
assert_eq!(app.history.total_count(), initial_count);
assert_eq!(app.output_mode, Some(OutputMode::Query));
assert!(app.should_quit);
}
#[test]
fn test_shift_enter_outputs_query_and_saves_successful_query() {
let mut app = app_with_query(".name");
let initial_count = app.history.total_count();
app.handle_key_event(key_with_mods(KeyCode::Enter, KeyModifiers::SHIFT));
assert_eq!(app.history.total_count(), initial_count + 1);
assert_eq!(app.output_mode, Some(OutputMode::Query));
assert!(app.should_quit);
}
#[test]
fn test_shift_enter_does_not_save_failed_query() {
let mut app = test_app(TEST_JSON);
app.input.editor_mode = EditorMode::Insert;
app.history = HistoryState::empty();
app.handle_key_event(key(KeyCode::Char('|')));
flush_debounced_query(&mut app);
let initial_count = app.history.total_count();
assert!(app.query.result.is_err());
app.handle_key_event(key_with_mods(KeyCode::Enter, KeyModifiers::SHIFT));
assert_eq!(app.history.total_count(), initial_count);
assert_eq!(app.output_mode, Some(OutputMode::Query));
assert!(app.should_quit);
}
#[test]
fn test_alt_enter_outputs_query_and_saves_successful_query() {
let mut app = app_with_query(".name");
let initial_count = app.history.total_count();
app.handle_key_event(key_with_mods(KeyCode::Enter, KeyModifiers::ALT));
assert_eq!(app.history.total_count(), initial_count + 1);
assert_eq!(app.output_mode, Some(OutputMode::Query));
assert!(app.should_quit);
}
#[test]
fn test_alt_enter_does_not_save_failed_query() {
let mut app = test_app(TEST_JSON);
app.input.editor_mode = EditorMode::Insert;
app.history = HistoryState::empty();
app.handle_key_event(key(KeyCode::Char('|')));
flush_debounced_query(&mut app);
let initial_count = app.history.total_count();
assert!(app.query.result.is_err());
app.handle_key_event(key_with_mods(KeyCode::Enter, KeyModifiers::ALT));
assert_eq!(app.history.total_count(), initial_count);
assert_eq!(app.output_mode, Some(OutputMode::Query));
assert!(app.should_quit);
}
#[test]
fn test_shift_tab_switches_focus_to_results() {
let mut app = app_with_query(".");
app.focus = Focus::InputField;
app.handle_key_event(key(KeyCode::BackTab));
assert_eq!(app.focus, Focus::ResultsPane);
}
#[test]
fn test_shift_tab_switches_focus_to_input() {
let mut app = app_with_query(".");
app.focus = Focus::ResultsPane;
app.handle_key_event(key(KeyCode::BackTab));
assert_eq!(app.focus, Focus::InputField);
}
#[test]
fn test_global_keys_work_regardless_of_focus() {
let mut app = app_with_query(".");
app.focus = Focus::ResultsPane;
app.handle_key_event(key_with_mods(KeyCode::Char('c'), KeyModifiers::CONTROL));
assert!(app.should_quit);
}
#[test]
fn test_insert_mode_text_input_updates_query() {
let mut app = app_with_query("");
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::Char('.')));
assert_eq!(app.query(), ".");
}
#[test]
fn test_query_execution_resets_scroll() {
let mut app = app_with_query("");
app.input.editor_mode = EditorMode::Insert;
app.results_scroll.offset = 50;
app.handle_key_event(key(KeyCode::Char('.')));
assert_eq!(app.results_scroll.offset, 0);
}
#[test]
fn test_history_with_emoji() {
let mut app = app_with_query("");
app.input.editor_mode = EditorMode::Insert;
app.history.add_entry_in_memory(".emoji_field 🚀");
app.handle_key_event(key_with_mods(KeyCode::Char('p'), KeyModifiers::CONTROL));
assert_eq!(app.query(), ".emoji_field 🚀");
}
#[test]
fn test_history_with_multibyte_chars() {
let mut app = app_with_query("");
app.input.editor_mode = EditorMode::Insert;
app.history.add_entry_in_memory(".café | .naïve");
app.handle_key_event(key_with_mods(KeyCode::Char('p'), KeyModifiers::CONTROL));
assert_eq!(app.query(), ".café | .naïve");
}
#[test]
fn test_history_search_with_unicode() {
let mut app = app_with_query("");
app.input.editor_mode = EditorMode::Insert;
app.history.add_entry_in_memory(".café");
app.history.add_entry_in_memory(".coffee");
app.handle_key_event(key_with_mods(KeyCode::Char('r'), KeyModifiers::CONTROL));
app.handle_key_event(key(KeyCode::Char('c')));
app.handle_key_event(key(KeyCode::Char('a')));
app.handle_key_event(key(KeyCode::Char('f')));
assert_eq!(app.history.filtered_count(), 1);
}
#[test]
fn test_cycling_stops_at_oldest() {
let mut app = app_with_query("");
app.input.editor_mode = EditorMode::Insert;
app.history.add_entry_in_memory(".first");
app.handle_key_event(key_with_mods(KeyCode::Char('p'), KeyModifiers::CONTROL));
assert_eq!(app.query(), ".first");
app.handle_key_event(key_with_mods(KeyCode::Char('p'), KeyModifiers::CONTROL));
app.handle_key_event(key_with_mods(KeyCode::Char('p'), KeyModifiers::CONTROL));
assert_eq!(app.query(), ".first");
}
#[test]
fn test_history_popup_with_single_entry() {
let mut app = app_with_query("");
app.input.editor_mode = EditorMode::Insert;
app.history.add_entry_in_memory(".single");
app.handle_key_event(key_with_mods(KeyCode::Char('r'), KeyModifiers::CONTROL));
assert!(app.history.is_visible());
app.handle_key_event(key(KeyCode::Up));
assert_eq!(app.history.selected_index(), 0);
app.handle_key_event(key(KeyCode::Down));
assert_eq!(app.history.selected_index(), 0);
}
#[test]
fn test_filter_with_no_matches() {
let mut app = app_with_query("");
app.input.editor_mode = EditorMode::Insert;
app.history.add_entry_in_memory(".foo");
app.history.add_entry_in_memory(".bar");
app.handle_key_event(key_with_mods(KeyCode::Char('r'), KeyModifiers::CONTROL));
app.handle_key_event(key(KeyCode::Char('x')));
app.handle_key_event(key(KeyCode::Char('y')));
app.handle_key_event(key(KeyCode::Char('z')));
assert_eq!(app.history.filtered_count(), 0);
}
#[test]
fn test_backspace_on_empty_search() {
let mut app = app_with_query("");
app.input.editor_mode = EditorMode::Insert;
app.history.add_entry_in_memory(".test");
app.handle_key_event(key_with_mods(KeyCode::Char('r'), KeyModifiers::CONTROL));
assert_eq!(app.history.search_query(), "");
app.handle_key_event(key(KeyCode::Backspace));
assert_eq!(app.history.search_query(), "");
}
#[test]
fn test_q_quits_in_results_pane_insert_mode() {
let mut app = app_with_query("");
app.focus = Focus::ResultsPane;
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::Char('q')));
assert!(app.should_quit);
}
#[test]
fn test_q_does_not_quit_in_input_field_insert_mode() {
let mut app = app_with_query("");
app.focus = Focus::InputField;
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::Char('q')));
assert!(!app.should_quit);
assert!(app.query().contains('q'));
}
#[test]
fn test_q_quits_in_input_field_normal_mode() {
let mut app = app_with_query("");
app.focus = Focus::InputField;
app.input.editor_mode = EditorMode::Normal;
app.handle_key_event(key(KeyCode::Char('q')));
assert!(app.should_quit);
}
#[test]
fn test_q_quits_in_results_pane_normal_mode() {
let mut app = app_with_query("");
app.focus = Focus::ResultsPane;
app.input.editor_mode = EditorMode::Normal;
app.handle_key_event(key(KeyCode::Char('q')));
assert!(app.should_quit);
}
#[test]
fn test_focus_switch_preserves_editor_mode() {
let mut app = app_with_query("");
app.focus = Focus::InputField;
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::BackTab));
assert_eq!(app.focus, Focus::ResultsPane);
assert_eq!(app.input.editor_mode, EditorMode::Insert);
app.handle_key_event(key(KeyCode::Char('q')));
assert!(app.should_quit);
}
#[test]
fn test_help_popup_initializes_hidden() {
let app = test_app(TEST_JSON);
assert!(!app.help.visible);
}
#[test]
fn test_f1_toggles_help_popup() {
let mut app = app_with_query(".");
assert!(!app.help.visible);
app.handle_key_event(key(KeyCode::F(1)));
assert!(app.help.visible);
app.handle_key_event(key(KeyCode::F(1)));
assert!(!app.help.visible);
}
#[test]
fn test_question_mark_toggles_help_in_normal_mode() {
let mut app = app_with_query(".");
app.input.editor_mode = EditorMode::Normal;
app.focus = Focus::InputField;
app.handle_key_event(key(KeyCode::Char('?')));
assert!(app.help.visible);
app.handle_key_event(key(KeyCode::Char('?')));
assert!(!app.help.visible);
}
#[test]
fn test_question_mark_does_not_toggle_help_in_insert_mode() {
let mut app = app_with_query("");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
app.handle_key_event(key(KeyCode::Char('?')));
assert!(!app.help.visible);
assert!(app.query().contains('?'));
}
#[test]
fn test_esc_closes_help_popup() {
let mut app = app_with_query(".");
app.help.visible = true;
app.handle_key_event(key(KeyCode::Esc));
assert!(!app.help.visible);
}
#[test]
fn test_q_closes_help_popup() {
let mut app = app_with_query(".");
app.help.visible = true;
app.handle_key_event(key(KeyCode::Char('q')));
assert!(!app.help.visible);
}
#[test]
fn test_help_popup_blocks_other_keys() {
let mut app = app_with_query(".");
app.help.visible = true;
app.input.editor_mode = EditorMode::Insert;
app.handle_key_event(key(KeyCode::Char('x')));
assert!(!app.query().contains('x'));
assert!(app.help.visible);
}
#[test]
fn test_f1_works_in_insert_mode() {
let mut app = app_with_query(".");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
app.handle_key_event(key(KeyCode::F(1)));
assert!(app.help.visible);
}
#[test]
fn test_help_popup_scroll_j_scrolls_down() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.update_bounds(60, 20);
app.help.scroll.offset = 0;
app.handle_key_event(key(KeyCode::Char('j')));
assert_eq!(app.help.scroll.offset, 1);
}
#[test]
fn test_help_popup_scroll_k_scrolls_up() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.offset = 5;
app.handle_key_event(key(KeyCode::Char('k')));
assert_eq!(app.help.scroll.offset, 4);
}
#[test]
fn test_help_popup_scroll_down_arrow() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.update_bounds(60, 20);
app.help.scroll.offset = 0;
app.handle_key_event(key(KeyCode::Down));
assert_eq!(app.help.scroll.offset, 1);
}
#[test]
fn test_help_popup_scroll_up_arrow() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.offset = 5;
app.handle_key_event(key(KeyCode::Up));
assert_eq!(app.help.scroll.offset, 4);
}
#[test]
fn test_help_popup_scroll_capital_j_scrolls_10() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.update_bounds(60, 20);
app.help.scroll.offset = 0;
app.handle_key_event(key(KeyCode::Char('J')));
assert_eq!(app.help.scroll.offset, 10);
}
#[test]
fn test_help_popup_scroll_capital_k_scrolls_10() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.offset = 15;
app.handle_key_event(key(KeyCode::Char('K')));
assert_eq!(app.help.scroll.offset, 5);
}
#[test]
fn test_help_popup_scroll_ctrl_d() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.update_bounds(60, 20);
app.help.scroll.offset = 0;
app.handle_key_event(key_with_mods(KeyCode::Char('d'), KeyModifiers::CONTROL));
assert_eq!(app.help.scroll.offset, 10);
}
#[test]
fn test_help_popup_scroll_ctrl_u() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.offset = 15;
app.handle_key_event(key_with_mods(KeyCode::Char('u'), KeyModifiers::CONTROL));
assert_eq!(app.help.scroll.offset, 5);
}
#[test]
fn test_help_popup_scroll_g_jumps_to_top() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.offset = 20;
app.handle_key_event(key(KeyCode::Char('g')));
assert_eq!(app.help.scroll.offset, 0);
}
#[test]
fn test_help_popup_scroll_capital_g_jumps_to_bottom() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.update_bounds(60, 20);
app.help.scroll.offset = 0;
app.handle_key_event(key(KeyCode::Char('G')));
assert_eq!(app.help.scroll.offset, app.help.scroll.max_offset);
}
#[test]
fn test_help_popup_scroll_k_saturates_at_zero() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.offset = 0;
app.handle_key_event(key(KeyCode::Char('k')));
assert_eq!(app.help.scroll.offset, 0);
}
#[test]
fn test_help_popup_close_resets_scroll() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.offset = 10;
app.handle_key_event(key(KeyCode::Esc));
assert!(!app.help.visible);
assert_eq!(app.help.scroll.offset, 0);
}
#[test]
fn test_help_popup_scroll_page_down() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.update_bounds(60, 20);
app.help.scroll.offset = 0;
app.handle_key_event(key(KeyCode::PageDown));
assert_eq!(app.help.scroll.offset, 10);
}
#[test]
fn test_help_popup_scroll_page_up() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.offset = 15;
app.handle_key_event(key(KeyCode::PageUp));
assert_eq!(app.help.scroll.offset, 5);
}
#[test]
fn test_help_popup_scroll_home_jumps_to_top() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.offset = 20;
app.handle_key_event(key(KeyCode::Home));
assert_eq!(app.help.scroll.offset, 0);
}
#[test]
fn test_help_popup_scroll_end_jumps_to_bottom() {
let mut app = app_with_query(".");
app.help.visible = true;
app.help.scroll.update_bounds(60, 20);
app.help.scroll.offset = 0;
app.handle_key_event(key(KeyCode::End));
assert_eq!(app.help.scroll.offset, app.help.scroll.max_offset);
}
#[test]
fn test_tab_does_not_accept_autocomplete_in_results_pane() {
let mut app = app_with_query(".na");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::ResultsPane;
let suggestions = vec![crate::autocomplete::Suggestion::new(
".name",
crate::autocomplete::SuggestionType::Field,
)];
app.autocomplete.update_suggestions(suggestions);
assert!(app.autocomplete.is_visible());
app.handle_key_event(key(KeyCode::Tab));
assert_eq!(app.query(), ".na"); assert!(app.autocomplete.is_visible()); }
#[test]
fn test_vim_navigation_blocked_when_help_visible() {
let mut app = app_with_query(".test");
app.input.editor_mode = EditorMode::Normal;
app.focus = Focus::InputField;
app.help.visible = true;
app.handle_key_event(key(KeyCode::Char('h'))); app.handle_key_event(key(KeyCode::Char('l'))); app.handle_key_event(key(KeyCode::Char('w'))); app.handle_key_event(key(KeyCode::Char('x')));
assert_eq!(app.query(), ".test");
assert!(app.help.visible);
}
#[test]
fn test_history_popup_enter_not_intercepted_by_global() {
let mut app = app_with_query("");
app.input.editor_mode = EditorMode::Insert;
app.history.add_entry_in_memory(".selected");
app.handle_key_event(key_with_mods(KeyCode::Char('r'), KeyModifiers::CONTROL));
assert!(app.history.is_visible());
app.handle_key_event(key(KeyCode::Enter));
assert!(!app.history.is_visible());
assert_eq!(app.query(), ".selected");
assert!(app.output_mode.is_none()); assert!(!app.should_quit); }
#[test]
fn test_tab_accepts_field_suggestion_replaces_from_dot() {
let mut app = app_with_query(".na");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
use crate::query::ResultType;
assert_eq!(
app.query.base_query_for_suggestions,
Some(".".to_string()),
"base_query should remain '.' since .na returns null"
);
assert_eq!(
app.query.base_type_for_suggestions,
Some(ResultType::Object),
"base_type should be Object (root object)"
);
let suggestions = vec![crate::autocomplete::Suggestion::new(
"name",
crate::autocomplete::SuggestionType::Field,
)];
app.autocomplete.update_suggestions(suggestions);
app.handle_key_event(key(KeyCode::Tab));
assert_eq!(app.query(), ".name");
assert!(!app.autocomplete.is_visible());
}
#[test]
fn test_tab_accepts_array_suggestion_appends() {
let mut app = app_with_query(".services");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
use crate::query::ResultType;
assert_eq!(
app.query.base_query_for_suggestions,
Some(".services".to_string()),
"base_query should be '.services'"
);
assert_eq!(
app.query.base_type_for_suggestions,
Some(ResultType::ArrayOfObjects),
"base_type should be ArrayOfObjects"
);
assert_eq!(app.input.textarea.cursor().1, 9);
let suggestions = vec![crate::autocomplete::Suggestion::new(
"[].name",
crate::autocomplete::SuggestionType::Field,
)];
app.autocomplete.update_suggestions(suggestions);
app.handle_key_event(key(KeyCode::Tab));
assert_eq!(app.query(), ".services[].name");
assert!(!app.autocomplete.is_visible());
}
#[test]
fn test_tab_accepts_array_suggestion_replaces_short_partial() {
let mut app = app_with_query(".services");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
use crate::query::ResultType;
assert_eq!(
app.query.base_query_for_suggestions,
Some(".services".to_string())
);
assert_eq!(
app.query.base_type_for_suggestions,
Some(ResultType::ArrayOfObjects)
);
app.input.textarea.insert_str(".s");
let suggestions = vec![crate::autocomplete::Suggestion::new(
"[].serviceArn",
crate::autocomplete::SuggestionType::Field,
)];
app.autocomplete.update_suggestions(suggestions);
app.handle_key_event(key(KeyCode::Tab));
assert_eq!(app.query(), ".services[].serviceArn");
assert!(!app.autocomplete.is_visible());
}
#[test]
fn test_tab_accepts_nested_array_suggestion() {
let mut app = app_with_query(".items[].tags");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
use crate::query::ResultType;
assert_eq!(
app.query.base_query_for_suggestions,
Some(".items[].tags".to_string()),
"base_query should be '.items[].tags'"
);
assert_eq!(
app.query.base_type_for_suggestions,
Some(ResultType::ArrayOfObjects),
"base_type should be ArrayOfObjects"
);
app.input.textarea.insert_char('.');
let suggestions = vec![crate::autocomplete::Suggestion::new(
"[].name",
crate::autocomplete::SuggestionType::Field,
)];
app.autocomplete.update_suggestions(suggestions);
app.handle_key_event(key(KeyCode::Tab));
assert_eq!(app.query(), ".items[].tags[].name");
assert!(!app.autocomplete.is_visible());
}
#[test]
fn test_tooltip_initializes_enabled() {
let app = test_app(TEST_JSON);
assert!(app.tooltip.enabled);
}
#[test]
fn test_ctrl_t_toggles_tooltip_from_enabled() {
let mut app = app_with_query(".");
assert!(app.tooltip.enabled);
app.handle_key_event(key_with_mods(KeyCode::Char('t'), KeyModifiers::CONTROL));
assert!(!app.tooltip.enabled);
}
#[test]
fn test_ctrl_t_toggles_tooltip_from_disabled() {
let mut app = app_with_query(".");
app.tooltip.toggle(); assert!(!app.tooltip.enabled);
app.handle_key_event(key_with_mods(KeyCode::Char('t'), KeyModifiers::CONTROL));
assert!(app.tooltip.enabled);
}
#[test]
fn test_ctrl_t_works_in_insert_mode() {
let mut app = app_with_query(".");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
assert!(app.tooltip.enabled);
app.handle_key_event(key_with_mods(KeyCode::Char('t'), KeyModifiers::CONTROL));
assert!(!app.tooltip.enabled);
}
#[test]
fn test_ctrl_t_works_in_normal_mode() {
let mut app = app_with_query(".");
app.input.editor_mode = EditorMode::Normal;
app.focus = Focus::InputField;
assert!(app.tooltip.enabled);
app.handle_key_event(key_with_mods(KeyCode::Char('t'), KeyModifiers::CONTROL));
assert!(!app.tooltip.enabled);
}
#[test]
fn test_ctrl_t_works_when_results_pane_focused() {
let mut app = app_with_query(".");
app.focus = Focus::ResultsPane;
assert!(app.tooltip.enabled);
app.handle_key_event(key_with_mods(KeyCode::Char('t'), KeyModifiers::CONTROL));
assert!(!app.tooltip.enabled);
}
#[test]
fn test_ctrl_t_preserves_current_function() {
let mut app = app_with_query("select(.x)");
app.tooltip.set_current_function(Some("select".to_string()));
assert!(app.tooltip.enabled);
app.handle_key_event(key_with_mods(KeyCode::Char('t'), KeyModifiers::CONTROL));
assert!(!app.tooltip.enabled);
assert_eq!(app.tooltip.current_function, Some("select".to_string()));
}
#[test]
fn test_ctrl_t_round_trip() {
let mut app = app_with_query(".");
let initial_enabled = app.tooltip.enabled;
app.handle_key_event(key_with_mods(KeyCode::Char('t'), KeyModifiers::CONTROL));
app.handle_key_event(key_with_mods(KeyCode::Char('t'), KeyModifiers::CONTROL));
assert_eq!(app.tooltip.enabled, initial_enabled);
}
#[test]
fn test_enter_accepts_suggestion_when_autocomplete_visible() {
let mut app = app_with_query(".na");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
let suggestions = vec![crate::autocomplete::Suggestion::new(
"name",
crate::autocomplete::SuggestionType::Field,
)];
app.autocomplete.update_suggestions(suggestions);
assert!(app.autocomplete.is_visible());
app.handle_key_event(key(KeyCode::Enter));
assert!(!app.should_quit);
assert!(app.output_mode.is_none());
assert_eq!(app.query(), ".name");
}
#[test]
fn test_enter_closes_autocomplete_popup_after_selection() {
let mut app = app_with_query(".na");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
let suggestions = vec![crate::autocomplete::Suggestion::new(
"name",
crate::autocomplete::SuggestionType::Field,
)];
app.autocomplete.update_suggestions(suggestions);
assert!(app.autocomplete.is_visible());
app.handle_key_event(key(KeyCode::Enter));
assert!(!app.autocomplete.is_visible());
}
#[test]
fn test_enter_exits_application_when_autocomplete_not_visible() {
let mut app = app_with_query(".");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
assert!(!app.autocomplete.is_visible());
app.handle_key_event(key(KeyCode::Enter));
assert!(app.should_quit);
assert_eq!(app.output_mode, Some(OutputMode::Results));
}
#[test]
fn test_enter_with_shift_modifier_bypasses_autocomplete_check() {
let mut app = app_with_query(".na");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
let suggestions = vec![crate::autocomplete::Suggestion::new(
"name",
crate::autocomplete::SuggestionType::Field,
)];
app.autocomplete.update_suggestions(suggestions);
assert!(app.autocomplete.is_visible());
app.handle_key_event(key_with_mods(KeyCode::Enter, KeyModifiers::SHIFT));
assert!(app.should_quit);
assert_eq!(app.output_mode, Some(OutputMode::Query));
}
#[test]
fn test_enter_with_alt_modifier_bypasses_autocomplete_check() {
let mut app = app_with_query(".na");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
let suggestions = vec![crate::autocomplete::Suggestion::new(
"name",
crate::autocomplete::SuggestionType::Field,
)];
app.autocomplete.update_suggestions(suggestions);
assert!(app.autocomplete.is_visible());
app.handle_key_event(key_with_mods(KeyCode::Enter, KeyModifiers::ALT));
assert!(app.should_quit);
assert_eq!(app.output_mode, Some(OutputMode::Query));
}
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_enter_tab_equivalence_for_autocomplete(
suggestion_type in prop_oneof![
Just(crate::autocomplete::SuggestionType::Field),
Just(crate::autocomplete::SuggestionType::Function),
],
suggestion_text in prop_oneof![
Just("name"),
Just("age"),
Just("city"),
Just("length"),
Just("keys"),
],
) {
let mut app_enter = app_with_query(".");
app_enter.input.editor_mode = EditorMode::Insert;
app_enter.focus = Focus::InputField;
let mut app_tab = app_with_query(".");
app_tab.input.editor_mode = EditorMode::Insert;
app_tab.focus = Focus::InputField;
let suggestion = crate::autocomplete::Suggestion::new(suggestion_text, suggestion_type.clone());
app_enter.autocomplete.update_suggestions(vec![suggestion.clone()]);
app_tab.autocomplete.update_suggestions(vec![suggestion]);
prop_assert!(app_enter.autocomplete.is_visible());
prop_assert!(app_tab.autocomplete.is_visible());
app_enter.handle_key_event(key(KeyCode::Enter));
app_tab.handle_key_event(key(KeyCode::Tab));
prop_assert_eq!(
app_enter.query(),
app_tab.query(),
"Enter and Tab should produce identical query strings"
);
prop_assert!(
!app_enter.autocomplete.is_visible(),
"Autocomplete should be hidden after Enter"
);
prop_assert!(
!app_tab.autocomplete.is_visible(),
"Autocomplete should be hidden after Tab"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_enter_accepts_autocomplete_and_closes_popup(
suggestion_text in prop_oneof![
Just("name"),
Just("age"),
Just("city"),
Just("services"),
Just("items"),
],
) {
let mut app = app_with_query(".");
app.input.editor_mode = EditorMode::Insert;
app.focus = Focus::InputField;
let suggestion = crate::autocomplete::Suggestion::new(
suggestion_text,
crate::autocomplete::SuggestionType::Field,
);
app.autocomplete.update_suggestions(vec![suggestion]);
prop_assert!(app.autocomplete.is_visible());
app.handle_key_event(key(KeyCode::Enter));
prop_assert!(
!app.autocomplete.is_visible(),
"Autocomplete should be hidden after Enter"
);
prop_assert!(
app.query().contains(suggestion_text),
"Query '{}' should contain suggestion text '{}'",
app.query(),
suggestion_text
);
prop_assert!(
!app.should_quit,
"Should not quit when accepting autocomplete"
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_enter_exits_when_autocomplete_not_visible(
focus_on_input in any::<bool>(),
insert_mode in any::<bool>(),
) {
let mut app = app_with_query(".");
app.focus = if focus_on_input {
Focus::InputField
} else {
Focus::ResultsPane
};
app.input.editor_mode = if insert_mode {
EditorMode::Insert
} else {
EditorMode::Normal
};
app.autocomplete.hide();
prop_assert!(!app.autocomplete.is_visible());
app.handle_key_event(key(KeyCode::Enter));
prop_assert!(
app.should_quit,
"Should quit when Enter pressed without autocomplete"
);
prop_assert_eq!(
app.output_mode,
Some(OutputMode::Results),
"Output mode should be Results"
);
}
}
}