2007 lines
73 KiB
Rust
2007 lines
73 KiB
Rust
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<PendingAction>,
|
|
pending_action_count: Option<String>,
|
|
pending_operand_count: Option<String>,
|
|
/// When you do something like "vaw", set this field after "va" was typed.
|
|
pending_visual_object: Option<TextObjectInclusion>,
|
|
/// This remembers the last `f`, `F`, `t`, `T` command so that it can be repeated with `;` or
|
|
/// `,`.
|
|
last_find_motion: Option<FindCharMotion>,
|
|
/// 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<VimEvent>,
|
|
}
|
|
|
|
#[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<char> 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<char> 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<PendingOperand>,
|
|
},
|
|
/// the full "g" command
|
|
G,
|
|
FindChar {
|
|
direction: Direction,
|
|
destination: FindCharDestination,
|
|
},
|
|
SquareBracket(Direction),
|
|
SetRegister,
|
|
}
|
|
|
|
impl From<char> 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<char> 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<char> 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<char> 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<char> for BracketChar {
|
|
type Error = ();
|
|
|
|
fn try_from(c: char) -> Result<Self, Self::Error> {
|
|
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<VimMode> 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<VimEventType> 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<char>),
|
|
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<Self> {
|
|
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<char> 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<VimEvent> {
|
|
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<VimEvent> {
|
|
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<VimEventType> {
|
|
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<VimEventType> {
|
|
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<VimEventType> {
|
|
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<VimEventType> {
|
|
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<VimEventType> {
|
|
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<VimEventType> {
|
|
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<VimEventType> {
|
|
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<u32> {
|
|
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<u32> {
|
|
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<Self>) {
|
|
if let Some(event) = self.fsa.typed_character(c) {
|
|
ctx.emit(event);
|
|
}
|
|
}
|
|
|
|
pub fn keypress(&mut self, keystroke: &Keystroke, ctx: &mut ModelContext<Self>) {
|
|
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>) {
|
|
self.fsa.force_insert_mode();
|
|
ctx.notify();
|
|
}
|
|
|
|
pub fn interrupt(&mut self, ctx: &mut ModelContext<Self>) {
|
|
self.fsa.interrupt();
|
|
ctx.notify();
|
|
}
|
|
}
|
|
|
|
impl Entity for VimModel {
|
|
type Event = VimEvent;
|
|
}
|
|
|
|
pub trait VimSubscriber {
|
|
fn handle_vim_event(
|
|
&mut self,
|
|
_handle: ModelHandle<VimModel>,
|
|
event: &VimEvent,
|
|
ctx: &mut ViewContext<Self>,
|
|
);
|
|
}
|
|
|
|
impl<T> VimSubscriber for T
|
|
where
|
|
T: VimHandler,
|
|
{
|
|
fn handle_vim_event(
|
|
&mut self,
|
|
_handle: ModelHandle<VimModel>,
|
|
event: &VimEvent,
|
|
ctx: &mut ViewContext<Self>,
|
|
) {
|
|
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<Self>);
|
|
/// A one-character motion of the cursor.
|
|
fn navigate_char(
|
|
&mut self,
|
|
count: u32,
|
|
character_motion: &CharacterMotion,
|
|
ctx: &mut ViewContext<Self>,
|
|
);
|
|
/// Word-related motion of the cursor.
|
|
fn navigate_word(&mut self, count: u32, word_motion: &WordMotion, ctx: &mut ViewContext<Self>);
|
|
/// Motions within the current line: 0, ^, $
|
|
fn navigate_line(&mut self, count: u32, line_motion: &LineMotion, ctx: &mut ViewContext<Self>);
|
|
fn first_nonwhitespace_motion(
|
|
&mut self,
|
|
count: u32,
|
|
motion: &FirstNonWhitespaceMotion,
|
|
ctx: &mut ViewContext<Self>,
|
|
);
|
|
/// Motions to a particular character on the current line.
|
|
fn find_char(
|
|
&mut self,
|
|
occurrence_count: u32,
|
|
find_char_motion: &FindCharMotion,
|
|
ctx: &mut ViewContext<Self>,
|
|
);
|
|
/// Navigate by paragraph: { and }.
|
|
fn navigate_paragraph(
|
|
&mut self,
|
|
count: u32,
|
|
direction: &Direction,
|
|
ctx: &mut ViewContext<Self>,
|
|
);
|
|
/// 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<Self>,
|
|
);
|
|
/// 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<Self>);
|
|
/// 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<Self>);
|
|
/// 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<Self>);
|
|
/// Cycle through matches for the existing search query (if any). Mapped to 'n'/'N'.
|
|
fn cycle_search(&mut self, direction: &Direction, ctx: &mut ViewContext<Self>);
|
|
/// 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<Self>);
|
|
/// Bring up the menu (command-line mode) to accept an ex-command. See ":help :" in Vim.
|
|
fn ex_command(&mut self, ctx: &mut ViewContext<Self>);
|
|
/// Execute `keywordprg`, i.e. Vim's `K` command. See ":help K" or ":help keywordprg"
|
|
fn keyword_prg(&mut self, ctx: &mut ViewContext<Self>);
|
|
fn visual_operator(
|
|
&mut self,
|
|
operator: &VimOperator,
|
|
motion_type: MotionType,
|
|
register_name: char,
|
|
ctx: &mut ViewContext<Self>,
|
|
);
|
|
fn visual_paste(
|
|
&mut self,
|
|
motion_type: MotionType,
|
|
read_register_name: char,
|
|
write_register_name: char,
|
|
ctx: &mut ViewContext<Self>,
|
|
);
|
|
fn visual_text_object(&mut self, text_object: &VimTextObject, ctx: &mut ViewContext<Self>);
|
|
fn jump_to_first_line(&mut self, ctx: &mut ViewContext<Self>);
|
|
fn jump_to_last_line(&mut self, ctx: &mut ViewContext<Self>);
|
|
fn jump_to_line(&mut self, line_number: u32, ctx: &mut ViewContext<Self>);
|
|
fn jump_to_matching_bracket(&mut self, ctx: &mut ViewContext<Self>);
|
|
fn jump_to_unmatched_bracket(&mut self, bracket: &BracketChar, ctx: &mut ViewContext<Self>);
|
|
fn paste(
|
|
&mut self,
|
|
count: u32,
|
|
direction: &Direction,
|
|
register_name: char,
|
|
ctx: &mut ViewContext<Self>,
|
|
);
|
|
fn insert_text(
|
|
&mut self,
|
|
text: &str,
|
|
position: &InsertPosition,
|
|
count: u32,
|
|
ctx: &mut ViewContext<Self>,
|
|
);
|
|
/// Join the current line to the next one.
|
|
fn join_line(&mut self, count: u32, ctx: &mut ViewContext<Self>);
|
|
|
|
fn undo(&mut self, ctx: &mut ViewContext<Self>);
|
|
fn change_mode(&mut self, old: &VimMode, new: &ModeTransition, ctx: &mut ViewContext<Self>);
|
|
fn backspace(&mut self, ctx: &mut ViewContext<Self>);
|
|
fn delete_forward(&mut self, ctx: &mut ViewContext<Self>);
|
|
fn escape(&mut self, ctx: &mut ViewContext<Self>);
|
|
/// Go to the definition of the symbol under cursor (gd).
|
|
fn goto_definition(&mut self, _ctx: &mut ViewContext<Self>) {}
|
|
/// Find references of the symbol under cursor (gr).
|
|
fn find_references(&mut self, _ctx: &mut ViewContext<Self>) {}
|
|
/// Show hover information for the symbol under cursor (gh).
|
|
fn show_hover(&mut self, _ctx: &mut ViewContext<Self>) {}
|
|
}
|