From 98b0e24e27217bf7c3da693270dfeff42b1ca775 Mon Sep 17 00:00:00 2001 From: Kyler Clay Date: Tue, 27 May 2025 02:41:19 -0400 Subject: [PATCH] prompt and buffer drawing appears functional --- src/fern.rs | 9 +- src/prompt/mod.rs | 13 +- src/prompt/readline/linebuf.rs | 1816 +++++++++++++++---------------- src/prompt/readline/mod.rs | 147 ++- src/prompt/readline/mode.rs | 9 +- src/prompt/readline/register.rs | 73 +- src/prompt/readline/term.rs | 156 ++- src/prompt/readline/vicmd.rs | 10 +- src/shopt.rs | 3 +- 9 files changed, 1114 insertions(+), 1122 deletions(-) diff --git a/src/fern.rs b/src/fern.rs index 4ebb998..3caf949 100644 --- a/src/fern.rs +++ b/src/fern.rs @@ -24,7 +24,8 @@ use crate::signal::sig_setup; use crate::state::source_rc; use crate::prelude::*; use clap::Parser; -use state::{read_vars, write_vars}; +use shopt::FernEditMode; +use state::{read_shopts, read_vars, write_shopts, write_vars}; #[derive(Parser,Debug)] struct FernArgs { @@ -98,7 +99,11 @@ fn fern_interactive() { let mut readline_err_count: u32 = 0; loop { // Main loop - let input = match prompt::read_line() { + let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode")) + .unwrap() + .map(|mode| mode.parse::().unwrap_or_default()) + .unwrap(); + let input = match prompt::read_line(edit_mode) { Ok(line) => { readline_err_count = 0; line diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index eb92a18..e727de6 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -3,9 +3,9 @@ pub mod highlight; use std::path::Path; -use readline::FernVi; +use readline::{FernVi, Readline}; -use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, state::read_shopts}; +use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, shopt::FernEditMode, state::read_shopts}; /// Initialize the line editor fn get_prompt() -> ShResult { @@ -20,8 +20,13 @@ fn get_prompt() -> ShResult { Ok(format!("\n{}",expand_prompt(&prompt)?)) } -pub fn read_line() -> ShResult { +pub fn read_line(edit_mode: FernEditMode) -> ShResult { + dbg!("hi"); let prompt = get_prompt()?; - let mut reader = FernVi::new(Some(prompt)); + let mut reader: Box = match edit_mode { + FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))), + FernEditMode::Emacs => todo!() + }; + dbg!("there"); reader.readline() } diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index 0fae256..0c1de60 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -1,5 +1,6 @@ -use std::{cmp::Ordering, fmt::Display, ops::{Deref, DerefMut, Range, RangeBounds, RangeInclusive}, sync::Arc}; +use std::{cmp::Ordering, fmt::Display, ops::{Deref, DerefMut, Range, RangeBounds, RangeInclusive}, str::FromStr, sync::Arc}; +use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}}; @@ -10,7 +11,9 @@ use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, RegisterName, TextObj #[derive(Debug, PartialEq, Eq)] pub enum CharClass { Alphanum, - Symbol + Symbol, + Whitespace, + Other } #[derive(Debug, Clone, PartialEq, Eq)] @@ -18,7 +21,7 @@ pub enum MotionKind { Forward(usize), To(usize), Backward(usize), - Range(Range), + Range((usize,usize)), Line(isize), // positive = up line, negative = down line ToLine(usize), Null, @@ -41,156 +44,44 @@ impl MotionKind { std::ops::Bound::Unbounded => panic!("called range constructor with no upper bound") }; if end > start { - Self::Range(start..end) + Self::Range((start,end)) } else { - Self::Range(end..start) + Self::Range((end,start)) } } } -#[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")?, - } +impl From<&str> for CharClass { + fn from(value: &str) -> Self { + if value.len() > 1 { + return Self::Symbol // Multi-byte grapheme } - 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 + if value.chars().all(char::is_alphanumeric) { + CharClass::Alphanum + } else if value.chars().all(char::is_whitespace) { + CharClass::Whitespace + } else if !value.chars().all(char::is_alphanumeric) { + CharClass::Symbol } else { - Self::Symbol + Self::Other } } } -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) +fn is_other_class_or_ws(a: &str, b: &str) -> bool { + let a = CharClass::from(a); + let b = CharClass::from(b); + if a == CharClass::Whitespace || b == CharClass::Whitespace { + true + } else { + a != b + } } pub struct UndoPayload { - buffer: TermCharBuf, + buffer: String, cursor: usize } @@ -198,19 +89,19 @@ pub struct UndoPayload { pub struct Edit { pub pos: usize, pub cursor_pos: usize, - pub old: TermCharBuf, - pub new: TermCharBuf + pub old: String, + pub new: String, } impl Edit { - pub fn diff(a: TermCharBuf, b: TermCharBuf, old_cursor_pos: usize) -> Self { + pub fn diff(a: &str, b: &str, old_cursor_pos: usize) -> Edit { use std::cmp::min; let mut start = 0; let max_start = min(a.len(), b.len()); // Calculate the prefix of the edit - while start < max_start && a[start] == b[start] { + while start < max_start && a.as_bytes()[start] == b.as_bytes()[start] { start += 1; } @@ -218,43 +109,42 @@ impl Edit { return Edit { pos: start, cursor_pos: old_cursor_pos, - old: TermCharBuf(vec![]), - new: TermCharBuf(vec![]), - } + old: String::new(), + new: String::new(), + }; } let mut end_a = a.len(); let mut end_b = b.len(); // Calculate the suffix of the edit - while end_a > start && end_b > start && a[end_a - 1] == b[end_b - 1] { + while end_a > start && end_b > start && a.as_bytes()[end_a - 1] == b.as_bytes()[end_b - 1] { end_a -= 1; end_b -= 1; } - // Slice off the prefix and suffix for both - let old = TermCharBuf(a[start..end_a].to_vec()); - let new = TermCharBuf(b[start..end_b].to_vec()); + // Slice off the prefix and suffix for both (safe because start/end are byte offsets) + let old = a[start..end_a].to_string(); + let new = b[start..end_b].to_string(); Edit { pos: start, cursor_pos: old_cursor_pos, old, - new + new, } } } #[derive(Default,Debug)] pub struct LineBuf { - buffer: TermCharBuf, + buffer: String, cursor: usize, clamp_cursor: bool, first_line_offset: usize, merge_edit: bool, undo_stack: Vec, redo_stack: Vec, - term_dims: (usize,usize) } impl LineBuf { @@ -262,917 +152,905 @@ impl LineBuf { 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.buffer = initial.to_string(); self } - pub fn set_first_line_offset(&mut self, offset: usize) { - self.first_line_offset = offset + pub fn as_str(&self) -> &str { + &self.buffer + } + pub fn take(&mut self) -> String { + let line = std::mem::take(&mut self.buffer); + *self = Self::default(); + line + } + pub fn byte_pos(&self) -> usize { + self.cursor + } + pub fn byte_len(&self) -> usize { + self.buffer.len() + } + pub fn is_empty(&self) -> bool { + self.buffer.is_empty() + } + pub fn clamp_cursor(&mut self) { + // Normal mode does not allow you to sit on the edge of the buffer, you must be hovering over a character + // Insert mode does let you set on the edge though, so that you can append new characters + // This method is used in Normal mode + dbg!("clamping"); + if self.cursor == self.byte_len() { + self.cursor_back(1); + } + } + pub fn grapheme_len(&self) -> usize { + self.buffer.grapheme_indices(true).count() + } + pub fn slice_from_cursor(&self) -> &str { + &self.buffer[self.cursor..] + } + pub fn slice_to_cursor(&self) -> &str { + if let Some(slice) = self.buffer.get(..self.cursor) { + slice + } else { + &self.buffer + } + + } + pub fn slice_from_cursor_to_end_of_line(&self) -> &str { + let end = self.end_of_line(); + &self.buffer[self.cursor..end] + } + pub fn slice_from_start_of_line_to_cursor(&self) -> &str { + let start = self.start_of_line(); + &self.buffer[start..self.cursor] + } + pub fn slice_from(&self, pos: usize) -> &str { + &self.buffer[pos..] + } + pub fn slice_to(&self, pos: usize) -> &str { + &self.buffer[..pos] } pub fn set_cursor_clamp(&mut self, yn: bool) { self.clamp_cursor = yn } - 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, first_line_offset: usize) -> usize { - let mut cur_line_len = 0; - let mut lines = 1; - let first_line_max_len = self.term_dims.1.saturating_sub(first_line_offset); - for char in self.buffer.iter() { - match char { - TermChar::Newline => { - lines += 1; - cur_line_len = 0; - } - TermChar::Grapheme(str) => { - cur_line_len += str.width().max(1); - if (lines == 1 && first_line_max_len > 0 && cur_line_len >= first_line_max_len) || cur_line_len > self.term_dims.1 { - lines += 1; - cur_line_len = 0; - } - } - } - } - lines - } - pub fn cursor_back(&mut self, count: usize) { - self.cursor = self.cursor.saturating_sub(count) - } - 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 update_term_dims(&mut self, x: usize, y: usize) { - self.term_dims = (x,y) - } - pub fn cursor_display_coords(&self, first_line_offset: Option) -> (usize, usize) { - let mut x = 0; - let mut y = 0; - let first_line_max_len = first_line_offset.map(|fl| self.term_dims.1.saturating_sub(fl)).unwrap_or_default(); - for i in 0..self.cursor() { - let ch = self.get_char(i).unwrap(); - match ch { - TermChar::Grapheme(str) => { - x += str.width().max(1); - if (y == 0 && first_line_max_len > 0 && x >= first_line_max_len) || x > self.term_dims.1 { - y += 1; - x = 0; - } - } - TermChar::Newline => { - y += 1; - x = 0; - } - } - } - - (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 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 { - start = self.num_or_len_minus_one(start); - while start > 0 && !cond(&self.buffer[start]) { - start -= 1; - } - if !inclusive { - if start > 0 { - start.saturating_add(1) - } else { - start - } + pub fn grapheme_at_cursor(&self) -> Option<&str> { + if self.cursor == self.byte_len() { + None } else { - start + self.slice_from_cursor().graphemes(true).next() } } - 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 => { - if self.on_word_bound(word, pos, dir) { - pos = pos.saturating_sub(1); - } - - if self.get_char(pos).is_some_and(|c| c.is_whitespace()) { - pos = self.backward_until(pos, |c| !c.is_whitespace(), true); - } else { - pos = self.backward_until(pos, |c| c.is_whitespace(), true); - pos = self.backward_until(pos, |c| !c.is_whitespace(), true); - } - } - } - } - Word::Normal => { - 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 => { - if self.on_word_bound(word, pos, dir) { - // Nudge - pos = pos.saturating_sub(1); - } - // If we are on whitespace, proceed until we are not, inclusively - if self.get_char(pos).is_some_and(|c| c.is_whitespace()) { - pos = self.backward_until(pos, |c| !c.is_whitespace(), true) - } else { - // If we are not on whitespace, proceed until we hit something different, inclusively - let this_char = self.get_char(pos).unwrap(); - pos = self.backward_until(pos, |c| is_other_class_or_ws(this_char, c), true); - // If we landed on whitespace, proceed until we are not on whitespace - if self.get_char(pos).is_some_and(|c| c.is_whitespace()) { - pos = self.backward_until(pos, |c| !c.is_whitespace(), true) - } - } - } - } - } - } - } - } - pos - } - pub fn eval_quote_obj(&self, target: &str, bound: Bound) -> Range { - let mut end; - let start; - let cursor = self.cursor(); - let ln_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false); - let mut line_chars = self.buffer[ln_start..cursor].iter(); - let mut in_quote = false; - while let Some(ch) = line_chars.next() { - let TermChar::Grapheme(ch) = ch else { unreachable!() }; - match ch.as_ref() { - "\\" => { - line_chars.next(); - } - "\"" => in_quote = !in_quote, - _ => { /* continue */ } - } - } - let mut start_pos = cursor; - let end_pos; - if !in_quote { - start_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches(target), true); - if !self.get_char(start_pos).is_some_and(|c| c.matches(target)) { - return cursor..cursor - } - end_pos = self.forward_until(start_pos, |c| c.matches("\n") || c.matches(target), true); - if !self.get_char(end_pos).is_some_and(|c| c.matches(target)) { - return cursor..cursor - } - start = start_pos; - end = end_pos; - } else { - start_pos = self.backward_until(start_pos, |c| c.matches("\n") || c.matches(target), true); - if !self.get_char(start_pos).is_some_and(|c| c.matches(target)) { - return cursor..cursor - } - end_pos = self.forward_until(self.num_or_len(start_pos + 1), |c| c.matches("\n") || c.matches(target), true); - if !self.get_char(end_pos).is_some_and(|c| c.matches(target)) { - return cursor..cursor - } - start = start_pos; - end = self.num_or_len(end_pos + 1); - - if bound == Bound::Around && self.get_char(end).is_some_and(|c| c.is_whitespace()) { - end += 1; - end = self.forward_until(end, |c| !c.is_whitespace(), true); - } - } - mk_range(start,end) - } - pub fn eval_delim_obj(&self, obj: &TextObj, bound: Bound) -> Range { - // FIXME: logic isn't completely robust i think - let opener = match obj { - TextObj::Brace => "{", - TextObj::Bracket => "[", - TextObj::Paren => "(", - TextObj::Angle => "<", - _ => unreachable!() - }; - let closer = match obj { - TextObj::Brace => "}", - TextObj::Bracket => "]", - TextObj::Paren => ")", - TextObj::Angle => ">", - _ => unreachable!() - }; - let mut end = None; - let mut start = None; - let mut delim_count: usize = 0; - let ln_range = self.cur_line_range(); - let cursor = self.cursor(); - let mut ln_chars = self.buffer[*ln_range.start()..cursor].iter().enumerate(); - while let Some((i,ch)) = ln_chars.next() { - let &TermChar::Grapheme(ch) = &ch else { unreachable!() }; - match ch.as_ref() { - "\\" => { - ln_chars.next(); - } - ch if ch == opener => { - start = Some(ln_range.start() + i); - delim_count += 1; - } - ch if ch == closer => delim_count -= 1, - _ => {} - } - } - - let mut start_pos = None; - let mut end_pos = None; - if delim_count == 0 { - let mut ln_chars = self.buffer[cursor..*ln_range.end()].iter().enumerate(); - while let Some((i,ch)) = ln_chars.next() { - let &TermChar::Grapheme(ch) = &ch else { unreachable!() }; - match ch.as_ref() { - "\\" => { - ln_chars.next(); - } - ch if ch == opener => { - if delim_count == 0 { - start_pos = Some(cursor + i); - } - delim_count += 1; - } - ch if ch == closer => { - delim_count -= 1; - if delim_count == 0 { - end_pos = Some(cursor + i); - } - } - _ => {} - } - } - - if start_pos.is_none() || end_pos.is_none() { - return cursor..cursor - } else { - start = start_pos; - end = end_pos; - } - } else { - let Some(strt) = start else { - dbg!("no start"); - dbg!("no start"); - dbg!("no start"); - dbg!("no start"); - dbg!("no start"); - dbg!("no start"); - return cursor..cursor - }; - let strt = self.num_or_len(strt + 1); // skip the paren - let target = delim_count.saturating_sub(1); - let mut ln_chars = self.buffer[strt..*ln_range.end()].iter().enumerate(); - dbg!(&ln_chars); - dbg!(&ln_chars); - dbg!(&ln_chars); - dbg!(&ln_chars); - - while let Some((i,ch)) = ln_chars.next() { - let &TermChar::Grapheme(ch) = &ch else { unreachable!() }; - match ch.as_ref() { - "\\" => { - ln_chars.next(); - } - ch if ch == opener => { - delim_count += 1; - } - ch if ch == closer => { - delim_count -= 1; - if delim_count == target { - end_pos = Some(strt + i); - } - } - _ => {} - } - } - dbg!(end_pos); - dbg!(end_pos); - dbg!(end_pos); - dbg!(start_pos); - dbg!(start_pos); - dbg!(start_pos); - dbg!(start_pos); - dbg!(start_pos); - dbg!(start_pos); - dbg!(start_pos); - if end_pos.is_none() { - return cursor..cursor - } else { - end = end_pos; - } - } - - let Some(mut start) = start else { - return cursor..cursor - }; - let Some(mut end) = end else { - return cursor..cursor - }; - match bound { - Bound::Inside => { - end = end.saturating_sub(1); - start = self.num_or_len(start + 1); - mk_range(start,end) - } - Bound::Around => mk_range(start,end) - } - - } - pub fn eval_text_obj(&self, obj: TextObj, bound: Bound) -> Range { - let mut start; - let mut end; - - 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 => return self.eval_quote_obj("\"", bound), - TextObj::SingleQuote => return self.eval_quote_obj("'", bound), - TextObj::BacktickQuote => return self.eval_quote_obj("`", bound), - TextObj::Paren | - TextObj::Bracket | - TextObj::Brace | - TextObj::Angle => return self.eval_delim_obj(&obj, bound), - TextObj::Tag => todo!(), - TextObj::Custom(_) => todo!(), - } - - if bound == Bound::Inside { - start = self.num_or_len_minus_one(start + 1); - end = end.saturating_sub(1); - } - start..end - } - pub fn validate_range(&self, range: &Range) -> bool { - range.end < self.buffer.len() - } - pub fn lines_from_cursor(&self, offset: isize) -> RangeInclusive { - let mut start; - let mut end; - match offset.cmp(0) { + pub fn grapheme_at_cursor_offset(&self, offset: isize) -> Option<&str> { + match offset.cmp(&0) { Ordering::Equal => { - return self.cur_line_range() - } - Ordering::Greater => { - let this_line = self.cur_line_range(); - start = *this_line.start(); - end = *this_line.end(); - for _ in 0..offset { - let next_ln = self.line_range_from_pos(self.num_or_len(end + 1)); - end = *this_line.end(); - } + return self.grapheme_at(self.cursor); } Ordering::Less => { + // Walk backward from the start of the line or buffer up to the cursor + // and count graphemes in reverse. + let rev_graphemes: Vec<&str> = self.slice_to_cursor().graphemes(true).collect(); + let idx = rev_graphemes.len().checked_sub((-offset) as usize)?; + rev_graphemes.get(idx).copied() + } + Ordering::Greater => { + self.slice_from_cursor() + .graphemes(true) + .nth(offset as usize) } } - start..=end } - pub fn line_range_from_pos(&self, pos: usize) -> RangeInclusive { - let mut line_start = self.backward_until(pos, |c| c == &TermChar::Newline, false); - let mut line_end = self.forward_until(pos, |c| c == &TermChar::Newline, true); - if self.get_char(line_start.saturating_sub(1)).is_none_or(|c| c != &TermChar::Newline) { - line_start = 0; + pub fn grapheme_at(&self, pos: usize) -> Option<&str> { + if pos >= self.byte_len() { + None + } else { + self.buffer.graphemes(true).nth(pos) } - if self.get_char(line_end).is_none_or(|c| c != &TermChar::Newline) { - line_end = self.buffer.len().saturating_sub(1); - line_start = self.backward_until(line_start, |c| c == &TermChar::Newline, true) + } + pub fn is_whitespace(&self, pos: usize) -> bool { + let Some(g) = self.grapheme_at(pos) else { + return false + }; + g.chars().all(char::is_whitespace) + } + pub fn on_whitespace(&self) -> bool { + self.is_whitespace(self.cursor) + } + pub fn next_pos(&self, n: usize) -> Option { + if self.cursor == self.byte_len() { + None + } else { + self.slice_from_cursor() + .grapheme_indices(true) + .take(n) + .last() + .map(|(i,s)| i + self.cursor + s.len()) + } + } + pub fn prev_pos(&self, n: usize) -> Option { + if self.cursor == 0 { + None + } else { + self.slice_to_cursor() + .grapheme_indices(true) + .rev() // <- walk backward + .take(n) + .last() + .map(|(i, _)| i) + } + } + pub fn cursor_back(&mut self, dist: usize) -> bool { + let Some(pos) = self.prev_pos(dist) else { + return false + }; + self.cursor = pos; + true + } + /// Up to but not including 'dist' + pub fn cursor_back_to(&mut self, dist: usize) -> bool { + let dist = dist.saturating_sub(1); + let Some(pos) = self.prev_pos(dist) else { + return false + }; + self.cursor = pos; + true + } + pub fn cursor_fwd(&mut self, dist: usize) -> bool { + let Some(pos) = self.next_pos(dist) else { + return false + }; + self.cursor = pos; + true + } + pub fn cursor_fwd_to(&mut self, dist: usize) -> bool { + let dist = dist.saturating_sub(1); + let Some(pos) = self.next_pos(dist) else { + return false + }; + self.cursor = pos; + true + } + pub fn count_display_lines(&self, offset: usize, term_width: usize) -> usize { + let mut lines = 0; + let mut col = offset.max(1); + for ch in self.buffer.chars() { + match ch { + '\n' => { + lines += 1; + col = 1; + } + _ => { + col += 1; + if col > term_width { + lines += 1; + col = 1 + } + } + } + } + lines + } + pub fn cursor_display_line_position(&self, offset: usize, term_width: usize) -> usize { + let mut lines = 0; + let mut col = offset.max(1); + for ch in self.slice_to_cursor().chars() { + match ch { + '\n' => { + lines += 1; + col = 1; + } + _ => { + col += 1; + if col > term_width { + lines += 1; + col = 1 + } + } + } + } + lines + } + pub fn display_coords(&self, term_width: usize) -> (usize,usize) { + let mut chars = self.slice_to_cursor().chars(); + + let mut lines = 0; + let mut col = 0; + for ch in chars { + match ch { + '\n' => { + lines += 1; + col = 1; + } + _ => { + col += 1; + if col > term_width { + lines += 1; + col = 1 + } + } + } + } + (lines,col) + } + pub fn cursor_display_coords(&self, first_ln_offset: usize, term_width: usize) -> (usize,usize) { + let (d_line,mut d_col) = self.display_coords(term_width); + let line = self.count_display_lines(first_ln_offset, term_width) - d_line; + + if line == self.count_lines() { + d_col += first_ln_offset; } - line_start..=self.num_or_len(line_end + 1) + (line,d_col) } - pub fn cur_line_range(&self) -> RangeInclusive { - let cursor = self.cursor(); - self.line_range_from_pos(cursor) + pub fn insert(&mut self, ch: char) { + if self.buffer.is_empty() { + self.buffer.push(ch) + } else { + self.buffer.insert(self.cursor, ch); + } } - pub fn on_first_line(&self) -> bool { - let cursor = self.cursor(); - let ln_start = self.backward_until(cursor, |c| c.matches("\n"), true); - !self.get_char(ln_start).is_some_and(|c| c.matches("\n")) + pub fn move_to(&mut self, pos: usize) -> bool { + if self.cursor == pos { + false + } else { + self.cursor = pos; + true + } } - pub fn on_last_line(&self) -> bool { - let cursor = self.cursor(); - let ln_end = self.forward_until(cursor, |c| c.matches("\n"), true); - !self.get_char(ln_end).is_some_and(|c| c.matches("\n")) + pub fn move_buf_start(&mut self) -> bool { + self.move_to(0) } - pub fn cur_line_col(&self) -> usize { - let cursor = self.cursor(); - let ln_span = self.cur_line_range(); - cursor.saturating_sub(*ln_span.start()) + pub fn move_buf_end(&mut self) -> bool { + self.move_to(self.byte_len()) } - /// Clamp a number to the length of the buffer - pub fn num_or_len_minus_one(&self, num: usize) -> usize { - num.min(self.buffer.len().saturating_sub(1)) + pub fn move_home(&mut self) -> bool { + let start = self.start_of_line(); + self.move_to(start) } - pub fn num_or_len(&self, num: usize) -> usize { - num.min(self.buffer.len()) + pub fn move_end(&mut self) -> bool { + let end = self.end_of_line(); + self.move_to(end) + } + pub fn start_of_line(&self) -> usize { + if let Some(i) = self.slice_to_cursor().rfind('\n') { + i + 1 // Land on start of this line, instead of the end of the last one + } else { + 0 + } + } + pub fn end_of_line(&self) -> usize { + if let Some(i) = self.slice_from_cursor().find('\n') { + i + self.cursor + } else { + self.byte_len() + } + } + pub fn this_line(&self) -> (usize,usize) { + ( + self.start_of_line(), + self.end_of_line() + ) + } + pub fn count_lines(&self) -> usize { + self.buffer + .chars() + .filter(|&c| c == '\n') + .count() + } + pub fn line_no(&self) -> usize { + self.slice_to_cursor() + .chars() + .filter(|&c| c == '\n') + .count() + } + /// Returns the (start, end) byte range for the given line number. + /// + /// - Line 0 starts at the beginning of the buffer and ends at the first newline (or end of buffer). + /// - Line 1 starts just after the first newline, ends at the second, etc. + /// + /// Returns `None` if the line number is beyond the last line in the buffer. + pub fn select_line(&self, n: usize) -> Option<(usize, usize)> { + let mut start = 0; + + let bytes = self.as_str(); // or whatever gives the full buffer as &str + let mut line_iter = bytes.match_indices('\n').map(|(i, _)| i + 1); + + // Advance to the nth newline (start of line n) + for _ in 0..n { + start = line_iter.next()?; + } + + // Find the next newline (end of line n), or end of buffer + let end = line_iter.next().unwrap_or(bytes.len()); + + Some((start, end)) + } + /// Find the span from the start of the nth line above the cursor, to the end of the current line. + /// + /// Returns (start,end) + /// 'start' is the first character after the previous newline, or the start of the buffer + /// 'end' is the index of the newline after the nth line + /// + /// The caller can choose whether to include the newline itself in the selection by using either + /// * `(start..end)` to exclude it + /// * `(start..=end)` to include it + pub fn select_lines_up(&self, n: usize) -> (usize,usize) { + let end = self.end_of_line(); + let mut start = self.start_of_line(); + if start == 0 { + return (start,end) + } + + for _ in 0..n { + if let Some(prev_newline) = self.slice_to(start - 1).rfind('\n') { + start = prev_newline + 1; + } else { + start = 0; + break + } + } + + (start,end) + } + /// Find the range from the start of this line, to the end of the nth line after the cursor + /// + /// Returns (start,end) + /// 'start' is the first character after the previous newline, or the start of the buffer + /// 'end' is the index of the newline after the nth line + /// + /// The caller can choose whether to include the newline itself in the selection by using either + /// * `(start..end)` to exclude it + /// * `(start..=end)` to include it + pub fn select_lines_down(&self, n: usize) -> (usize,usize) { + let mut end = self.end_of_line(); + let start = self.start_of_line(); + if end == self.byte_len() { + return (start,end) + } + + for _ in 0..n { + if let Some(next_newline) = self.slice_from(end).find('\n') { + end = next_newline + } else { + end = self.byte_len(); + break + } + } + + (start,end) + } + pub fn select_lines_to(&self, line_no: usize) -> (usize,usize) { + let cursor_line_no = self.line_no(); + let offset = (cursor_line_no as isize) - (line_no as isize); + match offset.cmp(&0) { + Ordering::Less => self.select_lines_down(offset.unsigned_abs()), + Ordering::Equal => self.this_line(), + Ordering::Greater => self.select_lines_up(offset as usize) + } + } + fn on_start_of_word(&self, size: Word) -> bool { + self.is_start_of_word(size, self.cursor) + } + fn on_end_of_word(&self, size: Word) -> bool { + self.is_end_of_word(size, self.cursor) + } + fn is_start_of_word(&self, size: Word, pos: usize) -> bool { + if self.grapheme_at(pos).is_some_and(|g| g.chars().all(char::is_whitespace)) { + return false + } + match size { + Word::Big => { + let Some(prev_g) = self.grapheme_at(pos.saturating_sub(1)) else { + return true // We are on the very first grapheme, so it is the start of a word + }; + prev_g.chars().all(char::is_whitespace) + } + Word::Normal => { + let Some(cur_g) = self.grapheme_at(pos) else { + return false // We aren't on a character to begin with + }; + let Some(prev_g) = self.grapheme_at(pos.saturating_sub(1)) else { + return true + }; + is_other_class_or_ws(cur_g, prev_g) + } + } + } + fn is_end_of_word(&self, size: Word, pos: usize) -> bool { + if self.grapheme_at(pos).is_some_and(|g| g.chars().all(char::is_whitespace)) { + return false + } + match size { + Word::Big => { + let Some(next_g) = self.grapheme_at(pos + 1) else { + return false + }; + next_g.chars().all(char::is_whitespace) + } + Word::Normal => { + let Some(cur_g) = self.grapheme_at(pos) else { + return false + }; + let Some(next_g) = self.grapheme_at(pos + 1) else { + return false + }; + is_other_class_or_ws(cur_g, next_g) + } + } + } + pub fn find_word_pos(&self, word: Word, to: To, dir: Direction) -> Option { + let mut pos = self.cursor; + match word { + Word::Big => { + match dir { + Direction::Forward => { + match to { + To::Start => { + if self.on_start_of_word(word) { + pos += 1; + if pos >= self.byte_len() { + return None + } + } + let ws_pos = self.find_from(pos, |c| CharClass::from(c) == CharClass::Whitespace)?; + let word_start = self.find_from(ws_pos, |c| CharClass::from(c) != CharClass::Whitespace)?; + Some(word_start) + } + To::End => { + match self.on_end_of_word(word) { + true => { + pos += 1; + if pos >= self.byte_len() { + return None + } + let word_start = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?; + match self.find_from(word_start, |c| CharClass::from(c) == CharClass::Whitespace) { + Some(n) => Some(n.saturating_sub(1)), // Land on char before whitespace + None => Some(self.byte_len()) // End of buffer + } + } + false => { + match self.find_from(pos, |c| CharClass::from(c) == CharClass::Whitespace) { + Some(n) => Some(n.saturating_sub(1)), // Land on char before whitespace + None => Some(self.byte_len()) // End of buffer + } + } + } + } + } + } + Direction::Backward => { + match to { + To::Start => { + match self.on_start_of_word(word) { + true => { + pos = pos.checked_sub(1)?; + let prev_word_end = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?; + match self.rfind_from(prev_word_end, |c| CharClass::from(c) == CharClass::Whitespace) { + Some(n) => Some(n + 1), // Land on char after whitespace + None => Some(0) // Start of buffer + } + } + false => { + let last_ws = self.rfind_from(pos, |c| CharClass::from(c) == CharClass::Whitespace)?; + let prev_word_end = self.rfind_from(last_ws, |c| CharClass::from(c) != CharClass::Whitespace)?; + match self.rfind_from(prev_word_end, |c| CharClass::from(c) == CharClass::Whitespace) { + Some(n) => Some(n + 1), // Land on char after whitespace + None => Some(0) // Start of buffer + } + } + } + } + To::End => { + if self.on_end_of_word(word) { + pos = pos.checked_sub(1)?; + } + let last_ws = self.rfind_from(pos, |c| CharClass::from(c) == CharClass::Whitespace)?; + let prev_word_end = self.rfind_from(last_ws, |c| CharClass::from(c) != CharClass::Whitespace)?; + Some(prev_word_end) + } + } + } + } + } + Word::Normal => { + match dir { + Direction::Forward => { + match to { + To::Start => { + if self.on_start_of_word(word) { + pos += 1; + if pos >= self.byte_len() { + return None + } + } + let cur_graph = self.grapheme_at(pos)?; + let diff_class_pos = self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph))?; + if let CharClass::Whitespace = CharClass::from(self.grapheme_at(diff_class_pos)?) { + let non_ws_pos = self.find_from(diff_class_pos, |c| CharClass::from(c) != CharClass::Whitespace)?; + Some(non_ws_pos) + } else { + Some(diff_class_pos) + } + } + To::End => { + match self.on_end_of_word(word) { + true => { + pos += 1; + if pos >= self.byte_len() { + return None + } + let cur_graph = self.grapheme_at(pos)?; + match self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph)) { + Some(n) => { + let cur_graph = self.grapheme_at(n)?; + if CharClass::from(cur_graph) == CharClass::Whitespace { + let Some(non_ws_pos) = self.find_from(n, |c| CharClass::from(c) != CharClass::Whitespace) else { + return Some(self.byte_len()) + }; + let cur_graph = self.grapheme_at(non_ws_pos)?; + let Some(word_end_pos) = self.find_from(non_ws_pos, |c| is_other_class_or_ws(c, cur_graph)) else { + return Some(self.byte_len()) + }; + Some(word_end_pos.saturating_sub(1)) + } else { + Some(pos.saturating_sub(1)) + } + } + None => Some(self.byte_len()) // End of buffer + } + } + false => { + let cur_graph = self.grapheme_at(pos)?; + match self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph)) { + Some(n) => Some(n.saturating_sub(1)), // Land on char before other char class + None => Some(self.byte_len()) // End of buffer + } + } + } + } + } + } + Direction::Backward => { + match to { + To::Start => { + if self.on_start_of_word(word) { + pos = pos.checked_sub(1)?; + } + let cur_graph = self.grapheme_at(pos)?; + let diff_class_pos = self.rfind_from(pos, |c| is_other_class_or_ws(c, cur_graph))?; + if let CharClass::Whitespace = self.grapheme_at(diff_class_pos)?.into() { + let prev_word_end = self.rfind_from(diff_class_pos, |c| CharClass::from(c) != CharClass::Whitespace)?; + let cur_graph = self.grapheme_at(prev_word_end)?; + let Some(prev_word_start) = self.rfind_from(prev_word_end, |c| is_other_class_or_ws(c, cur_graph)) else { + return Some(0) + }; + Some(prev_word_start + 1) + } else { + let cur_graph = self.grapheme_at(diff_class_pos)?; + let Some(prev_word_start) = self.rfind_from(diff_class_pos, |c| is_other_class_or_ws(c, cur_graph)) else { + return Some(0) + }; + Some(prev_word_start + 1) + } + } + To::End => { + if self.on_end_of_word(word) { + pos = pos.checked_sub(1)?; + } + let cur_graph = self.grapheme_at(pos)?; + let diff_class_pos = self.rfind_from(pos, |c|is_other_class_or_ws(c, cur_graph))?; + if let CharClass::Whitespace = self.grapheme_at(diff_class_pos)?.into() { + let prev_word_end = self.rfind_from(diff_class_pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(0); + Some(prev_word_end) + } else { + Some(diff_class_pos) + } + } + } + } + } + } + } + } + pub fn find bool>(&self, op: F) -> Option { + self.find_from(self.cursor, op) + } + pub fn rfind bool>(&self, op: F) -> Option { + self.rfind_from(self.cursor, op) + } + + /// Find the first grapheme at or after `pos` for which `op` returns true. + /// Returns the byte index of that grapheme in the buffer. + pub fn find_from bool>(&self, pos: usize, op: F) -> Option { + assert!(is_grapheme_boundary(&self.buffer, pos)); + + // Iterate over grapheme indices starting at `pos` + let slice = &self.slice_from(pos); + for (offset, grapheme) in slice.grapheme_indices(true) { + if op(grapheme) { + return Some(pos + offset); + } + } + None + } + /// Find the last grapheme at or before `pos` for which `op` returns true. + /// Returns the byte index of that grapheme in the buffer. + pub fn rfind_from bool>(&self, pos: usize, op: F) -> Option { + assert!(is_grapheme_boundary(&self.buffer, pos)); + + // Iterate grapheme boundaries backward up to pos + let slice = &self.slice_to(pos); + let graphemes = slice.grapheme_indices(true).rev(); + + for (offset, grapheme) in graphemes { + if op(grapheme) { + return Some(offset); + } + } + None } pub fn eval_motion(&self, motion: Motion) -> MotionKind { match motion { - Motion::WholeLine => MotionKind::range(self.cur_line_range()), - Motion::TextObj(text_obj, bound) => { - let range = self.eval_text_obj(text_obj, bound); - let range = mk_range(range.start, range.end); - let cursor = self.cursor(); - if range.start == cursor && range.end == cursor { - MotionKind::Null - } else { - MotionKind::range(range) - } + Motion::WholeLine => { + let (start,end) = self.this_line(); + MotionKind::range(start..=end) } + Motion::TextObj(text_obj, bound) => todo!(), 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) + let (start,_) = self.this_line(); + let first_graph_pos = self.find_from(start, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(start); + MotionKind::To(first_graph_pos) } - Motion::ToColumn(col) => { - let rng = self.cur_line_range(); - let column = (*rng.start() + (col.saturating_sub(1))).min(*rng.end()); - MotionKind::To(column) + Motion::BeginningOfLine => MotionKind::To(self.this_line().0), + Motion::EndOfLine => MotionKind::To(self.this_line().1), + Motion::BackwardWord(to, word) => { + let Some(pos) = self.find_word_pos(word, to, Direction::Backward) else { + return MotionKind::Null + }; + MotionKind::To(pos) } - Motion::BeginningOfLine => { - let cursor = self.cursor(); - let mut line_start = self.backward_until(cursor, |c| c == &TermChar::Newline, false); - if self.get_char(line_start.saturating_sub(1)).is_some_and(|c| c != &TermChar::Newline) { - line_start = 0; // FIXME: not sure if this logic is correct - } - MotionKind::To(line_start) + Motion::ForwardWord(to, word) => { + let Some(pos) = self.find_word_pos(word, to, Direction::Forward) else { + return MotionKind::Null + }; + MotionKind::To(pos) } - Motion::EndOfLine => { - let cursor = self.cursor(); - let mut line_end = self.forward_until(cursor, |c| c == &TermChar::Newline, false); - // If we didn't actually find a newline, we need to go to the end of the buffer - if self.get_char(line_end + 1).is_some_and(|c| c != &TermChar::Newline) { - line_end = self.buffer.len(); // FIXME: not sure if this logic is correct - } - MotionKind::To(line_end) - } - Motion::BackwardWord(dest, word) => MotionKind::To(self.find_word_pos(word, dest, Direction::Backward)), - Motion::ForwardWord(dest, word) => MotionKind::To(self.find_word_pos(word, dest, Direction::Forward)), Motion::CharSearch(direction, dest, ch) => { - let mut cursor = self.cursor(); - 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), + match direction { + Direction::Forward => { + let Some(pos) = self.slice_from_cursor().find(ch) else { + return MotionKind::Null + }; + match dest { + Dest::On => MotionKind::To(pos), + Dest::Before => MotionKind::To(pos.saturating_sub(1)), + Dest::After => todo!(), } } - }; + Direction::Backward => { + let Some(pos) = self.slice_to_cursor().rfind(ch) else { + return MotionKind::Null + }; + match dest { + Dest::On => MotionKind::To(pos), + Dest::Before => MotionKind::To(pos + 1), + Dest::After => todo!(), + } + } + } - if found_char { - MotionKind::To(stop_pos) - } else { - MotionKind::Null - } - } - Motion::Range(s, e) => { - if self.validate_range(&(s..e)) { - let range = mk_range(s, e); - MotionKind::range(range) - } else { - MotionKind::Null - } } Motion::BackwardChar => MotionKind::Backward(1), Motion::ForwardChar => MotionKind::Forward(1), - Motion::LineUp => { - if self.on_first_line() { - return MotionKind::Null // TODO: implement history scrolling here - } - let col = self.cur_line_col(); - let cursor = self.cursor(); - let mut ln_start = self.backward_until(cursor, |c| c.matches("\n"), true); - let ln_end = ln_start.saturating_sub(1); - ln_start = self.backward_until(ln_end, |c| c.matches("\n"), true); - let new_pos = (ln_start + col).min(ln_end); - MotionKind::To(new_pos) - } + Motion::LineUp => todo!(), Motion::LineDown => todo!(), - Motion::WholeBuffer => MotionKind::Range(0..self.buffer.len().saturating_sub(1)), + Motion::WholeBuffer => todo!(), Motion::BeginningOfBuffer => MotionKind::To(0), - Motion::EndOfBuffer => MotionKind::To(self.buffer.len().saturating_sub(1)), - Motion::Null => MotionKind::Null, - _ => unreachable!(), + Motion::EndOfBuffer => MotionKind::To(self.byte_len()), + Motion::ToColumn(n) => { + let (start,end) = self.this_line(); + let pos = start + n; + if pos > end { + MotionKind::To(end) + } else { + MotionKind::To(pos) + } + } + Motion::Range(_, _) => todo!(), + Motion::Builder(motion_builder) => todo!(), + Motion::RepeatMotion => todo!(), + Motion::RepeatMotionRev => todo!(), + Motion::Null => todo!(), } } pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> { match verb { Verb::Change | Verb::Delete => { - if self.buffer.is_empty() { - return Ok(()) - } 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::(); + let Some(pos) = self.next_pos(n) else { + return Ok(()) + }; + let range = self.cursor..pos; + assert!(range.end < self.byte_len()); + deleted = self.buffer.drain(range); } - 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::To(n) => { + let range = mk_range(self.cursor, n); + assert!(range.end < self.byte_len()); + deleted = self.buffer.drain(range); } 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)); + let Some(back) = self.prev_pos(n) else { + return Ok(()) + }; + let range = back..self.cursor; + dbg!(&range); + deleted = self.buffer.drain(range); } - MotionKind::Range(r) => { - deleted = self.buffer.drain(r.clone()).collect::(); - self.apply_motion(MotionKind::To(r.start)); + MotionKind::Range(range) => { + deleted = self.buffer.drain(range.0..range.1); } - MotionKind::Null => return Ok(()) - } - register.write_to_register(deleted); - } - Verb::DeleteChar(anchor) => { - if self.buffer.is_empty() { - return Ok(()) - } - match anchor { - Anchor::After => { - let pos = self.cursor(); - 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(r) => { - yanked = self.buffer[r.start..r.end] - .iter() - .cloned() - .collect::(); - self.apply_motion(MotionKind::To(r.start)); - } - MotionKind::ToScreenPos(pos) => todo!(), - MotionKind::Null => return Ok(()) - } - register.write_to_register(yanked); - } - Verb::ReplaceChar(ch) => { - let cursor = self.cursor(); - if let Some(c) = self.buffer.get_mut(cursor) { - let mut tc = TermChar::from(ch); - std::mem::swap(c, &mut tc) + MotionKind::Line(n) => { + let (start,end) = match n.cmp(&0) { + Ordering::Less => self.select_lines_up(n.abs() as usize), + Ordering::Equal => self.this_line(), + Ordering::Greater => self.select_lines_down(n as usize) + }; + let range = match verb { + Verb::Change => start..end, + Verb::Delete => start..end.saturating_add(1), + _ => unreachable!() + }; + deleted = self.buffer.drain(range); + } + MotionKind::ToLine(n) => { + let (start,end) = self.select_lines_to(n); + let range = match verb { + Verb::Change => start..end, + Verb::Delete => start..end.saturating_add(1), + _ => unreachable!() + }; + deleted = self.buffer.drain(range); + } + MotionKind::Null => return Ok(()), + MotionKind::ToScreenPos(n) => todo!(), } + register.write_to_register(deleted.collect()); self.apply_motion(motion); } + Verb::DeleteChar(anchor) => { + match anchor { + Anchor::After => { + if self.grapheme_at(self.cursor).is_some() { + self.buffer.remove(self.cursor); + } + } + Anchor::Before => { + if self.grapheme_at(self.cursor.saturating_sub(1)).is_some() { + self.buffer.remove(self.cursor.saturating_sub(1)); + } + } + } + } + Verb::Yank => todo!(), + Verb::ReplaceChar(_) => todo!(), Verb::Substitute => todo!(), Verb::ToggleCase => todo!(), Verb::Complete => todo!(), Verb::CompleteBackward => todo!(), - Verb::Undo => { - let Some(undo) = self.undo_stack.pop() else { - return Ok(()) - }; - flog!(DEBUG, undo); - let Edit { pos, cursor_pos, old, new } = undo; - let start = pos; - let end = pos + new.len(); - self.buffer.0.splice(start..end, old.0.clone()); - let cur_pos = self.cursor(); - self.cursor = cursor_pos; - let redo = Edit { pos, cursor_pos: cur_pos, old: new, new: old }; - flog!(DEBUG, redo); - self.redo_stack.push(redo); - } - Verb::Redo => { - let Some(Edit { pos, cursor_pos, old, new }) = self.redo_stack.pop() else { - return Ok(()) - }; - let start = pos; - let end = pos + new.len(); - self.buffer.0.splice(start..end, old.0.clone()); - let cur_pos = self.cursor(); - self.cursor = cursor_pos; - self.undo_stack.push(Edit { pos, cursor_pos: cur_pos, old: new, new: old }); - } + Verb::Undo => todo!(), + Verb::Redo => 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 { + Verb::Put(anchor) => todo!(), + Verb::InsertModeLineBreak(anchor) => { + match anchor { + Anchor::After => { + let (_,end) = self.this_line(); + self.cursor = end; + self.insert('\n'); self.cursor_fwd(1); - self.insert_at_cursor(char); + } + Anchor::Before => { + let (start,_) = self.this_line(); + self.cursor = start; + self.insert('\n'); } } } Verb::JoinLines => todo!(), Verb::InsertChar(ch) => { - self.insert_at_cursor(ch); + self.insert(ch); self.apply_motion(motion); } Verb::Insert(_) => todo!(), Verb::Breakline(anchor) => todo!(), Verb::Indent => todo!(), Verb::Dedent => todo!(), + Verb::Equalize => todo!(), Verb::AcceptLine => todo!(), - Verb::EndOfFile => { - if self.buffer.is_empty() { - sh_quit(0) - } else { - self.buffer.clear(); - self.cursor = 0; - } - } - Verb::InsertModeLineBreak(anchor) => { - match anchor { - Anchor::After => { - let rng = self.cur_line_range(); - self.apply_motion(MotionKind::To(self.num_or_len(rng.end() + 1))); - self.insert_at_cursor('\n'.into()); - self.apply_motion(MotionKind::Forward(1)); - } - Anchor::Before => todo!(), - } - } - Verb::Equalize => { - } + Verb::Builder(verb_builder) => todo!(), + Verb::EndOfFile => todo!(), + + Verb::OverwriteMode | Verb::InsertMode | Verb::NormalMode | - Verb::VisualMode | - Verb::OverwriteMode => { + Verb::VisualMode => { + /* Already handled */ self.apply_motion(motion); } } Ok(()) } pub fn apply_motion(&mut self, motion: MotionKind) { + dbg!(&motion); 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(r) => self.cursor_to(r.start), // TODO: not sure if this is correct in every case + MotionKind::Forward(n) => { + for _ in 0..n { + if !self.cursor_fwd(1) { + break + } + } + } + MotionKind::Backward(n) => { + for _ in 0..n { + if !self.cursor_back(1) { + break + } + } + } + MotionKind::To(n) => { + assert!((0..=self.byte_len()).contains(&n)); + self.cursor = n + } + MotionKind::Range(range) => { + assert!((0..self.byte_len()).contains(&range.0)); + if self.cursor != range.0 { + self.cursor = range.0 + } + } + MotionKind::Line(n) => { + match n.cmp(&0) { + Ordering::Equal => { + let (start,_) = self.this_line(); + self.cursor = start; + } + Ordering::Less => { + let (start,_) = self.select_lines_up(n.abs() as usize); + self.cursor = start; + } + Ordering::Greater => { + let (_,end) = self.select_lines_down(n.abs() as usize); + self.cursor = end.saturating_sub(1); + let (start,_) = self.this_line(); + self.cursor = start; + } + } + } + MotionKind::ToLine(n) => { + let Some((start,_)) = self.select_line(n) else { + return + }; + self.cursor = start; + } MotionKind::Null => { /* Pass */ } + MotionKind::ToScreenPos(_) => todo!(), } } - pub fn handle_edit(&mut self, old: TermCharBuf, new: TermCharBuf, curs_pos: usize) { + pub fn handle_edit(&mut self, old: String, new: String, curs_pos: usize) { if self.merge_edit { - let mut diff = Edit::diff(old, new, curs_pos); + let diff = Edit::diff(&old, &new, curs_pos); let Some(mut edit) = self.undo_stack.pop() else { self.undo_stack.push(diff); return }; - edit.new.append(&mut diff.new); + edit.new.push_str(&diff.new); self.undo_stack.push(edit); } else { - let diff = Edit::diff(old, new, curs_pos); + let diff = Edit::diff(&old, &new, curs_pos); self.undo_stack.push(diff); } } @@ -1193,7 +1071,7 @@ impl LineBuf { let motion_count = motion.as_ref().map(|m| m.0); let before = self.buffer.clone(); - let cursor_pos = self.cursor(); + let cursor_pos = self.cursor; for _ in 0..verb_count.unwrap_or(1) { for _ in 0..motion_count.unwrap_or(1) { @@ -1215,7 +1093,7 @@ impl LineBuf { self.redo_stack.clear(); } - if before.0 != after.0 && !is_undo_op { + if before != after && !is_undo_op { self.handle_edit(before, after, cursor_pos); } @@ -1262,6 +1140,10 @@ pub fn strip_ansi_codes_and_escapes(s: &str) -> String { out } +pub fn is_grapheme_boundary(s: &str, pos: usize) -> bool { + s.is_char_boundary(pos) && s.grapheme_indices(true).any(|(i,_)| i == pos) +} + fn mk_range(a: usize, b: usize) -> Range { std::cmp::min(a, b)..std::cmp::max(a, b) } diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 320ecfa..993e1b0 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -1,6 +1,6 @@ -use std::{collections::HashMap, sync::Mutex}; +use std::time::Duration; -use linebuf::{strip_ansi_codes_and_escapes, LineBuf, TermCharBuf}; +use linebuf::{strip_ansi_codes_and_escapes, LineBuf}; use mode::{CmdReplay, ViInsert, ViMode, ViNormal}; use term::Terminal; use unicode_width::UnicodeWidthStr; @@ -16,6 +16,11 @@ pub mod vicmd; pub mod mode; pub mod register; +/// Unified interface for different line editing methods +pub trait Readline { + fn readline(&mut self) -> ShResult; +} + pub struct FernVi { term: Terminal, line: LineBuf, @@ -25,91 +30,29 @@ pub struct FernVi { last_movement: Option, } -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"); - Self { - term: Terminal::new(), - line, - prompt, - mode: Box::new(ViInsert::new()), - last_action: None, - last_movement: None, - } - } - pub fn calculate_prompt_offset(&self) -> usize { - if self.prompt.ends_with('\n') { - return 0 - } - strip_ansi_codes_and_escapes(self.prompt.lines().last().unwrap_or_default()).width() - } - pub fn clear_line(&self) { - let prompt_lines = self.prompt.lines().count(); - let last_line_len = strip_ansi_codes_and_escapes(self.prompt.lines().last().unwrap_or_default()).width(); - let buf_lines = if self.prompt.ends_with('\n') { - self.line.count_lines(last_line_len) - } else { - // The prompt does not end with a newline, so one of the buffer's lines overlaps with it - self.line.count_lines(last_line_len).saturating_sub(1) - }; - let total = prompt_lines + buf_lines; - self.term.write_bytes(b"\r\n"); - self.term.write_bytes(format!("\r\x1b[{total}B").as_bytes()); - for _ in 0..total { - self.term.write_bytes(b"\r\x1b[2K\x1b[1A"); - } - self.term.write_bytes(b"\r\x1b[2K"); - } - pub fn print_buf(&self, refresh: bool) { - if refresh { - self.clear_line() - } - let mut prompt_lines = self.prompt.lines().peekable(); - let mut last_line_len = 0; - let lines = self.line.split_lines(); - while let Some(line) = prompt_lines.next() { - if prompt_lines.peek().is_none() { - last_line_len = strip_ansi_codes_and_escapes(line).width(); - self.term.write(line); - } else { +impl Readline for FernVi { + fn readline(&mut self) -> ShResult { + /* + self.term.writeln("This is a line!"); + self.term.writeln("This is a line!"); + self.term.writeln("This is a line!"); + let prompt_thing = "prompt thing -> "; + self.term.write(prompt_thing); + let line = "And another!"; + let mut iters: usize = 0; + let mut newlines_written = 0; + loop { + iters += 1; + for i in 0..iters { self.term.writeln(line); } + std::thread::sleep(Duration::from_secs(1)); + self.clear_lines(iters,prompt_thing.len() + 1); } - let mut lines_iter = lines.into_iter().peekable(); - - let pos = self.term.cursor_pos(); - while let Some(line) = lines_iter.next() { - if lines_iter.peek().is_some() { - self.term.writeln(&line); - } else { - self.term.write(&line); - } - } - self.term.move_cursor_to(pos); - - let (x, y) = self.line.cursor_display_coords(Some(last_line_len)); - - if y > 0 { - self.term.write(&format!("\r\x1b[{}B", y)); - } - - - let cursor_x = if y == 0 { x + last_line_len } else { x }; - - if cursor_x > 0 { - self.term.write(&format!("\r\x1b[{}C", cursor_x)); - } - self.term.write(&self.mode.cursor_style()); - } - pub fn readline(&mut self) -> ShResult { - self.line.set_first_line_offset(self.calculate_prompt_offset()); - let dims = self.term.get_dimensions()?; - self.line.update_term_dims(dims.0, dims.1); - self.print_buf(false); + panic!() + */ + self.print_buf(false)?; loop { - let dims = self.term.get_dimensions()?; - self.line.update_term_dims(dims.0, dims.1); let key = self.term.read_key(); let Some(cmd) = self.mode.handle_key(key) else { @@ -121,9 +64,45 @@ impl FernVi { } self.exec_cmd(cmd.clone())?; - self.print_buf(true); + self.print_buf(true)?; } } +} + +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"); + let term = Terminal::new(); + Self { + term, + line, + prompt, + mode: Box::new(ViInsert::new()), + last_action: None, + last_movement: None, + } + } + pub fn print_buf(&mut self, refresh: bool) -> ShResult<()> { + let (_,width) = self.term.get_dimensions()?; + if refresh { + self.term.unwrite()?; + } + let offset = self.calculate_prompt_offset(); + let mut line_buf = self.prompt.clone(); + line_buf.push_str(self.line.as_str()); + + self.term.recorded_write(&line_buf, offset)?; + self.term.position_cursor(self.line.cursor_display_coords(offset,width))?; + + Ok(()) + } + pub fn calculate_prompt_offset(&self) -> usize { + if self.prompt.ends_with('\n') { + return 0 + } + strip_ansi_codes_and_escapes(self.prompt.lines().last().unwrap_or_default()).width() + 1 // 1 indexed + } pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { if cmd.is_mode_transition() { let count = cmd.verb_count(); diff --git a/src/prompt/readline/mode.rs b/src/prompt/readline/mode.rs index 35806aa..9021275 100644 --- a/src/prompt/readline/mode.rs +++ b/src/prompt/readline/mode.rs @@ -4,7 +4,6 @@ use std::str::Chars; use nix::NixPath; use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; -use super::linebuf::TermChar; use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, MotionCmd, RegisterName, TextObj, To, Verb, VerbBuilder, VerbCmd, ViCmd, Word}; use crate::prelude::*; @@ -72,14 +71,8 @@ impl ViInsert { 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(VerbCmd(1,Verb::InsertChar(ch))); - self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)); - self.register_and_return() - } E(K::Char(ch), M::NONE) => { - self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar(TermChar::from(ch)))); + self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar(ch))); self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)); self.register_and_return() } diff --git a/src/prompt/readline/register.rs b/src/prompt/readline/register.rs index f5eeabe..5fdc12d 100644 --- a/src/prompt/readline/register.rs +++ b/src/prompt/readline/register.rs @@ -1,20 +1,18 @@ use std::sync::Mutex; -use super::linebuf::TermCharBuf; - pub static REGISTERS: Mutex = Mutex::new(Registers::new()); -pub fn read_register(ch: Option) -> Option { +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) { +pub fn write_register(ch: Option, buf: String) { let mut lock = REGISTERS.lock().unwrap(); if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) } } -pub fn append_register(ch: Option, buf: TermCharBuf) { +pub fn append_register(ch: Option, buf: String) { let mut lock = REGISTERS.lock().unwrap(); if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) } } @@ -53,33 +51,33 @@ pub struct Registers { 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![])), + default: Register(String::new()), + a: Register(String::new()), + b: Register(String::new()), + c: Register(String::new()), + d: Register(String::new()), + e: Register(String::new()), + f: Register(String::new()), + g: Register(String::new()), + h: Register(String::new()), + i: Register(String::new()), + j: Register(String::new()), + k: Register(String::new()), + l: Register(String::new()), + m: Register(String::new()), + n: Register(String::new()), + o: Register(String::new()), + p: Register(String::new()), + q: Register(String::new()), + r: Register(String::new()), + s: Register(String::new()), + t: Register(String::new()), + u: Register(String::new()), + v: Register(String::new()), + w: Register(String::new()), + x: Register(String::new()), + y: Register(String::new()), + z: Register(String::new()), } } pub fn get_reg(&self, ch: Option) -> Option<&Register> { @@ -153,17 +151,16 @@ impl Registers { } #[derive(Clone,Default,Debug)] -pub struct Register(TermCharBuf); - +pub struct Register(String); impl Register { - pub fn buf(&self) -> &TermCharBuf { + pub fn buf(&self) -> &String { &self.0 } - pub fn write(&mut self, buf: TermCharBuf) { + pub fn write(&mut self, buf: String) { self.0 = buf } - pub fn append(&mut self, mut buf: TermCharBuf) { - self.0.0.append(&mut buf.0) + pub fn append(&mut self, buf: String) { + self.0.push_str(&buf) } pub fn clear(&mut self) { self.0.clear() diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 67bf0d8..bc1a2b2 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -1,6 +1,7 @@ use std::{os::fd::{BorrowedFd, RawFd}, thread::sleep, time::{Duration, Instant}}; use nix::{errno::Errno, fcntl::{fcntl, FcntlArg, OFlag}, libc::{self, STDIN_FILENO}, sys::termios, unistd::{isatty, read, write}}; use nix::libc::{winsize, TIOCGWINSZ}; +use unicode_width::UnicodeWidthChar; use std::mem::zeroed; use std::io; @@ -8,10 +9,20 @@ use crate::libsh::error::ShResult; use super::keys::{KeyCode, KeyEvent, ModKeys}; +#[derive(Default,Debug)] +struct WriteMap { + lines: usize, + cols: usize, + offset: usize +} + #[derive(Debug)] pub struct Terminal { stdin: RawFd, stdout: RawFd, + recording: bool, + write_records: WriteMap, + cursor_records: WriteMap } impl Terminal { @@ -20,6 +31,13 @@ impl Terminal { Self { stdin: STDIN_FILENO, stdout: 1, + recording: false, + // Records for buffer writes + // Used to find the start of the buffer + write_records: WriteMap::default(), + // Records for cursor movements after writes + // Used to find the end of the buffer + cursor_records: WriteMap::default(), } } @@ -53,15 +71,24 @@ impl Terminal { Ok((ws.ws_row as usize, ws.ws_col as usize)) } - pub fn save_cursor_pos(&self) { + pub fn start_recording(&mut self, offset: usize) { + self.recording = true; + self.write_records.offset = offset; + } + + pub fn stop_recording(&mut self) { + self.recording = false; + } + + pub fn save_cursor_pos(&mut self) { self.write("\x1b[s") } - pub fn restore_cursor_pos(&self) { + pub fn restore_cursor_pos(&mut self) { self.write("\x1b[u") } - pub fn move_cursor_to(&self, (row,col): (usize,usize)) { + pub fn move_cursor_to(&mut self, (row,col): (usize,usize)) { self.write(&format!("\x1b[{row};{col}H",)) } @@ -118,7 +145,7 @@ impl Terminal { return n } Ok(_) => {} - Err(e) if e == Errno::EAGAIN => {} + Err(Errno::EAGAIN) => {} Err(e) => panic!("nonblocking read failed: {e}") } @@ -142,23 +169,126 @@ impl Terminal { 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"); - }); + pub fn reset_records(&mut self) { + self.write_records = Default::default(); + self.cursor_records = Default::default(); + } + + pub fn recorded_write(&mut self, buf: &str, offset: usize) -> ShResult<()> { + self.start_recording(offset); + self.write(buf); + self.stop_recording(); + Ok(()) + } + + pub fn unwrite(&mut self) -> ShResult<()> { + self.unposition_cursor()?; + let WriteMap { lines, cols, offset } = self.write_records; + for _ in 0..lines { + self.write("\x1b[2K\x1b[A") + } + let col = offset; + self.write(&format!("\x1b[{col}G\x1b[0K")); + self.reset_records(); + Ok(()) + } + + pub fn position_cursor(&mut self, (lines,col): (usize,usize)) -> ShResult<()> { + dbg!(self.cursor_pos()); + self.cursor_records.lines = lines; + self.cursor_records.cols = col; + self.cursor_records.offset = self.cursor_pos().1; + + for _ in 0..lines { + self.write("\x1b[A") + } + + self.write(&format!("\x1b[{col}G")); + + dbg!("done moving"); + dbg!(self.cursor_pos()); + + Ok(()) + } + + pub fn unposition_cursor(&mut self) ->ShResult<()> { + dbg!(self.cursor_pos()); + let WriteMap { lines, cols, offset } = self.cursor_records; + + for _ in 0..lines { + self.write("\x1b[B") + } + + self.write(&format!("\x1b[{offset}G")); + + dbg!("done moving back"); + dbg!(self.cursor_pos()); + + Ok(()) + } + + pub fn write_bytes(&mut self, buf: &[u8]) { + if self.recording { + let (_, width) = self.get_dimensions().unwrap(); + let mut bytes = buf.iter().map(|&b| b as char).peekable(); + while let Some(ch) = bytes.next() { + match ch { + '\n' => { + self.write_records.lines += 1; + self.write_records.cols = 0; + } + '\r' => { + self.write_records.cols = 0; + } + // Consume escape sequences + '\x1b' if bytes.peek() == Some(&'[') => { + bytes.next(); + while let Some(&ch) = bytes.peek() { + if ch.is_ascii_alphabetic() { + bytes.next(); + break + } else { + bytes.next(); + } + } + } + '\t' => { + let tab_size = 8; + let next_tab = tab_size - (self.write_records.cols % tab_size); + self.write_records.cols += next_tab; + if self.write_records.cols >= width { + self.write_records.lines += 1; + self.write_records.cols = 0; + } + } + _ if ch.is_control() => { + // ignore control characters for visual width + } + _ => { + let ch_width = ch.width().unwrap_or(0); + if self.write_records.cols + ch_width > width { + self.write_records.lines += 1; + self.write_records.cols = 0; + } + self.write_records.cols += ch_width; + } + } + } + } + write(unsafe { BorrowedFd::borrow_raw(self.stdout) }, buf).expect("Failed to write to stdout"); } - pub fn write(&self, s: &str) { + pub fn write(&mut self, s: &str) { self.write_bytes(s.as_bytes()); } - pub fn writeln(&self, s: &str) { + pub fn writeln(&mut self, s: &str) { self.write(s); - self.write_bytes(b"\r\n"); + self.write_bytes(b"\n"); } - pub fn clear(&self) { + pub fn clear(&mut self) { self.write_bytes(b"\x1b[2J\x1b[H"); } @@ -216,7 +346,7 @@ impl Terminal { KeyEvent(KeyCode::Null, ModKeys::empty()) } - pub fn cursor_pos(&self) -> (usize, usize) { + pub fn cursor_pos(&mut self) -> (usize, usize) { self.write("\x1b[6n"); let mut buf = [0u8;32]; let n = self.read_byte(&mut buf); diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs index f117ec3..8325235 100644 --- a/src/prompt/readline/vicmd.rs +++ b/src/prompt/readline/vicmd.rs @@ -1,4 +1,4 @@ -use super::{linebuf::{TermChar, TermCharBuf}, register::{append_register, read_register, write_register}}; +use super::register::{append_register, read_register, write_register}; #[derive(Clone,Copy,Debug)] pub struct RegisterName { @@ -30,14 +30,14 @@ impl RegisterName { pub fn count(&self) -> usize { self.count } - pub fn write_to_register(&self, buf: TermCharBuf) { + pub fn write_to_register(&self, buf: String) { if self.append { append_register(self.name, buf); } else { write_register(self.name, buf); } } - pub fn read_from_register(&self) -> Option { + pub fn read_from_register(&self) -> Option { read_register(self.name) } } @@ -153,7 +153,7 @@ pub enum Verb { NormalMode, VisualMode, JoinLines, - InsertChar(TermChar), + InsertChar(char), Insert(String), Breakline(Anchor), Indent, @@ -237,7 +237,7 @@ pub enum Motion { /// 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), + CharSearch(Direction,Dest,char), /// backward-char BackwardChar, /// forward-char diff --git a/src/shopt.rs b/src/shopt.rs index e5e7480..3163f94 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -38,8 +38,9 @@ impl Display for FernBellStyle { } } -#[derive(Clone, Copy, Debug)] +#[derive(Default, Clone, Copy, Debug)] pub enum FernEditMode { + #[default] Vi, Emacs }