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.editor.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.editor.move_left(),
KeyCode::Char('l') | KeyCode::Right => {
ed_op_ref!(self, move_right(false));
}
KeyCode::Char('j') | KeyCode::Down => {
ed_op_ref!(self, move_down());
}
KeyCode::Char('k') | KeyCode::Up => {
ed_op_ref!(self, move_up());
}
KeyCode::Char('0') | KeyCode::Home => self.editor.move_line_start(),
KeyCode::Char('G') => {
ed_op_ref!(self, 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') => {
ed_op!(self, snapshot());
self.apply_visual_op(Operator::Delete);
self.enter_mode(Mode::Normal);
}
KeyCode::Char('c') | KeyCode::Char('s') => {
ed_op!(self, snapshot());
self.apply_visual_op(Operator::Change);
}
KeyCode::Char('I') => self.visual_insert_at_start(),
KeyCode::Char('A') => self.visual_append_at_end(),
KeyCode::Char('S') | KeyCode::Char('R') => self.visual_change_lines(),
KeyCode::Char('C') => self.visual_change_to_eol(),
KeyCode::Char('D') => self.visual_delete_to_eol(),
KeyCode::Char('X') => self.visual_delete_lines(),
KeyCode::Char('Y') => self.visual_yank_lines(),
KeyCode::Char('u') => self.transform_case_selection(crate::editor::to_lower_keep_width),
KeyCode::Char('U') => self.transform_case_selection(crate::editor::to_upper_keep_width),
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),
KeyCode::Char(':') => self.open_command_from_visual(),
_ => {}
}
Ok(())
}
fn apply_visual_motion(&mut self, motion: MotionKind) {
let target = self
.active_doc()
.motion_target(self.editor.cursor, motion, 1);
self.editor.cursor = target;
}
fn swap_visual_endpoints(&mut self) {
if let Some(anchor) = self.visual_anchor {
let cur = self.editor.cursor;
self.editor.cursor = anchor;
self.visual_anchor = Some(cur);
}
}
fn toggle_case_selection(&mut self) {
self.transform_case_selection(crate::editor::flip_case_char_keep_width);
}
fn transform_case_selection(&mut self, f: fn(char) -> char) {
let Some(sel) = self.selection() else { return };
ed_op!(self, snapshot());
let doc = self.active_doc_mut();
match sel {
Selection::Char { from, to } => {
let end = doc.advance_one(to);
doc.transform_case_range(from, end, f);
}
Selection::Line { from_row, to_row } => {
doc.transform_case_lines(from_row, to_row, f);
}
Selection::Block { r0, c0, r1, c1 } => {
doc.transform_case_block(r0, c0, r1, c1, f);
}
}
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;
}
ed_op!(self, snapshot());
self.editor.cursor.row = from_row;
self.editor.cursor.col = 0;
let doc_ref = self.editor.doc.clone();
let doc = self
.documents
.get_mut(&doc_ref)
.expect("active doc present");
for _ in 0..(to_row - from_row) {
self.editor.join_next_line(doc);
}
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),
};
ed_op!(self, snapshot());
let indent_settings = self.indent_settings();
let doc_ref = self.editor.doc.clone();
let doc = self
.documents
.get_mut(&doc_ref)
.expect("active doc present");
for r in from_row..=to_row {
if indent {
self.editor.indent_line(doc, r, indent_settings);
} else {
self.editor.dedent_line(doc, r, indent_settings);
}
}
self.editor.cursor.row = from_row;
let col = {
let line = self.editor.current_line(doc);
line.chars().position(|c| !c.is_whitespace()).unwrap_or(0)
};
self.editor.cursor.col = col;
self.enter_mode(Mode::Normal);
}
fn visual_insert_at_start(&mut self) {
let Some(sel) = self.selection() else { return };
ed_op!(self, snapshot());
self.editor.extra_cursors.clear();
match sel {
Selection::Char { from, .. } => {
self.editor.cursor = from;
}
Selection::Line { from_row, to_row } => {
self.editor.cursor = Cursor {
row: from_row,
col: first_non_blank(&self.active_doc().lines[from_row]),
};
for r in (from_row + 1)..=to_row {
self.editor.extra_cursors.push(Cursor {
row: r,
col: first_non_blank(&self.active_doc().lines[r]),
});
}
}
Selection::Block { r0, c0, r1, .. } => {
self.editor.cursor = Cursor { row: r0, col: c0 };
for r in (r0 + 1)..=r1 {
let len = self.active_doc().lines[r].chars().count();
self.editor.extra_cursors.push(Cursor {
row: r,
col: c0.min(len),
});
}
}
}
self.enter_mode(Mode::Insert);
}
fn visual_append_at_end(&mut self) {
let Some(sel) = self.selection() else { return };
ed_op!(self, snapshot());
self.editor.extra_cursors.clear();
match sel {
Selection::Char { to, .. } => {
let len = self.active_doc().lines[to.row].chars().count();
self.editor.cursor = Cursor {
row: to.row,
col: (to.col + 1).min(len),
};
}
Selection::Line { from_row, to_row } => {
self.editor.cursor = Cursor {
row: from_row,
col: self.active_doc().lines[from_row].chars().count(),
};
for r in (from_row + 1)..=to_row {
self.editor.extra_cursors.push(Cursor {
row: r,
col: self.active_doc().lines[r].chars().count(),
});
}
}
Selection::Block { r0, r1, c1, .. } => {
let primary_len = self.active_doc().lines[r0].chars().count();
self.editor.cursor = Cursor {
row: r0,
col: (c1 + 1).min(primary_len),
};
for r in (r0 + 1)..=r1 {
let len = self.active_doc().lines[r].chars().count();
self.editor.extra_cursors.push(Cursor {
row: r,
col: (c1 + 1).min(len),
});
}
}
}
self.enter_mode(Mode::Insert);
}
fn visual_change_lines(&mut self) {
let Some((from_row, to_row)) = self.selection_row_span() else {
return;
};
ed_op!(self, snapshot());
self.editor.extra_cursors.clear();
ed_op!(self, delete_lines(from_row, to_row));
self.editor.cursor.col = 0;
self.enter_mode(Mode::Insert);
}
fn visual_change_to_eol(&mut self) {
let Some(sel) = self.selection() else { return };
match sel {
Selection::Block { r0, c0, r1, .. } => {
ed_op!(self, snapshot());
self.editor.extra_cursors.clear();
truncate_block_to_eol(self, r0, c0, r1);
self.editor.cursor = Cursor { row: r0, col: c0 };
for r in (r0 + 1)..=r1 {
let len = self.active_doc().lines[r].chars().count();
self.editor.extra_cursors.push(Cursor {
row: r,
col: c0.min(len),
});
}
self.enter_mode(Mode::Insert);
}
_ => self.visual_change_lines(),
}
}
fn visual_delete_to_eol(&mut self) {
let Some(sel) = self.selection() else { return };
match sel {
Selection::Block { r0, c0, r1, .. } => {
ed_op!(self, snapshot());
truncate_block_to_eol(self, r0, c0, r1);
self.editor.cursor = Cursor { row: r0, col: c0 };
ed_op_ref!(self, clamp_col(false));
self.enter_mode(Mode::Normal);
}
_ => self.visual_delete_lines(),
}
}
fn visual_delete_lines(&mut self) {
let Some((from_row, to_row)) = self.selection_row_span() else {
return;
};
ed_op!(self, snapshot());
ed_op!(self, delete_lines(from_row, to_row));
self.enter_mode(Mode::Normal);
}
fn visual_yank_lines(&mut self) {
let Some((from_row, to_row)) = self.selection_row_span() else {
return;
};
self.active_doc_mut().yank_lines(from_row, to_row);
self.sync_yank_to_clipboard();
self.push_toast(Toast::info("yanked"));
self.editor.cursor.row = from_row;
self.editor.cursor.col = 0;
self.enter_mode(Mode::Normal);
}
fn selection_row_span(&self) -> Option<(usize, usize)> {
Some(match self.selection()? {
Selection::Char { from, to } => (from.row, to.row),
Selection::Line { from_row, to_row } => (from_row, to_row),
Selection::Block { r0, r1, .. } => (r0, r1),
})
}
fn toggle_visual(&mut self, target: Mode) {
if self.editor.mode == target {
self.enter_mode(Mode::Normal);
} else {
self.editor.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.active_doc().advance_one(to);
match op {
Operator::Yank => {
self.active_doc_mut().yank_range(from, end);
self.sync_yank_to_clipboard();
self.push_toast(Toast::info("yanked"));
self.editor.cursor = from;
}
Operator::Delete => ed_op!(self, delete_range(from, end)),
Operator::Change => {
ed_op!(self, delete_range(from, end));
self.enter_mode(Mode::Insert);
}
Operator::Indent | Operator::Dedent => {
unreachable!("indent/dedent dispatched via indent_selection")
}
Operator::Comment => self.apply_visual_comment(from.row, end.row),
Operator::BlockComment => {
self.apply_visual_block_comment(from, end, from.row, end.row)
}
}
}
Selection::Line { from_row, to_row } => match op {
Operator::Yank => {
self.active_doc_mut().yank_lines(from_row, to_row);
self.sync_yank_to_clipboard();
self.push_toast(Toast::info("yanked"));
self.editor.cursor.row = from_row;
self.editor.cursor.col = 0;
}
Operator::Delete => ed_op!(self, delete_lines(from_row, to_row)),
Operator::Change => {
ed_op!(self, delete_lines(from_row, to_row));
self.enter_mode(Mode::Insert);
}
Operator::Indent | Operator::Dedent => {
unreachable!("indent/dedent dispatched via indent_selection")
}
Operator::Comment => self.apply_visual_comment(from_row, to_row),
Operator::BlockComment => {
let (lo_row, hi_row) = (from_row.min(to_row), from_row.max(to_row));
let hi_col = self.active_doc().lines[hi_row].chars().count();
self.apply_visual_block_comment(
Cursor {
row: lo_row,
col: 0,
},
Cursor {
row: hi_row,
col: hi_col,
},
lo_row,
hi_row,
);
}
},
Selection::Block { r0, c0, r1, c1 } => match op {
Operator::Yank => {
self.active_doc_mut().yank_block(r0, c0, r1, c1);
self.sync_yank_to_clipboard();
self.push_toast(Toast::info("yanked"));
self.editor.cursor = Cursor { row: r0, col: c0 };
}
Operator::Delete => ed_op!(self, delete_block(r0, c0, r1, c1)),
Operator::Change => {
self.editor.extra_cursors.clear();
ed_op!(self, delete_block(r0, c0, r1, c1));
self.editor.cursor = Cursor { row: r0, col: c0 };
for r in (r0 + 1)..=r1 {
let len = self.active_doc().lines[r].chars().count();
self.editor.extra_cursors.push(Cursor {
row: r,
col: c0.min(len),
});
}
self.enter_mode(Mode::Insert);
}
Operator::Indent | Operator::Dedent => {
unreachable!("indent/dedent dispatched via indent_selection")
}
Operator::Comment | Operator::BlockComment => self.apply_visual_comment(r0, r1),
},
}
}
fn apply_visual_comment(&mut self, row_a: usize, row_b: usize) {
let (lo, hi) = (row_a.min(row_b), row_a.max(row_b));
let rows: Vec<usize> = (lo..=hi).collect();
if !self.apply_line_comment(&rows) {
self.push_toast(Toast::error("no comment token for this buffer"));
}
}
fn apply_visual_block_comment(&mut self, from: Cursor, to: Cursor, row_a: usize, row_b: usize) {
let (lo, hi) = (row_a.min(row_b), row_a.max(row_b));
let rows: Vec<usize> = (lo..=hi).collect();
if !self.apply_block_comment(&rows, Some((from, to))) {
self.push_toast(Toast::error(
"no block- or line-comment tokens for this buffer",
));
}
}
}
fn first_non_blank(line: &str) -> usize {
line.chars().position(|c| !c.is_whitespace()).unwrap_or(0)
}
fn truncate_block_to_eol(app: &mut App, r0: usize, c0: usize, r1: usize) {
let r1 = r1.min(app.active_doc().lines.len().saturating_sub(1));
let max_len = (r0..=r1)
.map(|r| app.active_doc().lines[r].chars().count())
.max()
.unwrap_or(0);
if max_len <= c0 {
return;
}
ed_op!(app, delete_block(r0, c0, r1, max_len.saturating_sub(1)));
}
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,
})
}