finally found a good way to select line spans

This commit is contained in:
2025-06-07 03:50:35 -04:00
parent c9098b5805
commit b1b1b4b76f
5 changed files with 382 additions and 212 deletions

View File

@@ -92,9 +92,9 @@ pub enum MotionKind {
Inclusive((usize,usize)), // Range, inclusive
Exclusive((usize,usize)), // Range, exclusive
// Used for linewise operations like 'dj', left is the selected range, right is the cursor's new position
InclusiveWithTarget((usize,usize),usize),
ExclusiveWithTarget((usize,usize),usize),
// Used for linewise operations like 'dj', left is the selected range, right is the cursor's new position on the line
InclusiveWithTargetCol((usize,usize),usize),
ExclusiveWithTargetCol((usize,usize),usize),
Null
}
@@ -198,6 +198,9 @@ impl ClampedUsize {
self.max
}
}
/// Increment the ClampedUsize value
///
/// Returns false if the attempted increment is rejected by the clamp
pub fn inc(&mut self) -> bool {
let max = self.upper_bound();
if self.value == max {
@@ -206,6 +209,9 @@ impl ClampedUsize {
self.add(1);
true
}
/// Decrement the ClampedUsize value
///
/// Returns false if the attempted decrement would cause underflow
pub fn dec(&mut self) -> bool {
if self.value == 0 {
return false;
@@ -389,6 +395,7 @@ impl LineBuf {
let end = self.grapheme_indices()[end];
self.buffer.drain(start..end).collect()
};
flog!(DEBUG,drained);
self.update_graphemes();
drained
}
@@ -428,150 +435,93 @@ impl LineBuf {
self.last_selection = self.select_range.take();
}
}
pub fn rfind_newlines(&mut self, n: usize) -> (usize,bool) {
self.rfind_newlines_from(self.cursor.get(), n)
pub fn total_lines(&mut self) -> usize {
self.buffer
.graphemes(true)
.filter(|g| *g == "\n")
.count()
}
pub fn find_newlines(&mut self, n: usize) -> (usize,bool) {
self.find_newlines_from(self.cursor.get(), n)
}
pub fn find_newlines_in_direction(&mut self, start_pos: usize, n: usize, dir: Direction) -> (usize, bool) {
if n == 0 {
return (start_pos,true)
}
let mut indices_iter = self.directional_indices_iter_from(start_pos, dir);
let default = match dir {
Direction::Backward => 0,
Direction::Forward => self.cursor.max
};
let mut result;
let mut count = 0;
// Special case: newline at start_pos
if self.grapheme_at(start_pos) == Some("\n") {
count += 1;
indices_iter.next();
if n == 1 {
return (start_pos,true);
}
}
while let Some(i) = indices_iter.find(|i| self.grapheme_at(*i) == Some("\n")) {
result = i;
count += 1;
if count == n {
return (result, true);
}
}
(default, false)
}
pub fn rfind_newlines_from(&mut self, start_pos: usize, n: usize) -> (usize, bool) {
self.find_newlines_in_direction(start_pos, n, Direction::Backward)
}
pub fn find_newlines_from(&mut self, start_pos: usize, n: usize) -> (usize, bool) {
self.find_newlines_in_direction(start_pos, n, Direction::Forward)
}
pub fn find_index_for(&self, byte_pos: usize) -> Option<usize> {
self.grapheme_indices()
.binary_search(&byte_pos)
.ok()
}
pub fn start_of_cursor_line(&mut self) -> usize {
let (mut pos,_) = self.rfind_newlines(1);
if self.grapheme_at(pos) == Some("\n") || pos != 0 {
pos += 1; // Don't include the newline itself
}
pos
}
pub fn end_of_cursor_line(&mut self) -> usize {
self.find_newlines(1).0
}
pub fn this_line(&mut self) -> (usize,usize) {
(
self.start_of_cursor_line(),
self.end_of_cursor_line()
)
}
pub fn nth_prev_line(&mut self, n: usize) -> Option<(usize,usize)> {
if self.start_of_cursor_line() == 0 {
return None
}
let (start,_) = self.select_lines_up(n);
let slice = self.slice_from_cursor()?;
let end = slice.find('\n').unwrap_or(self.cursor.max);
Some((start,end))
pub fn cursor_line_number(&mut self) -> usize {
self.slice_to_cursor()
.map(|slice| {
slice.graphemes(true)
.filter(|g| *g == "\n")
.count()
}).unwrap_or(0)
}
pub fn nth_next_line(&mut self, n: usize) -> Option<(usize,usize)> {
if self.end_of_cursor_line() == self.cursor.max {
let line_no = self.cursor_line_number() + n;
if line_no > self.total_lines() {
return None
}
Some(self.line_bounds(line_no))
}
pub fn nth_prev_line(&mut self, n: usize) -> Option<(usize,usize)> {
let cursor_line_no = self.cursor_line_number();
if cursor_line_no == 0 {
return None
}
let line_no = cursor_line_no.saturating_sub(n);
if line_no > self.total_lines() {
return None
}
Some(self.line_bounds(line_no))
}
pub fn this_line(&mut self) -> (usize,usize) {
let line_no = self.cursor_line_number();
self.line_bounds(line_no)
}
pub fn start_of_line(&mut self) -> usize {
self.this_line().0
}
pub fn end_of_line(&mut self) -> usize {
self.this_line().1
}
pub fn select_lines_up(&mut self, n: usize) -> Option<(usize,usize)> {
if self.start_of_line() == 0 {
return None
}
let target_line = self.cursor_line_number().saturating_sub(n);
let end = self.end_of_line();
let (start,_) = self.line_bounds(target_line);
let (_,end) = self.select_lines_down(n);
let end_clamped = ClampedUsize::new(end, self.cursor.max, /*exclusive:*/ true);
let slice = self.slice_to(end_clamped.get())?;
let start = slice.rfind('\n').unwrap_or(0);
Some((start,end))
}
/// Include the leading newline, if any
pub fn prev_line_with_leading_newline(&mut self) -> Option<(usize,usize)> {
let (mut start,end) = self.nth_prev_line(1)?;
start = start.saturating_sub(1);
pub fn select_lines_down(&mut self, n: usize) -> Option<(usize,usize)> {
if self.end_of_line() == self.cursor.max {
return None
}
let target_line = self.cursor_line_number() + n;
let start = self.start_of_line();
let (_,end) = self.line_bounds(target_line);
Some((start,end))
}
/// Include the trailing newline, if any
pub fn prev_line_with_trailing_newline(&mut self) -> Option<(usize,usize)> {
let (start,mut end) = self.nth_prev_line(1)?;
end = (end + 1).min(self.cursor.max);
Some((start,end))
}
/// Include the leading newline, if any
pub fn next_line_with_leading_newline(&mut self) -> Option<(usize,usize)> {
let (mut start,end) = self.nth_next_line(1)?;
start = start.saturating_sub(1);
Some((start,end))
}
/// Include the trailing newline, if any
pub fn next_line_with_trailing_newline(&mut self) -> Option<(usize,usize)> {
let (start,mut end) = self.nth_next_line(1)?;
end = (end + 1).min(self.cursor.max);
Some((start,end))
}
pub fn select_lines_up(&mut self, n: usize) -> (usize,usize) {
let (mut start,end) = self.this_line();
if start == 0 {
return (start,end)
pub fn line_bounds(&mut self, n: usize) -> (usize,usize) {
if n > self.total_lines() {
panic!("Attempted to find line {n} when there are only {} lines",self.total_lines())
}
let mut grapheme_index = 0;
let mut start = 0;
// Fine the start of the line
for _ in 0..n {
let slice = self.slice_to(start - 1).unwrap();
if let Some(prev_nl) = slice.rfind('\n') {
start = self.find_index_for(prev_nl).unwrap();
} else {
start = 0;
break
for (_, g) in self.buffer.grapheme_indices(true).skip(grapheme_index) {
grapheme_index += 1;
if g == "\n" {
start = grapheme_index;
break;
}
}
(start,end)
}
pub fn select_lines_down(&mut self, n: usize) -> (usize,usize) {
let (start,mut end) = self.this_line();
if end == self.cursor.max {
return (start,end)
}
for _ in 0..=n {
let next_ln_start = end + 1;
if next_ln_start >= self.cursor.max {
end = self.cursor.max;
break
}
let slice = self.slice_from(next_ln_start).unwrap();
if let Some(next_nl) = slice.find('\n') {
end = self.find_index_for(next_nl).unwrap();
} else {
end = self.cursor.max;
break
let mut end = start;
// Find the end of the line
for (_, g) in self.buffer.grapheme_indices(true).skip(start) {
end += 1;
if g == "\n" {
break;
}
}
@@ -589,6 +539,8 @@ impl LineBuf {
return
};
edit.new.push_str(&diff.new);
edit.old.push_str(&diff.old);
@@ -830,6 +782,13 @@ impl LineBuf {
fn grapheme_index_for_display_col(&self, line: &str, target_col: usize) -> usize {
let mut col = 0;
for (grapheme_index, g) in line.graphemes(true).enumerate() {
if g == "\n" {
if self.cursor.exclusive {
return grapheme_index.saturating_sub(1)
} else {
return grapheme_index;
}
}
let w = g.width();
if col + w > target_col {
return grapheme_index;
@@ -840,7 +799,7 @@ impl LineBuf {
line.graphemes(true).count()
}
pub fn cursor_col(&mut self) -> usize {
let start = self.start_of_cursor_line();
let start = self.start_of_line();
let end = self.cursor.get();
let Some(slice) = self.slice_inclusive(start..=end) else {
return start
@@ -903,6 +862,24 @@ impl LineBuf {
pub fn find<F: Fn(&str) -> bool>(&mut self, op: F) -> usize {
self.find_from(self.cursor.get(), op)
}
pub fn replace_at_cursor(&mut self, new: &str) {
self.replace_at(self.cursor.get(), new);
}
pub fn replace_at(&mut self, pos: usize, new: &str) {
let Some(gr) = self.grapheme_at(pos).map(|gr| gr.to_string()) else {
self.buffer.push_str(new);
return
};
if &gr == "\n" {
// Do not replace the newline, push it forward instead
let byte_pos = self.index_byte_pos(pos);
self.buffer.insert_str(byte_pos, new);
return
}
let start = self.index_byte_pos(pos);
let end = start + gr.len();
self.buffer.replace_range(start..end, new);
}
pub fn eval_motion(&mut self, motion: MotionCmd) -> MotionKind {
let buffer = self.buffer.clone();
if self.has_hint() {
@@ -912,9 +889,33 @@ impl LineBuf {
let eval = match motion {
MotionCmd(count,Motion::WholeLine) => {
let start = self.start_of_cursor_line();
let end = self.find_newlines(count).0;
MotionKind::Inclusive((start,end))
let Some((start,end)) = (if count == 1 {
Some(self.this_line())
} else {
self.select_lines_down(count)
}) else {
return MotionKind::Null
};
let target_col = if let Some(col) = self.saved_col {
col
} else {
let col = self.cursor_col();
self.saved_col = Some(col);
col
};
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
return MotionKind::Null
};
flog!(DEBUG,target_col);
flog!(DEBUG,target_col);
let mut target_pos = self.grapheme_index_for_display_col(&line, target_col);
flog!(DEBUG,target_pos);
if self.cursor.exclusive && line.ends_with("\n") && self.grapheme_at(target_pos) == Some("\n") {
target_pos = target_pos.saturating_sub(1); // Don't land on the newline
}
MotionKind::InclusiveWithTargetCol((start,end),target_pos)
}
MotionCmd(count,Motion::WordMotion(to, word, dir)) => {
let pos = self.dispatch_word_motion(count, to, word, dir);
@@ -938,7 +939,7 @@ impl LineBuf {
}
MotionCmd(count,Motion::TextObj(text_obj, bound)) => todo!(),
MotionCmd(count,Motion::EndOfLastWord) => {
let start = self.start_of_cursor_line();
let start = self.start_of_line();
let mut newline_count = 0;
let mut indices = self.directional_indices_iter_from(start,Direction::Forward);
let mut last_graphical = None;
@@ -960,7 +961,7 @@ impl LineBuf {
MotionKind::On(last)
}
MotionCmd(_,Motion::BeginningOfFirstWord) => {
let start = self.start_of_cursor_line();
let start = self.start_of_line();
let mut indices = self.directional_indices_iter_from(start,Direction::Forward);
let mut first_graphical = None;
while let Some(idx) = indices.next() {
@@ -978,9 +979,16 @@ impl LineBuf {
};
MotionKind::On(first)
}
MotionCmd(_,Motion::BeginningOfLine) => MotionKind::On(self.start_of_cursor_line()),
MotionCmd(_,Motion::BeginningOfLine) => MotionKind::On(self.start_of_line()),
MotionCmd(count,Motion::EndOfLine) => {
let pos = self.find_newlines(count).0;
let pos = if count == 1 {
self.end_of_line()
} else if let Some((_,end)) = self.select_lines_down(count) {
end
} else {
self.end_of_line()
};
MotionKind::On(pos)
}
MotionCmd(count,Motion::CharSearch(direction, dest, ch)) => {
@@ -1008,8 +1016,29 @@ impl LineBuf {
}
MotionKind::Onto(pos.get())
}
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 @ (Motion::ForwardChar | Motion::BackwardChar)) => {
let mut target = self.cursor;
target.exclusive = false;
for _ in 0..count {
match motion {
Motion::BackwardChar => target.sub(1),
Motion::ForwardChar => {
if self.cursor.exclusive && self.grapheme_at(target.ret_add(1)) == Some("\n") {
flog!(DEBUG, "returning null");
return MotionKind::Null
}
target.add(1);
continue
}
_ => unreachable!()
}
if self.grapheme_at(target.get()) == Some("\n") {
flog!(DEBUG, "returning null outside of match");
return MotionKind::Null
}
}
MotionKind::On(target.get())
}
MotionCmd(count,Motion::LineDown) |
MotionCmd(count,Motion::LineUp) => {
let Some((start,end)) = (match motion.1 {
@@ -1017,12 +1046,11 @@ impl LineBuf {
Motion::LineDown => self.nth_next_line(1),
_ => unreachable!()
}) else {
flog!(WARN, "failed to find target line");
return MotionKind::Null
};
flog!(DEBUG, self.slice(start..end));
let target_col = if let Some(col) = self.saved_col {
let mut target_col = if let Some(col) = self.saved_col {
col
} else {
let col = self.cursor_col();
@@ -1033,15 +1061,21 @@ impl LineBuf {
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
return MotionKind::Null
};
let target_pos = start + self.grapheme_index_for_display_col(&line, target_col);
flog!(DEBUG,target_col);
flog!(DEBUG,target_col);
let mut target_pos = self.grapheme_index_for_display_col(&line, target_col);
flog!(DEBUG,target_pos);
if self.cursor.exclusive && line.ends_with("\n") && self.grapheme_at(target_pos) == Some("\n") {
target_pos = target_pos.saturating_sub(1); // Don't land on the newline
}
let (start,end) = match motion.1 {
Motion::LineUp => (start,self.end_of_cursor_line()),
Motion::LineDown => (self.start_of_cursor_line(),end),
Motion::LineUp => (start,self.end_of_line()),
Motion::LineDown => (self.start_of_line(),end),
_ => unreachable!()
};
MotionKind::InclusiveWithTarget((start,end),target_pos)
MotionKind::InclusiveWithTargetCol((start,end),target_pos)
}
MotionCmd(count,Motion::LineDownCharwise) |
MotionCmd(count,Motion::LineUpCharwise) => {
@@ -1149,9 +1183,13 @@ impl LineBuf {
std::cmp::Ordering::Equal => { /* Do nothing */ }
}
}
MotionKind::InclusiveWithTarget((_,_),start) |
MotionKind::ExclusiveWithTargetCol((_,_),col) |
MotionKind::InclusiveWithTargetCol((_,_),col) => {
let (start,end) = self.this_line();
let end = end.min(col);
self.cursor.set(start + end)
}
MotionKind::Inclusive((start,_)) |
MotionKind::ExclusiveWithTarget((_,_),start) |
MotionKind::Exclusive((start,_)) => {
self.cursor.set(start)
}
@@ -1183,14 +1221,14 @@ impl LineBuf {
};
ordered(self.cursor.get(), pos)
}
MotionKind::InclusiveWithTarget((start,end),_) |
MotionKind::Inclusive((start,end)) => {
MotionKind::InclusiveWithTargetCol((start,end),_) |
MotionKind::Inclusive((start,end)) => ordered(*start, *end),
MotionKind::ExclusiveWithTargetCol((start,end),_) |
MotionKind::Exclusive((start,end)) => {
let (start, mut end) = ordered(*start, *end);
end = ClampedUsize::new(end, self.cursor.max, false).ret_add(1);
end = end.saturating_sub(1);
(start,end)
}
MotionKind::ExclusiveWithTarget((start,end),_) |
MotionKind::Exclusive((start,end)) => ordered(*start, *end),
MotionKind::Null => return None
};
Some(range)
@@ -1212,8 +1250,12 @@ impl LineBuf {
};
register.write_to_register(register_text);
match motion {
MotionKind::ExclusiveWithTarget((_,_),pos) |
MotionKind::InclusiveWithTarget((_,_),pos) => self.cursor.set(pos),
MotionKind::ExclusiveWithTargetCol((_,_),pos) |
MotionKind::InclusiveWithTargetCol((_,_),pos) => {
let (start,end) = self.this_line();
self.cursor.set(start);
self.cursor.add(end.min(pos));
}
_ => self.cursor.set(start),
}
}
@@ -1227,8 +1269,55 @@ impl LineBuf {
self.buffer.replace_range(start..end, &rot13);
self.cursor.set(start);
}
Verb::ReplaceChar(_) => todo!(),
Verb::ToggleCase => todo!(),
Verb::ReplaceChar(ch) => {
let mut buf = [0u8;4];
let new = ch.encode_utf8(&mut buf);
self.replace_at_cursor(new);
self.apply_motion(motion);
}
Verb::ToggleCaseSingle => {
let Some(gr) = self.grapheme_at_cursor() else {
return Ok(())
};
if gr.len() > 1 || gr.is_empty() {
return Ok(())
}
let ch = gr.chars().next().unwrap();
if !ch.is_alphabetic() {
return Ok(())
}
let mut buf = [0u8;4];
let new = if ch.is_ascii_lowercase() {
ch.to_ascii_uppercase().encode_utf8(&mut buf)
} else {
ch.to_ascii_lowercase().encode_utf8(&mut buf)
};
self.replace_at_cursor(new);
}
Verb::ToggleCaseRange => {
let Some((start,end)) = self.range_from_motion(&motion) else {
return Ok(())
};
for i in start..end {
let Some(gr) = self.grapheme_at(i) else {
continue
};
if gr.len() > 1 || gr.is_empty() {
continue
}
let ch = gr.chars().next().unwrap();
if !ch.is_alphabetic() {
continue
}
let mut buf = [0u8;4];
let new = if ch.is_ascii_lowercase() {
ch.to_ascii_uppercase().encode_utf8(&mut buf)
} else {
ch.to_ascii_lowercase().encode_utf8(&mut buf)
};
self.replace_at(i,new);
}
}
Verb::ToLower => todo!(),
Verb::ToUpper => todo!(),
Verb::Complete => todo!(),
@@ -1252,14 +1341,8 @@ impl LineBuf {
Verb::Indent => todo!(),
Verb::Dedent => todo!(),
Verb::Equalize => todo!(),
Verb::AcceptLineOrNewline => todo!(),
Verb::EndOfFile => {
if self.buffer.is_empty() {
}
}
Verb::InsertModeLineBreak(anchor) => {
let end = self.end_of_cursor_line();
let end = self.end_of_line();
self.insert_at(end,'\n');
self.cursor.set(end);
match anchor {
@@ -1268,16 +1351,15 @@ impl LineBuf {
}
}
Verb::ReplaceMode |
Verb::EndOfFile |
Verb::InsertMode |
Verb::NormalMode |
Verb::VisualMode |
Verb::ReplaceMode |
Verb::VisualModeLine |
Verb::VisualModeBlock |
Verb::VisualModeSelectLast => {
/* Already handled */
self.apply_motion(motion);
}
Verb::AcceptLineOrNewline |
Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
}
Ok(())
}
@@ -1286,6 +1368,7 @@ impl LineBuf {
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
let is_line_motion = cmd.is_line_motion();
let is_undo_op = cmd.is_undo_op();
let is_inplace_edit = cmd.is_inplace_edit();
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
// Merge character inserts into one edit
@@ -1297,13 +1380,13 @@ impl LineBuf {
let ViCmd { register, verb, motion, raw_seq: _ } = cmd;
let verb_count = verb.as_ref().map(|v| v.0);
let verb_count = verb.as_ref().map(|v| v.0).unwrap_or(1);
let motion_count = motion.as_ref().map(|m| m.0);
let before = self.buffer.clone();
let cursor_pos = self.cursor.get();
for _ in 0..verb_count.unwrap_or(1) {
for i in 0..verb_count {
/*
* Let's evaluate the motion now
* If motion is None, we will try to use self.select_range
@@ -1320,6 +1403,19 @@ impl LineBuf {
if let Some(verb) = verb.clone() {
self.exec_verb(verb.1, motion_eval, register)?;
if is_inplace_edit && i != verb_count.saturating_sub(1) {
/*
Used to calculate motions for stuff like '5~' or '8rg'
Those verbs don't have a motion, and always land on
the last character that they operate on.
Therefore, we increment the cursor until we hit verb_count - 1
or the end of the buffer
*/
if !self.cursor.inc() {
break
}
}
} else {
self.apply_motion(motion_eval);
}

View File

@@ -94,7 +94,7 @@ impl FernVi {
pub fn get_layout(&mut self) -> Layout {
let line = self.editor.as_str().to_string();
let to_cursor = self.editor.slice_to_cursor().unwrap();
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
self.writer.get_layout_from_parts(&self.prompt, to_cursor, &line)
}

View File

@@ -102,6 +102,10 @@ impl ViCmd {
pub fn is_undo_op(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
}
pub fn is_inplace_edit(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceChar(_) | Verb::ToggleCaseSingle)) &&
self.motion.is_none()
}
pub fn is_line_motion(&self) -> bool {
self.motion.as_ref().is_some_and(|m| {
matches!(m.1,
@@ -165,7 +169,8 @@ pub enum Verb {
Yank,
Rot13, // lol
ReplaceChar(char),
ToggleCase,
ToggleCaseSingle,
ToggleCaseRange,
ToLower,
ToUpper,
Complete,
@@ -203,7 +208,8 @@ impl Verb {
Self::ReplaceChar(_) |
Self::ToLower |
Self::ToUpper |
Self::ToggleCase |
Self::ToggleCaseRange |
Self::ToggleCaseSingle |
Self::Put(_) |
Self::ReplaceMode |
Self::InsertModeLineBreak(_) |
@@ -221,7 +227,8 @@ impl Verb {
Self::Delete |
Self::Change |
Self::ReplaceChar(_) |
Self::ToggleCase |
Self::ToggleCaseRange |
Self::ToggleCaseSingle |
Self::ToLower |
Self::ToUpper |
Self::RepeatLast |

View File

@@ -383,6 +383,21 @@ impl ViNormal {
}
)
}
'~' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToggleCaseRange));
}
'u' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToLower));
}
'U' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToUpper));
}
'?' => {
chars_clone.next();
chars = chars_clone;
@@ -465,8 +480,8 @@ impl ViNormal {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
motion: Some(MotionCmd(count, Motion::ForwardChar)),
verb: Some(VerbCmd(count, Verb::ReplaceChar(ch))),
motion: None,
raw_seq: self.take_cmd()
}
)
@@ -485,8 +500,8 @@ impl ViNormal {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ToggleCase)),
motion: Some(MotionCmd(count, Motion::ForwardChar)),
verb: Some(VerbCmd(count, Verb::ToggleCaseSingle)),
motion: None,
raw_seq: self.take_cmd()
}
)
@@ -654,6 +669,9 @@ impl ViNormal {
('c', Some(VerbCmd(_,Verb::Change))) |
('y', Some(VerbCmd(_,Verb::Yank))) |
('=', Some(VerbCmd(_,Verb::Equalize))) |
('u', Some(VerbCmd(_,Verb::ToLower))) |
('U', Some(VerbCmd(_,Verb::ToUpper))) |
('~', Some(VerbCmd(_,Verb::ToggleCaseRange))) |
('>', Some(VerbCmd(_,Verb::Indent))) |
('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)),
_ => {}
@@ -1145,7 +1163,7 @@ impl ViVisual {
return Some(
ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ToggleCase)),
verb: Some(VerbCmd(1, Verb::ToggleCaseRange)),
motion: None,
raw_seq: self.take_cmd()
}

View File

@@ -2,16 +2,14 @@ use crate::prompt::readline::{linebuf::LineBuf, vimode::{ViInsert, ViMode, ViNor
use super::super::*;
fn assert_normal_cmd(cmd: &str, start: &str, cursor: usize, expected_buf: &str, expected_cursor: usize) {
fn normal_cmd(cmd: &str, buf: &str, cursor: usize, expected_buf: &str, expected_cursor: usize) -> bool {
let cmd = ViNormal::new()
.cmds_from_raw(cmd)
.pop()
.unwrap();
let mut buf = LineBuf::new().with_initial(start, cursor);
let mut buf = LineBuf::new().with_initial(buf, cursor);
buf.exec_cmd(cmd).unwrap();
assert_eq!(buf.as_str(),expected_buf);
assert_eq!(buf.cursor.get(),expected_cursor);
buf.as_str() == expected_buf && buf.cursor.get() == expected_cursor
}
#[test]
@@ -97,7 +95,7 @@ fn linebuf_this_line() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
let mut buf = LineBuf::new().with_initial(initial, 57);
let (start,end) = buf.this_line();
assert_eq!(buf.slice(start..end), Some("This is the third line"))
assert_eq!(buf.slice(start..end), Some("This is the third line\n"))
}
#[test]
@@ -105,7 +103,7 @@ fn linebuf_prev_line() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
let mut buf = LineBuf::new().with_initial(initial, 57);
let (start,end) = buf.nth_prev_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("This is the second line"))
assert_eq!(buf.slice(start..end), Some("This is the second line\n"))
}
#[test]
@@ -113,7 +111,7 @@ fn linebuf_prev_line_first_line_is_empty() {
let initial = "\nThis is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line";
let mut buf = LineBuf::new().with_initial(initial, 36);
let (start,end) = buf.nth_prev_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("This is the first line"))
assert_eq!(buf.slice(start..end), Some("This is the first line\n"))
}
#[test]
@@ -129,7 +127,7 @@ fn linebuf_next_line_last_line_is_empty() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n";
let mut buf = LineBuf::new().with_initial(initial, 57);
let (start,end) = buf.nth_next_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("This is the fourth line"))
assert_eq!(buf.slice(start..end), Some("This is the fourth line\n"))
}
#[test]
@@ -137,7 +135,7 @@ fn linebuf_next_line_several_trailing_newlines() {
let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n\n\n\n";
let mut buf = LineBuf::new().with_initial(initial, 81);
let (start,end) = buf.nth_next_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some(""))
assert_eq!(buf.slice(start..end), Some("\n"))
}
#[test]
@@ -146,7 +144,7 @@ fn linebuf_next_line_only_newlines() {
let mut buf = LineBuf::new().with_initial(initial, 7);
let (start,end) = buf.nth_next_line(1).unwrap();
assert_eq!(start, 8);
assert_eq!(buf.slice(start..end), Some(""))
assert_eq!(buf.slice(start..end), Some("\n"))
}
#[test]
@@ -154,8 +152,8 @@ fn linebuf_prev_line_only_newlines() {
let initial = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
let mut buf = LineBuf::new().with_initial(initial, 7);
let (start,end) = buf.nth_prev_line(1).unwrap();
assert_eq!(buf.slice(start..end), Some("\n"));
assert_eq!(start, 6);
assert_eq!(buf.slice(start..end), Some(""))
}
#[test]
@@ -202,90 +200,141 @@ fn linebuf_cursor_motion() {
#[test]
fn editor_delete_word() {
assert_normal_cmd(
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(
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(
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(
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(
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(
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(
assert!(normal_cmd(
"de",
"naïve café world",
0,
" café world", // deletes "naïve"
0
);
));
}
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
#[test]
fn editor_inplace_edit_cursor_position() {
assert!(normal_cmd(
"5~",
"foobar",
0,
"FOOBAr", // deletes "naïve"
4
));
assert!(normal_cmd(
"5rg",
"foobar",
0,
"gggggr", // deletes "naïve"
4
));
}
#[test]
fn editor_overshooting_motions() {
assert!(normal_cmd(
"5dw",
"foo bar",
0,
"", // deletes "naïve"
0
));
assert!(normal_cmd(
"3db",
"foo bar",
0,
"foo bar", // deletes "naïve"
0
));
assert!(normal_cmd(
"3dj",
"foo bar",
0,
"foo bar", // deletes "naïve"
0
));
assert!(normal_cmd(
"3dk",
"foo bar",
0,
"foo bar", // deletes "naïve"
0
));
}
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.";
#[test]
fn editor_delete_line_up() {
assert_normal_cmd(
assert!(normal_cmd(
"dk",
LOREM_IPSUM,
237,
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
126,
)
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.",
240,
))
}