more work on vi stuff

This commit is contained in:
2025-05-23 02:14:56 -04:00
parent ac170d31c8
commit 9db6137934
5 changed files with 1176 additions and 427 deletions

View File

@@ -1,8 +1,9 @@
use std::{fmt::Display, ops::{Deref, DerefMut, Range}, sync::Arc};
use std::{fmt::Display, ops::{Deref, DerefMut, Range, RangeBounds, RangeInclusive}, sync::Arc};
use unicode_width::UnicodeWidthStr;
use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}};
use crate::prelude::*;
use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, RegisterName, TextObj, To, Verb, ViCmd, Word};
@@ -12,15 +13,35 @@ pub enum CharClass {
Symbol
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MotionKind {
Forward(usize),
To(usize),
Backward(usize),
Range(usize,usize),
Range(Range<usize>),
Null
}
impl MotionKind {
pub fn range<R: RangeBounds<usize>>(range: R) -> Self {
let start = match range.start_bound() {
std::ops::Bound::Included(&start) => start,
std::ops::Bound::Excluded(&start) => start + 1,
std::ops::Bound::Unbounded => 0
};
let end = match range.end_bound() {
std::ops::Bound::Included(&end) => end,
std::ops::Bound::Excluded(&end) => end + 1,
std::ops::Bound::Unbounded => panic!("called range constructor with no upper bound")
};
if end > start {
Self::Range(start..end)
} else {
Self::Range(end..start)
}
}
}
#[derive(Clone,Default,Debug)]
pub struct TermCharBuf(pub Vec<TermChar>);
@@ -162,10 +183,71 @@ fn is_other_class_or_ws(a: &TermChar, b: &TermChar) -> bool {
CharClass::from(a) != CharClass::from(b)
}
pub struct UndoPayload {
buffer: TermCharBuf,
cursor: usize
}
#[derive(Default,Debug)]
pub struct Edit {
pub pos: usize,
pub cursor_pos: usize,
pub old: TermCharBuf,
pub new: TermCharBuf
}
impl Edit {
pub fn diff(a: TermCharBuf, b: TermCharBuf, old_cursor_pos: usize) -> Self {
use std::cmp::min;
let mut start = 0;
let max_start = min(a.len(), b.len());
// Calculate the prefix of the edit
while start < max_start && a[start] == b[start] {
start += 1;
}
if start == a.len() && start == b.len() {
return Edit {
pos: start,
cursor_pos: old_cursor_pos,
old: TermCharBuf(vec![]),
new: TermCharBuf(vec![]),
}
}
let mut end_a = a.len();
let mut end_b = b.len();
// Calculate the suffix of the edit
while end_a > start && end_b > start && a[end_a - 1] == b[end_b - 1] {
end_a -= 1;
end_b -= 1;
}
// Slice off the prefix and suffix for both
let old = TermCharBuf(a[start..end_a].to_vec());
let new = TermCharBuf(b[start..end_b].to_vec());
Edit {
pos: start,
cursor_pos: old_cursor_pos,
old,
new
}
}
}
#[derive(Default,Debug)]
pub struct LineBuf {
buffer: TermCharBuf,
cursor: usize,
clamp_cursor: bool,
merge_edit: bool,
undo_stack: Vec<Edit>,
redo_stack: Vec<Edit>,
term_dims: (usize,usize)
}
impl LineBuf {
@@ -179,6 +261,9 @@ impl LineBuf {
}
self
}
pub fn set_cursor_clamp(&mut self, yn: bool) {
self.clamp_cursor = yn
}
pub fn buffer(&self) -> &TermCharBuf {
&self.buffer
}
@@ -197,8 +282,26 @@ impl LineBuf {
let cursor = self.cursor();
self.buffer.insert(cursor,tc)
}
pub fn count_lines(&self) -> usize {
self.buffer.iter().filter(|&c| c == &TermChar::Newline).count()
pub fn count_lines(&self, first_line_offset: usize) -> usize {
let mut cur_line_len = 0;
let mut lines = 1;
let first_line_max_len = self.term_dims.1.saturating_sub(first_line_offset);
for char in self.buffer.iter() {
match char {
TermChar::Newline => {
lines += 1;
cur_line_len = 0;
}
TermChar::Grapheme(str) => {
cur_line_len += str.width().max(1);
if (lines == 1 && first_line_max_len > 0 && cur_line_len >= first_line_max_len) || cur_line_len > self.term_dims.1 {
lines += 1;
cur_line_len = 0;
}
}
}
}
lines
}
pub fn cursor_back(&mut self, count: usize) {
self.cursor = self.cursor.saturating_sub(count)
@@ -217,13 +320,23 @@ impl LineBuf {
self.cursor = self.cursor.saturating_sub(1)
}
}
pub fn cursor_display_coords(&self) -> (usize, usize) {
pub fn update_term_dims(&mut self, x: usize, y: usize) {
self.term_dims = (x,y)
}
pub fn cursor_display_coords(&self, first_line_offset: Option<usize>) -> (usize, usize) {
let mut x = 0;
let mut y = 0;
let first_line_max_len = first_line_offset.map(|fl| self.term_dims.1.saturating_sub(fl)).unwrap_or_default();
for i in 0..self.cursor() {
let ch = self.get_char(i).unwrap();
match ch {
TermChar::Grapheme(str) => x += str.width().max(1),
TermChar::Grapheme(str) => {
x += str.width().max(1);
if (y == 0 && first_line_max_len > 0 && x >= first_line_max_len) || x > self.term_dims.1 {
y += 1;
x = 0;
}
}
TermChar::Newline => {
y += 1;
x = 0;
@@ -246,20 +359,6 @@ impl LineBuf {
lines.push(cur_line);
lines
}
pub fn display_lines(&self) -> Vec<String> {
let line_bullet = "".styled(Style::Dim);
self.split_lines()
.into_iter()
.enumerate()
.map(|(i, line)| {
if i == 0 {
line.to_string()
} else {
format!("{line_bullet}{line}")
}
})
.collect()
}
pub fn on_word_bound(&self, word: Word, pos: usize, dir: Direction) -> bool {
let check_pos = match dir {
Direction::Forward => self.num_or_len(pos + 1),
@@ -276,6 +375,7 @@ impl LineBuf {
})
}
fn backward_until<F: Fn(&TermChar) -> bool>(&self, mut start: usize, cond: F, inclusive: bool) -> usize {
start = self.num_or_len_minus_one(start);
while start > 0 && !cond(&self.buffer[start]) {
start -= 1;
}
@@ -401,7 +501,18 @@ impl LineBuf {
pos = self.backward_until(pos, |c| c.is_whitespace(), false);
}
}
To::End => unreachable!()
To::End => {
if self.on_word_bound(word, pos, dir) {
pos = pos.saturating_sub(1);
}
if self.get_char(pos).is_some_and(|c| c.is_whitespace()) {
pos = self.backward_until(pos, |c| !c.is_whitespace(), true);
} else {
pos = self.backward_until(pos, |c| c.is_whitespace(), true);
pos = self.backward_until(pos, |c| !c.is_whitespace(), true);
}
}
}
}
Word::Normal => {
@@ -419,7 +530,24 @@ impl LineBuf {
pos = self.backward_until(pos, |c| is_other_class_or_ws(this_char, c), false);
}
}
To::End => unreachable!()
To::End => {
if self.on_word_bound(word, pos, dir) {
// Nudge
pos = pos.saturating_sub(1);
}
// If we are on whitespace, proceed until we are not, inclusively
if self.get_char(pos).is_some_and(|c| c.is_whitespace()) {
pos = self.backward_until(pos, |c| !c.is_whitespace(), true)
} else {
// If we are not on whitespace, proceed until we hit something different, inclusively
let this_char = self.get_char(pos).unwrap();
pos = self.backward_until(pos, |c| is_other_class_or_ws(this_char, c), true);
// If we landed on whitespace, proceed until we are not on whitespace
if self.get_char(pos).is_some_and(|c| c.is_whitespace()) {
pos = self.backward_until(pos, |c| !c.is_whitespace(), true)
}
}
}
}
}
}
@@ -427,9 +555,196 @@ impl LineBuf {
}
pos
}
pub fn eval_quote_obj(&self, target: &str, bound: Bound) -> Range<usize> {
let mut end;
let start;
let cursor = self.cursor();
let ln_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
let mut line_chars = self.buffer[ln_start..cursor].iter();
let mut in_quote = false;
while let Some(ch) = line_chars.next() {
let TermChar::Grapheme(ch) = ch else { unreachable!() };
match ch.as_ref() {
"\\" => {
line_chars.next();
}
"\"" => in_quote = !in_quote,
_ => { /* continue */ }
}
}
let mut start_pos = cursor;
let end_pos;
if !in_quote {
start_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches(target), true);
if !self.get_char(start_pos).is_some_and(|c| c.matches(target)) {
return cursor..cursor
}
end_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches(target), true);
if !self.get_char(end_pos).is_some_and(|c| c.matches(target)) {
return cursor..cursor
}
start = start_pos;
end = end_pos;
} else {
start_pos = self.backward_until(start_pos, |c| c.matches("\n") || c.matches(target), true);
if !self.get_char(start_pos).is_some_and(|c| c.matches(target)) {
return cursor..cursor
}
end_pos = self.forward_until(self.num_or_len(start_pos + 1), |c| c.matches("\n") || c.matches(target), true);
if !self.get_char(end_pos).is_some_and(|c| c.matches(target)) {
return cursor..cursor
}
start = start_pos;
end = self.num_or_len(end_pos + 1);
if bound == Bound::Around && self.get_char(end).is_some_and(|c| c.is_whitespace()) {
end += 1;
end = self.forward_until(end, |c| !c.is_whitespace(), true);
}
}
mk_range(start,end)
}
pub fn eval_delim_obj(&self, obj: &TextObj, bound: Bound) -> Range<usize> {
// FIXME: logic isn't completely robust i think
let opener = match obj {
TextObj::Brace => "{",
TextObj::Bracket => "[",
TextObj::Paren => "(",
TextObj::Angle => "<",
_ => unreachable!()
};
let closer = match obj {
TextObj::Brace => "}",
TextObj::Bracket => "]",
TextObj::Paren => ")",
TextObj::Angle => ">",
_ => unreachable!()
};
let mut end = None;
let mut start = None;
let mut delim_count: usize = 0;
let ln_range = self.cur_line_range();
let cursor = self.cursor();
let mut ln_chars = self.buffer[*ln_range.start()..cursor].iter().enumerate();
while let Some((i,ch)) = ln_chars.next() {
let &TermChar::Grapheme(ch) = &ch else { unreachable!() };
match ch.as_ref() {
"\\" => {
ln_chars.next();
}
ch if ch == opener => {
start = Some(ln_range.start() + i);
delim_count += 1;
}
ch if ch == closer => delim_count -= 1,
_ => {}
}
}
let mut start_pos = None;
let mut end_pos = None;
if delim_count == 0 {
let mut ln_chars = self.buffer[cursor..*ln_range.end()].iter().enumerate();
while let Some((i,ch)) = ln_chars.next() {
let &TermChar::Grapheme(ch) = &ch else { unreachable!() };
match ch.as_ref() {
"\\" => {
ln_chars.next();
}
ch if ch == opener => {
if delim_count == 0 {
start_pos = Some(cursor + i);
}
delim_count += 1;
}
ch if ch == closer => {
delim_count -= 1;
if delim_count == 0 {
end_pos = Some(cursor + i);
}
}
_ => {}
}
}
if start_pos.is_none() || end_pos.is_none() {
return cursor..cursor
} else {
start = start_pos;
end = end_pos;
}
} else {
let Some(strt) = start else {
dbg!("no start");
dbg!("no start");
dbg!("no start");
dbg!("no start");
dbg!("no start");
dbg!("no start");
return cursor..cursor
};
let strt = self.num_or_len(strt + 1); // skip the paren
let target = delim_count.saturating_sub(1);
let mut ln_chars = self.buffer[strt..*ln_range.end()].iter().enumerate();
dbg!(&ln_chars);
dbg!(&ln_chars);
dbg!(&ln_chars);
dbg!(&ln_chars);
while let Some((i,ch)) = ln_chars.next() {
let &TermChar::Grapheme(ch) = &ch else { unreachable!() };
match ch.as_ref() {
"\\" => {
ln_chars.next();
}
ch if ch == opener => {
delim_count += 1;
}
ch if ch == closer => {
delim_count -= 1;
if delim_count == target {
end_pos = Some(strt + i);
}
}
_ => {}
}
}
dbg!(end_pos);
dbg!(end_pos);
dbg!(end_pos);
dbg!(start_pos);
dbg!(start_pos);
dbg!(start_pos);
dbg!(start_pos);
dbg!(start_pos);
dbg!(start_pos);
dbg!(start_pos);
if end_pos.is_none() {
return cursor..cursor
} else {
end = end_pos;
}
}
let Some(mut start) = start else {
return cursor..cursor
};
let Some(mut end) = end else {
return cursor..cursor
};
match bound {
Bound::Inside => {
end = end.saturating_sub(1);
start = self.num_or_len(start + 1);
mk_range(start,end)
}
Bound::Around => mk_range(start,end)
}
}
pub fn eval_text_obj(&self, obj: TextObj, bound: Bound) -> Range<usize> {
let mut start = self.cursor();
let mut end = self.cursor();
let mut start;
let mut end;
match obj {
TextObj::Word(word) => {
@@ -455,84 +770,59 @@ impl LineBuf {
}
TextObj::Sentence => todo!(),
TextObj::Paragraph => todo!(),
TextObj::DoubleQuote => {
let cursor = self.cursor();
let ln_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
let mut line_chars = self.buffer[ln_start..cursor].iter();
let mut in_quote = false;
while let Some(ch) = line_chars.next() {
let TermChar::Grapheme(ch) = ch else { unreachable!() };
match ch.as_ref() {
"\\" => {
line_chars.next();
}
"\"" => in_quote = !in_quote,
_ => { /* continue */ }
}
}
let mut start_pos = cursor;
let end_pos;
if !in_quote {
start_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches("\""), true);
if !self.get_char(start_pos).is_some_and(|c| c.matches("\"")) {
return cursor..cursor
}
end_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches("\""), true);
if !self.get_char(end_pos).is_some_and(|c| c.matches("\"")) {
return cursor..cursor
}
start = start_pos;
end = end_pos;
} else {
start_pos = self.backward_until(start_pos, |c| c.matches("\n") || c.matches("\""), true);
if !self.get_char(start_pos).is_some_and(|c| c.matches("\"")) {
return cursor..cursor
}
end_pos = self.forward_until(self.num_or_len(start_pos + 1), |c| c.matches("\n") || c.matches("\""), true);
if !self.get_char(end_pos).is_some_and(|c| c.matches("\"")) {
return cursor..cursor
}
start = start_pos;
end = self.num_or_len(end_pos + 1);
if bound == Bound::Around && self.get_char(end).is_some_and(|c| c.is_whitespace()) {
end += 1;
end = self.forward_until(end, |c| !c.is_whitespace(), true);
}
}
}
TextObj::SingleQuote => todo!(),
TextObj::BacktickQuote => todo!(),
TextObj::Paren => todo!(),
TextObj::Bracket => todo!(),
TextObj::Brace => todo!(),
TextObj::Angle => todo!(),
TextObj::DoubleQuote => return self.eval_quote_obj("\"", bound),
TextObj::SingleQuote => return self.eval_quote_obj("'", bound),
TextObj::BacktickQuote => return self.eval_quote_obj("`", bound),
TextObj::Paren |
TextObj::Bracket |
TextObj::Brace |
TextObj::Angle => return self.eval_delim_obj(&obj, bound),
TextObj::Tag => todo!(),
TextObj::Custom(_) => todo!(),
}
if bound == Bound::Inside {
start = self.num_or_len(start + 1);
start = self.num_or_len_minus_one(start + 1);
end = end.saturating_sub(1);
}
start..end
}
pub fn validate_range(&self, range: &Range<usize>) -> bool {
range.end < self.buffer.len()
}
pub fn cur_line_range(&self) -> RangeInclusive<usize> {
let cursor = self.cursor();
let mut line_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
let mut line_end = self.forward_until(cursor, |c| c == &TermChar::Newline, true);
if self.get_char(line_start.saturating_sub(1)).is_none_or(|c| c != &TermChar::Newline) {
line_start = 0;
}
if self.get_char(line_end).is_none_or(|c| c != &TermChar::Newline) {
line_end = self.buffer.len().saturating_sub(1);
line_start = self.backward_until(line_start, |c| c == &TermChar::Newline, true)
}
line_start..=self.num_or_len(line_end + 1)
}
/// Clamp a number to the length of the buffer
pub fn num_or_len(&self, num: usize) -> usize {
pub fn num_or_len_minus_one(&self, num: usize) -> usize {
num.min(self.buffer.len().saturating_sub(1))
}
pub fn num_or_len(&self, num: usize) -> usize {
num.min(self.buffer.len())
}
pub fn eval_motion(&self, motion: Motion) -> MotionKind {
match motion {
Motion::WholeLine => {
let cursor = self.cursor();
let start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
let end = self.forward_until(cursor, |c| c == &TermChar::Newline, true);
MotionKind::Range(start,end)
}
Motion::WholeLine => MotionKind::range(self.cur_line_range()),
Motion::TextObj(text_obj, bound) => {
let range = self.eval_text_obj(text_obj, bound);
let range = mk_range(range.start, range.end);
MotionKind::Range(range.start,range.end)
let cursor = self.cursor();
if range.start == cursor && range.end == cursor {
MotionKind::Null
} else {
MotionKind::range(range)
}
}
Motion::BeginningOfFirstWord => {
let cursor = self.cursor();
@@ -540,17 +830,29 @@ impl LineBuf {
let first_print = self.forward_until(line_start, |c| !c.is_whitespace(), true);
MotionKind::To(first_print)
}
Motion::ToColumn(col) => {
let rng = self.cur_line_range();
let column = (*rng.start() + (col.saturating_sub(1))).min(*rng.end());
MotionKind::To(column)
}
Motion::BeginningOfLine => {
let cursor = self.cursor();
let line_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
let mut line_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false);
if self.get_char(line_start.saturating_sub(1)).is_some_and(|c| c != &TermChar::Newline) {
line_start = 0; // FIXME: not sure if this logic is correct
}
MotionKind::To(line_start)
}
Motion::EndOfLine => {
let cursor = self.cursor();
let line_end = self.forward_until(cursor, |c| c == &TermChar::Newline, false);
let mut line_end = self.forward_until(cursor, |c| c == &TermChar::Newline, false);
// If we didn't actually find a newline, we need to go to the end of the buffer
if self.get_char(line_end + 1).is_some_and(|c| c != &TermChar::Newline) {
line_end = self.buffer.len(); // FIXME: not sure if this logic is correct
}
MotionKind::To(line_end)
}
Motion::BackwardWord(word) => MotionKind::To(self.find_word_pos(word, To::Start, Direction::Backward)),
Motion::BackwardWord(dest, word) => MotionKind::To(self.find_word_pos(word, dest, Direction::Backward)),
Motion::ForwardWord(dest, word) => MotionKind::To(self.find_word_pos(word, dest, Direction::Forward)),
Motion::CharSearch(direction, dest, ch) => {
let mut cursor = self.cursor();
@@ -590,11 +892,19 @@ impl LineBuf {
MotionKind::Null
}
}
Motion::Range(s, e) => {
if self.validate_range(&(s..e)) {
let range = mk_range(s, e);
MotionKind::range(range)
} else {
MotionKind::Null
}
}
Motion::BackwardChar => MotionKind::Backward(1),
Motion::ForwardChar => MotionKind::Forward(1),
Motion::LineUp => todo!(),
Motion::LineDown => todo!(),
Motion::WholeBuffer => MotionKind::Range(0,self.buffer.len().saturating_sub(1)),
Motion::WholeBuffer => MotionKind::Range(0..self.buffer.len().saturating_sub(1)),
Motion::BeginningOfBuffer => MotionKind::To(0),
Motion::EndOfBuffer => MotionKind::To(self.buffer.len().saturating_sub(1)),
Motion::Null => MotionKind::Null,
@@ -623,9 +933,9 @@ impl LineBuf {
deleted = self.buffer.drain(back..cursor).collect::<TermCharBuf>();
self.apply_motion(MotionKind::To(back));
}
MotionKind::Range(s, e) => {
deleted = self.buffer.drain(s..e).collect::<TermCharBuf>();
self.apply_motion(MotionKind::To(s));
MotionKind::Range(r) => {
deleted = self.buffer.drain(r.clone()).collect::<TermCharBuf>();
self.apply_motion(MotionKind::To(r.start));
}
MotionKind::Null => return Ok(())
}
@@ -672,23 +982,55 @@ impl LineBuf {
.collect::<TermCharBuf>();
self.apply_motion(MotionKind::To(back));
}
MotionKind::Range(s, e) => {
yanked = self.buffer[s..e]
MotionKind::Range(r) => {
yanked = self.buffer[r.start..r.end]
.iter()
.cloned()
.collect::<TermCharBuf>();
self.apply_motion(MotionKind::To(s));
self.apply_motion(MotionKind::To(r.start));
}
MotionKind::Null => return Ok(())
}
register.write_to_register(yanked);
}
Verb::ReplaceChar(_) => todo!(),
Verb::ReplaceChar(ch) => {
let cursor = self.cursor();
if let Some(c) = self.buffer.get_mut(cursor) {
let mut tc = TermChar::from(ch);
std::mem::swap(c, &mut tc)
}
self.apply_motion(motion);
}
Verb::Substitute => todo!(),
Verb::ToggleCase => todo!(),
Verb::Complete => todo!(),
Verb::CompleteBackward => todo!(),
Verb::Undo => todo!(),
Verb::Undo => {
let Some(undo) = self.undo_stack.pop() else {
return Ok(())
};
flog!(DEBUG, undo);
let Edit { pos, cursor_pos, old, new } = undo;
let start = pos;
let end = pos + new.len();
self.buffer.0.splice(start..end, old.0.clone());
let cur_pos = self.cursor();
self.cursor = cursor_pos;
let redo = Edit { pos, cursor_pos: cur_pos, old: new, new: old };
flog!(DEBUG, redo);
self.redo_stack.push(redo);
}
Verb::Redo => {
let Some(Edit { pos, cursor_pos, old, new }) = self.redo_stack.pop() else {
return Ok(())
};
let start = pos;
let end = pos + new.len();
self.buffer.0.splice(start..end, old.0.clone());
let cur_pos = self.cursor();
self.cursor = cursor_pos;
self.undo_stack.push(Edit { pos, cursor_pos: cur_pos, old: new, new: old });
}
Verb::RepeatLast => todo!(),
Verb::Put(anchor) => {
if let Some(charbuf) = register.read_from_register() {
@@ -697,8 +1039,8 @@ impl LineBuf {
self.cursor_back(1);
}
for char in chars {
self.insert_at_cursor(char);
self.cursor_fwd(1);
self.insert_at_cursor(char);
}
}
}
@@ -720,6 +1062,19 @@ impl LineBuf {
self.cursor = 0;
}
}
Verb::InsertModeLineBreak(anchor) => {
match anchor {
Anchor::After => {
let rng = self.cur_line_range();
self.apply_motion(MotionKind::To(self.num_or_len(rng.end() + 1)));
self.insert_at_cursor('\n'.into());
self.apply_motion(MotionKind::Forward(1));
}
Anchor::Before => todo!(),
}
}
Verb::Equalize => {
}
Verb::InsertMode |
Verb::NormalMode |
Verb::VisualMode |
@@ -734,29 +1089,80 @@ impl LineBuf {
MotionKind::Forward(n) => self.cursor_fwd(n),
MotionKind::To(pos) => self.cursor_to(pos),
MotionKind::Backward(n) => self.cursor_back(n),
MotionKind::Range(s, _) => self.cursor_to(s), // TODO: not sure if this is correct in every case
MotionKind::Range(r) => self.cursor_to(r.start), // TODO: not sure if this is correct in every case
MotionKind::Null => { /* Pass */ }
}
}
pub fn handle_edit(&mut self, old: TermCharBuf, new: TermCharBuf, curs_pos: usize) {
if self.merge_edit {
let mut diff = Edit::diff(old, new, curs_pos);
let Some(mut edit) = self.undo_stack.pop() else {
self.undo_stack.push(diff);
return
};
dbg!("old");
dbg!(&edit);
edit.new.append(&mut diff.new);
dbg!("new");
dbg!(&edit);
self.undo_stack.push(edit);
} else {
let diff = Edit::diff(old, new, curs_pos);
self.undo_stack.push(diff);
}
}
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
let ViCmd { register, verb_count, verb, motion_count, motion, .. } = cmd;
flog!(DEBUG, cmd);
let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit());
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
let is_undo_op = cmd.is_undo_op();
// Merge character inserts into one edit
if self.merge_edit && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert()) {
self.merge_edit = false;
}
let ViCmd { register, verb, motion, .. } = cmd;
let verb_count = verb.as_ref().map(|v| v.0);
let motion_count = motion.as_ref().map(|m| m.0);
let before = self.buffer.clone();
let cursor_pos = self.cursor();
for _ in 0..verb_count.unwrap_or(1) {
for _ in 0..motion_count.unwrap_or(1) {
let motion = motion
.clone()
.map(|m| self.eval_motion(m))
.map(|m| self.eval_motion(m.1))
.unwrap_or(MotionKind::Null);
if let Some(verb) = verb.clone() {
self.exec_verb(verb, motion, register)?;
self.exec_verb(verb.1, motion, register)?;
} else {
self.apply_motion(motion);
}
}
}
self.clamp_cursor();
let after = self.buffer.clone();
if clear_redos {
self.redo_stack.clear();
}
if before.0 != after.0 && !is_undo_op {
self.handle_edit(before, after, cursor_pos);
}
if is_char_insert {
self.merge_edit = true;
}
if self.clamp_cursor {
self.clamp_cursor();
}
Ok(())
}
}