finally found a good way to select line spans
This commit is contained in:
@@ -92,9 +92,9 @@ pub enum MotionKind {
|
|||||||
Inclusive((usize,usize)), // Range, inclusive
|
Inclusive((usize,usize)), // Range, inclusive
|
||||||
Exclusive((usize,usize)), // Range, exclusive
|
Exclusive((usize,usize)), // Range, exclusive
|
||||||
|
|
||||||
// Used for linewise operations like 'dj', left is the selected range, right is the cursor's new position
|
// Used for linewise operations like 'dj', left is the selected range, right is the cursor's new position on the line
|
||||||
InclusiveWithTarget((usize,usize),usize),
|
InclusiveWithTargetCol((usize,usize),usize),
|
||||||
ExclusiveWithTarget((usize,usize),usize),
|
ExclusiveWithTargetCol((usize,usize),usize),
|
||||||
Null
|
Null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +198,9 @@ impl ClampedUsize {
|
|||||||
self.max
|
self.max
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Increment the ClampedUsize value
|
||||||
|
///
|
||||||
|
/// Returns false if the attempted increment is rejected by the clamp
|
||||||
pub fn inc(&mut self) -> bool {
|
pub fn inc(&mut self) -> bool {
|
||||||
let max = self.upper_bound();
|
let max = self.upper_bound();
|
||||||
if self.value == max {
|
if self.value == max {
|
||||||
@@ -206,6 +209,9 @@ impl ClampedUsize {
|
|||||||
self.add(1);
|
self.add(1);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
/// Decrement the ClampedUsize value
|
||||||
|
///
|
||||||
|
/// Returns false if the attempted decrement would cause underflow
|
||||||
pub fn dec(&mut self) -> bool {
|
pub fn dec(&mut self) -> bool {
|
||||||
if self.value == 0 {
|
if self.value == 0 {
|
||||||
return false;
|
return false;
|
||||||
@@ -389,6 +395,7 @@ impl LineBuf {
|
|||||||
let end = self.grapheme_indices()[end];
|
let end = self.grapheme_indices()[end];
|
||||||
self.buffer.drain(start..end).collect()
|
self.buffer.drain(start..end).collect()
|
||||||
};
|
};
|
||||||
|
flog!(DEBUG,drained);
|
||||||
self.update_graphemes();
|
self.update_graphemes();
|
||||||
drained
|
drained
|
||||||
}
|
}
|
||||||
@@ -428,154 +435,97 @@ impl LineBuf {
|
|||||||
self.last_selection = self.select_range.take();
|
self.last_selection = self.select_range.take();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn rfind_newlines(&mut self, n: usize) -> (usize,bool) {
|
pub fn total_lines(&mut self) -> usize {
|
||||||
self.rfind_newlines_from(self.cursor.get(), n)
|
self.buffer
|
||||||
|
.graphemes(true)
|
||||||
|
.filter(|g| *g == "\n")
|
||||||
|
.count()
|
||||||
}
|
}
|
||||||
pub fn find_newlines(&mut self, n: usize) -> (usize,bool) {
|
pub fn cursor_line_number(&mut self) -> usize {
|
||||||
self.find_newlines_from(self.cursor.get(), n)
|
self.slice_to_cursor()
|
||||||
|
.map(|slice| {
|
||||||
|
slice.graphemes(true)
|
||||||
|
.filter(|g| *g == "\n")
|
||||||
|
.count()
|
||||||
|
}).unwrap_or(0)
|
||||||
}
|
}
|
||||||
pub fn find_newlines_in_direction(&mut self, start_pos: usize, n: usize, dir: Direction) -> (usize, bool) {
|
pub fn nth_next_line(&mut self, n: usize) -> Option<(usize,usize)> {
|
||||||
if n == 0 {
|
let line_no = self.cursor_line_number() + n;
|
||||||
return (start_pos,true)
|
if line_no > self.total_lines() {
|
||||||
|
return None
|
||||||
}
|
}
|
||||||
let mut indices_iter = self.directional_indices_iter_from(start_pos, dir);
|
Some(self.line_bounds(line_no))
|
||||||
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)> {
|
pub fn nth_prev_line(&mut self, n: usize) -> Option<(usize,usize)> {
|
||||||
if self.start_of_cursor_line() == 0 {
|
let cursor_line_no = self.cursor_line_number();
|
||||||
|
if cursor_line_no == 0 {
|
||||||
return None
|
return None
|
||||||
}
|
}
|
||||||
|
let line_no = cursor_line_no.saturating_sub(n);
|
||||||
let (start,_) = self.select_lines_up(n);
|
if line_no > self.total_lines() {
|
||||||
let slice = self.slice_from_cursor()?;
|
|
||||||
let end = slice.find('\n').unwrap_or(self.cursor.max);
|
|
||||||
Some((start,end))
|
|
||||||
}
|
|
||||||
pub fn nth_next_line(&mut self, n: usize) -> Option<(usize, usize)> {
|
|
||||||
if self.end_of_cursor_line() == self.cursor.max {
|
|
||||||
return None
|
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))
|
Some((start,end))
|
||||||
}
|
}
|
||||||
/// Include the leading newline, if any
|
pub fn select_lines_down(&mut self, n: usize) -> Option<(usize,usize)> {
|
||||||
pub fn prev_line_with_leading_newline(&mut self) -> Option<(usize,usize)> {
|
if self.end_of_line() == self.cursor.max {
|
||||||
let (mut start,end) = self.nth_prev_line(1)?;
|
return None
|
||||||
start = start.saturating_sub(1);
|
}
|
||||||
|
let target_line = self.cursor_line_number() + n;
|
||||||
|
let start = self.start_of_line();
|
||||||
|
let (_,end) = self.line_bounds(target_line);
|
||||||
|
|
||||||
Some((start,end))
|
Some((start,end))
|
||||||
}
|
}
|
||||||
/// Include the trailing newline, if any
|
pub fn line_bounds(&mut self, n: usize) -> (usize,usize) {
|
||||||
pub fn prev_line_with_trailing_newline(&mut self) -> Option<(usize,usize)> {
|
if n > self.total_lines() {
|
||||||
let (start,mut end) = self.nth_prev_line(1)?;
|
panic!("Attempted to find line {n} when there are only {} lines",self.total_lines())
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut grapheme_index = 0;
|
||||||
|
let mut start = 0;
|
||||||
|
|
||||||
|
// Fine the start of the line
|
||||||
for _ in 0..n {
|
for _ in 0..n {
|
||||||
let slice = self.slice_to(start - 1).unwrap();
|
for (_, g) in self.buffer.grapheme_indices(true).skip(grapheme_index) {
|
||||||
if let Some(prev_nl) = slice.rfind('\n') {
|
grapheme_index += 1;
|
||||||
start = self.find_index_for(prev_nl).unwrap();
|
if g == "\n" {
|
||||||
} else {
|
start = grapheme_index;
|
||||||
start = 0;
|
break;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(start,end)
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(start, end)
|
||||||
}
|
}
|
||||||
pub fn handle_edit(&mut self, old: String, new: String, curs_pos: usize) {
|
pub fn handle_edit(&mut self, old: String, new: String, curs_pos: usize) {
|
||||||
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
|
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
|
||||||
@@ -589,6 +539,8 @@ impl LineBuf {
|
|||||||
return
|
return
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
edit.new.push_str(&diff.new);
|
edit.new.push_str(&diff.new);
|
||||||
edit.old.push_str(&diff.old);
|
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 {
|
fn grapheme_index_for_display_col(&self, line: &str, target_col: usize) -> usize {
|
||||||
let mut col = 0;
|
let mut col = 0;
|
||||||
for (grapheme_index, g) in line.graphemes(true).enumerate() {
|
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();
|
let w = g.width();
|
||||||
if col + w > target_col {
|
if col + w > target_col {
|
||||||
return grapheme_index;
|
return grapheme_index;
|
||||||
@@ -840,7 +799,7 @@ impl LineBuf {
|
|||||||
line.graphemes(true).count()
|
line.graphemes(true).count()
|
||||||
}
|
}
|
||||||
pub fn cursor_col(&mut self) -> usize {
|
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 end = self.cursor.get();
|
||||||
let Some(slice) = self.slice_inclusive(start..=end) else {
|
let Some(slice) = self.slice_inclusive(start..=end) else {
|
||||||
return start
|
return start
|
||||||
@@ -903,6 +862,24 @@ 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 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 {
|
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() {
|
||||||
@@ -912,9 +889,33 @@ impl LineBuf {
|
|||||||
|
|
||||||
let eval = match motion {
|
let eval = match motion {
|
||||||
MotionCmd(count,Motion::WholeLine) => {
|
MotionCmd(count,Motion::WholeLine) => {
|
||||||
let start = self.start_of_cursor_line();
|
let Some((start,end)) = (if count == 1 {
|
||||||
let end = self.find_newlines(count).0;
|
Some(self.this_line())
|
||||||
MotionKind::Inclusive((start,end))
|
} 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)) => {
|
MotionCmd(count,Motion::WordMotion(to, word, dir)) => {
|
||||||
let pos = self.dispatch_word_motion(count, 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::TextObj(text_obj, bound)) => todo!(),
|
||||||
MotionCmd(count,Motion::EndOfLastWord) => {
|
MotionCmd(count,Motion::EndOfLastWord) => {
|
||||||
let start = self.start_of_cursor_line();
|
let start = self.start_of_line();
|
||||||
let mut newline_count = 0;
|
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;
|
||||||
@@ -960,7 +961,7 @@ impl LineBuf {
|
|||||||
MotionKind::On(last)
|
MotionKind::On(last)
|
||||||
}
|
}
|
||||||
MotionCmd(_,Motion::BeginningOfFirstWord) => {
|
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 indices = self.directional_indices_iter_from(start,Direction::Forward);
|
||||||
let mut first_graphical = None;
|
let mut first_graphical = None;
|
||||||
while let Some(idx) = indices.next() {
|
while let Some(idx) = indices.next() {
|
||||||
@@ -978,9 +979,16 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
MotionKind::On(first)
|
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) => {
|
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)
|
MotionKind::On(pos)
|
||||||
}
|
}
|
||||||
MotionCmd(count,Motion::CharSearch(direction, dest, ch)) => {
|
MotionCmd(count,Motion::CharSearch(direction, dest, ch)) => {
|
||||||
@@ -1008,8 +1016,29 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
MotionKind::Onto(pos.get())
|
MotionKind::Onto(pos.get())
|
||||||
}
|
}
|
||||||
MotionCmd(count,Motion::BackwardChar) => MotionKind::On(self.cursor.ret_sub(1)),
|
MotionCmd(count,motion @ (Motion::ForwardChar | Motion::BackwardChar)) => {
|
||||||
MotionCmd(count,Motion::ForwardChar) => MotionKind::On(self.cursor.ret_add_inclusive(1)),
|
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::LineDown) |
|
||||||
MotionCmd(count,Motion::LineUp) => {
|
MotionCmd(count,Motion::LineUp) => {
|
||||||
let Some((start,end)) = (match motion.1 {
|
let Some((start,end)) = (match motion.1 {
|
||||||
@@ -1017,12 +1046,11 @@ impl LineBuf {
|
|||||||
Motion::LineDown => self.nth_next_line(1),
|
Motion::LineDown => self.nth_next_line(1),
|
||||||
_ => unreachable!()
|
_ => unreachable!()
|
||||||
}) else {
|
}) else {
|
||||||
flog!(WARN, "failed to find target line");
|
|
||||||
return MotionKind::Null
|
return MotionKind::Null
|
||||||
};
|
};
|
||||||
flog!(DEBUG, self.slice(start..end));
|
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
|
col
|
||||||
} else {
|
} else {
|
||||||
let col = self.cursor_col();
|
let col = self.cursor_col();
|
||||||
@@ -1033,15 +1061,21 @@ impl LineBuf {
|
|||||||
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
|
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
|
||||||
return MotionKind::Null
|
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 {
|
let (start,end) = match motion.1 {
|
||||||
Motion::LineUp => (start,self.end_of_cursor_line()),
|
Motion::LineUp => (start,self.end_of_line()),
|
||||||
Motion::LineDown => (self.start_of_cursor_line(),end),
|
Motion::LineDown => (self.start_of_line(),end),
|
||||||
_ => unreachable!()
|
_ => unreachable!()
|
||||||
};
|
};
|
||||||
|
|
||||||
MotionKind::InclusiveWithTarget((start,end),target_pos)
|
MotionKind::InclusiveWithTargetCol((start,end),target_pos)
|
||||||
}
|
}
|
||||||
MotionCmd(count,Motion::LineDownCharwise) |
|
MotionCmd(count,Motion::LineDownCharwise) |
|
||||||
MotionCmd(count,Motion::LineUpCharwise) => {
|
MotionCmd(count,Motion::LineUpCharwise) => {
|
||||||
@@ -1149,9 +1183,13 @@ impl LineBuf {
|
|||||||
std::cmp::Ordering::Equal => { /* Do nothing */ }
|
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::Inclusive((start,_)) |
|
||||||
MotionKind::ExclusiveWithTarget((_,_),start) |
|
|
||||||
MotionKind::Exclusive((start,_)) => {
|
MotionKind::Exclusive((start,_)) => {
|
||||||
self.cursor.set(start)
|
self.cursor.set(start)
|
||||||
}
|
}
|
||||||
@@ -1183,14 +1221,14 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
ordered(self.cursor.get(), pos)
|
ordered(self.cursor.get(), pos)
|
||||||
}
|
}
|
||||||
MotionKind::InclusiveWithTarget((start,end),_) |
|
MotionKind::InclusiveWithTargetCol((start,end),_) |
|
||||||
MotionKind::Inclusive((start,end)) => {
|
MotionKind::Inclusive((start,end)) => ordered(*start, *end),
|
||||||
|
MotionKind::ExclusiveWithTargetCol((start,end),_) |
|
||||||
|
MotionKind::Exclusive((start,end)) => {
|
||||||
let (start, mut end) = ordered(*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)
|
(start,end)
|
||||||
}
|
}
|
||||||
MotionKind::ExclusiveWithTarget((start,end),_) |
|
|
||||||
MotionKind::Exclusive((start,end)) => ordered(*start, *end),
|
|
||||||
MotionKind::Null => return None
|
MotionKind::Null => return None
|
||||||
};
|
};
|
||||||
Some(range)
|
Some(range)
|
||||||
@@ -1212,8 +1250,12 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
register.write_to_register(register_text);
|
register.write_to_register(register_text);
|
||||||
match motion {
|
match motion {
|
||||||
MotionKind::ExclusiveWithTarget((_,_),pos) |
|
MotionKind::ExclusiveWithTargetCol((_,_),pos) |
|
||||||
MotionKind::InclusiveWithTarget((_,_),pos) => self.cursor.set(pos),
|
MotionKind::InclusiveWithTargetCol((_,_),pos) => {
|
||||||
|
let (start,end) = self.this_line();
|
||||||
|
self.cursor.set(start);
|
||||||
|
self.cursor.add(end.min(pos));
|
||||||
|
}
|
||||||
_ => self.cursor.set(start),
|
_ => self.cursor.set(start),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1227,8 +1269,55 @@ impl LineBuf {
|
|||||||
self.buffer.replace_range(start..end, &rot13);
|
self.buffer.replace_range(start..end, &rot13);
|
||||||
self.cursor.set(start);
|
self.cursor.set(start);
|
||||||
}
|
}
|
||||||
Verb::ReplaceChar(_) => todo!(),
|
Verb::ReplaceChar(ch) => {
|
||||||
Verb::ToggleCase => todo!(),
|
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::ToLower => todo!(),
|
||||||
Verb::ToUpper => todo!(),
|
Verb::ToUpper => todo!(),
|
||||||
Verb::Complete => todo!(),
|
Verb::Complete => todo!(),
|
||||||
@@ -1252,14 +1341,8 @@ impl LineBuf {
|
|||||||
Verb::Indent => todo!(),
|
Verb::Indent => todo!(),
|
||||||
Verb::Dedent => todo!(),
|
Verb::Dedent => todo!(),
|
||||||
Verb::Equalize => todo!(),
|
Verb::Equalize => todo!(),
|
||||||
Verb::AcceptLineOrNewline => todo!(),
|
|
||||||
Verb::EndOfFile => {
|
|
||||||
if self.buffer.is_empty() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Verb::InsertModeLineBreak(anchor) => {
|
Verb::InsertModeLineBreak(anchor) => {
|
||||||
let end = self.end_of_cursor_line();
|
let end = self.end_of_line();
|
||||||
self.insert_at(end,'\n');
|
self.insert_at(end,'\n');
|
||||||
self.cursor.set(end);
|
self.cursor.set(end);
|
||||||
match anchor {
|
match anchor {
|
||||||
@@ -1268,16 +1351,15 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Verb::ReplaceMode |
|
Verb::EndOfFile |
|
||||||
Verb::InsertMode |
|
Verb::InsertMode |
|
||||||
Verb::NormalMode |
|
Verb::NormalMode |
|
||||||
Verb::VisualMode |
|
Verb::VisualMode |
|
||||||
|
Verb::ReplaceMode |
|
||||||
Verb::VisualModeLine |
|
Verb::VisualModeLine |
|
||||||
Verb::VisualModeBlock |
|
Verb::VisualModeBlock |
|
||||||
Verb::VisualModeSelectLast => {
|
Verb::AcceptLineOrNewline |
|
||||||
/* Already handled */
|
Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
|
||||||
self.apply_motion(motion);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
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_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_line_motion = cmd.is_line_motion();
|
||||||
let is_undo_op = cmd.is_undo_op();
|
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);
|
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
|
||||||
|
|
||||||
// Merge character inserts into one edit
|
// Merge character inserts into one edit
|
||||||
@@ -1297,13 +1380,13 @@ impl LineBuf {
|
|||||||
|
|
||||||
let ViCmd { register, verb, motion, raw_seq: _ } = cmd;
|
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 motion_count = motion.as_ref().map(|m| m.0);
|
||||||
|
|
||||||
let before = self.buffer.clone();
|
let before = self.buffer.clone();
|
||||||
let cursor_pos = self.cursor.get();
|
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
|
* 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
|
||||||
@@ -1320,6 +1403,19 @@ impl LineBuf {
|
|||||||
|
|
||||||
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)?;
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
self.apply_motion(motion_eval);
|
self.apply_motion(motion_eval);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ impl FernVi {
|
|||||||
|
|
||||||
pub fn get_layout(&mut self) -> Layout {
|
pub fn get_layout(&mut self) -> Layout {
|
||||||
let line = self.editor.as_str().to_string();
|
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)
|
self.writer.get_layout_from_parts(&self.prompt, to_cursor, &line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ impl ViCmd {
|
|||||||
pub fn is_undo_op(&self) -> bool {
|
pub fn is_undo_op(&self) -> bool {
|
||||||
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
|
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 {
|
pub fn is_line_motion(&self) -> bool {
|
||||||
self.motion.as_ref().is_some_and(|m| {
|
self.motion.as_ref().is_some_and(|m| {
|
||||||
matches!(m.1,
|
matches!(m.1,
|
||||||
@@ -165,7 +169,8 @@ pub enum Verb {
|
|||||||
Yank,
|
Yank,
|
||||||
Rot13, // lol
|
Rot13, // lol
|
||||||
ReplaceChar(char),
|
ReplaceChar(char),
|
||||||
ToggleCase,
|
ToggleCaseSingle,
|
||||||
|
ToggleCaseRange,
|
||||||
ToLower,
|
ToLower,
|
||||||
ToUpper,
|
ToUpper,
|
||||||
Complete,
|
Complete,
|
||||||
@@ -203,7 +208,8 @@ impl Verb {
|
|||||||
Self::ReplaceChar(_) |
|
Self::ReplaceChar(_) |
|
||||||
Self::ToLower |
|
Self::ToLower |
|
||||||
Self::ToUpper |
|
Self::ToUpper |
|
||||||
Self::ToggleCase |
|
Self::ToggleCaseRange |
|
||||||
|
Self::ToggleCaseSingle |
|
||||||
Self::Put(_) |
|
Self::Put(_) |
|
||||||
Self::ReplaceMode |
|
Self::ReplaceMode |
|
||||||
Self::InsertModeLineBreak(_) |
|
Self::InsertModeLineBreak(_) |
|
||||||
@@ -221,7 +227,8 @@ impl Verb {
|
|||||||
Self::Delete |
|
Self::Delete |
|
||||||
Self::Change |
|
Self::Change |
|
||||||
Self::ReplaceChar(_) |
|
Self::ReplaceChar(_) |
|
||||||
Self::ToggleCase |
|
Self::ToggleCaseRange |
|
||||||
|
Self::ToggleCaseSingle |
|
||||||
Self::ToLower |
|
Self::ToLower |
|
||||||
Self::ToUpper |
|
Self::ToUpper |
|
||||||
Self::RepeatLast |
|
Self::RepeatLast |
|
||||||
|
|||||||
@@ -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_clone.next();
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -465,8 +480,8 @@ impl ViNormal {
|
|||||||
return Some(
|
return Some(
|
||||||
ViCmd {
|
ViCmd {
|
||||||
register,
|
register,
|
||||||
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
|
verb: Some(VerbCmd(count, Verb::ReplaceChar(ch))),
|
||||||
motion: Some(MotionCmd(count, Motion::ForwardChar)),
|
motion: None,
|
||||||
raw_seq: self.take_cmd()
|
raw_seq: self.take_cmd()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -485,8 +500,8 @@ impl ViNormal {
|
|||||||
return Some(
|
return Some(
|
||||||
ViCmd {
|
ViCmd {
|
||||||
register,
|
register,
|
||||||
verb: Some(VerbCmd(1, Verb::ToggleCase)),
|
verb: Some(VerbCmd(count, Verb::ToggleCaseSingle)),
|
||||||
motion: Some(MotionCmd(count, Motion::ForwardChar)),
|
motion: None,
|
||||||
raw_seq: self.take_cmd()
|
raw_seq: self.take_cmd()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -654,6 +669,9 @@ impl ViNormal {
|
|||||||
('c', Some(VerbCmd(_,Verb::Change))) |
|
('c', Some(VerbCmd(_,Verb::Change))) |
|
||||||
('y', Some(VerbCmd(_,Verb::Yank))) |
|
('y', Some(VerbCmd(_,Verb::Yank))) |
|
||||||
('=', Some(VerbCmd(_,Verb::Equalize))) |
|
('=', 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::Indent))) |
|
||||||
('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)),
|
('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)),
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -1145,7 +1163,7 @@ impl ViVisual {
|
|||||||
return Some(
|
return Some(
|
||||||
ViCmd {
|
ViCmd {
|
||||||
register,
|
register,
|
||||||
verb: Some(VerbCmd(1, Verb::ToggleCase)),
|
verb: Some(VerbCmd(1, Verb::ToggleCaseRange)),
|
||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd()
|
raw_seq: self.take_cmd()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,14 @@ use crate::prompt::readline::{linebuf::LineBuf, vimode::{ViInsert, ViMode, ViNor
|
|||||||
|
|
||||||
use super::super::*;
|
use super::super::*;
|
||||||
|
|
||||||
|
fn normal_cmd(cmd: &str, buf: &str, cursor: usize, expected_buf: &str, expected_cursor: usize) -> bool {
|
||||||
fn assert_normal_cmd(cmd: &str, start: &str, cursor: usize, expected_buf: &str, expected_cursor: usize) {
|
|
||||||
let cmd = ViNormal::new()
|
let cmd = ViNormal::new()
|
||||||
.cmds_from_raw(cmd)
|
.cmds_from_raw(cmd)
|
||||||
.pop()
|
.pop()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut buf = LineBuf::new().with_initial(start, cursor);
|
let mut buf = LineBuf::new().with_initial(buf, cursor);
|
||||||
buf.exec_cmd(cmd).unwrap();
|
buf.exec_cmd(cmd).unwrap();
|
||||||
assert_eq!(buf.as_str(),expected_buf);
|
buf.as_str() == expected_buf && buf.cursor.get() == expected_cursor
|
||||||
assert_eq!(buf.cursor.get(),expected_cursor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 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 mut buf = LineBuf::new().with_initial(initial, 57);
|
||||||
let (start,end) = buf.this_line();
|
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]
|
#[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 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 mut buf = LineBuf::new().with_initial(initial, 57);
|
||||||
let (start,end) = buf.nth_prev_line(1).unwrap();
|
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]
|
#[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 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 mut buf = LineBuf::new().with_initial(initial, 36);
|
||||||
let (start,end) = buf.nth_prev_line(1).unwrap();
|
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]
|
#[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 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 mut buf = LineBuf::new().with_initial(initial, 57);
|
||||||
let (start,end) = buf.nth_next_line(1).unwrap();
|
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]
|
#[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 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 mut buf = LineBuf::new().with_initial(initial, 81);
|
||||||
let (start,end) = buf.nth_next_line(1).unwrap();
|
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]
|
#[test]
|
||||||
@@ -146,7 +144,7 @@ fn linebuf_next_line_only_newlines() {
|
|||||||
let mut buf = LineBuf::new().with_initial(initial, 7);
|
let mut buf = LineBuf::new().with_initial(initial, 7);
|
||||||
let (start,end) = buf.nth_next_line(1).unwrap();
|
let (start,end) = buf.nth_next_line(1).unwrap();
|
||||||
assert_eq!(start, 8);
|
assert_eq!(start, 8);
|
||||||
assert_eq!(buf.slice(start..end), Some(""))
|
assert_eq!(buf.slice(start..end), Some("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 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 mut buf = LineBuf::new().with_initial(initial, 7);
|
||||||
let (start,end) = buf.nth_prev_line(1).unwrap();
|
let (start,end) = buf.nth_prev_line(1).unwrap();
|
||||||
|
assert_eq!(buf.slice(start..end), Some("\n"));
|
||||||
assert_eq!(start, 6);
|
assert_eq!(start, 6);
|
||||||
assert_eq!(buf.slice(start..end), Some(""))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -202,90 +200,141 @@ fn linebuf_cursor_motion() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn editor_delete_word() {
|
fn editor_delete_word() {
|
||||||
assert_normal_cmd(
|
assert!(normal_cmd(
|
||||||
"dw",
|
"dw",
|
||||||
"The quick brown fox jumps over the lazy dog",
|
"The quick brown fox jumps over the lazy dog",
|
||||||
16,
|
16,
|
||||||
"The quick brown jumps over the lazy dog",
|
"The quick brown jumps over the lazy dog",
|
||||||
16
|
16
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn editor_delete_backwards() {
|
fn editor_delete_backwards() {
|
||||||
assert_normal_cmd(
|
assert!(normal_cmd(
|
||||||
"2db",
|
"2db",
|
||||||
"The quick brown fox jumps over the lazy dog",
|
"The quick brown fox jumps over the lazy dog",
|
||||||
16,
|
16,
|
||||||
"The fox jumps over the lazy dog",
|
"The fox jumps over the lazy dog",
|
||||||
4
|
4
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn editor_rot13_five_words_backwards() {
|
fn editor_rot13_five_words_backwards() {
|
||||||
assert_normal_cmd(
|
assert!(normal_cmd(
|
||||||
"g?5b",
|
"g?5b",
|
||||||
"The quick brown fox jumps over the lazy dog",
|
"The quick brown fox jumps over the lazy dog",
|
||||||
31,
|
31,
|
||||||
"The dhvpx oebja sbk whzcf bire the lazy dog",
|
"The dhvpx oebja sbk whzcf bire the lazy dog",
|
||||||
4
|
4
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn editor_delete_word_on_whitespace() {
|
fn editor_delete_word_on_whitespace() {
|
||||||
assert_normal_cmd(
|
assert!(normal_cmd(
|
||||||
"dw",
|
"dw",
|
||||||
"The quick brown fox",
|
"The quick brown fox",
|
||||||
10, // on the whitespace between "quick" and "brown"
|
10, // on the whitespace between "quick" and "brown"
|
||||||
"The quick brown fox",
|
"The quick brown fox",
|
||||||
10
|
10
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn editor_delete_5_words() {
|
fn editor_delete_5_words() {
|
||||||
assert_normal_cmd(
|
assert!(normal_cmd(
|
||||||
"5dw",
|
"5dw",
|
||||||
"The quick brown fox jumps over the lazy dog",
|
"The quick brown fox jumps over the lazy dog",
|
||||||
16,
|
16,
|
||||||
"The quick brown dog",
|
"The quick brown dog",
|
||||||
16
|
16
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn editor_delete_end_includes_last() {
|
fn editor_delete_end_includes_last() {
|
||||||
assert_normal_cmd(
|
assert!(normal_cmd(
|
||||||
"de",
|
"de",
|
||||||
"The quick brown fox::::jumps over the lazy dog",
|
"The quick brown fox::::jumps over the lazy dog",
|
||||||
16,
|
16,
|
||||||
"The quick brown ::::jumps over the lazy dog",
|
"The quick brown ::::jumps over the lazy dog",
|
||||||
16
|
16
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn editor_delete_end_unicode_word() {
|
fn editor_delete_end_unicode_word() {
|
||||||
assert_normal_cmd(
|
assert!(normal_cmd(
|
||||||
"de",
|
"de",
|
||||||
"naïve café world",
|
"naïve café world",
|
||||||
0,
|
0,
|
||||||
" café world", // deletes "naïve"
|
" café world", // deletes "naïve"
|
||||||
0
|
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]
|
#[test]
|
||||||
fn editor_delete_line_up() {
|
fn editor_delete_line_up() {
|
||||||
assert_normal_cmd(
|
assert!(normal_cmd(
|
||||||
"dk",
|
"dk",
|
||||||
LOREM_IPSUM,
|
LOREM_IPSUM,
|
||||||
237,
|
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.",
|
"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.",
|
||||||
126,
|
240,
|
||||||
)
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user