зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-08 00:15:50 +03:00
implement visual mode for vim example
Этот коммит содержится в:
172
examples/vim.rs
172
examples/vim.rs
@@ -13,9 +13,11 @@ use std::io;
|
|||||||
use std::io::BufRead;
|
use std::io::BufRead;
|
||||||
use tui_textarea::{CursorMove, Input, Key, Scrolling, TextArea};
|
use tui_textarea::{CursorMove, Input, Key, Scrolling, TextArea};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum Mode {
|
enum Mode {
|
||||||
Normal,
|
Normal,
|
||||||
Insert,
|
Insert,
|
||||||
|
Visual,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mode {
|
impl Mode {
|
||||||
@@ -23,6 +25,7 @@ impl Mode {
|
|||||||
match self {
|
match self {
|
||||||
Self::Normal => "type q to quit, type i to enter insert mode",
|
Self::Normal => "type q to quit, type i to enter insert mode",
|
||||||
Self::Insert => "type Esc to back to normal mode",
|
Self::Insert => "type Esc to back to normal mode",
|
||||||
|
Self::Visual => "type y to yank, type d to delete, type Esc to back to normal mode",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +33,7 @@ impl Mode {
|
|||||||
match self {
|
match self {
|
||||||
Self::Normal => Color::Reset,
|
Self::Normal => Color::Reset,
|
||||||
Self::Insert => Color::LightBlue,
|
Self::Insert => Color::LightBlue,
|
||||||
|
Self::Visual => Color::LightYellow,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,6 +43,7 @@ impl fmt::Display for Mode {
|
|||||||
match self {
|
match self {
|
||||||
Self::Normal => write!(f, "NORMAL"),
|
Self::Normal => write!(f, "NORMAL"),
|
||||||
Self::Insert => write!(f, "INSERT"),
|
Self::Insert => write!(f, "INSERT"),
|
||||||
|
Self::Visual => write!(f, "VISUAL"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,7 +68,7 @@ impl PendingState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transition(self, input: &Input) -> Option<Self> {
|
fn transition(self, input: &Input, mode: Mode) -> Option<Self> {
|
||||||
match input {
|
match input {
|
||||||
Input { key: Key::Null, .. } => None,
|
Input { key: Key::Null, .. } => None,
|
||||||
Input {
|
Input {
|
||||||
@@ -75,17 +80,17 @@ impl PendingState {
|
|||||||
key: Key::Char('d'),
|
key: Key::Char('d'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
..
|
..
|
||||||
} if self != Self::D => Some(Self::D),
|
} if self != Self::D && mode == Mode::Normal => Some(Self::D),
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('y'),
|
key: Key::Char('y'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
..
|
..
|
||||||
} if self != Self::Y => Some(Self::Y),
|
} if self != Self::Y && mode == Mode::Normal => Some(Self::Y),
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('c'),
|
key: Key::Char('c'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
..
|
..
|
||||||
} if self != Self::C => Some(Self::C),
|
} if self != Self::C && mode == Mode::Normal => Some(Self::C),
|
||||||
_ => Some(Self::None),
|
_ => Some(Self::None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,65 +132,93 @@ fn main() -> io::Result<()> {
|
|||||||
|
|
||||||
let input = crossterm::event::read()?.into();
|
let input = crossterm::event::read()?.into();
|
||||||
|
|
||||||
if pending.operator().is_some() && !textarea.is_selecting() {
|
if pending.operator().is_some() && mode != Mode::Visual {
|
||||||
textarea.start_selection();
|
textarea.start_selection();
|
||||||
}
|
}
|
||||||
let next_pending = pending.transition(&input); // Calculate next state before moving `input`
|
// Calculate next pending key state before moving `input`
|
||||||
|
let next_pending = pending.transition(&input, mode);
|
||||||
|
|
||||||
match mode {
|
mode = match mode {
|
||||||
Mode::Normal => match input {
|
Mode::Normal | Mode::Visual => match input {
|
||||||
// Mappings in normal mode
|
// Mappings in normal mode
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('h'),
|
key: Key::Char('h'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::Back),
|
} => {
|
||||||
|
textarea.move_cursor(CursorMove::Back);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('j'),
|
key: Key::Char('j'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::Down),
|
} => {
|
||||||
|
textarea.move_cursor(CursorMove::Down);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('k'),
|
key: Key::Char('k'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::Up),
|
} => {
|
||||||
|
textarea.move_cursor(CursorMove::Up);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('l'),
|
key: Key::Char('l'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::Forward),
|
} => {
|
||||||
|
textarea.move_cursor(CursorMove::Forward);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('w'),
|
key: Key::Char('w'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::WordForward),
|
} => {
|
||||||
|
textarea.move_cursor(CursorMove::WordForward);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('b'),
|
key: Key::Char('b'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::WordBack),
|
} => {
|
||||||
|
textarea.move_cursor(CursorMove::WordBack);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('^'),
|
key: Key::Char('^'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::Head),
|
} => {
|
||||||
|
textarea.move_cursor(CursorMove::Head);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('$'),
|
key: Key::Char('$'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::End),
|
} => {
|
||||||
|
textarea.move_cursor(CursorMove::End);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('D'),
|
key: Key::Char('D'),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
textarea.delete_line_by_end();
|
textarea.delete_line_by_end();
|
||||||
|
Mode::Normal
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('C'),
|
key: Key::Char('C'),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
textarea.delete_line_by_end();
|
textarea.delete_line_by_end();
|
||||||
mode = Mode::Insert;
|
textarea.cancel_selection();
|
||||||
|
Mode::Insert
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('p'),
|
key: Key::Char('p'),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
textarea.paste();
|
textarea.paste();
|
||||||
|
Mode::Normal
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('u'),
|
key: Key::Char('u'),
|
||||||
@@ -193,6 +226,7 @@ fn main() -> io::Result<()> {
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
textarea.undo();
|
textarea.undo();
|
||||||
|
Mode::Normal
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('r'),
|
key: Key::Char('r'),
|
||||||
@@ -200,30 +234,37 @@ fn main() -> io::Result<()> {
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
textarea.redo();
|
textarea.redo();
|
||||||
|
Mode::Normal
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('x'),
|
key: Key::Char('x'),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
textarea.delete_next_char();
|
textarea.delete_next_char();
|
||||||
|
Mode::Normal
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('i'),
|
key: Key::Char('i'),
|
||||||
..
|
..
|
||||||
} => mode = Mode::Insert,
|
} => {
|
||||||
|
textarea.cancel_selection();
|
||||||
|
Mode::Insert
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('a'),
|
key: Key::Char('a'),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
textarea.cancel_selection();
|
||||||
textarea.move_cursor(CursorMove::Forward);
|
textarea.move_cursor(CursorMove::Forward);
|
||||||
mode = Mode::Insert;
|
Mode::Insert
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('A'),
|
key: Key::Char('A'),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
textarea.cancel_selection();
|
||||||
textarea.move_cursor(CursorMove::End);
|
textarea.move_cursor(CursorMove::End);
|
||||||
mode = Mode::Insert;
|
Mode::Insert
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('o'),
|
key: Key::Char('o'),
|
||||||
@@ -231,7 +272,7 @@ fn main() -> io::Result<()> {
|
|||||||
} => {
|
} => {
|
||||||
textarea.move_cursor(CursorMove::End);
|
textarea.move_cursor(CursorMove::End);
|
||||||
textarea.insert_newline();
|
textarea.insert_newline();
|
||||||
mode = Mode::Insert;
|
Mode::Insert
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('O'),
|
key: Key::Char('O'),
|
||||||
@@ -240,14 +281,15 @@ fn main() -> io::Result<()> {
|
|||||||
textarea.move_cursor(CursorMove::Head);
|
textarea.move_cursor(CursorMove::Head);
|
||||||
textarea.insert_newline();
|
textarea.insert_newline();
|
||||||
textarea.move_cursor(CursorMove::Up);
|
textarea.move_cursor(CursorMove::Up);
|
||||||
mode = Mode::Insert;
|
Mode::Insert
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('I'),
|
key: Key::Char('I'),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
textarea.cancel_selection();
|
||||||
textarea.move_cursor(CursorMove::Head);
|
textarea.move_cursor(CursorMove::Head);
|
||||||
mode = Mode::Insert;
|
Mode::Insert
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('q'),
|
key: Key::Char('q'),
|
||||||
@@ -257,53 +299,84 @@ fn main() -> io::Result<()> {
|
|||||||
key: Key::Char('e'),
|
key: Key::Char('e'),
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
..
|
..
|
||||||
} => textarea.scroll((1, 0)),
|
} => {
|
||||||
|
textarea.scroll((1, 0));
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('y'),
|
key: Key::Char('y'),
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
..
|
..
|
||||||
} => textarea.scroll((-1, 0)),
|
} => {
|
||||||
|
textarea.scroll((-1, 0));
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('d'),
|
key: Key::Char('d'),
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
..
|
..
|
||||||
} => textarea.scroll(Scrolling::HalfPageDown),
|
} => {
|
||||||
|
textarea.scroll(Scrolling::HalfPageDown);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('u'),
|
key: Key::Char('u'),
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
..
|
..
|
||||||
} => textarea.scroll(Scrolling::HalfPageUp),
|
} => {
|
||||||
|
textarea.scroll(Scrolling::HalfPageUp);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('f'),
|
key: Key::Char('f'),
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
..
|
..
|
||||||
} => textarea.scroll(Scrolling::PageDown),
|
} => {
|
||||||
|
textarea.scroll(Scrolling::PageDown);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('b'),
|
key: Key::Char('b'),
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
..
|
..
|
||||||
} => textarea.scroll(Scrolling::PageUp),
|
} => {
|
||||||
|
textarea.scroll(Scrolling::PageUp);
|
||||||
|
mode
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('v'),
|
key: Key::Char('v'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
..
|
..
|
||||||
} => textarea.start_selection(),
|
} if mode == Mode::Normal => {
|
||||||
|
textarea.start_selection();
|
||||||
|
Mode::Visual
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('V'),
|
key: Key::Char('V'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
..
|
..
|
||||||
} => {
|
} if mode == Mode::Normal => {
|
||||||
textarea.move_cursor(CursorMove::Head);
|
textarea.move_cursor(CursorMove::Head);
|
||||||
textarea.start_selection();
|
textarea.start_selection();
|
||||||
textarea.move_cursor(CursorMove::End);
|
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::Esc, .. } => textarea.cancel_selection(),
|
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('g'),
|
key: Key::Char('g'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
..
|
..
|
||||||
} if pending == PendingState::G => {
|
} if pending == PendingState::G => {
|
||||||
textarea.move_cursor(CursorMove::Top);
|
textarea.move_cursor(CursorMove::Top);
|
||||||
|
mode
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('G'),
|
key: Key::Char('G'),
|
||||||
@@ -311,6 +384,7 @@ fn main() -> io::Result<()> {
|
|||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
textarea.move_cursor(CursorMove::Bottom);
|
textarea.move_cursor(CursorMove::Bottom);
|
||||||
|
mode
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char(c),
|
key: Key::Char(c),
|
||||||
@@ -325,28 +399,33 @@ fn main() -> io::Result<()> {
|
|||||||
if cursor == textarea.cursor() {
|
if cursor == textarea.cursor() {
|
||||||
textarea.move_cursor(CursorMove::End); // At the last line, move to end of the line instead
|
textarea.move_cursor(CursorMove::End); // At the last line, move to end of the line instead
|
||||||
}
|
}
|
||||||
|
mode
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('y'),
|
key: Key::Char('y'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
..
|
..
|
||||||
} if textarea.is_selecting() => textarea.copy(),
|
} if mode == Mode::Visual => {
|
||||||
|
textarea.copy();
|
||||||
|
Mode::Normal
|
||||||
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('d'),
|
key: Key::Char('d'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
..
|
..
|
||||||
} if textarea.is_selecting() => {
|
} if mode == Mode::Visual => {
|
||||||
textarea.cut();
|
textarea.cut();
|
||||||
|
Mode::Normal
|
||||||
}
|
}
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('c'),
|
key: Key::Char('c'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
..
|
..
|
||||||
} if textarea.is_selecting() => {
|
} if mode == Mode::Visual => {
|
||||||
textarea.cut();
|
textarea.cut();
|
||||||
mode = Mode::Insert;
|
Mode::Insert
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => mode,
|
||||||
},
|
},
|
||||||
Mode::Insert => match input {
|
Mode::Insert => match input {
|
||||||
Input { key: Key::Esc, .. }
|
Input { key: Key::Esc, .. }
|
||||||
@@ -355,26 +434,31 @@ fn main() -> io::Result<()> {
|
|||||||
ctrl: true,
|
ctrl: true,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
|
Mode::Normal // Back to normal mode with Esc or Ctrl+C
|
||||||
}
|
}
|
||||||
input => {
|
input => {
|
||||||
textarea.input(input); // Use default key mappings in insert mode
|
textarea.input(input); // Use default key mappings in insert mode
|
||||||
|
mode
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
if let Some(next_pending) = next_pending {
|
if let Some(next_pending) = next_pending {
|
||||||
match pending {
|
mode = match pending {
|
||||||
PendingState::D => {
|
PendingState::D => {
|
||||||
textarea.cut();
|
textarea.cut();
|
||||||
|
Mode::Normal
|
||||||
|
}
|
||||||
|
PendingState::Y => {
|
||||||
|
textarea.copy();
|
||||||
|
Mode::Normal
|
||||||
}
|
}
|
||||||
PendingState::Y => textarea.copy(),
|
|
||||||
PendingState::C => {
|
PendingState::C => {
|
||||||
textarea.cut();
|
textarea.cut();
|
||||||
mode = Mode::Insert;
|
Mode::Insert
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => mode,
|
||||||
}
|
};
|
||||||
pending = next_pending;
|
pending = next_pending;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user