From c465251976ca2fadaba772876a86fc9e15fb7278 Mon Sep 17 00:00:00 2001 From: Kyler Clay Date: Thu, 5 Jun 2025 01:48:44 -0400 Subject: [PATCH] more work on re-implementing the readline module --- src/prompt/mod.rs | 4 +- src/prompt/readline/keys.rs | 142 +++ src/prompt/readline/linebuf.rs | 1108 +++++++++++++++++++++- src/prompt/readline/mod.rs | 241 ++++- src/prompt/readline/register.rs | 168 ++++ src/prompt/readline/term.rs | 311 +++++-- src/prompt/readline/vicmd.rs | 393 ++++++++ src/prompt/readline/vimode.rs | 1520 +++++++++++++++++++++++++++++++ src/tests/mod.rs | 1 + src/tests/readline.rs | 131 +++ 10 files changed, 3947 insertions(+), 72 deletions(-) create mode 100644 src/prompt/readline/keys.rs create mode 100644 src/prompt/readline/register.rs create mode 100644 src/prompt/readline/vicmd.rs create mode 100644 src/prompt/readline/vimode.rs create mode 100644 src/tests/readline.rs diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 30b4f6d..95f6c3c 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -25,8 +25,8 @@ fn get_prompt() -> ShResult { pub fn readline(edit_mode: FernEditMode) -> ShResult { let prompt = get_prompt()?; let mut reader: Box = match edit_mode { - FernEditMode::Vi => Box::new(FernVi::new()), + FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))), FernEditMode::Emacs => todo!() }; - reader.readline(Some(prompt)) + reader.readline() } diff --git a/src/prompt/readline/keys.rs b/src/prompt/readline/keys.rs new file mode 100644 index 0000000..b8b1bd0 --- /dev/null +++ b/src/prompt/readline/keys.rs @@ -0,0 +1,142 @@ +use std::sync::Arc; +use unicode_segmentation::UnicodeSegmentation; + +// Credit to Rustyline for the design ideas in this module +// https://github.com/kkawakam/rustyline +#[derive(Clone,PartialEq,Eq,Debug)] +pub struct KeyEvent(pub KeyCode, pub ModKeys); + + +impl KeyEvent { + pub fn new(ch: &str, mut mods: ModKeys) -> Self { + use {KeyCode as K, KeyEvent as E, ModKeys as M}; + + let mut graphemes = ch.graphemes(true); + + let first = match graphemes.next() { + Some(g) => g, + None => return E(K::Null, mods), + }; + + // If more than one grapheme, it's not a single key event + if graphemes.next().is_some() { + return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired + } + + let mut chars = first.chars(); + + let single_char = chars.next(); + let is_single_char = chars.next().is_none(); + + match single_char { + Some(c) if is_single_char && c.is_control() => { + match c { + '\x00' => E(K::Char('@'), mods | M::CTRL), + '\x01' => E(K::Char('A'), mods | M::CTRL), + '\x02' => E(K::Char('B'), mods | M::CTRL), + '\x03' => E(K::Char('C'), mods | M::CTRL), + '\x04' => E(K::Char('D'), mods | M::CTRL), + '\x05' => E(K::Char('E'), mods | M::CTRL), + '\x06' => E(K::Char('F'), mods | M::CTRL), + '\x07' => E(K::Char('G'), mods | M::CTRL), + '\x08' => E(K::Backspace, mods), + '\x09' => { + if mods.contains(M::SHIFT) { + mods.remove(M::SHIFT); + E(K::BackTab, mods) + } else { + E(K::Tab, mods) + } + } + '\x0a' => E(K::Char('J'), mods | M::CTRL), + '\x0b' => E(K::Char('K'), mods | M::CTRL), + '\x0c' => E(K::Char('L'), mods | M::CTRL), + '\x0d' => E(K::Enter, mods), + '\x0e' => E(K::Char('N'), mods | M::CTRL), + '\x0f' => E(K::Char('O'), mods | M::CTRL), + '\x10' => E(K::Char('P'), mods | M::CTRL), + '\x11' => E(K::Char('Q'), mods | M::CTRL), + '\x12' => E(K::Char('R'), mods | M::CTRL), + '\x13' => E(K::Char('S'), mods | M::CTRL), + '\x14' => E(K::Char('T'), mods | M::CTRL), + '\x15' => E(K::Char('U'), mods | M::CTRL), + '\x16' => E(K::Char('V'), mods | M::CTRL), + '\x17' => E(K::Char('W'), mods | M::CTRL), + '\x18' => E(K::Char('X'), mods | M::CTRL), + '\x19' => E(K::Char('Y'), mods | M::CTRL), + '\x1a' => E(K::Char('Z'), mods | M::CTRL), + '\x1b' => E(K::Esc, mods), + '\x1c' => E(K::Char('\\'), mods | M::CTRL), + '\x1d' => E(K::Char(']'), mods | M::CTRL), + '\x1e' => E(K::Char('^'), mods | M::CTRL), + '\x1f' => E(K::Char('_'), mods | M::CTRL), + '\x7f' => E(K::Backspace, mods), + '\u{9b}' => E(K::Esc, mods | M::SHIFT), + _ => E(K::Null, mods), + } + } + Some(c) if is_single_char => { + if !mods.is_empty() { + mods.remove(M::SHIFT); + } + E(K::Char(c), mods) + } + _ => { + // multi-char grapheme (emoji, accented, etc) + if !mods.is_empty() { + mods.remove(M::SHIFT); + } + E(K::Grapheme(Arc::from(first)), mods) + } + } + } +} + +#[derive(Clone,PartialEq,Eq,Debug)] +pub enum KeyCode { + UnknownEscSeq, + Backspace, + BackTab, + BracketedPasteStart, + BracketedPasteEnd, + Char(char), + Grapheme(Arc), + Delete, + Down, + End, + Enter, + Esc, + F(u8), + Home, + Insert, + Left, + Null, + PageDown, + PageUp, + Right, + Tab, + Up, +} + +bitflags::bitflags! { + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + pub struct ModKeys: u8 { + /// Control modifier + const CTRL = 1<<3; + /// Escape or Alt modifier + const ALT = 1<<2; + /// Shift modifier + const SHIFT = 1<<1; + + /// No modifier + const NONE = 0; + /// Ctrl + Shift + const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits(); + /// Alt + Shift + const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits(); + /// Ctrl + Alt + const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits(); + /// Ctrl + Alt + Shift + const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits(); + } +} diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index af5957b..4c50cb5 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -1,9 +1,1115 @@ +use std::{ops::{Range, RangeBounds, RangeInclusive}, string::Drain}; + +use unicode_segmentation::UnicodeSegmentation; + +use super::{term::Layout, vicmd::{Direction, Motion, MotionBehavior, RegisterName, To, Verb, ViCmd, Word}}; +use crate::{libsh::error::ShResult, prelude::*}; + +#[derive(PartialEq,Eq,Debug,Clone,Copy)] +pub enum CharClass { + Alphanum, + Symbol, + Whitespace, + Other +} + +impl From<&str> for CharClass { + fn from(value: &str) -> Self { + if value.len() > 1 { + return Self::Symbol // Multi-byte grapheme + } + + if value.chars().all(char::is_alphanumeric) { + CharClass::Alphanum + } else if value.chars().all(char::is_whitespace) { + CharClass::Whitespace + } else if !value.chars().all(char::is_alphanumeric) { + CharClass::Symbol + } else { + Self::Other + } + } +} + +fn is_whitespace(a: &str) -> bool { + CharClass::from(a) == CharClass::Whitespace +} + +fn is_other_class(a: &str, b: &str) -> bool { + let a = CharClass::from(a); + let b = CharClass::from(b); + a != b +} + +fn is_other_class_not_ws(a: &str, b: &str) -> bool { + if is_whitespace(a) || is_whitespace(b) { + false + } else { + is_other_class(a, b) + } +} + +fn is_other_class_or_is_ws(a: &str, b: &str) -> bool { + if is_whitespace(a) || is_whitespace(b) { + true + } else { + is_other_class(a, b) + } +} + +fn is_other_class_and_is_ws(a: &str, b: &str) -> bool { + is_other_class(a, b) && (is_whitespace(a) || is_whitespace(b)) +} + +#[derive(Default,Clone,Copy,PartialEq,Eq,Debug)] +pub enum SelectAnchor { + #[default] + End, + Start +} + +#[derive(Clone,Copy,PartialEq,Eq,Debug)] +pub enum SelectMode { + Char(SelectAnchor), + Line(SelectAnchor), + Block(SelectAnchor), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MotionKind { + To(usize), // Absolute position, exclusive + On(usize), // Absolute position, inclusive + Inclusive((usize,usize)), // Range, inclusive + Exclusive((usize,usize)), // Range, exclusive + Null +} + +impl MotionKind { + pub fn inclusive(range: RangeInclusive) -> Self { + Self::Inclusive((*range.start(),*range.end())) + } + pub fn exclusive(range: Range) -> Self { + Self::Exclusive((range.start,range.end)) + } +} + +#[derive(Default,Debug)] +pub struct Edit { + pub pos: usize, + pub cursor_pos: usize, + pub old: String, + pub new: String, + pub merging: bool, +} + +impl Edit { + pub fn diff(a: &str, b: &str, old_cursor_pos: usize) -> Edit { + 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.as_bytes()[start] == b.as_bytes()[start] { + start += 1; + } + + if start == a.len() && start == b.len() { + return Edit { + pos: start, + cursor_pos: old_cursor_pos, + old: String::new(), + new: String::new(), + merging: false, + }; + } + + 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.as_bytes()[end_a - 1] == b.as_bytes()[end_b - 1] { + end_a -= 1; + end_b -= 1; + } + + // Slice off the prefix and suffix for both (safe because start/end are byte offsets) + let old = a[start..end_a].to_string(); + let new = b[start..end_b].to_string(); + + Edit { + pos: start, + cursor_pos: old_cursor_pos, + old, + new, + merging: false + } + } + pub fn start_merge(&mut self) { + self.merging = true + } + pub fn stop_merge(&mut self) { + self.merging = false + } + pub fn is_empty(&self) -> bool { + self.new.is_empty() && + self.old.is_empty() + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)] +/// A usize which will always exist between 0 and a given upper bound +/// +/// * The upper bound can be both inclusive and exclusive +/// * Used for the LineBuf cursor to enforce the `0 <= cursor < self.buffer.len()` invariant. +pub struct ClampedUsize { + value: usize, + max: usize, + exclusive: bool +} + +impl ClampedUsize { + pub fn new(value: usize, max: usize, exclusive: bool) -> Self { + let mut c = Self { value: 0, max, exclusive }; + c.set(value); + c + } + pub fn get(self) -> usize { + self.value + } + pub fn upper_bound(&self) -> usize { + if self.exclusive { + self.max.saturating_sub(1) + } else { + self.max + } + } + pub fn inc(&mut self) -> bool { + let max = self.upper_bound(); + if self.value == max { + return false; + } + self.add(1); + true + } + pub fn dec(&mut self) -> bool { + if self.value == 0 { + return false; + } + self.sub(1); + true + } + pub fn set(&mut self, value: usize) { + let max = self.upper_bound(); + self.value = value.clamp(0,max); + } + pub fn set_max(&mut self, max: usize) { + self.max = max; + self.set(self.get()); // Enforces the new maximum + } + pub fn add(&mut self, value: usize) { + let max = self.upper_bound(); + self.value = (self.value + value).clamp(0,max) + } + pub fn sub(&mut self, value: usize) { + self.value = self.value.saturating_sub(value) + } + /// Add a value to the wrapped usize, return the result + /// + /// Returns the result instead of mutating the inner value + pub fn ret_add(&self, value: usize) -> usize { + let max = self.upper_bound(); + (self.value + value).clamp(0,max) + } + /// Add a value to the wrapped usize, forcing inclusivity + pub fn ret_add_inclusive(&self, value: usize) -> usize { + let max = self.max; + (self.value + value).clamp(0,max) + } + /// Subtract a value from the wrapped usize, return the result + /// + /// Returns the result instead of mutating the inner value + pub fn ret_sub(&self, value: usize) -> usize { + self.value.saturating_sub(value) + } +} + +#[derive(Default,Debug)] pub struct LineBuf { - buffer: String + pub buffer: String, + pub hint: Option, + pub grapheme_indices: Option>, // Used to slice the buffer + pub cursor: ClampedUsize, // Used to index grapheme_indices + + pub select_mode: Option, + pub select_range: Option<(usize,usize)>, + pub last_selection: Option<(usize,usize)>, + + pub saved_col: Option, + + pub undo_stack: Vec, + pub redo_stack: Vec, } impl LineBuf { + pub fn new() -> Self { + Self::default() + } + /// Only update self.grapheme_indices if it is None + pub fn update_graphemes_lazy(&mut self) { + if self.grapheme_indices.is_none() { + self.update_graphemes(); + } + } + pub fn with_initial(mut self, buffer: &str, cursor: usize) -> Self { + self.buffer = buffer.to_string(); + self.update_graphemes(); + self.cursor = ClampedUsize::new(cursor, self.grapheme_indices().len(), self.cursor.exclusive); + self + } + pub fn has_hint(&self) -> bool { + self.hint.is_some() + } + pub fn hint(&self) -> Option<&String> { + self.hint.as_ref() + } + pub fn set_cursor_clamp(&mut self, yn: bool) { + self.cursor.exclusive = yn; + } + pub fn cursor_byte_pos(&mut self) -> usize { + self.index_byte_pos(self.cursor.get()) + } + pub fn index_byte_pos(&mut self, index: usize) -> usize { + self.update_graphemes_lazy(); + self.grapheme_indices() + .get(index) + .copied() + .unwrap_or(self.buffer.len()) + } + /// Update self.grapheme_indices with the indices of the current buffer + pub fn update_graphemes(&mut self) { + let indices: Vec<_> = self.buffer + .grapheme_indices(true) + .map(|(i,_)| i) + .collect(); + self.cursor.set_max(indices.len()); + self.grapheme_indices = Some(indices) + } + pub fn grapheme_indices(&self) -> &[usize] { + self.grapheme_indices.as_ref().unwrap() + } + pub fn grapheme_indices_owned(&self) -> Vec { + self.grapheme_indices.as_ref().cloned().unwrap_or_default() + } + pub fn grapheme_at(&mut self, pos: usize) -> Option<&str> { + self.update_graphemes_lazy(); + let indices = self.grapheme_indices(); + let start = indices.get(pos).copied()?; + let end = indices.get(pos + 1).copied().or_else(|| { + if pos + 1 == self.grapheme_indices().len() { + Some(self.buffer.len()) + } else { + None + } + })?; + self.buffer.get(start..end) + } + pub fn grapheme_at_cursor(&mut self) -> Option<&str> { + self.grapheme_at(self.cursor.get()) + } + pub fn slice(&mut self, range: Range) -> Option<&str> { + self.update_graphemes_lazy(); + let start_index = self.grapheme_indices().get(range.start).copied()?; + let end_index = self.grapheme_indices().get(range.end).copied().or_else(|| { + if range.end == self.grapheme_indices().len() { + Some(self.buffer.len()) + } else { + None + } + })?; + self.buffer.get(start_index..end_index) + } + pub fn slice_to(&mut self, end: usize) -> Option<&str> { + self.update_graphemes_lazy(); + flog!(DEBUG,end); + flog!(DEBUG,self.grapheme_indices().len()); + let grapheme_index = self.grapheme_indices().get(end).copied().or_else(|| { + if end == self.grapheme_indices().len() { + Some(self.buffer.len()) + } else { + None + } + })?; + self.buffer.get(..grapheme_index) + } + pub fn slice_from(&mut self, start: usize) -> Option<&str> { + self.update_graphemes_lazy(); + let grapheme_index = *self.grapheme_indices().get(start)?; + self.buffer.get(grapheme_index..) + } + pub fn slice_to_cursor(&mut self) -> Option<&str> { + self.slice_to(self.cursor.get()) + } + pub fn slice_from_cursor(&mut self) -> Option<&str> { + self.slice_from(self.cursor.get()) + } + pub fn drain(&mut self, start: usize, end: usize) -> String { + let drained = if end == self.grapheme_indices().len() { + let start = self.grapheme_indices()[start]; + self.buffer.drain(start..).collect() + } else { + let start = self.grapheme_indices()[start]; + let end = self.grapheme_indices()[end]; + self.buffer.drain(start..end).collect() + }; + self.update_graphemes(); + drained + } + pub fn push(&mut self, ch: char) { + self.buffer.push(ch); + self.update_graphemes(); + } + pub fn push_str(&mut self, slice: &str) { + self.buffer.push_str(slice); + self.update_graphemes(); + } + pub fn insert_at_cursor(&mut self, ch: char) { + let cursor_pos = self.cursor_byte_pos(); + self.buffer.insert(cursor_pos, ch); + self.update_graphemes(); + } + pub fn set_buffer(&mut self, buffer: String) { + self.buffer = buffer; + self.update_graphemes(); + } + pub fn select_range(&self) -> Option<(usize,usize)> { + self.select_range + } + pub fn start_selecting(&mut self, mode: SelectMode) { + self.select_mode = Some(mode); + let range_start = self.cursor; + let mut range_end = self.cursor; + range_end.add(1); + self.select_range = Some((range_start.get(),range_end.get())); + } + pub fn stop_selecting(&mut self) { + self.select_mode = None; + if self.select_range.is_some() { + self.last_selection = self.select_range.take(); + } + } + pub fn rfind_newlines(&mut self, n: usize) -> usize { + self.rfind_newlines_from(self.cursor.get(), n) + } + pub fn find_newlines(&mut self, n: usize) -> usize { + self.find_newlines_from(self.cursor.get(), n) + } + pub fn rfind_newlines_from(&mut self, start_pos: usize, n: usize) -> usize { + let Some(slice) = self.slice_to(start_pos) else { + return 0 + }; + + let mut offset = slice.len(); + let mut count = 0; + + for (i, b) in slice.bytes().rev().enumerate() { + if b == b'\n' { + count += 1; + if count == n { + offset = slice.len() - i - 1; + break; + } + } + } + + let byte_pos = if count == n { + offset // move to *after* the newline + } else { + 0 + }; + + self.find_index_for(byte_pos).unwrap_or(0) + } + pub fn find_newlines_from(&mut self, start_pos: usize, n: usize) -> usize { + let Some(slice) = self.slice_from(start_pos) else { + return self.cursor.max + }; + + let mut count = 0; + for (i, b) in slice.bytes().enumerate() { + if b == b'\n' { + count += 1; + if count == n { + let byte_pos = self.index_byte_pos(start_pos) + i; + return self.find_index_for(byte_pos).unwrap_or(self.cursor.max); + } + } + } + + self.cursor.max + } + pub fn find_index_for(&self, byte_pos: usize) -> Option { + 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 pos != 0 { + pos += 1; // Don't include the newline itself + } + pos + } + pub fn end_of_cursor_line(&mut self) -> usize { + self.find_newlines(1) + } + pub fn this_line(&mut self) -> (usize,usize) { + ( + self.start_of_cursor_line(), + self.end_of_cursor_line() + ) + } + pub fn prev_line(&mut self) -> Option<(usize,usize)> { + if self.start_of_cursor_line() == 0 { + return None + } + let mut start = self.rfind_newlines(2); + if start != 0 { + start += 1; + } + let end = self.find_newlines_from(start, 1); + Some((start, end)) + } + pub fn next_line(&mut self) -> Option<(usize,usize)> { + if self.end_of_cursor_line() == self.cursor.max { + return None; + } + let end = self.find_newlines(2); + let start = self.rfind_newlines_from(end, 1) + 1; + Some((start,end)) + } + pub fn select_lines_backward(&mut self, n: usize) -> (usize,usize) { + let mut start = self.rfind_newlines(n); + if start != 0 { + start += 1; + } + let end = self.end_of_cursor_line(); + (start,end) + } + pub fn select_lines_forward(&mut self, n: usize) -> (usize,usize) { + let start = self.start_of_cursor_line(); + let end = self.find_newlines(n); + (start,end) + } + 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); + if edit_is_merging { + let diff = Edit::diff(&old, &new, curs_pos); + if diff.is_empty() { + return + } + let Some(mut edit) = self.undo_stack.pop() else { + self.undo_stack.push(diff); + return + }; + + edit.new.push_str(&diff.new); + edit.old.push_str(&diff.old); + + self.undo_stack.push(edit); + } else { + let diff = Edit::diff(&old, &new, curs_pos); + if !diff.is_empty() { + self.undo_stack.push(diff); + } + } + } + + pub fn directional_indices_iter(&mut self, dir: Direction) -> Box> { + self.directional_indices_iter_from(self.cursor.get(), dir) + } + pub fn directional_indices_iter_from(&mut self, pos: usize, dir: Direction) -> Box> { + self.update_graphemes_lazy(); + let skip = if pos == 0 { 0 } else { pos + 1 }; + match dir { + Direction::Forward => { + Box::new( + self.grapheme_indices() + .to_vec() + .into_iter() + .skip(skip) + ) as Box> + } + Direction::Backward => { + Box::new( + self.grapheme_indices() + .to_vec() + .into_iter() + .take(pos) + .rev() + ) as Box> + } + } + } + + pub fn dispatch_word_motion(&mut self, to: To, word: Word, dir: Direction) -> usize { + // Not sorry for these method names btw + match to { + To::Start => { + match dir { + Direction::Forward => self.start_of_word_forward_or_end_of_word_backward(word, dir), + Direction::Backward => self.end_of_word_forward_or_start_of_word_backward(word, dir) + } + } + To::End => { + match dir { + Direction::Forward => self.end_of_word_forward_or_start_of_word_backward(word, dir), + Direction::Backward => self.start_of_word_forward_or_end_of_word_backward(word, dir), + } + } + } + } + + /// Finds the start of a word forward, or the end of a word backward + /// + /// Finding the start of a word in the forward direction, and finding the end of a word in the backward direction + /// are logically the same operation, if you use a reversed iterator for the backward motion. + pub fn start_of_word_forward_or_end_of_word_backward(&mut self, word: Word, dir: Direction) -> usize { + let mut pos = self.cursor.get(); + let default = match dir { + Direction::Backward => 0, + Direction::Forward => self.grapheme_indices().len() + }; + let mut indices_iter = self.directional_indices_iter(dir).peekable(); // And make it peekable + + match word { + Word::Big => { + let on_boundary = self.grapheme_at(pos).is_none_or(is_whitespace); + flog!(DEBUG,on_boundary); + flog!(DEBUG,pos); + if on_boundary { + let Some(idx) = indices_iter.next() else { return default }; + pos = idx; + } + flog!(DEBUG,pos); + + // Check current grapheme + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default + }; + let on_whitespace = is_whitespace(&cur_char); + flog!(DEBUG,on_whitespace); + + // Find the next whitespace + if !on_whitespace { + let Some(_ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) else { + return default + }; + } + + // Return the next visible grapheme position + let non_ws_pos = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))).unwrap_or(default); + non_ws_pos + } + Word::Normal => { + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default }; + let Some(next_idx) = indices_iter.next() else { return default }; + let on_boundary = self.grapheme_at(next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char)); + if on_boundary { + pos = next_idx + } + + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default + }; + let on_whitespace = is_whitespace(&cur_char); + + // Advance until hitting whitespace or a different character class + if !on_whitespace { + let other_class_pos = indices_iter.find( + |i| { + self.grapheme_at(*i) + .is_some_and(|c| is_other_class_or_is_ws(c, &cur_char)) + } + ); + let Some(other_class_pos) = other_class_pos else { + return default + }; + // If we hit a different character class, we return here + if self.grapheme_at(other_class_pos).is_some_and(|c| !is_whitespace(c)) { + return other_class_pos + } + } + + // We are now certainly on a whitespace character. Advance until a non-whitespace character. + let non_ws_pos = indices_iter.find( + |i| { + self.grapheme_at(*i) + .is_some_and(|c| !is_whitespace(c)) + } + ).unwrap_or(default); + non_ws_pos + } + } + } + /// Finds the end of a word forward, or the start of a word backward + /// + /// Finding the end of a word in the forward direction, and finding the start of a word in the backward direction + /// are logically the same operation, if you use a reversed iterator for the backward motion. + pub fn end_of_word_forward_or_start_of_word_backward(&mut self, word: Word, dir: Direction) -> usize { + let mut pos = self.cursor.get(); + let default = match dir { + Direction::Backward => 0, + Direction::Forward => self.grapheme_indices().len() + }; + + let mut indices_iter = self.directional_indices_iter(dir).peekable(); + + match word { + Word::Big => { + let Some(next_idx) = indices_iter.next() else { return default }; + let on_boundary = self.grapheme_at(next_idx).is_none_or(is_whitespace); + if on_boundary { + let Some(idx) = indices_iter.next() else { return default }; + pos = idx; + } + + // Check current grapheme + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default + }; + let on_whitespace = is_whitespace(&cur_char); + + // Advance iterator to next visible grapheme + if on_whitespace { + let Some(_non_ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) else { + return default + }; + } + + // The position of the next whitespace will tell us where the end (or start) of the word is + let Some(next_ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) else { + return default + }; + pos = next_ws_pos; + + if pos == self.grapheme_indices().len() { + // We reached the end of the buffer + pos + } else { + // We hit some whitespace, so we will go back one + match dir { + Direction::Forward => pos.saturating_sub(1), + Direction::Backward => pos + 1, + } + } + } + Word::Normal => { + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default }; + let Some(next_idx) = indices_iter.next() else { return default }; + let on_boundary = self.grapheme_at(next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char)); + if on_boundary { + pos = next_idx + } + + // Check current grapheme + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default + }; + let on_whitespace = is_whitespace(&cur_char); + + // Proceed to next visible grapheme + if on_whitespace { + let Some(non_ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) else { + return default + }; + pos = non_ws_pos + } + + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return self.grapheme_indices().len() + }; + // The position of the next differing character class will tell us where the end (or start) of the word is + let Some(next_ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| is_other_class_or_is_ws(c, &cur_char))) else { + return default + }; + pos = next_ws_pos; + + if pos == self.grapheme_indices().len() { + // We reached the end of the buffer + pos + } else { + // We hit some other character class, so we go back one + match dir { + Direction::Forward => pos.saturating_sub(1), + Direction::Backward => pos + 1, + } + } + } + } + } + pub fn rfind_from bool>(&mut self, pos: usize, op: F) -> usize { + let Some(slice) = self.slice_to(pos) else { + return self.grapheme_indices().len() + }; + for (offset,grapheme) in slice.grapheme_indices(true).rev() { + if op(grapheme) { + return pos + offset + } + } + self.grapheme_indices().len() + } + pub fn rfind_from_optional bool>(&mut self, pos: usize, op: F) -> Option { + let slice = self.slice_to(pos)?; + for (offset,grapheme) in slice.grapheme_indices(true).rev() { + if op(grapheme) { + return Some(pos + offset) + } + } + None + } + pub fn rfind bool>(&mut self, op: F) -> usize { + self.rfind_from(self.cursor.get(), op) + } + pub fn rfind_optional bool>(&mut self, op: F) -> Option { + self.rfind_from_optional(self.cursor.get(), op) + } + pub fn find_from bool>(&mut self, pos: usize, op: F) -> usize { + let Some(slice) = self.slice_from(pos) else { + return self.grapheme_indices().len() + }; + for (offset,grapheme) in slice.grapheme_indices(true) { + if op(grapheme) { + return pos + offset + } + } + self.grapheme_indices().len() + } + pub fn find_from_optional bool>(&mut self, pos: usize, op: F) -> Option { + let slice = self.slice_from(pos)?; + for (offset,grapheme) in slice.grapheme_indices(true) { + if op(grapheme) { + return Some(pos + offset) + } + } + None + } + pub fn find_optional bool>(&mut self, op: F) -> Option { + self.find_from_optional(self.cursor.get(), op) + } + pub fn find bool>(&mut self, op: F) -> usize { + self.find_from(self.cursor.get(), op) + } + pub fn eval_motion(&mut self, motion: Motion) -> MotionKind { + let buffer = self.buffer.clone(); + if self.has_hint() { + let hint = self.hint.clone().unwrap(); + self.push_str(&hint); + } + + let eval = match motion { + Motion::WholeLine => { + let (start,end) = self.this_line(); + MotionKind::Inclusive((start,end)) + } + Motion::WordMotion(to, word, dir) => MotionKind::On(self.dispatch_word_motion(to, word, dir)), + Motion::TextObj(text_obj, bound) => todo!(), + Motion::EndOfLastWord => { + let start = self.start_of_cursor_line(); + let mut indices = self.directional_indices_iter_from(start,Direction::Forward); + let mut last_graphical = None; + while let Some(idx) = indices.next() { + let grapheme = self.grapheme_at(idx).unwrap(); + if !is_whitespace(grapheme) { + last_graphical = Some(idx); + } + if grapheme == "\n" { + break + } + } + let Some(last) = last_graphical else { + return MotionKind::Null + }; + MotionKind::On(last) + } + Motion::BeginningOfFirstWord => { + let start = self.start_of_cursor_line(); + let mut indices = self.directional_indices_iter_from(start,Direction::Forward); + let mut first_graphical = None; + while let Some(idx) = indices.next() { + let grapheme = self.grapheme_at(idx).unwrap(); + if !is_whitespace(grapheme) { + flog!(DEBUG,grapheme); + first_graphical = Some(idx); + break + } + if grapheme == "\n" { + break + } + } + let Some(first) = first_graphical else { + return MotionKind::Null + }; + MotionKind::On(first) + } + Motion::BeginningOfLine => MotionKind::On(self.start_of_cursor_line()), + Motion::EndOfLine => MotionKind::On(self.end_of_cursor_line()), + Motion::CharSearch(direction, dest, ch) => todo!(), + Motion::BackwardChar => MotionKind::On(self.cursor.ret_sub(1)), + Motion::ForwardChar => MotionKind::On(self.cursor.ret_add_inclusive(1)), + Motion::LineUp => todo!(), + Motion::LineUpCharwise => todo!(), + Motion::ScreenLineUp => todo!(), + Motion::ScreenLineUpCharwise => todo!(), + Motion::LineDown => todo!(), + Motion::LineDownCharwise => todo!(), + Motion::ScreenLineDown => todo!(), + Motion::ScreenLineDownCharwise => todo!(), + Motion::BeginningOfScreenLine => todo!(), + Motion::FirstGraphicalOnScreenLine => todo!(), + Motion::HalfOfScreen => todo!(), + Motion::HalfOfScreenLineText => todo!(), + Motion::WholeBuffer => todo!(), + Motion::BeginningOfBuffer => todo!(), + Motion::EndOfBuffer => todo!(), + Motion::ToColumn(col) => todo!(), + Motion::ToDelimMatch => todo!(), + Motion::ToBrace(direction) => todo!(), + Motion::ToBracket(direction) => todo!(), + Motion::ToParen(direction) => todo!(), + Motion::Range(start, end) => todo!(), + Motion::RepeatMotion => todo!(), + Motion::RepeatMotionRev => todo!(), + Motion::Null => MotionKind::Null + }; + + self.set_buffer(buffer); + eval + } + pub fn apply_motion(&mut self, motion: MotionKind) { + let last_grapheme_pos = self + .grapheme_indices() + .len() + .saturating_sub(1); + + if self.has_hint() { + let hint = self.hint.take().unwrap(); + self.push_str(&hint); + self.move_cursor(motion); + + if self.cursor.get() > last_grapheme_pos { + let buf_end = if self.cursor.exclusive { + self.cursor.ret_add(1) + } else { + self.cursor.get() + }; + let remainder = self.slice_from(buf_end); + + if remainder.is_some_and(|slice| !slice.is_empty()) { + let remainder = remainder.unwrap().to_string(); + self.hint = Some(remainder); + } + + let buffer = self.slice_to(buf_end).unwrap_or_default(); + self.buffer = buffer.to_string(); + } else { + let old_buffer = self.slice_to(last_grapheme_pos + 1).unwrap().to_string(); + let old_hint = self.slice_from(last_grapheme_pos + 1).unwrap().to_string(); + + self.hint = Some(old_hint); + self.set_buffer(old_buffer); + } + } else { + self.move_cursor(motion); + } + } + pub fn move_cursor(&mut self, motion: MotionKind) { + match motion { + MotionKind::On(pos) => self.cursor.set(pos), + MotionKind::To(pos) => { + self.cursor.set(pos); + + match pos.cmp(&self.cursor.get()) { + std::cmp::Ordering::Less => { + self.cursor.add(1); + } + std::cmp::Ordering::Greater => { + self.cursor.sub(1); + } + std::cmp::Ordering::Equal => { /* Do nothing */ } + } + } + MotionKind::Inclusive((start,_)) | + MotionKind::Exclusive((start,_)) => { + self.cursor.set(start) + } + MotionKind::Null => { /* Do nothing */ } + } + } + pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> { + match verb { + Verb::Delete | + Verb::Yank | + Verb::Change => { + let (start,end) = match motion { + MotionKind::On(pos) => ordered(self.cursor.get(), pos), + MotionKind::To(pos) => { + let pos = match pos.cmp(&self.cursor.get()) { + std::cmp::Ordering::Less => pos + 1, + std::cmp::Ordering::Greater => pos - 1, + std::cmp::Ordering::Equal => pos, + }; + ordered(self.cursor.get(), pos) + } + MotionKind::Inclusive((start,end)) => { + let (start, mut end) = ordered(start, end); + end = ClampedUsize::new(end, self.cursor.max, false).ret_add(1); + (start,end) + } + MotionKind::Exclusive((start,end)) => ordered(start, end), + MotionKind::Null => return Ok(()) + }; + flog!(DEBUG,start,end); + let register_text = if verb == Verb::Yank { + self.slice(start..end) + .map(|c| c.to_string()) + .unwrap_or_default() + } else { + self.drain(start, end) + }; + register.write_to_register(register_text); + self.cursor.set(start); + } + Verb::Rot13 => todo!(), + Verb::ReplaceChar(_) => todo!(), + Verb::ToggleCase => todo!(), + Verb::ToLower => todo!(), + Verb::ToUpper => todo!(), + Verb::Complete => todo!(), + Verb::CompleteBackward => todo!(), + Verb::Undo => todo!(), + Verb::Redo => todo!(), + Verb::RepeatLast => todo!(), + Verb::Put(anchor) => todo!(), + Verb::SwapVisualAnchor => todo!(), + Verb::JoinLines => todo!(), + Verb::InsertChar(ch) => { + self.insert_at_cursor(ch); + self.cursor.add(1); + } + Verb::Insert(string) => { + self.push_str(&string); + let graphemes = string.graphemes(true).count(); + self.cursor.add(graphemes); + } + Verb::Breakline(anchor) => todo!(), + Verb::Indent => todo!(), + Verb::Dedent => todo!(), + Verb::Equalize => todo!(), + Verb::AcceptLineOrNewline => todo!(), + Verb::EndOfFile => { + if self.buffer.is_empty() { + + } + } + Verb::InsertModeLineBreak(anchor) => todo!(), + + Verb::ReplaceMode | + Verb::InsertMode | + Verb::NormalMode | + Verb::VisualMode | + Verb::VisualModeLine | + Verb::VisualModeBlock | + Verb::VisualModeSelectLast => { + /* Already handled */ + self.apply_motion(motion); + } + } + Ok(()) + } + pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { + 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_line_motion = cmd.is_line_motion(); + let is_undo_op = cmd.is_undo_op(); + let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging); + + // Merge character inserts into one edit + if edit_is_merging && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert()) { + if let Some(edit) = self.undo_stack.last_mut() { + edit.stop_merge(); + } + } + + let ViCmd { register, verb, motion, raw_seq: _ } = 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.get(); + + for _ in 0..verb_count.unwrap_or(1) { + for _ in 0..motion_count.unwrap_or(1) { + /* + * Let's evaluate the motion now + * If motion is None, we will try to use self.select_range + * If self.select_range is None, we will use MotionKind::Null + */ + let motion_eval = motion + .clone() + .map(|m| self.eval_motion(m.1)) + .unwrap_or({ + self.select_range + .map(MotionKind::Inclusive) + .unwrap_or(MotionKind::Null) + }); + + if let Some(verb) = verb.clone() { + self.exec_verb(verb.1, motion_eval, register)?; + } else { + self.apply_motion(motion_eval); + } + } + } + + let after = self.buffer.clone(); + if clear_redos { + self.redo_stack.clear(); + } + + if before != after && !is_undo_op { + self.handle_edit(before, after, cursor_pos); + /* + * The buffer has been edited, + * which invalidates the grapheme_indices vector + * We set it to None now, so that self.update_graphemes_lazy() + * will update it when it is needed again + */ + self.grapheme_indices = None; + } + + if !is_line_motion { + self.saved_col = None; + } + + if is_char_insert { + if let Some(edit) = self.undo_stack.last_mut() { + edit.start_merge(); + } + } + + Ok(()) + } pub fn as_str(&self) -> &str { &self.buffer // FIXME: this will have to be fixed up later } } + +pub fn ordered(start: usize, end: usize) -> (usize,usize) { + if start > end { + (end,start) + } else { + (start,end) + } +} diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index a8eadd6..c70eaba 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -1,32 +1,249 @@ -use linebuf::LineBuf; -use term::TermReader; +use keys::{KeyCode, KeyEvent, ModKeys}; +use linebuf::{LineBuf, SelectAnchor, SelectMode}; +use nix::libc::STDOUT_FILENO; +use term::{Layout, LineWriter, TermReader}; +use vicmd::{Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd}; +use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; -use crate::libsh::error::ShResult; +use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}}; +use crate::prelude::*; pub mod term; pub mod linebuf; pub mod layout; +pub mod keys; +pub mod vicmd; +pub mod register; +pub mod vimode; pub trait Readline { - fn readline(&mut self, prompt: Option) -> ShResult; + fn readline(&mut self) -> ShResult; } pub struct FernVi { reader: TermReader, - writer: TermWriter, + writer: LineWriter, + prompt: String, + mode: Box, + old_layout: Option, + repeat_action: Option, + repeat_motion: Option, editor: LineBuf } impl Readline for FernVi { - fn readline(&mut self, prompt: Option) -> ShResult { - todo!() - } -} + fn readline(&mut self) -> ShResult { + self.editor = LineBuf::new().with_initial("The quick brown fox jumps over the lazy dogThe quick brown fox jumps over the a", 1004); + let raw_mode = self.reader.raw_mode(); // Restores termios state on drop -impl FernVi { - pub fn new() -> Self { - Self { + loop { + let new_layout = self.get_layout(); + if let Some(layout) = self.old_layout.as_ref() { + flog!(DEBUG, "clearing???"); + self.writer.clear_rows(layout)?; + } + raw_mode.disable_for(|| self.print_line(new_layout))?; + let key = self.reader.read_key()?; + flog!(DEBUG, key); + + let Some(cmd) = self.mode.handle_key(key) else { + continue + }; + + if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) { + if self.editor.buffer.is_empty() { + std::mem::drop(raw_mode); + sh_quit(0); + } else { + self.editor.buffer.clear(); + continue + } + } + + self.exec_cmd(cmd)?; + + flog!(DEBUG,self.editor.buffer); } } } +impl Default for FernVi { + fn default() -> Self { + Self::new(None) + } +} + +impl FernVi { + pub fn new(prompt: Option) -> Self { + Self { + reader: TermReader::new(), + writer: LineWriter::new(STDOUT_FILENO), + prompt: prompt.unwrap_or("$ ".styled(Style::Green)), + mode: Box::new(ViInsert::new()), + old_layout: None, + repeat_action: None, + repeat_motion: None, + editor: LineBuf::new() + } + } + + pub fn get_layout(&mut self) -> Layout { + let line = self.editor.as_str().to_string(); + let to_cursor = self.editor.slice_to_cursor().unwrap(); + self.writer.get_layout_from_parts(&self.prompt, to_cursor, &line) + } + + pub fn print_line(&mut self, new_layout: Layout) -> ShResult<()> { + + self.writer.redraw( + &self.prompt, + &self.editor, + &new_layout + )?; + + self.writer.flush_write(&self.mode.cursor_style())?; + + self.old_layout = Some(new_layout); + Ok(()) + } + + pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> { + let mut selecting = false; + if cmd.is_mode_transition() { + let count = cmd.verb_count(); + let mut mode: Box = match cmd.verb().unwrap().1 { + Verb::Change | + Verb::InsertModeLineBreak(_) | + Verb::InsertMode => Box::new(ViInsert::new().with_count(count as u16)), + + Verb::NormalMode => Box::new(ViNormal::new()), + + Verb::ReplaceMode => Box::new(ViReplace::new()), + + Verb::VisualModeSelectLast => { + if self.mode.report_mode() != ModeReport::Visual { + self.editor.start_selecting(SelectMode::Char(SelectAnchor::End)); + } + let mut mode: Box = Box::new(ViVisual::new()); + std::mem::swap(&mut mode, &mut self.mode); + self.editor.set_cursor_clamp(self.mode.clamp_cursor()); + + return self.editor.exec_cmd(cmd) + } + Verb::VisualMode => { + selecting = true; + Box::new(ViVisual::new()) + } + + _ => unreachable!() + }; + + std::mem::swap(&mut mode, &mut self.mode); + + self.editor.set_cursor_clamp(self.mode.clamp_cursor()); + if mode.is_repeatable() { + self.repeat_action = mode.as_replay(); + } + + self.editor.exec_cmd(cmd)?; + + if selecting { + self.editor.start_selecting(SelectMode::Char(SelectAnchor::End)); + } else { + self.editor.stop_selecting(); + } + return Ok(()) + } else if cmd.is_cmd_repeat() { + let Some(replay) = self.repeat_action.clone() else { + return Ok(()) + }; + let ViCmd { verb, .. } = 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.editor.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() { + if let Some(v_mut) = cmd.verb.as_mut() { + v_mut.0 = count + } + if let Some(m_mut) = cmd.motion.as_mut() { + m_mut.0 = 1 + } + } else { + return Ok(()) // it has to have a verb to be repeatable, something weird happened + } + } + self.editor.exec_cmd(cmd)?; + } + _ => unreachable!("motions should be handled in the other branch") + } + return Ok(()) + } else if cmd.is_motion_repeat() { + match cmd.motion.as_ref().unwrap() { + MotionCmd(count,Motion::RepeatMotion) => { + let Some(motion) = self.repeat_motion.clone() else { + return Ok(()) + }; + let repeat_cmd = ViCmd { + register: RegisterName::default(), + verb: None, + motion: Some(motion), + raw_seq: format!("{count};") + }; + return self.editor.exec_cmd(repeat_cmd); + } + MotionCmd(count,Motion::RepeatMotionRev) => { + let Some(motion) = self.repeat_motion.clone() else { + return Ok(()) + }; + let mut new_motion = motion.invert_char_motion(); + new_motion.0 = *count; + let repeat_cmd = ViCmd { + register: RegisterName::default(), + verb: None, + motion: Some(new_motion), + raw_seq: format!("{count},") + }; + return self.editor.exec_cmd(repeat_cmd); + } + _ => unreachable!() + } + } + + if cmd.is_repeatable() { + if self.mode.report_mode() == ModeReport::Visual { + // The motion is assigned in the line buffer execution, so we also have to assign it here + // in order to be able to repeat it + let range = self.editor.select_range().unwrap(); + cmd.motion = Some(MotionCmd(1,Motion::Range(range.0, range.1))) + } + self.repeat_action = Some(CmdReplay::Single(cmd.clone())); + } + + if cmd.is_char_search() { + self.repeat_motion = cmd.motion.clone() + } + + self.editor.exec_cmd(cmd.clone())?; + + if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) { + self.editor.stop_selecting(); + let mut mode: Box = Box::new(ViNormal::new()); + std::mem::swap(&mut mode, &mut self.mode); + } + Ok(()) + } +} + diff --git a/src/prompt/readline/register.rs b/src/prompt/readline/register.rs new file mode 100644 index 0000000..5fdc12d --- /dev/null +++ b/src/prompt/readline/register.rs @@ -0,0 +1,168 @@ +use std::sync::Mutex; + +pub static REGISTERS: Mutex = Mutex::new(Registers::new()); + +pub fn read_register(ch: Option) -> Option { + let lock = REGISTERS.lock().unwrap(); + lock.get_reg(ch).map(|r| r.buf().clone()) +} + +pub fn write_register(ch: Option, buf: String) { + let mut lock = REGISTERS.lock().unwrap(); + if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) } +} + +pub fn append_register(ch: Option, buf: String) { + let mut lock = REGISTERS.lock().unwrap(); + if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) } +} + +#[derive(Default,Debug)] +pub struct Registers { + default: Register, + a: Register, + b: Register, + c: Register, + d: Register, + e: Register, + f: Register, + g: Register, + h: Register, + i: Register, + j: Register, + k: Register, + l: Register, + m: Register, + n: Register, + o: Register, + p: Register, + q: Register, + r: Register, + s: Register, + t: Register, + u: Register, + v: Register, + w: Register, + x: Register, + y: Register, + z: Register, +} + +impl Registers { + pub const fn new() -> Self { + Self { + default: Register(String::new()), + a: Register(String::new()), + b: Register(String::new()), + c: Register(String::new()), + d: Register(String::new()), + e: Register(String::new()), + f: Register(String::new()), + g: Register(String::new()), + h: Register(String::new()), + i: Register(String::new()), + j: Register(String::new()), + k: Register(String::new()), + l: Register(String::new()), + m: Register(String::new()), + n: Register(String::new()), + o: Register(String::new()), + p: Register(String::new()), + q: Register(String::new()), + r: Register(String::new()), + s: Register(String::new()), + t: Register(String::new()), + u: Register(String::new()), + v: Register(String::new()), + w: Register(String::new()), + x: Register(String::new()), + y: Register(String::new()), + z: Register(String::new()), + } + } + pub fn get_reg(&self, ch: Option) -> Option<&Register> { + let Some(ch) = ch else { + return Some(&self.default) + }; + match ch { + 'a' => Some(&self.a), + 'b' => Some(&self.b), + 'c' => Some(&self.c), + 'd' => Some(&self.d), + 'e' => Some(&self.e), + 'f' => Some(&self.f), + 'g' => Some(&self.g), + 'h' => Some(&self.h), + 'i' => Some(&self.i), + 'j' => Some(&self.j), + 'k' => Some(&self.k), + 'l' => Some(&self.l), + 'm' => Some(&self.m), + 'n' => Some(&self.n), + 'o' => Some(&self.o), + 'p' => Some(&self.p), + 'q' => Some(&self.q), + 'r' => Some(&self.r), + 's' => Some(&self.s), + 't' => Some(&self.t), + 'u' => Some(&self.u), + 'v' => Some(&self.v), + 'w' => Some(&self.w), + 'x' => Some(&self.x), + 'y' => Some(&self.y), + 'z' => Some(&self.z), + _ => None + } + } + pub fn get_reg_mut(&mut self, ch: Option) -> Option<&mut Register> { + let Some(ch) = ch else { + return Some(&mut self.default) + }; + match ch { + 'a' => Some(&mut self.a), + 'b' => Some(&mut self.b), + 'c' => Some(&mut self.c), + 'd' => Some(&mut self.d), + 'e' => Some(&mut self.e), + 'f' => Some(&mut self.f), + 'g' => Some(&mut self.g), + 'h' => Some(&mut self.h), + 'i' => Some(&mut self.i), + 'j' => Some(&mut self.j), + 'k' => Some(&mut self.k), + 'l' => Some(&mut self.l), + 'm' => Some(&mut self.m), + 'n' => Some(&mut self.n), + 'o' => Some(&mut self.o), + 'p' => Some(&mut self.p), + 'q' => Some(&mut self.q), + 'r' => Some(&mut self.r), + 's' => Some(&mut self.s), + 't' => Some(&mut self.t), + 'u' => Some(&mut self.u), + 'v' => Some(&mut self.v), + 'w' => Some(&mut self.w), + 'x' => Some(&mut self.x), + 'y' => Some(&mut self.y), + 'z' => Some(&mut self.z), + _ => None + } + } +} + +#[derive(Clone,Default,Debug)] +pub struct Register(String); +impl Register { + pub fn buf(&self) -> &String { + &self.0 + } + pub fn write(&mut self, buf: String) { + self.0 = buf + } + pub fn append(&mut self, buf: String) { + self.0.push_str(&buf) + } + pub fn clear(&mut self) { + self.0.clear() + } +} diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 007a1d0..19f9c02 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -1,12 +1,13 @@ -use std::{env, fmt::Write, io::{BufRead, BufReader, Read}, ops::{Deref, DerefMut}, os::fd::{AsFd, BorrowedFd, RawFd}}; +use std::{env, fmt::{Debug, Write}, io::{BufRead, BufReader, Read}, os::fd::{AsFd, BorrowedFd, RawFd}}; -use nix::{errno::Errno, libc, poll::{self, PollFlags, PollTimeout}, unistd::isatty}; +use nix::{errno::Errno, libc::{self, STDIN_FILENO}, poll::{self, PollFlags, PollTimeout}, sys::termios, unistd::isatty}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; -use crate::libsh::error::{ShErr, ShErrKind, ShResult}; +use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prompt::readline::keys::{KeyCode, ModKeys}}; +use crate::prelude::*; -use super::linebuf::LineBuf; +use super::{keys::KeyEvent, linebuf::LineBuf}; pub type Row = u16; pub type Col = u16; @@ -43,7 +44,7 @@ pub fn get_win_size(fd: RawFd) -> (Col,Row) { } else { size.ws_row }; - (cols.into(), rows.into()) + (cols, rows) } _ => (80,24) } @@ -182,7 +183,7 @@ pub struct TermBuffer { impl TermBuffer { pub fn new(tty: RawFd) -> Self { - assert!(isatty(tty).is_ok_and(|r| r == true)); + assert!(isatty(tty).is_ok_and(|r| r)); Self { tty } @@ -191,7 +192,7 @@ impl TermBuffer { impl Read for TermBuffer { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - assert!(isatty(self.tty).is_ok_and(|r| r == true)); + assert!(isatty(self.tty).is_ok_and(|r| r)); loop { match nix::unistd::read(self.tty, buf) { Ok(n) => return Ok(n), @@ -202,10 +203,52 @@ impl Read for TermBuffer { } } +pub struct RawModeGuard { + orig: termios::Termios, + fd: RawFd, +} + +impl RawModeGuard { + /// Disable raw mode temporarily for a specific operation + pub fn disable_for R, R>(&self, func: F) -> R { + unsafe { + let fd = BorrowedFd::borrow_raw(self.fd); + // Temporarily restore the original termios + termios::tcsetattr(fd, termios::SetArg::TCSANOW, &self.orig) + .expect("Failed to temporarily disable raw mode"); + + // Run the function + let result = func(); + + // Re-enable raw mode + let mut raw = self.orig.clone(); + termios::cfmakeraw(&mut raw); + termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw) + .expect("Failed to re-enable raw mode"); + + result + } + } +} + +impl Drop for RawModeGuard { + fn drop(&mut self) { + unsafe { + let _ = termios::tcsetattr(BorrowedFd::borrow_raw(self.fd), termios::SetArg::TCSANOW, &self.orig); + } + } +} + pub struct TermReader { buffer: BufReader } +impl Default for TermReader { + fn default() -> Self { + Self::new() + } +} + impl TermReader { pub fn new() -> Self { Self { @@ -213,8 +256,22 @@ impl TermReader { } } + pub fn raw_mode(&self) -> RawModeGuard { + let fd = self.buffer.get_ref().tty; + let orig = termios::tcgetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}).expect("Failed to get terminal attributes"); + let mut raw = orig.clone(); + termios::cfmakeraw(&mut raw); + termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &raw) + .expect("Failed to set terminal to raw mode"); + RawModeGuard { orig, fd } + } + + /// Execute some logic in raw mode + /// + /// Saves the termios before running the given function. + /// If the given function panics, the panic will halt momentarily to restore the termios pub fn poll(&mut self, timeout: PollTimeout) -> ShResult { - if self.buffer.buffer().len() > 0 { + if !self.buffer.buffer().is_empty() { return Ok(true) } @@ -234,6 +291,7 @@ impl TermReader { } pub fn peek_byte(&mut self) -> std::io::Result { + flog!(DEBUG,"filling buffer"); let buf = self.buffer.fill_buf()?; if buf.is_empty() { Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "EOF")) @@ -245,6 +303,113 @@ impl TermReader { pub fn consume_byte(&mut self) { self.buffer.consume(1); } + + pub fn read_key(&mut self) -> ShResult { + use core::str; + + let mut collected = Vec::with_capacity(4); + + loop { + let byte = self.next_byte()?; + collected.push(byte); + + // If it's an escape seq, delegate to ESC sequence handler + if collected[0] == 0x1b && collected.len() == 1 && self.poll(PollTimeout::ZERO)? { + return self.parse_esc_seq(); + } + + // Try parse as valid UTF-8 + if let Ok(s) = str::from_utf8(&collected) { + return Ok(KeyEvent::new(s, ModKeys::empty())); + } + + // UTF-8 max 4 bytes — if it’s invalid at this point, bail + if collected.len() >= 4 { + break; + } + } + + Ok(KeyEvent(KeyCode::Null, ModKeys::empty())) + } + + pub fn parse_esc_seq(&mut self) -> ShResult { + let mut seq = vec![0x1b]; + + let b1 = self.peek_byte()?; + self.consume_byte(); + seq.push(b1); + + match b1 { + b'[' => { + let b2 = self.peek_byte()?; + self.consume_byte(); + seq.push(b2); + + match b2 { + b'A' => Ok(KeyEvent(KeyCode::Up, ModKeys::empty())), + b'B' => Ok(KeyEvent(KeyCode::Down, ModKeys::empty())), + b'C' => Ok(KeyEvent(KeyCode::Right, ModKeys::empty())), + b'D' => Ok(KeyEvent(KeyCode::Left, ModKeys::empty())), + b'1'..=b'9' => { + let mut digits = vec![b2]; + + loop { + let b = self.peek_byte()?; + seq.push(b); + self.consume_byte(); + + if b == b'~' || b == b';' { + break; + } else if b.is_ascii_digit() { + digits.push(b); + } else { + break; + } + } + + let key = match digits.as_slice() { + [b'1'] => KeyCode::Home, + [b'3'] => KeyCode::Delete, + [b'4'] => KeyCode::End, + [b'5'] => KeyCode::PageUp, + [b'6'] => KeyCode::PageDown, + [b'7'] => KeyCode::Home, // xterm alternate + [b'8'] => KeyCode::End, // xterm alternate + + [b'1', b'5'] => KeyCode::F(5), + [b'1', b'7'] => KeyCode::F(6), + [b'1', b'8'] => KeyCode::F(7), + [b'1', b'9'] => KeyCode::F(8), + [b'2', b'0'] => KeyCode::F(9), + [b'2', b'1'] => KeyCode::F(10), + [b'2', b'3'] => KeyCode::F(11), + [b'2', b'4'] => KeyCode::F(12), + _ => KeyCode::Esc, + }; + + Ok(KeyEvent(key, ModKeys::empty())) + } + _ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())), + } + } + b'O' => { + let b2 = self.peek_byte()?; + self.consume_byte(); + seq.push(b2); + + let key = match b2 { + b'P' => KeyCode::F(1), + b'Q' => KeyCode::F(2), + b'R' => KeyCode::F(3), + b'S' => KeyCode::F(4), + _ => KeyCode::Esc, + }; + + Ok(KeyEvent(key, ModKeys::empty())) + } + _ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())), + } + } } impl AsFd for TermReader { @@ -261,6 +426,15 @@ pub struct Layout { pub end: Pos } +impl Debug for Layout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Layout: ")?; + writeln!(f, "\tPrompt End: {:?}",self.prompt_end)?; + writeln!(f, "\tCursor: {:?}",self.cursor)?; + writeln!(f, "\tEnd: {:?}",self.end) + } +} + impl Layout { pub fn new() -> Self { let w_calc = width_calculator(); @@ -273,6 +447,12 @@ impl Layout { } } +impl Default for Layout { + fn default() -> Self { + Self::new() + } +} + pub struct LineWriter { out: RawFd, t_cols: Col, // terminal width @@ -297,7 +477,8 @@ impl LineWriter { write_all(self.out, buf)?; Ok(()) } - pub fn clear_rows(&mut self, layout: &Layout) { + pub fn clear_rows(&mut self, layout: &Layout) -> ShResult<()> { + self.buffer.clear(); let rows_to_clear = layout.end.row; let cursor_row = layout.cursor.row; @@ -307,27 +488,31 @@ impl LineWriter { } for _ in 0..rows_to_clear { - self.buffer.push_str("\x1b[K\x1b[A"); + self.buffer.push_str("\x1b[2K\x1b[A"); } - self.buffer.push_str("\x1b[K"); - } - pub fn move_cursor(&mut self, old: Pos, new: Pos) -> ShResult<()> { + self.buffer.push_str("\x1b[2K"); + flog!(DEBUG, self.buffer); + write_all(self.out,self.buffer.as_str())?; self.buffer.clear(); - let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer"); + Ok(()) + } + pub fn get_cursor_movement(&self, old: Pos, new: Pos) -> ShResult { + let mut buffer = String::new(); + let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to cursor movement buffer"); match new.row.cmp(&old.row) { std::cmp::Ordering::Greater => { let shift = new.row - old.row; match shift { - 1 => self.buffer.push_str("\x1b[B"), - _ => write!(self.buffer, "\x1b[{shift}B").map_err(err)? + 1 => buffer.push_str("\x1b[B"), + _ => write!(buffer, "\x1b[{shift}B").map_err(err)? } } std::cmp::Ordering::Less => { let shift = old.row - new.row; match shift { - 1 => self.buffer.push_str("\x1b[A"), - _ => write!(self.buffer, "\x1b[{shift}A").map_err(err)? + 1 => buffer.push_str("\x1b[A"), + _ => write!(buffer, "\x1b[{shift}A").map_err(err)? } } std::cmp::Ordering::Equal => { /* Do nothing */ } @@ -337,20 +522,26 @@ impl LineWriter { std::cmp::Ordering::Greater => { let shift = new.col - old.col; match shift { - 1 => self.buffer.push_str("\x1b[C"), - _ => write!(self.buffer, "\x1b[{shift}C").map_err(err)? + 1 => buffer.push_str("\x1b[C"), + _ => write!(buffer, "\x1b[{shift}C").map_err(err)? } } std::cmp::Ordering::Less => { let shift = old.col - new.col; match shift { - 1 => self.buffer.push_str("\x1b[D"), - _ => write!(self.buffer, "\x1b[{shift}D").map_err(err)? + 1 => buffer.push_str("\x1b[D"), + _ => write!(buffer, "\x1b[{shift}D").map_err(err)? } } std::cmp::Ordering::Equal => { /* Do nothing */ } } - write_all(self.out, self.buffer.as_str())?; + Ok(buffer) + } + pub fn move_cursor(&mut self, old: Pos, new: Pos) -> ShResult<()> { + self.buffer.clear(); + let movement = self.get_cursor_movement(old, new)?; + + write_all(self.out, &movement)?; Ok(()) } @@ -358,46 +549,37 @@ impl LineWriter { &mut self, prompt: &str, line: &LineBuf, - old_layout: &Layout, new_layout: &Layout, ) -> ShResult<()> { let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer"); self.buffer.clear(); - self.clear_rows(old_layout); - let end = new_layout.end; let cursor = new_layout.cursor; self.buffer.push_str(prompt); self.buffer.push_str(line.as_str()); - if end.col == 0 - && end.row > 0 - { + if end.col == 0 && end.row > 0 { // The line has wrapped. We need to use our own line break. - self.buffer.push('\n') + self.buffer.push('\n'); } - let cursor_row_offset = end.row - cursor.row; - - match cursor_row_offset { - 0 => { /* Do nothing */ } - 1 => self.buffer.push_str("\x1b[A"), - _ => write!(self.buffer, "\x1b[{cursor_row_offset}A").map_err(err)? - } - - let cursor_col = cursor.col; - match cursor_col { - 0 => self.buffer.push('\r'), - 1 => self.buffer.push_str("\x1b[C"), - _ => write!(self.buffer, "\x1b[{cursor_col}C").map_err(err)? - } + let movement = self.get_cursor_movement(end, cursor)?; + write!(self.buffer, "{}", &movement).map_err(err)?; write_all(self.out, self.buffer.as_str())?; Ok(()) } + pub fn get_layout_from_parts(&mut self, prompt: &str, to_cursor: &str, to_end: &str) -> Layout { + self.update_t_cols(); + let prompt_end = self.calc_pos(prompt, Pos { col: 0, row: 0 }); + let cursor = self.calc_pos(to_cursor, prompt_end); + let end = self.calc_pos(to_end, prompt_end); + Layout { w_calc: width_calculator(), prompt_end, cursor, end } + } + pub fn calc_pos(&self, s: &str, orig: Pos) -> Pos { let mut pos = orig; let mut esc_seq = 0; @@ -417,7 +599,7 @@ impl LineWriter { pos.col = c_width; } } - if pos.col > self.t_cols { + if pos.col >= self.t_cols { pos.row += 1; pos.col = 0; } @@ -430,32 +612,47 @@ impl LineWriter { self.t_cols = t_cols; } - pub fn move_cursor_at_leftmost(&mut self, rdr: &mut TermReader) -> ShResult<()> { - if rdr.poll(PollTimeout::ZERO)? { + pub fn move_cursor_at_leftmost(&mut self, rdr: &mut TermReader, use_newline: bool) -> ShResult<()> { + let result = rdr.poll(PollTimeout::ZERO)?; + if result { // The terminals reply is going to be stuck behind the currently buffered output // So let's get out of here return Ok(()) } // Ping the cursor's position - self.flush_write("\x1b[6n")?; + self.flush_write("\x1b[6n\n")?; - // Excessively paranoid invariant checking - if !rdr.poll(PollTimeout::from(100u8))? - || rdr.next_byte()? as char != '\x1b' - || rdr.next_byte()? as char != '[' - || read_digits_until(rdr, ';')?.is_none() { - // Invariant is broken, get out + if !rdr.poll(PollTimeout::from(255u8))? { return Ok(()) } + + if rdr.next_byte()? as char != '\x1b' { + return Ok(()) + } + + if rdr.next_byte()? as char != '[' { + return Ok(()) + } + + if read_digits_until(rdr, ';')?.is_none() { + return Ok(()) + } + // We just consumed everything up to the column number, so let's get that now let col = read_digits_until(rdr, 'R')?; // The cursor is not at the leftmost, so let's fix that if col != Some(1) { - // We use '\n' instead of '\r' because if there's a bunch of garbage on this line, - // It might pollute the prompt/line buffer if those are shorter than said garbage - self.flush_write("\n")?; + if use_newline { + // We use '\n' instead of '\r' sometimes because if there's a bunch of garbage on this line, + // It might pollute the prompt/line buffer if those are shorter than said garbage + self.flush_write("\n")?; + } else { + // Sometimes though, we know that there's nothing to the right of the cursor after moving + // So we just move to the left. + self.flush_write("\r")?; + } } Ok(()) diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs new file mode 100644 index 0000000..b7984f8 --- /dev/null +++ b/src/prompt/readline/vicmd.rs @@ -0,0 +1,393 @@ +use super::register::{append_register, read_register, write_register}; + +//TODO: write tests that take edit results and cursor positions from actual neovim edits and test them against the behavior of this editor + +#[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: String) { + if self.append { + append_register(self.name, buf); + } else { + write_register(self.name, buf); + } + } + pub fn read_from_register(&self) -> Option { + read_register(self.name) + } +} + +impl Default for RegisterName { + fn default() -> Self { + Self { + name: None, + count: 1, + append: false + } + } +} + +#[derive(Clone,Default,Debug)] +pub struct ViCmd { + pub register: RegisterName, + pub verb: Option, + pub motion: Option, + pub raw_seq: String, +} + +impl ViCmd { + pub fn new() -> Self { + Self::default() + } + pub fn set_motion(&mut self, motion: MotionCmd) { + self.motion = Some(motion) + } + pub fn set_verb(&mut self, verb: VerbCmd) { + self.verb = Some(verb) + } + pub fn verb(&self) -> Option<&VerbCmd> { + self.verb.as_ref() + } + pub fn motion(&self) -> Option<&MotionCmd> { + self.motion.as_ref() + } + pub fn verb_count(&self) -> usize { + self.verb.as_ref().map(|v| v.0).unwrap_or(1) + } + pub fn motion_count(&self) -> usize { + self.motion.as_ref().map(|m| m.0).unwrap_or(1) + } + pub fn is_repeatable(&self) -> bool { + self.verb.as_ref().is_some_and(|v| v.1.is_repeatable()) + } + pub fn is_cmd_repeat(&self) -> bool { + self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatLast)) + } + pub fn is_motion_repeat(&self) -> bool { + self.motion.as_ref().is_some_and(|m| matches!(m.1,Motion::RepeatMotion | Motion::RepeatMotionRev)) + } + pub fn is_char_search(&self) -> bool { + self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::CharSearch(..))) + } + pub fn should_submit(&self) -> bool { + self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline)) + } + 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_line_motion(&self) -> bool { + self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::LineUp | Motion::LineDown)) + } + pub fn is_mode_transition(&self) -> bool { + self.verb.as_ref().is_some_and(|v| { + matches!(v.1, + Verb::Change | + Verb::InsertMode | + Verb::InsertModeLineBreak(_) | + Verb::NormalMode | + Verb::VisualModeSelectLast | + Verb::VisualMode | + Verb::ReplaceMode + ) + }) + } +} + +#[derive(Clone,Debug)] +pub struct VerbCmd(pub usize,pub Verb); +#[derive(Clone,Debug)] +pub struct MotionCmd(pub usize,pub Motion); + +impl MotionCmd { + pub fn invert_char_motion(self) -> Self { + let MotionCmd(count,Motion::CharSearch(dir, dest, ch)) = self else { + unreachable!() + }; + let new_dir = match dir { + Direction::Forward => Direction::Backward, + Direction::Backward => Direction::Forward, + }; + MotionCmd(count,Motion::CharSearch(new_dir, dest, ch)) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum Verb { + Delete, + Change, + Yank, + Rot13, // lol + ReplaceChar(char), + ToggleCase, + ToLower, + ToUpper, + Complete, + CompleteBackward, + Undo, + Redo, + RepeatLast, + Put(Anchor), + ReplaceMode, + InsertMode, + InsertModeLineBreak(Anchor), + NormalMode, + VisualMode, + VisualModeLine, + VisualModeBlock, // dont even know if im going to implement this + VisualModeSelectLast, + SwapVisualAnchor, + JoinLines, + InsertChar(char), + Insert(String), + Breakline(Anchor), + Indent, + Dedent, + Equalize, + AcceptLineOrNewline, + EndOfFile +} + + +impl Verb { + pub fn is_repeatable(&self) -> bool { + matches!(self, + Self::Delete | + Self::Change | + Self::ReplaceChar(_) | + Self::ToLower | + Self::ToUpper | + Self::ToggleCase | + Self::Put(_) | + Self::ReplaceMode | + Self::InsertModeLineBreak(_) | + Self::JoinLines | + Self::InsertChar(_) | + Self::Insert(_) | + Self::Breakline(_) | + Self::Indent | + Self::Dedent | + Self::Equalize + ) + } + pub fn is_edit(&self) -> bool { + matches!(self, + Self::Delete | + Self::Change | + Self::ReplaceChar(_) | + Self::ToggleCase | + Self::ToLower | + Self::ToUpper | + Self::RepeatLast | + Self::Put(_) | + Self::ReplaceMode | + Self::InsertModeLineBreak(_) | + Self::JoinLines | + Self::InsertChar(_) | + Self::Insert(_) | + Self::Breakline(_) | + Self::Rot13 | + Self::EndOfFile + ) + } + pub fn is_char_insert(&self) -> bool { + matches!(self, + Self::Change | + Self::InsertChar(_) | + Self::ReplaceChar(_) + ) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Motion { + WholeLine, + TextObj(TextObj, Bound), + EndOfLastWord, + BeginningOfFirstWord, + BeginningOfLine, + EndOfLine, + WordMotion(To,Word,Direction), + CharSearch(Direction,Dest,char), + BackwardChar, + ForwardChar, + LineUp, + LineUpCharwise, + ScreenLineUp, + ScreenLineUpCharwise, + LineDown, + LineDownCharwise, + ScreenLineDown, + ScreenLineDownCharwise, + BeginningOfScreenLine, + FirstGraphicalOnScreenLine, + HalfOfScreen, + HalfOfScreenLineText, + WholeBuffer, + BeginningOfBuffer, + EndOfBuffer, + ToColumn(usize), + ToDelimMatch, + ToBrace(Direction), + ToBracket(Direction), + ToParen(Direction), + Range(usize,usize), + RepeatMotion, + RepeatMotionRev, + Null +} + +#[derive(Clone,Copy,PartialEq,Eq,Debug)] +pub enum MotionBehavior { + Exclusive, + Inclusive, + Linewise +} + +impl Motion { + pub fn behavior(&self) -> MotionBehavior { + if self.is_linewise() { + MotionBehavior::Linewise + } else if self.is_exclusive() { + MotionBehavior::Exclusive + } else { + MotionBehavior::Inclusive + } + } + pub fn is_exclusive(&self) -> bool { + matches!(&self, + Self::BeginningOfLine | + Self::BeginningOfFirstWord | + Self::BeginningOfScreenLine | + Self::FirstGraphicalOnScreenLine | + Self::LineDownCharwise | + Self::LineUpCharwise | + Self::ScreenLineUpCharwise | + Self::ScreenLineDownCharwise | + Self::ToColumn(_) | + Self::TextObj(TextObj::ForwardSentence,_) | + Self::TextObj(TextObj::BackwardSentence,_) | + Self::TextObj(TextObj::ForwardParagraph,_) | + Self::TextObj(TextObj::BackwardParagraph,_) | + Self::CharSearch(Direction::Backward, _, _) | + Self::WordMotion(To::Start,_,_) | + Self::ToBrace(_) | + Self::ToBracket(_) | + Self::ToParen(_) | + Self::ScreenLineDown | + Self::ScreenLineUp | + Self::Range(_, _) + ) + } + pub fn is_linewise(&self) -> bool { + matches!(self, + Self::WholeLine | + Self::LineUp | + Self::LineDown | + Self::ScreenLineDown | + Self::ScreenLineUp + ) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Anchor { + After, + Before +} +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum TextObj { + /// `iw`, `aw` — inner word, around word + Word(Word), + + /// for stuff like 'dd' + Line, + + /// `is`, `as` — inner sentence, around sentence + ForwardSentence, + BackwardSentence, + + /// `ip`, `ap` — inner paragraph, around paragraph + ForwardParagraph, + BackwardParagraph, + + /// `i"`, `a"` — inner/around double quotes + DoubleQuote, + /// `i'`, `a'` + SingleQuote, + /// `i\``, `a\`` + BacktickQuote, + + /// `i)`, `a)` — round parens + Paren, + /// `i]`, `a]` + Bracket, + /// `i}`, `a}` + Brace, + /// `i<`, `a<` + Angle, + + /// `it`, `at` — HTML/XML tags + Tag, + + /// Custom user-defined objects maybe? + Custom(char), +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Word { + Big, + Normal +} +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Bound { + Inside, + Around +} + +#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] +pub enum Direction { + #[default] + Forward, + Backward +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Dest { + On, + Before, + After +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum To { + Start, + End +} diff --git a/src/prompt/readline/vimode.rs b/src/prompt/readline/vimode.rs new file mode 100644 index 0000000..d71b389 --- /dev/null +++ b/src/prompt/readline/vimode.rs @@ -0,0 +1,1520 @@ +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::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, VerbCmd, ViCmd, Word}; +use crate::prelude::*; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ModeReport { + Insert, + Normal, + Visual, + Replace, + Unknown +} + +#[derive(Debug,Clone)] +pub enum CmdReplay { + ModeReplay { cmds: Vec, repeat: u16 }, + Single(ViCmd), + Motion(Motion) +} + +impl CmdReplay { + 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 { + fn handle_key(&mut self, key: E) -> Option; + fn is_repeatable(&self) -> bool; + fn as_replay(&self) -> Option; + fn cursor_style(&self) -> String; + fn pending_seq(&self) -> Option; + fn move_cursor_on_undo(&self) -> bool; + fn clamp_cursor(&self) -> bool; + fn hist_scroll_start_pos(&self) -> Option; + fn report_mode(&self) -> ModeReport; +} + +#[derive(Default,Debug)] +pub struct ViInsert { + cmds: Vec, + pending_cmd: ViCmd, + repeat_count: u16 +} + +impl ViInsert { + pub fn new() -> Self { + Self::default() + } + pub fn with_count(mut self, repeat_count: u16) -> Self { + self.repeat_count = repeat_count; + self + } + pub fn register_and_return(&mut self) -> Option { + let cmd = self.take_cmd(); + self.register_cmd(&cmd); + Some(cmd) + } + pub fn ctrl_w_is_undo(&self) -> bool { + let insert_count = self.cmds.iter().filter(|cmd| { + matches!(cmd.verb(),Some(VerbCmd(1, Verb::InsertChar(_)))) + }).count(); + let backspace_count = self.cmds.iter().filter(|cmd| { + matches!(cmd.verb(),Some(VerbCmd(1, Verb::Delete))) + }).count(); + insert_count > backspace_count + } + pub fn register_cmd(&mut self, cmd: &ViCmd) { + self.cmds.push(cmd.clone()) + } + pub fn take_cmd(&mut self) -> ViCmd { + std::mem::take(&mut self.pending_cmd) + } +} + +impl ViMode for ViInsert { + fn handle_key(&mut self, key: E) -> Option { + match key { + E(K::Char(ch), M::NONE) => { + 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('W'), M::CTRL) => { + if self.ctrl_w_is_undo() { + self.pending_cmd.set_verb(VerbCmd(1,Verb::Undo)); + self.cmds.clear(); + Some(self.take_cmd()) + } else { + self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete)); + self.pending_cmd.set_motion(MotionCmd(1, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward))); + self.register_and_return() + } + } + E(K::Char('H'), M::CTRL) | + E(K::Backspace, M::NONE) => { + 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(VerbCmd(1,Verb::CompleteBackward)); + self.register_and_return() + } + + E(K::Char('I'), M::CTRL) | + E(K::Tab, M::NONE) => { + self.pending_cmd.set_verb(VerbCmd(1,Verb::Complete)); + self.register_and_return() + } + + E(K::Esc, M::NONE) => { + 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) + } + } + + fn is_repeatable(&self) -> bool { + true + } + + fn as_replay(&self) -> Option { + 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 + } + fn move_cursor_on_undo(&self) -> bool { + true + } + fn clamp_cursor(&self) -> bool { + false + } + fn hist_scroll_start_pos(&self) -> Option { + Some(To::End) + } + fn report_mode(&self) -> ModeReport { + ModeReport::Insert + } +} + +#[derive(Default,Debug)] +pub struct ViReplace { + cmds: Vec, + pending_cmd: ViCmd, + repeat_count: u16 +} + +impl ViReplace { + pub fn new() -> Self { + Self::default() + } + pub fn with_count(mut self, repeat_count: u16) -> Self { + self.repeat_count = repeat_count; + self + } + pub fn register_and_return(&mut self) -> Option { + let cmd = self.take_cmd(); + self.register_cmd(&cmd); + Some(cmd) + } + pub fn ctrl_w_is_undo(&self) -> bool { + let insert_count = self.cmds.iter().filter(|cmd| { + matches!(cmd.verb(),Some(VerbCmd(1, Verb::ReplaceChar(_)))) + }).count(); + let backspace_count = self.cmds.iter().filter(|cmd| { + matches!(cmd.verb(),Some(VerbCmd(1, Verb::Delete))) + }).count(); + insert_count > backspace_count + } + pub fn register_cmd(&mut self, cmd: &ViCmd) { + self.cmds.push(cmd.clone()) + } + pub fn take_cmd(&mut self) -> ViCmd { + std::mem::take(&mut self.pending_cmd) + } +} + +impl ViMode for ViReplace { + fn handle_key(&mut self, key: E) -> Option { + match key { + E(K::Char(ch), M::NONE) => { + self.pending_cmd.set_verb(VerbCmd(1,Verb::ReplaceChar(ch))); + self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)); + self.register_and_return() + } + E(K::Char('W'), M::CTRL) => { + if self.ctrl_w_is_undo() { + self.pending_cmd.set_verb(VerbCmd(1,Verb::Undo)); + self.cmds.clear(); + Some(self.take_cmd()) + } else { + self.pending_cmd.set_motion(MotionCmd(1, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward))); + self.register_and_return() + } + } + E(K::Char('H'), M::CTRL) | + E(K::Backspace, M::NONE) => { + self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); + self.register_and_return() + } + + E(K::BackTab, M::NONE) => { + 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(VerbCmd(1,Verb::Complete)); + self.register_and_return() + } + + E(K::Esc, M::NONE) => { + 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) + } + } + fn is_repeatable(&self) -> bool { + true + } + fn cursor_style(&self) -> String { + "\x1b[4 q".to_string() + } + fn pending_seq(&self) -> Option { + None + } + fn as_replay(&self) -> Option { + Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count)) + } + fn move_cursor_on_undo(&self) -> bool { + true + } + fn clamp_cursor(&self) -> bool { + true + } + fn hist_scroll_start_pos(&self) -> Option { + Some(To::End) + } + fn report_mode(&self) -> ModeReport { + ModeReport::Replace + } +} +#[derive(Default,Debug)] +pub struct ViNormal { + pending_seq: String, +} + +impl ViNormal { + pub fn new() -> Self { + Self::default() + } + pub fn clear_cmd(&mut self) { + self.pending_seq = String::new(); + } + 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(_) => 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 + #[track_caller] + pub fn quit_parse(&mut self) -> Option { + flog!(DEBUG, std::panic::Location::caller()); + 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); + 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 + }; + match ch { + 'g' => { + if let Some(ch) = chars_clone.peek() { + match ch { + 'v' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + '?' => { + chars_clone.next(); + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Rot13)); + } + _ => break 'verb_parse None + } + } else { + break 'verb_parse None + } + } + '.' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::RepeatLast)), + motion: None, + raw_seq: self.take_cmd(), + } + ) + } + 'x' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Delete)), + motion: Some(MotionCmd(1, Motion::ForwardChar)), + raw_seq: self.take_cmd() + } + ) + } + 'X' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Delete)), + motion: Some(MotionCmd(1, Motion::BackwardChar)), + raw_seq: self.take_cmd() + } + ) + } + 's' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Change)), + motion: Some(MotionCmd(1, Motion::ForwardChar)), + raw_seq: self.take_cmd() + } + ) + } + 'S' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Change)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd() + } + ) + } + '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))); + } + '>' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Indent)); + } + '<' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Dedent)); + } + 'r' => { + let ch = chars_clone.next()?; + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))), + motion: Some(MotionCmd(count, Motion::ForwardChar)), + raw_seq: self.take_cmd() + } + ) + } + 'R' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::ReplaceMode)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + '~' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::ToggleCase)), + 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() + } + ) + } + 'v' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::VisualMode)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'V' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::VisualModeLine)), + 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() + } + ) + } + 'J' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::JoinLines)), + motion: None, + 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)) + } + 'Y' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Yank)), + motion: Some(MotionCmd(1, Motion::EndOfLine)), + raw_seq: self.take_cmd() + } + ) + } + 'D' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Delete)), + motion: Some(MotionCmd(1, Motion::EndOfLine)), + raw_seq: self.take_cmd() + } + ) + } + 'C' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Change)), + motion: Some(MotionCmd(1, Motion::EndOfLine)), + raw_seq: self.take_cmd() + } + ) + } + '=' => { + 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 + }; + match (ch, &verb) { + ('?', Some(VerbCmd(_,Verb::Rot13))) | + ('d', Some(VerbCmd(_,Verb::Delete))) | + ('c', Some(VerbCmd(_,Verb::Change))) | + ('y', Some(VerbCmd(_,Verb::Yank))) | + ('=', Some(VerbCmd(_,Verb::Equalize))) | + ('>', 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::WordMotion(To::End, Word::Normal, Direction::Backward))); + } + 'E' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Backward))); + } + 'k' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp)); + } + 'j' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown)); + } + '_' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::EndOfLastWord)); + } + '0' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfScreenLine)); + } + '^' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::FirstGraphicalOnScreenLine)); + } + _ => return self.quit_parse() + } + } else { + break 'motion_parse None + } + } + 'G' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer)); + } + 'f' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, *ch))) + } + 'F' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, *ch))) + } + 't' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, *ch))) + } + 'T' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, *ch))) + } + ';' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion)); + } + ',' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev)); + } + '|' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count))); + } + '^' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfFirstWord)); + } + '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::WordMotion(To::Start, Word::Normal, Direction::Forward))); + } + 'W' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Forward))); + } + 'e' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Forward))); + } + 'E' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward))); + } + 'b' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward))); + } + 'B' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Backward))); + } + 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 => { + Some( + ViCmd { + register, + verb, + motion, + raw_seq: std::mem::take(&mut self.pending_seq) + } + ) + } + CmdState::Pending => { + None + } + CmdState::Invalid => { + self.pending_seq.clear(); + None + } + } + } +} + +impl ViMode for ViNormal { + fn handle_key(&mut self, key: E) -> Option { + match key { + E(K::Char(ch), M::NONE) => self.try_parse(ch), + E(K::Backspace, M::NONE) => { + Some(ViCmd { + register: Default::default(), + verb: None, + motion: Some(MotionCmd(1, Motion::BackwardChar)), + raw_seq: "".into(), + }) + } + 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() + } + ) + } + E(K::Esc, M::NONE) => { + self.clear_cmd(); + None + } + _ => { + if let Some(cmd) = common_cmds(key) { + self.clear_cmd(); + Some(cmd) + } else { + None + } + } + } + } + + fn is_repeatable(&self) -> bool { + false + } + + fn as_replay(&self) -> Option { + None + } + + fn cursor_style(&self) -> String { + "\x1b[2 q".to_string() + } + + fn pending_seq(&self) -> Option { + Some(self.pending_seq.clone()) + } + + fn move_cursor_on_undo(&self) -> bool { + false + } + fn clamp_cursor(&self) -> bool { + true + } + fn hist_scroll_start_pos(&self) -> Option { + None + } + fn report_mode(&self) -> ModeReport { + ModeReport::Normal + } +} + +#[derive(Default,Debug)] +pub struct ViVisual { + pending_seq: String, +} + +impl ViVisual { + pub fn new() -> Self { + Self::default() + } + pub fn clear_cmd(&mut self) { + self.pending_seq = String::new(); + } + 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(_) => 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 + #[track_caller] + pub fn quit_parse(&mut self) -> Option { + flog!(DEBUG, std::panic::Location::caller()); + 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); + 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 + }; + match ch { + 'g' => { + if let Some(ch) = chars_clone.peek() { + match ch { + 'v' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + '?' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Rot13)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + _ => break 'verb_parse None + } + } else { + break 'verb_parse None + } + } + '.' => { + 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::Delete)); + } + 'X' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Delete)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd(), + } + ) + } + 'Y' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Yank)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd() + } + ) + } + 'D' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Delete)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd() + } + ) + } + 'R' | + 'C' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Change)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd(), + } + ) + } + '>' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Indent)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd(), + } + ) + } + '<' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Dedent)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd(), + } + ) + } + '=' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::Equalize)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd(), + } + ) + } + 'p' | + 'P' => { + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before))); + } + 'r' => { + let ch = chars_clone.next()?; + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + '~' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(1, Verb::ToggleCase)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'u' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::ToLower)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'U' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::ToUpper)), + motion: None, + raw_seq: self.take_cmd() + } + ) + } + 'O' | + 'o' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::SwapVisualAnchor)), + 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() + } + ) + } + 'I' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::InsertMode)), + motion: Some(MotionCmd(1, Motion::BeginningOfLine)), + raw_seq: self.take_cmd() + } + ) + } + 'J' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::JoinLines)), + motion: None, + 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)) + } + _ => break 'verb_parse None + } + }; + + if let Some(verb) = verb { + return Some(ViCmd { + register, + verb: Some(verb), + motion: None, + raw_seq: self.take_cmd() + }) + } + + 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 + }; + match (ch, &verb) { + ('d', Some(VerbCmd(_,Verb::Delete))) | + ('c', Some(VerbCmd(_,Verb::Change))) | + ('y', Some(VerbCmd(_,Verb::Yank))) | + ('=', Some(VerbCmd(_,Verb::Equalize))) | + ('>', 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::WordMotion(To::End, Word::Normal, Direction::Backward))); + } + 'E' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Backward))); + } + 'k' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp)); + } + 'j' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown)); + } + _ => return self.quit_parse() + } + } else { + break 'motion_parse None + } + } + 'f' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, (*ch).into()))) + } + 'F' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, (*ch).into()))) + } + 't' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, (*ch).into()))) + } + 'T' => { + let Some(ch) = chars_clone.peek() else { + break 'motion_parse None + }; + + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, (*ch).into()))) + } + ';' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion)); + } + ',' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev)); + } + '|' => { + 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::WordMotion(To::Start, Word::Normal, Direction::Forward))); + } + 'W' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Forward))); + } + 'e' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Forward))); + } + 'E' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward))); + } + 'b' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward))); + } + 'B' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Backward))); + } + 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) + } + ); + cmd + } + CmdState::Pending => { + None + } + CmdState::Invalid => { + self.pending_seq.clear(); + None + } + } + } +} + +impl ViMode for ViVisual { + fn handle_key(&mut self, key: E) -> Option { + match key { + E(K::Char(ch), M::NONE) => self.try_parse(ch), + E(K::Backspace, M::NONE) => { + Some(ViCmd { + register: Default::default(), + verb: None, + motion: Some(MotionCmd(1, Motion::BackwardChar)), + raw_seq: "".into(), + }) + } + 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() + } + ) + } + E(K::Esc, M::NONE) => { + Some( + ViCmd { + register: Default::default(), + verb: Some(VerbCmd(1, Verb::NormalMode)), + motion: Some(MotionCmd(1, Motion::Null)), + raw_seq: self.take_cmd() + }) + } + _ => { + if let Some(cmd) = common_cmds(key) { + self.clear_cmd(); + Some(cmd) + } else { + None + } + } + } + } + + fn is_repeatable(&self) -> bool { + true + } + + fn as_replay(&self) -> Option { + None + } + + fn cursor_style(&self) -> String { + "\x1b[2 q".to_string() + } + + fn pending_seq(&self) -> Option { + Some(self.pending_seq.clone()) + } + + fn move_cursor_on_undo(&self) -> bool { + true + } + + fn clamp_cursor(&self) -> bool { + true + } + + fn hist_scroll_start_pos(&self) -> Option { + None + } + + fn report_mode(&self) -> ModeReport { + ModeReport::Visual + } +} + +pub fn common_cmds(key: E) -> Option { + let mut pending_cmd = ViCmd::new(); + match key { + 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::AcceptLineOrNewline)), + 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::Delete)); + pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar)); + } + E(K::Backspace, M::NONE) | + E(K::Char('H'), M::CTRL) => { + pending_cmd.set_verb(VerbCmd(1,Verb::Delete)); + pending_cmd.set_motion(MotionCmd(1, Motion::BackwardChar)); + } + _ => return None + } + Some(pending_cmd) +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 2c5083b..9dfe00c 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -26,6 +26,7 @@ pub mod error; pub mod getopt; pub mod script; pub mod highlight; +pub mod readline; /// Unsafe to use outside of tests pub fn get_nodes(input: &str, filter: F1) -> Vec diff --git a/src/tests/readline.rs b/src/tests/readline.rs new file mode 100644 index 0000000..5a86a51 --- /dev/null +++ b/src/tests/readline.rs @@ -0,0 +1,131 @@ +use crate::prompt::readline::linebuf::LineBuf; + +use super::super::*; + +#[test] +fn linebuf_empty_linebuf() { + let mut buf = LineBuf::new(); + assert_eq!(buf.as_str(), ""); + buf.update_graphemes_lazy(); + assert_eq!(buf.grapheme_indices(), &[]); + assert!(buf.slice(0..0).is_none()); +} + +#[test] +fn linebuf_ascii_content() { + let mut buf = LineBuf::new().with_initial("hello", 0); + + buf.update_graphemes_lazy(); + assert_eq!(buf.grapheme_indices(), &[0, 1, 2, 3, 4]); + + assert_eq!(buf.grapheme_at(0), Some("h")); + assert_eq!(buf.grapheme_at(4), Some("o")); + assert_eq!(buf.slice(1..4), Some("ell")); + assert_eq!(buf.slice_to(2), Some("he")); + assert_eq!(buf.slice_from(2), Some("llo")); +} + +#[test] +fn linebuf_unicode_graphemes() { + let mut buf = LineBuf::new().with_initial("a🇺🇸b́c", 0); + + buf.update_graphemes_lazy(); + let indices = buf.grapheme_indices(); + assert_eq!(indices.len(), 4); // 4 graphemes + 1 end marker + + assert_eq!(buf.grapheme_at(0), Some("a")); + assert_eq!(buf.grapheme_at(1), Some("🇺🇸")); + assert_eq!(buf.grapheme_at(2), Some("b́")); // b + combining accent + assert_eq!(buf.grapheme_at(3), Some("c")); + assert_eq!(buf.grapheme_at(4), None); // out of bounds + + assert_eq!(buf.slice(0..2), Some("a🇺🇸")); + assert_eq!(buf.slice(1..3), Some("🇺🇸b́")); + assert_eq!(buf.slice(2..4), Some("b́c")); +} + +#[test] +fn linebuf_slice_to_from_cursor() { + let mut buf = LineBuf::new().with_initial("abçd", 2); + + buf.update_graphemes_lazy(); + assert_eq!(buf.slice_to_cursor(), Some("ab")); + assert_eq!(buf.slice_from_cursor(), Some("çd")); +} + +#[test] +fn linebuf_out_of_bounds_slices() { + let mut buf = LineBuf::new().with_initial("test", 0); + + buf.update_graphemes_lazy(); + + assert_eq!(buf.grapheme_at(5), None); // out of bounds + assert_eq!(buf.slice(2..5), None); // end out of bounds + assert_eq!(buf.slice(4..4), None); // valid but empty +} + +#[test] +fn linebuf_this_line() { + let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line"; + let mut buf = LineBuf::new().with_initial(initial, 57); + let (start,end) = buf.this_line(); + assert_eq!(buf.slice(start..end), Some("This is the third line")) +} + +#[test] +fn linebuf_prev_line() { + let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line"; + let mut buf = LineBuf::new().with_initial(initial, 57); + let (start,end) = buf.prev_line().unwrap(); + assert_eq!(buf.slice(start..end), Some("This is the second line")) +} + +#[test] +fn linebuf_next_line() { + let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line"; + let mut buf = LineBuf::new().with_initial(initial, 57); + let (start,end) = buf.next_line().unwrap(); + assert_eq!(buf.slice(start..end), Some("This is the fourth line")) +} + +#[test] +fn linebuf_cursor_motion() { + let mut buf = LineBuf::new().with_initial("Thé quíck 🦊 bröwn fóx jumpś óver the 💤 lázy dóg 🐶", 0); + + buf.update_graphemes_lazy(); + let total = buf.grapheme_indices.as_ref().unwrap().len(); + + for i in 0..total { + buf.cursor.set(i); + + let expected_to = buf.buffer.get(..buf.grapheme_indices_owned()[i]).unwrap_or("").to_string(); + let expected_from = if i + 1 < total { + buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string() + } else { + // last grapheme, ends at buffer end + buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string() + }; + + let expected_at = { + let start = buf.grapheme_indices_owned()[i]; + let end = buf.grapheme_indices_owned().get(i + 1).copied().unwrap_or(buf.buffer.len()); + buf.buffer.get(start..end).map(|slice| slice.to_string()) + }; + + assert_eq!( + buf.slice_to_cursor(), + Some(expected_to.as_str()), + "Failed at cursor position {i}: slice_to_cursor" + ); + assert_eq!( + buf.slice_from_cursor(), + Some(expected_from.as_str()), + "Failed at cursor position {i}: slice_from_cursor" + ); + assert_eq!( + buf.grapheme_at(i).map(|slice| slice.to_string()), + expected_at, + "Failed at cursor position {i}: grapheme_at" + ); + } +}