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 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::*};
|
use crate::{libsh::error::ShResult, prelude::*};
|
||||||
|
|
||||||
#[derive(PartialEq,Eq,Debug,Clone,Copy)]
|
#[derive(PartialEq,Eq,Debug,Clone,Copy)]
|
||||||
@@ -15,18 +15,26 @@ pub enum CharClass {
|
|||||||
|
|
||||||
impl From<&str> for CharClass {
|
impl From<&str> for CharClass {
|
||||||
fn from(value: &str) -> Self {
|
fn from(value: &str) -> Self {
|
||||||
if value.len() > 1 {
|
let mut chars = value.chars();
|
||||||
return Self::Symbol // Multi-byte grapheme
|
|
||||||
|
// 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) {
|
if value.chars().all(char::is_alphanumeric) {
|
||||||
CharClass::Alphanum
|
CharClass::Alphanum
|
||||||
} else if value.chars().all(char::is_whitespace) {
|
} else if value.chars().all(char::is_whitespace) {
|
||||||
CharClass::Whitespace
|
CharClass::Whitespace
|
||||||
} else if !value.chars().all(char::is_alphanumeric) {
|
} else if value.chars().all(|c| !c.is_alphanumeric()) {
|
||||||
CharClass::Symbol
|
CharClass::Symbol
|
||||||
} else {
|
} else {
|
||||||
Self::Other
|
CharClass::Other
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,6 +87,7 @@ pub enum SelectMode {
|
|||||||
pub enum MotionKind {
|
pub enum MotionKind {
|
||||||
To(usize), // Absolute position, exclusive
|
To(usize), // Absolute position, exclusive
|
||||||
On(usize), // Absolute position, inclusive
|
On(usize), // Absolute position, inclusive
|
||||||
|
Onto(usize), // Absolute position, operations include the position but motions exclude it (wtf vim)
|
||||||
Inclusive((usize,usize)), // Range, inclusive
|
Inclusive((usize,usize)), // Range, inclusive
|
||||||
Exclusive((usize,usize)), // Range, exclusive
|
Exclusive((usize,usize)), // Range, exclusive
|
||||||
Null
|
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
|
// Not sorry for these method names btw
|
||||||
match to {
|
let mut pos = ClampedUsize::new(self.cursor.get(), self.cursor.max, false);
|
||||||
To::Start => {
|
for _ in 0..count {
|
||||||
match dir {
|
pos.set(match to {
|
||||||
Direction::Forward => self.start_of_word_forward_or_end_of_word_backward(word, dir),
|
To::Start => {
|
||||||
Direction::Backward => self.end_of_word_forward_or_start_of_word_backward(word, dir)
|
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 => {
|
||||||
To::End => {
|
match dir {
|
||||||
match dir {
|
Direction::Forward => self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, 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_from(pos.get(), word, dir),
|
||||||
Direction::Backward => self.start_of_word_forward_or_end_of_word_backward(word, dir),
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
pos.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the start of a word forward, or the end of a word backward
|
/// 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
|
/// 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.
|
/// 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 {
|
pub fn start_of_word_forward_or_end_of_word_backward_from(&mut self, mut pos: usize, word: Word, dir: Direction) -> usize {
|
||||||
let mut pos = self.cursor.get();
|
|
||||||
let default = match dir {
|
let default = match dir {
|
||||||
Direction::Backward => 0,
|
Direction::Backward => 0,
|
||||||
Direction::Forward => self.grapheme_indices().len()
|
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 {
|
match word {
|
||||||
Word::Big => {
|
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,on_boundary);
|
||||||
flog!(DEBUG,pos);
|
flog!(DEBUG,pos);
|
||||||
if on_boundary {
|
if on_boundary {
|
||||||
@@ -613,16 +628,18 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
Word::Normal => {
|
Word::Normal => {
|
||||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default };
|
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 Some(next_idx) = indices_iter.peek() 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 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 {
|
if on_boundary {
|
||||||
pos = next_idx
|
pos = *next_idx
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||||
return default
|
return default
|
||||||
};
|
};
|
||||||
let on_whitespace = is_whitespace(&cur_char);
|
let on_whitespace = is_whitespace(&cur_char);
|
||||||
|
flog!(DEBUG,on_whitespace);
|
||||||
|
|
||||||
// Advance until hitting whitespace or a different character class
|
// Advance until hitting whitespace or a different character class
|
||||||
if !on_whitespace {
|
if !on_whitespace {
|
||||||
@@ -648,6 +665,7 @@ impl LineBuf {
|
|||||||
.is_some_and(|c| !is_whitespace(c))
|
.is_some_and(|c| !is_whitespace(c))
|
||||||
}
|
}
|
||||||
).unwrap_or(default);
|
).unwrap_or(default);
|
||||||
|
flog!(DEBUG,self.grapheme_at(non_ws_pos));
|
||||||
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
|
/// 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.
|
/// 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 {
|
pub fn end_of_word_forward_or_start_of_word_backward_from(&mut self, mut pos: usize, word: Word, dir: Direction) -> usize {
|
||||||
let mut pos = self.cursor.get();
|
|
||||||
let default = match dir {
|
let default = match dir {
|
||||||
Direction::Backward => 0,
|
Direction::Backward => 0,
|
||||||
Direction::Forward => self.grapheme_indices().len()
|
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 {
|
match word {
|
||||||
Word::Big => {
|
Word::Big => {
|
||||||
let Some(next_idx) = indices_iter.next() else { return default };
|
let Some(next_idx) = indices_iter.peek() else { return default };
|
||||||
let on_boundary = self.grapheme_at(next_idx).is_none_or(is_whitespace);
|
let on_boundary = self.grapheme_at(*next_idx).is_none_or(is_whitespace);
|
||||||
if on_boundary {
|
if on_boundary {
|
||||||
let Some(idx) = indices_iter.next() else { return default };
|
let Some(idx) = indices_iter.next() else { return default };
|
||||||
pos = idx;
|
pos = idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check current grapheme
|
// Check current grapheme
|
||||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else {
|
||||||
return default
|
return default
|
||||||
@@ -706,9 +722,10 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
Word::Normal => {
|
Word::Normal => {
|
||||||
let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default };
|
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 Some(next_idx) = indices_iter.peek() 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 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 {
|
if on_boundary {
|
||||||
|
let next_idx = indices_iter.next().unwrap();
|
||||||
pos = next_idx
|
pos = next_idx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,7 +817,7 @@ impl LineBuf {
|
|||||||
pub fn find<F: Fn(&str) -> bool>(&mut self, op: F) -> usize {
|
pub fn find<F: Fn(&str) -> bool>(&mut self, op: F) -> usize {
|
||||||
self.find_from(self.cursor.get(), op)
|
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();
|
let buffer = self.buffer.clone();
|
||||||
if self.has_hint() {
|
if self.has_hint() {
|
||||||
let hint = self.hint.clone().unwrap();
|
let hint = self.hint.clone().unwrap();
|
||||||
@@ -808,14 +825,35 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let eval = match motion {
|
let eval = match motion {
|
||||||
Motion::WholeLine => {
|
MotionCmd(count,Motion::WholeLine) => {
|
||||||
let (start,end) = self.this_line();
|
let start = self.start_of_cursor_line();
|
||||||
|
let end = self.find_newlines(count);
|
||||||
MotionKind::Inclusive((start,end))
|
MotionKind::Inclusive((start,end))
|
||||||
}
|
}
|
||||||
Motion::WordMotion(to, word, dir) => MotionKind::On(self.dispatch_word_motion(to, word, dir)),
|
MotionCmd(count,Motion::WordMotion(to, word, dir)) => {
|
||||||
Motion::TextObj(text_obj, bound) => todo!(),
|
let pos = self.dispatch_word_motion(count, to, word, dir);
|
||||||
Motion::EndOfLastWord => {
|
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 start = self.start_of_cursor_line();
|
||||||
|
let mut newline_count = 0;
|
||||||
let mut indices = self.directional_indices_iter_from(start,Direction::Forward);
|
let mut indices = self.directional_indices_iter_from(start,Direction::Forward);
|
||||||
let mut last_graphical = None;
|
let mut last_graphical = None;
|
||||||
while let Some(idx) = indices.next() {
|
while let Some(idx) = indices.next() {
|
||||||
@@ -824,7 +862,10 @@ impl LineBuf {
|
|||||||
last_graphical = Some(idx);
|
last_graphical = Some(idx);
|
||||||
}
|
}
|
||||||
if grapheme == "\n" {
|
if grapheme == "\n" {
|
||||||
break
|
newline_count += 1;
|
||||||
|
if newline_count == count {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let Some(last) = last_graphical else {
|
let Some(last) = last_graphical else {
|
||||||
@@ -832,7 +873,7 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
MotionKind::On(last)
|
MotionKind::On(last)
|
||||||
}
|
}
|
||||||
Motion::BeginningOfFirstWord => {
|
MotionCmd(_,Motion::BeginningOfFirstWord) => {
|
||||||
let start = self.start_of_cursor_line();
|
let start = self.start_of_cursor_line();
|
||||||
let mut indices = self.directional_indices_iter_from(start,Direction::Forward);
|
let mut indices = self.directional_indices_iter_from(start,Direction::Forward);
|
||||||
let mut first_graphical = None;
|
let mut first_graphical = None;
|
||||||
@@ -852,35 +893,38 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
MotionKind::On(first)
|
MotionKind::On(first)
|
||||||
}
|
}
|
||||||
Motion::BeginningOfLine => MotionKind::On(self.start_of_cursor_line()),
|
MotionCmd(_,Motion::BeginningOfLine) => MotionKind::On(self.start_of_cursor_line()),
|
||||||
Motion::EndOfLine => MotionKind::On(self.end_of_cursor_line()),
|
MotionCmd(count,Motion::EndOfLine) => {
|
||||||
Motion::CharSearch(direction, dest, ch) => todo!(),
|
let pos = self.find_newlines(count);
|
||||||
Motion::BackwardChar => MotionKind::On(self.cursor.ret_sub(1)),
|
MotionKind::On(pos)
|
||||||
Motion::ForwardChar => MotionKind::On(self.cursor.ret_add_inclusive(1)),
|
}
|
||||||
Motion::LineUp => todo!(),
|
MotionCmd(count,Motion::CharSearch(direction, dest, ch)) => todo!(),
|
||||||
Motion::LineUpCharwise => todo!(),
|
MotionCmd(count,Motion::BackwardChar) => MotionKind::On(self.cursor.ret_sub(1)),
|
||||||
Motion::ScreenLineUp => todo!(),
|
MotionCmd(count,Motion::ForwardChar) => MotionKind::On(self.cursor.ret_add_inclusive(1)),
|
||||||
Motion::ScreenLineUpCharwise => todo!(),
|
MotionCmd(count,Motion::LineUp) => todo!(),
|
||||||
Motion::LineDown => todo!(),
|
MotionCmd(count,Motion::LineUpCharwise) => todo!(),
|
||||||
Motion::LineDownCharwise => todo!(),
|
MotionCmd(count,Motion::ScreenLineUp) => todo!(),
|
||||||
Motion::ScreenLineDown => todo!(),
|
MotionCmd(count,Motion::ScreenLineUpCharwise) => todo!(),
|
||||||
Motion::ScreenLineDownCharwise => todo!(),
|
MotionCmd(count,Motion::LineDown) => todo!(),
|
||||||
Motion::BeginningOfScreenLine => todo!(),
|
MotionCmd(count,Motion::LineDownCharwise) => todo!(),
|
||||||
Motion::FirstGraphicalOnScreenLine => todo!(),
|
MotionCmd(count,Motion::ScreenLineDown) => todo!(),
|
||||||
Motion::HalfOfScreen => todo!(),
|
MotionCmd(count,Motion::ScreenLineDownCharwise) => todo!(),
|
||||||
Motion::HalfOfScreenLineText => todo!(),
|
MotionCmd(count,Motion::BeginningOfScreenLine) => todo!(),
|
||||||
Motion::WholeBuffer => todo!(),
|
MotionCmd(count,Motion::FirstGraphicalOnScreenLine) => todo!(),
|
||||||
Motion::BeginningOfBuffer => todo!(),
|
MotionCmd(count,Motion::HalfOfScreen) => todo!(),
|
||||||
Motion::EndOfBuffer => todo!(),
|
MotionCmd(count,Motion::HalfOfScreenLineText) => todo!(),
|
||||||
Motion::ToColumn(col) => todo!(),
|
MotionCmd(count,Motion::WholeBuffer) => todo!(),
|
||||||
Motion::ToDelimMatch => todo!(),
|
MotionCmd(count,Motion::BeginningOfBuffer) => todo!(),
|
||||||
Motion::ToBrace(direction) => todo!(),
|
MotionCmd(count,Motion::EndOfBuffer) => todo!(),
|
||||||
Motion::ToBracket(direction) => todo!(),
|
MotionCmd(count,Motion::ToColumn(col)) => todo!(),
|
||||||
Motion::ToParen(direction) => todo!(),
|
MotionCmd(count,Motion::ToDelimMatch) => todo!(),
|
||||||
Motion::Range(start, end) => todo!(),
|
MotionCmd(count,Motion::ToBrace(direction)) => todo!(),
|
||||||
Motion::RepeatMotion => todo!(),
|
MotionCmd(count,Motion::ToBracket(direction)) => todo!(),
|
||||||
Motion::RepeatMotionRev => todo!(),
|
MotionCmd(count,Motion::ToParen(direction)) => todo!(),
|
||||||
Motion::Null => MotionKind::Null
|
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);
|
self.set_buffer(buffer);
|
||||||
@@ -925,6 +969,7 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
pub fn move_cursor(&mut self, motion: MotionKind) {
|
pub fn move_cursor(&mut self, motion: MotionKind) {
|
||||||
match motion {
|
match motion {
|
||||||
|
MotionKind::Onto(pos) | // Onto follows On's behavior for cursor movements
|
||||||
MotionKind::On(pos) => self.cursor.set(pos),
|
MotionKind::On(pos) => self.cursor.set(pos),
|
||||||
MotionKind::To(pos) => {
|
MotionKind::To(pos) => {
|
||||||
self.cursor.set(pos);
|
self.cursor.set(pos);
|
||||||
@@ -946,28 +991,48 @@ impl LineBuf {
|
|||||||
MotionKind::Null => { /* Do nothing */ }
|
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<()> {
|
pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> {
|
||||||
match verb {
|
match verb {
|
||||||
Verb::Delete |
|
Verb::Delete |
|
||||||
Verb::Yank |
|
Verb::Yank |
|
||||||
Verb::Change => {
|
Verb::Change => {
|
||||||
let (start,end) = match motion {
|
let Some((start,end)) = self.range_from_motion(&motion) else {
|
||||||
MotionKind::On(pos) => ordered(self.cursor.get(), pos),
|
return Ok(())
|
||||||
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(())
|
|
||||||
};
|
};
|
||||||
flog!(DEBUG,start,end);
|
flog!(DEBUG,start,end);
|
||||||
let register_text = if verb == Verb::Yank {
|
let register_text = if verb == Verb::Yank {
|
||||||
@@ -980,7 +1045,20 @@ impl LineBuf {
|
|||||||
register.write_to_register(register_text);
|
register.write_to_register(register_text);
|
||||||
self.cursor.set(start);
|
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::ReplaceChar(_) => todo!(),
|
||||||
Verb::ToggleCase => todo!(),
|
Verb::ToggleCase => todo!(),
|
||||||
Verb::ToLower => todo!(),
|
Verb::ToLower => todo!(),
|
||||||
@@ -1050,26 +1128,24 @@ impl LineBuf {
|
|||||||
let cursor_pos = self.cursor.get();
|
let cursor_pos = self.cursor.get();
|
||||||
|
|
||||||
for _ in 0..verb_count.unwrap_or(1) {
|
for _ in 0..verb_count.unwrap_or(1) {
|
||||||
for _ in 0..motion_count.unwrap_or(1) {
|
/*
|
||||||
/*
|
* Let's evaluate the motion now
|
||||||
* Let's evaluate the motion now
|
* If motion is None, we will try to use self.select_range
|
||||||
* If motion is None, we will try to use self.select_range
|
* If self.select_range is None, we will use MotionKind::Null
|
||||||
* If self.select_range is None, we will use MotionKind::Null
|
*/
|
||||||
*/
|
let motion_eval = motion
|
||||||
let motion_eval = motion
|
.clone()
|
||||||
.clone()
|
.map(|m| self.eval_motion(m))
|
||||||
.map(|m| self.eval_motion(m.1))
|
.unwrap_or({
|
||||||
.unwrap_or({
|
self.select_range
|
||||||
self.select_range
|
.map(MotionKind::Inclusive)
|
||||||
.map(MotionKind::Inclusive)
|
.unwrap_or(MotionKind::Null)
|
||||||
.unwrap_or(MotionKind::Null)
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(verb) = verb.clone() {
|
if let Some(verb) = verb.clone() {
|
||||||
self.exec_verb(verb.1, motion_eval, register)?;
|
self.exec_verb(verb.1, motion_eval, register)?;
|
||||||
} else {
|
} else {
|
||||||
self.apply_motion(motion_eval);
|
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) {
|
pub fn ordered(start: usize, end: usize) -> (usize,usize) {
|
||||||
if start > end {
|
if start > end {
|
||||||
(end,start)
|
(end,start)
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ use std::iter::Peekable;
|
|||||||
use std::str::Chars;
|
use std::str::Chars;
|
||||||
|
|
||||||
use nix::NixPath;
|
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 super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, VerbCmd, ViCmd, Word};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
@@ -51,6 +52,17 @@ pub trait ViMode {
|
|||||||
fn clamp_cursor(&self) -> bool;
|
fn clamp_cursor(&self) -> bool;
|
||||||
fn hist_scroll_start_pos(&self) -> Option<To>;
|
fn hist_scroll_start_pos(&self) -> Option<To>;
|
||||||
fn report_mode(&self) -> ModeReport;
|
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)]
|
#[derive(Default,Debug)]
|
||||||
@@ -136,6 +148,7 @@ impl ViMode for ViInsert {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn is_repeatable(&self) -> bool {
|
fn is_repeatable(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,34 @@
|
|||||||
use crate::prompt::readline::linebuf::LineBuf;
|
use crate::prompt::readline::{linebuf::LineBuf, vimode::{ViInsert, ViMode, ViNormal}};
|
||||||
|
|
||||||
use super::super::*;
|
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]
|
#[test]
|
||||||
fn linebuf_empty_linebuf() {
|
fn linebuf_empty_linebuf() {
|
||||||
let mut buf = LineBuf::new();
|
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