From 9db6137934b844d7c7eb539d8a8516f6bf405463 Mon Sep 17 00:00:00 2001 From: Kyler Clay Date: Fri, 23 May 2025 02:14:56 -0400 Subject: [PATCH] more work on vi stuff --- src/prompt/readline/linebuf.rs | 614 ++++++++++++++++++++++++------ src/prompt/readline/mod.rs | 107 +++++- src/prompt/readline/mode.rs | 657 +++++++++++++++++++++------------ src/prompt/readline/term.rs | 58 ++- src/prompt/readline/vicmd.rs | 167 +++++---- 5 files changed, 1176 insertions(+), 427 deletions(-) diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index 81d6906..48ef3a4 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -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), Null } +impl MotionKind { + pub fn range>(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); @@ -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, + redo_stack: Vec, + 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) { 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 { - 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 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 { + 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 { + // 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 { - 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) -> bool { + range.end < self.buffer.len() + } + pub fn cur_line_range(&self) -> RangeInclusive { + 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::(); self.apply_motion(MotionKind::To(back)); } - MotionKind::Range(s, e) => { - deleted = self.buffer.drain(s..e).collect::(); - self.apply_motion(MotionKind::To(s)); + MotionKind::Range(r) => { + deleted = self.buffer.drain(r.clone()).collect::(); + self.apply_motion(MotionKind::To(r.start)); } MotionKind::Null => return Ok(()) } @@ -672,23 +982,55 @@ impl LineBuf { .collect::(); 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::(); - 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(()) } } diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 1bb8752..56d65ea 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -4,9 +4,10 @@ use linebuf::{strip_ansi_codes_and_escapes, LineBuf, TermCharBuf}; use mode::{CmdReplay, ViInsert, ViMode, ViNormal}; use term::Terminal; use unicode_width::UnicodeWidthStr; -use vicmd::{Verb, ViCmd}; +use vicmd::{MotionCmd, RegisterName, Verb, VerbCmd, ViCmd}; -use crate::libsh::{error::ShResult, term::{Style, Styled}}; +use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}}; +use crate::prelude::*; pub mod keys; pub mod term; @@ -20,7 +21,8 @@ pub struct FernVi { line: LineBuf, prompt: String, mode: Box, - repeat_action: Option, + last_action: Option, + last_movement: Option, } impl FernVi { @@ -33,19 +35,22 @@ impl FernVi { line, prompt, mode: Box::new(ViInsert::new()), - repeat_action: None, + last_action: None, + last_movement: None, } } pub fn clear_line(&self) { let prompt_lines = self.prompt.lines().count(); + let last_line_len = strip_ansi_codes_and_escapes(self.prompt.lines().last().unwrap_or_default()).width(); let buf_lines = if self.prompt.ends_with('\n') { - self.line.count_lines() + self.line.count_lines(last_line_len) } else { // The prompt does not end with a newline, so one of the buffer's lines overlaps with it - self.line.count_lines().saturating_sub(1) + self.line.count_lines(last_line_len).saturating_sub(1) }; let total = prompt_lines + buf_lines; self.term.write_bytes(b"\r\n"); + self.term.write_bytes(format!("\r\x1b[{total}B").as_bytes()); for _ in 0..total { self.term.write_bytes(b"\r\x1b[2K\x1b[1A"); } @@ -57,7 +62,7 @@ impl FernVi { } let mut prompt_lines = self.prompt.lines().peekable(); let mut last_line_len = 0; - let lines = self.line.display_lines(); + let lines = self.line.split_lines(); while let Some(line) = prompt_lines.next() { if prompt_lines.peek().is_none() { last_line_len = strip_ansi_codes_and_escapes(line).width(); @@ -66,9 +71,9 @@ impl FernVi { self.term.writeln(line); } } - let num_lines = lines.len(); let mut lines_iter = lines.into_iter().peekable(); + let pos = self.term.cursor_pos(); while let Some(line) = lines_iter.next() { if lines_iter.peek().is_some() { self.term.writeln(&line); @@ -76,23 +81,30 @@ impl FernVi { self.term.write(&line); } } + self.term.move_cursor_to(pos); - let (x, y) = self.line.cursor_display_coords(); - let y = num_lines.saturating_sub(y + 1); + let (x, y) = self.line.cursor_display_coords(Some(last_line_len)); if y > 0 { - self.term.write(&format!("\r\x1b[{}A", y)); + self.term.write(&format!("\r\x1b[{}B", y)); } - // Add prompt offset to X only if cursor is on the last line (y == 0) + let cursor_x = if y == 0 { x + last_line_len } else { x }; - self.term.write(&format!("\r\x1b[{}C", cursor_x)); + if cursor_x > 0 { + self.term.write(&format!("\r\x1b[{}C", cursor_x)); + } self.term.write(&self.mode.cursor_style()); } pub fn readline(&mut self) -> ShResult { + let dims = self.term.get_dimensions()?; + self.line.update_term_dims(dims.0, dims.1); self.print_buf(false); loop { + let dims = self.term.get_dimensions()?; + self.line.update_term_dims(dims.0, dims.1); + let key = self.term.read_key(); let Some(cmd) = self.mode.handle_key(key) else { continue @@ -109,9 +121,16 @@ impl FernVi { pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { if cmd.is_mode_transition() { let count = cmd.verb_count(); - let mut mode: Box = match cmd.verb().unwrap() { - Verb::InsertMode => Box::new(ViInsert::new().with_count(count)), - Verb::NormalMode => Box::new(ViNormal::new()), + let mut mode: Box = match cmd.verb().unwrap().1 { + Verb::InsertModeLineBreak(_) | + Verb::InsertMode => { + self.line.set_cursor_clamp(false); + Box::new(ViInsert::new().with_count(count as u16)) + } + Verb::NormalMode => { + self.line.set_cursor_clamp(true); + Box::new(ViNormal::new()) + } Verb::VisualMode => todo!(), Verb::OverwriteMode => todo!(), _ => unreachable!() @@ -121,10 +140,60 @@ impl FernVi { self.term.write(&mode.cursor_style()); if mode.is_repeatable() { - self.repeat_action = mode.as_replay(); + self.last_action = mode.as_replay(); } - } - self.line.exec_cmd(cmd)?; + } else if cmd.is_cmd_repeat() { + let Some(replay) = self.last_action.clone() else { + return Ok(()) + }; + let ViCmd { register, verb, motion, raw_seq } = cmd; + let VerbCmd(count,_) = verb.unwrap(); + match replay { + CmdReplay::ModeReplay { cmds, mut repeat } => { + if count > 1 { + repeat = count as u16; + } + for _ in 0..repeat { + let cmds = cmds.clone(); + for cmd in cmds { + self.line.exec_cmd(cmd)? + } + } + } + CmdReplay::Single(mut cmd) => { + if count > 1 { + // Override the counts with the one passed to the '.' command + if cmd.verb.is_some() { + cmd.verb.as_mut().map(|v| v.0 = count); + cmd.motion.as_mut().map(|m| m.0 = 0); + } else { + return Ok(()) // it has to have a verb to be repeatable, something weird happened + } + } + self.line.exec_cmd(cmd)?; + } + _ => unreachable!("motions should be handled in the other branch") + } + return Ok(()) + } else if cmd.is_motion_repeat() { + match cmd.verb.as_ref().unwrap().1 { + Verb::RepeatMotion => { + let Some(motion) = self.last_movement.clone() else { + return Ok(()) + }; + let repeat_cmd = ViCmd { + register: RegisterName::default(), + verb: None, + motion: Some(motion), + raw_seq: ";".into() + }; + self.line.exec_cmd(repeat_cmd)?; + } + Verb::RepeatMotionRev => {} + _ => unreachable!() + } + } + self.line.exec_cmd(cmd.clone())?; Ok(()) } } diff --git a/src/prompt/readline/mode.rs b/src/prompt/readline/mode.rs index 4fcf3f0..72b2632 100644 --- a/src/prompt/readline/mode.rs +++ b/src/prompt/readline/mode.rs @@ -1,16 +1,36 @@ +use std::iter::Peekable; +use std::str::Chars; + +use nix::NixPath; + use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; use super::linebuf::TermChar; -use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, TextObj, To, Verb, VerbBuilder, ViCmd, Word}; +use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, MotionCmd, RegisterName, TextObj, To, Verb, VerbBuilder, VerbCmd, ViCmd, Word}; +use crate::prelude::*; -pub struct CmdReplay { - cmds: Vec, - repeat: u16 +#[derive(Debug,Clone)] +pub enum CmdReplay { + ModeReplay { cmds: Vec, repeat: u16 }, + Single(ViCmd), + Motion(Motion) } impl CmdReplay { - pub fn new(cmds: Vec, repeat: u16) -> Self { - Self { cmds, repeat } + pub fn mode(cmds: Vec, repeat: u16) -> Self { + Self::ModeReplay { cmds, repeat } } + pub fn single(cmd: ViCmd) -> Self { + Self::Single(cmd) + } + pub fn motion(motion: Motion) -> Self { + Self::Motion(motion) + } +} + +pub enum CmdState { + Pending, + Complete, + Invalid } pub trait ViMode { @@ -18,6 +38,7 @@ pub trait ViMode { fn is_repeatable(&self) -> bool; fn as_replay(&self) -> Option; fn cursor_style(&self) -> String; + fn pending_seq(&self) -> Option; } #[derive(Default,Debug)] @@ -53,36 +74,36 @@ impl ViMode for ViInsert { match key { E(K::Grapheme(ch), M::NONE) => { let ch = TermChar::from(ch); - self.pending_cmd.set_verb(Verb::InsertChar(ch)); - self.pending_cmd.set_motion(Motion::ForwardChar); + self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar(ch))); + self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)); self.register_and_return() } E(K::Char(ch), M::NONE) => { - self.pending_cmd.set_verb(Verb::InsertChar(TermChar::from(ch))); - self.pending_cmd.set_motion(Motion::ForwardChar); + self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar(TermChar::from(ch)))); + self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)); self.register_and_return() } E(K::Char('H'), M::CTRL) | E(K::Backspace, M::NONE) => { - self.pending_cmd.set_verb(Verb::Delete); - self.pending_cmd.set_motion(Motion::BackwardChar); + self.pending_cmd.set_verb(VerbCmd(1,Verb::Delete)); + self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); self.register_and_return() } E(K::BackTab, M::NONE) => { - self.pending_cmd.set_verb(Verb::CompleteBackward); + self.pending_cmd.set_verb(VerbCmd(1,Verb::CompleteBackward)); self.register_and_return() } E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => { - self.pending_cmd.set_verb(Verb::Complete); + self.pending_cmd.set_verb(VerbCmd(1,Verb::Complete)); self.register_and_return() } E(K::Esc, M::NONE) => { - self.pending_cmd.set_verb(Verb::NormalMode); - self.pending_cmd.set_motion(Motion::BackwardChar); + self.pending_cmd.set_verb(VerbCmd(1,Verb::NormalMode)); + self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); self.register_and_return() } _ => common_cmds(key) @@ -94,230 +115,416 @@ impl ViMode for ViInsert { } fn as_replay(&self) -> Option { - Some(CmdReplay::new(self.cmds.clone(), self.repeat_count)) + Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count)) } fn cursor_style(&self) -> String { "\x1b[6 q".to_string() } + fn pending_seq(&self) -> Option { + None + } } #[derive(Default,Debug)] pub struct ViNormal { - pending_cmd: ViCmd, + pending_seq: String, } impl ViNormal { pub fn new() -> Self { Self::default() } - pub fn take_cmd(&mut self) -> ViCmd { - std::mem::take(&mut self.pending_cmd) - } pub fn clear_cmd(&mut self) { - self.pending_cmd = ViCmd::new(); + self.pending_seq = String::new(); } - fn handle_pending_builder(&mut self, key: E) -> Option { - if self.pending_cmd.wants_register { - if let E(K::Char(ch @ ('a'..='z' | 'A'..='Z')), M::NONE) = key { - self.pending_cmd.set_register(ch); - return None - } else { - self.clear_cmd(); - return None - } - } else if let Some(Verb::Builder(_)) = &self.pending_cmd.verb { - todo!() // Don't have any verb builders yet, but might later - } else if let Some(Motion::Builder(builder)) = self.pending_cmd.motion.clone() { - match builder { - MotionBuilder::CharSearch(direction, dest, _) => { - if let E(K::Char(ch), M::NONE) = key { - self.pending_cmd.set_motion(Motion::CharSearch( - direction.unwrap(), - dest.unwrap(), - ch.into(), - )); - return Some(self.take_cmd()); - } else { - self.clear_cmd(); - return None; - } - } - MotionBuilder::TextObj(_, bound) => { - if let Some(bound) = bound { - if let E(K::Char(ch), M::NONE) = key { - let obj = match ch { - 'w' => TextObj::Word(Word::Normal), - 'W' => TextObj::Word(Word::Big), - '(' | ')' => TextObj::Paren, - '[' | ']' => TextObj::Bracket, - '{' | '}' => TextObj::Brace, - '<' | '>' => TextObj::Angle, - '"' => TextObj::DoubleQuote, - '\'' => TextObj::SingleQuote, - '`' => TextObj::BacktickQuote, - _ => TextObj::Custom(ch), - }; - self.pending_cmd.set_motion(Motion::TextObj(obj, bound)); - return Some(self.take_cmd()); - } else { - self.clear_cmd(); - return None; - } - } else if let E(K::Char(ch), M::NONE) = key { - let bound = match ch { - 'i' => Bound::Inside, - 'a' => Bound::Around, - _ => { - self.clear_cmd(); - return None; - } - }; - self.pending_cmd.set_motion(Motion::Builder(MotionBuilder::TextObj(None, Some(bound)))); - return None; - } else { - self.clear_cmd(); - return None; - } - } + pub fn take_cmd(&mut self) -> String { + std::mem::take(&mut self.pending_seq) + } + fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState { + if verb.is_none() { + match motion { + Some(Motion::TextObj(_,_)) => return CmdState::Invalid, + Some(_) => return CmdState::Complete, + None => return CmdState::Pending } } + if verb.is_some() && motion.is_none() { + match verb.unwrap() { + Verb::Put(_) | + Verb::DeleteChar(_) => CmdState::Complete, + _ => CmdState::Pending + } + } else { + CmdState::Complete + } + } + pub fn parse_count(&self, chars: &mut Peekable>) -> Option { + let mut count = String::new(); + let Some(_digit @ '1'..='9') = chars.peek() else { + return None + }; + count.push(chars.next().unwrap()); + while let Some(_digit @ '0'..='9') = chars.peek() { + count.push(chars.next().unwrap()); + } + if !count.is_empty() { + count.parse::().ok() + } else { + None + } + } + /// End the parse and clear the pending sequence + pub fn quit_parse(&mut self) -> Option { + flog!(WARN, "exiting parse early with sequence: {}",self.pending_seq); + self.clear_cmd(); None } + pub fn try_parse(&mut self, ch: char) -> Option { + self.pending_seq.push(ch); + flog!(DEBUG, self.pending_seq); + let mut chars = self.pending_seq.chars().peekable(); + + let register = 'reg_parse: { + let mut chars_clone = chars.clone(); + let count = self.parse_count(&mut chars_clone); + + let Some('"') = chars_clone.next() else { + break 'reg_parse RegisterName::default() + }; + + let Some(reg_name) = chars_clone.next() else { + return None // Pending register name + }; + match reg_name { + 'a'..='z' | + 'A'..='Z' => { /* proceed */ } + _ => return self.quit_parse() + } + + chars = chars_clone; + RegisterName::new(Some(reg_name), count) + }; + + let verb = 'verb_parse: { + let mut chars_clone = chars.clone(); + let count = self.parse_count(&mut chars_clone).unwrap_or(1); + + let Some(ch) = chars_clone.next() else { + break 'verb_parse None + }; + flog!(DEBUG, "parsing verb char '{}'",ch); + match ch { + '.' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::RepeatLast)), + motion: None, + raw_seq: self.take_cmd(), + } + ) + } + 'x' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::DeleteChar(Anchor::After))); + } + 'X' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::DeleteChar(Anchor::Before))); + } + 'p' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::After))); + } + 'P' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before))); + } + 'r' => { + let Some(ch) = chars_clone.next() else { + return None + }; + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))), + motion: Some(MotionCmd(count, Motion::ForwardChar)), + raw_seq: self.take_cmd() + } + ) + } + 'u' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Undo)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'o' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::After))), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'O' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::Before))), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'a' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::InsertMode)), + motion: Some(MotionCmd(1, Motion::ForwardChar)), + raw_seq: self.take_cmd() + } + ) + } + 'A' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::InsertMode)), + motion: Some(MotionCmd(1, Motion::EndOfLine)), + raw_seq: self.take_cmd() + } + ) + } + 'i' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::InsertMode)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'I' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::InsertMode)), + motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)), + raw_seq: self.take_cmd() + } + ) + } + 'y' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Yank)) + } + 'd' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Delete)) + } + 'c' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Change)) + } + '=' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Equalize)) + } + _ => break 'verb_parse None + } + }; + + let motion = 'motion_parse: { + let mut chars_clone = chars.clone(); + let count = self.parse_count(&mut chars_clone).unwrap_or(1); + + let Some(ch) = chars_clone.next() else { + break 'motion_parse None + }; + flog!(DEBUG, "parsing motion char '{}'",ch); + match (ch, &verb) { + ('d', Some(VerbCmd(_,Verb::Delete))) | + ('c', Some(VerbCmd(_,Verb::Change))) | + ('y', Some(VerbCmd(_,Verb::Yank))) | + ('>', Some(VerbCmd(_,Verb::Indent))) | + ('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)), + _ => {} + } + match ch { + 'g' => { + if let Some(ch) = chars_clone.peek() { + match ch { + 'g' => { + chars_clone.next(); + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer)) + } + 'e' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Normal))); + } + 'E' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Big))); + } + _ => return self.quit_parse() + } + } else { + break 'motion_parse None + } + } + '|' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count))); + } + '0' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine)); + } + '$' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine)); + } + 'k' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::LineUp)); + } + 'j' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::LineDown)); + } + 'h' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar)); + } + 'l' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar)); + } + 'w' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Normal))); + } + 'W' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Big))); + } + 'e' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Normal))); + } + 'E' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Big))); + } + 'b' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Normal))); + } + 'B' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Big))); + } + ch if ch == 'i' || ch == 'a' => { + let bound = match ch { + 'i' => Bound::Inside, + 'a' => Bound::Around, + _ => unreachable!() + }; + if chars_clone.peek().is_none() { + break 'motion_parse None + } + let obj = match chars_clone.next().unwrap() { + 'w' => TextObj::Word(Word::Normal), + 'W' => TextObj::Word(Word::Big), + '"' => TextObj::DoubleQuote, + '\'' => TextObj::SingleQuote, + '(' | ')' | 'b' => TextObj::Paren, + '{' | '}' | 'B' => TextObj::Brace, + '[' | ']' => TextObj::Bracket, + '<' | '>' => TextObj::Angle, + _ => return self.quit_parse() + }; + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj, bound))) + } + _ => return self.quit_parse(), + } + }; + + if chars.peek().is_some() { + flog!(WARN, "Unused characters in Vi command parse!"); + flog!(WARN, "{:?}",chars) + } + + let verb_ref = verb.as_ref().map(|v| &v.1); + let motion_ref = motion.as_ref().map(|m| &m.1); + + match self.validate_combination(verb_ref, motion_ref) { + CmdState::Complete => { + let cmd = Some( + ViCmd { + register, + verb, + motion, + raw_seq: std::mem::take(&mut self.pending_seq) + } + ); + flog!(DEBUG, cmd); + cmd + } + CmdState::Pending => { + flog!(DEBUG, "pending sequence: {}", self.pending_seq); + None + } + CmdState::Invalid => { + flog!(DEBUG, "invalid sequence: {}",self.pending_seq); + self.pending_seq.clear(); + None + } + } + } } impl ViMode for ViNormal { fn handle_key(&mut self, key: E) -> Option { - if let E(K::Char(ch),M::NONE) = key { - self.pending_cmd.append_seq_char(ch); - } - if self.pending_cmd.is_building() { - return self.handle_pending_builder(key) - } + flog!(DEBUG, key); match key { - E(K::Char(digit @ '0'..='9'), M::NONE) => self.pending_cmd.append_digit(digit), - E(K::Char('"'),M::NONE) => { - if self.pending_cmd.is_empty() { - if self.pending_cmd.register().name().is_none() { - self.pending_cmd.wants_register = true; - } else { - self.clear_cmd(); + E(K::Char(ch), M::NONE) => self.try_parse(ch), + E(K::Char('R'), M::CTRL) => { + let mut chars = self.pending_seq.chars().peekable(); + let count = self.parse_count(&mut chars).unwrap_or(1); + Some( + ViCmd { + register: RegisterName::default(), + verb: Some(VerbCmd(count,Verb::Redo)), + motion: None, + raw_seq: self.take_cmd() } - } else { + ) + } + E(K::Esc, M::NONE) => { + self.clear_cmd(); + None + } + _ => { + if let Some(cmd) = common_cmds(key) { self.clear_cmd(); - } - return None - } - E(K::Char('i'),M::NONE) if self.pending_cmd.verb().is_some() => { - self.pending_cmd.set_motion(Motion::Builder(MotionBuilder::TextObj(None, Some(Bound::Inside)))); - } - E(K::Char('a'),M::NONE) if self.pending_cmd.verb().is_some() => { - self.pending_cmd.set_motion(Motion::Builder(MotionBuilder::TextObj(None, Some(Bound::Around)))); - } - E(K::Char('h'),M::NONE) => self.pending_cmd.set_motion(Motion::BackwardChar), - E(K::Char('j'),M::NONE) => self.pending_cmd.set_motion(Motion::LineDown), - E(K::Char('k'),M::NONE) => self.pending_cmd.set_motion(Motion::LineUp), - E(K::Char('l'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardChar), - E(K::Char('w'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::Start, Word::Normal)), - E(K::Char('W'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::Start, Word::Big)), - E(K::Char('e'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::End, Word::Normal)), - E(K::Char('E'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::End, Word::Big)), - E(K::Char('b'),M::NONE) => self.pending_cmd.set_motion(Motion::BackwardWord(Word::Normal)), - E(K::Char('B'),M::NONE) => self.pending_cmd.set_motion(Motion::BackwardWord(Word::Big)), - E(K::Char('x'),M::NONE) => self.pending_cmd.set_verb(Verb::DeleteChar(Anchor::After)), - E(K::Char('X'),M::NONE) => self.pending_cmd.set_verb(Verb::DeleteChar(Anchor::Before)), - E(K::Char('d'),M::NONE) => { - if self.pending_cmd.verb().is_none() { - self.pending_cmd.set_verb(Verb::Delete) - } else if let Some(verb) = self.pending_cmd.verb() { - if verb == &Verb::Delete { - self.pending_cmd.set_motion(Motion::WholeLine); - } else { - self.clear_cmd(); - } + Some(cmd) + } else { + None } } - E(K::Char('c'),M::NONE) => { - if self.pending_cmd.verb().is_none() { - self.pending_cmd.set_verb(Verb::Change) - } else if let Some(verb) = self.pending_cmd.verb() { - if verb == &Verb::Change { - self.pending_cmd.set_motion(Motion::WholeLine); - } else { - self.clear_cmd(); - } - } - } - E(K::Char('y'),M::NONE) => { - if self.pending_cmd.verb().is_none() { - self.pending_cmd.set_verb(Verb::Yank) - } else if let Some(verb) = self.pending_cmd.verb() { - if verb == &Verb::Yank { - self.pending_cmd.set_motion(Motion::WholeLine); - } else { - self.clear_cmd(); - } - } - } - E(K::Char('p'),M::NONE) => self.pending_cmd.set_verb(Verb::Put(Anchor::After)), - E(K::Char('P'),M::NONE) => self.pending_cmd.set_verb(Verb::Put(Anchor::Before)), - E(K::Char('D'),M::NONE) => { - self.pending_cmd.set_verb(Verb::Delete); - self.pending_cmd.set_motion(Motion::EndOfLine); - } - E(K::Char('f'),M::NONE) => { - let builder = MotionBuilder::CharSearch( - Some(Direction::Forward), - Some(Dest::On), - None - ); - self.pending_cmd.set_motion(Motion::Builder(builder)); - } - E(K::Char('F'),M::NONE) => { - let builder = MotionBuilder::CharSearch( - Some(Direction::Backward), - Some(Dest::On), - None - ); - self.pending_cmd.set_motion(Motion::Builder(builder)); - } - E(K::Char('t'),M::NONE) => { - let builder = MotionBuilder::CharSearch( - Some(Direction::Forward), - Some(Dest::Before), - None - ); - self.pending_cmd.set_motion(Motion::Builder(builder)); - } - E(K::Char('T'),M::NONE) => { - let builder = MotionBuilder::CharSearch( - Some(Direction::Backward), - Some(Dest::Before), - None - ); - self.pending_cmd.set_motion(Motion::Builder(builder)); - } - E(K::Char('i'),M::NONE) => { - self.pending_cmd.set_verb(Verb::InsertMode); - } - E(K::Char('I'),M::NONE) => { - self.pending_cmd.set_verb(Verb::InsertMode); - self.pending_cmd.set_motion(Motion::BeginningOfFirstWord); - } - E(K::Char('a'),M::NONE) => { - self.pending_cmd.set_verb(Verb::InsertMode); - self.pending_cmd.set_motion(Motion::ForwardChar); - } - E(K::Char('A'),M::NONE) => { - self.pending_cmd.set_verb(Verb::InsertMode); - self.pending_cmd.set_motion(Motion::EndOfLine); - } - _ => return common_cmds(key) - } - if self.pending_cmd.is_complete() { - Some(self.take_cmd()) - } else { - None } } @@ -332,28 +539,26 @@ impl ViMode for ViNormal { fn cursor_style(&self) -> String { "\x1b[2 q".to_string() } + + fn pending_seq(&self) -> Option { + Some(self.pending_seq.clone()) + } } pub fn common_cmds(key: E) -> Option { let mut pending_cmd = ViCmd::new(); match key { - E(K::Home, M::NONE) => pending_cmd.set_motion(Motion::BeginningOfLine), - E(K::End, M::NONE) => pending_cmd.set_motion(Motion::EndOfLine), - E(K::Left, M::NONE) => pending_cmd.set_motion(Motion::BackwardChar), - E(K::Right, M::NONE) => pending_cmd.set_motion(Motion::ForwardChar), - E(K::Up, M::NONE) => pending_cmd.set_motion(Motion::LineUp), - E(K::Down, M::NONE) => pending_cmd.set_motion(Motion::LineDown), - E(K::Enter, M::NONE) => pending_cmd.set_verb(Verb::AcceptLine), - E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(Verb::EndOfFile), + E(K::Home, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::BeginningOfLine)), + E(K::End, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::EndOfLine)), + E(K::Left, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)), + E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)), + E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineUp)), + E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineDown)), + E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::AcceptLine)), + E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1,Verb::EndOfFile)), + E(K::Delete, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::DeleteChar(Anchor::After))), E(K::Backspace, M::NONE) | - E(K::Char('H'), M::CTRL) => { - pending_cmd.set_verb(Verb::Delete); - pending_cmd.set_motion(Motion::BackwardChar); - } - E(K::Delete, M::NONE) => { - pending_cmd.set_verb(Verb::Delete); - pending_cmd.set_motion(Motion::ForwardChar); - } + E(K::Char('H'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1,Verb::DeleteChar(Anchor::Before))), _ => return None } Some(pending_cmd) diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 7780e56..67bf0d8 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -1,5 +1,10 @@ use std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}}; -use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}}; +use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::{self, STDIN_FILENO}, sys::termios, unistd::{isatty, read, write}}; +use nix::libc::{winsize, TIOCGWINSZ}; +use std::mem::zeroed; +use std::io; + +use crate::libsh::error::ShResult; use super::keys::{KeyCode, KeyEvent, ModKeys}; @@ -32,6 +37,34 @@ impl Terminal { .expect("Failed to restore terminal settings"); } + + pub fn get_dimensions(&self) -> ShResult<(usize, usize)> { + if !isatty(self.stdin).unwrap_or(false) { + return Err(io::Error::new(io::ErrorKind::Other, "Not a TTY"))?; + } + + let mut ws: winsize = unsafe { zeroed() }; + + let res = unsafe { libc::ioctl(self.stdin, TIOCGWINSZ, &mut ws) }; + if res == -1 { + return Err(io::Error::last_os_error())?; + } + + Ok((ws.ws_row as usize, ws.ws_col as usize)) + } + + pub fn save_cursor_pos(&self) { + self.write("\x1b[s") + } + + pub fn restore_cursor_pos(&self) { + self.write("\x1b[u") + } + + pub fn move_cursor_to(&self, (row,col): (usize,usize)) { + self.write(&format!("\x1b[{row};{col}H",)) + } + pub fn with_raw_mode R, R>(func: F) -> R { let saved = Self::raw_mode(); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func)); @@ -48,6 +81,29 @@ impl Terminal { }) } + fn read_blocks_then_read(&self, buf: &mut [u8], timeout: Duration) -> Option { + Self::with_raw_mode(|| { + self.read_blocks(false); + let start = Instant::now(); + loop { + match read(self.stdin, buf) { + Ok(n) if n > 0 => { + self.read_blocks(true); + return Some(n); + } + Ok(_) => {} + Err(e) if e == Errno::EAGAIN => {} + Err(_) => return None, + } + if start.elapsed() > timeout { + self.read_blocks(true); + return None; + } + sleep(Duration::from_millis(1)); + } + }) + } + /// Same as read_byte(), only non-blocking with a very short timeout pub fn peek_byte(&self, buf: &mut [u8]) -> usize { const TIMEOUT_DUR: Duration = Duration::from_millis(50); diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs index e1d1e3f..26147fe 100644 --- a/src/prompt/readline/vicmd.rs +++ b/src/prompt/readline/vicmd.rs @@ -1,18 +1,35 @@ use super::{linebuf::{TermChar, TermCharBuf}, register::{append_register, read_register, write_register}}; -#[derive(Clone,Copy,Default,Debug)] +#[derive(Clone,Copy,Debug)] pub struct RegisterName { name: Option, + count: usize, append: bool } impl RegisterName { + pub fn new(name: Option, count: Option) -> Self { + let Some(ch) = name else { + return Self::default() + }; + + let append = ch.is_uppercase(); + let name = ch.to_ascii_lowercase(); + Self { + name: Some(name), + count: count.unwrap_or(1), + append + } + } pub fn name(&self) -> Option { self.name } pub fn is_append(&self) -> bool { self.append } + pub fn count(&self) -> usize { + self.count + } pub fn write_to_register(&self, buf: TermCharBuf) { if self.append { append_register(self.name, buf); @@ -25,29 +42,21 @@ impl RegisterName { } } +impl Default for RegisterName { + fn default() -> Self { + Self { + name: None, + count: 1, + append: false + } + } +} + #[derive(Clone,Default,Debug)] pub struct ViCmd { - pub wants_register: bool, // Waiting for register character - - /// Register to read from/write to - pub register_count: Option, pub register: RegisterName, - - /// Verb to perform - pub verb_count: Option, - pub verb: Option, - - /// Motion to perform - pub motion_count: Option, - pub motion: Option, - - /// Count digits are held here until we know what we are counting - /// Once a register/verb/motion is set, the count is taken from here - pub pending_count: Option, - - /// The actual keys the user typed for this command - /// Maybe display this somewhere around the prompt later? - /// Prompt escape sequence maybe? + pub verb: Option, + pub motion: Option, pub raw_seq: String, } @@ -55,79 +64,54 @@ impl ViCmd { pub fn new() -> Self { Self::default() } - pub fn set_register(&mut self, register: char) { - let append = register.is_uppercase(); - let name = Some(register.to_ascii_lowercase()); - let reg_name = RegisterName { name, append }; - self.register = reg_name; - self.register_count = self.pending_count.take(); - self.wants_register = false; + pub fn set_motion(&mut self, motion: MotionCmd) { + self.motion = Some(motion) } - pub fn append_seq_char(&mut self, ch: char) { - self.raw_seq.push(ch) + pub fn set_verb(&mut self, verb: VerbCmd) { + self.verb = Some(verb) } - pub fn is_empty(&self) -> bool { - !self.wants_register && - self.register.name.is_none() && - self.verb_count.is_none() && - self.verb.is_none() && - self.motion_count.is_none() && - self.motion.is_none() - } - pub fn set_verb(&mut self, verb: Verb) { - self.verb = Some(verb); - self.verb_count = self.pending_count.take(); - } - pub fn set_motion(&mut self, motion: Motion) { - self.motion = Some(motion); - self.motion_count = self.pending_count.take(); - } - pub fn register(&self) -> RegisterName { - self.register - } - pub fn verb(&self) -> Option<&Verb> { + pub fn verb(&self) -> Option<&VerbCmd> { self.verb.as_ref() } - pub fn verb_count(&self) -> u16 { - self.verb_count.unwrap_or(1) - } - pub fn motion(&self) -> Option<&Motion> { + pub fn motion(&self) -> Option<&MotionCmd> { self.motion.as_ref() } - pub fn motion_count(&self) -> u16 { - self.motion_count.unwrap_or(1) + pub fn verb_count(&self) -> usize { + self.verb.as_ref().map(|v| v.0).unwrap_or(1) } - pub fn append_digit(&mut self, digit: char) { - // Convert char digit to a number (assuming ASCII '0'..'9') - let digit_val = digit.to_digit(10).expect("digit must be 0-9") as u16; - self.pending_count = Some(match self.pending_count { - Some(count) => count * 10 + digit_val, - None => digit_val, - }); + pub fn motion_count(&self) -> usize { + self.motion.as_ref().map(|m| m.0).unwrap_or(1) } - pub fn is_building(&self) -> bool { - matches!(self.verb, Some(Verb::Builder(_))) || - matches!(self.motion, Some(Motion::Builder(_))) || - self.wants_register + pub fn is_cmd_repeat(&self) -> bool { + self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatLast)) } - pub fn is_complete(&self) -> bool { - !( - (self.verb.is_none() && self.motion.is_none()) || - (self.verb.is_none() && self.motion.as_ref().is_some_and(|m| m.needs_verb())) || - (self.motion.is_none() && self.verb.as_ref().is_some_and(|v| v.needs_motion())) || - self.is_building() - ) + pub fn is_motion_repeat(&self) -> bool { + self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatMotion | Verb::RepeatMotionRev)) } pub fn should_submit(&self) -> bool { - self.verb.as_ref().is_some_and(|v| *v == Verb::AcceptLine) + self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLine)) + } + 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_mode_transition(&self) -> bool { self.verb.as_ref().is_some_and(|v| { - matches!(*v, Verb::InsertMode | Verb::NormalMode | Verb::OverwriteMode | Verb::VisualMode) + matches!(v.1, + Verb::InsertMode | + Verb::InsertModeLineBreak(_) | + Verb::NormalMode | + Verb::VisualMode | + Verb::OverwriteMode + ) }) } } +#[derive(Clone,Debug)] +pub struct VerbCmd(pub usize,pub Verb); +#[derive(Clone,Debug)] +pub struct MotionCmd(pub usize,pub Motion); + #[derive(Debug, Clone, Eq, PartialEq)] #[non_exhaustive] pub enum Verb { @@ -141,10 +125,14 @@ pub enum Verb { Complete, CompleteBackward, Undo, + Redo, RepeatLast, + RepeatMotion, + RepeatMotionRev, Put(Anchor), OverwriteMode, InsertMode, + InsertModeLineBreak(Anchor), NormalMode, VisualMode, JoinLines, @@ -153,6 +141,7 @@ pub enum Verb { Breakline(Anchor), Indent, Dedent, + Equalize, AcceptLine, Builder(VerbBuilder), EndOfFile @@ -172,6 +161,28 @@ impl Verb { Self::Yank ) } + pub fn is_edit(&self) -> bool { + matches!(self, + Self::Delete | + Self::DeleteChar(_) | + Self::Change | + Self::ReplaceChar(_) | + Self::Substitute | + Self::ToggleCase | + Self::RepeatLast | + Self::Put(_) | + Self::OverwriteMode | + Self::InsertModeLineBreak(_) | + Self::JoinLines | + Self::InsertChar(_) | + Self::Insert(_) | + Self::Breakline(_) | + Self::EndOfFile + ) + } + pub fn is_char_insert(&self) -> bool { + matches!(self, Self::InsertChar(_)) + } } #[derive(Debug, Clone, Eq, PartialEq)] @@ -185,7 +196,7 @@ pub enum Motion { /// end-of-line EndOfLine, /// backward-word, vi-prev-word - BackwardWord(Word), // Backward until start of word + BackwardWord(To, Word), // Backward until start of word /// forward-word, vi-end-word, vi-next-word ForwardWord(To, Word), // Forward until start/end of word /// character-search, character-search-backward, vi-char-search @@ -204,6 +215,8 @@ pub enum Motion { BeginningOfBuffer, /// end-of-register EndOfBuffer, + ToColumn(usize), + Range(usize,usize), Builder(MotionBuilder), Null }