зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-03 14:26:17 +03:00
Merge branch 'vim'
Этот коммит содержится в:
@@ -70,7 +70,7 @@ name = "variable"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
name = "modal"
|
||||
name = "vim"
|
||||
required-features = ["crossterm"]
|
||||
|
||||
[[example]]
|
||||
|
||||
52
README.md
52
README.md
@@ -77,13 +77,15 @@ cargo run --example variable
|
||||
|
||||
Simple textarea with variable height following the number of lines.
|
||||
|
||||
### [`modal`](./examples/modal.rs)
|
||||
### [`vim`](./examples/vim.rs)
|
||||
|
||||
```sh
|
||||
cargo run --example modal
|
||||
cargo run --example vim
|
||||
```
|
||||
|
||||
Simple modal text editor like `vi`.
|
||||
Vim-like modal text editor. Vim emulation is implemented as a state machine.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/rhysd/ss/master/tui-textarea/vim.gif" width=590 height=156 alt="Vim emulation example">
|
||||
|
||||
### [`popup_placeholder`](./examples/popup_placeholder.rs)
|
||||
|
||||
@@ -513,49 +515,9 @@ notify how to move the cursor.
|
||||
| `textarea.scroll(Scrolling::HalfPageUp)` | Scroll up the viewport by half-page |
|
||||
| `textarea.scroll((row, col))` | Scroll down the viewport to (row, col) position |
|
||||
|
||||
To define your own key mappings, simply call the above methods in your code instead of `TextArea::input()` method. The
|
||||
following example defines modal key mappings like Vim.
|
||||
To define your own key mappings, simply call the above methods in your code instead of `TextArea::input()` method.
|
||||
|
||||
```rust,ignore
|
||||
use crossterm::event::{Event, read};
|
||||
use tui_textarea::{Input, Key, CursorMove, Scrolling};
|
||||
|
||||
let mut textarea = ...;
|
||||
|
||||
enum Mode {
|
||||
Normal,
|
||||
Insert,
|
||||
}
|
||||
|
||||
let mut mode = Mode::Normal;
|
||||
|
||||
// Event loop
|
||||
loop {
|
||||
// ...
|
||||
|
||||
match mode {
|
||||
Mode::Normal => match read()?.into() {
|
||||
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('i'), .. } => mode = Mode::Insert, // Enter insert mode
|
||||
// ...Add more mappings
|
||||
_ => {},
|
||||
},
|
||||
Mode::Insert => match read()?.into() {
|
||||
Input { key: Key::Esc, .. } => {
|
||||
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
|
||||
}
|
||||
input => {
|
||||
textarea.input(input); // Use default key mappings in insert mode
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See [`modal` example](./examples/modal.rs) for working example. It implements more Vim-like key mappings.
|
||||
See the [`vim` example](./examples/vim.rs) for working example. It implements more Vim-like key modal mappings.
|
||||
|
||||
If you don't want to use default key mappings, `TextArea::input_without_shortcuts()` method can be used instead of
|
||||
`TextArea::input()`. The method only handles very basic operations such as inserting/deleting single characters, tabs,
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::widgets::{Block, Borders};
|
||||
use ratatui::Terminal;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use tui_textarea::{CursorMove, Input, Key, Scrolling, TextArea};
|
||||
|
||||
enum Mode {
|
||||
Normal,
|
||||
Insert,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
fn help_message(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Normal => "type q to quit, type i to enter insert mode",
|
||||
Self::Insert => "type Esc to back to normal mode",
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_color(&self) -> Color {
|
||||
match self {
|
||||
Self::Normal => Color::Reset,
|
||||
Self::Insert => Color::LightBlue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Mode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
Self::Normal => write!(f, "NORMAL"),
|
||||
Self::Insert => write!(f, "INSERT"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// State machine to handle pending key inputs
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum PendingState {
|
||||
None,
|
||||
G,
|
||||
D, // 'Delete' operator
|
||||
Y, // 'Yank' operator
|
||||
C, // 'Change' operator
|
||||
}
|
||||
|
||||
impl PendingState {
|
||||
fn operator(&self) -> Option<char> {
|
||||
match self {
|
||||
Self::D => Some('d'),
|
||||
Self::Y => Some('y'),
|
||||
Self::C => Some('c'),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn transition(self, input: &Input) -> Option<Self> {
|
||||
match input {
|
||||
Input { key: Key::Null, .. } => None,
|
||||
Input {
|
||||
key: Key::Char('g'),
|
||||
ctrl: false,
|
||||
..
|
||||
} if self != Self::G => Some(Self::G),
|
||||
Input {
|
||||
key: Key::Char('d'),
|
||||
ctrl: false,
|
||||
..
|
||||
} if self != Self::D => Some(Self::D),
|
||||
Input {
|
||||
key: Key::Char('y'),
|
||||
ctrl: false,
|
||||
..
|
||||
} if self != Self::Y => Some(Self::Y),
|
||||
Input {
|
||||
key: Key::Char('c'),
|
||||
ctrl: false,
|
||||
..
|
||||
} if self != Self::C => Some(Self::C),
|
||||
_ => Some(Self::None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
enable_raw_mode()?;
|
||||
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut term = Terminal::new(backend)?;
|
||||
|
||||
let mut textarea = if let Some(path) = env::args().nth(1) {
|
||||
let file = fs::File::open(path)?;
|
||||
io::BufReader::new(file)
|
||||
.lines()
|
||||
.collect::<io::Result<_>>()?
|
||||
} else {
|
||||
TextArea::default()
|
||||
};
|
||||
|
||||
let mut mode = Mode::Normal;
|
||||
let mut pending = PendingState::None;
|
||||
|
||||
loop {
|
||||
// Show help message and current mode in title of the block
|
||||
let title = format!("{} MODE ({})", mode, mode.help_message());
|
||||
let block = Block::default().borders(Borders::ALL).title(title);
|
||||
textarea.set_block(block);
|
||||
|
||||
// Change the cursor color looking at current mode
|
||||
let color = mode.cursor_color();
|
||||
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
|
||||
textarea.set_cursor_style(style);
|
||||
|
||||
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
|
||||
|
||||
let input = crossterm::event::read()?.into();
|
||||
|
||||
if pending.operator().is_some() && !textarea.is_selecting() {
|
||||
textarea.start_selection();
|
||||
}
|
||||
let next_pending = pending.transition(&input); // Calculate next state before moving `input`
|
||||
|
||||
match mode {
|
||||
Mode::Normal => match input {
|
||||
// Mappings in normal mode
|
||||
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();
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('C'),
|
||||
..
|
||||
} => {
|
||||
textarea.delete_line_by_end();
|
||||
mode = Mode::Insert;
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('p'),
|
||||
..
|
||||
} => {
|
||||
textarea.paste();
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('u'),
|
||||
ctrl: false,
|
||||
..
|
||||
} => {
|
||||
textarea.undo();
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('r'),
|
||||
ctrl: true,
|
||||
..
|
||||
} => {
|
||||
textarea.redo();
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('x'),
|
||||
..
|
||||
} => {
|
||||
textarea.delete_next_char();
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('i'),
|
||||
..
|
||||
} => mode = Mode::Insert,
|
||||
Input {
|
||||
key: Key::Char('a'),
|
||||
..
|
||||
} => {
|
||||
textarea.move_cursor(CursorMove::Forward);
|
||||
mode = Mode::Insert;
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('A'),
|
||||
..
|
||||
} => {
|
||||
textarea.move_cursor(CursorMove::End);
|
||||
mode = Mode::Insert;
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('o'),
|
||||
..
|
||||
} => {
|
||||
textarea.move_cursor(CursorMove::End);
|
||||
textarea.insert_newline();
|
||||
mode = Mode::Insert;
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('O'),
|
||||
..
|
||||
} => {
|
||||
textarea.move_cursor(CursorMove::Head);
|
||||
textarea.insert_newline();
|
||||
textarea.move_cursor(CursorMove::Up);
|
||||
mode = Mode::Insert;
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('I'),
|
||||
..
|
||||
} => {
|
||||
textarea.move_cursor(CursorMove::Head);
|
||||
mode = Mode::Insert;
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('q'),
|
||||
..
|
||||
} => break,
|
||||
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,
|
||||
..
|
||||
} => textarea.start_selection(),
|
||||
Input {
|
||||
key: Key::Char('V'),
|
||||
ctrl: false,
|
||||
..
|
||||
} => {
|
||||
textarea.move_cursor(CursorMove::Head);
|
||||
textarea.start_selection();
|
||||
textarea.move_cursor(CursorMove::End);
|
||||
}
|
||||
Input { key: Key::Esc, .. } => textarea.cancel_selection(),
|
||||
Input {
|
||||
key: Key::Char('g'),
|
||||
ctrl: false,
|
||||
..
|
||||
} if pending == PendingState::G => {
|
||||
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 pending.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
|
||||
}
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('y'),
|
||||
ctrl: false,
|
||||
..
|
||||
} if textarea.is_selecting() => textarea.copy(),
|
||||
Input {
|
||||
key: Key::Char('d'),
|
||||
ctrl: false,
|
||||
..
|
||||
} if textarea.is_selecting() => {
|
||||
textarea.cut();
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('c'),
|
||||
ctrl: false,
|
||||
..
|
||||
} if textarea.is_selecting() => {
|
||||
textarea.cut();
|
||||
mode = Mode::Insert;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Mode::Insert => match input {
|
||||
Input { key: Key::Esc, .. }
|
||||
| Input {
|
||||
key: Key::Char('c'),
|
||||
ctrl: true,
|
||||
..
|
||||
} => {
|
||||
mode = Mode::Normal; // Back to normal mode with Esc or Ctrl+C
|
||||
}
|
||||
input => {
|
||||
textarea.input(input); // Use default key mappings in insert mode
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
if let Some(next_pending) = next_pending {
|
||||
match pending {
|
||||
PendingState::D => {
|
||||
textarea.cut();
|
||||
}
|
||||
PendingState::Y => textarea.copy(),
|
||||
PendingState::C => {
|
||||
textarea.cut();
|
||||
mode = Mode::Insert;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
pending = next_pending;
|
||||
}
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
crossterm::execute!(
|
||||
term.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
term.show_cursor()?;
|
||||
|
||||
println!("Lines: {:?}", textarea.lines());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
432
examples/vim.rs
Обычный файл
432
examples/vim.rs
Обычный файл
@@ -0,0 +1,432 @@
|
||||
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
};
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::widgets::{Block, Borders};
|
||||
use ratatui::Terminal;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use tui_textarea::{CursorMove, Input, Key, Scrolling, TextArea};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
Normal,
|
||||
Insert,
|
||||
Visual,
|
||||
Operator(char),
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
fn help_message(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Normal => "type q to quit, type i to enter insert 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",
|
||||
Self::Operator(_) => "move cursor to apply operator",
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_color(&self) -> Color {
|
||||
match self {
|
||||
Self::Normal => Color::Reset,
|
||||
Self::Insert => Color::LightBlue,
|
||||
Self::Visual => Color::LightYellow,
|
||||
Self::Operator(_) => Color::LightGreen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Mode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
Self::Normal => write!(f, "NORMAL"),
|
||||
Self::Insert => write!(f, "INSERT"),
|
||||
Self::Visual => write!(f, "VISUAL"),
|
||||
Self::Operator(c) => write!(f, "OPERATOR({})", c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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<()> {
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
enable_raw_mode()?;
|
||||
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut term = Terminal::new(backend)?;
|
||||
|
||||
let mut textarea = if let Some(path) = env::args().nth(1) {
|
||||
let file = fs::File::open(path)?;
|
||||
io::BufReader::new(file)
|
||||
.lines()
|
||||
.collect::<io::Result<_>>()?
|
||||
} else {
|
||||
TextArea::default()
|
||||
};
|
||||
|
||||
let mut vim = Vim::new(Mode::Normal);
|
||||
loop {
|
||||
// Show help message and current mode in title of the block
|
||||
let title = format!("{} MODE ({})", vim.mode, vim.mode.help_message());
|
||||
let block = Block::default().borders(Borders::ALL).title(title);
|
||||
textarea.set_block(block);
|
||||
|
||||
// Change the cursor color looking at current mode
|
||||
let color = vim.mode.cursor_color();
|
||||
let style = Style::default().fg(color).add_modifier(Modifier::REVERSED);
|
||||
textarea.set_cursor_style(style);
|
||||
|
||||
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
|
||||
|
||||
vim = match vim.input(crossterm::event::read()?.into(), &mut textarea) {
|
||||
Transition::Nop => vim,
|
||||
Transition::Mode(mode) => Vim::new(mode),
|
||||
Transition::Pending(input) => vim.with_pending(input),
|
||||
Transition::Quit => break,
|
||||
}
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
crossterm::execute!(
|
||||
term.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
term.show_cursor()?;
|
||||
|
||||
println!("Lines: {:?}", textarea.lines());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ссылка в новой задаче
Block a user