implemented some more editor tests
This commit is contained in:
@@ -2,7 +2,7 @@ use std::{ops::{Range, RangeBounds, RangeInclusive}, string::Drain};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::{term::Layout, vicmd::{Direction, Motion, MotionBehavior, RegisterName, To, Verb, ViCmd, Word}};
|
||||
use super::{term::Layout, vicmd::{Direction, Motion, MotionBehavior, MotionCmd, RegisterName, To, Verb, ViCmd, Word}};
|
||||
use crate::{libsh::error::ShResult, prelude::*};
|
||||
|
||||
#[derive(PartialEq,Eq,Debug,Clone,Copy)]
|
||||
@@ -15,18 +15,26 @@ pub enum CharClass {
|
||||
|
||||
impl From<&str> for CharClass {
|
||||
fn from(value: &str) -> Self {
|
||||
if value.len() > 1 {
|
||||
return Self::Symbol // Multi-byte grapheme
|
||||
let mut chars = value.chars();
|
||||
|
||||
// Empty string fallback
|
||||
let Some(first) = chars.next() else {
|
||||
return Self::Other;
|
||||
};
|
||||
|
||||
if first.is_alphanumeric() && chars.all(|c| c.is_ascii_punctuation() || c == '\u{0301}' || c == '\u{0308}') {
|
||||
// Handles things like `ï`, `é`, etc., by manually allowing common diacritics
|
||||
return CharClass::Alphanum;
|
||||
}
|
||||
|
||||
if value.chars().all(char::is_alphanumeric) {
|
||||
CharClass::Alphanum
|
||||
} else if value.chars().all(char::is_whitespace) {
|
||||
CharClass::Whitespace
|
||||
} else if !value.chars().all(char::is_alphanumeric) {
|
||||
} else if value.chars().all(|c| !c.is_alphanumeric()) {
|
||||
CharClass::Symbol
|
||||
} else {
|
||||
Self::Other
|
||||
CharClass::Other
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +87,7 @@ pub enum SelectMode {
|
||||
pub enum MotionKind {
|
||||
To(usize), // Absolute position, exclusive
|
||||
On(usize), // Absolute position, inclusive
|
||||
Onto(usize), // Absolute position, operations include the position but motions exclude it (wtf vim)
|
||||
Inclusive((usize,usize)), // Range, inclusive
|
||||
Exclusive((usize,usize)), // Range, exclusive
|
||||
Null
|
||||
@@ -552,39 +561,45 @@ impl LineBuf {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch_word_motion(&mut self, to: To, word: Word, dir: Direction) -> usize {
|
||||
pub fn dispatch_word_motion(&mut self, count: usize, to: To, word: Word, dir: Direction) -> usize {
|
||||
// Not sorry for these method names btw
|
||||
match to {
|
||||
To::Start => {
|
||||
match dir {
|
||||
Direction::Forward => self.start_of_word_forward_or_end_of_word_backward(word, dir),
|
||||
Direction::Backward => self.end_of_word_forward_or_start_of_word_backward(word, dir)
|
||||
let mut pos = ClampedUsize::new(self.cursor.get(), self.cursor.max, false);
|
||||
for _ in 0..count {
|
||||
pos.set(match to {
|
||||
To::Start => {
|
||||
match dir {
|
||||
Direction::Forward => self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir),
|
||||
Direction::Backward => self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
To::End => {
|
||||
match dir {
|
||||
Direction::Forward => self.end_of_word_forward_or_start_of_word_backward(word, dir),
|
||||
Direction::Backward => self.start_of_word_forward_or_end_of_word_backward(word, dir),
|
||||
To::End => {
|
||||
match dir {
|
||||
Direction::Forward => self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir),
|
||||
Direction::Backward => self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pos.get()
|
||||
}
|
||||
|
||||
/// Finds the start of a word forward, or the end of a word backward
|
||||
///
|
||||
/// Finding the start of a word in the forward direction, and finding the end of a word in the backward direction
|
||||
/// are logically the same operation, if you use a reversed iterator for the backward motion.
|
||||
pub fn start_of_word_forward_or_end_of_word_backward(&mut self, word: Word, dir: Direction) -> usize {
|
||||
let mut pos = self.cursor.get();
|
||||
pub fn start_of_word_forward_or_end_of_word_backward_from(&mut self, mut pos: usize, word: Word, dir: Direction) -> usize {
|
||||
let default = match dir {
|
||||
Direction::Backward => 0,
|
||||
Direction::Forward => self.grapheme_indices().len()
|
||||
};
|
||||
let mut indices_iter = self.directional_indices_iter(dir).peekable(); // And make it peekable
|
||||
let mut indices_iter = self.directional_indices_iter_from(pos,dir).peekable(); // And make it peekable
|
||||
|
||||
match word {
|
||||
Word::Big => {
|
||||
let on_boundary = self.grapheme_at(pos).is_none_or(is_whitespace);
|
||||
let Some(next) = indices_iter.peek() else {
|
||||
return default
|
||||
};
|
||||
let on_boundary = self.grapheme_at(*next).is_none_or(is_whitespace);
|
||||
flog!(DEBUG,on_boundary);
|
||||
flog!(DEBUG,pos);
|
||||
if on_boundary {
|
||||
@@ -613,16 +628,18 @@ impl LineBuf {
|
||||
}
|
||||
Word::Normal => {
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default };
|
||||
let Some(next_idx) = indices_iter.next() else { return default };
|
||||
let on_boundary = self.grapheme_at(next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char));
|
||||
let Some(next_idx) = indices_iter.peek() else { return default };
|
||||
let on_boundary = !is_whitespace(&cur_char) && self.grapheme_at(*next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char));
|
||||
flog!(DEBUG,on_boundary);
|
||||
if on_boundary {
|
||||
pos = next_idx
|
||||
pos = *next_idx
|
||||
}
|
||||
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default
|
||||
};
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
flog!(DEBUG,on_whitespace);
|
||||
|
||||
// Advance until hitting whitespace or a different character class
|
||||
if !on_whitespace {
|
||||
@@ -648,6 +665,7 @@ impl LineBuf {
|
||||
.is_some_and(|c| !is_whitespace(c))
|
||||
}
|
||||
).unwrap_or(default);
|
||||
flog!(DEBUG,self.grapheme_at(non_ws_pos));
|
||||
non_ws_pos
|
||||
}
|
||||
}
|
||||
@@ -656,24 +674,22 @@ impl LineBuf {
|
||||
///
|
||||
/// Finding the end of a word in the forward direction, and finding the start of a word in the backward direction
|
||||
/// are logically the same operation, if you use a reversed iterator for the backward motion.
|
||||
pub fn end_of_word_forward_or_start_of_word_backward(&mut self, word: Word, dir: Direction) -> usize {
|
||||
let mut pos = self.cursor.get();
|
||||
pub fn end_of_word_forward_or_start_of_word_backward_from(&mut self, mut pos: usize, word: Word, dir: Direction) -> usize {
|
||||
let default = match dir {
|
||||
Direction::Backward => 0,
|
||||
Direction::Forward => self.grapheme_indices().len()
|
||||
};
|
||||
|
||||
let mut indices_iter = self.directional_indices_iter(dir).peekable();
|
||||
let mut indices_iter = self.directional_indices_iter_from(pos,dir).peekable();
|
||||
|
||||
match word {
|
||||
Word::Big => {
|
||||
let Some(next_idx) = indices_iter.next() else { return default };
|
||||
let on_boundary = self.grapheme_at(next_idx).is_none_or(is_whitespace);
|
||||
let Some(next_idx) = indices_iter.peek() else { return default };
|
||||
let on_boundary = self.grapheme_at(*next_idx).is_none_or(is_whitespace);
|
||||
if on_boundary {
|
||||
let Some(idx) = indices_iter.next() else { return default };
|
||||
pos = idx;
|
||||
}
|
||||
|
||||
// Check current grapheme
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||
return default
|
||||
@@ -706,9 +722,10 @@ impl LineBuf {
|
||||
}
|
||||
Word::Normal => {
|
||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default };
|
||||
let Some(next_idx) = indices_iter.next() else { return default };
|
||||
let on_boundary = self.grapheme_at(next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char));
|
||||
let Some(next_idx) = indices_iter.peek() else { return default };
|
||||
let on_boundary = !is_whitespace(&cur_char) && self.grapheme_at(*next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char));
|
||||
if on_boundary {
|
||||
let next_idx = indices_iter.next().unwrap();
|
||||
pos = next_idx
|
||||
}
|
||||
|
||||
@@ -800,7 +817,7 @@ impl LineBuf {
|
||||
pub fn find<F: Fn(&str) -> bool>(&mut self, op: F) -> usize {
|
||||
self.find_from(self.cursor.get(), op)
|
||||
}
|
||||
pub fn eval_motion(&mut self, motion: Motion) -> MotionKind {
|
||||
pub fn eval_motion(&mut self, motion: MotionCmd) -> MotionKind {
|
||||
let buffer = self.buffer.clone();
|
||||
if self.has_hint() {
|
||||
let hint = self.hint.clone().unwrap();
|
||||
@@ -808,14 +825,35 @@ impl LineBuf {
|
||||
}
|
||||
|
||||
let eval = match motion {
|
||||
Motion::WholeLine => {
|
||||
let (start,end) = self.this_line();
|
||||
MotionCmd(count,Motion::WholeLine) => {
|
||||
let start = self.start_of_cursor_line();
|
||||
let end = self.find_newlines(count);
|
||||
MotionKind::Inclusive((start,end))
|
||||
}
|
||||
Motion::WordMotion(to, word, dir) => MotionKind::On(self.dispatch_word_motion(to, word, dir)),
|
||||
Motion::TextObj(text_obj, bound) => todo!(),
|
||||
Motion::EndOfLastWord => {
|
||||
MotionCmd(count,Motion::WordMotion(to, word, dir)) => {
|
||||
let pos = self.dispatch_word_motion(count, to, word, dir);
|
||||
let mut pos = ClampedUsize::new(pos,self.cursor.max,false);
|
||||
// End-based operations must include the last character
|
||||
// But the cursor must also stop just before it when moving
|
||||
// So we have to do some weird shit to reconcile this behavior
|
||||
if to == To::End {
|
||||
match dir {
|
||||
Direction::Forward => {
|
||||
MotionKind::Onto(pos.get())
|
||||
}
|
||||
Direction::Backward => {
|
||||
let (start,end) = ordered(self.cursor.get(),pos.get());
|
||||
MotionKind::Inclusive((start,end))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MotionKind::On(pos.get())
|
||||
}
|
||||
}
|
||||
MotionCmd(count,Motion::TextObj(text_obj, bound)) => todo!(),
|
||||
MotionCmd(count,Motion::EndOfLastWord) => {
|
||||
let start = self.start_of_cursor_line();
|
||||
let mut newline_count = 0;
|
||||
let mut indices = self.directional_indices_iter_from(start,Direction::Forward);
|
||||
let mut last_graphical = None;
|
||||
while let Some(idx) = indices.next() {
|
||||
@@ -824,7 +862,10 @@ impl LineBuf {
|
||||
last_graphical = Some(idx);
|
||||
}
|
||||
if grapheme == "\n" {
|
||||
break
|
||||
newline_count += 1;
|
||||
if newline_count == count {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(last) = last_graphical else {
|
||||
@@ -832,7 +873,7 @@ impl LineBuf {
|
||||
};
|
||||
MotionKind::On(last)
|
||||
}
|
||||
Motion::BeginningOfFirstWord => {
|
||||
MotionCmd(_,Motion::BeginningOfFirstWord) => {
|
||||
let start = self.start_of_cursor_line();
|
||||
let mut indices = self.directional_indices_iter_from(start,Direction::Forward);
|
||||
let mut first_graphical = None;
|
||||
@@ -852,35 +893,38 @@ impl LineBuf {
|
||||
};
|
||||
MotionKind::On(first)
|
||||
}
|
||||
Motion::BeginningOfLine => MotionKind::On(self.start_of_cursor_line()),
|
||||
Motion::EndOfLine => MotionKind::On(self.end_of_cursor_line()),
|
||||
Motion::CharSearch(direction, dest, ch) => todo!(),
|
||||
Motion::BackwardChar => MotionKind::On(self.cursor.ret_sub(1)),
|
||||
Motion::ForwardChar => MotionKind::On(self.cursor.ret_add_inclusive(1)),
|
||||
Motion::LineUp => todo!(),
|
||||
Motion::LineUpCharwise => todo!(),
|
||||
Motion::ScreenLineUp => todo!(),
|
||||
Motion::ScreenLineUpCharwise => todo!(),
|
||||
Motion::LineDown => todo!(),
|
||||
Motion::LineDownCharwise => todo!(),
|
||||
Motion::ScreenLineDown => todo!(),
|
||||
Motion::ScreenLineDownCharwise => todo!(),
|
||||
Motion::BeginningOfScreenLine => todo!(),
|
||||
Motion::FirstGraphicalOnScreenLine => todo!(),
|
||||
Motion::HalfOfScreen => todo!(),
|
||||
Motion::HalfOfScreenLineText => todo!(),
|
||||
Motion::WholeBuffer => todo!(),
|
||||
Motion::BeginningOfBuffer => todo!(),
|
||||
Motion::EndOfBuffer => todo!(),
|
||||
Motion::ToColumn(col) => todo!(),
|
||||
Motion::ToDelimMatch => todo!(),
|
||||
Motion::ToBrace(direction) => todo!(),
|
||||
Motion::ToBracket(direction) => todo!(),
|
||||
Motion::ToParen(direction) => todo!(),
|
||||
Motion::Range(start, end) => todo!(),
|
||||
Motion::RepeatMotion => todo!(),
|
||||
Motion::RepeatMotionRev => todo!(),
|
||||
Motion::Null => MotionKind::Null
|
||||
MotionCmd(_,Motion::BeginningOfLine) => MotionKind::On(self.start_of_cursor_line()),
|
||||
MotionCmd(count,Motion::EndOfLine) => {
|
||||
let pos = self.find_newlines(count);
|
||||
MotionKind::On(pos)
|
||||
}
|
||||
MotionCmd(count,Motion::CharSearch(direction, dest, ch)) => todo!(),
|
||||
MotionCmd(count,Motion::BackwardChar) => MotionKind::On(self.cursor.ret_sub(1)),
|
||||
MotionCmd(count,Motion::ForwardChar) => MotionKind::On(self.cursor.ret_add_inclusive(1)),
|
||||
MotionCmd(count,Motion::LineUp) => todo!(),
|
||||
MotionCmd(count,Motion::LineUpCharwise) => todo!(),
|
||||
MotionCmd(count,Motion::ScreenLineUp) => todo!(),
|
||||
MotionCmd(count,Motion::ScreenLineUpCharwise) => todo!(),
|
||||
MotionCmd(count,Motion::LineDown) => todo!(),
|
||||
MotionCmd(count,Motion::LineDownCharwise) => todo!(),
|
||||
MotionCmd(count,Motion::ScreenLineDown) => todo!(),
|
||||
MotionCmd(count,Motion::ScreenLineDownCharwise) => todo!(),
|
||||
MotionCmd(count,Motion::BeginningOfScreenLine) => todo!(),
|
||||
MotionCmd(count,Motion::FirstGraphicalOnScreenLine) => todo!(),
|
||||
MotionCmd(count,Motion::HalfOfScreen) => todo!(),
|
||||
MotionCmd(count,Motion::HalfOfScreenLineText) => todo!(),
|
||||
MotionCmd(count,Motion::WholeBuffer) => todo!(),
|
||||
MotionCmd(count,Motion::BeginningOfBuffer) => todo!(),
|
||||
MotionCmd(count,Motion::EndOfBuffer) => todo!(),
|
||||
MotionCmd(count,Motion::ToColumn(col)) => todo!(),
|
||||
MotionCmd(count,Motion::ToDelimMatch) => todo!(),
|
||||
MotionCmd(count,Motion::ToBrace(direction)) => todo!(),
|
||||
MotionCmd(count,Motion::ToBracket(direction)) => todo!(),
|
||||
MotionCmd(count,Motion::ToParen(direction)) => todo!(),
|
||||
MotionCmd(count,Motion::Range(start, end)) => todo!(),
|
||||
MotionCmd(count,Motion::RepeatMotion) => todo!(),
|
||||
MotionCmd(count,Motion::RepeatMotionRev) => todo!(),
|
||||
MotionCmd(count,Motion::Null) => MotionKind::Null
|
||||
};
|
||||
|
||||
self.set_buffer(buffer);
|
||||
@@ -925,6 +969,7 @@ impl LineBuf {
|
||||
}
|
||||
pub fn move_cursor(&mut self, motion: MotionKind) {
|
||||
match motion {
|
||||
MotionKind::Onto(pos) | // Onto follows On's behavior for cursor movements
|
||||
MotionKind::On(pos) => self.cursor.set(pos),
|
||||
MotionKind::To(pos) => {
|
||||
self.cursor.set(pos);
|
||||
@@ -946,28 +991,48 @@ impl LineBuf {
|
||||
MotionKind::Null => { /* Do nothing */ }
|
||||
}
|
||||
}
|
||||
pub fn range_from_motion(&mut self, motion: &MotionKind) -> Option<(usize,usize)> {
|
||||
let range = match motion {
|
||||
MotionKind::On(pos) => ordered(self.cursor.get(), *pos),
|
||||
MotionKind::Onto(pos) => {
|
||||
// For motions which include the character at the cursor during operations
|
||||
// but exclude the character during movements
|
||||
let mut pos = ClampedUsize::new(*pos, self.cursor.max, false);
|
||||
let mut cursor_pos = self.cursor;
|
||||
|
||||
// The end of the range must be incremented by one
|
||||
match pos.get().cmp(&self.cursor.get()) {
|
||||
std::cmp::Ordering::Less => cursor_pos.add(1),
|
||||
std::cmp::Ordering::Greater => pos.add(1),
|
||||
std::cmp::Ordering::Equal => {}
|
||||
}
|
||||
ordered(cursor_pos.get(),pos.get())
|
||||
}
|
||||
MotionKind::To(pos) => {
|
||||
let pos = match pos.cmp(&self.cursor.get()) {
|
||||
std::cmp::Ordering::Less => *pos + 1,
|
||||
std::cmp::Ordering::Greater => *pos - 1,
|
||||
std::cmp::Ordering::Equal => *pos,
|
||||
};
|
||||
ordered(self.cursor.get(), pos)
|
||||
}
|
||||
MotionKind::Inclusive((start,end)) => {
|
||||
let (start, mut end) = ordered(*start, *end);
|
||||
end = ClampedUsize::new(end, self.cursor.max, false).ret_add(1);
|
||||
(start,end)
|
||||
}
|
||||
MotionKind::Exclusive((start,end)) => ordered(*start, *end),
|
||||
MotionKind::Null => return None
|
||||
};
|
||||
Some(range)
|
||||
}
|
||||
pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> {
|
||||
match verb {
|
||||
Verb::Delete |
|
||||
Verb::Yank |
|
||||
Verb::Change => {
|
||||
let (start,end) = match motion {
|
||||
MotionKind::On(pos) => ordered(self.cursor.get(), pos),
|
||||
MotionKind::To(pos) => {
|
||||
let pos = match pos.cmp(&self.cursor.get()) {
|
||||
std::cmp::Ordering::Less => pos + 1,
|
||||
std::cmp::Ordering::Greater => pos - 1,
|
||||
std::cmp::Ordering::Equal => pos,
|
||||
};
|
||||
ordered(self.cursor.get(), pos)
|
||||
}
|
||||
MotionKind::Inclusive((start,end)) => {
|
||||
let (start, mut end) = ordered(start, end);
|
||||
end = ClampedUsize::new(end, self.cursor.max, false).ret_add(1);
|
||||
(start,end)
|
||||
}
|
||||
MotionKind::Exclusive((start,end)) => ordered(start, end),
|
||||
MotionKind::Null => return Ok(())
|
||||
let Some((start,end)) = self.range_from_motion(&motion) else {
|
||||
return Ok(())
|
||||
};
|
||||
flog!(DEBUG,start,end);
|
||||
let register_text = if verb == Verb::Yank {
|
||||
@@ -980,7 +1045,20 @@ impl LineBuf {
|
||||
register.write_to_register(register_text);
|
||||
self.cursor.set(start);
|
||||
}
|
||||
Verb::Rot13 => todo!(),
|
||||
Verb::Rot13 => {
|
||||
flog!(DEBUG,motion);
|
||||
let Some((start,end)) = self.range_from_motion(&motion) else {
|
||||
return Ok(())
|
||||
};
|
||||
flog!(DEBUG,start,end);
|
||||
let slice = self.slice(start..end)
|
||||
.unwrap_or_default();
|
||||
flog!(DEBUG,slice);
|
||||
let rot13 = rot13(slice);
|
||||
flog!(DEBUG,rot13);
|
||||
self.buffer.replace_range(start..end, &rot13);
|
||||
self.cursor.set(start);
|
||||
}
|
||||
Verb::ReplaceChar(_) => todo!(),
|
||||
Verb::ToggleCase => todo!(),
|
||||
Verb::ToLower => todo!(),
|
||||
@@ -1050,26 +1128,24 @@ impl LineBuf {
|
||||
let cursor_pos = self.cursor.get();
|
||||
|
||||
for _ in 0..verb_count.unwrap_or(1) {
|
||||
for _ in 0..motion_count.unwrap_or(1) {
|
||||
/*
|
||||
* Let's evaluate the motion now
|
||||
* If motion is None, we will try to use self.select_range
|
||||
* If self.select_range is None, we will use MotionKind::Null
|
||||
*/
|
||||
let motion_eval = motion
|
||||
.clone()
|
||||
.map(|m| self.eval_motion(m.1))
|
||||
.unwrap_or({
|
||||
self.select_range
|
||||
.map(MotionKind::Inclusive)
|
||||
.unwrap_or(MotionKind::Null)
|
||||
});
|
||||
/*
|
||||
* Let's evaluate the motion now
|
||||
* If motion is None, we will try to use self.select_range
|
||||
* If self.select_range is None, we will use MotionKind::Null
|
||||
*/
|
||||
let motion_eval = motion
|
||||
.clone()
|
||||
.map(|m| self.eval_motion(m))
|
||||
.unwrap_or({
|
||||
self.select_range
|
||||
.map(MotionKind::Inclusive)
|
||||
.unwrap_or(MotionKind::Null)
|
||||
});
|
||||
|
||||
if let Some(verb) = verb.clone() {
|
||||
self.exec_verb(verb.1, motion_eval, register)?;
|
||||
} else {
|
||||
self.apply_motion(motion_eval);
|
||||
}
|
||||
if let Some(verb) = verb.clone() {
|
||||
self.exec_verb(verb.1, motion_eval, register)?;
|
||||
} else {
|
||||
self.apply_motion(motion_eval);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1106,6 +1182,22 @@ impl LineBuf {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate alphabetic characters by 13 alphabetic positions
|
||||
pub fn rot13(input: &str) -> String {
|
||||
input.chars()
|
||||
.map(|c| {
|
||||
if c.is_ascii_lowercase() {
|
||||
let offset = b'a';
|
||||
(((c as u8 - offset + 13) % 26) + offset) as char
|
||||
} else if c.is_ascii_uppercase() {
|
||||
let offset = b'A';
|
||||
(((c as u8 - offset + 13) % 26) + offset) as char
|
||||
} else {
|
||||
c
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn ordered(start: usize, end: usize) -> (usize,usize) {
|
||||
if start > end {
|
||||
(end,start)
|
||||
|
||||
@@ -2,8 +2,9 @@ use std::iter::Peekable;
|
||||
use std::str::Chars;
|
||||
|
||||
use nix::NixPath;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M};
|
||||
use super::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, VerbCmd, ViCmd, Word};
|
||||
use crate::prelude::*;
|
||||
|
||||
@@ -51,6 +52,17 @@ pub trait ViMode {
|
||||
fn clamp_cursor(&self) -> bool;
|
||||
fn hist_scroll_start_pos(&self) -> Option<To>;
|
||||
fn report_mode(&self) -> ModeReport;
|
||||
fn cmds_from_raw(&mut self, raw: &str) -> Vec<ViCmd> {
|
||||
let mut cmds = vec![];
|
||||
for ch in raw.graphemes(true) {
|
||||
let key = E::new(ch, M::NONE);
|
||||
let Some(cmd) = self.handle_key(key) else {
|
||||
continue
|
||||
};
|
||||
cmds.push(cmd)
|
||||
}
|
||||
cmds
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default,Debug)]
|
||||
@@ -136,6 +148,7 @@ impl ViMode for ViInsert {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn is_repeatable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
use crate::prompt::readline::linebuf::LineBuf;
|
||||
use crate::prompt::readline::{linebuf::LineBuf, vimode::{ViInsert, ViMode, ViNormal}};
|
||||
|
||||
use super::super::*;
|
||||
|
||||
fn assert_normal_cmd(cmd: &str, start: &str, cursor: usize, expected_buf: &str, expected_cursor: usize) {
|
||||
let cmd = ViNormal::new()
|
||||
.cmds_from_raw(cmd)
|
||||
.pop()
|
||||
.unwrap();
|
||||
let mut buf = LineBuf::new().with_initial(start, cursor);
|
||||
buf.exec_cmd(cmd).unwrap();
|
||||
assert_eq!(buf.as_str(),expected_buf);
|
||||
assert_eq!(buf.cursor.get(),expected_cursor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vimode_insert_cmds() {
|
||||
let raw = "abcdefghijklmnopqrstuvwxyz1234567890-=[];'<>/\\x1b";
|
||||
let mut mode = ViInsert::new();
|
||||
let cmds = mode.cmds_from_raw(raw);
|
||||
insta::assert_debug_snapshot!(cmds)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vimode_normal_cmds() {
|
||||
let raw = "d2wg?5b2P5x";
|
||||
let mut mode = ViNormal::new();
|
||||
let cmds = mode.cmds_from_raw(raw);
|
||||
insta::assert_debug_snapshot!(cmds)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linebuf_empty_linebuf() {
|
||||
let mut buf = LineBuf::new();
|
||||
@@ -129,3 +156,80 @@ fn linebuf_cursor_motion() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_delete_word() {
|
||||
assert_normal_cmd(
|
||||
"dw",
|
||||
"The quick brown fox jumps over the lazy dog",
|
||||
16,
|
||||
"The quick brown jumps over the lazy dog",
|
||||
16
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_delete_backwards() {
|
||||
assert_normal_cmd(
|
||||
"2db",
|
||||
"The quick brown fox jumps over the lazy dog",
|
||||
16,
|
||||
"The fox jumps over the lazy dog",
|
||||
4
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_rot13_five_words_backwards() {
|
||||
assert_normal_cmd(
|
||||
"g?5b",
|
||||
"The quick brown fox jumps over the lazy dog",
|
||||
31,
|
||||
"The dhvpx oebja sbk whzcf bire the lazy dog",
|
||||
4
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_delete_word_on_whitespace() {
|
||||
assert_normal_cmd(
|
||||
"dw",
|
||||
"The quick brown fox",
|
||||
10, // on the whitespace between "quick" and "brown"
|
||||
"The quick brown fox",
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_delete_5_words() {
|
||||
assert_normal_cmd(
|
||||
"5dw",
|
||||
"The quick brown fox jumps over the lazy dog",
|
||||
16,
|
||||
"The quick brown dog",
|
||||
16
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_delete_end_includes_last() {
|
||||
assert_normal_cmd(
|
||||
"de",
|
||||
"The quick brown fox::::jumps over the lazy dog",
|
||||
16,
|
||||
"The quick brown ::::jumps over the lazy dog",
|
||||
16
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_delete_end_unicode_word() {
|
||||
assert_normal_cmd(
|
||||
"de",
|
||||
"naïve café world",
|
||||
0,
|
||||
" café world", // deletes "naïve"
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user