зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-08 08:25:51 +03:00
refactor Vim emulation as state machine
Этот коммит содержится в:
@@ -83,7 +83,7 @@ Simple textarea with variable height following the number of lines.
|
|||||||
cargo run --example vim
|
cargo run --example vim
|
||||||
```
|
```
|
||||||
|
|
||||||
Vim-like modal text editor.
|
Vim-like modal text editor. Vim emulation is implemented as a state machine.
|
||||||
|
|
||||||
### [`popup_placeholder`](./examples/popup_placeholder.rs)
|
### [`popup_placeholder`](./examples/popup_placeholder.rs)
|
||||||
|
|
||||||
|
|||||||
691
examples/vim.rs
691
examples/vim.rs
@@ -52,6 +52,332 @@ impl fmt::Display for Mode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// State of Vim emulation
|
||||||
|
struct Vim {
|
||||||
|
mode: Mode,
|
||||||
|
pending: Input, // Pending input to handle a sequence with two keys like gg
|
||||||
|
}
|
||||||
|
|
||||||
|
// How the Vim emulation state transitions
|
||||||
|
enum Transition {
|
||||||
|
Nop,
|
||||||
|
Mode(Mode),
|
||||||
|
Pending(Input),
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vim {
|
||||||
|
fn new(mode: Mode) -> Self {
|
||||||
|
Self {
|
||||||
|
mode,
|
||||||
|
pending: Input::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_pending(self, pending: Input) -> Self {
|
||||||
|
Self {
|
||||||
|
mode: self.mode,
|
||||||
|
pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input(&self, input: Input, textarea: &mut TextArea<'_>) -> Transition {
|
||||||
|
if input.key == Key::Null {
|
||||||
|
return Transition::Nop;
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.mode {
|
||||||
|
Mode::Normal | Mode::Visual | Mode::Operator(_) => {
|
||||||
|
match input {
|
||||||
|
Input {
|
||||||
|
key: Key::Char('h'),
|
||||||
|
..
|
||||||
|
} => textarea.move_cursor(CursorMove::Back),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('j'),
|
||||||
|
..
|
||||||
|
} => textarea.move_cursor(CursorMove::Down),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('k'),
|
||||||
|
..
|
||||||
|
} => textarea.move_cursor(CursorMove::Up),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('l'),
|
||||||
|
..
|
||||||
|
} => textarea.move_cursor(CursorMove::Forward),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('w'),
|
||||||
|
..
|
||||||
|
} => textarea.move_cursor(CursorMove::WordForward),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('b'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} => textarea.move_cursor(CursorMove::WordBack),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('^'),
|
||||||
|
..
|
||||||
|
} => textarea.move_cursor(CursorMove::Head),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('$'),
|
||||||
|
..
|
||||||
|
} => textarea.move_cursor(CursorMove::End),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('D'),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.delete_line_by_end();
|
||||||
|
return Transition::Mode(Mode::Normal);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('C'),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.delete_line_by_end();
|
||||||
|
textarea.cancel_selection();
|
||||||
|
return Transition::Mode(Mode::Insert);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('p'),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.paste();
|
||||||
|
return Transition::Mode(Mode::Normal);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('u'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.undo();
|
||||||
|
return Transition::Mode(Mode::Normal);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('r'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.redo();
|
||||||
|
return Transition::Mode(Mode::Normal);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('x'),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.delete_next_char();
|
||||||
|
return Transition::Mode(Mode::Normal);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('i'),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.cancel_selection();
|
||||||
|
return Transition::Mode(Mode::Insert);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('a'),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.cancel_selection();
|
||||||
|
textarea.move_cursor(CursorMove::Forward);
|
||||||
|
return Transition::Mode(Mode::Insert);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('A'),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.cancel_selection();
|
||||||
|
textarea.move_cursor(CursorMove::End);
|
||||||
|
return Transition::Mode(Mode::Insert);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('o'),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.move_cursor(CursorMove::End);
|
||||||
|
textarea.insert_newline();
|
||||||
|
return Transition::Mode(Mode::Insert);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('O'),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.move_cursor(CursorMove::Head);
|
||||||
|
textarea.insert_newline();
|
||||||
|
textarea.move_cursor(CursorMove::Up);
|
||||||
|
return Transition::Mode(Mode::Insert);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('I'),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.cancel_selection();
|
||||||
|
textarea.move_cursor(CursorMove::Head);
|
||||||
|
return Transition::Mode(Mode::Insert);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('q'),
|
||||||
|
..
|
||||||
|
} => return Transition::Quit,
|
||||||
|
Input {
|
||||||
|
key: Key::Char('e'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => textarea.scroll((1, 0)),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('y'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => textarea.scroll((-1, 0)),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('d'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => textarea.scroll(Scrolling::HalfPageDown),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('u'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => textarea.scroll(Scrolling::HalfPageUp),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('f'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => textarea.scroll(Scrolling::PageDown),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('b'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => textarea.scroll(Scrolling::PageUp),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('v'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if self.mode == Mode::Normal => {
|
||||||
|
textarea.start_selection();
|
||||||
|
return Transition::Mode(Mode::Visual);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('V'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if self.mode == Mode::Normal => {
|
||||||
|
textarea.move_cursor(CursorMove::Head);
|
||||||
|
textarea.start_selection();
|
||||||
|
textarea.move_cursor(CursorMove::End);
|
||||||
|
return Transition::Mode(Mode::Visual);
|
||||||
|
}
|
||||||
|
Input { key: Key::Esc, .. }
|
||||||
|
| Input {
|
||||||
|
key: Key::Char('v'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if self.mode == Mode::Visual => {
|
||||||
|
textarea.cancel_selection();
|
||||||
|
return Transition::Mode(Mode::Normal);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('g'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if matches!(
|
||||||
|
self.pending,
|
||||||
|
Input {
|
||||||
|
key: Key::Char('g'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
textarea.move_cursor(CursorMove::Top)
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('G'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} => textarea.move_cursor(CursorMove::Bottom),
|
||||||
|
Input {
|
||||||
|
key: Key::Char(c),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if self.mode == Mode::Operator(c) => {
|
||||||
|
// Handle yy, dd, cc. (This is not strictly the same behavior as Vim)
|
||||||
|
textarea.move_cursor(CursorMove::Head);
|
||||||
|
textarea.start_selection();
|
||||||
|
let cursor = textarea.cursor();
|
||||||
|
textarea.move_cursor(CursorMove::Down);
|
||||||
|
if cursor == textarea.cursor() {
|
||||||
|
textarea.move_cursor(CursorMove::End); // At the last line, move to end of the line instead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char(op @ ('y' | 'd' | 'c')),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if self.mode == Mode::Normal => {
|
||||||
|
textarea.start_selection();
|
||||||
|
return Transition::Mode(Mode::Operator(op));
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('y'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if self.mode == Mode::Visual => {
|
||||||
|
textarea.copy();
|
||||||
|
return Transition::Mode(Mode::Normal);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('d'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if self.mode == Mode::Visual => {
|
||||||
|
textarea.cut();
|
||||||
|
return Transition::Mode(Mode::Normal);
|
||||||
|
}
|
||||||
|
Input {
|
||||||
|
key: Key::Char('c'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if self.mode == Mode::Visual => {
|
||||||
|
textarea.cut();
|
||||||
|
return Transition::Mode(Mode::Insert);
|
||||||
|
}
|
||||||
|
input => return Transition::Pending(input),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the pending operator
|
||||||
|
match self.mode {
|
||||||
|
Mode::Operator('y') => {
|
||||||
|
textarea.copy();
|
||||||
|
Transition::Mode(Mode::Normal)
|
||||||
|
}
|
||||||
|
Mode::Operator('d') => {
|
||||||
|
textarea.cut();
|
||||||
|
Transition::Mode(Mode::Normal)
|
||||||
|
}
|
||||||
|
Mode::Operator('c') => {
|
||||||
|
textarea.cut();
|
||||||
|
Transition::Mode(Mode::Insert)
|
||||||
|
}
|
||||||
|
_ => Transition::Nop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Mode::Insert => match input {
|
||||||
|
Input { key: Key::Esc, .. }
|
||||||
|
| Input {
|
||||||
|
key: Key::Char('c'),
|
||||||
|
ctrl: true,
|
||||||
|
..
|
||||||
|
} => Transition::Mode(Mode::Normal),
|
||||||
|
input => {
|
||||||
|
textarea.input(input); // Use default key mappings in insert mode
|
||||||
|
Transition::Mode(Mode::Insert)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> io::Result<()> {
|
fn main() -> io::Result<()> {
|
||||||
let stdout = io::stdout();
|
let stdout = io::stdout();
|
||||||
let mut stdout = stdout.lock();
|
let mut stdout = stdout.lock();
|
||||||
@@ -70,374 +396,25 @@ fn main() -> io::Result<()> {
|
|||||||
TextArea::default()
|
TextArea::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut mode = Mode::Normal;
|
let mut vim = Vim::new(Mode::Normal);
|
||||||
let mut pending = Input::default();
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Show help message and current mode in title of the block
|
// Show help message and current mode in title of the block
|
||||||
let title = format!("{} MODE ({})", mode, mode.help_message());
|
let title = format!("{} MODE ({})", vim.mode, vim.mode.help_message());
|
||||||
let block = Block::default().borders(Borders::ALL).title(title);
|
let block = Block::default().borders(Borders::ALL).title(title);
|
||||||
textarea.set_block(block);
|
textarea.set_block(block);
|
||||||
|
|
||||||
// Change the cursor color looking at current mode
|
// Change the cursor color looking at current mode
|
||||||
let color = mode.cursor_color();
|
let color = vim.mode.cursor_color();
|
||||||
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
|
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
|
||||||
textarea.set_cursor_style(style);
|
textarea.set_cursor_style(style);
|
||||||
|
|
||||||
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
|
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
|
||||||
|
|
||||||
let input: Input = crossterm::event::read()?.into();
|
vim = match vim.input(crossterm::event::read()?.into(), &mut textarea) {
|
||||||
if input.key == Key::Null {
|
Transition::Nop => vim,
|
||||||
continue;
|
Transition::Mode(mode) => Vim::new(mode),
|
||||||
}
|
Transition::Pending(input) => vim.with_pending(input),
|
||||||
|
Transition::Quit => break,
|
||||||
let operator = if let Mode::Operator(op) = mode {
|
|
||||||
textarea.start_selection();
|
|
||||||
Some(op)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
mode = match mode {
|
|
||||||
Mode::Normal | Mode::Visual | Mode::Operator(_) => match input {
|
|
||||||
// Mappings in normal mode
|
|
||||||
Input {
|
|
||||||
key: Key::Char('h'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.move_cursor(CursorMove::Back);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('j'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.move_cursor(CursorMove::Down);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('k'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.move_cursor(CursorMove::Up);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('l'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.move_cursor(CursorMove::Forward);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('w'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.move_cursor(CursorMove::WordForward);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('b'),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.move_cursor(CursorMove::WordBack);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('^'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.move_cursor(CursorMove::Head);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('$'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.move_cursor(CursorMove::End);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('D'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.delete_line_by_end();
|
|
||||||
Mode::Normal
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('C'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.delete_line_by_end();
|
|
||||||
textarea.cancel_selection();
|
|
||||||
Mode::Insert
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('p'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.paste();
|
|
||||||
Mode::Normal
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('u'),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.undo();
|
|
||||||
Mode::Normal
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('r'),
|
|
||||||
ctrl: true,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.redo();
|
|
||||||
Mode::Normal
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('x'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.delete_next_char();
|
|
||||||
Mode::Normal
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('i'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.cancel_selection();
|
|
||||||
Mode::Insert
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('a'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.cancel_selection();
|
|
||||||
textarea.move_cursor(CursorMove::Forward);
|
|
||||||
Mode::Insert
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('A'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.cancel_selection();
|
|
||||||
textarea.move_cursor(CursorMove::End);
|
|
||||||
Mode::Insert
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('o'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.move_cursor(CursorMove::End);
|
|
||||||
textarea.insert_newline();
|
|
||||||
Mode::Insert
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('O'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.move_cursor(CursorMove::Head);
|
|
||||||
textarea.insert_newline();
|
|
||||||
textarea.move_cursor(CursorMove::Up);
|
|
||||||
Mode::Insert
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('I'),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.cancel_selection();
|
|
||||||
textarea.move_cursor(CursorMove::Head);
|
|
||||||
Mode::Insert
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('q'),
|
|
||||||
..
|
|
||||||
} => break,
|
|
||||||
Input {
|
|
||||||
key: Key::Char('e'),
|
|
||||||
ctrl: true,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.scroll((1, 0));
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('y'),
|
|
||||||
ctrl: true,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.scroll((-1, 0));
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('d'),
|
|
||||||
ctrl: true,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.scroll(Scrolling::HalfPageDown);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('u'),
|
|
||||||
ctrl: true,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.scroll(Scrolling::HalfPageUp);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('f'),
|
|
||||||
ctrl: true,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.scroll(Scrolling::PageDown);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('b'),
|
|
||||||
ctrl: true,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.scroll(Scrolling::PageUp);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('v'),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} if mode == Mode::Normal => {
|
|
||||||
textarea.start_selection();
|
|
||||||
Mode::Visual
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('V'),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} if mode == Mode::Normal => {
|
|
||||||
textarea.move_cursor(CursorMove::Head);
|
|
||||||
textarea.start_selection();
|
|
||||||
textarea.move_cursor(CursorMove::End);
|
|
||||||
Mode::Visual
|
|
||||||
}
|
|
||||||
Input { key: Key::Esc, .. }
|
|
||||||
| Input {
|
|
||||||
key: Key::Char('v'),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} if mode == Mode::Visual => {
|
|
||||||
textarea.cancel_selection();
|
|
||||||
Mode::Normal
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('g'),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} if matches!(
|
|
||||||
pending,
|
|
||||||
Input {
|
|
||||||
key: Key::Char('g'),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
textarea.move_cursor(CursorMove::Top);
|
|
||||||
pending = Input::default();
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('G'),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
textarea.move_cursor(CursorMove::Bottom);
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char(c),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} if operator == Some(c) => {
|
|
||||||
// Handle yy, dd, cc. (This is not strictly the same behavior as Vim)
|
|
||||||
textarea.move_cursor(CursorMove::Head);
|
|
||||||
textarea.start_selection();
|
|
||||||
let cursor = textarea.cursor();
|
|
||||||
textarea.move_cursor(CursorMove::Down);
|
|
||||||
if cursor == textarea.cursor() {
|
|
||||||
textarea.move_cursor(CursorMove::End); // At the last line, move to end of the line instead
|
|
||||||
}
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char(op @ ('y' | 'd' | 'c')),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} if mode == Mode::Normal => Mode::Operator(op),
|
|
||||||
Input {
|
|
||||||
key: Key::Char('y'),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} if mode == Mode::Visual => {
|
|
||||||
textarea.copy();
|
|
||||||
Mode::Normal
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('d'),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} if mode == Mode::Visual => {
|
|
||||||
textarea.cut();
|
|
||||||
Mode::Normal
|
|
||||||
}
|
|
||||||
Input {
|
|
||||||
key: Key::Char('c'),
|
|
||||||
ctrl: false,
|
|
||||||
..
|
|
||||||
} if mode == Mode::Visual => {
|
|
||||||
textarea.cut();
|
|
||||||
Mode::Insert
|
|
||||||
}
|
|
||||||
input => {
|
|
||||||
pending = input;
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Mode::Insert => match input {
|
|
||||||
Input { key: Key::Esc, .. }
|
|
||||||
| Input {
|
|
||||||
key: Key::Char('c'),
|
|
||||||
ctrl: true,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
Mode::Normal // Back to normal mode with Esc or Ctrl+C
|
|
||||||
}
|
|
||||||
input => {
|
|
||||||
textarea.input(input); // Use default key mappings in insert mode
|
|
||||||
mode
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(op) = operator {
|
|
||||||
if mode != Mode::Normal && mode != Mode::Visual {
|
|
||||||
mode = match op {
|
|
||||||
'y' => {
|
|
||||||
textarea.copy();
|
|
||||||
Mode::Normal
|
|
||||||
}
|
|
||||||
'd' => {
|
|
||||||
textarea.cut();
|
|
||||||
Mode::Normal
|
|
||||||
}
|
|
||||||
'c' => {
|
|
||||||
textarea.cut();
|
|
||||||
Mode::Insert
|
|
||||||
}
|
|
||||||
_ => mode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user