diff --git a/Cargo.lock b/Cargo.lock index 01c5fe6..e0a0477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,7 @@ dependencies = [ "nix", "pretty_assertions", "regex", + "unicode-segmentation", "unicode-width", ] @@ -336,6 +337,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 409109c..5d29568 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ insta = "1.42.2" nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] } pretty_assertions = "1.4.1" regex = "1.11.1" +unicode-segmentation = "1.12.0" unicode-width = "0.2.0" [[bin]] diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 0c6bea4..eb92a18 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -3,7 +3,7 @@ pub mod highlight; use std::path::Path; -use readline::FernReader; +use readline::FernVi; use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, state::read_shopts}; @@ -22,6 +22,6 @@ fn get_prompt() -> ShResult { pub fn read_line() -> ShResult { let prompt = get_prompt()?; - let mut reader = FernReader::new(prompt); + let mut reader = FernVi::new(Some(prompt)); reader.readline() } diff --git a/src/prompt/readline/keys.rs b/src/prompt/readline/keys.rs index eb9d60f..6cd784a 100644 --- a/src/prompt/readline/keys.rs +++ b/src/prompt/readline/keys.rs @@ -1,63 +1,93 @@ +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,Debug)] pub struct KeyEvent(pub KeyCode, pub ModKeys); + impl KeyEvent { - pub fn new(ch: char, mut mods: ModKeys) -> Self { + pub fn new(ch: &str, mut mods: ModKeys) -> Self { use {KeyCode as K, KeyEvent as E, ModKeys as M}; - if !ch.is_control() { - if !mods.is_empty() { - mods.remove(M::SHIFT); // TODO Validate: no SHIFT even if - // `c` is uppercase - } - return E(K::Char(ch), mods); + 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 } - match ch { - '\x00' => E(K::Char('@'), mods | M::CTRL), // '\0' - '\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), // '\a' - '\x08' => E(K::Backspace, mods), // '\b' - '\x09' => { - // '\t' - if mods.contains(M::SHIFT) { - mods.remove(M::SHIFT); - E(K::BackTab, mods) - } else { - E(K::Tab, mods) + + 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), } } - '\x0a' => E(K::Char('J'), mods | M::CTRL), // '\n' (10) - '\x0b' => E(K::Char('K'), mods | M::CTRL), - '\x0c' => E(K::Char('L'), mods | M::CTRL), - '\x0d' => E(K::Enter, mods), // '\r' (13) - '\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), // Ctrl-[, '\e' - '\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), // Rubout, Ctrl-? - '\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) + } } } } @@ -70,6 +100,7 @@ pub enum KeyCode { BracketedPasteStart, BracketedPasteEnd, Char(char), + Grapheme(Arc), Delete, Down, End, diff --git a/src/prompt/readline/line.rs b/src/prompt/readline/line.rs deleted file mode 100644 index e3566e9..0000000 --- a/src/prompt/readline/line.rs +++ /dev/null @@ -1,678 +0,0 @@ -use std::ops::Range; - -use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prompt::readline::linecmd::Anchor}; - -use super::linecmd::{At, CharSearch, MoveCmd, Movement, Repeat, Verb, VerbCmd, Word}; - - -#[derive(Default,Debug)] -pub struct LineBuf { - pub buffer: Vec, - pub inserting: bool, - pub last_insert: String, - cursor: usize -} - -impl LineBuf { - pub fn new() -> Self { - Self::default() - } - pub fn with_initial(mut self, init: S) -> Self { - self.buffer = init.to_string().chars().collect(); - self - } - pub fn begin_insert(&mut self) { - self.inserting = true; - } - pub fn finish_insert(&mut self) { - self.inserting = false; - } - pub fn take_ins_text(&mut self) -> String { - std::mem::take(&mut self.last_insert) - } - pub fn display_lines(&self) -> Vec { - let line_bullet = "∙ ".styled(Style::Dim); - self.split_lines() - .into_iter() - .enumerate() - .map(|(i, line)| { - if i == 0 { - line.to_string() - } else { - format!("{line_bullet}{line}") - } - }) - .collect() - } - pub fn repos_cursor(&mut self) { - if self.cursor >= self.len() { - self.cursor = self.len_minus_one(); - } - } - pub fn split_lines(&self) -> Vec { - let line = self.prepare_line(); - let mut lines = vec![]; - let mut cur_line = String::new(); - for ch in line.chars() { - match ch { - '\n' => lines.push(std::mem::take(&mut cur_line)), - _ => cur_line.push(ch) - } - } - lines.push(cur_line); - lines - } - pub fn count_lines(&self) -> usize { - self.buffer.iter().filter(|&&c| c == '\n').count() - } - pub fn cursor(&self) -> usize { - self.cursor - } - pub fn prepare_line(&self) -> String { - self.buffer - .iter() - .filter(|&&c| c != '\r') - .collect::() - } - pub fn clear(&mut self) { - self.buffer.clear(); - self.cursor = 0; - } - pub fn cursor_display_coords(&self) -> (usize, usize) { - let mut x = 0; - let mut y = 0; - for i in 0..self.cursor() { - let ch = self.get_char(i); - match ch { - '\n' => { - y += 1; - x = 0; - } - '\r' => continue, - _ => { - x += 1; - } - } - } - - (x, y) - } - pub fn cursor_real_coords(&self) -> (usize,usize) { - let mut x = 0; - let mut y = 0; - for i in 0..self.cursor() { - let ch = self.get_char(i); - match ch { - '\n' => { - y += 1; - x = 0; - } - _ => { - x += 1; - } - } - } - - (x, y) - } - pub fn backspace(&mut self) { - if self.cursor() == 0 { - return - } - self.delete_pos(self.cursor() - 1); - } - pub fn delete(&mut self) { - if self.cursor() >= self.buffer.len() { - return - } - self.delete_pos(self.cursor()); - } - pub fn delete_pos(&mut self, pos: usize) { - self.buffer.remove(pos); - if pos < self.cursor() { - self.cursor = self.cursor.saturating_sub(1) - } - } - pub fn insert_at_pos(&mut self, pos: usize, ch: char) { - self.buffer.insert(pos, ch) - } - pub fn insert_at_cursor(&mut self, ch: char) { - self.buffer.insert(self.cursor, ch); - self.move_cursor_right(); - } - pub fn insert_after_cursor(&mut self, ch: char) { - self.buffer.insert(self.cursor, ch); - } - pub fn backspace_at_cursor(&mut self) { - assert!(self.cursor <= self.buffer.len()); - if self.buffer.is_empty() { - return - } - self.buffer.remove(self.cursor.saturating_sub(1)); - self.move_cursor_left(); - } - pub fn del_at_cursor(&mut self) { - assert!(self.cursor <= self.buffer.len()); - if self.buffer.is_empty() || self.cursor == self.buffer.len() { - return - } - self.buffer.remove(self.cursor); - } - pub fn move_cursor_left(&mut self) { - self.cursor = self.cursor.saturating_sub(1); - } - pub fn move_cursor_start(&mut self) { - self.cursor = 0; - } - pub fn move_cursor_end(&mut self) { - self.cursor = self.buffer.len(); - } - pub fn move_cursor_right(&mut self) { - if self.cursor == self.buffer.len() { - return - } - self.cursor = self.cursor.saturating_add(1); - } - pub fn del_from_cursor(&mut self) { - self.buffer.truncate(self.cursor); - } - pub fn del_word_back(&mut self) { - if self.cursor == 0 { - return - } - let end = self.cursor; - let mut start = self.cursor; - - while start > 0 && self.buffer[start - 1].is_whitespace() { - start -= 1; - } - - while start > 0 && !self.buffer[start - 1].is_whitespace() { - start -= 1; - } - - self.buffer.drain(start..end); - self.cursor = start; - } - pub fn len(&self) -> usize { - self.buffer.len() - } - pub fn len_minus_one(&self) -> usize { - self.buffer.len().saturating_sub(1) - } - pub fn is_empty(&self) -> bool { - self.buffer.is_empty() - } - pub fn cursor_char(&self) -> Option<&char> { - self.buffer.get(self.cursor()) - } - pub fn get_char(&self, pos: usize) -> char { - assert!((0..self.len()).contains(&pos)); - - self.buffer[pos] - } - pub fn prev_char(&self) -> Option { - if self.cursor() == 0 { - None - } else { - Some(self.get_char(self.cursor() - 1)) - } - } - pub fn next_char(&self) -> Option { - if self.cursor() == self.len_minus_one() { - None - } else { - Some(self.get_char(self.cursor() + 1)) - } - } - pub fn on_word_bound_left(&self) -> bool { - if self.cursor() == 0 { - return false - } - let Some(ch) = self.cursor_char() else { - return false - }; - let cur_char_class = CharClass::from(*ch); - let prev_char_pos = self.cursor().saturating_sub(1).max(0); - cur_char_class.is_opposite(self.get_char(prev_char_pos)) - } - pub fn on_word_bound_right(&self) -> bool { - if self.cursor() >= self.len_minus_one() { - return false - } - let Some(ch) = self.cursor_char() else { - return false - }; - let cur_char_class = CharClass::from(*ch); - let next_char_pos = self.cursor().saturating_add(1).min(self.len()); - cur_char_class.is_opposite(self.get_char(next_char_pos)) - } - fn backward_until bool>(&self, mut start: usize, cond: F) -> usize { - while start > 0 && !cond(start) { - start -= 1; - } - start - } - fn forward_until bool>(&self, mut start: usize, cond: F) -> usize { - while start < self.len() && !cond(start) { - start += 1; - } - start - } - pub fn calc_range(&mut self, movement: &Movement) -> Range { - let mut start = self.cursor(); - let mut end = self.cursor(); - - match movement { - Movement::WholeLine => { - start = self.backward_until(start, |pos| self.buffer[pos] == '\n'); - if self.buffer.get(start) == Some(&'\n') { - start += 1; // Exclude the previous newline - } - end = self.forward_until(end, |pos| self.buffer[pos] == '\n'); - } - Movement::BeginningOfLine => { - start = self.backward_until(start, |pos| self.buffer[pos] == '\n'); - } - Movement::BeginningOfFirstWord => { - let start_of_line = self.backward_until(start, |pos| self.buffer[pos] == '\n'); - start = self.forward_until(start_of_line, |pos| !self.buffer[pos].is_whitespace()); - } - Movement::EndOfLine => { - end = self.forward_until(end, |pos| self.buffer[pos] == '\n'); - } - Movement::BackwardWord(word) => { - match word { - Word::Big => { - if self.cursor_char().is_none() { - self.cursor = self.cursor.saturating_sub(1); - start = start.saturating_sub(1) - } - // Skip whitespace - let Some(cur_char) = self.cursor_char() else { - return start..end - }; - if cur_char.is_whitespace() { - start = self.backward_until(start, |pos| !self.buffer[pos].is_whitespace()) - } - - let ch_class = CharClass::from(self.get_char(start)); - let should_step = self.prev_char().is_some_and(|ch| ch_class.is_opposite(ch)); - // If we are on a word boundary, move forward one character - // If we are now on whitespace, skip it - if should_step { - start = start.saturating_sub(1).max(0); - if self.get_char(start).is_whitespace() { - start = self.backward_until(start, |pos| !self.buffer[pos].is_whitespace()) - } - } - start = self.backward_until(start, |pos| self.buffer[pos].is_whitespace()); - if self.get_char(start).is_whitespace() { - start += 1; - } - } - Word::Normal => { - if self.cursor_char().is_none() { - self.cursor = self.cursor.saturating_sub(1); - start = start.saturating_sub(1) - } - let Some(cur_char) = self.cursor_char() else { - return start..end - }; - // Skip whitespace - if cur_char.is_whitespace() { - start = self.backward_until(start, |pos| !self.get_char(pos).is_whitespace()) - } - - let ch_class = CharClass::from(self.get_char(start)); - let should_step = self.prev_char().is_some_and(|ch| ch_class.is_opposite(ch)); - // If we are on a word boundary, move forward one character - // If we are now on whitespace, skip it - if should_step { - start = start.saturating_sub(1).max(0); - if self.get_char(start).is_whitespace() { - start = self.backward_until(start, |pos| !self.get_char(pos).is_whitespace()) - } - } - - // Find an alternate charclass to stop at - let cur_char = self.get_char(start); - let cur_char_class = CharClass::from(cur_char); - start = self.backward_until(start, |pos| cur_char_class.is_opposite(self.get_char(pos))); - if cur_char_class.is_opposite(self.get_char(start)) { - start += 1; - } - } - } - } - Movement::ForwardWord(at, word) => { - let Some(cur_char) = self.cursor_char() else { - return start..end - }; - let is_ws = |pos: usize| self.buffer[pos].is_whitespace(); - let not_ws = |pos: usize| !self.buffer[pos].is_whitespace(); - - match word { - Word::Big => { - if cur_char.is_whitespace() { - end = self.forward_until(end, not_ws); - } - - let ch_class = CharClass::from(self.buffer[end]); - let should_step = self.next_char().is_some_and(|ch| ch_class.is_opposite(ch)); - - if should_step { - end = end.saturating_add(1).min(self.len_minus_one()); - if self.get_char(end).is_whitespace() { - end = self.forward_until(end, |pos| !self.get_char(pos).is_whitespace()) - } - } - - match at { - At::Start => { - if !should_step { - end = self.forward_until(end, is_ws); - end = self.forward_until(end, not_ws); - } - } - At::AfterEnd => { - end = self.forward_until(end, is_ws); - } - At::BeforeEnd => { - end = self.forward_until(end, is_ws); - if self.buffer.get(end).is_some_and(|ch| ch.is_whitespace()) { - end = end.saturating_sub(1); - } - } - } - } - Word::Normal => { - if cur_char.is_whitespace() { - end = self.forward_until(end, not_ws); - } - - let ch_class = CharClass::from(self.buffer[end]); - let should_step = self.next_char().is_some_and(|ch| ch_class.is_opposite(ch)); - - if should_step { - end = end.saturating_add(1).min(self.len_minus_one()); - if self.get_char(end).is_whitespace() { - end = self.forward_until(end, |pos| !self.get_char(pos).is_whitespace()) - } - } - - match at { - At::Start => { - if !should_step { - end = self.forward_until(end, |pos| ch_class.is_opposite(self.buffer[pos])); - if self.get_char(end).is_whitespace() { - end = self.forward_until(end, |pos| !self.get_char(pos).is_whitespace()) - } - } - } - At::AfterEnd => { - end = self.forward_until(end, |pos| ch_class.is_opposite(self.buffer[pos])); - } - At::BeforeEnd => { - end = self.forward_until(end, |pos| ch_class.is_opposite(self.buffer[pos])); - if self.buffer.get(end).is_some_and(|ch| ch.is_whitespace()) { - end = end.saturating_sub(1); - } - } - } - } - } - } - Movement::BackwardChar => { - start = start.saturating_sub(1); - } - Movement::ForwardChar => { - end = end.saturating_add(1); - } - Movement::TextObj(text_obj, bound) => todo!(), - Movement::CharSearch(char_search) => { - match char_search { - CharSearch::FindFwd(ch) => { - let ch = ch.unwrap(); - end = end.saturating_add(1).min(self.len_minus_one()); - let search = self.forward_until(end, |pos| self.buffer[pos] == ch); - - // we check anyway because it may have reached the end without finding anything - if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) { - end = search; - } - } - CharSearch::FwdTo(ch) => { - let ch = ch.unwrap(); - end = end.saturating_add(1).min(self.len_minus_one()); - let search = self.forward_until(end, |pos| self.buffer[pos] == ch); - - // we check anyway because it may have reached the end without finding anything - if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) { - end = search.saturating_sub(1); - } - } - CharSearch::FindBkwd(ch) => { - let ch = ch.unwrap(); - start = start.saturating_sub(1); - let search = self.backward_until(start, |pos| self.buffer[pos] == ch); - - // we check anyway because it may have reached the end without finding anything - if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) { - start = search; - } - } - CharSearch::BkwdTo(ch) => { - let ch = ch.unwrap(); - start = start.saturating_sub(1); - let search = self.backward_until(start, |pos| self.buffer[pos] == ch); - - // we check anyway because it may have reached the end without finding anything - if self.buffer.get(search).is_some_and(|&s_ch| s_ch == ch) { - start = search.saturating_add(1); - } - } - } - } - Movement::LineUp => todo!(), - Movement::LineDown => todo!(), - Movement::WholeBuffer => { - start = 0; - end = self.len_minus_one(); - } - Movement::BeginningOfBuffer => { - start = 0; - } - Movement::EndOfBuffer => { - end = self.len_minus_one(); - } - Movement::Null => {/* nothing */} - } - - end = end.min(self.len()); - - start..end - } - pub fn exec_vi_cmd(&mut self, verb: Option, move_cmd: Option) -> ShResult<()> { - match (verb, move_cmd) { - (Some(v), None) => self.exec_vi_verb(v), - (None, Some(m)) => self.exec_vi_movement(m), - (Some(v), Some(m)) => self.exec_vi_moveverb(v,m), - (None, None) => unreachable!() - } - } - pub fn exec_vi_verb(&mut self, verb: Verb) -> ShResult<()> { - assert!(!verb.needs_movement()); - match verb { - Verb::DeleteOne(anchor) => { - match anchor { - Anchor::After => { - self.delete(); - } - Anchor::Before => { - self.backspace(); - } - } - } - Verb::Breakline(anchor) => { - match anchor { - Anchor::Before => { - let last_newline = self.backward_until(self.cursor(), |pos| self.get_char(pos) == '\n'); - self.cursor = last_newline; - self.insert_at_cursor('\n'); - self.insert_at_cursor('\r'); - } - Anchor::After => { - let next_newline = self.forward_until(self.cursor(), |pos| self.get_char(pos) == '\n'); - self.cursor = next_newline; - self.insert_at_cursor('\n'); - self.insert_at_cursor('\r'); - } - } - } - Verb::InsertChar(ch) => { - if self.inserting { - self.last_insert.push(ch); - } - self.insert_at_cursor(ch) - } - Verb::Insert(text) => { - for ch in text.chars() { - if self.inserting { - self.last_insert.push(ch); - } - self.insert_at_cursor(ch); - } - } - Verb::InsertMode => todo!(), - Verb::JoinLines => todo!(), - Verb::ToggleCase => todo!(), - Verb::OverwriteMode => todo!(), - Verb::Substitute => todo!(), - Verb::Put(_) => todo!(), - Verb::Undo => todo!(), - Verb::RepeatLast => todo!(), - Verb::Dedent => { - let mut start_pos = self.backward_until(self.cursor(), |pos| self.get_char(pos) == '\n'); - if self.get_char(start_pos) == '\n' { - start_pos += 1; - } - if self.get_char(start_pos) == '\t' { - self.delete_pos(start_pos); - } - } - Verb::Indent => { - let mut line_start = self.backward_until(self.cursor(), |pos| self.get_char(pos) == '\n'); - if self.get_char(line_start) == '\n' { - line_start += 1; - } - self.insert_at_pos(line_start, '\t'); - } - Verb::ReplaceChar(_) => todo!(), - _ => unreachable!() - } - Ok(()) - } - pub fn exec_vi_movement(&mut self, move_cmd: MoveCmd) -> ShResult<()> { - let MoveCmd { move_count, movement } = move_cmd; - for _ in 0..move_count { - let range = self.calc_range(&movement); - if range.start != self.cursor() { - self.cursor = range.start.max(0); - } else { - self.cursor = range.end.min(self.len()); - } - } - Ok(()) - } - pub fn exec_vi_moveverb(&mut self, verb: Verb, move_cmd: MoveCmd) -> ShResult<()> { - let MoveCmd { move_count, movement } = move_cmd; - match verb { - Verb::Delete => { - (0..move_count).for_each(|_| { - let range = self.calc_range(&movement); - let range = range.start..(range.end + 1).min(self.len()); - self.buffer.drain(range); - self.repos_cursor(); - }); - } - Verb::Change => { - (0..move_count).for_each(|_| { - let range = self.calc_range(&movement); - let range = range.start..(range.end + 1).min(self.len()); - self.buffer.drain(range); - self.repos_cursor(); - }); - } - Verb::Repeat(rep) => { - - } - Verb::DeleteOne(anchor) => todo!(), - Verb::Breakline(anchor) => todo!(), - Verb::Yank => todo!(), - Verb::ReplaceChar(_) => todo!(), - Verb::Substitute => todo!(), - Verb::ToggleCase => todo!(), - Verb::Undo => todo!(), - Verb::RepeatLast => todo!(), - Verb::Put(anchor) => todo!(), - Verb::OverwriteMode => todo!(), - Verb::InsertMode => todo!(), - Verb::JoinLines => todo!(), - Verb::InsertChar(_) => todo!(), - Verb::Indent => todo!(), - Verb::Dedent => todo!(), - } - Ok(()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum CharClass { - AlphaNum, - Symbol, -} - -impl CharClass { - pub fn is_opposite(&self, other: char) -> bool { - let other_class = CharClass::from(other); - other_class != *self - } -} - -impl From for CharClass { - fn from(value: char) -> Self { - if value.is_alphanumeric() || value == '_' { - CharClass::AlphaNum - } else { - CharClass::Symbol - } - } -} - - -pub fn strip_ansi_codes_and_escapes(s: &str) -> String { - let mut out = String::with_capacity(s.len()); - let mut chars = s.chars().peekable(); - - while let Some(c) = chars.next() { - if c == '\x1b' && chars.peek() == Some(&'[') { - // Skip over the escape sequence - chars.next(); // consume '[' - while let Some(&ch) = chars.peek() { - if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() { - chars.next(); // consume final letter - break; - } - chars.next(); // consume intermediate characters - } - } else { - match c { - '\n' | - '\r' => { /* Continue */ } - _ => out.push(c) - } - } - } - out -} diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs new file mode 100644 index 0000000..81d6906 --- /dev/null +++ b/src/prompt/readline/linebuf.rs @@ -0,0 +1,798 @@ +use std::{fmt::Display, ops::{Deref, DerefMut, Range}, sync::Arc}; + +use unicode_width::UnicodeWidthStr; + +use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}}; + +use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, RegisterName, TextObj, To, Verb, ViCmd, Word}; + +#[derive(Debug, PartialEq, Eq)] +pub enum CharClass { + Alphanum, + Symbol +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MotionKind { + Forward(usize), + To(usize), + Backward(usize), + Range(usize,usize), + Null +} + +#[derive(Clone,Default,Debug)] +pub struct TermCharBuf(pub Vec); + +impl Deref for TermCharBuf { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for TermCharBuf { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Display for TermCharBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for ch in &self.0 { + match ch { + TermChar::Grapheme(str) => write!(f, "{str}")?, + TermChar::Newline => write!(f, "\r\n")?, + } + } + Ok(()) + } +} + +impl FromIterator for TermCharBuf { + fn from_iter>(iter: T) -> Self { + let mut buf = vec![]; + for item in iter { + buf.push(item) + } + Self(buf) + } +} + +impl From for String { + fn from(value: TermCharBuf) -> Self { + let mut string = String::new(); + for char in value.0 { + match char { + TermChar::Grapheme(str) => string.push_str(&str), + TermChar::Newline => { + string.push('\r'); + string.push('\n'); + } + } + } + string + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum TermChar { + Grapheme(Arc), + // Treated as '\n' in the code, printed as '\r\n' to the terminal + Newline +} + +impl TermChar { + pub fn is_whitespace(&self) -> bool { + match self { + TermChar::Newline => true, + TermChar::Grapheme(ch) => { + ch.chars().next().is_some_and(|c| c.is_whitespace()) + } + } + } + pub fn matches(&self, other: &str) -> bool { + match self { + TermChar::Grapheme(ch) => { + ch.as_ref() == other + } + TermChar::Newline => other == "\n" + } + } +} + +impl From> for TermChar { + fn from(value: Arc) -> Self { + Self::Grapheme(value) + } +} + +impl From for TermChar { + fn from(value: char) -> Self { + match value { + '\n' => Self::Newline, + ch => Self::Grapheme(Arc::from(ch.to_string())) + } + } +} + +impl Display for TermChar { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TermChar::Grapheme(str) => { + write!(f,"{str}") + } + TermChar::Newline => { + write!(f,"\r\n") + } + } + } +} + +impl From<&TermChar> for CharClass { + fn from(value: &TermChar) -> Self { + match value { + TermChar::Newline => Self::Symbol, + TermChar::Grapheme(ch) => { + if ch.chars().next().is_some_and(|c| c.is_alphanumeric()) { + Self::Alphanum + } else { + Self::Symbol + } + } + } + } +} + +impl From for CharClass { + fn from(value: char) -> Self { + if value.is_alphanumeric() { + Self::Alphanum + } else { + Self::Symbol + } + } +} + +fn is_other_class_or_ws(a: &TermChar, b: &TermChar) -> bool { + if a.is_whitespace() || b.is_whitespace() { + return true; + } + + CharClass::from(a) != CharClass::from(b) +} + +#[derive(Default,Debug)] +pub struct LineBuf { + buffer: TermCharBuf, + cursor: usize, +} + +impl LineBuf { + pub fn new() -> Self { + Self::default() + } + pub fn with_initial(mut self, initial: &str) -> Self { + let chars = initial.chars(); + for char in chars { + self.buffer.push(char.into()) + } + self + } + pub fn buffer(&self) -> &TermCharBuf { + &self.buffer + } + pub fn cursor(&self) -> usize { + self.cursor + } + pub fn cursor_char(&self) -> Option<&TermChar> { + let tc = self.buffer.get(self.cursor())?; + Some(tc) + } + pub fn get_char(&self, pos: usize) -> Option<&TermChar> { + let tc = self.buffer.get(pos)?; + Some(tc) + } + pub fn insert_at_cursor(&mut self, tc: TermChar) { + let cursor = self.cursor(); + self.buffer.insert(cursor,tc) + } + pub fn count_lines(&self) -> usize { + self.buffer.iter().filter(|&c| c == &TermChar::Newline).count() + } + pub fn cursor_back(&mut self, count: usize) { + self.cursor = self.cursor.saturating_sub(count) + } + pub fn cursor_fwd(&mut self, count: usize) { + self.cursor = self.num_or_len(self.cursor + count) + } + pub fn cursor_to(&mut self, pos: usize) { + self.cursor = self.num_or_len(pos) + } + pub fn prepare_line(&self) -> String { + self.buffer.to_string() + } + pub fn clamp_cursor(&mut self) { + if self.cursor_char().is_none() && !self.buffer.is_empty() { + self.cursor = self.cursor.saturating_sub(1) + } + } + pub fn cursor_display_coords(&self) -> (usize, usize) { + let mut x = 0; + let mut y = 0; + for i in 0..self.cursor() { + let ch = self.get_char(i).unwrap(); + match ch { + TermChar::Grapheme(str) => x += str.width().max(1), + TermChar::Newline => { + y += 1; + x = 0; + } + } + } + + (x, y) + } + pub fn split_lines(&self) -> Vec { + let line = self.prepare_line(); + let mut lines = vec![]; + let mut cur_line = String::new(); + for ch in line.chars() { + match ch { + '\n' => lines.push(std::mem::take(&mut cur_line)), + _ => cur_line.push(ch) + } + } + lines.push(cur_line); + lines + } + pub fn display_lines(&self) -> Vec { + let line_bullet = "∙ ".styled(Style::Dim); + self.split_lines() + .into_iter() + .enumerate() + .map(|(i, line)| { + if i == 0 { + line.to_string() + } else { + format!("{line_bullet}{line}") + } + }) + .collect() + } + pub fn on_word_bound(&self, word: Word, pos: usize, dir: Direction) -> bool { + let check_pos = match dir { + Direction::Forward => self.num_or_len(pos + 1), + Direction::Backward => pos.saturating_sub(1) + }; + let Some(curr_char) = self.cursor_char() else { + return false + }; + self.get_char(check_pos).is_some_and(|c| { + match word { + Word::Big => c.is_whitespace(), + Word::Normal => is_other_class_or_ws(curr_char, c) + } + }) + } + fn backward_until bool>(&self, mut start: usize, cond: F, inclusive: bool) -> usize { + while start > 0 && !cond(&self.buffer[start]) { + start -= 1; + } + if !inclusive { + if start > 0 { + start.saturating_add(1) + } else { + start + } + } else { + start + } + } + fn forward_until bool>(&self, mut start: usize, cond: F, inclusive: bool) -> usize { + while start < self.buffer.len() && !cond(&self.buffer[start]) { + start += 1; + } + if !inclusive { + if start < self.buffer.len() { + start.saturating_sub(1) + } else { + start + } + } else { + start + } + } + pub fn find_word_pos(&self, word: Word, dest: To, dir: Direction) -> usize { + let mut pos = self.cursor(); + match dir { + Direction::Forward => { + match word { + Word::Big => { + match dest { + To::Start => { + if self.on_word_bound(word, pos, dir) { + // Push the cursor off of the word + pos = self.num_or_len(pos + 1); + } + // Pass the current word if any + if self.get_char(pos).is_some_and(|c| !c.is_whitespace()) { + pos = self.forward_until(pos, |c| c.is_whitespace(), true); + } + // Land on the start of the next word + pos = self.forward_until(pos, |c| !c.is_whitespace(), true) + } + To::End => { + if self.on_word_bound(word, pos, dir) { + // Push the cursor off of the word + pos = self.num_or_len(pos + 1); + } + if self.get_char(pos).is_some_and(|c| !c.is_whitespace()) { + // We are in a word + // Go to the end of the current word + pos = self.forward_until(pos, |c| c.is_whitespace(), false) + } else { + // We are outside of a word + // Find the next word, then go to the end of it + pos = self.forward_until(pos, |c| !c.is_whitespace(), true); + pos = self.forward_until(pos, |c| c.is_whitespace(), false) + } + } + } + } + Word::Normal => { + match dest { + To::Start => { + if self.on_word_bound(word, pos, dir) { + // Push the cursor off of the word + pos = self.num_or_len(pos + 1); + } + if self.get_char(pos).is_some_and(|c| !c.is_whitespace()) { + // We are inside of a word + // Find the next instance of whitespace or a different char class + let this_char = self.get_char(pos).unwrap(); + pos = self.forward_until(pos, |c| is_other_class_or_ws(this_char, c), true); + + // If we found whitespace, continue until we find non-whitespace + if self.get_char(pos).is_some_and(|c| c.is_whitespace()) { + pos = self.forward_until(pos, |c| !c.is_whitespace(), true) + } + } else { + // We are in whitespace, proceed to the next word + pos = self.forward_until(pos, |c| !c.is_whitespace(), true) + } + } + To::End => { + if self.on_word_bound(word, pos, dir) { + // Push the cursor off of the word + pos = self.num_or_len(pos + 1); + } + if self.get_char(pos).is_some_and(|c| !c.is_whitespace()) { + // Proceed up until the next differing char class + let this_char = self.get_char(pos).unwrap(); + pos = self.forward_until(pos, |c| is_other_class_or_ws(this_char, c), false); + } else { + // Find the next non-whitespace character + pos = self.forward_until(pos, |c| !c.is_whitespace(), true); + // Then proceed until a differing char class is found + let this_char = self.get_char(pos).unwrap(); + pos = self.forward_until(pos, |c|is_other_class_or_ws(this_char, c), false); + } + } + } + } + } + } + Direction::Backward => { + match word { + Word::Big => { + match dest { + To::Start => { + if self.on_word_bound(word, pos, dir) { + // Push the cursor off + pos = pos.saturating_sub(1); + } + if self.get_char(pos).is_some_and(|c| !c.is_whitespace()) { + // We are in a word, go to the start of it + pos = self.backward_until(pos, |c| c.is_whitespace(), false); + } else { + // We are not in a word, find one and go to the start of it + pos = self.backward_until(pos, |c| !c.is_whitespace(), true); + pos = self.backward_until(pos, |c| c.is_whitespace(), false); + } + } + To::End => unreachable!() + } + } + Word::Normal => { + match dest { + To::Start => { + if self.on_word_bound(word, pos, dir) { + pos = pos.saturating_sub(1); + } + if self.get_char(pos).is_some_and(|c| !c.is_whitespace()) { + let this_char = self.get_char(pos).unwrap(); + pos = self.backward_until(pos, |c| is_other_class_or_ws(this_char, c), false) + } else { + pos = self.backward_until(pos, |c| !c.is_whitespace(), true); + let this_char = self.get_char(pos).unwrap(); + pos = self.backward_until(pos, |c| is_other_class_or_ws(this_char, c), false); + } + } + To::End => unreachable!() + } + } + } + } + } + pos + } + pub fn eval_text_obj(&self, obj: TextObj, bound: Bound) -> Range { + let mut start = self.cursor(); + let mut end = self.cursor(); + + match obj { + TextObj::Word(word) => { + start = match self.on_word_bound(word, self.cursor(), Direction::Backward) { + true => self.cursor(), + false => self.find_word_pos(word, To::Start, Direction::Backward), + }; + end = match self.on_word_bound(word, self.cursor(), Direction::Forward) { + true => self.cursor(), + false => self.find_word_pos(word, To::End, Direction::Forward), + }; + end = self.num_or_len(end + 1); + if bound == Bound::Around { + end = self.forward_until(end, |c| c.is_whitespace(), true); + end = self.forward_until(end, |c| !c.is_whitespace(), true); + } + return start..end + } + TextObj::Line => { + let cursor = self.cursor(); + start = self.backward_until(cursor, |c| c == &TermChar::Newline, false); + end = self.forward_until(cursor, |c| c == &TermChar::Newline, true); + } + TextObj::Sentence => todo!(), + TextObj::Paragraph => todo!(), + TextObj::DoubleQuote => { + let cursor = self.cursor(); + let ln_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false); + let mut line_chars = self.buffer[ln_start..cursor].iter(); + let mut in_quote = false; + while let Some(ch) = line_chars.next() { + let TermChar::Grapheme(ch) = ch else { unreachable!() }; + match ch.as_ref() { + "\\" => { + line_chars.next(); + } + "\"" => in_quote = !in_quote, + _ => { /* continue */ } + } + } + let mut start_pos = cursor; + let end_pos; + if !in_quote { + start_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches("\""), true); + if !self.get_char(start_pos).is_some_and(|c| c.matches("\"")) { + return cursor..cursor + } + end_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches("\""), true); + if !self.get_char(end_pos).is_some_and(|c| c.matches("\"")) { + return cursor..cursor + } + start = start_pos; + end = end_pos; + } else { + start_pos = self.backward_until(start_pos, |c| c.matches("\n") || c.matches("\""), true); + if !self.get_char(start_pos).is_some_and(|c| c.matches("\"")) { + return cursor..cursor + } + end_pos = self.forward_until(self.num_or_len(start_pos + 1), |c| c.matches("\n") || c.matches("\""), true); + if !self.get_char(end_pos).is_some_and(|c| c.matches("\"")) { + return cursor..cursor + } + start = start_pos; + end = self.num_or_len(end_pos + 1); + + if bound == Bound::Around && self.get_char(end).is_some_and(|c| c.is_whitespace()) { + end += 1; + end = self.forward_until(end, |c| !c.is_whitespace(), true); + } + } + } + TextObj::SingleQuote => todo!(), + TextObj::BacktickQuote => todo!(), + TextObj::Paren => todo!(), + TextObj::Bracket => todo!(), + TextObj::Brace => todo!(), + TextObj::Angle => todo!(), + TextObj::Tag => todo!(), + TextObj::Custom(_) => todo!(), + } + + if bound == Bound::Inside { + start = self.num_or_len(start + 1); + end = end.saturating_sub(1); + } + start..end + } + /// Clamp a number to the length of the buffer + pub fn num_or_len(&self, num: usize) -> usize { + num.min(self.buffer.len().saturating_sub(1)) + } + pub fn eval_motion(&self, motion: Motion) -> MotionKind { + match motion { + Motion::WholeLine => { + let cursor = self.cursor(); + let start = self.backward_until(cursor, |c| c == &TermChar::Newline, false); + let end = self.forward_until(cursor, |c| c == &TermChar::Newline, true); + MotionKind::Range(start,end) + } + Motion::TextObj(text_obj, bound) => { + let range = self.eval_text_obj(text_obj, bound); + let range = mk_range(range.start, range.end); + MotionKind::Range(range.start,range.end) + } + Motion::BeginningOfFirstWord => { + let cursor = self.cursor(); + let line_start = self.backward_until(cursor, |c| c == &TermChar::Newline, true); + let first_print = self.forward_until(line_start, |c| !c.is_whitespace(), true); + MotionKind::To(first_print) + } + Motion::BeginningOfLine => { + let cursor = self.cursor(); + let line_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false); + MotionKind::To(line_start) + } + Motion::EndOfLine => { + let cursor = self.cursor(); + let line_end = self.forward_until(cursor, |c| c == &TermChar::Newline, false); + MotionKind::To(line_end) + } + Motion::BackwardWord(word) => MotionKind::To(self.find_word_pos(word, To::Start, Direction::Backward)), + Motion::ForwardWord(dest, word) => MotionKind::To(self.find_word_pos(word, dest, Direction::Forward)), + Motion::CharSearch(direction, dest, ch) => { + let mut cursor = self.cursor(); + let inclusive = matches!(dest, Dest::On); + + let stop_condition = |c: &TermChar| { + c == &TermChar::Newline || + c == &ch + }; + if self.cursor_char().is_some_and(|c| c == &ch) { + // We are already on the character we are looking for + // Let's nudge the cursor + match direction { + Direction::Backward => cursor = self.cursor().saturating_sub(1), + Direction::Forward => cursor = self.num_or_len(self.cursor() + 1), + } + } + + let stop_pos = match direction { + Direction::Forward => self.forward_until(cursor, stop_condition, inclusive), + Direction::Backward => self.backward_until(cursor, stop_condition, inclusive), + }; + + let found_char = match dest { + Dest::On => self.get_char(stop_pos).is_some_and(|c| c == &ch), + _ => { + match direction { + Direction::Forward => self.get_char(stop_pos + 1).is_some_and(|c| c == &ch), + Direction::Backward => self.get_char(stop_pos.saturating_sub(1)).is_some_and(|c| c == &ch), + } + } + }; + + if found_char { + MotionKind::To(stop_pos) + } else { + MotionKind::Null + } + } + Motion::BackwardChar => MotionKind::Backward(1), + Motion::ForwardChar => MotionKind::Forward(1), + Motion::LineUp => todo!(), + Motion::LineDown => todo!(), + Motion::WholeBuffer => MotionKind::Range(0,self.buffer.len().saturating_sub(1)), + Motion::BeginningOfBuffer => MotionKind::To(0), + Motion::EndOfBuffer => MotionKind::To(self.buffer.len().saturating_sub(1)), + Motion::Null => MotionKind::Null, + Motion::Builder(_) => unreachable!(), + } + } + pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> { + match verb { + Verb::Change | + Verb::Delete => { + let deleted; + match motion { + MotionKind::Forward(n) => { + let fwd = self.num_or_len(self.cursor() + n); + let cursor = self.cursor(); + deleted = self.buffer.drain(cursor..=fwd).collect::(); + } + MotionKind::To(pos) => { + let range = mk_range(self.cursor(), pos); + deleted = self.buffer.drain(range.clone()).collect::(); + self.apply_motion(MotionKind::To(range.start)); + } + MotionKind::Backward(n) => { + let back = self.cursor.saturating_sub(n); + let cursor = self.cursor(); + deleted = self.buffer.drain(back..cursor).collect::(); + self.apply_motion(MotionKind::To(back)); + } + MotionKind::Range(s, e) => { + deleted = self.buffer.drain(s..e).collect::(); + self.apply_motion(MotionKind::To(s)); + } + MotionKind::Null => return Ok(()) + } + register.write_to_register(deleted); + } + Verb::DeleteChar(anchor) => { + match anchor { + Anchor::After => { + let pos = self.num_or_len(self.cursor() + 1); + self.buffer.remove(pos); + } + Anchor::Before => { + let pos = self.cursor.saturating_sub(1); + self.buffer.remove(pos); + self.cursor = self.cursor.saturating_sub(1); + } + } + } + Verb::Yank => { + let yanked; + match motion { + MotionKind::Forward(n) => { + let fwd = self.num_or_len(self.cursor() + n); + let cursor = self.cursor(); + yanked = self.buffer[cursor..=fwd] + .iter() + .cloned() + .collect::(); + } + MotionKind::To(pos) => { + let range = mk_range(self.cursor(), pos); + yanked = self.buffer[range.clone()] + .iter() + .cloned() + .collect::(); + self.apply_motion(MotionKind::To(range.start)); + } + MotionKind::Backward(n) => { + let back = self.cursor.saturating_sub(n); + let cursor = self.cursor(); + yanked = self.buffer[back..cursor] + .iter() + .cloned() + .collect::(); + self.apply_motion(MotionKind::To(back)); + } + MotionKind::Range(s, e) => { + yanked = self.buffer[s..e] + .iter() + .cloned() + .collect::(); + self.apply_motion(MotionKind::To(s)); + } + MotionKind::Null => return Ok(()) + } + register.write_to_register(yanked); + } + Verb::ReplaceChar(_) => todo!(), + Verb::Substitute => todo!(), + Verb::ToggleCase => todo!(), + Verb::Complete => todo!(), + Verb::CompleteBackward => todo!(), + Verb::Undo => todo!(), + Verb::RepeatLast => todo!(), + Verb::Put(anchor) => { + if let Some(charbuf) = register.read_from_register() { + let chars = charbuf.0.into_iter(); + if anchor == Anchor::Before { + self.cursor_back(1); + } + for char in chars { + self.insert_at_cursor(char); + self.cursor_fwd(1); + } + } + } + Verb::JoinLines => todo!(), + Verb::InsertChar(ch) => { + self.insert_at_cursor(ch); + self.apply_motion(motion); + } + Verb::Insert(_) => todo!(), + Verb::Breakline(anchor) => todo!(), + Verb::Indent => todo!(), + Verb::Dedent => todo!(), + Verb::AcceptLine => todo!(), + Verb::EndOfFile => { + if self.buffer.is_empty() { + sh_quit(0) + } else { + self.buffer.clear(); + self.cursor = 0; + } + } + Verb::InsertMode | + Verb::NormalMode | + Verb::VisualMode | + Verb::OverwriteMode => { + self.apply_motion(motion); + } + } + Ok(()) + } + pub fn apply_motion(&mut self, motion: MotionKind) { + match motion { + MotionKind::Forward(n) => self.cursor_fwd(n), + MotionKind::To(pos) => self.cursor_to(pos), + MotionKind::Backward(n) => self.cursor_back(n), + MotionKind::Range(s, _) => self.cursor_to(s), // TODO: not sure if this is correct in every case + MotionKind::Null => { /* Pass */ } + } + } + pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { + let ViCmd { register, verb_count, verb, motion_count, motion, .. } = cmd; + + for _ in 0..verb_count.unwrap_or(1) { + for _ in 0..motion_count.unwrap_or(1) { + let motion = motion + .clone() + .map(|m| self.eval_motion(m)) + .unwrap_or(MotionKind::Null); + + if let Some(verb) = verb.clone() { + self.exec_verb(verb, motion, register)?; + } else { + self.apply_motion(motion); + } + } + } + + self.clamp_cursor(); + Ok(()) + } +} + +impl Display for LineBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f,"{}",self.buffer) + } +} + +pub fn strip_ansi_codes_and_escapes(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + let mut chars = s.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '\x1b' && chars.peek() == Some(&'[') { + // Skip over the escape sequence + chars.next(); // consume '[' + while let Some(&ch) = chars.peek() { + if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() { + chars.next(); // consume final letter + break; + } + chars.next(); // consume intermediate characters + } + } else { + match c { + '\n' | + '\r' => { /* Continue */ } + _ => out.push(c) + } + } + } + out +} + +fn mk_range(a: usize, b: usize) -> Range { + std::cmp::min(a, b)..std::cmp::max(a, b) +} diff --git a/src/prompt/readline/linecmd.rs b/src/prompt/readline/linecmd.rs deleted file mode 100644 index c5c72c0..0000000 --- a/src/prompt/readline/linecmd.rs +++ /dev/null @@ -1,420 +0,0 @@ -// Credit to Rustyline for enumerating these editor commands -// https://github.com/kkawakam/rustyline - -use crate::libsh::error::{ShErr, ShErrKind, ShResult}; -use crate::prelude::*; - -pub type RepeatCount = u16; - -#[derive(Default, Debug, Clone, Eq, PartialEq)] -pub struct ViCmdBuilder { - verb_count: Option, - verb: Option, - move_count: Option, - movement: Option, -} - -impl ViCmdBuilder { - pub fn new() -> Self { - Self::default() - } - pub fn with_verb_count(self, verb_count: u16) -> Self { - let Self { verb_count: _, verb, move_count, movement } = self; - Self { verb_count: Some(verb_count), verb, move_count, movement } - } - pub fn with_verb(self, verb: Verb) -> Self { - let Self { verb_count, verb: _, move_count, movement } = self; - Self { verb_count, verb: Some(verb), move_count, movement } - } - pub fn with_move_count(self, move_count: u16) -> Self { - let Self { verb_count, verb, move_count: _, movement } = self; - Self { verb_count, verb, move_count: Some(move_count), movement } - } - pub fn with_movement(self, movement: Movement) -> Self { - let Self { verb_count, verb, move_count, movement: _ } = self; - Self { verb_count, verb, move_count, movement: Some(movement) } - } - pub fn verb_count(&self) -> Option { - self.verb_count - } - pub fn move_count(&self) -> Option { - self.move_count - } - pub fn movement(&self) -> Option<&Movement> { - self.movement.as_ref() - } - pub fn verb(&self) -> Option<&Verb> { - self.verb.as_ref() - } - pub fn append_digit(&mut self, digit: char) { - // Convert char digit to a number (assuming ASCII '0'..'9') - let digit_val = digit.to_digit(10).expect("digit must be 0-9") as u16; - - if self.verb.is_none() { - // Append to verb_count - self.verb_count = Some(match self.verb_count { - Some(count) => count * 10 + digit_val, - None => digit_val, - }); - } else { - // Append to move_count - self.move_count = Some(match self.move_count { - Some(count) => count * 10 + digit_val, - None => digit_val, - }); - } - } - pub fn is_unfinished(&self) -> bool { - (self.verb.is_none() && self.movement.is_none()) || - (self.verb.is_none() && self.movement.as_ref().is_some_and(|m| m.needs_verb())) || - (self.movement.is_none() && self.verb.as_ref().is_some_and(|v| v.needs_movement())) - } - pub fn build(self) -> ShResult { - if self.is_unfinished() { - flog!(ERROR, "Unfinished Builder: {:?}", self); - return Err( - ShErr::simple(ShErrKind::ReadlineErr, "called ViCmdBuilder::build() with an unfinished builder") - ) - } - let Self { verb_count, verb, move_count, movement } = self; - let verb_count = verb_count.unwrap_or(1); - let move_count = move_count.unwrap_or(if verb.is_none() { verb_count } else { 1 }); - let verb = verb.map(|v| VerbCmd { verb_count, verb: v }); - let movement = movement.map(|m| MoveCmd { move_count, movement: m }); - Ok(match (verb, movement) { - (Some(v), Some(m)) => ViCmd::MoveVerb(v, m), - (Some(v), None) => ViCmd::Verb(v), - (None, Some(m)) => ViCmd::Move(m), - (None, None) => unreachable!(), - }) - } -} - - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum ViCmd { - MoveVerb(VerbCmd, MoveCmd), - Verb(VerbCmd), - Move(MoveCmd) -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct VerbCmd { - pub verb_count: u16, - pub verb: Verb -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct MoveCmd { - pub move_count: u16, - pub movement: Movement -} - -#[derive(Default, Debug, Clone, Eq, PartialEq)] -pub struct Repeat { - pub movement: Option, - pub verb: Option>, - pub ins_text: Option -} - -impl Repeat { - pub fn from_cmd(cmd: ViCmd) -> Self { - match cmd { - ViCmd::MoveVerb(verb_cmd, move_cmd) => { - Self { - movement: Some(move_cmd), - verb: Some(Box::new(verb_cmd)), - ins_text: None, - } - } - ViCmd::Verb(verb_cmd) => { - Self { - movement: None, - verb: Some(Box::new(verb_cmd)), - ins_text: None, - } - } - ViCmd::Move(move_cmd) => { - Self { - movement: Some(move_cmd), - verb: None, - ins_text: None, - } - } - } - } - pub fn set_ins_text(&mut self, ins_text: String) { - self.ins_text = Some(ins_text); - } - fn is_empty(&self) -> bool { - self.movement.is_none() && self.verb.is_none() && self.ins_text.is_none() - } - pub fn to_option(self) -> Option { - if self.is_empty() { - None - } else { - Some(self) - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -#[non_exhaustive] -pub enum Verb { - /// `d`, `D` — delete motion or line - Delete, - /// `x`, `X` — delete one char, forward or back - DeleteOne(Anchor), - /// `c`, `C` — change (delete + insert) - Change, - /// `y`, `Y` — yank (copy) - Yank, - /// `r` — replace a single character - ReplaceChar(char), - /// `s` or `S` — substitute (change + single char or line) - Substitute, - /// `~` — swap case - ToggleCase, - /// `u` — undo - Undo, - /// `.` — repeat last edit - RepeatLast, - /// `p`, `P` — paste - Put(Anchor), - /// `R` — overwrite characters - OverwriteMode, - /// `i`, `a`, `I`, `A`, `o`, `O` — insert/append text - InsertMode, - /// `J` — join lines - JoinLines, - Repeat(Repeat), - InsertChar(char), - Insert(String), - Breakline(Anchor), - Indent, - Dedent -} - -impl Verb { - pub fn needs_movement(&self) -> bool { - match self { - Verb::DeleteOne(_) | - Verb::InsertMode | - Verb::JoinLines | - Verb::ToggleCase | - Verb::OverwriteMode | - Verb::Substitute | - Verb::Put(_) | - Verb::Undo | - Verb::RepeatLast | - Verb::Dedent | - Verb::Indent | - Verb::InsertChar(_) | - Verb::Breakline(_) | - Verb::Repeat(_) | - Verb::Insert(_) | - Verb::ReplaceChar(_) => false, - Verb::Delete | - Verb::Change | - Verb::Yank => true - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum Movement { - /// Whole current line (not really a movement but a range) - WholeLine, - TextObj(TextObj, Bound), - BeginningOfFirstWord, - /// beginning-of-line - BeginningOfLine, - /// end-of-line - EndOfLine, - /// backward-word, vi-prev-word - BackwardWord(Word), // Backward until start of word - /// forward-word, vi-end-word, vi-next-word - ForwardWord(At, Word), // Forward until start/end of word - /// character-search, character-search-backward, vi-char-search - CharSearch(CharSearch), - /// backward-char - BackwardChar, - /// forward-char - ForwardChar, - /// move to the same column on the previous line - LineUp, - /// move to the same column on the next line - LineDown, - /// Whole user input (not really a movement but a range) - WholeBuffer, - /// beginning-of-buffer - BeginningOfBuffer, - /// end-of-buffer - EndOfBuffer, - Null -} - -impl Movement { - pub fn needs_verb(&self) -> bool { - match self { - Self::WholeLine | - Self::BeginningOfLine | - Self::BeginningOfFirstWord | - Self::EndOfLine | - Self::BackwardWord(_) | - Self::ForwardWord(_, _) | - Self::CharSearch(_) | - Self::BackwardChar | - Self::ForwardChar | - Self::LineUp | - Self::LineDown | - Self::WholeBuffer | - Self::BeginningOfBuffer | - Self::EndOfBuffer => false, - Self::Null | - Self::TextObj(_, _) => true - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum TextObj { - /// `iw`, `aw` — inner word, around word - Word, - - /// `is`, `as` — inner sentence, around sentence - Sentence, - - /// `ip`, `ap` — inner paragraph, around paragraph - Paragraph, - - /// `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 (if you support it) - Tag, - - /// Custom user-defined objects maybe? - Custom(char), -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum Bound { - Inside, - Around -} - -#[derive(Debug, Clone, Eq, PartialEq)] -#[non_exhaustive] -pub enum LineCmd { - Abort, - AcceptLine, - BeginningOfHistory, - CapitalizeWord, - ClearScreen, - Complete, - CompleteBackward, - CompleteHint, - DowncaseWord, - EndOfFile, - EndOfHistory, - ForwardSearchHistory, - HistorySearchBackward, - HistorySearchForward, - Insert(String), - Interrupt, - Move(Movement), - NextHistory, - Noop, - Overwrite(char), - PreviousHistory, - QuotedInsert, - Repaint, - ReverseSearchHistory, - Suspend, - TransposeChars, - TransposeWords, - YankPop, - LineUpOrPreviousHistory, - LineDownOrNextHistory, - Newline, - AcceptOrInsertLine { accept_in_the_middle: bool }, - /// 🧵 New: vi-style editing command - ViCmd(ViCmd), - /// unknown/unmapped key - Unknown, - Null, -} -impl LineCmd { - pub fn backspace() -> Self { - let cmd = ViCmdBuilder::new() - .with_verb(Verb::DeleteOne(Anchor::Before)) - .build() - .unwrap(); - Self::ViCmd(cmd) - } - const fn is_repeatable_change(&self) -> bool { - matches!( - *self, - Self::Insert(..) - | Self::ViCmd(..) - ) - } - - const fn is_repeatable(&self) -> bool { - match *self { - Self::Move(_) => true, - _ => self.is_repeatable_change(), - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Copy)] -pub enum At { - Start, - BeforeEnd, - AfterEnd -} - -#[derive(Debug, Clone, Eq, PartialEq, Copy)] -pub enum Anchor { - After, - Before -} - -#[derive(Debug, Clone, Eq, PartialEq, Copy)] -pub enum CharSearch { - FindFwd(Option), - FwdTo(Option), - FindBkwd(Option), - BkwdTo(Option), -} - -#[derive(Debug, Clone, Eq, PartialEq, Copy)] -pub enum Word { - Big, - Normal -} - - -#[derive(Default,Debug,Clone,Copy,PartialEq,Eq,PartialOrd,Ord)] -pub enum InputMode { - Normal, - #[default] - Insert, - Visual, - Replace -} diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 6d130d0..1bb8752 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -1,86 +1,49 @@ -use std::{arch::asm, os::fd::BorrowedFd}; +use std::{collections::HashMap, sync::Mutex}; -use keys::KeyEvent; -use line::{strip_ansi_codes_and_escapes, LineBuf}; -use linecmd::{Anchor, At, CharSearch, InputMode, LineCmd, MoveCmd, Movement, Verb, VerbCmd, ViCmd, ViCmdBuilder, Word}; -use nix::{libc::STDIN_FILENO, sys::termios::{self, Termios}, unistd::read}; +use linebuf::{strip_ansi_codes_and_escapes, LineBuf, TermCharBuf}; +use mode::{CmdReplay, ViInsert, ViMode, ViNormal}; use term::Terminal; use unicode_width::UnicodeWidthStr; +use vicmd::{Verb, ViCmd}; + +use crate::libsh::{error::ShResult, term::{Style, Styled}}; -use crate::{libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit}, prelude::*}; -use linecmd::Repeat; -pub mod term; -pub mod line; pub mod keys; -pub mod linecmd; +pub mod term; +pub mod linebuf; +pub mod vicmd; +pub mod mode; +pub mod register; -/// Add a verb to a specified ViCmdBuilder, then build it -/// -/// Returns the built value as a LineCmd::ViCmd -macro_rules! build_verb { - ($cmd:expr,$verb:expr) => {{ - $cmd.with_verb($verb).build().map(|cmd| LineCmd::ViCmd(cmd)) - }} +pub struct FernVi { + term: Terminal, + line: LineBuf, + prompt: String, + mode: Box, + repeat_action: Option, } -/// Add a movement to a specified ViCmdBuilder, then build it -/// -/// Returns the built value as a LineCmd::ViCmd -macro_rules! build_movement { - ($cmd:expr,$move:expr) => {{ - $cmd.with_movement($move).build().map(|cmd| LineCmd::ViCmd(cmd)) - }} -} +impl FernVi { + pub fn new(prompt: Option) -> Self { + let prompt = prompt.unwrap_or("$ ".styled(Style::Green | Style::Bold)); + let line = LineBuf::new().with_initial("The quick brown fox jumps over the lazy dog");//\nThe quick brown fox jumps over the lazy dog\nThe quick brown fox jumps over the lazy dog\n"); -/// Add both a movement and a verb to a specified ViCmdBuilder, then build it -/// -/// Returns the built value as a LineCmd::ViCmd -macro_rules! build_moveverb { - ($cmd:expr,$verb:expr,$move:expr) => {{ - $cmd.with_movement($move).with_verb($verb).build().map(|cmd| LineCmd::ViCmd(cmd)) - }} -} - -#[derive(Default,Debug)] -pub struct FernReader { - pub term: Terminal, - pub prompt: String, - pub line: LineBuf, - pub edit_mode: InputMode, - pub last_vicmd: Option, -} - -impl FernReader { - pub fn new(prompt: String) -> Self { - let line = LineBuf::new().with_initial("The quick brown fox jumped over the lazy dog."); Self { term: Terminal::new(), - prompt, line, - edit_mode: Default::default(), - last_vicmd: Default::default() + prompt, + mode: Box::new(ViInsert::new()), + repeat_action: None, } } - fn pack_line(&mut self) -> String { - self.line - .buffer - .iter() - .collect::() - } - pub fn readline(&mut self) -> ShResult { - self.display_line(/*refresh: */ false); - loop { - let cmd = self.next_cmd()?; - if cmd == LineCmd::AcceptLine { - return Ok(self.pack_line()) - } - self.execute_cmd(cmd)?; - self.display_line(/* refresh: */ true); - } - } - fn clear_line(&self) { + pub fn clear_line(&self) { let prompt_lines = self.prompt.lines().count(); - let buf_lines = self.line.count_lines().saturating_sub(1); // One of the buffer's lines will overlap with the prompt. probably. + let buf_lines = if self.prompt.ends_with('\n') { + self.line.count_lines() + } else { + // The prompt does not end with a newline, so one of the buffer's lines overlaps with it + self.line.count_lines().saturating_sub(1) + }; let total = prompt_lines + buf_lines; self.term.write_bytes(b"\r\n"); for _ in 0..total { @@ -88,9 +51,9 @@ impl FernReader { } self.term.write_bytes(b"\r\x1b[2K"); } - fn display_line(&mut self, refresh: bool) { + pub fn print_buf(&self, refresh: bool) { if refresh { - self.clear_line(); + self.clear_line() } let mut prompt_lines = self.prompt.lines().peekable(); let mut last_line_len = 0; @@ -114,315 +77,54 @@ impl FernReader { } } - if num_lines == 1 { - let cursor_offset = self.line.cursor() + last_line_len; - self.term.write(&format!("\r\x1b[{}C", cursor_offset)); - } else { - let (x, y) = self.line.cursor_display_coords(); - // Y-axis movements are 1-indexed and must move up from the bottom - // Therefore, add 1 to Y and subtract that number from the number of lines - // to find the number of times we have to push the cursor upward - let y = num_lines.saturating_sub(y+1); - if y > 0 { - self.term.write(&format!("\r\x1b[{}A", y)) - } - self.term.write(&format!("\r\x1b[{}C", x+2)); // Factor in the line bullet thing - } - match self.edit_mode { - InputMode::Replace | - InputMode::Insert => { - self.term.write("\x1b[6 q") - } - InputMode::Normal | - InputMode::Visual => { - self.term.write("\x1b[2 q") - } - } - } - pub fn set_normal_mode(&mut self) { - self.edit_mode = InputMode::Normal; - self.line.finish_insert(); - let ins_text = self.line.take_ins_text(); - self.last_vicmd.as_mut().map(|cmd| cmd.set_ins_text(ins_text)); - } - pub fn set_insert_mode(&mut self) { - self.edit_mode = InputMode::Insert; - self.line.begin_insert(); - } - pub fn next_cmd(&mut self) -> ShResult { - let vi_cmd = ViCmdBuilder::new(); - match self.edit_mode { - InputMode::Normal => self.get_normal_cmd(vi_cmd), - InputMode::Insert => self.get_insert_cmd(vi_cmd), - InputMode::Visual => todo!(), - InputMode::Replace => todo!(), - } - } - pub fn get_insert_cmd(&mut self, pending_cmd: ViCmdBuilder) -> ShResult { - use keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; - let key = self.term.read_key(); - let cmd = match key { - E(K::Char(ch), M::NONE) => build_verb!(pending_cmd, Verb::InsertChar(ch))?, + let (x, y) = self.line.cursor_display_coords(); + let y = num_lines.saturating_sub(y + 1); - E(K::Char('H'), M::CTRL) | - E(K::Backspace, M::NONE) => LineCmd::backspace(), - - E(K::BackTab, M::NONE) => LineCmd::CompleteBackward, - - E(K::Char('I'), M::CTRL) | - E(K::Tab, M::NONE) => LineCmd::Complete, - - E(K::Esc, M::NONE) => { - build_movement!(pending_cmd, Movement::BackwardChar)? - } - _ => { - flog!(INFO, "unhandled key in get_insert_cmd, trying common_cmd..."); - return self.common_cmd(key, pending_cmd) - } - }; - Ok(cmd) - } - - pub fn get_normal_cmd(&mut self, mut pending_cmd: ViCmdBuilder) -> ShResult { - use keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; - let key = self.term.read_key(); - - if let E(K::Char(ch), M::NONE) = key { - if pending_cmd.movement().is_some_and(|m| matches!(m, Movement::CharSearch(_))) { - let Movement::CharSearch(charsearch) = pending_cmd.movement().unwrap() else {unreachable!()}; - match charsearch { - CharSearch::FindFwd(_) => { - let finalized = CharSearch::FindFwd(Some(ch)); - return build_movement!(pending_cmd, Movement::CharSearch(finalized)) - } - CharSearch::FwdTo(_) => { - let finalized = CharSearch::FwdTo(Some(ch)); - return build_movement!(pending_cmd, Movement::CharSearch(finalized)) - } - CharSearch::FindBkwd(_) => { - let finalized = CharSearch::FindBkwd(Some(ch)); - return build_movement!(pending_cmd, Movement::CharSearch(finalized)) - } - CharSearch::BkwdTo(_) => { - let finalized = CharSearch::BkwdTo(Some(ch)); - return build_movement!(pending_cmd, Movement::CharSearch(finalized)) - } - } - } + if y > 0 { + self.term.write(&format!("\r\x1b[{}A", y)); } - if let E(K::Char(digit @ '0'..='9'), M::NONE) = key { - pending_cmd.append_digit(digit); - return self.get_normal_cmd(pending_cmd); - } - let cmd = match key { - E(K::Char('h'), M::NONE) => { - let cmd = pending_cmd - .with_movement(Movement::BackwardChar) - .build()?; - LineCmd::ViCmd(cmd) - } - E(K::Char('.'), M::NONE) => { - match &self.last_vicmd { - None => LineCmd::Null, - Some(cmd) => { - build_verb!(pending_cmd, Verb::Repeat(cmd.clone()))? - } - } - } - E(K::Char('j'), M::NONE) => LineCmd::LineDownOrNextHistory, - E(K::Char('k'), M::NONE) => LineCmd::LineUpOrPreviousHistory, - E(K::Char('D'), M::NONE) => build_moveverb!(pending_cmd,Verb::Delete,Movement::EndOfLine)?, - E(K::Char('C'), M::NONE) => build_moveverb!(pending_cmd,Verb::Change,Movement::EndOfLine)?, - E(K::Char('Y'), M::NONE) => build_moveverb!(pending_cmd,Verb::Yank,Movement::EndOfLine)?, - E(K::Char('l'), M::NONE) => build_movement!(pending_cmd,Movement::ForwardChar)?, - E(K::Char('w'), M::NONE) => build_movement!(pending_cmd,Movement::ForwardWord(At::Start, Word::Normal))?, - E(K::Char('W'), M::NONE) => build_movement!(pending_cmd,Movement::ForwardWord(At::Start, Word::Big))?, - E(K::Char('b'), M::NONE) => build_movement!(pending_cmd,Movement::BackwardWord(Word::Normal))?, - E(K::Char('B'), M::NONE) => build_movement!(pending_cmd,Movement::BackwardWord(Word::Big))?, - E(K::Char('e'), M::NONE) => build_movement!(pending_cmd,Movement::ForwardWord(At::BeforeEnd, Word::Normal))?, - E(K::Char('E'), M::NONE) => build_movement!(pending_cmd,Movement::ForwardWord(At::BeforeEnd, Word::Big))?, - E(K::Char('^'), M::NONE) => build_movement!(pending_cmd,Movement::BeginningOfFirstWord)?, - E(K::Char('0'), M::NONE) => build_movement!(pending_cmd,Movement::BeginningOfLine)?, - E(K::Char('$'), M::NONE) => build_movement!(pending_cmd,Movement::EndOfLine)?, - E(K::Char('x'), M::NONE) => build_verb!(pending_cmd,Verb::DeleteOne(Anchor::After))?, - E(K::Char('o'), M::NONE) => { - self.set_insert_mode(); - build_verb!(pending_cmd,Verb::Breakline(Anchor::After))? - } - E(K::Char('O'), M::NONE) => { - self.set_insert_mode(); - build_verb!(pending_cmd,Verb::Breakline(Anchor::Before))? - } - E(K::Char('i'), M::NONE) => { - self.set_insert_mode(); - LineCmd::Null - } - E(K::Char('I'), M::NONE) => { - self.set_insert_mode(); - build_movement!(pending_cmd,Movement::BeginningOfFirstWord)? - } - E(K::Char('a'), M::NONE) => { - self.set_insert_mode(); - build_movement!(pending_cmd,Movement::ForwardChar)? - } - E(K::Char('A'), M::NONE) => { - self.set_insert_mode(); - build_movement!(pending_cmd,Movement::EndOfLine)? - } - E(K::Char('c'), M::NONE) => { - if pending_cmd.verb() == Some(&Verb::Change) { - build_moveverb!(pending_cmd,Verb::Change,Movement::WholeLine)? - } else { - pending_cmd = pending_cmd.with_verb(Verb::Change); - self.get_normal_cmd(pending_cmd)? - } - } - E(K::Char('>'), M::NONE) => { - if pending_cmd.verb() == Some(&Verb::Indent) { - build_verb!(pending_cmd,Verb::Indent)? - } else { - pending_cmd = pending_cmd.with_verb(Verb::Indent); - self.get_normal_cmd(pending_cmd)? - } - } - E(K::Char('<'), M::NONE) => { - if pending_cmd.verb() == Some(&Verb::Dedent) { - build_verb!(pending_cmd,Verb::Dedent)? - } else { - pending_cmd = pending_cmd.with_verb(Verb::Dedent); - self.get_normal_cmd(pending_cmd)? - } - } - E(K::Char('d'), M::NONE) => { - if pending_cmd.verb() == Some(&Verb::Delete) { - LineCmd::ViCmd(pending_cmd.with_movement(Movement::WholeLine).build()?) - } else { - pending_cmd = pending_cmd.with_verb(Verb::Delete); - self.get_normal_cmd(pending_cmd)? - } - } - E(K::Char('f'), M::NONE) => { - pending_cmd = pending_cmd.with_movement(Movement::CharSearch(CharSearch::FindFwd(None))); - self.get_normal_cmd(pending_cmd)? - } - E(K::Char('F'), M::NONE) => { - pending_cmd = pending_cmd.with_movement(Movement::CharSearch(CharSearch::FindBkwd(None))); - self.get_normal_cmd(pending_cmd)? - } - E(K::Char('t'), M::NONE) => { - pending_cmd = pending_cmd.with_movement(Movement::CharSearch(CharSearch::FwdTo(None))); - self.get_normal_cmd(pending_cmd)? - } - E(K::Char('T'), M::NONE) => { - pending_cmd = pending_cmd.with_movement(Movement::CharSearch(CharSearch::BkwdTo(None))); - self.get_normal_cmd(pending_cmd)? - } - _ => { - flog!(INFO, "unhandled key in get_normal_cmd, trying common_cmd..."); - return self.common_cmd(key, pending_cmd) - } - }; - Ok(cmd) - } + // Add prompt offset to X only if cursor is on the last line (y == 0) + let cursor_x = if y == 0 { x + last_line_len } else { x }; - pub fn common_cmd(&mut self, key: KeyEvent, pending_cmd: ViCmdBuilder) -> ShResult { - use keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; - match key { - E(K::Home, M::NONE) => build_movement!(pending_cmd,Movement::BeginningOfLine), - E(K::End, M::NONE) => build_movement!(pending_cmd,Movement::EndOfLine), - E(K::Left, M::NONE) => build_movement!(pending_cmd,Movement::BackwardChar), - E(K::Right, M::NONE) => build_movement!(pending_cmd,Movement::ForwardChar), - E(K::Delete, M::NONE) => build_moveverb!(pending_cmd,Verb::Delete,Movement::ForwardChar), - E(K::Up, M::NONE) => Ok(LineCmd::LineUpOrPreviousHistory), - E(K::Down, M::NONE) => Ok(LineCmd::LineDownOrNextHistory), - E(K::Enter, M::NONE) => Ok(LineCmd::AcceptLine), - E(K::Char('D'), M::CTRL) => Ok(LineCmd::EndOfFile), - E(K::Backspace, M::NONE) | - E(K::Char('h'), M::CTRL) => { - Ok(LineCmd::backspace()) + self.term.write(&format!("\r\x1b[{}C", cursor_x)); + self.term.write(&self.mode.cursor_style()); + } + pub fn readline(&mut self) -> ShResult { + self.print_buf(false); + loop { + let key = self.term.read_key(); + let Some(cmd) = self.mode.handle_key(key) else { + continue + }; + + if cmd.should_submit() { + return Ok(self.line.to_string()); } - _ => Err(ShErr::simple(ShErrKind::ReadlineErr,format!("Unhandled common key event: {key:?}"))) + + self.exec_cmd(cmd.clone())?; + self.print_buf(true); } } - pub fn handle_repeat(&mut self, cmd: &ViCmd) -> ShResult<()> { - Ok(()) - } - pub fn exec_vi_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { - self.last_vicmd = Some(Repeat::from_cmd(cmd.clone())); - match cmd { - ViCmd::MoveVerb(verb_cmd, move_cmd) => { - let VerbCmd { verb_count, verb } = verb_cmd; - for _ in 0..verb_count { - self.line.exec_vi_cmd(Some(verb.clone()), Some(move_cmd.clone()))?; - } - if verb == Verb::Change { - self.set_insert_mode(); - } + pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { + if cmd.is_mode_transition() { + let count = cmd.verb_count(); + let mut mode: Box = match cmd.verb().unwrap() { + Verb::InsertMode => Box::new(ViInsert::new().with_count(count)), + Verb::NormalMode => Box::new(ViNormal::new()), + Verb::VisualMode => todo!(), + Verb::OverwriteMode => todo!(), + _ => unreachable!() + }; + + std::mem::swap(&mut mode, &mut self.mode); + self.term.write(&mode.cursor_style()); + + if mode.is_repeatable() { + self.repeat_action = mode.as_replay(); } - ViCmd::Verb(verb_cmd) => { - let VerbCmd { verb_count, verb } = verb_cmd; - for _ in 0..verb_count { - self.line.exec_vi_cmd(Some(verb.clone()), None)?; - } - } - ViCmd::Move(move_cmd) => { - self.line.exec_vi_cmd(None, Some(move_cmd))?; - } - } - Ok(()) - } - pub fn execute_cmd(&mut self, cmd: LineCmd) -> ShResult<()> { - match cmd { - LineCmd::ViCmd(cmd) => self.exec_vi_cmd(cmd)?, - LineCmd::Abort => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::BeginningOfHistory => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::CapitalizeWord => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::ClearScreen => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::Complete => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::CompleteBackward => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::CompleteHint => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::DowncaseWord => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::EndOfFile => { - if self.line.buffer.is_empty() { - sh_quit(0); - } else { - self.line.clear(); - } - } - LineCmd::EndOfHistory => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::ForwardSearchHistory => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::HistorySearchBackward => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::HistorySearchForward => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::Insert(_) => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::Interrupt => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::Move(_) => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::NextHistory => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::Noop => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::Repaint => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::Overwrite(ch) => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::PreviousHistory => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::QuotedInsert => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::ReverseSearchHistory => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::Suspend => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::TransposeChars => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::TransposeWords => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::Unknown => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::YankPop => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::LineUpOrPreviousHistory => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::LineDownOrNextHistory => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::Newline => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::AcceptOrInsertLine { .. } => todo!("Unhandled cmd: {cmd:?}"), - LineCmd::Null => { /* Pass */ } - _ => todo!("Unhandled cmd: {cmd:?}"), - } + } + self.line.exec_cmd(cmd)?; Ok(()) } } - -impl Drop for FernReader { - fn drop(&mut self) { - self.term.write("\x1b[2 q"); - } -} - diff --git a/src/prompt/readline/mode.rs b/src/prompt/readline/mode.rs new file mode 100644 index 0000000..4fcf3f0 --- /dev/null +++ b/src/prompt/readline/mode.rs @@ -0,0 +1,360 @@ +use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; +use super::linebuf::TermChar; +use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, TextObj, To, Verb, VerbBuilder, ViCmd, Word}; + +pub struct CmdReplay { + cmds: Vec, + repeat: u16 +} + +impl CmdReplay { + pub fn new(cmds: Vec, repeat: u16) -> Self { + Self { cmds, repeat } + } +} + +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; +} + +#[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); + return Some(cmd) + } + 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::Grapheme(ch), M::NONE) => { + let ch = TermChar::from(ch); + self.pending_cmd.set_verb(Verb::InsertChar(ch)); + self.pending_cmd.set_motion(Motion::ForwardChar); + self.register_and_return() + } + E(K::Char(ch), M::NONE) => { + self.pending_cmd.set_verb(Verb::InsertChar(TermChar::from(ch))); + self.pending_cmd.set_motion(Motion::ForwardChar); + self.register_and_return() + } + E(K::Char('H'), M::CTRL) | + E(K::Backspace, M::NONE) => { + self.pending_cmd.set_verb(Verb::Delete); + self.pending_cmd.set_motion(Motion::BackwardChar); + self.register_and_return() + } + + E(K::BackTab, M::NONE) => { + self.pending_cmd.set_verb(Verb::CompleteBackward); + self.register_and_return() + } + + E(K::Char('I'), M::CTRL) | + E(K::Tab, M::NONE) => { + self.pending_cmd.set_verb(Verb::Complete); + self.register_and_return() + } + + E(K::Esc, M::NONE) => { + self.pending_cmd.set_verb(Verb::NormalMode); + self.pending_cmd.set_motion(Motion::BackwardChar); + self.register_and_return() + } + _ => common_cmds(key) + } + } + + fn is_repeatable(&self) -> bool { + true + } + + fn as_replay(&self) -> Option { + Some(CmdReplay::new(self.cmds.clone(), self.repeat_count)) + } + + fn cursor_style(&self) -> String { + "\x1b[6 q".to_string() + } +} + +#[derive(Default,Debug)] +pub struct ViNormal { + pending_cmd: ViCmd, +} + +impl ViNormal { + pub fn new() -> Self { + Self::default() + } + pub fn take_cmd(&mut self) -> ViCmd { + std::mem::take(&mut self.pending_cmd) + } + pub fn clear_cmd(&mut self) { + self.pending_cmd = ViCmd::new(); + } + fn handle_pending_builder(&mut self, key: E) -> Option { + if self.pending_cmd.wants_register { + if let E(K::Char(ch @ ('a'..='z' | 'A'..='Z')), M::NONE) = key { + self.pending_cmd.set_register(ch); + return None + } else { + self.clear_cmd(); + return None + } + } else if let Some(Verb::Builder(_)) = &self.pending_cmd.verb { + todo!() // Don't have any verb builders yet, but might later + } else if let Some(Motion::Builder(builder)) = self.pending_cmd.motion.clone() { + match builder { + MotionBuilder::CharSearch(direction, dest, _) => { + if let E(K::Char(ch), M::NONE) = key { + self.pending_cmd.set_motion(Motion::CharSearch( + direction.unwrap(), + dest.unwrap(), + ch.into(), + )); + return Some(self.take_cmd()); + } else { + self.clear_cmd(); + return None; + } + } + MotionBuilder::TextObj(_, bound) => { + if let Some(bound) = bound { + if let E(K::Char(ch), M::NONE) = key { + let obj = match ch { + 'w' => TextObj::Word(Word::Normal), + 'W' => TextObj::Word(Word::Big), + '(' | ')' => TextObj::Paren, + '[' | ']' => TextObj::Bracket, + '{' | '}' => TextObj::Brace, + '<' | '>' => TextObj::Angle, + '"' => TextObj::DoubleQuote, + '\'' => TextObj::SingleQuote, + '`' => TextObj::BacktickQuote, + _ => TextObj::Custom(ch), + }; + self.pending_cmd.set_motion(Motion::TextObj(obj, bound)); + return Some(self.take_cmd()); + } else { + self.clear_cmd(); + return None; + } + } else if let E(K::Char(ch), M::NONE) = key { + let bound = match ch { + 'i' => Bound::Inside, + 'a' => Bound::Around, + _ => { + self.clear_cmd(); + return None; + } + }; + self.pending_cmd.set_motion(Motion::Builder(MotionBuilder::TextObj(None, Some(bound)))); + return None; + } else { + self.clear_cmd(); + return None; + } + } + } + } + None + } +} + +impl ViMode for ViNormal { + fn handle_key(&mut self, key: E) -> Option { + if let E(K::Char(ch),M::NONE) = key { + self.pending_cmd.append_seq_char(ch); + } + if self.pending_cmd.is_building() { + return self.handle_pending_builder(key) + } + match key { + E(K::Char(digit @ '0'..='9'), M::NONE) => self.pending_cmd.append_digit(digit), + E(K::Char('"'),M::NONE) => { + if self.pending_cmd.is_empty() { + if self.pending_cmd.register().name().is_none() { + self.pending_cmd.wants_register = true; + } else { + self.clear_cmd(); + } + } else { + self.clear_cmd(); + } + return None + } + E(K::Char('i'),M::NONE) if self.pending_cmd.verb().is_some() => { + self.pending_cmd.set_motion(Motion::Builder(MotionBuilder::TextObj(None, Some(Bound::Inside)))); + } + E(K::Char('a'),M::NONE) if self.pending_cmd.verb().is_some() => { + self.pending_cmd.set_motion(Motion::Builder(MotionBuilder::TextObj(None, Some(Bound::Around)))); + } + E(K::Char('h'),M::NONE) => self.pending_cmd.set_motion(Motion::BackwardChar), + E(K::Char('j'),M::NONE) => self.pending_cmd.set_motion(Motion::LineDown), + E(K::Char('k'),M::NONE) => self.pending_cmd.set_motion(Motion::LineUp), + E(K::Char('l'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardChar), + E(K::Char('w'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::Start, Word::Normal)), + E(K::Char('W'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::Start, Word::Big)), + E(K::Char('e'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::End, Word::Normal)), + E(K::Char('E'),M::NONE) => self.pending_cmd.set_motion(Motion::ForwardWord(To::End, Word::Big)), + E(K::Char('b'),M::NONE) => self.pending_cmd.set_motion(Motion::BackwardWord(Word::Normal)), + E(K::Char('B'),M::NONE) => self.pending_cmd.set_motion(Motion::BackwardWord(Word::Big)), + E(K::Char('x'),M::NONE) => self.pending_cmd.set_verb(Verb::DeleteChar(Anchor::After)), + E(K::Char('X'),M::NONE) => self.pending_cmd.set_verb(Verb::DeleteChar(Anchor::Before)), + E(K::Char('d'),M::NONE) => { + if self.pending_cmd.verb().is_none() { + self.pending_cmd.set_verb(Verb::Delete) + } else if let Some(verb) = self.pending_cmd.verb() { + if verb == &Verb::Delete { + self.pending_cmd.set_motion(Motion::WholeLine); + } else { + self.clear_cmd(); + } + } + } + E(K::Char('c'),M::NONE) => { + if self.pending_cmd.verb().is_none() { + self.pending_cmd.set_verb(Verb::Change) + } else if let Some(verb) = self.pending_cmd.verb() { + if verb == &Verb::Change { + self.pending_cmd.set_motion(Motion::WholeLine); + } else { + self.clear_cmd(); + } + } + } + E(K::Char('y'),M::NONE) => { + if self.pending_cmd.verb().is_none() { + self.pending_cmd.set_verb(Verb::Yank) + } else if let Some(verb) = self.pending_cmd.verb() { + if verb == &Verb::Yank { + self.pending_cmd.set_motion(Motion::WholeLine); + } else { + self.clear_cmd(); + } + } + } + E(K::Char('p'),M::NONE) => self.pending_cmd.set_verb(Verb::Put(Anchor::After)), + E(K::Char('P'),M::NONE) => self.pending_cmd.set_verb(Verb::Put(Anchor::Before)), + E(K::Char('D'),M::NONE) => { + self.pending_cmd.set_verb(Verb::Delete); + self.pending_cmd.set_motion(Motion::EndOfLine); + } + E(K::Char('f'),M::NONE) => { + let builder = MotionBuilder::CharSearch( + Some(Direction::Forward), + Some(Dest::On), + None + ); + self.pending_cmd.set_motion(Motion::Builder(builder)); + } + E(K::Char('F'),M::NONE) => { + let builder = MotionBuilder::CharSearch( + Some(Direction::Backward), + Some(Dest::On), + None + ); + self.pending_cmd.set_motion(Motion::Builder(builder)); + } + E(K::Char('t'),M::NONE) => { + let builder = MotionBuilder::CharSearch( + Some(Direction::Forward), + Some(Dest::Before), + None + ); + self.pending_cmd.set_motion(Motion::Builder(builder)); + } + E(K::Char('T'),M::NONE) => { + let builder = MotionBuilder::CharSearch( + Some(Direction::Backward), + Some(Dest::Before), + None + ); + self.pending_cmd.set_motion(Motion::Builder(builder)); + } + E(K::Char('i'),M::NONE) => { + self.pending_cmd.set_verb(Verb::InsertMode); + } + E(K::Char('I'),M::NONE) => { + self.pending_cmd.set_verb(Verb::InsertMode); + self.pending_cmd.set_motion(Motion::BeginningOfFirstWord); + } + E(K::Char('a'),M::NONE) => { + self.pending_cmd.set_verb(Verb::InsertMode); + self.pending_cmd.set_motion(Motion::ForwardChar); + } + E(K::Char('A'),M::NONE) => { + self.pending_cmd.set_verb(Verb::InsertMode); + self.pending_cmd.set_motion(Motion::EndOfLine); + } + _ => return common_cmds(key) + } + if self.pending_cmd.is_complete() { + Some(self.take_cmd()) + } else { + None + } + } + + fn is_repeatable(&self) -> bool { + false + } + + fn as_replay(&self) -> Option { + None + } + + fn cursor_style(&self) -> String { + "\x1b[2 q".to_string() + } +} + +pub fn common_cmds(key: E) -> Option { + let mut pending_cmd = ViCmd::new(); + match key { + E(K::Home, M::NONE) => pending_cmd.set_motion(Motion::BeginningOfLine), + E(K::End, M::NONE) => pending_cmd.set_motion(Motion::EndOfLine), + E(K::Left, M::NONE) => pending_cmd.set_motion(Motion::BackwardChar), + E(K::Right, M::NONE) => pending_cmd.set_motion(Motion::ForwardChar), + E(K::Up, M::NONE) => pending_cmd.set_motion(Motion::LineUp), + E(K::Down, M::NONE) => pending_cmd.set_motion(Motion::LineDown), + E(K::Enter, M::NONE) => pending_cmd.set_verb(Verb::AcceptLine), + E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(Verb::EndOfFile), + E(K::Backspace, M::NONE) | + E(K::Char('H'), M::CTRL) => { + pending_cmd.set_verb(Verb::Delete); + pending_cmd.set_motion(Motion::BackwardChar); + } + E(K::Delete, M::NONE) => { + pending_cmd.set_verb(Verb::Delete); + pending_cmd.set_motion(Motion::ForwardChar); + } + _ => return None + } + Some(pending_cmd) +} diff --git a/src/prompt/readline/register.rs b/src/prompt/readline/register.rs new file mode 100644 index 0000000..f5eeabe --- /dev/null +++ b/src/prompt/readline/register.rs @@ -0,0 +1,171 @@ +use std::sync::Mutex; + +use super::linebuf::TermCharBuf; + +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: TermCharBuf) { + 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: TermCharBuf) { + 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(TermCharBuf(vec![])), + a: Register(TermCharBuf(vec![])), + b: Register(TermCharBuf(vec![])), + c: Register(TermCharBuf(vec![])), + d: Register(TermCharBuf(vec![])), + e: Register(TermCharBuf(vec![])), + f: Register(TermCharBuf(vec![])), + g: Register(TermCharBuf(vec![])), + h: Register(TermCharBuf(vec![])), + i: Register(TermCharBuf(vec![])), + j: Register(TermCharBuf(vec![])), + k: Register(TermCharBuf(vec![])), + l: Register(TermCharBuf(vec![])), + m: Register(TermCharBuf(vec![])), + n: Register(TermCharBuf(vec![])), + o: Register(TermCharBuf(vec![])), + p: Register(TermCharBuf(vec![])), + q: Register(TermCharBuf(vec![])), + r: Register(TermCharBuf(vec![])), + s: Register(TermCharBuf(vec![])), + t: Register(TermCharBuf(vec![])), + u: Register(TermCharBuf(vec![])), + v: Register(TermCharBuf(vec![])), + w: Register(TermCharBuf(vec![])), + x: Register(TermCharBuf(vec![])), + y: Register(TermCharBuf(vec![])), + z: Register(TermCharBuf(vec![])), + } + } + 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(TermCharBuf); + +impl Register { + pub fn buf(&self) -> &TermCharBuf { + &self.0 + } + pub fn write(&mut self, buf: TermCharBuf) { + self.0 = buf + } + pub fn append(&mut self, mut buf: TermCharBuf) { + self.0.0.append(&mut buf.0) + } + pub fn clear(&mut self) { + self.0.clear() + } +} diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index a402bfd..7780e56 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -1,5 +1,5 @@ -use std::os::fd::{BorrowedFd, RawFd}; -use nix::{libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}}; +use std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}}; +use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::STDIN_FILENO, sys::termios, unistd::{isatty, read, write}}; use super::keys::{KeyCode, KeyEvent, ModKeys}; @@ -48,6 +48,44 @@ impl Terminal { }) } + /// Same as read_byte(), only non-blocking with a very short timeout + pub fn peek_byte(&self, buf: &mut [u8]) -> usize { + const TIMEOUT_DUR: Duration = Duration::from_millis(50); + Self::with_raw_mode(|| { + self.read_blocks(false); + + let start = Instant::now(); + loop { + match read(self.stdin, buf) { + Ok(n) if n > 0 => { + self.read_blocks(true); + return n + } + Ok(_) => {} + Err(e) if e == Errno::EAGAIN => {} + Err(e) => panic!("nonblocking read failed: {e}") + } + + if start.elapsed() >= TIMEOUT_DUR { + self.read_blocks(true); + return 0 + } + + sleep(Duration::from_millis(1)); + } + }) + } + + pub fn read_blocks(&self, yn: bool) { + let flags = OFlag::from_bits_truncate(fcntl(self.stdin, FcntlArg::F_GETFL).unwrap()); + let new_flags = if !yn { + flags | OFlag::O_NONBLOCK + } else { + flags & !OFlag::O_NONBLOCK + }; + fcntl(self.stdin, FcntlArg::F_SETFL(new_flags)).unwrap(); + } + pub fn write_bytes(&self, buf: &[u8]) { Self::with_raw_mode(|| { write(unsafe{BorrowedFd::borrow_raw(self.stdout)}, buf).expect("Failed to write to stdout"); @@ -69,27 +107,56 @@ impl Terminal { } pub fn read_key(&self) -> KeyEvent { - let mut buf = [0;8]; - let n = self.read_byte(&mut buf); + use core::str; - if buf[0] == 0x1b { - if n >= 3 && buf[1] == b'[' { - return match buf[2] { - b'A' => KeyEvent(KeyCode::Up, ModKeys::empty()), - b'B' => KeyEvent(KeyCode::Down, ModKeys::empty()), - b'C' => KeyEvent(KeyCode::Right, ModKeys::empty()), - b'D' => KeyEvent(KeyCode::Left, ModKeys::empty()), - _ => KeyEvent(KeyCode::Esc, ModKeys::empty()), - }; + let mut buf = [0u8; 8]; + let mut collected = Vec::with_capacity(5); + + loop { + let n = self.read_byte(&mut buf[..1]); // Read one byte at a time + if n == 0 { + continue; } - return KeyEvent(KeyCode::Esc, ModKeys::empty()); - } + collected.push(buf[0]); - if let Ok(s) = core::str::from_utf8(&buf[..n]) { - if let Some(ch) = s.chars().next() { - return KeyEvent::new(ch, ModKeys::NONE); + // ESC sequences + if collected[0] == 0x1b && collected.len() == 1 { + // Peek next byte if any + let n = self.peek_byte(&mut buf[..1]); + if n == 0 { + return KeyEvent(KeyCode::Esc, ModKeys::empty()); + } + collected.push(buf[0]); + + if buf[0] == b'[' { + // Read third byte + let _ = self.read_byte(&mut buf[..1]); + collected.push(buf[0]); + + return match buf[0] { + b'A' => KeyEvent(KeyCode::Up, ModKeys::empty()), + b'B' => KeyEvent(KeyCode::Down, ModKeys::empty()), + b'C' => KeyEvent(KeyCode::Right, ModKeys::empty()), + b'D' => KeyEvent(KeyCode::Left, ModKeys::empty()), + _ => KeyEvent(KeyCode::Esc, ModKeys::empty()), + }; + } + + return KeyEvent(KeyCode::Esc, ModKeys::empty()); + } + + // Try parse valid UTF-8 from collected bytes + if let Ok(s) = str::from_utf8(&collected) { + return KeyEvent::new(s, ModKeys::empty()); + } + + // If it's not valid UTF-8 yet, loop to collect more bytes + if collected.len() >= 4 { + // UTF-8 max char length is 4; if it's still invalid, give up + break; } } + KeyEvent(KeyCode::Null, ModKeys::empty()) } diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs new file mode 100644 index 0000000..e1d1e3f --- /dev/null +++ b/src/prompt/readline/vicmd.rs @@ -0,0 +1,294 @@ +use super::{linebuf::{TermChar, TermCharBuf}, register::{append_register, read_register, write_register}}; + +#[derive(Clone,Copy,Default,Debug)] +pub struct RegisterName { + name: Option, + append: bool +} + +impl RegisterName { + pub fn name(&self) -> Option { + self.name + } + pub fn is_append(&self) -> bool { + self.append + } + pub fn write_to_register(&self, buf: TermCharBuf) { + 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) + } +} + +#[derive(Clone,Default,Debug)] +pub struct ViCmd { + pub wants_register: bool, // Waiting for register character + + /// Register to read from/write to + pub register_count: Option, + pub register: RegisterName, + + /// Verb to perform + pub verb_count: Option, + pub verb: Option, + + /// Motion to perform + pub motion_count: Option, + pub motion: Option, + + /// Count digits are held here until we know what we are counting + /// Once a register/verb/motion is set, the count is taken from here + pub pending_count: Option, + + /// The actual keys the user typed for this command + /// Maybe display this somewhere around the prompt later? + /// Prompt escape sequence maybe? + pub raw_seq: String, +} + +impl ViCmd { + pub fn new() -> Self { + Self::default() + } + pub fn set_register(&mut self, register: char) { + let append = register.is_uppercase(); + let name = Some(register.to_ascii_lowercase()); + let reg_name = RegisterName { name, append }; + self.register = reg_name; + self.register_count = self.pending_count.take(); + self.wants_register = false; + } + pub fn append_seq_char(&mut self, ch: char) { + self.raw_seq.push(ch) + } + pub fn is_empty(&self) -> bool { + !self.wants_register && + self.register.name.is_none() && + self.verb_count.is_none() && + self.verb.is_none() && + self.motion_count.is_none() && + self.motion.is_none() + } + pub fn set_verb(&mut self, verb: Verb) { + self.verb = Some(verb); + self.verb_count = self.pending_count.take(); + } + pub fn set_motion(&mut self, motion: Motion) { + self.motion = Some(motion); + self.motion_count = self.pending_count.take(); + } + pub fn register(&self) -> RegisterName { + self.register + } + pub fn verb(&self) -> Option<&Verb> { + self.verb.as_ref() + } + pub fn verb_count(&self) -> u16 { + self.verb_count.unwrap_or(1) + } + pub fn motion(&self) -> Option<&Motion> { + self.motion.as_ref() + } + pub fn motion_count(&self) -> u16 { + self.motion_count.unwrap_or(1) + } + pub fn append_digit(&mut self, digit: char) { + // Convert char digit to a number (assuming ASCII '0'..'9') + let digit_val = digit.to_digit(10).expect("digit must be 0-9") as u16; + self.pending_count = Some(match self.pending_count { + Some(count) => count * 10 + digit_val, + None => digit_val, + }); + } + pub fn is_building(&self) -> bool { + matches!(self.verb, Some(Verb::Builder(_))) || + matches!(self.motion, Some(Motion::Builder(_))) || + self.wants_register + } + pub fn is_complete(&self) -> bool { + !( + (self.verb.is_none() && self.motion.is_none()) || + (self.verb.is_none() && self.motion.as_ref().is_some_and(|m| m.needs_verb())) || + (self.motion.is_none() && self.verb.as_ref().is_some_and(|v| v.needs_motion())) || + self.is_building() + ) + } + pub fn should_submit(&self) -> bool { + self.verb.as_ref().is_some_and(|v| *v == Verb::AcceptLine) + } + pub fn is_mode_transition(&self) -> bool { + self.verb.as_ref().is_some_and(|v| { + matches!(*v, Verb::InsertMode | Verb::NormalMode | Verb::OverwriteMode | Verb::VisualMode) + }) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum Verb { + Delete, + DeleteChar(Anchor), + Change, + Yank, + ReplaceChar(char), + Substitute, + ToggleCase, + Complete, + CompleteBackward, + Undo, + RepeatLast, + Put(Anchor), + OverwriteMode, + InsertMode, + NormalMode, + VisualMode, + JoinLines, + InsertChar(TermChar), + Insert(String), + Breakline(Anchor), + Indent, + Dedent, + AcceptLine, + Builder(VerbBuilder), + EndOfFile +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum VerbBuilder { +} + +impl Verb { + pub fn needs_motion(&self) -> bool { + matches!(self, + Self::Indent | + Self::Dedent | + Self::Delete | + Self::Change | + Self::Yank + ) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Motion { + /// Whole current line (not really a movement but a range) + WholeLine, + TextObj(TextObj, Bound), + BeginningOfFirstWord, + /// beginning-of-line + BeginningOfLine, + /// end-of-line + EndOfLine, + /// backward-word, vi-prev-word + BackwardWord(Word), // Backward until start of word + /// forward-word, vi-end-word, vi-next-word + ForwardWord(To, Word), // Forward until start/end of word + /// character-search, character-search-backward, vi-char-search + CharSearch(Direction,Dest,TermChar), + /// backward-char + BackwardChar, + /// forward-char + ForwardChar, + /// move to the same column on the previous line + LineUp, + /// move to the same column on the next line + LineDown, + /// Whole user input (not really a movement but a range) + WholeBuffer, + /// beginning-of-register + BeginningOfBuffer, + /// end-of-register + EndOfBuffer, + Builder(MotionBuilder), + Null +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum MotionBuilder { + CharSearch(Option,Option,Option), + TextObj(Option,Option) +} + +impl Motion { + pub fn needs_verb(&self) -> bool { + matches!(self, Self::TextObj(_, _)) + } +} + +#[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 + Sentence, + + /// `ip`, `ap` — inner paragraph, around paragraph + Paragraph, + + /// `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 (if you support it) + 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_old.rs b/src/prompt/readline_old.rs deleted file mode 100644 index 4050835..0000000 --- a/src/prompt/readline_old.rs +++ /dev/null @@ -1,86 +0,0 @@ -use rustyline::{completion::Completer, hint::{Hint, Hinter}, history::SearchDirection, validate::{ValidationResult, Validator}, Helper}; - -use crate::{libsh::term::{Style, Styled}, parse::{lex::{LexFlags, LexStream}, ParseStream}}; -use crate::prelude::*; - -#[derive(Default,Debug)] -pub struct FernReadline; - -impl FernReadline { - pub fn new() -> Self { - Self - } - pub fn search_hist(value: &str, ctx: &rustyline::Context<'_>) -> Option { - let len = ctx.history().len(); - for i in 0..len { - let entry = ctx.history().get(i, SearchDirection::Reverse).unwrap().unwrap(); - if entry.entry.starts_with(value) { - return Some(entry.entry.into_owned()) - } - } - None - } -} - -impl Helper for FernReadline {} - -impl Completer for FernReadline { - type Candidate = String; -} - -pub struct FernHint { - raw: String, - styled: String -} - -impl FernHint { - pub fn new(raw: String) -> Self { - let styled = (&raw).styled(Style::Dim | Style::BrightBlack); - Self { raw, styled } - } -} - -impl Hint for FernHint { - fn display(&self) -> &str { - &self.styled - } - fn completion(&self) -> Option<&str> { - if !self.raw.is_empty() { - Some(&self.raw) - } else { - None - } - } -} - -impl Hinter for FernReadline { - type Hint = FernHint; - fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option { - if line.is_empty() { - return None - } - let ent = Self::search_hist(line,ctx)?; - let entry_raw = ent.get(pos..)?.to_string(); - Some(FernHint::new(entry_raw)) - } -} - -impl Validator for FernReadline { - fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result { - let mut tokens = vec![]; - let tk_stream = LexStream::new(Arc::new(ctx.input().to_string()), LexFlags::empty()); - for tk in tk_stream { - if tk.is_err() { - return Ok(ValidationResult::Incomplete) - } - tokens.push(tk.unwrap()); - } - let nd_stream = ParseStream::new(tokens); - for nd in nd_stream { - if nd.is_err() { - return Ok(ValidationResult::Incomplete) - } - } - Ok(ValidationResult::Valid(None)) - } -}