From 09767c968216cd4ff097f0255453f9fd132b8ca3 Mon Sep 17 00:00:00 2001 From: Kyler Clay Date: Fri, 30 May 2025 19:06:09 -0400 Subject: [PATCH] work on implementing visual mode --- src/parse/mod.rs | 2 - src/prompt/readline/linebuf.rs | 198 ++++++++++- src/prompt/readline/mod.rs | 41 ++- src/prompt/readline/mode.rs | 577 +++++++++++++++++++++++++++++++++ src/prompt/readline/vicmd.rs | 9 + 5 files changed, 808 insertions(+), 19 deletions(-) diff --git a/src/parse/mod.rs b/src/parse/mod.rs index e25ea94..d97391a 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -709,8 +709,6 @@ impl ParseStream { node_tks.extend(node.tokens.clone()); body.push(node); } - dbg!(&self.tokens); - panic!("{body:#?}"); self.catch_separator(&mut node_tks); if !self.next_tk_is_some() { self.panic_mode(&mut node_tks); diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index 308876d..af22c09 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -16,10 +16,11 @@ pub enum CharClass { Other } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MotionKind { Forward(usize), - To(usize), + To(usize), // Land just before + On(usize), // Land directly on Backward(usize), Range((usize,usize)), Line(isize), // positive = up line, negative = down line @@ -31,6 +32,41 @@ pub enum MotionKind { ScreenLine(isize) } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SelectionAnchor { + Start, + End +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SelectionMode { + Char(SelectionAnchor), + Line(SelectionAnchor), + Block(SelectionAnchor) +} + +impl SelectionMode { + pub fn anchor(&self) -> &SelectionAnchor { + match self { + SelectionMode::Char(anchor) | + SelectionMode::Line(anchor) | + SelectionMode::Block(anchor) => anchor + } + } + pub fn invert_anchor(&mut self) { + match self { + SelectionMode::Char(anchor) | + SelectionMode::Line(anchor) | + SelectionMode::Block(anchor) => { + *anchor = match anchor { + SelectionAnchor::Start => SelectionAnchor::End, + SelectionAnchor::End => SelectionAnchor::Start + } + } + } + } +} + impl MotionKind { pub fn range>(range: R) -> Self { let start = match range.start_bound() { @@ -157,6 +193,8 @@ pub struct LineBuf { hint: Option, cursor: usize, clamp_cursor: bool, + select_mode: Option, + selected_range: Option>, first_line_offset: usize, saved_col: Option, term_dims: (usize,usize), // Height, width @@ -174,6 +212,20 @@ impl LineBuf { self.buffer = initial.to_string(); self } + pub fn selected_range(&self) -> Option<&Range> { + self.selected_range.as_ref() + } + pub fn is_selecting(&self) -> bool { + self.select_mode.is_some() + } + pub fn stop_selecting(&mut self) { + self.select_mode = None; + self.selected_range = None; + } + pub fn start_selecting(&mut self, mode: SelectionMode) { + self.select_mode = Some(mode); + self.selected_range = Some(self.cursor..(self.cursor + 1).min(self.byte_len().saturating_sub(1))) + } pub fn has_hint(&self) -> bool { self.hint.is_some() } @@ -271,6 +323,13 @@ impl LineBuf { pub fn set_cursor_clamp(&mut self, yn: bool) { self.clamp_cursor = yn } + pub fn g_idx_to_byte_pos(&self, pos: usize) -> Option { + if pos >= self.byte_len() { + None + } else { + self.buffer.grapheme_indices(true).map(|(i,_)| i).nth(pos) + } + } pub fn grapheme_at_cursor(&self) -> Option<&str> { if self.cursor == self.byte_len() { None @@ -518,6 +577,10 @@ impl LineBuf { let end = self.end_of_line(); self.move_to(end) } + /// Consume the LineBuf and return the buffer + pub fn pack_line(self) -> String { + self.buffer + } pub fn accept_hint(&mut self) { if let Some(hint) = self.hint.take() { flog!(DEBUG, "accepting hint"); @@ -1014,6 +1077,10 @@ impl LineBuf { let next_char = self.grapheme_at(self.next_pos(1)?)?; let next_char_class = CharClass::from(next_char); if cur_char_class != next_char_class && next_char_class != CharClass::Whitespace { + let Some(end_pos) = self.find_from(pos, |c| is_other_class_or_ws(c, next_char)) else { + return Some(self.byte_len()) + }; + pos = end_pos.saturating_sub(1); return Some(pos) } @@ -1185,7 +1252,10 @@ impl LineBuf { let Some(pos) = self.find_word_pos(word, to, Direction::Forward) else { return MotionKind::Null }; - MotionKind::To(pos) + match to { + To::Start => MotionKind::To(pos), + To::End => MotionKind::On(pos), + } } Motion::CharSearch(direction, dest, ch) => { match direction { @@ -1230,11 +1300,15 @@ impl LineBuf { MotionKind::To(pos) } } - Motion::Range(_, _) => todo!(), + Motion::Range(start, end) => { + let start = start.clamp(0, self.byte_len().saturating_sub(1)); + let end = end.clamp(0, self.byte_len().saturating_sub(1)); + MotionKind::range(mk_range(start, end)) + } Motion::Builder(_) => todo!(), Motion::RepeatMotion => todo!(), Motion::RepeatMotionRev => todo!(), - Motion::Null => todo!(), + Motion::Null => MotionKind::Null } } pub fn calculate_display_offset(&self, n_lines: isize) -> Option { @@ -1308,6 +1382,10 @@ impl LineBuf { assert!(range.end <= self.byte_len()); Some(range) } + MotionKind::On(n) => { + let range = mk_range_inclusive(self.cursor, *n); + Some(range) + } MotionKind::Backward(n) => { let pos = self.prev_pos(*n)?; let range = pos..self.cursor; @@ -1396,14 +1474,13 @@ impl LineBuf { match verb { Verb::Change | Verb::Delete => { - let Some(range) = self.get_range_from_motion(&verb, &motion) else { + let Some(mut range) = self.get_range_from_motion(&verb, &motion) else { return Ok(()) }; let restore_col = matches!(motion, MotionKind::Line(_)) && matches!(verb, Verb::Delete); if restore_col { self.saved_col = Some(self.cursor_column()) } - let deleted = self.buffer.drain(range.clone()); register.write_to_register(deleted.collect()); @@ -1429,6 +1506,18 @@ impl LineBuf { } } } + Verb::SwapVisualAnchor => { + if let Some(range) = self.selected_range() { + if let Some(mut mode) = self.select_mode { + mode.invert_anchor(); + self.cursor = match mode.anchor() { + SelectionAnchor::Start => range.start, + SelectionAnchor::End => range.end, + }; + self.select_mode = Some(mode); + } + } + } Verb::Yank => { let Some(range) = self.get_range_from_motion(&verb, &motion) else { return Ok(()) @@ -1447,6 +1536,38 @@ impl LineBuf { self.cursor = cursor_pos } Verb::Substitute => todo!(), + Verb::ToLower => { + let Some(range) = self.get_range_from_motion(&verb, &motion) else { + return Ok(()) + }; + let mut new_range = String::new(); + let slice = &self.buffer[range.clone()]; + for ch in slice.chars() { + if ch.is_ascii_uppercase() { + new_range.push(ch.to_ascii_lowercase()) + } else { + new_range.push(ch) + } + } + self.buffer.replace_range(range.clone(), &new_range); + self.cursor = range.end; + } + Verb::ToUpper => { + let Some(range) = self.get_range_from_motion(&verb, &motion) else { + return Ok(()) + }; + let mut new_range = String::new(); + let slice = &self.buffer[range.clone()]; + for ch in slice.chars() { + if ch.is_ascii_lowercase() { + new_range.push(ch.to_ascii_uppercase()) + } else { + new_range.push(ch) + } + } + self.buffer.replace_range(range.clone(), &new_range); + self.cursor = range.end; + } Verb::ToggleCase => { let Some(range) = self.get_range_from_motion(&verb, &motion) else { return Ok(()) @@ -1539,7 +1660,6 @@ impl LineBuf { let next_line = &self.buffer[nstart..nend].trim_start().to_string(); // strip leading whitespace flog!(DEBUG,next_line); let replace_newline_with_space = !line.ends_with([' ', '\t']); - self.cursor = end; if replace_newline_with_space { self.buffer.replace_range(end..end+1, " "); @@ -1587,6 +1707,8 @@ impl LineBuf { Verb::ReplaceMode | Verb::InsertMode | Verb::NormalMode | + Verb::VisualModeLine | + Verb::VisualModeBlock | Verb::VisualMode => { /* Already handled */ self.apply_motion(/*forced*/ true,motion); @@ -1595,6 +1717,7 @@ impl LineBuf { Ok(()) } pub fn apply_motion(&mut self, forced: bool, motion: MotionKind) { + match motion { MotionKind::Forward(n) => { for _ in 0..n { @@ -1618,6 +1741,7 @@ impl LineBuf { } } } + MotionKind::On(n) | MotionKind::To(n) => { if n > self.byte_len() { self.cursor = self.byte_len(); @@ -1666,6 +1790,40 @@ impl LineBuf { self.cursor = pos; } } + if let Some(mut mode) = self.select_mode { + let Some(range) = self.selected_range.clone() else { + return + }; + let (mut start,mut end) = (range.start,range.end); + match mode { + SelectionMode::Char(anchor) => { + match anchor { + SelectionAnchor::Start => { + start = self.cursor; + } + SelectionAnchor::End => { + end = self.cursor; + } + } + } + SelectionMode::Line(anchor) => todo!(), + SelectionMode::Block(anchor) => todo!(), + } + if start >= end { + flog!(DEBUG, "inverting anchor"); + mode.invert_anchor(); + flog!(DEBUG,start,end); + std::mem::swap(&mut start, &mut end); + match mode.anchor() { + SelectionAnchor::Start => end += 1, + SelectionAnchor::End => start -= 1, + } + self.select_mode = Some(mode); + flog!(DEBUG,start,end); + flog!(DEBUG,mode); + } + self.selected_range = Some(start..end); + } } pub fn edit_is_merging(&self) -> bool { self.undo_stack.last().is_some_and(|edit| edit.merging) @@ -1719,7 +1877,12 @@ impl LineBuf { let motion_eval = motion .clone() .map(|m| self.eval_motion(m.1)) - .unwrap_or(MotionKind::Null); + .unwrap_or({ + self.selected_range + .clone() + .map(MotionKind::range) + .unwrap_or(MotionKind::Null) + }); flog!(DEBUG,self.hint); if let Some(verb) = verb.clone() { @@ -1756,6 +1919,9 @@ impl LineBuf { } } + flog!(DEBUG, self.select_mode); + flog!(DEBUG, self.selected_range); + if self.clamp_cursor { self.clamp_cursor(); } @@ -1767,6 +1933,20 @@ impl LineBuf { impl Display for LineBuf { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut full_buf = self.buffer.clone(); + if let Some(range) = self.selected_range.clone() { + let mode = self.select_mode.unwrap(); + match mode.anchor() { + SelectionAnchor::Start => { + let inclusive = range.start..=range.end; + let selected = full_buf[inclusive.clone()].styled(Style::BgWhite | Style::Black); + full_buf.replace_range(inclusive, &selected); + } + SelectionAnchor::End => { + let selected = full_buf[range.clone()].styled(Style::BgWhite | Style::Black); + full_buf.replace_range(range, &selected); + } + } + } if let Some(hint) = self.hint.as_ref() { full_buf.push_str(&hint.styled(Style::BrightBlack)); } diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 57133dc..8ef6731 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -2,8 +2,8 @@ use std::time::Duration; use history::{History, SearchConstraint, SearchKind}; use keys::{KeyCode, KeyEvent, ModKeys}; -use linebuf::{strip_ansi_codes_and_escapes, LineBuf}; -use mode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace}; +use linebuf::{strip_ansi_codes_and_escapes, LineBuf, SelectionAnchor, SelectionMode}; +use mode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; use term::Terminal; use unicode_width::UnicodeWidthStr; use vicmd::{Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd}; @@ -307,12 +307,13 @@ impl FernVi { } strip_ansi_codes_and_escapes(self.prompt.lines().last().unwrap_or_default()).width() + 1 // 1 indexed } - pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { + pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> { + let mut selecting = false; if cmd.is_mode_transition() { let count = cmd.verb_count(); let mut mode: Box = match cmd.verb().unwrap().1 { - Verb::InsertModeLineBreak(_) | Verb::Change | + Verb::InsertModeLineBreak(_) | Verb::InsertMode => { Box::new(ViInsert::new().with_count(count as u16)) } @@ -322,7 +323,11 @@ impl FernVi { Verb::ReplaceMode => { Box::new(ViReplace::new().with_count(count as u16)) } - Verb::VisualMode => todo!(), + Verb::VisualMode => { + selecting = true; + self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End)); + Box::new(ViVisual::new()) + } _ => unreachable!() }; @@ -334,7 +339,13 @@ impl FernVi { if mode.is_repeatable() { self.last_action = mode.as_replay(); } - return self.line.exec_cmd(cmd); + self.line.exec_cmd(cmd)?; + if selecting { + self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End)); + } else { + self.line.stop_selecting(); + } + return Ok(()) } else if cmd.is_cmd_repeat() { let Some(replay) = self.last_action.clone() else { return Ok(()) @@ -405,12 +416,26 @@ impl FernVi { } if cmd.is_repeatable() { + if self.mode.report_mode() == ModeReport::Visual { + // The motion is assigned in the line buffer execution, so we also have to assign it here + // in order to be able to repeat it + let range = self.line.selected_range().unwrap(); + cmd.motion = Some(MotionCmd(1,Motion::Range(range.start, range.end))) + } self.last_action = Some(CmdReplay::Single(cmd.clone())); - } + } + if cmd.is_char_search() { self.last_movement = cmd.motion.clone() } - self.line.exec_cmd(cmd.clone()) + self.line.exec_cmd(cmd.clone())?; + + if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) { + self.line.stop_selecting(); + let mut mode: Box = Box::new(ViNormal::new()); + std::mem::swap(&mut mode, &mut self.mode); + } + Ok(()) } } diff --git a/src/prompt/readline/mode.rs b/src/prompt/readline/mode.rs index 988cc83..4ee5f9f 100644 --- a/src/prompt/readline/mode.rs +++ b/src/prompt/readline/mode.rs @@ -7,6 +7,7 @@ use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, MotionCmd, RegisterName, TextObj, To, Verb, VerbBuilder, VerbCmd, ViCmd, Word}; use crate::prelude::*; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ModeReport { Insert, Normal, @@ -432,6 +433,26 @@ impl ViNormal { } ) } + 'v' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::VisualMode)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'V' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::VisualModeLine)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } 'o' => { return Some( ViCmd { @@ -814,6 +835,562 @@ impl ViMode for ViNormal { } } +#[derive(Default,Debug)] +pub struct ViVisual { + pending_seq: String, +} + +impl ViVisual { + pub fn new() -> Self { + Self::default() + } + pub fn clear_cmd(&mut self) { + self.pending_seq = String::new(); + } + pub fn take_cmd(&mut self) -> String { + std::mem::take(&mut self.pending_seq) + } + fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState { + if verb.is_none() { + match motion { + Some(Motion::TextObj(_,_)) => return CmdState::Invalid, + Some(_) => return CmdState::Complete, + None => return CmdState::Pending + } + } + if verb.is_some() && motion.is_none() { + match verb.unwrap() { + Verb::Put(_) | + Verb::DeleteChar(_) => CmdState::Complete, + _ => CmdState::Pending + } + } else { + CmdState::Complete + } + } + pub fn parse_count(&self, chars: &mut Peekable>) -> Option { + let mut count = String::new(); + let Some(_digit @ '1'..='9') = chars.peek() else { + return None + }; + count.push(chars.next().unwrap()); + while let Some(_digit @ '0'..='9') = chars.peek() { + count.push(chars.next().unwrap()); + } + if !count.is_empty() { + count.parse::().ok() + } else { + None + } + } + /// End the parse and clear the pending sequence + #[track_caller] + pub fn quit_parse(&mut self) -> Option { + flog!(DEBUG, std::panic::Location::caller()); + flog!(WARN, "exiting parse early with sequence: {}",self.pending_seq); + self.clear_cmd(); + None + } + pub fn try_parse(&mut self, ch: char) -> Option { + self.pending_seq.push(ch); + let mut chars = self.pending_seq.chars().peekable(); + + let register = 'reg_parse: { + let mut chars_clone = chars.clone(); + let count = self.parse_count(&mut chars_clone); + + let Some('"') = chars_clone.next() else { + break 'reg_parse RegisterName::default() + }; + + let Some(reg_name) = chars_clone.next() else { + return None // Pending register name + }; + match reg_name { + 'a'..='z' | + 'A'..='Z' => { /* proceed */ } + _ => return self.quit_parse() + } + + chars = chars_clone; + RegisterName::new(Some(reg_name), count) + }; + + let verb = 'verb_parse: { + let mut chars_clone = chars.clone(); + let count = self.parse_count(&mut chars_clone).unwrap_or(1); + + let Some(ch) = chars_clone.next() else { + break 'verb_parse None + }; + match ch { + '.' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::RepeatLast)), + motion: None, + raw_seq: self.take_cmd(), + } + ) + } + 'x' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Delete)); + } + 'X' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Delete)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd(), + } + ) + } + 'Y' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Yank)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd() + } + ) + } + 'D' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Delete)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd() + } + ) + } + 'R' | + 'C' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Change)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd(), + } + ) + } + '>' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Indent)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd(), + } + ) + } + '<' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Dedent)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd(), + } + ) + } + '=' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Equalize)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd(), + } + ) + } + 'p' | + 'P' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before))); + } + 'r' => { + let ch = chars_clone.next()?; + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + '~' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::ToggleCase)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'u' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::ToLower)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'U' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::ToUpper)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'O' | + 'o' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::SwapVisualAnchor)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'A' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::InsertMode)), + motion: Some(MotionCmd(1, Motion::ForwardChar)), + raw_seq: self.take_cmd() + } + ) + } + 'I' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::InsertMode)), + motion: Some(MotionCmd(1, Motion::BeginningOfLine)), + raw_seq: self.take_cmd() + } + ) + } + 'J' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::JoinLines)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'y' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Yank)) + } + 'd' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Delete)) + } + 'c' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Change)) + } + _ => break 'verb_parse None + } + }; + + if let Some(verb) = verb { + return Some(ViCmd { + register, + verb: Some(verb), + motion: None, + raw_seq: self.take_cmd() + }) + } + + let motion = 'motion_parse: { + let mut chars_clone = chars.clone(); + let count = self.parse_count(&mut chars_clone).unwrap_or(1); + + let Some(ch) = chars_clone.next() else { + break 'motion_parse None + }; + match (ch, &verb) { + ('d', Some(VerbCmd(_,Verb::Delete))) | + ('c', Some(VerbCmd(_,Verb::Change))) | + ('y', Some(VerbCmd(_,Verb::Yank))) | + ('=', Some(VerbCmd(_,Verb::Equalize))) | + ('>', Some(VerbCmd(_,Verb::Indent))) | + ('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)), + _ => {} + } + match ch { + 'g' => { + if let Some(ch) = chars_clone.peek() { + match ch { + 'g' => { + chars_clone.next(); + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer)) + } + 'e' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Normal))); + } + 'E' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Big))); + } + 'k' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp)); + } + 'j' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown)); + } + _ => return self.quit_parse() + } + } else { + break 'motion_parse None + } + } + 'f' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, (*ch).into()))) + } + 'F' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, (*ch).into()))) + } + 't' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, (*ch).into()))) + } + 'T' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, (*ch).into()))) + } + ';' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion)); + } + ',' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev)); + } + '|' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count))); + } + '0' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine)); + } + '$' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine)); + } + 'k' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::LineUp)); + } + 'j' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::LineDown)); + } + 'h' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar)); + } + 'l' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar)); + } + 'w' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Normal))); + } + 'W' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Big))); + } + 'e' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Normal))); + } + 'E' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Big))); + } + 'b' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Normal))); + } + 'B' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Big))); + } + ch if ch == 'i' || ch == 'a' => { + let bound = match ch { + 'i' => Bound::Inside, + 'a' => Bound::Around, + _ => unreachable!() + }; + if chars_clone.peek().is_none() { + break 'motion_parse None + } + let obj = match chars_clone.next().unwrap() { + 'w' => TextObj::Word(Word::Normal), + 'W' => TextObj::Word(Word::Big), + '"' => TextObj::DoubleQuote, + '\'' => TextObj::SingleQuote, + '(' | ')' | 'b' => TextObj::Paren, + '{' | '}' | 'B' => TextObj::Brace, + '[' | ']' => TextObj::Bracket, + '<' | '>' => TextObj::Angle, + _ => return self.quit_parse() + }; + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj, bound))) + } + _ => return self.quit_parse(), + } + }; + + if chars.peek().is_some() { + flog!(WARN, "Unused characters in Vi command parse!"); + flog!(WARN, "{:?}",chars) + } + + let verb_ref = verb.as_ref().map(|v| &v.1); + let motion_ref = motion.as_ref().map(|m| &m.1); + + match self.validate_combination(verb_ref, motion_ref) { + CmdState::Complete => { + let cmd = Some( + ViCmd { + register, + verb, + motion, + raw_seq: std::mem::take(&mut self.pending_seq) + } + ); + cmd + } + CmdState::Pending => { + None + } + CmdState::Invalid => { + self.pending_seq.clear(); + None + } + } + } +} + +impl ViMode for ViVisual { + fn handle_key(&mut self, key: E) -> Option { + match key { + E(K::Char(ch), M::NONE) => self.try_parse(ch), + E(K::Backspace, M::NONE) => { + Some(ViCmd { + register: Default::default(), + verb: None, + motion: Some(MotionCmd(1, Motion::BackwardChar)), + raw_seq: "".into(), + }) + } + E(K::Char('R'), M::CTRL) => { + let mut chars = self.pending_seq.chars().peekable(); + let count = self.parse_count(&mut chars).unwrap_or(1); + Some( + ViCmd { + register: RegisterName::default(), + verb: Some(VerbCmd(count,Verb::Redo)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + E(K::Esc, M::NONE) => { + Some( + ViCmd { + register: Default::default(), + verb: Some(VerbCmd(1, Verb::NormalMode)), + motion: Some(MotionCmd(1, Motion::Null)), + raw_seq: self.take_cmd() + }) + } + _ => { + if let Some(cmd) = common_cmds(key) { + self.clear_cmd(); + Some(cmd) + } else { + None + } + } + } + } + + fn is_repeatable(&self) -> bool { + true + } + + fn as_replay(&self) -> Option { + None + } + + fn cursor_style(&self) -> String { + "\x1b[2 q".to_string() + } + + fn pending_seq(&self) -> Option { + Some(self.pending_seq.clone()) + } + + fn move_cursor_on_undo(&self) -> bool { + true + } + + fn clamp_cursor(&self) -> bool { + true + } + + fn hist_scroll_start_pos(&self) -> Option { + None + } + + fn report_mode(&self) -> ModeReport { + ModeReport::Visual + } +} + pub fn common_cmds(key: E) -> Option { let mut pending_cmd = ViCmd::new(); match key { diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs index 0a251ff..a7c8b9f 100644 --- a/src/prompt/readline/vicmd.rs +++ b/src/prompt/readline/vicmd.rs @@ -145,6 +145,8 @@ pub enum Verb { ReplaceChar(char), Substitute, ToggleCase, + ToLower, + ToUpper, Complete, CompleteBackward, Undo, @@ -156,6 +158,9 @@ pub enum Verb { InsertModeLineBreak(Anchor), NormalMode, VisualMode, + VisualModeLine, + VisualModeBlock, + SwapVisualAnchor, JoinLines, InsertChar(char), Insert(String), @@ -189,6 +194,8 @@ impl Verb { Self::Change | Self::ReplaceChar(_) | Self::Substitute | + Self::ToLower | + Self::ToUpper | Self::ToggleCase | Self::Put(_) | Self::ReplaceMode | @@ -210,6 +217,8 @@ impl Verb { Self::ReplaceChar(_) | Self::Substitute | Self::ToggleCase | + Self::ToLower | + Self::ToUpper | Self::RepeatLast | Self::Put(_) | Self::ReplaceMode |