use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::action::{MotionKind, Operator};
use crate::app::{App, Selection, Toast};
use crate::editor::Cursor;
use crate::mode::Mode;
impl App {
pub(super) fn handle_visual_key(&mut self, key: KeyEvent) -> Result<()> {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if std::mem::take(&mut self.visual_g_pending) {
match key.code {
KeyCode::Char('g') => self.buffer.move_file_start(),
KeyCode::Char('e') => self.apply_visual_motion(MotionKind::WordEndBack),
KeyCode::Char('E') => self.apply_visual_motion(MotionKind::BigWordEndBack),
KeyCode::Char('_') => self.apply_visual_motion(MotionKind::LineLastNonBlank),
KeyCode::Char('s') => self.apply_visual_motion(MotionKind::LineFirstNonBlank),
KeyCode::Char('l') => self.apply_visual_motion(MotionKind::LineEnd),
KeyCode::Char('c') => self.apply_visual_motion(MotionKind::ViewportMiddle),
KeyCode::Char('b') => self.apply_visual_motion(MotionKind::ViewportBottom),
KeyCode::Char('n') => self.run_search_select(self.search.last_forward),
KeyCode::Char('N') => self.run_search_select(!self.search.last_forward),
_ => {}
}
return Ok(());
}
if let Some(motion) = visual_motion_for(key) {
self.apply_visual_motion(motion);
return Ok(());
}
match key.code {
KeyCode::Esc => self.enter_mode(Mode::Normal),
KeyCode::Char('h') | KeyCode::Left => self.buffer.move_left(),
KeyCode::Char('l') | KeyCode::Right => self.buffer.move_right(false),
KeyCode::Char('j') | KeyCode::Down => self.buffer.move_down(),
KeyCode::Char('k') | KeyCode::Up => self.buffer.move_up(),
KeyCode::Char('0') | KeyCode::Home => self.buffer.move_line_start(),
KeyCode::Char('G') => self.buffer.move_file_end(),
KeyCode::Char('g') => self.visual_g_pending = true,
KeyCode::Char('*') => self.search_word_under_cursor(true),
KeyCode::Char('#') => self.search_word_under_cursor(false),
KeyCode::Char('o') => self.swap_visual_endpoints(),
KeyCode::Char('v') if !ctrl => self.toggle_visual(Mode::Visual),
KeyCode::Char('v') if ctrl => self.toggle_visual(Mode::VisualBlock),
KeyCode::Char('V') => self.toggle_visual(Mode::VisualLine),
KeyCode::Char('y') => {
self.apply_visual_op(Operator::Yank);
self.enter_mode(Mode::Normal);
}
KeyCode::Char('d') | KeyCode::Char('x') => {
self.buffer.snapshot();
self.apply_visual_op(Operator::Delete);
self.enter_mode(Mode::Normal);
}
KeyCode::Char('c') | KeyCode::Char('s') => {
self.buffer.snapshot();
self.apply_visual_op(Operator::Change);
}
KeyCode::Char('~') => self.toggle_case_selection(),
KeyCode::Char('J') => self.join_selection_lines(),
KeyCode::Char('>') => self.indent_selection(true),
KeyCode::Char('<') => self.indent_selection(false),
_ => {}
}
Ok(())
}
fn apply_visual_motion(&mut self, motion: MotionKind) {
let target = self.buffer.motion_target(self.buffer.cursor, motion, 1);
self.buffer.cursor = target;
}
fn swap_visual_endpoints(&mut self) {
if let Some(anchor) = self.visual_anchor {
let cur = self.buffer.cursor;
self.buffer.cursor = anchor;
self.visual_anchor = Some(cur);
}
}
fn toggle_case_selection(&mut self) {
let Some(sel) = self.selection() else { return };
self.buffer.snapshot();
match sel {
Selection::Char { from, to } => {
let end = self.buffer.advance_one(to);
self.buffer.toggle_case_range(from, end);
}
Selection::Line { from_row, to_row } => {
self.buffer.toggle_case_lines(from_row, to_row);
}
Selection::Block { r0, c0, r1, c1 } => {
self.buffer.toggle_case_block(r0, c0, r1, c1);
}
}
self.enter_mode(Mode::Normal);
}
fn join_selection_lines(&mut self) {
let Some(sel) = self.selection() else { return };
let (from_row, to_row) = match sel {
Selection::Char { from, to } => (from.row, to.row),
Selection::Line { from_row, to_row } => (from_row, to_row),
Selection::Block { r0, r1, .. } => (r0, r1),
};
if from_row == to_row {
return;
}
self.buffer.snapshot();
self.buffer.cursor.row = from_row;
self.buffer.cursor.col = 0;
for _ in 0..(to_row - from_row) {
self.buffer.join_next_line();
}
self.enter_mode(Mode::Normal);
}
fn indent_selection(&mut self, indent: bool) {
let Some(sel) = self.selection() else { return };
let (from_row, to_row) = match sel {
Selection::Char { from, to } => (from.row, to.row),
Selection::Line { from_row, to_row } => (from_row, to_row),
Selection::Block { r0, r1, .. } => (r0, r1),
};
self.buffer.snapshot();
let indent_settings = self.indent_settings();
for r in from_row..=to_row {
if indent {
self.buffer.indent_line(r, indent_settings);
} else {
self.buffer.dedent_line(r, indent_settings);
}
}
self.buffer.cursor.row = from_row;
let line = self.buffer.current_line();
let col = line.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
self.buffer.cursor.col = col;
self.enter_mode(Mode::Normal);
}
fn toggle_visual(&mut self, target: Mode) {
if self.mode == target {
self.enter_mode(Mode::Normal);
} else {
self.mode = target;
}
}
fn apply_visual_op(&mut self, op: Operator) {
let Some(sel) = self.selection() else { return };
match sel {
Selection::Char { from, to } => {
let end = self.buffer.advance_one(to);
match op {
Operator::Yank => {
self.buffer.yank_range(from, end);
self.sync_yank_to_clipboard();
self.toast = Toast::info("yanked");
self.buffer.cursor = from;
}
Operator::Delete => self.buffer.delete_range(from, end),
Operator::Change => {
self.buffer.delete_range(from, end);
self.enter_mode(Mode::Insert);
}
Operator::Indent | Operator::Dedent => {
unreachable!("indent/dedent dispatched via indent_selection")
}
}
}
Selection::Line { from_row, to_row } => match op {
Operator::Yank => {
self.buffer.yank_lines(from_row, to_row);
self.sync_yank_to_clipboard();
self.toast = Toast::info("yanked");
self.buffer.cursor.row = from_row;
self.buffer.cursor.col = 0;
}
Operator::Delete => self.buffer.delete_lines(from_row, to_row),
Operator::Change => {
self.buffer.delete_lines(from_row, to_row);
self.enter_mode(Mode::Insert);
}
Operator::Indent | Operator::Dedent => {
unreachable!("indent/dedent dispatched via indent_selection")
}
},
Selection::Block { r0, c0, r1, c1 } => match op {
Operator::Yank => {
self.buffer.yank_block(r0, c0, r1, c1);
self.sync_yank_to_clipboard();
self.toast = Toast::info("yanked");
self.buffer.cursor = Cursor { row: r0, col: c0 };
}
Operator::Delete => self.buffer.delete_block(r0, c0, r1, c1),
Operator::Change => {
self.buffer.delete_block(r0, c0, r1, c1);
self.enter_mode(Mode::Insert);
}
Operator::Indent | Operator::Dedent => {
unreachable!("indent/dedent dispatched via indent_selection")
}
},
}
}
}
fn visual_motion_for(key: KeyEvent) -> Option<MotionKind> {
use MotionKind as M;
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if ctrl {
return match key.code {
KeyCode::Char('d') => Some(M::HalfPageDown),
KeyCode::Char('u') => Some(M::HalfPageUp),
KeyCode::Char('f') => Some(M::PageDown),
KeyCode::Char('b') => Some(M::PageUp),
_ => None,
};
}
Some(match key.code {
KeyCode::Char('w') => M::WordForward,
KeyCode::Char('b') => M::WordBack,
KeyCode::Char('e') => M::WordEnd,
KeyCode::Char('W') => M::BigWordForward,
KeyCode::Char('B') => M::BigWordBack,
KeyCode::Char('E') => M::BigWordEnd,
KeyCode::Char('^') => M::LineFirstNonBlank,
KeyCode::Char('$') | KeyCode::End => M::LineEnd,
KeyCode::Char('%') => M::BracketMatch,
KeyCode::Char('H') => M::ViewportTop,
KeyCode::Char('M') => M::ViewportMiddle,
KeyCode::Char('L') => M::ViewportBottom,
KeyCode::Char('{') => M::ParagraphBack,
KeyCode::Char('}') => M::ParagraphForward,
_ => return None,
})
}