use crate::{Action, EditResult, Key, KeyCode, LineEditor, TextEdit};
use std::ops::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Mode {
#[default]
Normal,
Insert,
OperatorPending(Operator),
Visual,
ReplaceChar,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Operator {
Delete,
Change,
Yank,
}
#[derive(Debug, Clone)]
pub struct VimLineEditor {
cursor: usize,
mode: Mode,
visual_anchor: Option<usize>,
yank_buffer: String,
}
impl Default for VimLineEditor {
fn default() -> Self {
Self::new()
}
}
impl VimLineEditor {
pub fn new() -> Self {
Self {
cursor: 0,
mode: Mode::Normal,
visual_anchor: None,
yank_buffer: String::new(),
}
}
pub fn mode(&self) -> Mode {
self.mode
}
fn clamp_cursor(&mut self, text: &str) {
self.cursor = self.cursor.min(text.len());
}
fn move_left(&mut self, text: &str) {
if self.cursor > 0 {
let mut new_pos = self.cursor - 1;
while new_pos > 0 && !text.is_char_boundary(new_pos) {
new_pos -= 1;
}
self.cursor = new_pos;
}
}
fn move_right(&mut self, text: &str) {
if self.cursor < text.len() {
let mut new_pos = self.cursor + 1;
while new_pos < text.len() && !text.is_char_boundary(new_pos) {
new_pos += 1;
}
self.cursor = new_pos;
}
}
fn move_line_start(&mut self, text: &str) {
self.cursor = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
}
fn move_first_non_blank(&mut self, text: &str) {
self.move_line_start(text);
let line_start = self.cursor;
for (i, c) in text[line_start..].char_indices() {
if c == '\n' || !c.is_whitespace() {
self.cursor = line_start + i;
return;
}
}
}
fn move_line_end_impl(&mut self, text: &str, past_end: bool) {
let line_end = text[self.cursor..]
.find('\n')
.map(|i| self.cursor + i)
.unwrap_or(text.len());
if past_end || line_end == 0 {
self.cursor = line_end;
} else {
let mut last_char_start = line_end.saturating_sub(1);
while last_char_start > 0 && !text.is_char_boundary(last_char_start) {
last_char_start -= 1;
}
self.cursor = last_char_start;
}
}
fn move_line_end(&mut self, text: &str) {
self.move_line_end_impl(text, false);
}
fn move_line_end_insert(&mut self, text: &str) {
self.move_line_end_impl(text, true);
}
fn move_word_forward(&mut self, text: &str) {
let bytes = text.as_bytes();
let mut pos = self.cursor;
while pos < bytes.len() && !bytes[pos].is_ascii_whitespace() {
pos += 1;
}
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
self.cursor = pos;
}
fn move_word_backward(&mut self, text: &str) {
let bytes = text.as_bytes();
let mut pos = self.cursor;
while pos > 0 && bytes[pos - 1].is_ascii_whitespace() {
pos -= 1;
}
while pos > 0 && !bytes[pos - 1].is_ascii_whitespace() {
pos -= 1;
}
self.cursor = pos;
}
fn move_word_end(&mut self, text: &str) {
let bytes = text.as_bytes();
let mut pos = self.cursor;
if pos < bytes.len() {
pos += 1;
}
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
while pos < bytes.len() && !bytes[pos].is_ascii_whitespace() {
pos += 1;
}
if pos > self.cursor + 1 {
pos -= 1;
}
self.cursor = pos;
}
fn move_up(&mut self, text: &str) {
let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
if line_start == 0 {
return;
}
let col = self.cursor - line_start;
let prev_line_start = text[..line_start - 1]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0);
let prev_line_end = line_start - 1; let prev_line_len = prev_line_end - prev_line_start;
self.cursor = prev_line_start + col.min(prev_line_len);
}
fn move_down(&mut self, text: &str) {
let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
let col = self.cursor - line_start;
let Some(newline_pos) = text[self.cursor..].find('\n') else {
return;
};
let next_line_start = self.cursor + newline_pos + 1;
if next_line_start >= text.len() {
self.cursor = text.len();
return;
}
let next_line_end = text[next_line_start..]
.find('\n')
.map(|i| next_line_start + i)
.unwrap_or(text.len());
let next_line_len = next_line_end - next_line_start;
self.cursor = next_line_start + col.min(next_line_len);
}
fn move_to_matching_bracket(&mut self, text: &str) {
if self.cursor >= text.len() {
return;
}
let char_at_cursor = text[self.cursor..].chars().next();
let c = match char_at_cursor {
Some(c) => c,
None => return,
};
let pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
for (open, close) in pairs.iter() {
if c == *open {
if let Some(pos) = self.find_matching_forward(text, *open, *close) {
self.cursor = pos;
}
return;
}
if c == *close {
if let Some(pos) = self.find_matching_backward(text, *open, *close) {
self.cursor = pos;
}
return;
}
}
}
fn find_matching_forward(&self, text: &str, open: char, close: char) -> Option<usize> {
let mut depth = 1;
let mut pos = self.cursor;
pos += open.len_utf8();
for (i, c) in text[pos..].char_indices() {
if c == open {
depth += 1;
} else if c == close {
depth -= 1;
if depth == 0 {
return Some(pos + i);
}
}
}
None
}
fn find_matching_backward(&self, text: &str, open: char, close: char) -> Option<usize> {
let mut depth = 1;
let search_text = &text[..self.cursor];
for (i, c) in search_text.char_indices().rev() {
if c == close {
depth += 1;
} else if c == open {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
}
None
}
fn delete_char(&mut self, text: &str) -> EditResult {
if self.cursor >= text.len() {
return EditResult::none();
}
let start = self.cursor;
let mut end = self.cursor + 1;
while end < text.len() && !text.is_char_boundary(end) {
end += 1;
}
let deleted = text[start..end].to_string();
if end >= text.len() && self.cursor > 0 {
self.move_left(text);
}
EditResult::edit_and_yank(TextEdit::Delete { start, end }, deleted)
}
fn delete_to_end(&mut self, text: &str) -> EditResult {
let end = text[self.cursor..]
.find('\n')
.map(|i| self.cursor + i)
.unwrap_or(text.len());
if self.cursor >= end {
return EditResult::none();
}
let deleted = text[self.cursor..end].to_string();
EditResult::edit_and_yank(
TextEdit::Delete {
start: self.cursor,
end,
},
deleted,
)
}
fn delete_line(&mut self, text: &str) -> EditResult {
let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
let line_end = text[self.cursor..]
.find('\n')
.map(|i| self.cursor + i + 1) .unwrap_or(text.len());
let (start, end) = if line_start == 0 && line_end == text.len() {
(0, text.len())
} else if line_end == text.len() && line_start > 0 {
(line_start - 1, text.len())
} else {
(line_start, line_end)
};
let deleted = text[start..end].to_string();
self.cursor = start;
EditResult::edit_and_yank(TextEdit::Delete { start, end }, deleted)
}
fn paste_after(&mut self, text: &str) -> EditResult {
if self.yank_buffer.is_empty() {
return EditResult::none();
}
let insert_pos = (self.cursor + 1).min(text.len());
let to_insert = self.yank_buffer.clone();
self.cursor = (insert_pos + to_insert.len())
.saturating_sub(1)
.min(text.len() + to_insert.len());
EditResult::edit(TextEdit::Insert {
at: insert_pos,
text: to_insert,
})
}
fn paste_before(&mut self, text: &str) -> EditResult {
if self.yank_buffer.is_empty() {
return EditResult::none();
}
let to_insert = self.yank_buffer.clone();
let insert_pos = self.cursor.min(text.len());
self.cursor = (insert_pos + to_insert.len()).min(text.len() + to_insert.len());
EditResult::edit(TextEdit::Insert {
at: insert_pos,
text: to_insert,
})
}
fn handle_normal(&mut self, key: Key, text: &str) -> EditResult {
match key.code {
KeyCode::Char('i') => {
self.mode = Mode::Insert;
EditResult::none()
}
KeyCode::Char('a') => {
self.mode = Mode::Insert;
self.move_right(text);
EditResult::none()
}
KeyCode::Char('A') => {
self.mode = Mode::Insert;
self.move_line_end_insert(text);
EditResult::none()
}
KeyCode::Char('I') => {
self.mode = Mode::Insert;
self.move_first_non_blank(text);
EditResult::none()
}
KeyCode::Char('o') => {
self.mode = Mode::Insert;
self.move_line_end(text);
let pos = self.cursor;
self.cursor = pos + 1;
EditResult::edit(TextEdit::Insert {
at: pos,
text: "\n".to_string(),
})
}
KeyCode::Char('O') => {
self.mode = Mode::Insert;
self.move_line_start(text);
let pos = self.cursor;
EditResult::edit(TextEdit::Insert {
at: pos,
text: "\n".to_string(),
})
}
KeyCode::Char('v') => {
self.mode = Mode::Visual;
self.visual_anchor = Some(self.cursor);
EditResult::none()
}
KeyCode::Char('h') | KeyCode::Left => {
self.move_left(text);
EditResult::cursor_only()
}
KeyCode::Char('l') | KeyCode::Right => {
self.move_right(text);
EditResult::cursor_only()
}
KeyCode::Char('j') => {
self.move_down(text);
EditResult::cursor_only()
}
KeyCode::Char('k') => {
self.move_up(text);
EditResult::cursor_only()
}
KeyCode::Char('0') | KeyCode::Home => {
self.move_line_start(text);
EditResult::cursor_only()
}
KeyCode::Char('^') => {
self.move_first_non_blank(text);
EditResult::cursor_only()
}
KeyCode::Char('$') | KeyCode::End => {
self.move_line_end(text);
EditResult::cursor_only()
}
KeyCode::Char('w') => {
self.move_word_forward(text);
EditResult::cursor_only()
}
KeyCode::Char('b') => {
self.move_word_backward(text);
EditResult::cursor_only()
}
KeyCode::Char('e') => {
self.move_word_end(text);
EditResult::cursor_only()
}
KeyCode::Char('%') => {
self.move_to_matching_bracket(text);
EditResult::cursor_only()
}
KeyCode::Char('c') if key.ctrl => EditResult::action(Action::Cancel),
KeyCode::Char('d') => {
self.mode = Mode::OperatorPending(Operator::Delete);
EditResult::none()
}
KeyCode::Char('c') => {
self.mode = Mode::OperatorPending(Operator::Change);
EditResult::none()
}
KeyCode::Char('y') => {
self.mode = Mode::OperatorPending(Operator::Yank);
EditResult::none()
}
KeyCode::Char('x') => self.delete_char(text),
KeyCode::Char('D') => self.delete_to_end(text),
KeyCode::Char('C') => {
self.mode = Mode::Insert;
self.delete_to_end(text)
}
KeyCode::Char('r') => {
self.mode = Mode::ReplaceChar;
EditResult::none()
}
KeyCode::Char('p') => self.paste_after(text),
KeyCode::Char('P') => self.paste_before(text),
KeyCode::Up => EditResult::action(Action::HistoryPrev),
KeyCode::Down => EditResult::action(Action::HistoryNext),
KeyCode::Enter if !key.shift => EditResult::action(Action::Submit),
KeyCode::Enter if key.shift => {
self.mode = Mode::Insert;
let pos = self.cursor;
self.cursor = pos + 1;
EditResult::edit(TextEdit::Insert {
at: pos,
text: "\n".to_string(),
})
}
KeyCode::Escape => EditResult::none(),
_ => EditResult::none(),
}
}
fn handle_insert(&mut self, key: Key, text: &str) -> EditResult {
match key.code {
KeyCode::Escape => {
self.mode = Mode::Normal;
if self.cursor > 0 {
self.move_left(text);
}
EditResult::none()
}
KeyCode::Char('c') if key.ctrl => {
self.mode = Mode::Normal;
EditResult::none()
}
KeyCode::Char(c) if !key.ctrl && !key.alt => {
let pos = self.cursor;
self.cursor = pos + c.len_utf8();
EditResult::edit(TextEdit::Insert {
at: pos,
text: c.to_string(),
})
}
KeyCode::Backspace => {
if self.cursor == 0 {
return EditResult::none();
}
let mut start = self.cursor - 1;
while start > 0 && !text.is_char_boundary(start) {
start -= 1;
}
let end = self.cursor; self.cursor = start;
EditResult::edit(TextEdit::Delete { start, end })
}
KeyCode::Delete => self.delete_char(text),
KeyCode::Left => {
self.move_left(text);
EditResult::cursor_only()
}
KeyCode::Right => {
self.move_right(text);
EditResult::cursor_only()
}
KeyCode::Up => {
self.move_up(text);
EditResult::cursor_only()
}
KeyCode::Down => {
self.move_down(text);
EditResult::cursor_only()
}
KeyCode::Home => {
self.move_line_start(text);
EditResult::cursor_only()
}
KeyCode::End => {
self.move_line_end_insert(text);
EditResult::cursor_only()
}
KeyCode::Enter => {
let pos = self.cursor;
self.cursor = pos + 1;
EditResult::edit(TextEdit::Insert {
at: pos,
text: "\n".to_string(),
})
}
_ => EditResult::none(),
}
}
fn handle_operator_pending(&mut self, op: Operator, key: Key, text: &str) -> EditResult {
if key.code == KeyCode::Escape {
self.mode = Mode::Normal;
return EditResult::none();
}
let is_line_op = matches!(
(op, key.code),
(Operator::Delete, KeyCode::Char('d'))
| (Operator::Change, KeyCode::Char('c'))
| (Operator::Yank, KeyCode::Char('y'))
);
if is_line_op {
self.mode = Mode::Normal;
return self.apply_operator_line(op, text);
}
let start = self.cursor;
match key.code {
KeyCode::Char('w') => {
if op == Operator::Change {
self.move_word_end(text);
if self.cursor < text.len() {
self.cursor += 1;
}
} else {
self.move_word_forward(text);
}
}
KeyCode::Char('b') => self.move_word_backward(text),
KeyCode::Char('e') => {
self.move_word_end(text);
if self.cursor < text.len() {
self.cursor += 1;
}
}
KeyCode::Char('0') | KeyCode::Home => self.move_line_start(text),
KeyCode::Char('$') | KeyCode::End => self.move_line_end(text),
KeyCode::Char('^') => self.move_first_non_blank(text),
KeyCode::Char('h') | KeyCode::Left => self.move_left(text),
KeyCode::Char('l') | KeyCode::Right => self.move_right(text),
KeyCode::Char('j') => self.move_down(text),
KeyCode::Char('k') => self.move_up(text),
_ => {
self.mode = Mode::Normal;
return EditResult::none();
}
}
let end = self.cursor;
self.mode = Mode::Normal;
if start == end {
return EditResult::none();
}
let (range_start, range_end) = if start < end {
(start, end)
} else {
(end, start)
};
self.apply_operator(op, range_start, range_end, text)
}
fn apply_operator(&mut self, op: Operator, start: usize, end: usize, text: &str) -> EditResult {
let affected = text[start..end].to_string();
self.yank_buffer = affected.clone();
self.cursor = start;
match op {
Operator::Delete => {
EditResult::edit_and_yank(TextEdit::Delete { start, end }, affected)
}
Operator::Change => {
self.mode = Mode::Insert;
EditResult::edit_and_yank(TextEdit::Delete { start, end }, affected)
}
Operator::Yank => {
EditResult {
yanked: Some(affected),
..Default::default()
}
}
}
}
fn apply_operator_line(&mut self, op: Operator, text: &str) -> EditResult {
match op {
Operator::Delete => self.delete_line(text),
Operator::Change => {
let result = self.delete_line(text);
self.mode = Mode::Insert;
result
}
Operator::Yank => {
let line_start = text[..self.cursor].rfind('\n').map(|i| i + 1).unwrap_or(0);
let line_end = text[self.cursor..]
.find('\n')
.map(|i| self.cursor + i + 1)
.unwrap_or(text.len());
let line = text[line_start..line_end].to_string();
self.yank_buffer = line.clone();
EditResult {
yanked: Some(line),
..Default::default()
}
}
}
}
fn handle_visual(&mut self, key: Key, text: &str) -> EditResult {
match key.code {
KeyCode::Escape => {
self.mode = Mode::Normal;
self.visual_anchor = None;
EditResult::none()
}
KeyCode::Char('h') | KeyCode::Left => {
self.move_left(text);
EditResult::cursor_only()
}
KeyCode::Char('l') | KeyCode::Right => {
self.move_right(text);
EditResult::cursor_only()
}
KeyCode::Char('j') => {
self.move_down(text);
EditResult::cursor_only()
}
KeyCode::Char('k') => {
self.move_up(text);
EditResult::cursor_only()
}
KeyCode::Char('w') => {
self.move_word_forward(text);
EditResult::cursor_only()
}
KeyCode::Char('b') => {
self.move_word_backward(text);
EditResult::cursor_only()
}
KeyCode::Char('e') => {
self.move_word_end(text);
EditResult::cursor_only()
}
KeyCode::Char('0') | KeyCode::Home => {
self.move_line_start(text);
EditResult::cursor_only()
}
KeyCode::Char('$') | KeyCode::End => {
self.move_line_end(text);
EditResult::cursor_only()
}
KeyCode::Char('d') | KeyCode::Char('x') => {
let (start, end) = self.selection_range();
self.mode = Mode::Normal;
self.visual_anchor = None;
self.apply_operator(Operator::Delete, start, end, text)
}
KeyCode::Char('c') => {
let (start, end) = self.selection_range();
self.mode = Mode::Normal;
self.visual_anchor = None;
self.apply_operator(Operator::Change, start, end, text)
}
KeyCode::Char('y') => {
let (start, end) = self.selection_range();
self.mode = Mode::Normal;
self.visual_anchor = None;
self.apply_operator(Operator::Yank, start, end, text)
}
_ => EditResult::none(),
}
}
fn handle_replace_char(&mut self, key: Key, text: &str) -> EditResult {
self.mode = Mode::Normal;
match key.code {
KeyCode::Escape => EditResult::none(),
KeyCode::Char(c) if !key.ctrl && !key.alt => {
if self.cursor >= text.len() {
return EditResult::none();
}
let mut end = self.cursor + 1;
while end < text.len() && !text.is_char_boundary(end) {
end += 1;
}
EditResult {
edits: vec![
TextEdit::Insert {
at: self.cursor,
text: c.to_string(),
},
TextEdit::Delete {
start: self.cursor,
end,
},
],
..Default::default()
}
}
_ => EditResult::none(),
}
}
fn selection_range(&self) -> (usize, usize) {
let anchor = self.visual_anchor.unwrap_or(self.cursor);
if self.cursor < anchor {
(self.cursor, anchor)
} else {
(anchor, self.cursor + 1) }
}
}
impl LineEditor for VimLineEditor {
fn handle_key(&mut self, key: Key, text: &str) -> EditResult {
self.clamp_cursor(text);
let result = match self.mode {
Mode::Normal => self.handle_normal(key, text),
Mode::Insert => self.handle_insert(key, text),
Mode::OperatorPending(op) => self.handle_operator_pending(op, key, text),
Mode::Visual => self.handle_visual(key, text),
Mode::ReplaceChar => self.handle_replace_char(key, text),
};
if let Some(ref yanked) = result.yanked {
self.yank_buffer = yanked.clone();
}
result
}
fn cursor(&self) -> usize {
self.cursor
}
fn status(&self) -> &str {
match self.mode {
Mode::Normal => "NORMAL",
Mode::Insert => "INSERT",
Mode::OperatorPending(Operator::Delete) => "d...",
Mode::OperatorPending(Operator::Change) => "c...",
Mode::OperatorPending(Operator::Yank) => "y...",
Mode::Visual => "VISUAL",
Mode::ReplaceChar => "r...",
}
}
fn selection(&self) -> Option<Range<usize>> {
if self.mode == Mode::Visual {
let (start, end) = self.selection_range();
Some(start..end)
} else {
None
}
}
fn reset(&mut self) {
self.cursor = 0;
self.mode = Mode::Normal;
self.visual_anchor = None;
}
fn set_cursor(&mut self, pos: usize, text: &str) {
let pos = pos.min(text.len());
self.cursor = if text.is_char_boundary(pos) {
pos
} else {
let mut p = pos;
while p > 0 && !text.is_char_boundary(p) {
p -= 1;
}
p
};
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_motion() {
let mut editor = VimLineEditor::new();
let text = "hello world";
editor.handle_key(Key::char('l'), text);
assert_eq!(editor.cursor(), 1);
editor.handle_key(Key::char('w'), text);
assert_eq!(editor.cursor(), 6);
editor.handle_key(Key::char('$'), text);
assert_eq!(editor.cursor(), 10);
editor.handle_key(Key::char('0'), text);
assert_eq!(editor.cursor(), 0);
}
#[test]
fn test_mode_switching() {
let mut editor = VimLineEditor::new();
let text = "hello";
assert_eq!(editor.mode(), Mode::Normal);
editor.handle_key(Key::char('i'), text);
assert_eq!(editor.mode(), Mode::Insert);
editor.handle_key(Key::code(KeyCode::Escape), text);
assert_eq!(editor.mode(), Mode::Normal);
}
#[test]
fn test_delete_word() {
let mut editor = VimLineEditor::new();
let text = "hello world";
editor.handle_key(Key::char('d'), text);
editor.handle_key(Key::char('w'), text);
assert_eq!(editor.mode(), Mode::Normal);
}
#[test]
fn test_insert_char() {
let mut editor = VimLineEditor::new();
let text = "";
editor.handle_key(Key::char('i'), text);
let result = editor.handle_key(Key::char('x'), text);
assert_eq!(result.edits.len(), 1);
match &result.edits[0] {
TextEdit::Insert { at, text } => {
assert_eq!(*at, 0);
assert_eq!(text, "x");
}
_ => panic!("Expected Insert"),
}
}
#[test]
fn test_visual_mode() {
let mut editor = VimLineEditor::new();
let text = "hello world";
editor.handle_key(Key::char('v'), text);
assert_eq!(editor.mode(), Mode::Visual);
editor.handle_key(Key::char('w'), text);
let sel = editor.selection().unwrap();
assert_eq!(sel.start, 0);
assert!(sel.end > 0);
}
#[test]
fn test_backspace_ascii() {
let mut editor = VimLineEditor::new();
let mut text = String::from("abc");
editor.handle_key(Key::char('i'), &text);
editor.handle_key(Key::code(KeyCode::End), &text);
assert_eq!(editor.cursor(), 3);
let result = editor.handle_key(Key::code(KeyCode::Backspace), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "ab");
assert_eq!(editor.cursor(), 2);
}
#[test]
fn test_backspace_unicode() {
let mut editor = VimLineEditor::new();
let mut text = String::from("a😀b");
editor.handle_key(Key::char('i'), &text);
editor.handle_key(Key::code(KeyCode::End), &text);
editor.handle_key(Key::code(KeyCode::Left), &text); assert_eq!(editor.cursor(), 5);
let result = editor.handle_key(Key::code(KeyCode::Backspace), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "ab");
assert_eq!(editor.cursor(), 1);
}
#[test]
fn test_yank_and_paste() {
let mut editor = VimLineEditor::new();
let mut text = String::from("hello world");
editor.handle_key(Key::char('y'), &text);
let result = editor.handle_key(Key::char('w'), &text);
assert!(result.yanked.is_some());
assert_eq!(result.yanked.unwrap(), "hello ");
editor.handle_key(Key::char('$'), &text);
let result = editor.handle_key(Key::char('p'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "hello worldhello ");
}
#[test]
fn test_visual_mode_delete() {
let mut editor = VimLineEditor::new();
let mut text = String::from("hello world");
editor.handle_key(Key::char('v'), &text);
assert_eq!(editor.mode(), Mode::Visual);
editor.handle_key(Key::char('e'), &text);
let result = editor.handle_key(Key::char('d'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, " world");
assert_eq!(editor.mode(), Mode::Normal);
}
#[test]
fn test_operator_pending_escape() {
let mut editor = VimLineEditor::new();
let text = "hello world";
editor.handle_key(Key::char('d'), text);
assert!(matches!(editor.mode(), Mode::OperatorPending(_)));
editor.handle_key(Key::code(KeyCode::Escape), text);
assert_eq!(editor.mode(), Mode::Normal);
}
#[test]
fn test_replace_char() {
let mut editor = VimLineEditor::new();
let mut text = String::from("hello");
editor.handle_key(Key::char('r'), &text);
assert_eq!(editor.mode(), Mode::ReplaceChar);
let result = editor.handle_key(Key::char('x'), &text);
assert_eq!(editor.mode(), Mode::Normal);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "xello");
}
#[test]
fn test_replace_char_escape() {
let mut editor = VimLineEditor::new();
let text = "hello";
editor.handle_key(Key::char('r'), text);
assert_eq!(editor.mode(), Mode::ReplaceChar);
editor.handle_key(Key::code(KeyCode::Escape), text);
assert_eq!(editor.mode(), Mode::Normal);
}
#[test]
fn test_cw_no_trailing_space() {
let mut editor = VimLineEditor::new();
let mut text = String::from("hello world");
editor.handle_key(Key::char('c'), &text);
let result = editor.handle_key(Key::char('w'), &text);
assert_eq!(editor.mode(), Mode::Insert);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, " world");
}
#[test]
fn test_dw_includes_trailing_space() {
let mut editor = VimLineEditor::new();
let mut text = String::from("hello world");
editor.handle_key(Key::char('d'), &text);
let result = editor.handle_key(Key::char('w'), &text);
assert_eq!(editor.mode(), Mode::Normal);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "world");
}
#[test]
fn test_paste_at_empty_buffer() {
let mut editor = VimLineEditor::new();
let yank_text = String::from("test");
editor.handle_key(Key::char('y'), &yank_text);
editor.handle_key(Key::char('w'), &yank_text);
let mut text = String::new();
editor.set_cursor(0, &text);
let result = editor.handle_key(Key::char('p'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "test");
}
#[test]
fn test_dollar_cursor_on_last_char() {
let mut editor = VimLineEditor::new();
let text = "abc";
editor.handle_key(Key::char('$'), text);
assert_eq!(editor.cursor(), 2);
let text = "x";
editor.set_cursor(0, text);
editor.handle_key(Key::char('$'), text);
assert_eq!(editor.cursor(), 0); }
#[test]
fn test_x_delete_last_char_moves_cursor_left() {
let mut editor = VimLineEditor::new();
let mut text = String::from("abc");
editor.handle_key(Key::char('$'), &text);
assert_eq!(editor.cursor(), 2);
let result = editor.handle_key(Key::char('x'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "ab");
assert_eq!(editor.cursor(), 1); }
#[test]
fn test_x_delete_middle_char_cursor_stays() {
let mut editor = VimLineEditor::new();
let mut text = String::from("abc");
editor.handle_key(Key::char('l'), &text);
assert_eq!(editor.cursor(), 1);
let result = editor.handle_key(Key::char('x'), &text);
for edit in result.edits.into_iter().rev() {
edit.apply(&mut text);
}
assert_eq!(text, "ac");
assert_eq!(editor.cursor(), 1);
}
#[test]
fn test_percent_bracket_matching() {
let mut editor = VimLineEditor::new();
let text = "(hello world)";
assert_eq!(editor.cursor(), 0);
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 12);
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 0);
}
#[test]
fn test_percent_nested_brackets() {
let mut editor = VimLineEditor::new();
let text = "([{<>}])";
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 7);
editor.set_cursor(1, text);
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 6);
editor.set_cursor(2, text);
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 5);
editor.set_cursor(3, text);
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), 4); }
#[test]
fn test_percent_on_non_bracket() {
let mut editor = VimLineEditor::new();
let text = "hello";
let orig_cursor = editor.cursor();
editor.handle_key(Key::char('%'), text);
assert_eq!(editor.cursor(), orig_cursor);
}
}