use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tui_textarea::CursorMove;
use crate::app::App;
use crate::clipboard;
use crate::editor::EditorMode;
use crate::editor::char_search::{
CharSearchState, SearchDirection, SearchType, execute_char_search,
};
use crate::editor::mode::TextObjectScope;
use crate::editor::text_objects::{TextObjectTarget, execute_text_object};
use crate::help::HelpTab;
pub fn handle_insert_mode_key(app: &mut App, key: KeyEvent) {
app.input.reset_manual_scroll();
let content_changed = app.input.textarea.input(key);
if content_changed {
app.history.reset_cycling();
app.debouncer.schedule_execution();
app.results_scroll.reset();
app.results_cursor.reset();
app.error_overlay_visible = false;
app.input
.brace_tracker
.rebuild(app.input.textarea.lines()[0].as_ref());
}
app.update_autocomplete();
app.update_tooltip();
}
pub fn handle_normal_mode_key(app: &mut App, key: KeyEvent) {
app.input.reset_manual_scroll();
match key.code {
KeyCode::Char('?') => {
if app.help.visible {
app.help.reset();
} else {
app.help.active_tab = HelpTab::Input;
app.help.visible = true;
}
}
KeyCode::Char('h') | KeyCode::Left => {
app.input.textarea.move_cursor(CursorMove::Back);
}
KeyCode::Char('l') | KeyCode::Right => {
app.input.textarea.move_cursor(CursorMove::Forward);
}
KeyCode::Char('0') | KeyCode::Char('^') | KeyCode::Home => {
app.input.textarea.move_cursor(CursorMove::Head);
}
KeyCode::Char('$') | KeyCode::End => {
app.input.textarea.move_cursor(CursorMove::End);
}
KeyCode::Char('w') => {
app.input.textarea.move_cursor(CursorMove::WordForward);
}
KeyCode::Char('b') => {
app.input.textarea.move_cursor(CursorMove::WordBack);
}
KeyCode::Char('e') => {
app.input.textarea.move_cursor(CursorMove::WordEnd);
}
KeyCode::Char('i') => {
app.input.editor_mode = EditorMode::Insert;
}
KeyCode::Char('a') => {
app.input.textarea.move_cursor(CursorMove::Forward);
app.input.editor_mode = EditorMode::Insert;
}
KeyCode::Char('I') => {
app.input.textarea.move_cursor(CursorMove::Head);
app.input.editor_mode = EditorMode::Insert;
}
KeyCode::Char('A') => {
app.input.textarea.move_cursor(CursorMove::End);
app.input.editor_mode = EditorMode::Insert;
}
KeyCode::Char('x') => {
app.input.textarea.delete_next_char();
execute_query(app);
}
KeyCode::Char('X') => {
app.input.textarea.delete_char();
execute_query(app);
}
KeyCode::Char('D') => {
app.input.textarea.delete_line_by_end();
execute_query(app);
}
KeyCode::Char('C') => {
app.input.textarea.delete_line_by_end();
app.input.textarea.cancel_selection();
app.input.editor_mode = EditorMode::Insert;
execute_query(app);
}
KeyCode::Char('d') => {
app.input.editor_mode = EditorMode::Operator('d');
app.input.textarea.start_selection();
}
KeyCode::Char('c') => {
app.input.editor_mode = EditorMode::Operator('c');
app.input.textarea.start_selection();
}
KeyCode::Char('y') => {
app.input.editor_mode = EditorMode::Operator('y');
}
KeyCode::Char('f') => {
app.input.editor_mode =
EditorMode::CharSearch(SearchDirection::Forward, SearchType::Find);
}
KeyCode::Char('F') => {
app.input.editor_mode =
EditorMode::CharSearch(SearchDirection::Backward, SearchType::Find);
}
KeyCode::Char('t') => {
app.input.editor_mode =
EditorMode::CharSearch(SearchDirection::Forward, SearchType::Till);
}
KeyCode::Char('T') => {
app.input.editor_mode =
EditorMode::CharSearch(SearchDirection::Backward, SearchType::Till);
}
KeyCode::Char(';') => {
repeat_last_char_search(app, false);
}
KeyCode::Char(',') => {
repeat_last_char_search(app, true);
}
KeyCode::Char('/') => {
crate::search::search_events::open_search(app);
}
KeyCode::Char('u') => {
app.input.textarea.undo();
execute_query(app);
}
KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.input.textarea.redo();
execute_query(app);
}
_ => {}
}
app.update_tooltip();
}
pub fn handle_operator_mode_key(app: &mut App, key: KeyEvent) {
app.input.reset_manual_scroll();
let operator = match app.input.editor_mode {
EditorMode::Operator(op) => op,
_ => return,
};
if key.code == KeyCode::Char(operator) {
match operator {
'y' => {
clipboard::clipboard_events::handle_yank_key(app, app.clipboard_backend);
app.input.editor_mode = EditorMode::Normal;
}
'd' | 'c' => {
app.input.textarea.delete_line_by_head();
app.input.textarea.delete_line_by_end();
app.input.editor_mode = if operator == 'c' {
EditorMode::Insert
} else {
EditorMode::Normal
};
execute_query(app);
}
_ => {
app.input.editor_mode = EditorMode::Normal;
}
}
return;
}
if matches!(operator, 'd' | 'c')
&& let Some((direction, search_type)) = operator_char_search_from_key(key.code)
{
let start_col = app.input.textarea.cursor().1;
app.input.textarea.cancel_selection();
app.input.editor_mode =
EditorMode::OperatorCharSearch(operator, start_col, direction, search_type);
app.update_tooltip();
return;
}
let motion_applied = match key.code {
KeyCode::Char('w') => {
app.input.textarea.move_cursor(CursorMove::WordForward);
true
}
KeyCode::Char('b') => {
app.input.textarea.move_cursor(CursorMove::WordBack);
true
}
KeyCode::Char('e') => {
app.input.textarea.move_cursor(CursorMove::WordEnd);
app.input.textarea.move_cursor(CursorMove::Forward);
true
}
KeyCode::Char('0') | KeyCode::Char('^') | KeyCode::Home => {
app.input.textarea.move_cursor(CursorMove::Head);
true
}
KeyCode::Char('$') | KeyCode::End => {
app.input.textarea.move_cursor(CursorMove::End);
true
}
KeyCode::Char('h') | KeyCode::Left => {
app.input.textarea.move_cursor(CursorMove::Back);
true
}
KeyCode::Char('l') | KeyCode::Right => {
app.input.textarea.move_cursor(CursorMove::Forward);
true
}
KeyCode::Char('i') => {
app.input.textarea.cancel_selection();
app.input.editor_mode = EditorMode::TextObject(operator, TextObjectScope::Inner);
app.update_tooltip();
return;
}
KeyCode::Char('a') => {
app.input.textarea.cancel_selection();
app.input.editor_mode = EditorMode::TextObject(operator, TextObjectScope::Around);
app.update_tooltip();
return;
}
_ => false,
};
if motion_applied {
match operator {
'd' => {
app.input.textarea.cut();
app.input.editor_mode = EditorMode::Normal;
}
'c' => {
app.input.textarea.cut();
app.input.editor_mode = EditorMode::Insert;
}
_ => {
app.input.textarea.cancel_selection();
app.input.editor_mode = EditorMode::Normal;
}
}
execute_query(app);
} else {
app.input.textarea.cancel_selection();
app.input.editor_mode = EditorMode::Normal;
}
app.update_tooltip();
}
pub fn handle_char_search_mode_key(app: &mut App, key: KeyEvent) {
app.input.reset_manual_scroll();
let (direction, search_type) = match app.input.editor_mode {
EditorMode::CharSearch(dir, st) => (dir, st),
_ => return,
};
if let KeyCode::Char(target) = key.code {
let found = execute_char_search(&mut app.input.textarea, target, direction, search_type);
if found {
app.input.last_char_search = Some(CharSearchState {
character: target,
direction,
search_type,
});
}
}
app.input.editor_mode = EditorMode::Normal;
app.update_tooltip();
}
pub fn handle_operator_char_search_mode_key(app: &mut App, key: KeyEvent) {
app.input.reset_manual_scroll();
let (operator, start_col, direction, search_type) = match app.input.editor_mode {
EditorMode::OperatorCharSearch(op, start, dir, st) => (op, start, dir, st),
_ => return,
};
if key.code == KeyCode::Esc {
app.input.textarea.cancel_selection();
app.input.editor_mode = EditorMode::Normal;
app.update_tooltip();
return;
}
let target = match key.code {
KeyCode::Char(ch) => ch,
_ => {
app.input.textarea.cancel_selection();
app.input.editor_mode = EditorMode::Normal;
app.update_tooltip();
return;
}
};
let text = app
.input
.textarea
.lines()
.first()
.map(|s| s.as_str())
.unwrap_or("");
let range = find_operator_char_range(text, start_col, target, direction, search_type);
if let Some((start, end)) = range {
cut_range(&mut app.input.textarea, start, end);
app.input.editor_mode = if operator == 'c' {
EditorMode::Insert
} else {
EditorMode::Normal
};
execute_query(app);
} else {
app.input.editor_mode = EditorMode::Normal;
}
app.update_tooltip();
}
pub fn handle_text_object_mode_key(app: &mut App, key: KeyEvent) {
app.input.reset_manual_scroll();
let (operator, scope) = match app.input.editor_mode {
EditorMode::TextObject(op, sc) => (op, sc),
_ => return,
};
if let KeyCode::Char(target_char) = key.code {
if let Some(target) = TextObjectTarget::from_char(target_char) {
let success = execute_text_object(&mut app.input.textarea, target, scope);
if success {
app.input.editor_mode = if operator == 'c' {
EditorMode::Insert
} else {
EditorMode::Normal
};
execute_query(app);
} else {
app.input.editor_mode = EditorMode::Normal;
}
} else {
app.input.editor_mode = EditorMode::Normal;
}
} else {
app.input.editor_mode = EditorMode::Normal;
}
app.update_tooltip();
}
fn repeat_last_char_search(app: &mut App, reverse: bool) {
if let Some(search) = app.input.last_char_search {
let direction = if reverse {
search.direction.opposite()
} else {
search.direction
};
execute_char_search(
&mut app.input.textarea,
search.character,
direction,
search.search_type,
);
}
}
pub fn execute_query(app: &mut App) {
execute_query_with_auto_show(app);
}
pub fn execute_query_with_auto_show(app: &mut App) {
let query_state = match &mut app.query {
Some(q) => q,
None => return,
};
let query = app.input.textarea.lines()[0].as_ref();
app.input.brace_tracker.rebuild(query);
query_state.execute_async(query);
app.results_scroll.reset();
app.results_cursor.reset();
app.error_overlay_visible = false;
}
fn operator_char_search_from_key(key: KeyCode) -> Option<(SearchDirection, SearchType)> {
match key {
KeyCode::Char('f') => Some((SearchDirection::Forward, SearchType::Find)),
KeyCode::Char('F') => Some((SearchDirection::Backward, SearchType::Find)),
KeyCode::Char('t') => Some((SearchDirection::Forward, SearchType::Till)),
KeyCode::Char('T') => Some((SearchDirection::Backward, SearchType::Till)),
_ => None,
}
}
fn find_operator_char_range(
text: &str,
cursor_col: usize,
target: char,
direction: SearchDirection,
search_type: SearchType,
) -> Option<(usize, usize)> {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() || cursor_col >= chars.len() {
return None;
}
let match_index = find_char_match_index(&chars, cursor_col, target, direction)?;
let (start, end) = match direction {
SearchDirection::Forward => {
let start = cursor_col;
let end = match search_type {
SearchType::Find => match_index + 1,
SearchType::Till => match_index,
};
(start, end)
}
SearchDirection::Backward => {
let start = match search_type {
SearchType::Find => match_index,
SearchType::Till => match_index + 1,
};
let end = cursor_col + 1;
(start, end)
}
};
(start < end).then_some((start, end))
}
fn find_char_match_index(
chars: &[char],
cursor_col: usize,
target: char,
direction: SearchDirection,
) -> Option<usize> {
match direction {
SearchDirection::Forward => {
let search_start = cursor_col + 1;
if search_start >= chars.len() {
return None;
}
(search_start..chars.len()).find(|&i| chars[i] == target)
}
SearchDirection::Backward => {
if cursor_col == 0 {
return None;
}
(0..cursor_col).rev().find(|&i| chars[i] == target)
}
}
}
fn cut_range(textarea: &mut tui_textarea::TextArea, start: usize, end: usize) {
textarea.cancel_selection();
textarea.move_cursor(CursorMove::Head);
for _ in 0..start {
textarea.move_cursor(CursorMove::Forward);
}
textarea.start_selection();
for _ in start..end {
textarea.move_cursor(CursorMove::Forward);
}
textarea.cut();
}
#[cfg(test)]
#[path = "editor_events_tests.rs"]
mod editor_events_tests;