use galaxy_core::safe_info; use galaxyui::{keymap::Keystroke, Entity, ModelContext, ModelHandle, ViewContext}; use crate::register::{valid_register_name, BLACK_HOLE_REGISTER}; /// ASCII code for backspace. /// In Normal and Visual modes, Vim treats backspace as a leftward character motion. const BACKSPACE_CHAR: char = '\u{8}'; /// Represents the Vim modes we currently support. Insert mode is the default instead of normal /// mode to match the behavior of Bash and Zsh's vi mode. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum VimMode { Normal, #[default] Insert, Visual(MotionType), Replace, } impl VimMode { /// Convenience method for constructing a [`ModeTransition`] at a specific insert position. /// Note that a non-default [`InsertPosition`] is only applicable for [`VimMode::Insert`]. fn transition_with_motion(&self, position: InsertPosition) -> ModeTransition { ModeTransition { mode: *self, position, } } } /// A finite-state automaton for interpreting the meaning of Vim key sequences. It takes /// individual keystrokes, performs necessary state transitions for each one, and emits a VimEvent /// every time the corresponding editor View needs to change in turn. #[derive(Default, Debug)] pub struct VimFSA { pub mode: VimMode, /// The characters entered which have yet to form a completed command. /// See ":help showcmd" in Vim. showcmd: String, pending_action: Option, pending_action_count: Option, pending_operand_count: Option, /// When you do something like "vaw", set this field after "va" was typed. pending_visual_object: Option, /// This remembers the last `f`, `F`, `t`, `T` command so that it can be repeated with `;` or /// `,`. last_find_motion: Option, /// Vim has char-named registers which act as separate clipboards. These can be specified on a /// per-command basis using the `"` command, see ':help "' in Vim. register: char, /// Holds the last [`VimEvent`] where [`VimEventType::for_dot_repeat`] returns `Some`. dot_repeat_event: Option, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Direction { Backward, Forward, } impl Direction { pub fn opposite(&self) -> Self { match self { Self::Backward => Self::Forward, Self::Forward => Self::Backward, } } } /// For WordMotions, indicates whether we're moving between the word beginnings or endings. #[derive(Copy, Clone, Debug, PartialEq)] pub enum WordBound { Start, End, } /// This enum distinguishes between `w`/`W`, `b`/`B`, etc. /// See https://vimdoc.sourceforge.net/htmldoc/motion.html#word /// or enter ":help word" in Vim. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WordType { /// More traditional definition of words, as used by `w`, `b`, etc. which break on punctuation. Default, /// Words which include any punctuation, as used by `W`, `B`, etc. The Vim docs refer to these /// as "WORD" in all caps. Enter ":help WORD" in Vim. /// /// Their source code uses the term "bigword" instead. See here: /// https://github.com/neovim/neovim/blob/0fd8eb8aa/src/nvim/textobject.c#L486 BigWord, } /// In general, word motions have upper/lowercase pairs, e.g. `w` and `W` which perform analogous /// motions on different definitions of "word". The lowercase versions match what most other /// programs consider a "word", and so this is named "Default". The uppercase ones redefine /// word-breaking symbols as being word-characters, and is referred to in the Vim docs as a "WORD", /// but the Vim source code calls them "bigwords". impl From for WordType { fn from(c: char) -> Self { if c.is_ascii_uppercase() { Self::BigWord } else { Self::Default } } } /// Word-based motions of the cursor, e.g. for the "w" and "b" commands. #[derive(Clone, Debug)] pub struct WordMotion { pub direction: Direction, pub bound: WordBound, pub word_type: WordType, } impl WordMotion { pub fn new(direction: Direction, bound: WordBound, word_type: WordType) -> Self { Self { direction, bound, word_type, } } } /// Line-based motions of the cursor, e.g. for the "$" and "0" commands. /// Note that not all line-based operations have a direction. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum LineMotion { Start, FirstNonWhitespace, End, } /// Character-based motions of the cursor /// such as "h", "j", "k", "l" and arrow keys. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum CharacterMotion { Up, Down, Left, Right, /// WrappingLeft wraps around line boundaries, /// whereas Left stops at line boundaries. WrappingLeft, /// WrappingRight wraps around line boundaries, /// whereas Right stops at line boundaries. WrappingRight, } /// Motions "+", "-", "_" #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum FirstNonWhitespaceMotion { Up, Down, DownMinusOne, } /// Motions for "f", "F", "t", and "T". #[derive(Clone, Debug)] pub struct FindCharMotion { pub direction: Direction, pub destination: FindCharDestination, pub is_repetition: bool, pub c: char, } impl FindCharMotion { fn with_opposite_direction(mut self) -> Self { self.direction = self.direction.opposite(); self } /// Sets `initial_search` to false. fn repetition(mut self) -> Self { self.is_repetition = true; self } } /// A pending operand is an as-yet incomplete operand, which is whatever comes after an operator. /// An example would be when `di` is entered and we're waiting for the last character, e.g. `diw`. /// This is similar to the [`PendingAction`] enum, but these must be different enums because the /// possible characters for pending operands is a subset of the pending actions. #[derive(Clone, Debug)] enum PendingOperand { /// Can only be ge, gg in this context G, TextObject(TextObjectInclusion), FindChar { direction: Direction, destination: FindCharDestination, }, SquareBracket(Direction), } impl From for PendingOperand { fn from(c: char) -> Self { match c { 'g' => Self::G, 'i' => Self::TextObject(TextObjectInclusion::Inner), 'a' => Self::TextObject(TextObjectInclusion::Around), 'f' => Self::FindChar { direction: Direction::Forward, destination: FindCharDestination::AtChar, }, 't' => Self::FindChar { direction: Direction::Forward, destination: FindCharDestination::BeforeChar, }, 'F' => Self::FindChar { direction: Direction::Backward, destination: FindCharDestination::AtChar, }, 'T' => Self::FindChar { direction: Direction::Backward, destination: FindCharDestination::BeforeChar, }, '[' => Self::SquareBracket(Direction::Backward), ']' => Self::SquareBracket(Direction::Forward), _ => panic!("invalid char for PendingOperand: {c}"), } } } /// This distinguishes between `f` vs `t`, as well as `F` vs `T`. #[derive(Clone, Copy, Debug)] pub enum FindCharDestination { /// When the match is found, move the cursor _on_ that matched char. AtChar, /// When the match is found, move the cursor _before_ that matched char. Note that "before" /// when moving left means "one char to the _right_ of the match". BeforeChar, } /// An enum for vim actions. /// For example: delete, change #[derive(Clone, Debug)] enum PendingAction { Operation { operator: VimOperator, pending_operand: Option, }, /// the full "g" command G, FindChar { direction: Direction, destination: FindCharDestination, }, SquareBracket(Direction), SetRegister, } impl From for PendingAction { fn from(c: char) -> Self { match c { 'd' => Self::Operation { operator: VimOperator::Delete, pending_operand: None, }, 'c' => Self::Operation { operator: VimOperator::Change, pending_operand: None, }, 'y' => Self::Operation { operator: VimOperator::Yank, pending_operand: None, }, 'g' => Self::G, 'f' => Self::FindChar { direction: Direction::Forward, destination: FindCharDestination::AtChar, }, 't' => Self::FindChar { direction: Direction::Forward, destination: FindCharDestination::BeforeChar, }, 'F' => Self::FindChar { direction: Direction::Backward, destination: FindCharDestination::AtChar, }, 'T' => Self::FindChar { direction: Direction::Backward, destination: FindCharDestination::BeforeChar, }, '[' => Self::SquareBracket(Direction::Backward), ']' => Self::SquareBracket(Direction::Forward), '"' => Self::SetRegister, _ => panic!("Invalid char for PendingAction: {c}"), } } } /// An enum for all vim motions. #[derive(Clone, Debug)] pub enum VimMotion { Character(CharacterMotion), Word(WordMotion), Line(LineMotion), FirstNonWhitespace(FirstNonWhitespaceMotion), /// See ":help f" in Vim. FindChar(FindCharMotion), JumpToFirstLine, JumpToLastLine, /// Jump to a specific line number. See ":help G" in Vim. JumpToLine(u32), /// See ":help %" in Vim. JumpToMatchingBracket, /// See ":help [" in Vim. JumpToUnmatchedBracket(BracketChar), /// See ":help {" in Vim. Paragraph(Direction), } /// This enum mirrors Vim's "MotionType", see: /// https://github.com/neovim/neovim/blob/0fd8eb8/src/nvim/normal.h#L19-L24 /// See ":help linewise" in Vim. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum MotionType { Charwise, Linewise, } /// From the Vim docs on text objects: /// > The commands that start with "a" select "a"n object including white space, the commands /// > starting with "i" select an "inner" object without white space, or just the white space. Thus /// > the "inner" commands always select less text than the "a" commands." /// > See https://vimdoc.sourceforge.net/htmldoc/motion.html#text-objects /// > or enter ":help text-objects" in Vim. #[derive(Clone, Copy, Debug)] pub enum TextObjectInclusion { /// For text objects like "a word" or "a block", etc. Around, /// For text objects like "inner word" or "inner block", etc. Inner, } /// Each text object is a "product" of an object type, e.g. word, sentence, block, and two states /// which the Vim docs refer to as "a" or "inner". So, there's "a word" and "inner word". #[derive(Clone, Debug)] pub struct VimTextObject { pub inclusion: TextObjectInclusion, pub object_type: TextObjectType, } impl From for TextObjectType { fn from(c: char) -> Self { match c { 'w' | 'W' => TextObjectType::Word(WordType::from(c)), 'p' => TextObjectType::Paragraph, '\'' | '"' | '`' => TextObjectType::Quote(QuoteType::from(c)), 'b' | 'B' | '(' | ')' | '[' | ']' | '{' | '}' => { TextObjectType::Block(BracketType::from(c)) } _ => panic!("Invalid char for TextObjectType: {c}"), } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum QuoteType { Single, Double, Backtick, } impl QuoteType { pub fn is_char(&self, c: char) -> bool { match self { Self::Single => c == '\'', Self::Double => c == '"', Self::Backtick => c == '`', } } } impl From for QuoteType { fn from(c: char) -> Self { match c { '\'' => Self::Single, '"' => Self::Double, '`' => Self::Backtick, _ => panic!("invalid char for QuoteType: {c}"), } } } /// Whether a bracket is opening "{" or closing "}". #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum BracketEnd { Opening, Closing, } /// What kind of bracket, either "(", "[", or "{". #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum BracketType { Parenthesis, CurlyBrace, SquareBracket, } impl From for BracketType { fn from(c: char) -> Self { match c { '(' | ')' | 'b' => Self::Parenthesis, '[' | ']' => Self::SquareBracket, '{' | '}' | 'B' => Self::CurlyBrace, _ => panic!("Invalid char for BracketType: {c}"), } } } /// Struct to fully represent a bracket character. #[derive(Clone, Debug, Eq, PartialEq)] pub struct BracketChar { pub(crate) end: BracketEnd, pub(crate) kind: BracketType, } impl BracketChar { /// Check if the raw character passed in is the same as self. pub fn is_char(&self, c: char) -> bool { match Self::try_from(c) { Ok(other) => *self == other, Err(_) => false, } } /// Check if the raw character passed in complements self to form a pair, e.g. "(" and ")". pub fn complements(&self, other: char) -> bool { match Self::try_from(other) { Ok(other) => self.kind == other.kind && self.end != other.end, Err(_) => false, } } } impl TryFrom for BracketChar { type Error = (); fn try_from(c: char) -> Result { Ok(match c { '(' => Self { end: BracketEnd::Opening, kind: BracketType::Parenthesis, }, ')' => Self { end: BracketEnd::Closing, kind: BracketType::Parenthesis, }, '[' => Self { end: BracketEnd::Opening, kind: BracketType::SquareBracket, }, ']' => Self { end: BracketEnd::Closing, kind: BracketType::SquareBracket, }, '{' => Self { end: BracketEnd::Opening, kind: BracketType::CurlyBrace, }, '}' => Self { end: BracketEnd::Closing, kind: BracketType::CurlyBrace, }, _ => return Err(()), }) } } #[derive(Clone, Debug)] pub enum TextObjectType { /// Enter ":help aw" in Vim. Word(WordType), /// Enter ":help ap" in Vim. Paragraph, /// Enter ":help aquote" in Vim. Quote(QuoteType), /// Enter ":help a{" in Vim. Block(BracketType), } /// This enumerates all the positions relative to the cursor where insert mode can begin. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub enum InsertPosition { #[default] AtCursor, AfterCursor, LineFirstNonWhitespace, LineEnd, LineAbove, LineBelow, } /// When changing modes, and the new mode is insert mode, it isn't enough to specify that the new /// mode is insert mode. It also needs an insert position to distinguish `i` from `a`, `I`, `A`, /// `o`, and `O`. Therefore, this struct wraps the new mode plus the new position. Note that /// `position` is only ever non-default for insert mode. #[derive(Clone, Debug)] pub struct ModeTransition { pub mode: VimMode, pub position: InsertPosition, } impl From for ModeTransition { fn from(mode: VimMode) -> Self { ModeTransition { mode, position: Default::default(), } } } /// Representation of a single, atomic Vim operation. #[derive(Clone, Debug)] pub struct VimEvent { event_type: VimEventType, count: u32, } impl From for VimEvent { fn from(event_type: VimEventType) -> Self { VimEvent { event_type, count: 1, } } } /// An enumeration of all Vim operations to be done on Views. #[derive(Clone, Debug)] pub enum VimEventType { InsertChar(char), Navigate(VimMotion), ReplaceChar(Option), ToggleCase, Search(Direction), CycleSearch(Direction), SearchWordAtCursor(Direction), KeywordPrg, ExCommand, Paste { direction: Direction, register_name: char, }, JoinLine, ChangeMode { new: ModeTransition, old: VimMode, }, Undo, Backspace, DeleteForward, Escape, Operation { operator: VimOperator, operand: VimOperand, register_name: char, /// This field is kept empty for all operators except [`VimOperator::Change`] when it is /// dot-repeated. replacement_text: String, }, InsertText { text: String, position: InsertPosition, }, VisualOperator { operator: VimOperator, motion_type: MotionType, register_name: char, }, VisualPaste { motion_type: MotionType, read_register_name: char, write_register_name: char, }, VisualTextObject(VimTextObject), GotoDefinition, FindReferences, ShowHover, } impl VimEventType { /// This method determines which event types will respect the `.` command. If they are not /// repeatable, return None. Most events just repeat themselves exactly. Some map to other /// event types. Entering insert mode, for example, starts as [`VimEventType::ChangeMode`], but /// when it's repeated does a different event [`VimEventType::InsertText`] which inserts the /// text all at once. fn for_dot_repeat(&self) -> Option { match self { VimEventType::ReplaceChar(_) | VimEventType::ToggleCase | VimEventType::Paste { .. } | VimEventType::InsertText { .. } | VimEventType::JoinLine | VimEventType::DeleteForward => Some(self.clone()), VimEventType::Operation { operator, .. } if *operator == VimOperator::Change || *operator == VimOperator::Delete => { Some(self.clone()) } VimEventType::ChangeMode { new: ModeTransition { mode: VimMode::Insert, position, }, .. } => Some(VimEventType::InsertText { text: String::new(), position: *position, }), VimEventType::ChangeMode { new: ModeTransition { mode: VimMode::Replace, .. }, .. } => Some(VimEventType::ReplaceChar(None)), VimEventType::Operation { .. } | VimEventType::ChangeMode { .. } | VimEventType::Navigate(_) | VimEventType::Search(_) | VimEventType::CycleSearch(_) | VimEventType::SearchWordAtCursor(_) | VimEventType::KeywordPrg | VimEventType::ExCommand | VimEventType::InsertChar(_) | VimEventType::Undo | VimEventType::VisualOperator { .. } | VimEventType::VisualPaste { .. } | VimEventType::VisualTextObject(_) | VimEventType::Backspace | VimEventType::Escape | VimEventType::GotoDefinition | VimEventType::FindReferences | VimEventType::ShowHover => None, } } } /// "Operators" are `d`, `c`, `y`, etc. /// See https://vimdoc.sourceforge.net/htmldoc/motion.html#operator /// or enter ":help operator" in Vim. /// They must have an operand, see [`VimOperand`], to become a complete operation, see /// [`VimEventType::Operation`]. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum VimOperator { Delete, Change, Yank, ToggleCase, Uppercase, Lowercase, ToggleComment, } impl From for VimOperator { fn from(c: char) -> Self { match c { 'd' | 'D' | 'x' | 'X' => Self::Delete, 'c' | 'C' | 's' | 'S' => Self::Change, 'y' | 'Y' => Self::Yank, '~' => Self::ToggleCase, 'u' => Self::Lowercase, 'U' => Self::Uppercase, _ => panic!("invalid char for VimOperator: {c}"), } } } /// Whatever can come after an operator, see [`VimOperator`]. #[derive(Clone, Debug)] pub enum VimOperand { Motion { motion_type: MotionType, motion: VimMotion, }, TextObject(VimTextObject), Line, } impl VimFSA { pub fn new() -> Self { Self { mode: VimMode::default(), showcmd: String::new(), pending_action: None, pending_action_count: None, pending_operand_count: None, pending_visual_object: None, last_find_motion: None, // When doing an operation that reads/writes to a register, the default (unnamed) regsiter // is called ". register: '"', dot_repeat_event: None, } } pub fn clear(&mut self) { self.showcmd = String::new(); self.pending_action = None; self.pending_action_count = None; self.pending_operand_count = None; self.pending_visual_object = None; self.register = '"'; } pub fn interrupt(&mut self) { self.clear(); match self.mode { VimMode::Replace | VimMode::Visual(_) => { self.mode = VimMode::Normal; } VimMode::Insert | VimMode::Normal => {} } } pub fn state(&self) -> VimState<'_> { VimState { mode: self.mode, showcmd: &self.showcmd, } } /// For processing Vim keypresses that are represented by a single char. fn typed_character(&mut self, c: char) -> Option { self.showcmd.push(c); let event_type = match self.mode { VimMode::Insert => self.handle_insert_char(c), VimMode::Normal => match self.pending_action.clone() { Some(pending_action) => self.handle_normal_pending_action(c, pending_action)?, None => self.handle_normal_nothing_pending(c)?, }, VimMode::Visual(motion_type) => self.handle_visual_command(c, motion_type)?, VimMode::Replace => { self.mode = VimMode::Normal; VimEventType::ReplaceChar(Some(c)) } }; let count = self.compute_event_count(c, &event_type); self.clear(); self.dot_repeat_event = event_type .for_dot_repeat() .map(|event_type| VimEvent { event_type, count }) .or(self.dot_repeat_event.take()); Some(VimEvent { event_type, count }) } /// Like Self::typed_character, but for keypresses that aren't representable by a single char. fn keypress(&mut self, keystroke: &str) -> Option { let event = match keystroke { "escape" => match self.mode { VimMode::Normal => { self.clear(); VimEventType::Escape.into() } VimMode::Replace | VimMode::Visual(_) => { self.change_mode(VimMode::Normal.into()).into() } VimMode::Insert => self.exit_insert_mode(), }, "backspace" => match self.mode { VimMode::Insert => self.handle_insert_mode_backspace().into(), VimMode::Visual(_) | VimMode::Normal => { return self.typed_character(BACKSPACE_CHAR) } VimMode::Replace => self.change_mode(VimMode::Normal.into()).into(), }, "enter" | "shift-enter" | "numpadenter" => match self.mode { VimMode::Insert => VimEventType::InsertChar('\n').into(), VimMode::Normal | VimMode::Visual(_) => VimEventType::Navigate( VimMotion::FirstNonWhitespace(FirstNonWhitespaceMotion::Down), ) .into(), VimMode::Replace => self.change_mode(VimMode::Normal.into()).into(), }, "tab" | "shift-tab" => match self.mode { VimMode::Insert => VimEventType::InsertChar('\t').into(), _ => return None, }, "delete" => match self.mode { VimMode::Insert => self.handle_insert_mode_delete().into(), VimMode::Normal | VimMode::Visual(_) => self.typed_character('x')?, VimMode::Replace => self.change_mode(VimMode::Normal.into()).into(), }, _ => return None, }; Some(event) } /// We need to consider counts in two positions, e.g. `2d3w`. The `2` and `3` are called /// "action count" and "operand count" respectively. The final count will be ther product of /// the two counts. fn compute_event_count(&self, c: char, event_type: &VimEventType) -> u32 { let this_action_count = self.get_action_count(); match (c, event_type) { // Dot-repeat is a special case where a None count takes the count of the repeated // event, and specifying a count overrides it. Note that this means `1.` and `.` are // not the same! // // Replace mode is another special case where we remember the count // that was entered when switching into replace mode. ('.', _) | (_, VimEventType::ReplaceChar(_)) => { this_action_count.unwrap_or_else(|| { self.dot_repeat_event .as_ref() .map(|event| event.count) .unwrap_or(1) }) } _ => this_action_count.unwrap_or(1) * self.get_operand_count().unwrap_or(1), } } /// Exiting insert mode is simple when it had been entered without a count, you just chenge the /// mode. However, if there was a count, we need to repeat the text that was entered n - 1 /// times. fn exit_insert_mode(&mut self) -> VimEvent { // First, check if insert mode had been entered with a count > 1, and extract the necessary // data. All that data is stored in `[Self::dot_repeat_event]`. let number_repeated_text_to_insert = self .dot_repeat_event .as_ref() .filter(|event| event.count > 1) .and_then(|event| match &event.event_type { VimEventType::InsertText { text, position } => { let position = match position { InsertPosition::LineAbove | InsertPosition::LineBelow => *position, _ => InsertPosition::AtCursor, }; Some((text.to_owned(), position, event.count - 1)) } _ => None, }); match number_repeated_text_to_insert { Some((text, position, count)) => { self.mode = VimMode::Normal; VimEvent { event_type: VimEventType::InsertText { text, position }, count, } } None => self.change_mode(VimMode::Normal.into()).into(), } } /// [`Self::typed_character`] dispatches to this method if [`Self::pending_action`] is None and /// we're in [`VimMode::Normal`]. fn handle_normal_nothing_pending(&mut self, c: char) -> Option { let event = match c { 'h' | 'j' | 'k' | 'l' | ' ' | BACKSPACE_CHAR => { VimEventType::Navigate(VimMotion::Character(char_motion_for_character(c))) } 'w' | 'W' | 'b' | 'B' | 'e' | 'E' => { VimEventType::Navigate(VimMotion::Word(word_motion_for_character(c))) } '^' | '$' => VimEventType::Navigate(VimMotion::Line(line_motion_for_character(c))), '+' | '-' | '_' => VimEventType::Navigate(VimMotion::FirstNonWhitespace( first_nonwhitespace_motion_for_char(c), )), '0' => match self.pending_action_count { None => VimEventType::Navigate(VimMotion::Line(LineMotion::Start)), Some(_) => { // append "0" to the digit string self.pending_action_count .get_or_insert_with(String::new) .push(c); return None; } }, '1'..='9' => { // append digit to the digit string self.pending_action_count .get_or_insert_with(String::new) .push(c); return None; } 'i' => self .change_mode(VimMode::Insert.transition_with_motion(InsertPosition::AtCursor)), 'a' => self.change_mode( VimMode::Insert.transition_with_motion(InsertPosition::AfterCursor), ), 'o' => self .change_mode(VimMode::Insert.transition_with_motion(InsertPosition::LineBelow)), 'O' => self .change_mode(VimMode::Insert.transition_with_motion(InsertPosition::LineAbove)), 'I' => self.change_mode( VimMode::Insert.transition_with_motion(InsertPosition::LineFirstNonWhitespace), ), 'A' => self .change_mode(VimMode::Insert.transition_with_motion(InsertPosition::LineEnd)), 'x' => self.create_operation( VimOperator::Delete, VimOperand::Motion { motion: VimMotion::Character(CharacterMotion::Right), motion_type: MotionType::Charwise, }, ), 'X' => self.create_operation( VimOperator::Delete, VimOperand::Motion { motion: VimMotion::Character(CharacterMotion::Left), motion_type: MotionType::Charwise, }, ), 'r' => self.change_mode(VimMode::Replace.into()), 'g' | 'd' | 'c' | 'y' | 'f' | 'F' | 't' | 'T' | '[' | ']' | '"' => { self.pending_action = Some(PendingAction::from(c)); return None; } 'D' => self.create_operation( VimOperator::Delete, VimOperand::Motion { motion: VimMotion::Line(LineMotion::End), motion_type: MotionType::Charwise, }, ), 'C' => { self.mode = VimMode::Insert; self.create_operation( VimOperator::Change, VimOperand::Motion { motion: VimMotion::Line(LineMotion::End), motion_type: MotionType::Charwise, }, ) } // Note that "Y" is not a built-in command in Vim, but it is an NeoVim. NeoVim added it // because it is an obvious analogy to "C" and "D". It only takes 4 lines of code for // us to implement. 'Y' => self.create_operation(VimOperator::Yank, VimOperand::Line), 's' => { self.mode = VimMode::Insert; self.create_operation( VimOperator::Change, VimOperand::Motion { motion: VimMotion::Character(CharacterMotion::Right), motion_type: MotionType::Charwise, }, ) } 'S' => { self.mode = VimMode::Insert; self.create_operation(VimOperator::Change, VimOperand::Line) } 'p' => VimEventType::Paste { direction: Direction::Forward, register_name: self.register, }, 'P' => VimEventType::Paste { direction: Direction::Backward, register_name: self.register, }, 'J' => VimEventType::JoinLine, 'K' => VimEventType::KeywordPrg, 'u' => VimEventType::Undo, '~' => VimEventType::ToggleCase, '/' => VimEventType::Search(Direction::Forward), '?' => VimEventType::Search(Direction::Backward), 'n' => VimEventType::CycleSearch(Direction::Forward), 'N' => VimEventType::CycleSearch(Direction::Backward), '*' => VimEventType::SearchWordAtCursor(Direction::Forward), '#' => VimEventType::SearchWordAtCursor(Direction::Backward), ':' => VimEventType::ExCommand, 'G' => match self.get_action_count() { Some(line_number) => VimEventType::Navigate(VimMotion::JumpToLine(line_number)), None => VimEventType::Navigate(VimMotion::JumpToLastLine), }, '%' => VimEventType::Navigate(VimMotion::JumpToMatchingBracket), '{' => VimEventType::Navigate(VimMotion::Paragraph(Direction::Backward)), '}' => VimEventType::Navigate(VimMotion::Paragraph(Direction::Forward)), ';' => { if let Some(motion) = &self.last_find_motion { VimEventType::Navigate(VimMotion::FindChar(motion.clone().repetition())) } else { self.clear(); return None; } } ',' => { if let Some(motion) = &self.last_find_motion { VimEventType::Navigate(VimMotion::FindChar( motion.clone().repetition().with_opposite_direction(), )) } else { self.clear(); return None; } } '.' => { if let Some(event) = &self.dot_repeat_event { event.event_type.clone() } else { return None; } } 'v' => self.change_mode(VimMode::Visual(MotionType::Charwise).into()), 'V' => self.change_mode(VimMode::Visual(MotionType::Linewise).into()), _ => { self.clear(); return None; } }; Some(event) } /// [`Self::typed_character`] dispatches to this method if [`Self::pending_action`] is Some and /// we're in [`VimMode::Normal`]. fn handle_normal_pending_action( &mut self, c: char, action: PendingAction, ) -> Option { let event = match action { PendingAction::G => match c { 'e' | 'E' => VimEventType::Navigate(VimMotion::Word(WordMotion { direction: Direction::Backward, bound: WordBound::End, word_type: WordType::from(c), })), 'g' => VimEventType::Navigate(VimMotion::JumpToFirstLine), 'd' => VimEventType::GotoDefinition, 'h' => VimEventType::ShowHover, 'r' => VimEventType::FindReferences, // For two-character operations (g~, gu, gU, gc), // replace the existing pending action, the generic G, // with a more specific pending action. '~' | 'u' | 'U' => { self.pending_action = Some(PendingAction::Operation { operator: VimOperator::from(c), pending_operand: None, }); return None; } 'c' => { self.pending_action = Some(PendingAction::Operation { operator: VimOperator::ToggleComment, pending_operand: None, }); return None; } _ => { self.clear(); return None; } }, PendingAction::FindChar { direction, destination, } => { let motion = FindCharMotion { direction, destination, is_repetition: false, c, }; self.last_find_motion = Some(motion.clone()); VimEventType::Navigate(VimMotion::FindChar(motion)) } PendingAction::SquareBracket(direction) => match c { '(' | ')' | '{' | '}' | '[' | ']' => { VimEventType::Navigate(VimMotion::JumpToUnmatchedBracket(BracketChar { end: match direction { Direction::Backward => BracketEnd::Closing, Direction::Forward => BracketEnd::Opening, }, kind: BracketType::from(c), })) } _ => { self.clear(); return None; } }, PendingAction::SetRegister => { // Accept this register name only if it's valid. if valid_register_name(c) { self.register = c; } self.pending_action = None; return None; } PendingAction::Operation { operator, pending_operand, } => { let event = match pending_operand { Some(operand) => self.handle_normal_pending_operand(c, operator, operand)?, None => self.handle_normal_pending_operation(c, operator)?, }; if operator == VimOperator::Change { self.mode = VimMode::Insert; } event } }; Some(event) } fn handle_normal_pending_operation( &mut self, c: char, operator: VimOperator, ) -> Option { let event_type = match c { 'd' if operator == VimOperator::Delete => { self.create_operation(operator, VimOperand::Line) } 'c' if operator == VimOperator::Change => { self.create_operation(operator, VimOperand::Line) } 'y' if operator == VimOperator::Yank => { self.create_operation(operator, VimOperand::Line) } // Support gcc (toggle comment line) 'c' if operator == VimOperator::ToggleComment => { self.create_operation(operator, VimOperand::Line) } 'i' | 'a' | 'g' | 'f' | 'F' | 't' | 'T' | '[' | ']' => { self.pending_action = Some(PendingAction::Operation { operator, pending_operand: Some(PendingOperand::from(c)), }); return None; } 'h' | 'l' | ' ' | BACKSPACE_CHAR => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::Character(char_motion_for_character(c)), motion_type: MotionType::Charwise, }, ), 'j' | 'k' => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::Character(char_motion_for_character(c)), motion_type: MotionType::Linewise, }, ), 'w' | 'W' | 'b' | 'B' | 'e' | 'E' => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::Word(match operator { VimOperator::Change => closed_word_motion_for_character(c), _ => word_motion_for_character(c), }), motion_type: MotionType::Charwise, }, ), '^' | '$' => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::Line(line_motion_for_character(c)), motion_type: MotionType::Charwise, }, ), '+' | '-' | '_' => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::FirstNonWhitespace(first_nonwhitespace_motion_for_char(c)), motion_type: MotionType::Linewise, }, ), 'G' => self.create_operation( operator, VimOperand::Motion { motion: match self.get_operand_count() { Some(line_number) => VimMotion::JumpToLine(line_number), None => VimMotion::JumpToLastLine, }, motion_type: MotionType::Linewise, }, ), '{' => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::Paragraph(Direction::Backward), motion_type: MotionType::Linewise, }, ), '}' => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::Paragraph(Direction::Forward), motion_type: MotionType::Linewise, }, ), '0' => match self.pending_operand_count { None => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::Line(LineMotion::Start), motion_type: MotionType::Charwise, }, ), Some(_) => { // append "0" to the digit string self.pending_operand_count .get_or_insert_with(String::new) .push(c); return None; } }, '1'..='9' => { // append digit to the digit string self.pending_operand_count .get_or_insert_with(String::new) .push(c); return None; } '%' => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::JumpToMatchingBracket, motion_type: MotionType::Charwise, }, ), ';' => { if let Some(motion) = &self.last_find_motion { self.create_operation( operator, VimOperand::Motion { motion: VimMotion::FindChar(motion.clone().repetition()), motion_type: MotionType::Charwise, }, ) } else { self.clear(); return None; } } ',' => { if let Some(motion) = &self.last_find_motion { self.create_operation( operator, VimOperand::Motion { motion: VimMotion::FindChar( motion.clone().repetition().with_opposite_direction(), ), motion_type: MotionType::Charwise, }, ) } else { self.clear(); return None; } } _ => { self.clear(); return None; } }; Some(event_type) } fn handle_normal_pending_operand( &mut self, c: char, operator: VimOperator, operand: PendingOperand, ) -> Option { let event_type = match operand { PendingOperand::G => match c { 'e' | 'E' => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::Word(WordMotion { direction: Direction::Backward, bound: WordBound::End, word_type: WordType::from(c), }), motion_type: MotionType::Charwise, }, ), 'g' => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::JumpToFirstLine, motion_type: MotionType::Linewise, }, ), _ => { self.clear(); return None; } }, PendingOperand::FindChar { direction, destination, } => { let motion = FindCharMotion { direction, destination, is_repetition: false, c, }; self.last_find_motion = Some(motion.clone()); self.create_operation( operator, VimOperand::Motion { motion: VimMotion::FindChar(FindCharMotion { direction, destination, is_repetition: false, c, }), motion_type: MotionType::Charwise, }, ) } PendingOperand::TextObject(inclusion) => match c { 'w' | 'W' | 'p' | '\'' | '"' | '`' | 'b' | 'B' | '(' | ')' | '[' | ']' | '{' | '}' => self.create_operation( operator, VimOperand::TextObject(VimTextObject { inclusion, object_type: TextObjectType::from(c), }), ), _ => { self.clear(); return None; } }, PendingOperand::SquareBracket(direction) => match c { '(' | ')' | '{' | '}' | '[' | ']' => self.create_operation( operator, VimOperand::Motion { motion: VimMotion::JumpToUnmatchedBracket(BracketChar { end: match direction { Direction::Backward => BracketEnd::Closing, Direction::Forward => BracketEnd::Opening, }, kind: BracketType::from(c), }), motion_type: MotionType::Charwise, }, ), _ => { self.clear(); return None; } }, }; Some(event_type) } /// [`Self::typed_character`] dispatches to this method if we're in [`VimMode::Visual`]. fn handle_visual_command(&mut self, c: char, motion_type: MotionType) -> Option { let event_type = match self.pending_action.clone() { Some(pending_action) => self.handle_visual_pending_action(c, pending_action)?, None => match self.pending_visual_object { Some(inclusion) => match c { 'w' | 'W' | 'p' | '\'' | '"' | '`' | 'b' | 'B' | '(' | ')' | '[' | ']' | '{' | '}' => { if c == 'p' { self.mode = VimMode::Visual(MotionType::Linewise); } VimEventType::VisualTextObject(VimTextObject { inclusion, object_type: TextObjectType::from(c), }) } _ => { self.clear(); return None; } }, None => self.handle_visual_nothing_pending(c, motion_type)?, }, }; Some(event_type) } fn handle_visual_nothing_pending( &mut self, c: char, motion_type: MotionType, ) -> Option { let event_type = match c { 'h' | 'j' | 'k' | 'l' | ' ' | BACKSPACE_CHAR => { VimEventType::Navigate(VimMotion::Character(char_motion_for_character(c))) } 'w' | 'W' | 'b' | 'B' | 'e' | 'E' => { VimEventType::Navigate(VimMotion::Word(word_motion_for_character(c))) } '^' | '$' => VimEventType::Navigate(VimMotion::Line(line_motion_for_character(c))), '+' | '-' | '_' => VimEventType::Navigate(VimMotion::FirstNonWhitespace( first_nonwhitespace_motion_for_char(c), )), '0' => match self.pending_action_count { None => VimEventType::Navigate(VimMotion::Line(LineMotion::Start)), Some(_) => { // append "0" to the digit string self.pending_action_count .get_or_insert_with(String::new) .push(c); return None; } }, '1'..='9' => { // append digit to the digit string self.pending_action_count .get_or_insert_with(String::new) .push(c); return None; } 'G' => match self.get_action_count() { Some(line_number) => VimEventType::Navigate(VimMotion::JumpToLine(line_number)), None => VimEventType::Navigate(VimMotion::JumpToLastLine), }, '%' => VimEventType::Navigate(VimMotion::JumpToMatchingBracket), '{' => VimEventType::Navigate(VimMotion::Paragraph(Direction::Backward)), '}' => VimEventType::Navigate(VimMotion::Paragraph(Direction::Forward)), ';' => { if let Some(motion) = &self.last_find_motion { VimEventType::Navigate(VimMotion::FindChar(motion.clone().repetition())) } else { self.clear(); return None; } } ',' => { if let Some(motion) = &self.last_find_motion { VimEventType::Navigate(VimMotion::FindChar( motion.clone().repetition().with_opposite_direction(), )) } else { self.clear(); return None; } } 'd' | 'D' | 'y' | 'Y' | 'x' | 'X' | '~' | 'u' | 'U' => { let event_type = self.create_visual_operator(c, motion_type); self.mode = VimMode::Normal; event_type } 'c' | 'C' | 's' | 'S' => { let event_type = self.create_visual_operator(c, motion_type); self.mode = VimMode::Insert; event_type } 'p' | 'P' => { self.mode = VimMode::Normal; let write_register_name = if c == 'p' { self.register } else { BLACK_HOLE_REGISTER }; VimEventType::VisualPaste { motion_type, read_register_name: self.register, write_register_name, } } 'g' | 'f' | 'F' | 't' | 'T' | '[' | ']' | '"' => { self.pending_action = Some(PendingAction::from(c)); return None; } 'i' => { self.pending_visual_object = Some(TextObjectInclusion::Inner); return None; } 'a' => { self.pending_visual_object = Some(TextObjectInclusion::Around); return None; } 'v' => match motion_type { MotionType::Charwise => self.change_mode(VimMode::Normal.into()), MotionType::Linewise => { self.change_mode(VimMode::Visual(MotionType::Charwise).into()) } }, 'V' => match motion_type { MotionType::Linewise => self.change_mode(VimMode::Normal.into()), MotionType::Charwise => { self.change_mode(VimMode::Visual(MotionType::Linewise).into()) } }, _ => return None, }; Some(event_type) } fn handle_visual_pending_action( &mut self, c: char, pending_action: PendingAction, ) -> Option { let event_type = match pending_action { PendingAction::G => match c { 'e' | 'E' => VimEventType::Navigate(VimMotion::Word(WordMotion { direction: Direction::Backward, bound: WordBound::End, word_type: WordType::from(c), })), 'g' => VimEventType::Navigate(VimMotion::JumpToFirstLine), 'c' => { let motion_type = match self.mode { VimMode::Visual(mt) => mt, _ => MotionType::Charwise, }; self.mode = VimMode::Normal; VimEventType::VisualOperator { operator: VimOperator::ToggleComment, motion_type, register_name: self.register, } } _ => { self.clear(); return None; } }, PendingAction::FindChar { direction, destination, } => { let motion = FindCharMotion { direction, destination, is_repetition: false, c, }; self.last_find_motion = Some(motion.clone()); VimEventType::Navigate(VimMotion::FindChar(motion)) } PendingAction::SquareBracket(direction) => match c { '(' | ')' | '{' | '}' | '[' | ']' => { VimEventType::Navigate(VimMotion::JumpToUnmatchedBracket(BracketChar { end: match direction { Direction::Backward => BracketEnd::Closing, Direction::Forward => BracketEnd::Opening, }, kind: BracketType::from(c), })) } _ => { self.clear(); return None; } }, PendingAction::SetRegister => { // Accept this register name only if it's valid. if valid_register_name(c) { self.register = c; } self.pending_action = None; return None; } _ => { self.clear(); return None; } }; Some(event_type) } /// Helper function to change modes. /// (1) Updates `self.mode`, and /// (2) returns a VimEvent to be handled externally. fn change_mode(&mut self, mode_trans: ModeTransition) -> VimEventType { let old_mode = self.mode; self.mode = mode_trans.mode; VimEventType::ChangeMode { new: mode_trans, old: old_mode, } } /// Parse an unsigned integer from self.pending_action_count. fn get_action_count(&self) -> Option { self.pending_action_count .as_ref() .and_then(|s| s.parse().ok()) } /// Parse an unsigned integer from self.pending_operand_count. fn get_operand_count(&self) -> Option { self.pending_operand_count .as_ref() .and_then(|s| s.parse().ok()) } /// This is for when the view needs to initiate a switch to insert mode, e.g. when the user /// does an interaction, say selecting text with the mouse, which doesn't make sense in the /// current mode. fn force_insert_mode(&mut self) { self.clear(); self.mode = VimMode::Insert; } fn create_operation(&self, operator: VimOperator, operand: VimOperand) -> VimEventType { VimEventType::Operation { operator, operand, register_name: self.register, replacement_text: String::new(), } } fn create_visual_operator(&self, c: char, motion_type: MotionType) -> VimEventType { // Capitalized operators will ignore if we had been in charwise visual mode, and become // linewise. // 'U' (for Uppercase) is a special case of a capitalized charwise operator. let motion_type = if c.is_ascii_uppercase() && c != 'U' { MotionType::Linewise } else { motion_type }; VimEventType::VisualOperator { operator: VimOperator::from(c), motion_type, register_name: self.register, } } /// When in insert mode, we need to keep track of chars entered for dot-repeat. How this /// happens depends on the [`VimEventType`] variant we have in [`Self::dot_repeat_event`]. fn dot_repeat_text_mut(&mut self) -> Option<&mut String> { match &mut self.dot_repeat_event { Some(VimEvent { event_type: VimEventType::InsertText { ref mut text, .. }, .. }) | Some(VimEvent { event_type: VimEventType::Operation { operator: VimOperator::Change, replacement_text: ref mut text, .. }, .. }) => Some(text), _ => None, } } /// Makes sure we append to the [`Self::dot_repeat_event`] accordingly. fn handle_insert_char(&mut self, c: char) -> VimEventType { if let Some(text) = self.dot_repeat_text_mut() { text.push(c); } VimEventType::InsertChar(c) } /// Makes sure we pop from the [`Self::dot_repeat_event`] accordingly. fn handle_insert_mode_backspace(&mut self) -> VimEventType { if let Some(text) = self.dot_repeat_text_mut() { text.pop(); } VimEventType::Backspace } /// Handles delete forward in insert mode without contaminating registers. fn handle_insert_mode_delete(&mut self) -> VimEventType { // Track delete-forward events for dot-repeat if self.dot_repeat_event.is_none() { self.dot_repeat_event = Some(VimEvent { event_type: VimEventType::DeleteForward, count: 1, }); } VimEventType::DeleteForward } } /// Match typed characters with their associated word motion fn word_motion_for_character(c: char) -> WordMotion { let word_type = WordType::from(c); match c { 'w' => WordMotion::new(Direction::Forward, WordBound::Start, word_type), 'W' => WordMotion::new(Direction::Forward, WordBound::Start, word_type), 'b' => WordMotion::new(Direction::Backward, WordBound::Start, word_type), 'B' => WordMotion::new(Direction::Backward, WordBound::Start, word_type), 'e' => WordMotion::new(Direction::Forward, WordBound::End, word_type), 'E' => WordMotion::new(Direction::Forward, WordBound::End, word_type), _ => panic!("could not match character {c} with a word motion"), } } /// Match typed characters with a closed word motion. /// This means forward motions (w/W and e/E) will both match to e/E. fn closed_word_motion_for_character(c: char) -> WordMotion { let word_type = WordType::from(c); match c { 'w' | 'e' => WordMotion::new(Direction::Forward, WordBound::End, word_type), 'W' | 'E' => WordMotion::new(Direction::Forward, WordBound::End, word_type), 'b' => WordMotion::new(Direction::Backward, WordBound::Start, word_type), 'B' => WordMotion::new(Direction::Backward, WordBound::Start, word_type), _ => panic!("could not match character {c} with a closed word motion"), } } /// Match typed characters with their associated line motion fn line_motion_for_character(c: char) -> LineMotion { match c { '0' => LineMotion::Start, '^' => LineMotion::FirstNonWhitespace, '$' => LineMotion::End, _ => panic!("could not match character {c} with a line motion"), } } /// Match typed characters with their associated character motion fn char_motion_for_character(c: char) -> CharacterMotion { match c { 'h' => CharacterMotion::Left, 'l' => CharacterMotion::Right, 'k' => CharacterMotion::Up, 'j' => CharacterMotion::Down, ' ' => CharacterMotion::WrappingRight, BACKSPACE_CHAR => CharacterMotion::WrappingLeft, _ => panic!("could not match character {c} with a single-character motion"), } } fn first_nonwhitespace_motion_for_char(c: char) -> FirstNonWhitespaceMotion { match c { '+' => FirstNonWhitespaceMotion::Down, '-' => FirstNonWhitespaceMotion::Up, '_' => FirstNonWhitespaceMotion::DownMinusOne, _ => panic!("could not match character {c} with a first nonwhitespace motion"), } } /// A single struct encapsulating the publicly accessible state from the VimFSA. pub struct VimState<'a> { pub mode: VimMode, pub showcmd: &'a str, } /// This struct is a wrapper around the VimFSA that turns it into a galaxyui::Entity. We want to keep /// the VimFSA independent of our UI framework, so anything involving galaxyui should live here /// instead. #[derive(Default)] pub struct VimModel { fsa: VimFSA, } impl VimModel { pub fn new() -> Self { Self { fsa: VimFSA::new() } } pub fn state(&self) -> VimState<'_> { self.fsa.state() } pub fn typed_character(&mut self, c: char, ctx: &mut ModelContext) { if let Some(event) = self.fsa.typed_character(c) { ctx.emit(event); } } pub fn keypress(&mut self, keystroke: &Keystroke, ctx: &mut ModelContext) { if let Some(event) = self.fsa.keypress(keystroke.key.as_str()) { ctx.emit(event); } } pub fn force_insert_mode(&mut self, ctx: &mut ModelContext) { self.fsa.force_insert_mode(); ctx.notify(); } pub fn interrupt(&mut self, ctx: &mut ModelContext) { self.fsa.interrupt(); ctx.notify(); } } impl Entity for VimModel { type Event = VimEvent; } pub trait VimSubscriber { fn handle_vim_event( &mut self, _handle: ModelHandle, event: &VimEvent, ctx: &mut ViewContext, ); } impl VimSubscriber for T where T: VimHandler, { fn handle_vim_event( &mut self, _handle: ModelHandle, event: &VimEvent, ctx: &mut ViewContext, ) { safe_info!( safe: ("Handling vim event, count {:?}", event.count), full: ("Handling vim event type {:?}, count {:?}", event.event_type, event.count) ); match &event.event_type { VimEventType::InsertChar(c) => self.insert_char(*c, ctx), VimEventType::Navigate(motion) => match motion { VimMotion::Character(motion) => self.navigate_char(event.count, motion, ctx), VimMotion::Word(motion) => self.navigate_word(event.count, motion, ctx), VimMotion::Line(motion) => self.navigate_line(event.count, motion, ctx), VimMotion::FirstNonWhitespace(motion) => { self.first_nonwhitespace_motion(event.count, motion, ctx) } VimMotion::FindChar(motion) => self.find_char(event.count, motion, ctx), VimMotion::Paragraph(direction) => { self.navigate_paragraph(event.count, direction, ctx) } VimMotion::JumpToFirstLine => self.jump_to_first_line(ctx), VimMotion::JumpToLastLine => self.jump_to_last_line(ctx), VimMotion::JumpToLine(line_number) => self.jump_to_line(*line_number, ctx), VimMotion::JumpToMatchingBracket => self.jump_to_matching_bracket(ctx), VimMotion::JumpToUnmatchedBracket(bracket) => { self.jump_to_unmatched_bracket(bracket, ctx) } }, VimEventType::Operation { operator, operand, register_name, replacement_text, } => self.operation( operator, event.count, operand, *register_name, replacement_text.as_str(), ctx, ), VimEventType::ReplaceChar(Some(c)) => self.replace_char(*c, event.count, ctx), VimEventType::ReplaceChar(_) => {} VimEventType::Paste { direction, register_name, } => self.paste(event.count, direction, *register_name, ctx), VimEventType::InsertText { text, position } => { self.insert_text(text.as_str(), position, event.count, ctx) } VimEventType::JoinLine => self.join_line(event.count, ctx), VimEventType::Undo => self.undo(ctx), VimEventType::ToggleCase => self.toggle_case(event.count, ctx), VimEventType::Search(direction) => self.search(direction, ctx), VimEventType::CycleSearch(direction) => self.cycle_search(direction, ctx), VimEventType::SearchWordAtCursor(direction) => { self.search_word_at_cursor(direction, ctx) } VimEventType::KeywordPrg => self.keyword_prg(ctx), VimEventType::ExCommand => self.ex_command(ctx), VimEventType::VisualOperator { operator, motion_type, register_name, } => self.visual_operator(operator, *motion_type, *register_name, ctx), VimEventType::VisualPaste { motion_type, read_register_name, write_register_name, } => self.visual_paste(*motion_type, *read_register_name, *write_register_name, ctx), VimEventType::VisualTextObject(text_object) => { self.visual_text_object(text_object, ctx) } // Escape is idempotent VimEventType::Escape => self.escape(ctx), VimEventType::ChangeMode { new, old } => self.change_mode(old, new, ctx), VimEventType::Backspace => self.backspace(ctx), VimEventType::DeleteForward => self.delete_forward(ctx), VimEventType::GotoDefinition => self.goto_definition(ctx), VimEventType::FindReferences => self.find_references(ctx), VimEventType::ShowHover => self.show_hover(ctx), }; } } /// To be implemented by Views that support Vim keybindings. pub trait VimHandler { /// A character to be inserted to the buffer. fn insert_char(&mut self, c: char, ctx: &mut ViewContext); /// A one-character motion of the cursor. fn navigate_char( &mut self, count: u32, character_motion: &CharacterMotion, ctx: &mut ViewContext, ); /// Word-related motion of the cursor. fn navigate_word(&mut self, count: u32, word_motion: &WordMotion, ctx: &mut ViewContext); /// Motions within the current line: 0, ^, $ fn navigate_line(&mut self, count: u32, line_motion: &LineMotion, ctx: &mut ViewContext); fn first_nonwhitespace_motion( &mut self, count: u32, motion: &FirstNonWhitespaceMotion, ctx: &mut ViewContext, ); /// Motions to a particular character on the current line. fn find_char( &mut self, occurrence_count: u32, find_char_motion: &FindCharMotion, ctx: &mut ViewContext, ); /// Navigate by paragraph: { and }. fn navigate_paragraph( &mut self, count: u32, direction: &Direction, ctx: &mut ViewContext, ); /// For all "operator commands", e.g. d, c, y. See ":help operator" in Vim, or click here: /// https://vimdoc.sourceforge.net/htmldoc/motion.html#operator fn operation( &mut self, operator: &VimOperator, operand_count: u32, operand: &VimOperand, register_name: char, replacement_text: &str, ctx: &mut ViewContext, ); /// Replace a character with another. /// If `char_count` is greater than the number of characters remaining in the current line, /// the replace operation is cancelled. fn replace_char(&mut self, c: char, char_count: u32, ctx: &mut ViewContext); /// Switch between upper/lowercase for character on cursor. /// Even if `char_count` is greater than the number of characters remaining in the current line, /// only characters in the current line are toggled. fn toggle_case(&mut self, char_count: u32, ctx: &mut ViewContext); /// Open the search experience for the relevant context, e.g. in a shell, that may be searching /// shell history, while in notebooks, that may be searching the editor buffer. fn search(&mut self, direction: &Direction, ctx: &mut ViewContext); /// Cycle through matches for the existing search query (if any). Mapped to 'n'/'N'. fn cycle_search(&mut self, direction: &Direction, ctx: &mut ViewContext); /// Initiate a search for the word nearest to the cursor. If the cursor is inside a word, /// search that. If it is on whitespace or punctuation, go forward to the nearest word. fn search_word_at_cursor(&mut self, direction: &Direction, ctx: &mut ViewContext); /// Bring up the menu (command-line mode) to accept an ex-command. See ":help :" in Vim. fn ex_command(&mut self, ctx: &mut ViewContext); /// Execute `keywordprg`, i.e. Vim's `K` command. See ":help K" or ":help keywordprg" fn keyword_prg(&mut self, ctx: &mut ViewContext); fn visual_operator( &mut self, operator: &VimOperator, motion_type: MotionType, register_name: char, ctx: &mut ViewContext, ); fn visual_paste( &mut self, motion_type: MotionType, read_register_name: char, write_register_name: char, ctx: &mut ViewContext, ); fn visual_text_object(&mut self, text_object: &VimTextObject, ctx: &mut ViewContext); fn jump_to_first_line(&mut self, ctx: &mut ViewContext); fn jump_to_last_line(&mut self, ctx: &mut ViewContext); fn jump_to_line(&mut self, line_number: u32, ctx: &mut ViewContext); fn jump_to_matching_bracket(&mut self, ctx: &mut ViewContext); fn jump_to_unmatched_bracket(&mut self, bracket: &BracketChar, ctx: &mut ViewContext); fn paste( &mut self, count: u32, direction: &Direction, register_name: char, ctx: &mut ViewContext, ); fn insert_text( &mut self, text: &str, position: &InsertPosition, count: u32, ctx: &mut ViewContext, ); /// Join the current line to the next one. fn join_line(&mut self, count: u32, ctx: &mut ViewContext); fn undo(&mut self, ctx: &mut ViewContext); fn change_mode(&mut self, old: &VimMode, new: &ModeTransition, ctx: &mut ViewContext); fn backspace(&mut self, ctx: &mut ViewContext); fn delete_forward(&mut self, ctx: &mut ViewContext); fn escape(&mut self, ctx: &mut ViewContext); /// Go to the definition of the symbol under cursor (gd). fn goto_definition(&mut self, _ctx: &mut ViewContext) {} /// Find references of the symbol under cursor (gr). fn find_references(&mut self, _ctx: &mut ViewContext) {} /// Show hover information for the symbol under cursor (gh). fn show_hover(&mut self, _ctx: &mut ViewContext) {} }