diff --git a/Cargo.toml b/Cargo.toml index 5d29568..dda9159 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,13 +13,15 @@ debug = true bitflags = "2.8.0" clap = { version = "4.5.38", features = ["derive"] } glob = "0.3.2" -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" +nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] } regex = "1.11.1" unicode-segmentation = "1.12.0" unicode-width = "0.2.0" +[dev-dependencies] +insta = "1.42.2" +pretty_assertions = "1.4.1" + [[bin]] name = "fern" path = "src/fern.rs" diff --git a/src/fern.rs b/src/fern.rs index 3caf949..a405e4e 100644 --- a/src/fern.rs +++ b/src/fern.rs @@ -103,7 +103,7 @@ fn fern_interactive() { .unwrap() .map(|mode| mode.parse::().unwrap_or_default()) .unwrap(); - let input = match prompt::read_line(edit_mode) { + let input = match prompt::readline(edit_mode) { Ok(line) => { readline_err_count = 0; line diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index ba70ca7..ae9cc9c 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -22,7 +22,7 @@ fn get_prompt() -> ShResult { expand_prompt(&prompt) } -pub fn read_line(edit_mode: FernEditMode) -> ShResult { +pub fn readline(edit_mode: FernEditMode) -> ShResult { let prompt = get_prompt()?; let mut reader: Box = match edit_mode { FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?), diff --git a/src/prompt/readline/history.rs b/src/prompt/readline/history.rs index f12898d..99d768f 100644 --- a/src/prompt/readline/history.rs +++ b/src/prompt/readline/history.rs @@ -1,6 +1,6 @@ use std::{env, fmt::{Write,Display}, fs::{self, OpenOptions}, io::Write as IoWrite, path::{Path, PathBuf}, str::FromStr, time::{Duration, SystemTime, UNIX_EPOCH}}; -use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}}; +use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::prelude::*; use super::vicmd::Direction; // surprisingly useful @@ -206,6 +206,10 @@ impl History { &self.entries } + pub fn masked_entries(&self) -> &[HistEntry] { + &self.search_mask + } + pub fn push_empty_entry(&mut self) { } @@ -245,6 +249,7 @@ impl History { } pub fn constrain_entries(&mut self, constraint: SearchConstraint) { + flog!(DEBUG,constraint); let SearchConstraint { kind, term } = constraint; match kind { SearchKind::Prefix => { @@ -273,7 +278,7 @@ impl History { if self.cursor_entry().is_some_and(|ent| ent.is_new() && !ent.command().is_empty()) { let entry = self.hint_entry()?; let prefix = self.cursor_entry()?.command(); - Some(entry.command().strip_prefix(prefix)?.to_string()) + Some(entry.command().to_string()) } else { None } diff --git a/src/prompt/readline/keys.rs b/src/prompt/readline/keys.rs index 6cd784a..b8b1bd0 100644 --- a/src/prompt/readline/keys.rs +++ b/src/prompt/readline/keys.rs @@ -3,7 +3,7 @@ use unicode_segmentation::UnicodeSegmentation; // Credit to Rustyline for the design ideas in this module // https://github.com/kkawakam/rustyline -#[derive(Clone,Debug)] +#[derive(Clone,PartialEq,Eq,Debug)] pub struct KeyEvent(pub KeyCode, pub ModKeys); @@ -92,7 +92,7 @@ impl KeyEvent { } } -#[derive(Clone,Debug)] +#[derive(Clone,PartialEq,Eq,Debug)] pub enum KeyCode { UnknownEscSeq, Backspace, diff --git a/src/prompt/readline/layout.rs b/src/prompt/readline/layout.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index 788fd6b..8417314 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -1,118 +1,54 @@ -use std::{cmp::Ordering, fmt::Display, ops::{Range, RangeBounds}}; +use std::{fmt::Display, ops::{Range, RangeInclusive}}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; -use crate::libsh::{error::ShResult, sys::sh_quit, term::{Style, Styled}}; -use crate::prelude::*; +use super::vicmd::{Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, ViCmd, Word}; +use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*}; -use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, RegisterName, TextObj, To, Verb, ViCmd, Word}; - -#[derive(Debug, PartialEq, Eq)] +#[derive(Default,PartialEq,Eq,Debug,Clone,Copy)] pub enum CharClass { + #[default] Alphanum, Symbol, Whitespace, Other } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MotionKind { - Forward(usize), - To(usize), // Land just before - On(usize), // Land directly on - Before(usize), // Had to make a separate one for char searches, for some reason - Backward(usize), - Range((usize,usize)), - Line(isize), // positive = up line, negative = down line - ToLine(usize), - Null, - - /// Absolute position based on display width of characters - /// Factors in the length of the prompt, and skips newlines - ScreenLine(isize) -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum SelectionAnchor { - Start, - #[default] - End -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SelectionMode { - Char(SelectionAnchor), - Line(SelectionAnchor), - Block(SelectionAnchor) -} - -impl Default for SelectionMode { - fn default() -> Self { - Self::Char(Default::default()) - } -} - -impl SelectionMode { - pub fn anchor(&self) -> &SelectionAnchor { - match self { - SelectionMode::Char(anchor) | - SelectionMode::Line(anchor) | - SelectionMode::Block(anchor) => anchor - } - } - pub fn invert_anchor(&mut self) { - match self { - SelectionMode::Char(anchor) | - SelectionMode::Line(anchor) | - SelectionMode::Block(anchor) => { - *anchor = match anchor { - SelectionAnchor::Start => SelectionAnchor::End, - SelectionAnchor::End => SelectionAnchor::Start - } - } - } - } -} - -impl MotionKind { - pub fn range>(range: R) -> Self { - let start = match range.start_bound() { - std::ops::Bound::Included(&start) => start, - std::ops::Bound::Excluded(&start) => start + 1, - std::ops::Bound::Unbounded => 0 - }; - let end = match range.end_bound() { - std::ops::Bound::Included(&end) => end, - std::ops::Bound::Excluded(&end) => end + 1, - std::ops::Bound::Unbounded => panic!("called range constructor with no upper bound") - }; - if end > start { - Self::Range((start,end)) - } else { - Self::Range((end,start)) - } - } -} - impl From<&str> for CharClass { fn from(value: &str) -> Self { - if value.len() > 1 { - return Self::Symbol // Multi-byte grapheme + let mut chars = value.chars(); + + // Empty string fallback + let Some(first) = chars.next() else { + return Self::Other; + }; + + if first.is_alphanumeric() && chars.all(|c| c.is_ascii_punctuation() || c == '\u{0301}' || c == '\u{0308}') { + // Handles things like `ï`, `é`, etc., by manually allowing common diacritics + return CharClass::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) { + } else if value.chars().all(|c| !c.is_alphanumeric()) { CharClass::Symbol } else { - Self::Other + CharClass::Other } } } +impl From for CharClass { + fn from(value: char) -> Self { + let mut buf = [0u8; 4]; // max UTF-8 char size + let slice = value.encode_utf8(&mut buf); // get str slice + CharClass::from(slice as &str) + } +} + fn is_whitespace(a: &str) -> bool { CharClass::from(a) == CharClass::Whitespace } @@ -123,7 +59,15 @@ fn is_other_class(a: &str, b: &str) -> bool { a != b } -fn is_other_class_or_ws(a: &str, b: &str) -> bool { +fn is_other_class_not_ws(a: &str, b: &str) -> bool { + if is_whitespace(a) || is_whitespace(b) { + false + } else { + is_other_class(a, b) + } +} + +fn is_other_class_or_is_ws(a: &str, b: &str) -> bool { if is_whitespace(a) || is_whitespace(b) { true } else { @@ -131,6 +75,68 @@ fn is_other_class_or_ws(a: &str, b: &str) -> bool { } } +#[derive(Default,Clone,Copy,PartialEq,Eq,Debug)] +pub enum SelectAnchor { + #[default] + End, + Start +} + +#[derive(Clone,Copy,PartialEq,Eq,Debug)] +pub enum SelectMode { + Char(SelectAnchor), + Line(SelectAnchor), + Block(SelectAnchor), +} + +impl SelectMode { + pub fn anchor(&self) -> &SelectAnchor { + match self { + SelectMode::Char(anchor) | + SelectMode::Line(anchor) | + SelectMode::Block(anchor) => anchor + } + } + pub fn invert_anchor(&mut self) { + match self { + SelectMode::Char(anchor) | + SelectMode::Line(anchor) | + SelectMode::Block(anchor) => { + *anchor = match anchor { + SelectAnchor::Start => SelectAnchor::End, + SelectAnchor::End => SelectAnchor::Start + } + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MotionKind { + To(usize), // Absolute position, exclusive + On(usize), // Absolute position, inclusive + Onto(usize), // Absolute position, operations include the position but motions exclude it (wtf vim) + Inclusive((usize,usize)), // Range, inclusive + Exclusive((usize,usize)), // Range, exclusive + + // Used for linewise operations like 'dj', left is the selected range, right is the cursor's new position on the line + InclusiveWithTargetCol((usize,usize),usize), + ExclusiveWithTargetCol((usize,usize),usize), + Null +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MotionRange {} + +impl MotionKind { + pub fn inclusive(range: RangeInclusive) -> Self { + Self::Inclusive((*range.start(),*range.end())) + } + pub fn exclusive(range: Range) -> Self { + Self::Exclusive((range.start,range.end)) + } +} + #[derive(Default,Debug)] pub struct Edit { pub pos: usize, @@ -195,1815 +201,413 @@ impl Edit { } } +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)] +/// A usize which will always exist between 0 and a given upper bound +/// +/// * The upper bound can be both inclusive and exclusive +/// * Used for the LineBuf cursor to enforce the `0 <= cursor < self.buffer.len()` invariant. +pub struct ClampedUsize { + value: usize, + max: usize, + exclusive: bool +} + +impl ClampedUsize { + pub fn new(value: usize, max: usize, exclusive: bool) -> Self { + let mut c = Self { value: 0, max, exclusive }; + c.set(value); + c + } + pub fn get(self) -> usize { + self.value + } + pub fn upper_bound(&self) -> usize { + if self.exclusive { + self.max.saturating_sub(1) + } else { + self.max + } + } + /// Increment the ClampedUsize value + /// + /// Returns false if the attempted increment is rejected by the clamp + pub fn inc(&mut self) -> bool { + let max = self.upper_bound(); + if self.value == max { + return false; + } + self.add(1); + true + } + /// Decrement the ClampedUsize value + /// + /// Returns false if the attempted decrement would cause underflow + pub fn dec(&mut self) -> bool { + if self.value == 0 { + return false; + } + self.sub(1); + true + } + pub fn set(&mut self, value: usize) { + let max = self.upper_bound(); + self.value = value.clamp(0,max); + } + pub fn set_max(&mut self, max: usize) { + self.max = max; + self.set(self.get()); // Enforces the new maximum + } + pub fn add(&mut self, value: usize) { + let max = self.upper_bound(); + self.value = (self.value + value).clamp(0,max) + } + pub fn sub(&mut self, value: usize) { + self.value = self.value.saturating_sub(value) + } + /// Add a value to the wrapped usize, return the result + /// + /// Returns the result instead of mutating the inner value + pub fn ret_add(&self, value: usize) -> usize { + let max = self.upper_bound(); + (self.value + value).clamp(0,max) + } + /// Add a value to the wrapped usize, forcing inclusivity + pub fn ret_add_inclusive(&self, value: usize) -> usize { + let max = self.max; + (self.value + value).clamp(0,max) + } + /// Subtract a value from the wrapped usize, return the result + /// + /// Returns the result instead of mutating the inner value + pub fn ret_sub(&self, value: usize) -> usize { + self.value.saturating_sub(value) + } +} + #[derive(Default,Debug)] pub struct LineBuf { - buffer: String, - hint: Option, - cursor: usize, - clamp_cursor: bool, - select_mode: Option, - selected_range: Option>, - last_selected_range: Option>, - first_line_offset: usize, - saved_col: Option, - term_dims: (usize,usize), // Height, width - move_cursor_on_undo: bool, - undo_stack: Vec, - redo_stack: Vec, - tab_stop: usize + pub buffer: String, + pub hint: Option, + pub grapheme_indices: Option>, // Used to slice the buffer + pub cursor: ClampedUsize, // Used to index grapheme_indices + + pub select_mode: Option, + pub select_range: Option<(usize,usize)>, + pub last_selection: Option<(usize,usize)>, + + pub insert_mode_start_pos: Option, + pub saved_col: Option, + + pub undo_stack: Vec, + pub redo_stack: Vec, } impl LineBuf { pub fn new() -> Self { - Self { tab_stop: 8, ..Default::default() } + Self::default() } - pub fn with_initial(mut self, initial: &str) -> Self { - self.buffer = initial.to_string(); - self - } - pub fn selected_range(&self) -> Option<&Range> { - self.selected_range.as_ref() - } - pub fn is_selecting(&self) -> bool { - self.select_mode.is_some() - } - pub fn stop_selecting(&mut self) { - self.select_mode = None; - if self.selected_range().is_some() { - self.last_selected_range = self.selected_range.take(); + /// Only update self.grapheme_indices if it is None + pub fn update_graphemes_lazy(&mut self) { + if self.grapheme_indices.is_none() { + self.update_graphemes(); } } - pub fn start_selecting(&mut self, mode: SelectionMode) { - self.select_mode = Some(mode); - self.selected_range = Some(self.cursor..(self.cursor + 1).min(self.byte_len().saturating_sub(1))) + pub fn with_initial(mut self, buffer: &str, cursor: usize) -> Self { + self.buffer = buffer.to_string(); + self.update_graphemes(); + self.cursor = ClampedUsize::new(cursor, self.grapheme_indices().len(), self.cursor.exclusive); + self + } + pub fn take_buf(&mut self) -> String { + std::mem::take(&mut self.buffer) } pub fn has_hint(&self) -> bool { self.hint.is_some() } + pub fn hint(&self) -> Option<&String> { + self.hint.as_ref() + } pub fn set_hint(&mut self, hint: Option) { - self.hint = hint - } - 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 saved_col(&self) -> Option { - self.saved_col - } - pub fn update_term_dims(&mut self, dims: (usize,usize)) { - self.term_dims = dims - } - 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 at_end_of_buffer(&self) -> bool { - if self.clamp_cursor { - self.cursor == self.byte_len().saturating_sub(1) - } else { - self.cursor == self.byte_len() - } - } - pub fn undos(&self) -> usize { - self.undo_stack.len() - } - pub fn is_empty(&self) -> bool { - self.buffer.is_empty() - } - pub fn set_move_cursor_on_undo(&mut self, yn: bool) { - self.move_cursor_on_undo = yn; - } - 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 - if self.cursor == self.byte_len() || self.grapheme_at_cursor() == Some("\n") { - self.cursor_back(1); - } - } - pub fn clamp_range(&self, range: Range) -> Range { - let (mut start,mut end) = (range.start,range.end); - start = start.max(0); - end = end.min(self.byte_len()); - start..end - } - pub fn grapheme_len(&self) -> usize { - self.buffer.grapheme_indices(true).count() - } - pub fn slice_from_cursor(&self) -> &str { - if let Some(slice) = &self.buffer.get(self.cursor..) { - slice - } else { - "" - } - } - pub fn slice_to_cursor(&self) -> &str { - if let Some(slice) = self.buffer.get(..self.cursor) { - slice - } else { - &self.buffer - } - - } - pub fn into_line(self) -> String { - 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 g_idx_to_byte_pos(&self, pos: usize) -> Option { - if pos >= self.byte_len() { - None - } else { - self.buffer.grapheme_indices(true).map(|(i,_)| i).nth(pos) - } - } - pub fn grapheme_at_cursor(&self) -> Option<&str> { - if self.cursor == self.byte_len() { - None - } else { - self.slice_from_cursor().graphemes(true).next() - } - } - pub fn grapheme_at_cursor_offset(&self, offset: isize) -> Option<&str> { - match offset.cmp(&0) { - Ordering::Equal => { - self.grapheme_at(self.cursor) + if let Some(hint) = hint { + let hint = hint.strip_prefix(&self.buffer).unwrap(); // If this ever panics, I will eat my hat + if !hint.is_empty() { + self.hint = Some(hint.to_string()) } - 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) - } - } - } - pub fn grapheme_at(&self, pos: usize) -> Option<&str> { - if pos >= self.byte_len() { - None } else { - self.buffer.graphemes(true).nth(pos) + self.hint = None } - } - 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 sync_cursor(&mut self) { - if !self.buffer.is_char_boundary(self.cursor) { - self.cursor = self.prev_pos(1).unwrap_or(0) - } - } - pub fn cursor_back(&mut self, dist: usize) -> bool { - let Some(pos) = self.prev_pos(dist) else { - return false - }; - self.cursor = pos; - true - } - /// Constrain the cursor to the current line - pub fn cursor_back_confined(&mut self, dist: usize) -> bool { - for _ in 0..dist { - let Some(pos) = self.prev_pos(1) else { - return false - }; - if let Some("\n") = self.grapheme_at(pos) { - return false - } - if !self.cursor_back(1) { - return false - } - } - true - } - pub fn cursor_fwd_confined(&mut self, dist: usize) -> bool { - for _ in 0..dist { - let Some(pos) = self.next_pos(1) else { - return false - }; - if let Some("\n") = self.grapheme_at(pos) { - return false - } - if !self.cursor_fwd(1) { - return false - } - } - 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 - } - - fn compute_display_positions<'a>( - text: impl Iterator, - start_col: usize, - tab_stop: usize, - term_width: usize, - ) -> (usize, usize) { - let mut lines = 0; - let mut col = start_col; - - for grapheme in text { - match grapheme { - "\n" => { - lines += 1; - col = 1; - } - "\t" => { - let spaces_to_next_tab = tab_stop - (col % tab_stop); - if col + spaces_to_next_tab > term_width { - lines += 1; - col = 1; - } else { - col += spaces_to_next_tab; - } - - // Don't ask why this is here - // I don't know either - // All I know is that it only finds the correct cursor position - // if i add one to the column here, for literally no reason - // Thank you linux terminal :) - col += 1; - } - _ => { - col += grapheme.width(); - if col > term_width { - lines += 1; - col = 1; - } - } - } - } - if col == term_width { - lines += 1; - // Don't ask why col has to be set to zero here but one everywhere else - // I don't know either - // All I know is that it only finds the correct cursor position - // if I set col to 0 here, and 1 everywhere else - // Thank you linux terminal :) - col = 0; - } - - (lines, col) - } - pub fn count_display_lines(&self, offset: usize, term_width: usize) -> usize { - let (lines, _) = Self::compute_display_positions( - self.buffer.graphemes(true), - offset, - self.tab_stop, - term_width, - ); - lines - } - - pub fn cursor_display_line_position(&self, offset: usize, term_width: usize) -> usize { - let (lines, _) = Self::compute_display_positions( - self.slice_to_cursor().graphemes(true), - offset, - self.tab_stop, - term_width, - ); - lines - } - - pub fn display_coords(&self, term_width: usize) -> (usize, usize) { - Self::compute_display_positions( - self.slice_to_cursor().graphemes(true), - 0, - self.tab_stop, - term_width, - ) - } - - pub fn cursor_display_coords(&self, term_width: usize) -> (usize, usize) { - let (d_line, mut d_col) = self.display_coords(term_width); - let total_lines = self.count_display_lines(0, term_width); - let is_first_line = self.start_of_line() == 0; - let mut logical_line = total_lines - d_line; - - if is_first_line { - d_col += self.first_line_offset; - if d_col > term_width { - logical_line = logical_line.saturating_sub(1); - d_col -= term_width; - } - } - - (logical_line, d_col) - } - 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 move_to(&mut self, pos: usize) -> bool { - if self.cursor == pos { - false - } else { - self.cursor = pos; - true - } - } - pub fn move_buf_start(&mut self) -> bool { - self.move_to(0) - } - pub fn move_buf_end(&mut self) -> bool { - if self.clamp_cursor { - self.move_to(self.byte_len().saturating_sub(1)) - } else { - self.move_to(self.byte_len()) - } - } - pub fn move_home(&mut self) -> bool { - let start = self.start_of_line(); - self.move_to(start) - } - pub fn move_end(&mut self) -> bool { - let end = self.end_of_line(); - self.move_to(end) - } - /// Consume the LineBuf and return the buffer - pub fn pack_line(self) -> String { - self.buffer + flog!(DEBUG,self.hint) } pub fn accept_hint(&mut self) { - if let Some(hint) = self.hint.take() { - let old_buf = self.buffer.clone(); - self.buffer.push_str(&hint); - let new_buf = self.buffer.clone(); - self.handle_edit(old_buf, new_buf, self.cursor); - self.move_buf_end(); - } + let Some(hint) = self.hint.take() else { return }; + + let old = self.buffer.clone(); + let cursor_pos = self.cursor.get(); + self.push_str(&hint); + let new = self.as_str(); + let edit = Edit::diff(&old, new, cursor_pos); + self.undo_stack.push(edit); + self.cursor.add(hint.len()); } - pub fn accept_hint_partial(&mut self, accept_to: usize) { - if let Some(hint) = self.hint.take() { - let accepted = &hint[..accept_to]; - let remainder = &hint[accept_to..]; - self.buffer.push_str(accepted); - self.hint = Some(remainder.to_string()); - } + pub fn set_cursor_clamp(&mut self, yn: bool) { + self.cursor.exclusive = yn; } - /// If we have a hint, then motions are able to extend into it - /// and partially accept pieces of it, instead of the whole thing - pub fn apply_motion_with_hint(&mut self, motion: MotionKind) { - let buffer_end = self.byte_len().saturating_sub(1); - flog!(DEBUG,self.hint); - if let Some(hint) = self.hint.take() { - self.buffer.push_str(&hint); - flog!(DEBUG,motion); - self.apply_motion(/*forced*/ true, motion); - flog!(DEBUG, self.cursor); - flog!(DEBUG, self.grapheme_at_cursor()); - if self.cursor > buffer_end { - let remainder = if self.clamp_cursor { - self.slice_from((self.cursor + 1).min(self.byte_len())) - } else { - self.slice_from_cursor() - }; - flog!(DEBUG,remainder); - if !remainder.is_empty() { - self.hint = Some(remainder.to_string()); - } - let buffer = if self.clamp_cursor { - self.slice_to((self.cursor + 1).min(self.byte_len())) - } else { - self.slice_to_cursor() - }; - flog!(DEBUG,buffer); - self.buffer = buffer.to_string(); - flog!(DEBUG,self.hint); + pub fn cursor_byte_pos(&mut self) -> usize { + self.index_byte_pos(self.cursor.get()) + } + pub fn index_byte_pos(&mut self, index: usize) -> usize { + self.update_graphemes_lazy(); + self.grapheme_indices() + .get(index) + .copied() + .unwrap_or(self.buffer.len()) + } + pub fn read_idx_byte_pos(&self, index: usize) -> usize { + self.grapheme_indices() + .get(index) + .copied() + .unwrap_or(self.buffer.len()) + } + /// Update self.grapheme_indices with the indices of the current buffer + #[track_caller] + pub fn update_graphemes(&mut self) { + let indices: Vec<_> = self.buffer + .grapheme_indices(true) + .map(|(i,_)| i) + .collect(); + flog!(DEBUG,std::panic::Location::caller()); + self.cursor.set_max(indices.len()); + self.grapheme_indices = Some(indices) + } + pub fn grapheme_indices(&self) -> &[usize] { + self.grapheme_indices.as_ref().unwrap() + } + pub fn grapheme_indices_owned(&self) -> Vec { + self.grapheme_indices.as_ref().cloned().unwrap_or_default() + } + pub fn grapheme_at(&mut self, pos: usize) -> Option<&str> { + self.update_graphemes_lazy(); + let indices = self.grapheme_indices(); + let start = indices.get(pos).copied()?; + let end = indices.get(pos + 1).copied().or_else(|| { + if pos + 1 == self.grapheme_indices().len() { + Some(self.buffer.len()) } else { - let old_hint = self.slice_from(buffer_end + 1); - flog!(DEBUG,old_hint); - self.hint = Some(old_hint.to_string()); - let buffer = self.slice_to(buffer_end + 1); - flog!(DEBUG,buffer); - self.buffer = buffer.to_string(); + None } + })?; + self.buffer.get(start..end) + } + pub fn grapheme_at_cursor(&mut self) -> Option<&str> { + self.grapheme_at(self.cursor.get()) + } + pub fn mark_insert_mode_start_pos(&mut self) { + self.insert_mode_start_pos = Some(self.cursor.get()) + } + pub fn clear_insert_mode_start_pos(&mut self) { + self.insert_mode_start_pos = None + } + pub fn slice(&mut self, range: Range) -> Option<&str> { + self.update_graphemes_lazy(); + let start_index = self.grapheme_indices().get(range.start).copied()?; + let end_index = self.grapheme_indices().get(range.end).copied().or_else(|| { + if range.end == self.grapheme_indices().len() { + Some(self.buffer.len()) + } else { + None + } + })?; + self.buffer.get(start_index..end_index) + } + pub fn slice_inclusive(&mut self, range: RangeInclusive) -> Option<&str> { + self.update_graphemes_lazy(); + let start_index = self.grapheme_indices().get(*range.start()).copied()?; + let end_index = self.grapheme_indices().get(*range.end()).copied().or_else(|| { + if *range.end() == self.grapheme_indices().len() { + Some(self.buffer.len()) + } else { + None + } + })?; + self.buffer.get(start_index..end_index) + } + pub fn slice_to(&mut self, end: usize) -> Option<&str> { + self.update_graphemes_lazy(); + let grapheme_index = self.grapheme_indices().get(end).copied().or_else(|| { + if end == self.grapheme_indices().len() { + Some(self.buffer.len()) + } else { + None + } + })?; + self.buffer.get(..grapheme_index) + } + pub fn slice_from(&mut self, start: usize) -> Option<&str> { + self.update_graphemes_lazy(); + let grapheme_index = *self.grapheme_indices().get(start)?; + self.buffer.get(grapheme_index..) + } + pub fn slice_to_cursor(&mut self) -> Option<&str> { + self.slice_to(self.cursor.get()) + } + pub fn slice_to_cursor_inclusive(&mut self) -> Option<&str> { + self.slice_to(self.cursor.ret_add(1)) + } + pub fn slice_from_cursor(&mut self) -> Option<&str> { + self.slice_from(self.cursor.get()) + } + pub fn remove(&mut self, pos: usize) { + let idx = self.index_byte_pos(pos); + self.buffer.remove(idx); + self.update_graphemes(); + } + pub fn drain(&mut self, start: usize, end: usize) -> String { + let drained = if end == self.grapheme_indices().len() { + if start == self.grapheme_indices().len() { + return String::new() + } + let start = self.grapheme_indices()[start]; + self.buffer.drain(start..).collect() + } else { + let start = self.grapheme_indices()[start]; + let end = self.grapheme_indices()[end]; + self.buffer.drain(start..end).collect() + }; + flog!(DEBUG,drained); + self.update_graphemes(); + drained + } + pub fn push(&mut self, ch: char) { + self.buffer.push(ch); + self.update_graphemes(); + } + pub fn push_str(&mut self, slice: &str) { + self.buffer.push_str(slice); + self.update_graphemes(); + } + pub fn insert_at_cursor(&mut self, ch: char) { + self.insert_at(self.cursor.get(), ch); + } + pub fn insert_at(&mut self, pos: usize, ch: char) { + let pos = self.index_byte_pos(pos); + self.buffer.insert(pos, ch); + self.update_graphemes(); + } + pub fn set_buffer(&mut self, buffer: String) { + self.buffer = buffer; + self.update_graphemes(); + } + pub fn select_range(&self) -> Option<(usize,usize)> { + self.select_range + } + pub fn start_selecting(&mut self, mode: SelectMode) { + self.select_mode = Some(mode); + let range_start = self.cursor; + let mut range_end = self.cursor; + range_end.add(1); + self.select_range = Some((range_start.get(),range_end.get())); + } + pub fn stop_selecting(&mut self) { + self.select_mode = None; + if self.select_range.is_some() { + self.last_selection = self.select_range.take(); } } - pub fn find_prev_line_pos(&mut self) -> Option { + pub fn total_lines(&mut self) -> usize { + self.buffer + .graphemes(true) + .filter(|g| *g == "\n") + .count() + } + pub fn cursor_line_number(&mut self) -> usize { + self.slice_to_cursor() + .map(|slice| { + slice.graphemes(true) + .filter(|g| *g == "\n") + .count() + }).unwrap_or(0) + } + pub fn nth_next_line(&mut self, n: usize) -> Option<(usize,usize)> { + let line_no = self.cursor_line_number() + n; + if line_no > self.total_lines() { + return None + } + Some(self.line_bounds(line_no)) + } + pub fn nth_prev_line(&mut self, n: usize) -> Option<(usize,usize)> { + let cursor_line_no = self.cursor_line_number(); + if cursor_line_no == 0 { + return None + } + let line_no = cursor_line_no.saturating_sub(n); + if line_no > self.total_lines() { + return None + } + Some(self.line_bounds(line_no)) + } + pub fn this_line(&mut self) -> (usize,usize) { + let line_no = self.cursor_line_number(); + self.line_bounds(line_no) + } + pub fn start_of_line(&mut self) -> usize { + self.this_line().0 + } + pub fn end_of_line(&mut self) -> usize { + self.this_line().1 + } + pub fn select_lines_up(&mut self, n: usize) -> Option<(usize,usize)> { if self.start_of_line() == 0 { return None - }; - let col = self.saved_col.unwrap_or(self.cursor_column()); - let line = self.line_no(); - if self.saved_col.is_none() { - self.saved_col = Some(col); } - let (start,end) = self.select_line(line - 1).unwrap(); - Some((start + col).min(end.saturating_sub(1))) - } - pub fn find_next_line_pos(&mut self) -> Option { - if self.end_of_line() == self.byte_len() { - return None - }; - let col = self.saved_col.unwrap_or(self.cursor_column()); - let line = self.line_no(); - if self.saved_col.is_none() { - self.saved_col = Some(col); - } - let (start,end) = self.select_line(line + 1).unwrap(); - Some((start + col).min(end.saturating_sub(1))) - } - pub fn cursor_column(&self) -> usize { - let line_start = self.start_of_line(); - self.buffer[line_start..self.cursor].graphemes(true).count() - } - 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 prev_line(&self, offset: usize) -> (usize,usize) { - let (start,_) = self.select_lines_up(offset); - let end = self.slice_from_cursor().find('\n').unwrap_or(self.byte_len()); - (start,end) - } - pub fn next_line(&self, offset: usize) -> Option<(usize,usize)> { - if self.this_line().1 == self.byte_len() { - return None - } - let (_,mut end) = self.select_lines_down(offset); - end = end.min(self.byte_len().saturating_sub(1)); - let start = self.slice_to(end + 1).rfind('\n').unwrap_or(0); + let target_line = self.cursor_line_number().saturating_sub(n); + let end = self.end_of_line(); + let (start,_) = self.line_bounds(target_line); + Some((start,end)) } - 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 { - let slice = self.slice_to(start - 1); - if let Some(prev_newline) = slice.rfind('\n') { - start = prev_newline; - } 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 { - let next_ln_start = end + 1; - if next_ln_start >= self.byte_len() { - end = self.byte_len(); - break - } - if let Some(next_newline) = self.slice_from(next_ln_start).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 eval_text_object(&self, obj: TextObj, bound: Bound) -> Option> { - flog!(DEBUG, obj); - flog!(DEBUG, bound); - match obj { - TextObj::Word(word) => { - match word { - Word::Big => match bound { - Bound::Inside => { - let start = self.rfind(is_whitespace) - .map(|pos| pos+1) - .unwrap_or(0); - let end = self.find(is_whitespace) - .map(|pos| pos-1) - .unwrap_or(self.byte_len()); - Some(start..end) - } - Bound::Around => { - let start = self.rfind(is_whitespace) - .map(|pos| pos+1) - .unwrap_or(0); - let mut end = self.find(is_whitespace) - .unwrap_or(self.byte_len()); - if end != self.byte_len() { - end = self.find_from(end,|c| !is_whitespace(c)) - .map(|pos| pos-1) - .unwrap_or(self.byte_len()) - } - Some(start..end) - } - } - Word::Normal => match bound { - Bound::Inside => { - let cur_graph = self.grapheme_at_cursor()?; - let start = self.rfind(|c| is_other_class(c, cur_graph)) - .map(|pos| pos+1) - .unwrap_or(0); - let end = self.find(|c| is_other_class(c, cur_graph)) - .map(|pos| pos-1) - .unwrap_or(self.byte_len()); - Some(start..end) - } - Bound::Around => { - let cur_graph = self.grapheme_at_cursor()?; - let start = self.rfind(|c| is_other_class(c, cur_graph)) - .map(|pos| pos+1) - .unwrap_or(0); - let mut end = self.find(|c| is_other_class(c, cur_graph)) - .unwrap_or(self.byte_len()); - if end != self.byte_len() && self.is_whitespace(end) { - end = self.find_from(end,|c| !is_whitespace(c)) - .map(|pos| pos-1) - .unwrap_or(self.byte_len()) - } else { - end -= 1; - } - Some(start..end) - } - } - } - } - TextObj::Line => todo!(), - TextObj::Sentence => todo!(), - TextObj::Paragraph => todo!(), - TextObj::DoubleQuote => todo!(), - TextObj::SingleQuote => todo!(), - TextObj::BacktickQuote => todo!(), - TextObj::Paren => todo!(), - TextObj::Bracket => todo!(), - TextObj::Brace => todo!(), - TextObj::Angle => todo!(), - TextObj::Tag => todo!(), - TextObj::Custom(_) => todo!(), - } - } - pub fn get_screen_line_positions(&self) -> Vec { - let (start,end) = self.this_line(); - let mut screen_starts = vec![start]; - let line = &self.buffer[start..end]; - let term_width = self.term_dims.1; - let mut col = 1; - if start == 0 { - col = self.first_line_offset - } - - for (byte, grapheme) in line.grapheme_indices(true) { - let width = grapheme.width(); - if col + width > term_width { - screen_starts.push(start + byte); - col = width; - } else { - col += width; - } - } - - screen_starts - } - pub fn start_of_screen_line(&self) -> usize { - let screen_starts = self.get_screen_line_positions(); - let mut screen_start = screen_starts[0]; - let start_of_logical_line = self.start_of_line(); - flog!(DEBUG,screen_starts); - flog!(DEBUG,self.cursor); - - for (i,pos) in screen_starts.iter().enumerate() { - if *pos > self.cursor { - break - } else { - screen_start = screen_starts[i]; - } - } - if screen_start != start_of_logical_line { - screen_start += 1; // FIXME: doesn't account for grapheme bounds - } - screen_start - } - pub fn this_screen_line(&self) -> (usize,usize) { - let screen_starts = self.get_screen_line_positions(); - let mut screen_start = screen_starts[0]; - let mut screen_end = self.end_of_line().saturating_sub(1); - let start_of_logical_line = self.start_of_line(); - flog!(DEBUG,screen_starts); - flog!(DEBUG,self.cursor); - - for (i,pos) in screen_starts.iter().enumerate() { - if *pos > self.cursor { - screen_end = screen_starts[i].saturating_sub(1); - break; - } else { - screen_start = screen_starts[i]; - } - } - if screen_start != start_of_logical_line { - screen_start += 1; // FIXME: doesn't account for grapheme bounds - } - (screen_start,screen_end) - } - pub fn find_word_pos(&self, word: Word, to: To, dir: Direction) -> Option { - // FIXME: This uses a lot of hardcoded +1/-1 offsets, but they need to account for grapheme boundaries - let mut pos = self.cursor; - match word { - Word::Big => { - match dir { - Direction::Forward => { - match to { - To::Start => { - if self.on_whitespace() { - return self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) - } - if self.on_start_of_word(word) { - pos += 1; - if pos >= self.byte_len() { - return Some(self.byte_len()) - } - } - let Some(ws_pos) = self.find_from(pos, |c| CharClass::from(c) == CharClass::Whitespace) else { - return Some(self.byte_len()) - }; - let word_start = self.find_from(ws_pos, |c| CharClass::from(c) != CharClass::Whitespace)?; - Some(word_start) - } - To::End => { - if self.on_whitespace() { - let Some(non_ws_pos) = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else { - return Some(self.byte_len()) - }; - pos = non_ws_pos - } - match self.on_end_of_word(word) { - true => { - pos += 1; - if pos >= self.byte_len() { - return Some(self.byte_len()) - } - 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 => { - if self.on_whitespace() { - let Some(non_ws_pos) = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else { - return Some(0) - }; - pos = non_ws_pos - } - match self.on_start_of_word(word) { - true => { - pos = pos.checked_sub(1)?; - let Some(prev_word_end) = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else { - return Some(0) - }; - 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 => { - match self.rfind_from(pos, |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_whitespace() { - return Some(self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(0)) - } - if self.on_end_of_word(word) { - pos = pos.checked_sub(1)?; - } - let Some(last_ws) = self.rfind_from(pos, |c| CharClass::from(c) == CharClass::Whitespace) else { - return Some(0) - }; - let Some(prev_word_end) = self.rfind_from(last_ws, |c| CharClass::from(c) != CharClass::Whitespace) else { - return Some(0) - }; - Some(prev_word_end) - } - } - } - } - } - Word::Normal => { - match dir { - Direction::Forward => { - match to { - To::Start => { - if self.on_whitespace() { - return Some(self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(self.byte_len())) - } - if self.on_start_of_word(word) { - let cur_char_class = CharClass::from(self.grapheme_at_cursor()?); - pos += 1; - if pos >= self.byte_len() { - return Some(self.byte_len()) - } - let next_char = self.grapheme_at(self.next_pos(1)?)?; - let next_char_class = CharClass::from(next_char); - if cur_char_class != next_char_class && next_char_class != CharClass::Whitespace { - return Some(pos) - } - } - let cur_graph = self.grapheme_at(pos)?; - let Some(diff_class_pos) = self.find_from(pos, |c| is_other_class_or_ws(c, cur_graph)) else { - return Some(self.byte_len()) - }; - 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 => { - flog!(DEBUG,self.buffer); - if self.on_whitespace() { - let Some(non_ws_pos) = self.find_from(pos, |c| CharClass::from(c) != CharClass::Whitespace) else { - return Some(self.byte_len()) - }; - pos = non_ws_pos - } - match self.on_end_of_word(word) { - true => { - flog!(DEBUG, "on end of word"); - let cur_char_class = CharClass::from(self.grapheme_at_cursor()?); - pos += 1; - if pos >= self.byte_len() { - return Some(self.byte_len()) - } - let next_char = self.grapheme_at(self.next_pos(1)?)?; - let next_char_class = CharClass::from(next_char); - if cur_char_class != next_char_class && next_char_class != CharClass::Whitespace { - let Some(end_pos) = self.find_from(pos, |c| is_other_class_or_ws(c, next_char)) else { - return Some(self.byte_len()) - }; - pos = end_pos.saturating_sub(1); - return Some(pos) - } - - 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 => { - flog!(DEBUG, "not on end of word"); - let cur_graph = self.grapheme_at(pos)?; - flog!(DEBUG,cur_graph); - 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_whitespace() { - pos = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?; - } - match self.on_start_of_word(word) { - true => { - pos = pos.checked_sub(1)?; - let cur_char_class = CharClass::from(self.grapheme_at_cursor()?); - let prev_char = self.grapheme_at(self.prev_pos(1)?)?; - let prev_char_class = CharClass::from(prev_char); - let is_diff_class = cur_char_class != prev_char_class && prev_char_class != CharClass::Whitespace; - if is_diff_class && self.is_start_of_word(Word::Normal, self.prev_pos(1)?) { - return Some(pos) - } - let prev_word_end = self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace)?; - let cur_graph = self.grapheme_at(prev_word_end)?; - match self.rfind_from(prev_word_end, |c| is_other_class_or_ws(c, cur_graph)) { - Some(n) => Some(n + 1), // Land on char after whitespace - None => Some(0) // Start of buffer - } - } - false => { - let cur_graph = self.grapheme_at(pos)?; - match self.rfind_from(pos, |c| is_other_class_or_ws(c, cur_graph)) { - Some(n) => Some(n + 1), // Land on char after whitespace - None => Some(0) // Start of buffer - } - } - } - } - To::End => { - if self.on_whitespace() { - return Some(self.rfind_from(pos, |c| CharClass::from(c) != CharClass::Whitespace).unwrap_or(0)) - } - if self.on_end_of_word(word) { - pos = pos.checked_sub(1)?; - let cur_char_class = CharClass::from(self.grapheme_at_cursor()?); - let prev_char = self.grapheme_at(self.prev_pos(1)?)?; - let prev_char_class = CharClass::from(prev_char); - if cur_char_class != prev_char_class && prev_char_class != CharClass::Whitespace { - return Some(pos) - } - } - let cur_graph = self.grapheme_at(pos)?; - let Some(diff_class_pos) = self.rfind_from(pos, |c|is_other_class_or_ws(c, cur_graph)) else { - return Some(0) - }; - 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 { - - // 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 { - - // 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_with_hint(&mut self, motion: Motion) -> MotionKind { - let Some(hint) = self.hint.as_ref() else { - return MotionKind::Null - }; - let buffer = self.buffer.clone(); - self.buffer.push_str(hint); - let motion_eval = self.eval_motion(motion); - self.buffer = buffer; - motion_eval - } - pub fn eval_motion(&mut self, motion: Motion) -> MotionKind { - flog!(DEBUG,self.buffer); - flog!(DEBUG,motion); - match motion { - Motion::WholeLine => MotionKind::Line(0), - Motion::TextObj(text_obj, bound) => { - let Some(range) = self.eval_text_object(text_obj, bound) else { - return MotionKind::Null - }; - MotionKind::range(range) - } - Motion::BeginningOfFirstWord => { - 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::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::ForwardWord(to, word) => { - let Some(pos) = self.find_word_pos(word, to, Direction::Forward) else { - return MotionKind::Null - }; - match to { - To::Start => MotionKind::To(pos), - To::End => MotionKind::On(pos), - } - } - Motion::CharSearch(direction, dest, ch) => { - let ch = format!("{ch}"); - let saved_cursor = self.cursor; - match direction { - Direction::Forward => { - if self.grapheme_at_cursor().is_some_and(|c| c == ch) { - self.cursor_fwd(1); - } - let Some(pos) = self.find(|c| c == ch) else { - self.cursor = saved_cursor; - return MotionKind::Null - }; - self.cursor = saved_cursor; - match dest { - Dest::On => MotionKind::On(pos), - Dest::Before => MotionKind::Before(pos), - Dest::After => todo!(), - } - } - Direction::Backward => { - if self.grapheme_at_cursor().is_some_and(|c| c == ch) { - self.cursor_back(1); - } - let Some(pos) = self.rfind(|c| c == ch) else { - self.cursor = saved_cursor; - return MotionKind::Null - }; - self.cursor = saved_cursor; - match dest { - Dest::On => MotionKind::On(pos), - Dest::Before => MotionKind::Before(pos), - Dest::After => todo!(), - } - } - } - - } - Motion::BackwardChar => MotionKind::Backward(1), - Motion::ForwardChar => MotionKind::Forward(1), - Motion::LineUp => MotionKind::Line(-1), - Motion::LineDown => MotionKind::Line(1), - Motion::ScreenLineUp => MotionKind::ScreenLine(-1), - Motion::ScreenLineDown => MotionKind::ScreenLine(1), - Motion::WholeBuffer => todo!(), - Motion::BeginningOfBuffer => MotionKind::To(0), - 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(start, end) => { - let start = start.clamp(0, self.byte_len().saturating_sub(1)); - let end = end.clamp(0, self.byte_len().saturating_sub(1)); - MotionKind::range(mk_range(start, end)) - } - Motion::EndOfLastWord => { - let Some(search_start) = self.next_pos(1) else { - return MotionKind::Null - }; - let mut last_graph_pos = None; - for (i,graph) in self.buffer[search_start..].grapheme_indices(true) { - flog!(DEBUG, last_graph_pos); - flog!(DEBUG, graph); - if graph == "\n" && last_graph_pos.is_some() { - return MotionKind::On(search_start + last_graph_pos.unwrap()) - } else if !is_whitespace(graph) { - last_graph_pos = Some(i) - } - } - flog!(DEBUG,self.byte_len()); - last_graph_pos - .map(|pos| MotionKind::On(search_start + pos)) - .unwrap_or(MotionKind::Null) - } - Motion::BeginningOfScreenLine => { - let screen_start = self.start_of_screen_line(); - MotionKind::On(screen_start) - } - Motion::FirstGraphicalOnScreenLine => { - let (start,end) = self.this_screen_line(); - flog!(DEBUG,start,end); - let slice = &self.buffer[start..=end]; - for (i,grapheme) in slice.grapheme_indices(true) { - if !is_whitespace(grapheme) { - return MotionKind::On(start + i) - } - } - MotionKind::On(start) - } - Motion::HalfOfScreen => todo!(), - Motion::HalfOfScreenLineText => todo!(), - Motion::Builder(_) => todo!(), - Motion::RepeatMotion => todo!(), - Motion::RepeatMotionRev => todo!(), - Motion::Null => MotionKind::Null, - } - } - pub fn calculate_display_offset(&self, n_lines: isize) -> Option { - let (start,end) = self.this_line(); - let graphemes: Vec<(usize, usize, &str)> = self.buffer[start..end] - .graphemes(true) - .scan(start, |idx, g| { - let current = *idx; - *idx += g.len(); // Advance by number of bytes - Some((g.width(), current, g)) - }).collect(); - - let mut cursor_line_index = 0; - let mut cursor_visual_col = 0; - let mut screen_lines = vec![]; - let mut cur_line = vec![]; - let mut line_width = 0; - - for (width, byte_idx, grapheme) in graphemes { - if byte_idx == self.cursor { - // Save this to later find column - cursor_line_index = screen_lines.len(); - cursor_visual_col = line_width; - } - - let new_line_width = line_width + width; - if new_line_width > self.term_dims.1 { - screen_lines.push(std::mem::take(&mut cur_line)); - cur_line.push((width, byte_idx, grapheme)); - line_width = width; - } else { - cur_line.push((width, byte_idx, grapheme)); - line_width = new_line_width; - } - } - - if !cur_line.is_empty() { - screen_lines.push(cur_line); - } - - if screen_lines.len() == 1 { + pub fn select_lines_down(&mut self, n: usize) -> Option<(usize,usize)> { + if self.end_of_line() == self.cursor.max { return None } - - let target_line_index = (cursor_line_index as isize + n_lines) - .clamp(0, (screen_lines.len() - 1) as isize) as usize; - - let mut col = 0; - for (width, byte_idx, _) in &screen_lines[target_line_index] { - if col + width > cursor_visual_col { - return Some(*byte_idx); - } - col += width; - } - - // If you went past the end of the line - screen_lines[target_line_index] - .last() - .map(|(_, byte_idx, _)| *byte_idx) - } - pub fn get_range_from_motion(&self, verb: &Verb, motion: &MotionKind) -> Option> { - let range = match motion { - MotionKind::Forward(n) => { - let pos = self.next_pos(*n)?; - let range = self.cursor..pos; - assert!(range.end <= self.byte_len()); - Some(range) - } - MotionKind::To(n) => { - let range = mk_range(self.cursor, *n); - assert!(range.end <= self.byte_len()); - Some(range) - } - MotionKind::On(n) => { - let range = mk_range_inclusive(self.cursor, *n); - Some(range) - } - MotionKind::Before(n) => { - let n = match n.cmp(&self.cursor) { - Ordering::Less => (n + 1).min(self.byte_len()), - Ordering::Equal => n.saturating_sub(1), - Ordering::Greater => *n - }; - let range = mk_range_inclusive(n, self.cursor); - Some(range) - } - MotionKind::Backward(n) => { - let pos = self.prev_pos(*n)?; - let range = pos..self.cursor; - Some(range) - } - MotionKind::Range(range) => { - Some(range.0..range.1) - } - MotionKind::Line(n) => { - match n.cmp(&0) { - Ordering::Less => { - let (start,end) = self.select_lines_up(n.unsigned_abs()); - let mut range = match verb { - Verb::Delete => mk_range_inclusive(start,end), - _ => mk_range(start,end), - }; - range = self.clamp_range(range); - Some(range) - } - Ordering::Equal => { - let (start,end) = self.this_line(); - let mut range = match verb { - Verb::Delete => mk_range_inclusive(start,end), - _ => mk_range(start,end), - }; - range = self.clamp_range(range); - Some(range) - } - Ordering::Greater => { - let (start, mut end) = self.select_lines_down(*n as usize); - end = (end + 1).min(self.byte_len() - 1); - let mut range = match verb { - Verb::Delete => mk_range_inclusive(start,end), - _ => mk_range(start,end), - }; - range = self.clamp_range(range); - Some(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!() - }; - Some(range) - } - MotionKind::Null => None, - MotionKind::ScreenLine(n) => { - let pos = self.calculate_display_offset(*n)?; - Some(mk_range(pos, self.cursor)) - } - }; - range.map(|rng| self.clamp_range(rng)) - } - pub fn indent_lines(&mut self, range: Range) { - let (start,end) = (range.start,range.end); + let target_line = self.cursor_line_number() + n; + let start = self.start_of_line(); + let (_,end) = self.line_bounds(target_line); - self.buffer.insert(start, '\t'); + Some((start,end)) + } + pub fn line_bounds(&mut self, n: usize) -> (usize,usize) { + if n > self.total_lines() { + panic!("Attempted to find line {n} when there are only {} lines",self.total_lines()) + } - let graphemes = self.buffer[start + 1..end].grapheme_indices(true); - let mut tab_insert_indices = vec![]; - let mut next_is_tab_pos = false; - for (i,g) in graphemes { + let mut grapheme_index = 0; + let mut start = 0; + + // Fine the start of the line + for _ in 0..n { + for (_, g) in self.buffer.grapheme_indices(true).skip(grapheme_index) { + grapheme_index += 1; + if g == "\n" { + start = grapheme_index; + break; + } + } + } + + let mut end = start; + // Find the end of the line + for (_, g) in self.buffer.grapheme_indices(true).skip(start) { + end += 1; if g == "\n" { - next_is_tab_pos = true; - } else if next_is_tab_pos { - tab_insert_indices.push(start + i + 1); - next_is_tab_pos = false; + break; } } - for i in tab_insert_indices { - if i < self.byte_len() { - self.buffer.insert(i, '\t'); - } - } - } - pub fn dedent_lines(&mut self, range: Range) { - - todo!() - } - pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> { - match verb { - Verb::Change | - Verb::Delete => { - let Some(mut range) = self.get_range_from_motion(&verb, &motion) else { - return Ok(()) - }; - let restore_col = matches!(motion, MotionKind::Line(_)) && matches!(verb, Verb::Delete); - if restore_col { - self.saved_col = Some(self.cursor_column()) - } - let deleted = self.buffer.drain(range.clone()); - register.write_to_register(deleted.collect()); - - self.cursor = range.start; - if restore_col { - let saved = self.saved_col.unwrap(); - let line_start = self.this_line().0; - - self.cursor = line_start + saved; - } - } - 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)); - self.cursor_back(1); - } - } - } - } - Verb::VisualModeSelectLast => { - if let Some(range) = self.last_selected_range.as_ref() { - self.selected_range = Some(range.clone()); - let mode = self.select_mode.unwrap_or_default(); - self.cursor = match mode.anchor() { - SelectionAnchor::Start => range.start, - SelectionAnchor::End => range.end - } - } - } - Verb::SwapVisualAnchor => { - if let Some(range) = self.selected_range() { - if let Some(mut mode) = self.select_mode { - mode.invert_anchor(); - self.cursor = match mode.anchor() { - SelectionAnchor::Start => range.start, - SelectionAnchor::End => range.end, - }; - self.select_mode = Some(mode); - } - } - } - Verb::Yank => { - let Some(range) = self.get_range_from_motion(&verb, &motion) else { - return Ok(()) - }; - let yanked = &self.buffer[range.clone()]; - register.write_to_register(yanked.to_string()); - self.cursor = range.start; - } - Verb::ReplaceChar(c) => { - let Some(range) = self.get_range_from_motion(&verb, &motion) else { - return Ok(()) - }; - let delta = range.end - range.start; - let new_range = format!("{c}").repeat(delta); - let cursor_pos = range.end; - self.buffer.replace_range(range, &new_range); - self.cursor = cursor_pos - } - Verb::Substitute => todo!(), - Verb::ToLower => { - let Some(range) = self.get_range_from_motion(&verb, &motion) else { - return Ok(()) - }; - let mut new_range = String::new(); - let slice = &self.buffer[range.clone()]; - for ch in slice.chars() { - if ch.is_ascii_uppercase() { - new_range.push(ch.to_ascii_lowercase()) - } else { - new_range.push(ch) - } - } - self.buffer.replace_range(range.clone(), &new_range); - self.cursor = range.end; - } - Verb::ToUpper => { - let Some(range) = self.get_range_from_motion(&verb, &motion) else { - return Ok(()) - }; - let mut new_range = String::new(); - let slice = &self.buffer[range.clone()]; - for ch in slice.chars() { - if ch.is_ascii_lowercase() { - new_range.push(ch.to_ascii_uppercase()) - } else { - new_range.push(ch) - } - } - self.buffer.replace_range(range.clone(), &new_range); - self.cursor = range.end; - } - Verb::ToggleCase => { - let Some(range) = self.get_range_from_motion(&verb, &motion) else { - return Ok(()) - }; - let mut new_range = String::new(); - let slice = &self.buffer[range.clone()]; - for ch in slice.chars() { - if ch.is_ascii_lowercase() { - new_range.push(ch.to_ascii_uppercase()) - } else if ch.is_ascii_uppercase() { - new_range.push(ch.to_ascii_lowercase()) - } else { - new_range.push(ch) - } - } - self.buffer.replace_range(range.clone(), &new_range); - self.cursor = range.end; - } - Verb::Complete => todo!(), - Verb::CompleteBackward => todo!(), - Verb::Undo => { - let Some(undo) = self.undo_stack.pop() else { - return Ok(()) - }; - let Edit { pos, cursor_pos, old, new, .. } = undo; - let range = pos..pos + new.len(); - self.buffer.replace_range(range, &old); - let redo_cursor_pos = self.cursor; - if self.move_cursor_on_undo { - self.cursor = cursor_pos; - } - let redo = Edit { pos, cursor_pos: redo_cursor_pos, old: new, new: old, merging: false }; - self.redo_stack.push(redo); - } - Verb::Redo => { - let Some(redo) = self.redo_stack.pop() else { - return Ok(()) - }; - let Edit { pos, cursor_pos, old, new, .. } = redo; - let range = pos..pos + new.len(); - self.buffer.replace_range(range, &old); - let undo_cursor_pos = self.cursor; - if self.move_cursor_on_undo { - self.cursor = cursor_pos; - } - let undo = Edit { pos, cursor_pos: undo_cursor_pos, old: new, new: old, merging: false }; - self.undo_stack.push(undo); - } - Verb::RepeatLast => todo!(), - Verb::Put(anchor) => { - let Some(register_content) = register.read_from_register() else { - return Ok(()) - }; - match anchor { - Anchor::After => { - for ch in register_content.chars() { - self.cursor_fwd(1); // Only difference is which one you start with - self.insert(ch); - } - } - Anchor::Before => { - for ch in register_content.chars() { - self.insert(ch); - self.cursor_fwd(1); - } - } - } - } - Verb::InsertModeLineBreak(anchor) => { - match anchor { - Anchor::After => { - let (_,end) = self.this_line(); - self.cursor = end; - self.insert('\n'); - self.cursor_fwd(1); - } - Anchor::Before => { - let (start,_) = self.this_line(); - self.cursor = start; - self.insert('\n'); - } - } - } - Verb::JoinLines => { - let (start,end) = self.this_line(); - let Some((nstart,nend)) = self.next_line(1) else { - return Ok(()) - }; - let line = &self.buffer[start..end]; - let next_line = &self.buffer[nstart..nend].trim_start().to_string(); // strip leading whitespace - let replace_newline_with_space = !line.ends_with([' ', '\t']); - self.cursor = end; - if replace_newline_with_space { - self.buffer.replace_range(end..end+1, " "); - self.buffer.replace_range(end+1..nend, next_line); - } else { - self.buffer.replace_range(end..end+1, ""); - self.buffer.replace_range(end..nend, next_line); - } - } - Verb::InsertChar(ch) => { - self.insert(ch); - self.apply_motion(/*forced*/ true, motion); - } - Verb::Insert(str) => { - for ch in str.chars() { - self.insert(ch); - self.cursor_fwd(1); - } - } - Verb::Breakline(anchor) => todo!(), - Verb::Indent => { - let Some(range) = self.get_range_from_motion(&verb, &motion) else { - return Ok(()) - }; - self.indent_lines(range) - } - Verb::Dedent => { - let Some(range) = self.get_range_from_motion(&verb, &motion) else { - return Ok(()) - }; - self.dedent_lines(range) - } - Verb::Rot13 => { - let Some(range) = self.get_range_from_motion(&verb, &motion) else { - return Ok(()) - }; - let slice = &self.buffer[range.clone()]; - let rot13 = rot13(slice); - self.buffer.replace_range(range, &rot13); - } - Verb::Equalize => todo!(), // I fear this one - Verb::Builder(verb_builder) => todo!(), - Verb::EndOfFile => { - if !self.buffer.is_empty() { - self.cursor = 0; - self.buffer.clear(); - } else { - sh_quit(0) - } - } - - Verb::AcceptLine | - Verb::ReplaceMode | - Verb::InsertMode | - Verb::NormalMode | - Verb::VisualModeLine | - Verb::VisualModeBlock | - Verb::VisualMode => { - /* Already handled */ - self.apply_motion(/*forced*/ true,motion); - } - } - Ok(()) - } - pub fn apply_motion(&mut self, forced: bool, motion: MotionKind) { - - match motion { - MotionKind::Forward(n) => { - for _ in 0..n { - if forced { - if !self.cursor_fwd(1) { - break - } - } else if !self.cursor_fwd_confined(1) { - break - } - } - } - MotionKind::Backward(n) => { - for _ in 0..n { - if forced { - if !self.cursor_back(1) { - break - } - } else if !self.cursor_back_confined(1) { - break - } - } - } - MotionKind::To(n) | - MotionKind::On(n) => { - if n > self.byte_len() { - self.cursor = self.byte_len(); - } else { - self.cursor = n - } - } - MotionKind::Before(n) => { - if n > self.byte_len() { - self.cursor = self.byte_len(); - } else { - match n.cmp(&self.cursor) { - Ordering::Less => { - let n = (n + 1).min(self.byte_len()); - self.cursor = n - } - Ordering::Equal => { - self.cursor = n - } - Ordering::Greater => { - let n = n.saturating_sub(1); - 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 => (), - Ordering::Less => { - for _ in 0..n.unsigned_abs() { - let Some(pos) = self.find_prev_line_pos() else { - return - }; - self.cursor = pos; - } - } - Ordering::Greater => { - for _ in 0..n.unsigned_abs() { - let Some(pos) = self.find_next_line_pos() else { - return - }; - self.cursor = pos; - } - } - } - } - MotionKind::ToLine(n) => { - let Some((start,_)) = self.select_line(n) else { - return - }; - self.cursor = start; - } - MotionKind::Null => { /* Pass */ } - MotionKind::ScreenLine(n) => { - let Some(pos) = self.calculate_display_offset(n) else { - return - }; - self.cursor = pos; - } - } - if let Some(mut mode) = self.select_mode { - let Some(range) = self.selected_range.clone() else { - return - }; - let (mut start,mut end) = (range.start,range.end); - match mode { - SelectionMode::Char(anchor) => { - match anchor { - SelectionAnchor::Start => { - start = self.cursor; - } - SelectionAnchor::End => { - end = self.cursor; - } - } - } - SelectionMode::Line(anchor) => todo!(), - SelectionMode::Block(anchor) => todo!(), - } - if start >= end { - mode.invert_anchor(); - std::mem::swap(&mut start, &mut end); - - self.select_mode = Some(mode); - } - self.selected_range = Some(start..end); - } - } - pub fn edit_is_merging(&self) -> bool { - self.undo_stack.last().is_some_and(|edit| edit.merging) + (start, end) } pub fn handle_edit(&mut self, old: String, new: String, curs_pos: usize) { - if self.edit_is_merging() { + let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging); + if edit_is_merging { let diff = Edit::diff(&old, &new, curs_pos); if diff.is_empty() { return @@ -2013,6 +617,8 @@ impl LineBuf { return }; + + edit.new.push_str(&diff.new); edit.old.push_str(&diff.old); @@ -2024,60 +630,1494 @@ impl LineBuf { } } } + + pub fn directional_indices_iter(&mut self, dir: Direction) -> Box> { + self.directional_indices_iter_from(self.cursor.get(), dir) + } + pub fn directional_indices_iter_from(&mut self, pos: usize, dir: Direction) -> Box> { + self.update_graphemes_lazy(); + let skip = if pos == 0 { 0 } else { pos + 1 }; + match dir { + Direction::Forward => { + Box::new( + self.grapheme_indices() + .to_vec() + .into_iter() + .skip(skip) + ) as Box> + } + Direction::Backward => { + Box::new( + self.grapheme_indices() + .to_vec() + .into_iter() + .take(pos) + .rev() + ) as Box> + } + } + } + pub fn dispatch_text_obj( + &mut self, + count: usize, + text_obj: TextObj, + bound: Bound + ) -> Option<(usize,usize)> { + match text_obj { + // Text groups + TextObj::Word(word) => self.text_obj_word(count, bound, word), + TextObj::Sentence(dir) => self.text_obj_sentence(count, dir, bound), + TextObj::Paragraph(dir) => self.text_obj_paragraph(count, dir, bound), + + // Quoted blocks + TextObj::DoubleQuote | + TextObj::SingleQuote | + TextObj::BacktickQuote => self.text_obj_quote(count, text_obj, bound), + + // Delimited blocks + TextObj::Paren | + TextObj::Bracket | + TextObj::Brace | + TextObj::Angle => self.text_obj_delim(count, text_obj, bound), + + // Other stuff + TextObj::Tag => todo!(), + TextObj::Custom(_) => todo!(), + } + } + pub fn text_obj_word(&mut self, count: usize, bound: Bound, word: Word) -> Option<(usize,usize)> { + todo!() + } + pub fn text_obj_sentence(&mut self, count: usize, dir: Direction, bound: Bound) -> Option<(usize, usize)> { + todo!() + } + pub fn text_obj_paragraph(&mut self, count: usize, dir: Direction, bound: Bound) -> Option<(usize, usize)> { + todo!() + } + pub fn text_obj_delim(&mut self, count: usize, text_obj: TextObj, bound: Bound) -> Option<(usize,usize)> { + let mut backward_indices = (0..self.cursor.get()).rev(); + let (opener,closer) = match text_obj { + TextObj::Paren => ("(",")"), + TextObj::Bracket => ("[","]"), + TextObj::Brace => ("{","}"), + TextObj::Angle => ("<",">"), + _ => unreachable!() + }; + + let mut start_pos = None; + let mut closer_count: u32 = 0; + while let Some(idx) = backward_indices.next() { + let gr = self.grapheme_at(idx)?.to_string(); + if gr != closer && gr != opener { continue } + + let mut escaped = false; + while let Some(idx) = backward_indices.next() { + // Keep consuming indices as long as they refer to a backslash + let Some("\\") = self.grapheme_at(idx) else { + break + }; + // On each backslash, flip this boolean + escaped = !escaped + } + + // If there are an even number of backslashes (or none), we are not escaped + // Therefore, we have found the start position + if !escaped { + if gr == closer { + closer_count += 1; + } else if closer_count == 0 { + start_pos = Some(idx); + break + } else { + closer_count = closer_count.saturating_sub(1) + } + } + } + + let (mut start, mut end) = if let Some(pos) = start_pos { + let start = pos; + let mut forward_indices = start+1..self.cursor.max; + let mut end = None; + let mut opener_count: u32 = 0; + + while let Some(idx) = forward_indices.next() { + match self.grapheme_at(idx)? { + "\\" => { forward_indices.next(); } + gr if gr == opener => opener_count += 1, + gr if gr == closer => { + if opener_count == 0 { + end = Some(idx); + break + } else { + opener_count = opener_count.saturating_sub(1); + } + } + _ => { /* Continue */ } + } + } + + (start,end?) + } else { + let mut forward_indices = self.cursor.get()..self.cursor.max; + let mut start = None; + let mut end = None; + let mut opener_count: u32 = 0; + + while let Some(idx) = forward_indices.next() { + match self.grapheme_at(idx)? { + "\\" => { forward_indices.next(); } + gr if gr == opener => { + if opener_count == 0 { + start = Some(idx); + } + opener_count += 1; + } + gr if gr == closer => { + if opener_count == 1 { + end = Some(idx); + break + } else { + opener_count = opener_count.saturating_sub(1) + } + } + _ => { /* Continue */ } + } + } + + (start?,end?) + }; + + match bound { + Bound::Inside => { + // Start includes the quote, so push it forward + start += 1; + } + Bound::Around => { + // End excludes the quote, so push it forward + end += 1; + + // We also need to include any trailing whitespace + let end_of_line = self.end_of_line(); + let remainder = end..end_of_line; + for idx in remainder { + let Some(gr) = self.grapheme_at(idx) else { break }; + flog!(DEBUG, gr); + if is_whitespace(gr) { + end += 1; + } else { + break + } + } + } + } + + Some((start,end)) + } + pub fn text_obj_quote(&mut self, count: usize, text_obj: TextObj, bound: Bound) -> Option<(usize,usize)> { + let (start,end) = self.this_line(); // Only operates on the current line + + // Get the grapheme indices backward from the cursor + let mut backward_indices = (start..self.cursor.get()).rev(); + let target = match text_obj { + TextObj::DoubleQuote => "\"", + TextObj::SingleQuote => "'", + TextObj::BacktickQuote => "`", + _ => unreachable!() + }; + let mut start_pos = None; + while let Some(idx) = backward_indices.next() { + match self.grapheme_at(idx)? { + gr if gr == target => { + // We are going backwards, so we need to handle escapes differently + // These things were not meant to be read backwards, so it's a little fucked up + let mut escaped = false; + while let Some(idx) = backward_indices.next() { + // Keep consuming indices as long as they refer to a backslash + let Some("\\") = self.grapheme_at(idx) else { + break + }; + // On each backslash, flip this boolean + escaped = !escaped + } + + // If there are an even number of backslashes, we are not escaped + // Therefore, we have found the start position + if !escaped { + start_pos = Some(idx); + break + } + } + _ => { /* Continue */ } + } + } + + // Try to find a quote backwards + let (mut start, mut end) = if let Some(pos) = start_pos { + // Found one, only one more to go + let start = pos; + let mut forward_indices = start+1..end; + let mut end = None; + + while let Some(idx) = forward_indices.next() { + match self.grapheme_at(idx)? { + "\\" => { forward_indices.next(); } + gr if gr == target => { + end = Some(idx); + break; + } + _ => { /* Continue */ } + } + } + let end = end?; + + (start,end) + } else { + // Did not find one, have two find two of them forward now + let mut forward_indices = self.cursor.get()..end; + let mut start = None; + let mut end = None; + + while let Some(idx) = forward_indices.next() { + match self.grapheme_at(idx)? { + "\\" => { forward_indices.next(); } + gr if gr == target => { + start = Some(idx); + break + } + _ => { /* Continue */ } + } + } + let start = start?; + + while let Some(idx) = forward_indices.next() { + match self.grapheme_at(idx)? { + "\\" => { forward_indices.next(); } + gr if gr == target => { + end = Some(idx); + break; + } + _ => { /* Continue */ } + } + } + let end = end?; + + (start,end) + }; + + match bound { + Bound::Inside => { + // Start includes the quote, so push it forward + start += 1; + } + Bound::Around => { + // End excludes the quote, so push it forward + end += 1; + + // We also need to include any trailing whitespace + let end_of_line = self.end_of_line(); + let remainder = end..end_of_line; + for idx in remainder { + let Some(gr) = self.grapheme_at(idx) else { break }; + flog!(DEBUG, gr); + if is_whitespace(gr) { + end += 1; + } else { + break + } + } + } + } + + Some((start, end)) + } + pub fn dispatch_word_motion( + &mut self, + count: usize, + to: To, + word: Word, + dir: Direction, + include_last_char: bool + ) -> usize { + // Not sorry for these method names btw + let mut pos = ClampedUsize::new(self.cursor.get(), self.cursor.max, false); + for i in 0..count { + // We alter 'include_last_char' to only be true on the last iteration + // Therefore, '5cw' will find the correct range for the first four and stop on the end of the fifth word + let include_last_char_and_is_last_word = include_last_char && i == count.saturating_sub(1); + pos.set(match to { + To::Start => { + match dir { + Direction::Forward => self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir, include_last_char_and_is_last_word), + Direction::Backward => 'backward: { + // We also need to handle insert mode's Ctrl+W behaviors here + let target = self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir); + + // Check to see if we are in insert mode + let Some(start_pos) = self.insert_mode_start_pos else { + break 'backward target + }; + // If we are in front of start_pos, and we would cross start_pos to reach target + // then stop at start_pos + if start_pos > target && self.cursor.get() > start_pos { + return start_pos + } else { + // We are behind start_pos, now we just reset it + if self.cursor.get() < start_pos { + self.clear_insert_mode_start_pos(); + } + break 'backward target + } + } + } + } + To::End => { + match dir { + Direction::Forward => self.end_of_word_forward_or_start_of_word_backward_from(pos.get(), word, dir), + Direction::Backward => self.start_of_word_forward_or_end_of_word_backward_from(pos.get(), word, dir, false), + } + } + }); + } + pos.get() + } + + /// Finds the start of a word forward, or the end of a word backward + /// + /// Finding the start of a word in the forward direction, and finding the end of a word in the backward direction + /// are logically the same operation, if you use a reversed iterator for the backward motion. + /// + /// Tied with 'end_of_word_forward_or_start_of_word_backward_from()' for the longest method name I have ever written + pub fn start_of_word_forward_or_end_of_word_backward_from(&mut self, mut pos: usize, word: Word, dir: Direction, include_last_char: bool) -> usize { + let default = match dir { + Direction::Backward => 0, + Direction::Forward => self.grapheme_indices().len() + }; + let mut indices_iter = self.directional_indices_iter_from(pos,dir).peekable(); // And make it peekable + + match word { + Word::Big => { + let Some(next) = indices_iter.peek() else { + return default + }; + let on_boundary = self.grapheme_at(*next).is_none_or(is_whitespace); + if on_boundary { + let Some(idx) = indices_iter.next() else { return default }; + // We have a 'cw' call, do not include the trailing whitespace + if include_last_char { + return idx; + } else { + pos = idx; + } + } + + // Check current grapheme + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default + }; + let on_whitespace = is_whitespace(&cur_char); + + // Find the next whitespace + if !on_whitespace { + let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) else { + return default + }; + if include_last_char { + return ws_pos + } + } + + // Return the next visible grapheme position + let non_ws_pos = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))).unwrap_or(default); + non_ws_pos + } + Word::Normal => { + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default }; + let Some(next_idx) = indices_iter.peek() else { return default }; + let on_boundary = !is_whitespace(&cur_char) && self.grapheme_at(*next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char)); + if on_boundary { + if include_last_char { + return *next_idx + } else { + pos = *next_idx; + } + } + + let Some(next_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default + }; + if is_other_class_not_ws(&cur_char, &next_char) { + return pos + } + let on_whitespace = is_whitespace(&cur_char); + + // Advance until hitting whitespace or a different character class + if !on_whitespace { + let other_class_pos = indices_iter.find( + |i| { + self.grapheme_at(*i) + .is_some_and(|c| is_other_class_or_is_ws(c, &next_char)) + } + ); + let Some(other_class_pos) = other_class_pos else { + return default + }; + // If we hit a different character class, we return here + if self.grapheme_at(other_class_pos).is_some_and(|c| !is_whitespace(c)) || include_last_char { + return other_class_pos + } + } + + // We are now certainly on a whitespace character. Advance until a non-whitespace character. + let non_ws_pos = indices_iter.find( + |i| { + self.grapheme_at(*i) + .is_some_and(|c| !is_whitespace(c)) + } + ).unwrap_or(default); + non_ws_pos + } + } + } + /// Finds the end of a word forward, or the start of a word backward + /// + /// Finding the end of a word in the forward direction, and finding the start of a word in the backward direction + /// are logically the same operation, if you use a reversed iterator for the backward motion. + pub fn end_of_word_forward_or_start_of_word_backward_from(&mut self, mut pos: usize, word: Word, dir: Direction) -> usize { + let default = match dir { + Direction::Backward => 0, + Direction::Forward => self.grapheme_indices().len() + }; + + let mut indices_iter = self.directional_indices_iter_from(pos,dir).peekable(); + + match word { + Word::Big => { + let Some(next_idx) = indices_iter.peek() else { return default }; + let on_boundary = self.grapheme_at(*next_idx).is_none_or(is_whitespace); + if on_boundary { + let Some(idx) = indices_iter.next() else { return default }; + pos = idx; + } + // Check current grapheme + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default + }; + let on_whitespace = is_whitespace(&cur_char); + + // Advance iterator to next visible grapheme + if on_whitespace { + let Some(_non_ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) else { + return default + }; + } + + // The position of the next whitespace will tell us where the end (or start) of the word is + let Some(next_ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace)) else { + return default + }; + pos = next_ws_pos; + + if pos == self.grapheme_indices().len() { + // We reached the end of the buffer + pos + } else { + // We hit some whitespace, so we will go back one + match dir { + Direction::Forward => pos.saturating_sub(1), + Direction::Backward => pos + 1, + } + } + } + Word::Normal => { + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { return default }; + let Some(next_idx) = indices_iter.peek() else { return default }; + let on_boundary = !is_whitespace(&cur_char) && self.grapheme_at(*next_idx).is_none_or(|c| is_other_class_or_is_ws(c, &cur_char)); + if on_boundary { + let next_idx = indices_iter.next().unwrap(); + pos = next_idx + } + + // Check current grapheme + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return default + }; + let on_whitespace = is_whitespace(&cur_char); + + // Proceed to next visible grapheme + if on_whitespace { + let Some(non_ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| !is_whitespace(c))) else { + return default + }; + pos = non_ws_pos + } + + let Some(cur_char) = self.grapheme_at(pos).map(|c| c.to_string()) else { + return self.grapheme_indices().len() + }; + // The position of the next differing character class will tell us where the end (or start) of the word is + let Some(next_ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(|c| is_other_class_or_is_ws(c, &cur_char))) else { + return default + }; + pos = next_ws_pos; + + if pos == self.grapheme_indices().len() { + // We reached the end of the buffer + pos + } else { + // We hit some other character class, so we go back one + match dir { + Direction::Forward => pos.saturating_sub(1), + Direction::Backward => pos + 1, + } + } + } + } + } + fn grapheme_index_for_display_col(&self, line: &str, target_col: usize) -> usize { + let mut col = 0; + for (grapheme_index, g) in line.graphemes(true).enumerate() { + if g == "\n" { + if self.cursor.exclusive { + return grapheme_index.saturating_sub(1) + } else { + return grapheme_index; + } + } + let w = g.width(); + if col + w > target_col { + return grapheme_index; + } + col += w; + } + // If we reach here, the target_col is past end of line + line.graphemes(true).count() + } + pub fn cursor_max(&self) -> usize { + self.cursor.max + } + pub fn cursor_at_max(&self) -> bool { + self.cursor.get() == self.cursor.upper_bound() + } + pub fn cursor_col(&mut self) -> usize { + let start = self.start_of_line(); + let end = self.cursor.get(); + let Some(slice) = self.slice_inclusive(start..=end) else { + return start + }; + + slice + .graphemes(true) + .map(|g| g.width()) + .sum() + } + pub fn rfind_from bool>(&mut self, pos: usize, op: F) -> usize { + let Some(slice) = self.slice_to(pos) else { + return self.grapheme_indices().len() + }; + for (offset,grapheme) in slice.grapheme_indices(true).rev() { + if op(grapheme) { + return pos + offset + } + } + self.grapheme_indices().len() + } + pub fn rfind_from_optional bool>(&mut self, pos: usize, op: F) -> Option { + let slice = self.slice_to(pos)?; + for (offset,grapheme) in slice.grapheme_indices(true).rev() { + if op(grapheme) { + return Some(pos + offset) + } + } + None + } + pub fn rfind bool>(&mut self, op: F) -> usize { + self.rfind_from(self.cursor.get(), op) + } + pub fn rfind_optional bool>(&mut self, op: F) -> Option { + self.rfind_from_optional(self.cursor.get(), op) + } + pub fn find_from bool>(&mut self, pos: usize, op: F) -> usize { + let Some(slice) = self.slice_from(pos) else { + return self.grapheme_indices().len() + }; + for (offset,grapheme) in slice.grapheme_indices(true) { + if op(grapheme) { + return pos + offset + } + } + self.grapheme_indices().len() + } + pub fn find_from_optional bool>(&mut self, pos: usize, op: F) -> Option { + let slice = self.slice_from(pos)?; + for (offset,grapheme) in slice.grapheme_indices(true) { + if op(grapheme) { + return Some(pos + offset) + } + } + None + } + pub fn find_optional bool>(&mut self, op: F) -> Option { + self.find_from_optional(self.cursor.get(), op) + } + pub fn find bool>(&mut self, op: F) -> usize { + self.find_from(self.cursor.get(), op) + } + pub fn insert_str_at(&mut self, pos: usize, new: &str) { + let idx = self.index_byte_pos(pos); + self.buffer.insert_str(idx, new); + self.update_graphemes(); + } + pub fn replace_at_cursor(&mut self, new: &str) { + self.replace_at(self.cursor.get(), new); + } + pub fn force_replace_at(&mut self, pos: usize, new: &str) { + let Some(gr) = self.grapheme_at(pos).map(|gr| gr.to_string()) else { + self.buffer.push_str(new); + return + }; + let start = self.index_byte_pos(pos); + let end = start + gr.len(); + self.buffer.replace_range(start..end, new); + } + pub fn replace_at(&mut self, pos: usize, new: &str) { + let Some(gr) = self.grapheme_at(pos).map(|gr| gr.to_string()) else { + self.buffer.push_str(new); + return + }; + if &gr == "\n" { + // Do not replace the newline, push it forward instead + let byte_pos = self.index_byte_pos(pos); + self.buffer.insert_str(byte_pos, new); + return + } + let start = self.index_byte_pos(pos); + let end = start + gr.len(); + self.buffer.replace_range(start..end, new); + } + pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind { + let buffer = self.buffer.clone(); + if self.has_hint() { + let hint = self.hint.clone().unwrap(); + self.push_str(&hint); + } + + let eval = match motion { + MotionCmd(count,Motion::WholeLine) => { + let Some((start,end)) = (if count == 1 { + Some(self.this_line()) + } else { + self.select_lines_down(count) + }) else { + return MotionKind::Null + }; + + let target_col = if let Some(col) = self.saved_col { + col + } else { + let col = self.cursor_col(); + self.saved_col = Some(col); + col + }; + + let Some(line) = self.slice(start..end).map(|s| s.to_string()) else { + return MotionKind::Null + }; + flog!(DEBUG,target_col); + flog!(DEBUG,target_col); + let mut target_pos = self.grapheme_index_for_display_col(&line, target_col); + flog!(DEBUG,target_pos); + if self.cursor.exclusive && line.ends_with("\n") && self.grapheme_at(target_pos) == Some("\n") { + target_pos = target_pos.saturating_sub(1); // Don't land on the newline + } + MotionKind::InclusiveWithTargetCol((start,end),target_pos) + } + MotionCmd(count,Motion::WordMotion(to, word, dir)) => { + // 'cw' is a weird case + // if you are on the word's left boundary, it will not delete whitespace after the end of the word + let include_last_char = verb == Some(&Verb::Change) && + matches!(motion.1, Motion::WordMotion(To::Start, _, Direction::Forward)); + + let pos = self.dispatch_word_motion(count, to, word, dir, include_last_char); + let pos = ClampedUsize::new(pos,self.cursor.max,false); + // End-based operations must include the last character + // But the cursor must also stop just before it when moving + // So we have to do some weird shit to reconcile this behavior + if to == To::End { + match dir { + Direction::Forward => { + MotionKind::Onto(pos.get()) + } + Direction::Backward => { + let (start,end) = ordered(self.cursor.get(),pos.get()); + MotionKind::Inclusive((start,end)) + } + } + } else { + MotionKind::On(pos.get()) + } + } + MotionCmd(count,Motion::TextObj(text_obj, bound)) => { + let Some((start,end)) = self.dispatch_text_obj(count, text_obj, bound) else { + return MotionKind::Null + }; + + MotionKind::Inclusive((start,end)) + } + MotionCmd(count,Motion::ToDelimMatch) => todo!(), + MotionCmd(count,Motion::ToBrace(direction)) => todo!(), + MotionCmd(count,Motion::ToBracket(direction)) => todo!(), + MotionCmd(count,Motion::ToParen(direction)) => todo!(), + MotionCmd(count,Motion::EndOfLastWord) => { + let start = self.start_of_line(); + let mut newline_count = 0; + let mut indices = self.directional_indices_iter_from(start,Direction::Forward); + let mut last_graphical = None; + while let Some(idx) = indices.next() { + let grapheme = self.grapheme_at(idx).unwrap(); + if !is_whitespace(grapheme) { + last_graphical = Some(idx); + } + if grapheme == "\n" { + newline_count += 1; + if newline_count == count { + break + } + } + } + let Some(last) = last_graphical else { + return MotionKind::Null + }; + MotionKind::On(last) + } + MotionCmd(_,Motion::BeginningOfFirstWord) => { + let start = self.start_of_line(); + let mut indices = self.directional_indices_iter_from(start,Direction::Forward); + let mut first_graphical = None; + while let Some(idx) = indices.next() { + let grapheme = self.grapheme_at(idx).unwrap(); + if !is_whitespace(grapheme) { + first_graphical = Some(idx); + break + } + if grapheme == "\n" { + break + } + } + let Some(first) = first_graphical else { + return MotionKind::Null + }; + MotionKind::On(first) + } + MotionCmd(_,Motion::BeginningOfLine) => MotionKind::On(self.start_of_line()), + MotionCmd(count,Motion::EndOfLine) => { + let pos = if count == 1 { + self.end_of_line() + } else if let Some((_,end)) = self.select_lines_down(count) { + end + } else { + self.end_of_line() + }; + + MotionKind::On(pos) + } + MotionCmd(count,Motion::CharSearch(direction, dest, ch)) => { + let ch_str = &format!("{ch}"); + let mut pos = self.cursor; + for _ in 0..count { + let mut indices_iter = self.directional_indices_iter_from(pos.get(), direction); + + let Some(ch_pos) = indices_iter.position(|i| { + self.grapheme_at(i) == Some(ch_str) + }) else { + return MotionKind::Null + }; + match direction { + Direction::Forward => pos.add(ch_pos + 1), + Direction::Backward => pos.sub(ch_pos.saturating_sub(1)), + } + + if dest == Dest::Before { + match direction { + Direction::Forward => pos.sub(1), + Direction::Backward => pos.add(1), + } + } + } + MotionKind::Onto(pos.get()) + } + MotionCmd(count,motion @ (Motion::ForwardChar | Motion::BackwardChar)) => { + let mut target = self.cursor; + target.exclusive = false; + for _ in 0..count { + match motion { + Motion::BackwardChar => target.sub(1), + Motion::ForwardChar => { + if self.cursor.exclusive && self.grapheme_at(target.ret_add(1)) == Some("\n") { + flog!(DEBUG, "returning null"); + return MotionKind::Null + } + target.add(1); + continue + } + _ => unreachable!() + } + if self.grapheme_at(target.get()) == Some("\n") { + flog!(DEBUG, "returning null outside of match"); + return MotionKind::Null + } + } + MotionKind::On(target.get()) + } + MotionCmd(count, Motion::ForwardCharForced) => MotionKind::On(self.cursor.ret_add(count)), + MotionCmd(count, Motion::BackwardCharForced) => MotionKind::On(self.cursor.ret_sub(count)), + MotionCmd(count,Motion::LineDown) | + MotionCmd(count,Motion::LineUp) => { + let Some((start,end)) = (match motion.1 { + Motion::LineUp => self.nth_prev_line(1), + Motion::LineDown => self.nth_next_line(1), + _ => unreachable!() + }) else { + return MotionKind::Null + }; + flog!(DEBUG, self.slice(start..end)); + + let mut target_col = if let Some(col) = self.saved_col { + col + } else { + let col = self.cursor_col(); + self.saved_col = Some(col); + col + }; + + let Some(line) = self.slice(start..end).map(|s| s.to_string()) else { + return MotionKind::Null + }; + flog!(DEBUG,target_col); + flog!(DEBUG,target_col); + let mut target_pos = self.grapheme_index_for_display_col(&line, target_col); + flog!(DEBUG,target_pos); + if self.cursor.exclusive && line.ends_with("\n") && self.grapheme_at(target_pos) == Some("\n") { + target_pos = target_pos.saturating_sub(1); // Don't land on the newline + } + + let (start,end) = match motion.1 { + Motion::LineUp => (start,self.end_of_line()), + Motion::LineDown => (self.start_of_line(),end), + _ => unreachable!() + }; + + MotionKind::InclusiveWithTargetCol((start,end),target_pos) + } + MotionCmd(count,Motion::LineDownCharwise) | + MotionCmd(count,Motion::LineUpCharwise) => { + let Some((start,end)) = (match motion.1 { + Motion::LineUpCharwise => self.nth_prev_line(1), + Motion::LineDownCharwise => self.nth_next_line(1), + _ => unreachable!() + }) else { + return MotionKind::Null + }; + flog!(DEBUG,start,end); + flog!(DEBUG, self.slice(start..end)); + + let target_col = if let Some(col) = self.saved_col { + col + } else { + let col = self.cursor_col(); + self.saved_col = Some(col); + col + }; + + let Some(line) = self.slice(start..end).map(|s| s.to_string()) else { + return MotionKind::Null + }; + let target_pos = start + self.grapheme_index_for_display_col(&line, target_col); + + MotionKind::On(target_pos) + } + MotionCmd(count,Motion::ScreenLineUp) => todo!(), + MotionCmd(count,Motion::ScreenLineUpCharwise) => todo!(), + MotionCmd(count,Motion::ScreenLineDown) => todo!(), + MotionCmd(count,Motion::ScreenLineDownCharwise) => todo!(), + MotionCmd(count,Motion::BeginningOfScreenLine) => todo!(), + MotionCmd(count,Motion::FirstGraphicalOnScreenLine) => todo!(), + MotionCmd(count,Motion::HalfOfScreen) => todo!(), + MotionCmd(count,Motion::HalfOfScreenLineText) => todo!(), + MotionCmd(_count,Motion::WholeBuffer) => MotionKind::Exclusive((0,self.grapheme_indices().len())), + MotionCmd(_count,Motion::BeginningOfBuffer) => MotionKind::On(0), + MotionCmd(_count,Motion::EndOfBuffer) => MotionKind::To(self.grapheme_indices().len()), + MotionCmd(_count,Motion::ToColumn) => todo!(), + MotionCmd(count,Motion::Range(start, end)) => { + let mut final_end = end; + if self.cursor.exclusive { + final_end += 1; + } + let delta = end - start; + let count = count.saturating_sub(1); // Becomes number of times to multiply the range + + for _ in 0..count { + final_end += delta; + } + + final_end = final_end.min(self.cursor.max); + MotionKind::Inclusive((start,final_end)) + } + MotionCmd(count,Motion::RepeatMotion) => todo!(), + MotionCmd(count,Motion::RepeatMotionRev) => todo!(), + MotionCmd(count,Motion::Null) => MotionKind::Null + }; + + self.set_buffer(buffer); + eval + } + pub fn apply_motion(&mut self, motion: MotionKind) { + let last_grapheme_pos = self + .grapheme_indices() + .len() + .saturating_sub(1); + + if self.has_hint() { + let hint = self.hint.take().unwrap(); + let saved_buffer = self.buffer.clone(); // cringe + self.push_str(&hint); + self.move_cursor(motion); + + let has_consumed_hint = ( + self.cursor.exclusive && self.cursor.get() >= last_grapheme_pos + ) || ( + !self.cursor.exclusive && self.cursor.get() > last_grapheme_pos + ); + flog!(DEBUG,has_consumed_hint); + flog!(DEBUG,self.cursor.get()); + flog!(DEBUG,last_grapheme_pos); + + if has_consumed_hint { + let buf_end = if self.cursor.exclusive { + self.cursor.ret_add(1) + } else { + self.cursor.get() + }; + let remainder = self.slice_from(buf_end); + + if remainder.is_some_and(|slice| !slice.is_empty()) { + let remainder = remainder.unwrap().to_string(); + self.hint = Some(remainder); + } + + let buffer = self.slice_to(buf_end).unwrap_or_default(); + self.buffer = buffer.to_string(); + } else { + let old_buffer = self.slice_to(last_grapheme_pos + 1).unwrap().to_string(); + let Some(old_hint) = self.slice_from(last_grapheme_pos + 1) else { + self.set_buffer(format!("{saved_buffer}{hint}")); + self.hint = None; + return + }; + + self.hint = Some(old_hint.to_string()); + self.set_buffer(old_buffer); + } + } else { + self.move_cursor(motion); + } + self.update_graphemes(); + self.update_select_range(); + } + pub fn update_select_range(&mut self) { + if let Some(mut mode) = self.select_mode { + let Some((mut start,mut end)) = self.select_range.clone() else { + return + }; + match mode { + SelectMode::Char(anchor) => { + match anchor { + SelectAnchor::Start => { + start = self.cursor.get(); + } + SelectAnchor::End => { + end = self.cursor.get(); + } + } + } + SelectMode::Line(anchor) => todo!(), + SelectMode::Block(anchor) => todo!(), + } + if start >= end { + mode.invert_anchor(); + std::mem::swap(&mut start, &mut end); + + self.select_mode = Some(mode); + } + self.select_range = Some((start,end)); + } + } + pub fn move_cursor(&mut self, motion: MotionKind) { + match motion { + MotionKind::Onto(pos) | // Onto follows On's behavior for cursor movements + MotionKind::On(pos) => self.cursor.set(pos), + MotionKind::To(pos) => { + self.cursor.set(pos); + + match pos.cmp(&self.cursor.get()) { + std::cmp::Ordering::Less => { + self.cursor.add(1); + } + std::cmp::Ordering::Greater => { + self.cursor.sub(1); + } + std::cmp::Ordering::Equal => { /* Do nothing */ } + } + } + MotionKind::ExclusiveWithTargetCol((_,_),col) | + MotionKind::InclusiveWithTargetCol((_,_),col) => { + let (start,end) = self.this_line(); + let end = end.min(col); + self.cursor.set(start + end) + } + MotionKind::Inclusive((start,_)) | + MotionKind::Exclusive((start,_)) => { + self.cursor.set(start) + } + MotionKind::Null => { /* Do nothing */ } + } + } + pub fn range_from_motion(&mut self, motion: &MotionKind) -> Option<(usize,usize)> { + let range = match motion { + MotionKind::On(pos) => ordered(self.cursor.get(), *pos), + MotionKind::Onto(pos) => { + // For motions which include the character at the cursor during operations + // but exclude the character during movements + let mut pos = ClampedUsize::new(*pos, self.cursor.max, false); + let mut cursor_pos = self.cursor; + + // The end of the range must be incremented by one + match pos.get().cmp(&self.cursor.get()) { + std::cmp::Ordering::Less => cursor_pos.add(1), + std::cmp::Ordering::Greater => pos.add(1), + std::cmp::Ordering::Equal => {} + } + ordered(cursor_pos.get(),pos.get()) + } + MotionKind::To(pos) => { + let pos = match pos.cmp(&self.cursor.get()) { + std::cmp::Ordering::Less => *pos + 1, + std::cmp::Ordering::Greater => *pos - 1, + std::cmp::Ordering::Equal => *pos, + }; + ordered(self.cursor.get(), pos) + } + MotionKind::InclusiveWithTargetCol((start,end),_) | + MotionKind::Inclusive((start,end)) => ordered(*start, *end), + MotionKind::ExclusiveWithTargetCol((start,end),_) | + MotionKind::Exclusive((start,end)) => { + let (start, mut end) = ordered(*start, *end); + end = end.saturating_sub(1); + (start,end) + } + MotionKind::Null => return None + }; + Some(range) + } + #[allow(clippy::unnecessary_to_owned)] + pub fn exec_verb(&mut self, verb: Verb, motion: MotionKind, register: RegisterName) -> ShResult<()> { + match verb { + Verb::Delete | + Verb::Yank | + Verb::Change => { + let Some((start,end)) = self.range_from_motion(&motion) else { + return Ok(()) + }; + let register_text = if verb == Verb::Yank { + self.slice(start..end) + .map(|c| c.to_string()) + .unwrap_or_default() + } else { + let drained = self.drain(start, end); + self.update_graphemes(); + flog!(DEBUG,self.cursor); + drained + }; + register.write_to_register(register_text); + match motion { + MotionKind::ExclusiveWithTargetCol((_,_),pos) | + MotionKind::InclusiveWithTargetCol((_,_),pos) => { + let (start,end) = self.this_line(); + self.cursor.set(start); + self.cursor.add(end.min(pos)); + } + _ => self.cursor.set(start), + } + } + Verb::Rot13 => { + let Some((start,end)) = self.range_from_motion(&motion) else { + return Ok(()) + }; + let slice = self.slice(start..end) + .unwrap_or_default(); + let rot13 = rot13(slice); + self.buffer.replace_range(start..end, &rot13); + self.cursor.set(start); + } + Verb::ReplaceChar(ch) => { + let mut buf = [0u8;4]; + let new = ch.encode_utf8(&mut buf); + self.replace_at_cursor(new); + self.apply_motion(motion); + } + Verb::ReplaceCharInplace(ch,count) => { + for i in 0..count { + let mut buf = [0u8;4]; + let new = ch.encode_utf8(&mut buf); + self.replace_at_cursor(new); + + // try to increment the cursor until we are on the last iteration + // or until we hit the end of the buffer + if i != count.saturating_sub(1) && !self.cursor.inc() { + break + } + } + } + Verb::ToggleCaseInplace(count) => { + for i in 0..count { + let Some(gr) = self.grapheme_at_cursor() else { + return Ok(()) + }; + if gr.len() > 1 || gr.is_empty() { + return Ok(()) + } + let ch = gr.chars().next().unwrap(); + if !ch.is_alphabetic() { + return Ok(()) + } + let mut buf = [0u8;4]; + let new = if ch.is_ascii_lowercase() { + ch.to_ascii_uppercase().encode_utf8(&mut buf) + } else { + ch.to_ascii_lowercase().encode_utf8(&mut buf) + }; + self.replace_at_cursor(new); + + // try to increment the cursor until we are on the last iteration + // or until we hit the end of the buffer + if i != count.saturating_sub(1) && !self.cursor.inc() { + break + } + } + } + Verb::ToggleCaseRange => { + let Some((start,end)) = self.range_from_motion(&motion) else { + return Ok(()) + }; + for i in start..end { + let Some(gr) = self.grapheme_at(i) else { + continue + }; + if gr.len() > 1 || gr.is_empty() { + continue + } + let ch = gr.chars().next().unwrap(); + if !ch.is_alphabetic() { + continue + } + let mut buf = [0u8;4]; + let new = if ch.is_ascii_lowercase() { + ch.to_ascii_uppercase().encode_utf8(&mut buf) + } else { + ch.to_ascii_lowercase().encode_utf8(&mut buf) + }; + self.replace_at(i,new); + } + } + Verb::ToLower => { + let Some((start,end)) = self.range_from_motion(&motion) else { + return Ok(()) + }; + for i in start..end { + let Some(gr) = self.grapheme_at(i) else { + continue + }; + if gr.len() > 1 || gr.is_empty() { + continue + } + let ch = gr.chars().next().unwrap(); + if !ch.is_alphabetic() { + continue + } + let mut buf = [0u8;4]; + let new = if ch.is_ascii_uppercase() { + ch.to_ascii_lowercase().encode_utf8(&mut buf) + } else { + ch.encode_utf8(&mut buf) + }; + self.replace_at(i,new); + } + } + Verb::ToUpper => { + let Some((start,end)) = self.range_from_motion(&motion) else { + return Ok(()) + }; + for i in start..end { + let Some(gr) = self.grapheme_at(i) else { + continue + }; + if gr.len() > 1 || gr.is_empty() { + continue + } + let ch = gr.chars().next().unwrap(); + if !ch.is_alphabetic() { + continue + } + let mut buf = [0u8;4]; + let new = if ch.is_ascii_lowercase() { + ch.to_ascii_uppercase().encode_utf8(&mut buf) + } else { + ch.encode_utf8(&mut buf) + }; + self.replace_at(i,new); + } + } + Verb::Redo | + Verb::Undo => { + let (edit_provider,edit_receiver) = match verb { + // Redo = pop from redo stack, push to undo stack + Verb::Redo => (&mut self.redo_stack, &mut self.undo_stack), + // Undo = pop from undo stack, push to redo stack + Verb::Undo => (&mut self.undo_stack, &mut self.redo_stack), + _ => unreachable!() + }; + let Some(edit) = edit_provider.pop() else { return Ok(()) }; + let Edit { pos, cursor_pos, old, new, merging: _ } = edit; + + self.buffer.replace_range(pos..pos + new.len(), &old); + let new_cursor_pos = self.cursor.get(); + let in_insert_mode = !self.cursor.exclusive; + + if in_insert_mode { + self.cursor.set(cursor_pos) + } + let new_edit = Edit { pos, cursor_pos: new_cursor_pos, old: new, new: old, merging: false }; + edit_receiver.push(new_edit); + self.update_graphemes(); + } + Verb::RepeatLast => todo!(), + Verb::Put(anchor) => { + let Some(content) = register.read_from_register() else { + return Ok(()) + }; + let insert_idx = match anchor { + Anchor::After => self.cursor.ret_add(1), + Anchor::Before => self.cursor.get() + }; + self.insert_str_at(insert_idx, &content); + self.cursor.add(content.len().saturating_sub(1)); + } + Verb::SwapVisualAnchor => { + if let Some((start,end)) = self.select_range() { + if let Some(mut mode) = self.select_mode { + mode.invert_anchor(); + let new_cursor_pos = match mode.anchor() { + SelectAnchor::Start => start, + SelectAnchor::End => end, + }; + self.cursor.set(new_cursor_pos); + self.select_mode = Some(mode) + } + } + } + Verb::JoinLines => { + let start = self.start_of_line(); + let Some((_,mut end)) = self.nth_next_line(1) else { + return Ok(()) + }; + end = end.saturating_sub(1); // exclude the last newline + let mut last_was_whitespace = false; + for i in start..end { + let Some(gr) = self.grapheme_at(i) else { + continue + }; + if gr == "\n" { + if last_was_whitespace { + self.remove(i); + } else { + self.force_replace_at(i, " "); + } + last_was_whitespace = false; + continue + } + last_was_whitespace = is_whitespace(gr); + } + } + Verb::InsertChar(ch) => { + self.insert_at_cursor(ch); + self.cursor.add(1); + } + Verb::Insert(string) => { + self.push_str(&string); + let graphemes = string.graphemes(true).count(); + self.cursor.add(graphemes); + } + Verb::Indent => { + let Some((start,end)) = self.range_from_motion(&motion) else { + return Ok(()) + }; + self.insert_at(start, '\t'); + let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter(); + while let Some(idx) = range_indices.next() { + let gr = self.grapheme_at(idx).unwrap(); + if gr == "\n" { + let Some(idx) = range_indices.next() else { + self.push('\t'); + break + }; + self.insert_at(idx, '\t'); + } + } + + match motion { + MotionKind::ExclusiveWithTargetCol((_,_),pos) | + MotionKind::InclusiveWithTargetCol((_,_),pos) => { + self.cursor.set(start); + let end = self.end_of_line(); + self.cursor.add(end.min(pos)); + } + _ => self.cursor.set(start), + } + } + Verb::Dedent => { + let (start,end) = self.this_line(); + + } + Verb::Equalize => todo!(), + Verb::InsertModeLineBreak(anchor) => { + let (mut start,end) = self.this_line(); + if start == 0 && end == self.cursor.max { + match anchor { + Anchor::After => { + self.push('\n'); + self.cursor.set(self.cursor_max()); + return Ok(()) + } + Anchor::Before => { + self.insert_at(0, '\n'); + self.cursor.set(0); + return Ok(()) + } + } + } + // We want the position of the newline, or start of buffer + start = start.saturating_sub(1).min(self.cursor.max); + match anchor { + Anchor::After => { + self.cursor.set(end); + self.insert_at_cursor('\n'); + } + Anchor::Before => { + self.cursor.set(start); + self.insert_at_cursor('\n'); + self.cursor.add(1); + } + } + } + + Verb::Complete | + Verb::EndOfFile | + Verb::InsertMode | + Verb::NormalMode | + Verb::VisualMode | + Verb::ReplaceMode | + Verb::VisualModeLine | + Verb::VisualModeBlock | + Verb::CompleteBackward | + Verb::AcceptLineOrNewline | + Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these + } + Ok(()) + } pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit()); let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert()); let is_line_motion = cmd.is_line_motion(); let is_undo_op = cmd.is_undo_op(); + let is_inplace_edit = cmd.is_inplace_edit(); + let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging); // Merge character inserts into one edit - if self.edit_is_merging() && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert()) { + if edit_is_merging && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert()) { if let Some(edit) = self.undo_stack.last_mut() { edit.stop_merge(); } } - let ViCmd { register, verb, motion, .. } = cmd; + let ViCmd { register, verb, motion, flags, raw_seq: _ } = cmd; - let verb_count = verb.as_ref().map(|v| v.0); - let motion_count = motion.as_ref().map(|m| m.0); + let verb_cmd_ref = verb.as_ref(); + let verb_ref = verb_cmd_ref.map(|v| v.1.clone()); + let verb_count = verb_cmd_ref.map(|v| v.0).unwrap_or(1); let before = self.buffer.clone(); - let cursor_pos = self.cursor; + let cursor_pos = self.cursor.get(); - for _ in 0..verb_count.unwrap_or(1) { - for _ in 0..motion_count.unwrap_or(1) { - let motion_eval = motion - .clone() - .map(|m| self.eval_motion(m.1)) - .unwrap_or({ - self.selected_range - .clone() - .map(MotionKind::range) - .unwrap_or(MotionKind::Null) - }); + /* + * Let's evaluate the motion now + * If we got some weird command like 'dvw' we will have to simulate a visual selection to get the range + * If motion is None, we will try to use self.select_range + * If self.select_range is None, we will use MotionKind::Null + */ + let motion_eval = if flags.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) { + let motion = motion + .clone() + .map(|m| self.eval_motion(verb_ref.as_ref(), m)) + .unwrap_or(MotionKind::Null); + let mode = match flags { + CmdFlags::VISUAL => SelectMode::Char(SelectAnchor::End), + CmdFlags::VISUAL_LINE => SelectMode::Line(SelectAnchor::End), + CmdFlags::VISUAL_BLOCK => SelectMode::Block(SelectAnchor::End), + _ => unreachable!() + }; + // Start a selection + self.start_selecting(mode); + // Apply the cursor motion + self.apply_motion(motion); - if let Some(verb) = verb.clone() { - self.exec_verb(verb.1, motion_eval, register)?; - } else if self.has_hint() { - let motion_eval = motion - .clone() - .map(|m| self.eval_motion_with_hint(m.1)) - .unwrap_or(MotionKind::Null); - self.apply_motion_with_hint(motion_eval); - } else { - self.apply_motion(/*forced*/ false,motion_eval); - } - } + // Use the selection range created by the motion + self.select_range + .map(MotionKind::Inclusive) + .unwrap_or(MotionKind::Null) + } else { + motion + .clone() + .map(|m| self.eval_motion(verb_ref.as_ref(), m)) + .unwrap_or({ + self.select_range + .map(MotionKind::Inclusive) + .unwrap_or(MotionKind::Null) + }) + }; + + if let Some(verb) = verb.clone() { + self.exec_verb(verb.1, motion_eval, register)?; + } else { + self.apply_motion(motion_eval); } + /* Done executing, do some cleanup */ + let after = self.buffer.clone(); if clear_redos { self.redo_stack.clear(); } - if before != after && !is_undo_op { - self.handle_edit(before, after, cursor_pos); + if before != after { + if !is_undo_op { + self.handle_edit(before, after, cursor_pos); + } + /* + * The buffer has been edited, + * which invalidates the grapheme_indices vector + * We set it to None now, so that self.update_graphemes_lazy() + * will update it when it is needed again + */ + self.update_graphemes(); } if !is_line_motion { @@ -2090,32 +2130,33 @@ impl LineBuf { } } - - if self.clamp_cursor { - self.clamp_cursor(); - } - self.sync_cursor(); Ok(()) } + pub fn as_str(&self) -> &str { + &self.buffer // FIXME: this will have to be fixed up later + } } impl Display for LineBuf { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut full_buf = self.buffer.clone(); - if let Some(range) = self.selected_range.clone() { - let mode = self.select_mode.unwrap_or_default(); + if let Some((start,end)) = self.select_range.clone() { + let mode = self.select_mode.unwrap(); + let start_byte = self.read_idx_byte_pos(start); + let end_byte = self.read_idx_byte_pos(end); + match mode.anchor() { - SelectionAnchor::Start => { - let mut inclusive = range.start..=range.end; - if *inclusive.end() == self.byte_len() { - inclusive = range.start..=range.end.saturating_sub(1); + SelectAnchor::Start => { + let mut inclusive = start_byte..=end_byte; + if *inclusive.end() == full_buf.len() { + inclusive = start_byte..=end_byte.saturating_sub(1); } let selected = full_buf[inclusive.clone()].styled(Style::BgWhite | Style::Black); full_buf.replace_range(inclusive, &selected); } - SelectionAnchor::End => { - let selected = full_buf[range.clone()].styled(Style::BgWhite | Style::Black); - full_buf.replace_range(range, &selected); + SelectAnchor::End => { + let selected = full_buf[start..end].styled(Style::BgWhite | Style::Black); + full_buf.replace_range(start_byte..end_byte, &selected); } } } @@ -2126,32 +2167,7 @@ impl Display for LineBuf { } } -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 -} - +/// Rotate alphabetic characters by 13 alphabetic positions pub fn rot13(input: &str) -> String { input.chars() .map(|c| { @@ -2164,19 +2180,13 @@ pub fn rot13(input: &str) -> String { } else { c } - }) - .collect() + }).collect() } -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_inclusive(a: usize, b: usize) -> Range { - let b = b + 1; - std::cmp::min(a, b)..std::cmp::max(a, b) -} - -fn mk_range(a: usize, b: usize) -> Range { - std::cmp::min(a, b)..std::cmp::max(a, b) +pub fn ordered(start: usize, end: usize) -> (usize,usize) { + if start > end { + (end,start) + } else { + (start,end) + } } diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index fce4ce7..3b84fd8 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -1,141 +1,186 @@ -use std::time::Duration; - use history::{History, SearchConstraint, SearchKind}; use keys::{KeyCode, KeyEvent, ModKeys}; -use linebuf::{strip_ansi_codes_and_escapes, LineBuf, SelectionAnchor, SelectionMode}; -use mode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; -use term::Terminal; -use unicode_width::UnicodeWidthStr; -use vicmd::{Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd}; +use linebuf::{LineBuf, SelectAnchor, SelectMode}; +use nix::libc::STDOUT_FILENO; +use term::{get_win_size, raw_mode, KeyReader, Layout, LineWriter, TermReader, TermWriter}; +use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd}; +use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; -use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}}; +use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit, term::{Style, Styled}}; use crate::prelude::*; -pub mod keys; pub mod term; pub mod linebuf; +pub mod layout; +pub mod keys; pub mod vicmd; -pub mod mode; pub mod register; +pub mod vimode; pub mod history; -const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore\nmagna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo\nconsequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; - -/* - * Known issues: - * If the line buffer scrolls past the terminal height, shit gets fucked - * the cursor sometimes spazzes out during redraw, but ends up in the right place - */ - -/// Unified interface for different line editing methods pub trait Readline { fn readline(&mut self) -> ShResult; } pub struct FernVi { - term: Terminal, - line: LineBuf, - history: History, - prompt: String, - mode: Box, - last_action: Option, - last_movement: Option, + pub reader: Box, + pub writer: Box, + pub prompt: String, + pub mode: Box, + pub old_layout: Option, + pub repeat_action: Option, + pub repeat_motion: Option, + pub editor: LineBuf, + pub history: History } impl Readline for FernVi { fn readline(&mut self) -> ShResult { - /* a monument to the insanity of debugging this shit - 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); - } - panic!() - */ - self.print_buf(false)?; - loop { - let key = self.term.read_key(); + let raw_mode_guard = raw_mode(); // Restores termios state on drop + + loop { + raw_mode_guard.disable_for(|| self.print_line())?; + + let Some(key) = self.reader.read_key() else { + raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; + std::mem::drop(raw_mode_guard); + return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF")) + }; + flog!(DEBUG, key); - if let KeyEvent(KeyCode::Char('V'), ModKeys::CTRL) = key { - self.handle_verbatim()?; - continue - } if self.should_accept_hint(&key) { - self.line.accept_hint(); - self.history.update_pending_cmd(self.line.as_str()); - self.print_buf(true)?; + self.editor.accept_hint(); + self.history.update_pending_cmd(self.editor.as_str()); + self.print_line()?; continue } - let Some(cmd) = self.mode.handle_key(key) else { + let Some(mut cmd) = self.mode.handle_key(key) else { continue }; + cmd.alter_line_motion_if_no_verb(); if self.should_grab_history(&cmd) { - flog!(DEBUG, "scrolling"); self.scroll_history(cmd); - self.print_buf(true)?; + self.print_line()?; continue } - - if cmd.should_submit() { - self.term.unposition_cursor()?; - self.term.write("\n"); - let command = std::mem::take(&mut self.line).pack_line(); - if !command.is_empty() { - // We're just going to trim the command - // reduces clutter in the case of two history commands whose only difference is insignificant whitespace - self.history.update_pending_cmd(&command); - self.history.save()?; + raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; + std::mem::drop(raw_mode_guard); + return Ok(self.editor.take_buf()) + } + + if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) { + if self.editor.buffer.is_empty() { + std::mem::drop(raw_mode_guard); + sh_quit(0); + } else { + self.editor.buffer.clear(); + continue } - return Ok(command); } - let line = self.line.to_string(); - self.exec_cmd(cmd.clone())?; - let new_line = self.line.as_str(); - let has_changes = line != new_line; - flog!(DEBUG, has_changes); + flog!(DEBUG,cmd); - if has_changes { - self.history.update_pending_cmd(self.line.as_str()); + let before = self.editor.buffer.clone(); + self.exec_cmd(cmd)?; + let after = self.editor.as_str(); + + if before != after { + self.history.update_pending_cmd(self.editor.as_str()); } - self.print_buf(true)?; + let hint = self.history.get_hint(); + self.editor.set_hint(hint); } } } impl FernVi { pub fn new(prompt: Option) -> ShResult { - let prompt = prompt.unwrap_or("$ ".styled(Style::Green | Style::Bold)); - let line = LineBuf::new();//.with_initial(LOREM_IPSUM); - let term = Terminal::new(); - let history = History::new()?; Ok(Self { - term, - line, - history, - prompt, + reader: Box::new(TermReader::new()), + writer: Box::new(TermWriter::new(STDOUT_FILENO)), + prompt: prompt.unwrap_or("$ ".styled(Style::Green)), mode: Box::new(ViInsert::new()), - last_action: None, - last_movement: None, + old_layout: None, + repeat_action: None, + repeat_motion: None, + editor: LineBuf::new().with_initial("this buffer has (some delimited) text", 0), + history: History::new()? }) } + + pub fn get_layout(&mut self) -> Layout { + let line = self.editor.to_string(); + flog!(DEBUG,line); + let to_cursor = self.editor.slice_to_cursor().unwrap_or_default(); + let (cols,_) = get_win_size(STDIN_FILENO); + Layout::from_parts( + /*tab_stop:*/ 8, + cols, + &self.prompt, + to_cursor, + &line + ) + } + pub fn scroll_history(&mut self, cmd: ViCmd) { + flog!(DEBUG,"scrolling"); + /* + if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) { + let constraint = SearchConstraint::new(SearchKind::Prefix, self.editor.to_string()); + self.history.constrain_entries(constraint); + } + */ + let count = &cmd.motion().unwrap().0; + let motion = &cmd.motion().unwrap().1; + flog!(DEBUG,count,motion); + flog!(DEBUG,self.history.masked_entries()); + let entry = match motion { + Motion::LineUpCharwise => { + let Some(hist_entry) = self.history.scroll(-(*count as isize)) else { + return + }; + flog!(DEBUG,"found entry"); + flog!(DEBUG,hist_entry.command()); + hist_entry + } + Motion::LineDownCharwise => { + let Some(hist_entry) = self.history.scroll(*count as isize) else { + return + }; + flog!(DEBUG,"found entry"); + flog!(DEBUG,hist_entry.command()); + hist_entry + } + _ => unreachable!() + }; + let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col()); + let mut buf = LineBuf::new().with_initial(entry.command(),0); + let line_end = buf.end_of_line(); + if let Some(dest) = self.mode.hist_scroll_start_pos() { + match dest { + To::Start => { + /* Already at 0 */ + } + To::End => { + // History entries cannot be empty + // So this subtraction is safe (maybe) + buf.cursor.add(line_end); + } + } + } else { + let target = (col).min(line_end); + buf.cursor.add(target); + } + + self.editor = buf + } pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { - if self.line.at_end_of_buffer() && self.line.has_hint() { + flog!(DEBUG,self.editor.cursor_at_max()); + flog!(DEBUG,self.editor.cursor); + if self.editor.cursor_at_max() && self.editor.has_hint() { match self.mode.report_mode() { ModeReport::Replace | ModeReport::Insert => { @@ -164,211 +209,97 @@ impl FernVi { false } } - /// Ctrl+V handler - pub fn handle_verbatim(&mut self) -> ShResult<()> { - let mut buf = [0u8; 8]; - let mut collected = Vec::new(); - loop { - let n = self.term.read_byte(&mut buf[..1]); - if n == 0 { - continue; - } - collected.push(buf[0]); - - // If it starts with ESC, treat as escape sequence - if collected[0] == 0x1b { - loop { - let n = self.term.peek_byte(&mut buf[..1]); - if n == 0 { - break - } - collected.push(buf[0]); - // Ends a CSI sequence - if (0x40..=0x7e).contains(&buf[0]) { - break; - } - } - let Ok(seq) = std::str::from_utf8(&collected) else { - return Ok(()) - }; - let cmd = ViCmd { - register: Default::default(), - verb: Some(VerbCmd(1, Verb::Insert(seq.to_string()))), - motion: None, - raw_seq: seq.to_string(), - }; - self.line.exec_cmd(cmd)?; - } - - // Optional: handle other edge cases, e.g., raw control codes - if collected[0] < 0x20 || collected[0] == 0x7F { - let ctrl_seq = std::str::from_utf8(&collected).unwrap(); - let cmd = ViCmd { - register: Default::default(), - verb: Some(VerbCmd(1, Verb::Insert(ctrl_seq.to_string()))), - motion: None, - raw_seq: ctrl_seq.to_string(), - }; - self.line.exec_cmd(cmd)?; - break; - } - - // Try to parse as UTF-8 if it's a valid Unicode sequence - if let Ok(s) = std::str::from_utf8(&collected) { - if s.chars().count() == 1 { - let ch = s.chars().next().unwrap(); - // You got a literal Unicode char - eprintln!("Got char: {:?}", ch); - break; - } - } - - } - Ok(()) - } - pub fn scroll_history(&mut self, cmd: ViCmd) { - if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) { - let constraint = SearchConstraint::new(SearchKind::Prefix, self.line.to_string()); - self.history.constrain_entries(constraint); - } - let count = &cmd.motion().unwrap().0; - let motion = &cmd.motion().unwrap().1; - flog!(DEBUG,count,motion); - let entry = match motion { - Motion::LineUp => { - let Some(hist_entry) = self.history.scroll(-(*count as isize)) else { - return - }; - flog!(DEBUG,"found entry"); - flog!(DEBUG,hist_entry.command()); - hist_entry - } - Motion::LineDown => { - let Some(hist_entry) = self.history.scroll(*count as isize) else { - return - }; - flog!(DEBUG,"found entry"); - flog!(DEBUG,hist_entry.command()); - hist_entry - } - _ => unreachable!() - }; - let col = self.line.saved_col().unwrap_or(self.line.cursor_column()); - let mut buf = LineBuf::new().with_initial(entry.command()); - let line_end = buf.end_of_line(); - if let Some(dest) = self.mode.hist_scroll_start_pos() { - match dest { - To::Start => { - /* Already at 0 */ - } - To::End => { - // History entries cannot be empty - // So this subtraction is safe (maybe) - buf.cursor_fwd_to(line_end + 1); - } - } - } else { - let target = (col + 1).min(line_end + 1); - buf.cursor_fwd_to(target); - } - - self.line = buf - } - - pub fn should_grab_history(&self, cmd: &ViCmd) -> bool { + pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool { cmd.verb().is_none() && ( - cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUp))) && - self.line.start_of_line() == 0 + cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise))) && + self.editor.start_of_line() == 0 ) || ( - cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDown))) && - self.line.end_of_line() == self.line.byte_len() + cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) && + self.editor.end_of_line() == self.editor.cursor_max() && + !self.history.cursor_entry().is_some_and(|ent| ent.is_new()) ) } - pub fn print_buf(&mut self, refresh: bool) -> ShResult<()> { - let (height,width) = self.term.get_dimensions()?; - if refresh { - self.term.unwrite()?; + + pub fn print_line(&mut self) -> ShResult<()> { + let new_layout = self.get_layout(); + if let Some(layout) = self.old_layout.as_ref() { + self.writer.clear_rows(layout)?; } - let hint = self.history.get_hint(); - self.line.set_hint(hint); - let offset = self.calculate_prompt_offset(); - self.line.set_first_line_offset(offset); - self.line.update_term_dims((height,width)); - let mut line_buf = self.prompt.clone(); - line_buf.push_str(&self.line.to_string()); + self.writer.redraw( + &self.prompt, + &self.editor, + &new_layout + )?; - self.term.recorded_write(&line_buf, offset)?; - self.term.position_cursor(self.line.cursor_display_coords(width))?; + self.writer.flush_write(&self.mode.cursor_style())?; - self.term.write(&self.mode.cursor_style()); + self.old_layout = Some(new_layout); 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, mut cmd: ViCmd) -> ShResult<()> { let mut selecting = false; + let mut is_insert_mode = false; if cmd.is_mode_transition() { let count = cmd.verb_count(); let mut mode: Box = match cmd.verb().unwrap().1 { Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => { + is_insert_mode = true; Box::new(ViInsert::new().with_count(count as u16)) } + Verb::NormalMode => { Box::new(ViNormal::new()) } - Verb::ReplaceMode => { - Box::new(ViReplace::new().with_count(count as u16)) - } + + Verb::ReplaceMode => Box::new(ViReplace::new()), + Verb::VisualModeSelectLast => { if self.mode.report_mode() != ModeReport::Visual { - self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End)); + self.editor.start_selecting(SelectMode::Char(SelectAnchor::End)); } let mut mode: Box = Box::new(ViVisual::new()); std::mem::swap(&mut mode, &mut self.mode); - self.line.set_cursor_clamp(self.mode.clamp_cursor()); - self.line.set_move_cursor_on_undo(self.mode.move_cursor_on_undo()); - self.term.write(&mode.cursor_style()); - return self.line.exec_cmd(cmd) + self.editor.set_cursor_clamp(self.mode.clamp_cursor()); + + return self.editor.exec_cmd(cmd) } Verb::VisualMode => { selecting = true; - self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End)); Box::new(ViVisual::new()) } + _ => unreachable!() }; - flog!(DEBUG, self.mode.report_mode()); - flog!(DEBUG, mode.report_mode()); std::mem::swap(&mut mode, &mut self.mode); - flog!(DEBUG, self.mode.report_mode()); - self.line.set_cursor_clamp(self.mode.clamp_cursor()); - self.line.set_move_cursor_on_undo(self.mode.move_cursor_on_undo()); - self.term.write(&mode.cursor_style()); - if mode.is_repeatable() { - self.last_action = mode.as_replay(); + self.repeat_action = mode.as_replay(); } - self.line.exec_cmd(cmd)?; + + self.editor.exec_cmd(cmd)?; + self.editor.set_cursor_clamp(self.mode.clamp_cursor()); + if selecting { - self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End)); + self.editor.start_selecting(SelectMode::Char(SelectAnchor::End)); } else { - self.line.stop_selecting(); + self.editor.stop_selecting(); + } + if is_insert_mode { + self.editor.mark_insert_mode_start_pos(); + } else { + self.editor.clear_insert_mode_start_pos(); } return Ok(()) } else if cmd.is_cmd_repeat() { - let Some(replay) = self.last_action.clone() else { + let Some(replay) = self.repeat_action.clone() else { return Ok(()) }; let ViCmd { verb, .. } = cmd; @@ -381,7 +312,7 @@ impl FernVi { for _ in 0..repeat { let cmds = cmds.clone(); for cmd in cmds { - self.line.exec_cmd(cmd)? + self.editor.exec_cmd(cmd)? } } } @@ -399,7 +330,7 @@ impl FernVi { return Ok(()) // it has to have a verb to be repeatable, something weird happened } } - self.line.exec_cmd(cmd)?; + self.editor.exec_cmd(cmd)?; } _ => unreachable!("motions should be handled in the other branch") } @@ -407,19 +338,20 @@ impl FernVi { } else if cmd.is_motion_repeat() { match cmd.motion.as_ref().unwrap() { MotionCmd(count,Motion::RepeatMotion) => { - let Some(motion) = self.last_movement.clone() else { + let Some(motion) = self.repeat_motion.clone() else { return Ok(()) }; let repeat_cmd = ViCmd { register: RegisterName::default(), verb: None, motion: Some(motion), - raw_seq: format!("{count};") + raw_seq: format!("{count};"), + flags: CmdFlags::empty() }; - return self.line.exec_cmd(repeat_cmd); + return self.editor.exec_cmd(repeat_cmd); } MotionCmd(count,Motion::RepeatMotionRev) => { - let Some(motion) = self.last_movement.clone() else { + let Some(motion) = self.repeat_motion.clone() else { return Ok(()) }; let mut new_motion = motion.invert_char_motion(); @@ -428,9 +360,10 @@ impl FernVi { register: RegisterName::default(), verb: None, motion: Some(new_motion), - raw_seq: format!("{count},") + raw_seq: format!("{count},"), + flags: CmdFlags::empty() }; - return self.line.exec_cmd(repeat_cmd); + return self.editor.exec_cmd(repeat_cmd); } _ => unreachable!() } @@ -440,23 +373,24 @@ impl FernVi { if self.mode.report_mode() == ModeReport::Visual { // The motion is assigned in the line buffer execution, so we also have to assign it here // in order to be able to repeat it - let range = self.line.selected_range().unwrap(); - cmd.motion = Some(MotionCmd(1,Motion::Range(range.start, range.end))) + let range = self.editor.select_range().unwrap(); + cmd.motion = Some(MotionCmd(1,Motion::Range(range.0, range.1))) } - self.last_action = Some(CmdReplay::Single(cmd.clone())); + self.repeat_action = Some(CmdReplay::Single(cmd.clone())); } if cmd.is_char_search() { - self.last_movement = cmd.motion.clone() + self.repeat_motion = cmd.motion.clone() } - self.line.exec_cmd(cmd.clone())?; + self.editor.exec_cmd(cmd.clone())?; if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) { - self.line.stop_selecting(); + self.editor.stop_selecting(); let mut mode: Box = Box::new(ViNormal::new()); std::mem::swap(&mut mode, &mut self.mode); } Ok(()) } } + diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 2526929..36f3468 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -1,459 +1,690 @@ -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; +use std::{env, fmt::{Debug, Write}, io::{BufRead, BufReader, Read}, iter::Peekable, os::fd::{AsFd, BorrowedFd, RawFd}, str::Chars}; -use crate::libsh::error::ShResult; +use nix::{errno::Errno, libc::{self, STDIN_FILENO}, poll::{self, PollFlags, PollTimeout}, sys::termios, unistd::isatty}; +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; + +use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prompt::readline::keys::{KeyCode, ModKeys}}; use crate::prelude::*; -use super::keys::{KeyCode, KeyEvent, ModKeys}; +use super::{keys::KeyEvent, linebuf::LineBuf}; -#[derive(Default,Debug)] -struct WriteMap { - lines: usize, - cols: usize, - offset: usize +pub fn raw_mode() -> RawModeGuard { + let orig = termios::tcgetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}).expect("Failed to get terminal attributes"); + let mut raw = orig.clone(); + termios::cfmakeraw(&mut raw); + termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &raw) + .expect("Failed to set terminal to raw mode"); + RawModeGuard { orig, fd: STDIN_FILENO } } -#[derive(Debug)] -pub struct Terminal { - stdin: RawFd, - stdout: RawFd, - recording: bool, - write_records: WriteMap, - cursor_records: WriteMap +pub type Row = u16; +pub type Col = u16; + +#[derive(Default,Clone,Copy,PartialEq,Eq,PartialOrd,Ord,Debug)] +pub struct Pos { + col: Col, + row: Row } -impl Terminal { - pub fn new() -> Self { - assert!(isatty(STDIN_FILENO).unwrap()); - 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(), - } +// I'd like to thank rustyline for this idea +nix::ioctl_read_bad!(win_size, libc::TIOCGWINSZ, libc::winsize); + +pub fn get_win_size(fd: RawFd) -> (Col,Row) { + use std::mem::zeroed; + + if cfg!(test) { + return (80,24) } - fn raw_mode() -> termios::Termios { - let orig = termios::tcgetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}).expect("Failed to get terminal attributes"); - let mut raw = orig.clone(); - termios::cfmakeraw(&mut raw); - termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &raw) - .expect("Failed to set terminal to raw mode"); - orig - } - - pub fn restore_termios(termios: termios::Termios) { - termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &termios) - .expect("Failed to restore terminal settings"); - } - - - pub fn get_dimensions(&self) -> ShResult<(usize, usize)> { - if !isatty(self.stdin).unwrap_or(false) { - return Err(io::Error::new(io::ErrorKind::Other, "Not a TTY"))?; - } - - let mut ws: winsize = unsafe { zeroed() }; - - let res = unsafe { libc::ioctl(self.stdin, TIOCGWINSZ, &mut ws) }; - if res == -1 { - return Err(io::Error::last_os_error())?; - } - - Ok((ws.ws_row as usize, ws.ws_col as usize)) - } - - pub fn 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(&mut self) { - self.write("\x1b[u") - } - - pub fn move_cursor_to(&mut self, (row,col): (usize,usize)) { - self.write(&format!("\x1b[{row};{col}H",)) - } - - pub fn with_raw_mode R, R>(func: F) -> R { - let saved = Self::raw_mode(); - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(func)); - Self::restore_termios(saved); - match result { - Ok(r) => r, - Err(e) => std::panic::resume_unwind(e), - } - } - - pub fn read_byte(&self, buf: &mut [u8]) -> usize { - Self::with_raw_mode(|| { - read(self.stdin, buf).expect("Failed to read from stdin") - }) - } - - fn read_blocks_then_read(&self, buf: &mut [u8], timeout: Duration) -> Option { - Self::with_raw_mode(|| { - self.read_blocks(false); - let start = Instant::now(); - loop { - match read(self.stdin, buf) { - Ok(n) if n > 0 => { - self.read_blocks(true); - return Some(n); - } - Ok(_) => {} - Err(e) if e == Errno::EAGAIN => {} - Err(_) => return None, - } - if start.elapsed() > timeout { - self.read_blocks(true); - return None; - } - sleep(Duration::from_millis(1)); + unsafe { + let mut size: libc::winsize = zeroed(); + match win_size(fd, &mut size) { + Ok(0) => { + /* rustyline code says: + In linux pseudo-terminals are created with dimensions of + zero. If host application didn't initialize the correct + size before start we treat zero size as 80 columns and + infinite rows + */ + let cols = if size.ws_col == 0 { 80 } else { size.ws_col }; + let rows = if size.ws_row == 0 { + u16::MAX + } else { + size.ws_row + }; + (cols, rows) } - }) + _ => (80,24) + } } +} - /// 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(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)); - } - }) +fn write_all(fd: RawFd, buf: &str) -> nix::Result<()> { + let mut bytes = buf.as_bytes(); + while !bytes.is_empty() { + match nix::unistd::write(unsafe { BorrowedFd::borrow_raw(fd) }, bytes) { + Ok(0) => return Err(Errno::EIO), + Ok(n) => bytes = &bytes[n..], + Err(Errno::EINTR) => {} + Err(r) => return Err(r), + } } + Ok(()) +} - 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 +// Big credit to rustyline for this +fn width(s: &str, esc_seq: &mut u8) -> u16 { + let w_calc = width_calculator(); + if *esc_seq == 1 { + if s == "[" { + // CSI + *esc_seq = 2; } else { - flags & !OFlag::O_NONBLOCK - }; - fcntl(self.stdin, FcntlArg::F_SETFL(new_flags)).unwrap(); - } - - 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(()) - } - - /// Rewinds terminal writing, clears lines and lands on the anchor point of the prompt - pub fn unwrite(&mut self) -> ShResult<()> { - self.unposition_cursor()?; - let WriteMap { lines, cols, offset } = self.write_records; - for _ in 0..lines { - self.write_unrecorded("\x1b[2K\x1b[A") + // two-character sequence + *esc_seq = 0; } - let col = offset; - self.write_unrecorded(&format!("\x1b[{col}G\x1b[0K")); - self.reset_records(); - Ok(()) + 0 + } else if *esc_seq == 2 { + if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') { + /*} else if s == "m" { + // last + *esc_seq = 0;*/ + } else { + // not supported + *esc_seq = 0; } - - pub fn position_cursor(&mut self, (lines,col): (usize,usize)) -> ShResult<()> { - flog!(DEBUG,lines); - flog!(DEBUG,col); - self.cursor_records.lines = lines; - self.cursor_records.cols = col; - self.cursor_records.offset = self.cursor_pos().1; - - for _ in 0..lines { - self.write_unrecorded("\x1b[A") - } - - let (_, width) = self.get_dimensions().unwrap(); - // holy hack spongebob - // basically if we've written to the edge of the terminal - // and the cursor is at term_width + 1 (column 1 on the next line) - // then we are going to manually write a newline - // to position the cursor correctly - if self.write_records.cols == width && self.cursor_records.cols == 1 { - self.cursor_records.lines += 1; - self.write_records.lines += 1; - self.cursor_records.cols = 1; - self.write_records.cols = 1; - write(unsafe { BorrowedFd::borrow_raw(self.stdout) }, b"\n").expect("Failed to write to stdout"); - } - - self.write_unrecorded(&format!("\x1b[{col}G")); - - Ok(()) + 0 + } else if s == "\x1b" { + *esc_seq = 1; + 0 + } else if s == "\n" { + 0 + } else { + w_calc.width(s) as u16 } +} - /// Rewinds cursor positioning, lands on the end of the buffer - pub fn unposition_cursor(&mut self) ->ShResult<()> { - let WriteMap { lines, cols, offset } = self.cursor_records; - - for _ in 0..lines { - self.write_unrecorded("\x1b[B") - } - - self.write_unrecorded(&format!("\x1b[{offset}G")); - - Ok(()) +pub fn width_calculator() -> Box { + match env::var("TERM_PROGRAM").as_deref() { + Ok("Apple_Terminal") => Box::new(UnicodeWidth), + Ok("iTerm.app") => Box::new(UnicodeWidth), + Ok("WezTerm") => Box::new(UnicodeWidth), + Err(std::env::VarError::NotPresent) => match std::env::var("TERM").as_deref() { + Ok("xterm-kitty") => Box::new(NoZwj), + _ => Box::new(WcWidth) + }, + _ => Box::new(WcWidth) } +} - pub fn write_bytes(&mut self, buf: &[u8], record: bool) { - if self.recording && record { // The function parameter allows us to make sneaky writes while the terminal is 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 { - flog!(DEBUG,ch_width,self.write_records.cols,width,self.write_records.lines); - self.write_records.lines += 1; - self.write_records.cols = ch_width; - } - self.write_records.cols += ch_width; - } - } - } - flog!(DEBUG,self.write_records.cols); - } - write(unsafe { BorrowedFd::borrow_raw(self.stdout) }, buf).expect("Failed to write to stdout"); - } - - - pub fn write(&mut self, s: &str) { - self.write_bytes(s.as_bytes(), true); - } - - pub fn write_unrecorded(&mut self, s: &str) { - self.write_bytes(s.as_bytes(), false); - } - - pub fn writeln(&mut self, s: &str) { - self.write(s); - self.write_bytes(b"\n", true); - } - - pub fn clear(&mut self) { - self.write_bytes(b"\x1b[2J\x1b[H", false); - } - - pub fn read_key(&self) -> KeyEvent { - use core::str; - - 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 { +fn read_digits_until(rdr: &mut TermReader, sep: char) -> ShResult> { + let mut num: u32 = 0; + loop { + match rdr.next_byte()? as char { + digit @ '0'..='9' => { + let digit = digit.to_digit(10).unwrap(); + num = append_digit(num, digit); continue; } - collected.push(buf[0]); + c if c == sep => break, + _ => return Ok(None), + } + } + Ok(Some(num)) +} - // ESC sequences - if collected[0] == 0x1b && collected.len() == 1 { - if let Some(code) = self.parse_esc_seq(&mut buf) { - return code +pub fn append_digit(left: u32, right: u32) -> u32 { + left.saturating_mul(10) + .saturating_add(right) +} + + +pub trait WidthCalculator { + fn width(&self, text: &str) -> usize; +} + +pub trait KeyReader { + fn read_key(&mut self) -> Option; +} + +pub trait LineWriter { + fn clear_rows(&mut self, layout: &Layout) -> ShResult<()>; + fn redraw( + &mut self, + prompt: &str, + line: &LineBuf, + new_layout: &Layout, + ) -> ShResult<()>; + fn flush_write(&mut self, buf: &str) -> ShResult<()>; +} + +#[derive(Clone,Copy,Debug)] +pub struct UnicodeWidth; + +impl WidthCalculator for UnicodeWidth { + fn width(&self, text: &str) -> usize { + text.width() + } +} + +#[derive(Clone,Copy,Debug)] +pub struct WcWidth; + +impl WcWidth { + pub fn cwidth(&self, ch: char) -> usize { + ch.width().unwrap() + } +} + +impl WidthCalculator for WcWidth { + fn width(&self, text: &str) -> usize { + let mut width = 0; + for ch in text.chars() { + width += self.cwidth(ch) + } + width + } +} + +const ZWJ: char = '\u{200D}'; +#[derive(Clone,Copy,Debug)] +pub struct NoZwj; + +impl WidthCalculator for NoZwj { + fn width(&self, text: &str) -> usize { + let mut width = 0; + for slice in text.split(ZWJ) { + width += UnicodeWidth.width(slice); + } + width + } +} + +pub struct TermBuffer { + tty: RawFd +} + +impl TermBuffer { + pub fn new(tty: RawFd) -> Self { + assert!(isatty(tty).is_ok_and(|r| r)); + Self { + tty + } + } +} + +impl Read for TermBuffer { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + assert!(isatty(self.tty).is_ok_and(|r| r)); + loop { + match nix::unistd::read(self.tty, buf) { + Ok(n) => return Ok(n), + Err(Errno::EINTR) => {} + Err(e) => return Err(std::io::Error::from_raw_os_error(e as i32)) + } + } + } +} + +pub struct RawModeGuard { + orig: termios::Termios, + fd: RawFd, +} + +impl RawModeGuard { + /// Disable raw mode temporarily for a specific operation + pub fn disable_for R, R>(&self, func: F) -> R { + unsafe { + let fd = BorrowedFd::borrow_raw(self.fd); + // Temporarily restore the original termios + termios::tcsetattr(fd, termios::SetArg::TCSANOW, &self.orig) + .expect("Failed to temporarily disable raw mode"); + + // Run the function + let result = func(); + + // Re-enable raw mode + let mut raw = self.orig.clone(); + termios::cfmakeraw(&mut raw); + termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw) + .expect("Failed to re-enable raw mode"); + + result + } + } +} + +impl Drop for RawModeGuard { + fn drop(&mut self) { + unsafe { + let _ = termios::tcsetattr(BorrowedFd::borrow_raw(self.fd), termios::SetArg::TCSANOW, &self.orig); + } + } +} + +pub struct TermReader { + buffer: BufReader +} + +impl Default for TermReader { + fn default() -> Self { + Self::new() + } +} + +impl TermReader { + pub fn new() -> Self { + Self { + buffer: BufReader::new(TermBuffer::new(1)) + } + } + + + /// Execute some logic in raw mode + /// + /// Saves the termios before running the given function. + /// If the given function panics, the panic will halt momentarily to restore the termios + pub fn poll(&mut self, timeout: PollTimeout) -> ShResult { + if !self.buffer.buffer().is_empty() { + return Ok(true) + } + + let mut fds = [poll::PollFd::new(self.as_fd(),PollFlags::POLLIN)]; + let r = poll::poll(&mut fds, timeout); + match r { + Ok(n) => Ok(n != 0), + Err(Errno::EINTR) => Ok(false), + Err(e) => Err(e.into()) + } + } + + pub fn next_byte(&mut self) -> std::io::Result { + let mut buf = [0u8]; + self.buffer.read_exact(&mut buf)?; + Ok(buf[0]) + } + + pub fn peek_byte(&mut self) -> std::io::Result { + let buf = self.buffer.fill_buf()?; + if buf.is_empty() { + Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "EOF")) + } else { + Ok(buf[0]) + } + } + + pub fn consume_byte(&mut self) { + self.buffer.consume(1); + } + + + + pub fn parse_esc_seq(&mut self) -> ShResult { + let mut seq = vec![0x1b]; + + let b1 = self.peek_byte()?; + self.consume_byte(); + seq.push(b1); + + match b1 { + b'[' => { + let b2 = self.peek_byte()?; + self.consume_byte(); + seq.push(b2); + + match b2 { + b'A' => Ok(KeyEvent(KeyCode::Up, ModKeys::empty())), + b'B' => Ok(KeyEvent(KeyCode::Down, ModKeys::empty())), + b'C' => Ok(KeyEvent(KeyCode::Right, ModKeys::empty())), + b'D' => Ok(KeyEvent(KeyCode::Left, ModKeys::empty())), + b'1'..=b'9' => { + let mut digits = vec![b2]; + + loop { + let b = self.peek_byte()?; + seq.push(b); + self.consume_byte(); + + if b == b'~' || b == b';' { + break; + } else if b.is_ascii_digit() { + digits.push(b); + } else { + break; + } + } + + let key = match digits.as_slice() { + [b'1'] => KeyCode::Home, + [b'3'] => KeyCode::Delete, + [b'4'] => KeyCode::End, + [b'5'] => KeyCode::PageUp, + [b'6'] => KeyCode::PageDown, + [b'7'] => KeyCode::Home, // xterm alternate + [b'8'] => KeyCode::End, // xterm alternate + + [b'1', b'5'] => KeyCode::F(5), + [b'1', b'7'] => KeyCode::F(6), + [b'1', b'8'] => KeyCode::F(7), + [b'1', b'9'] => KeyCode::F(8), + [b'2', b'0'] => KeyCode::F(9), + [b'2', b'1'] => KeyCode::F(10), + [b'2', b'3'] => KeyCode::F(11), + [b'2', b'4'] => KeyCode::F(12), + _ => KeyCode::Esc, + }; + + Ok(KeyEvent(key, ModKeys::empty())) + } + _ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())), } } + b'O' => { + let b2 = self.peek_byte()?; + self.consume_byte(); + seq.push(b2); - // Try parse valid UTF-8 from collected bytes - if let Ok(s) = str::from_utf8(&collected) { - return KeyEvent::new(s, ModKeys::empty()); + let key = match b2 { + b'P' => KeyCode::F(1), + b'Q' => KeyCode::F(2), + b'R' => KeyCode::F(3), + b'S' => KeyCode::F(4), + _ => KeyCode::Esc, + }; + + Ok(KeyEvent(key, ModKeys::empty())) + } + _ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())), + } + } + +} + +impl KeyReader for TermReader { + fn read_key(&mut self) -> Option { + use core::str; + + let mut collected = Vec::with_capacity(4); + + loop { + let byte = self.next_byte().ok()?; + flog!(DEBUG, "read byte: {:?}",byte as char); + collected.push(byte); + + // If it's an escape seq, delegate to ESC sequence handler + if collected[0] == 0x1b && collected.len() == 1 && self.poll(PollTimeout::ZERO).ok()? { + return self.parse_esc_seq().ok(); } - // If it's not valid UTF-8 yet, loop to collect more bytes + // Try parse as valid UTF-8 + if let Ok(s) = str::from_utf8(&collected) { + return Some(KeyEvent::new(s, ModKeys::empty())); + } + + // UTF-8 max 4 bytes — if it’s invalid at this point, bail if collected.len() >= 4 { - // UTF-8 max char length is 4; if it's still invalid, give up break; } } - KeyEvent(KeyCode::Null, ModKeys::empty()) + None + } +} + +impl AsFd for TermReader { + fn as_fd(&self) -> BorrowedFd<'_> { + let fd = self.buffer.get_ref().tty; + unsafe { BorrowedFd::borrow_raw(fd) } + } +} + +pub struct Layout { + pub w_calc: Box, + pub prompt_end: Pos, + pub cursor: Pos, + pub end: Pos +} + +impl Debug for Layout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Layout: ")?; + writeln!(f, "\tPrompt End: {:?}",self.prompt_end)?; + writeln!(f, "\tCursor: {:?}",self.cursor)?; + writeln!(f, "\tEnd: {:?}",self.end) + } +} + +impl Layout { + pub fn new() -> Self { + let w_calc = width_calculator(); + Self { + w_calc, + prompt_end: Pos::default(), + cursor: Pos::default(), + end: Pos::default(), + } + } + pub fn from_parts( + tab_stop: u16, + term_width: u16, + prompt: &str, + to_cursor: &str, + to_end: &str, + ) -> Self { + flog!(DEBUG,to_cursor); + let prompt_end = Self::calc_pos(tab_stop, term_width, prompt, Pos { col: 0, row: 0 }); + let cursor = Self::calc_pos(tab_stop, term_width, to_cursor, prompt_end); + let end = Self::calc_pos(tab_stop, term_width, to_end, prompt_end); + Layout { w_calc: width_calculator(), prompt_end, cursor, end } } - pub fn parse_esc_seq(&self, buf: &mut [u8]) -> Option { - let mut collected = vec![0x1b]; - - // Peek next byte - let _ = self.peek_byte(&mut buf[..1]); - let b1 = buf[0]; - collected.push(b1); - - match b1 { - b'[' => { - // Next byte(s) determine the sequence - let _ = self.peek_byte(&mut buf[..1]); - let b2 = buf[0]; - collected.push(b2); - - match b2 { - b'A' => Some(KeyEvent(KeyCode::Up, ModKeys::empty())), - b'B' => Some(KeyEvent(KeyCode::Down, ModKeys::empty())), - b'C' => Some(KeyEvent(KeyCode::Right, ModKeys::empty())), - b'D' => Some(KeyEvent(KeyCode::Left, ModKeys::empty())), - b'1'..=b'9' => { - // Might be Delete/Home/etc - let mut digits = vec![b2]; - - // Keep reading until we hit `~` or `;` (modifiers) - loop { - let _ = self.peek_byte(&mut buf[..1]); - let b = buf[0]; - collected.push(b); - - if b == b'~' { - break; - } else if b == b';' { - // modifier-aware sequence, like `ESC [ 1 ; 5 ~` - // You may want to parse the full thing - break; - } else if !b.is_ascii_digit() { - break; - } else { - digits.push(b); - } - } - - let key = match digits.as_slice() { - [b'1'] => KeyCode::Home, - [b'3'] => KeyCode::Delete, - [b'4'] => KeyCode::End, - [b'5'] => KeyCode::PageUp, - [b'6'] => KeyCode::PageDown, - [b'7'] => KeyCode::Home, // xterm alternate - [b'8'] => KeyCode::End, // xterm alternate - - // Function keys - [b'1',b'5'] => KeyCode::F(5), - [b'1',b'7'] => KeyCode::F(6), - [b'1',b'8'] => KeyCode::F(7), - [b'1',b'9'] => KeyCode::F(8), - [b'2',b'0'] => KeyCode::F(9), - [b'2',b'1'] => KeyCode::F(10), - [b'2',b'3'] => KeyCode::F(11), - [b'2',b'4'] => KeyCode::F(12), - _ => KeyCode::Esc, - }; - - Some(KeyEvent(key, ModKeys::empty())) - } - _ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())), - } - } - b'O' => { - let _ = self.peek_byte(&mut buf[..1]); - let b2 = buf[0]; - collected.push(b2); - - let key = match b2 { - b'P' => KeyCode::F(1), - b'Q' => KeyCode::F(2), - b'R' => KeyCode::F(3), - b'S' => KeyCode::F(4), - _ => KeyCode::Esc, - }; - - Some(KeyEvent(key, ModKeys::empty())) - } - _ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())), - } - } - - pub fn cursor_pos(&mut self) -> (usize, usize) { - self.write_unrecorded("\x1b[6n"); - let mut buf = [0u8;32]; - let n = self.read_byte(&mut buf); - - - let response = std::str::from_utf8(&buf[..n]).unwrap_or(""); - let mut row = 0; - let mut col = 0; - if let Some(caps) = response.strip_prefix("\x1b[").and_then(|s| s.strip_suffix("R")) { - let mut parts = caps.split(';'); - if let (Some(rowstr), Some(colstr)) = (parts.next(), parts.next()) { - row = rowstr.parse().unwrap_or(1); - col = colstr.parse().unwrap_or(1); + pub fn calc_pos(tab_stop: u16, term_width: u16, s: &str, orig: Pos) -> Pos { + let mut pos = orig; + let mut esc_seq = 0; + for c in s.graphemes(true) { + if c == "\n" { + pos.row += 1; + pos.col = 0; + } + let c_width = if c == "\t" { + tab_stop - (pos.col % tab_stop) + } else { + width(c, &mut esc_seq) + }; + pos.col += c_width; + if pos.col > term_width { + pos.row += 1; + pos.col = c_width; } } - (row,col) + if pos.col >= term_width { + pos.row += 1; + pos.col = 0; + } + + pos } } -impl Default for Terminal { +impl Default for Layout { fn default() -> Self { - Self::new() + Self::new() + } +} + +pub struct TermWriter { + out: RawFd, + t_cols: Col, // terminal width + buffer: String, + w_calc: Box, + tab_stop: u16, +} + +impl TermWriter { + pub fn new(out: RawFd) -> Self { + let w_calc = width_calculator(); + let (t_cols,_) = get_win_size(out); + Self { + out, + t_cols, + buffer: String::new(), + w_calc, + tab_stop: 8 // TODO: add a way to configure this + } + } + pub fn get_cursor_movement(&self, old: Pos, new: Pos) -> ShResult { + let mut buffer = String::new(); + let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to cursor movement buffer"); + + match new.row.cmp(&old.row) { + std::cmp::Ordering::Greater => { + let shift = new.row - old.row; + match shift { + 1 => buffer.push_str("\x1b[B"), + _ => write!(buffer, "\x1b[{shift}B").map_err(err)? + } + } + std::cmp::Ordering::Less => { + let shift = old.row - new.row; + match shift { + 1 => buffer.push_str("\x1b[A"), + _ => write!(buffer, "\x1b[{shift}A").map_err(err)? + } + } + std::cmp::Ordering::Equal => { /* Do nothing */ } + } + + match new.col.cmp(&old.col) { + std::cmp::Ordering::Greater => { + let shift = new.col - old.col; + match shift { + 1 => buffer.push_str("\x1b[C"), + _ => write!(buffer, "\x1b[{shift}C").map_err(err)? + } + } + std::cmp::Ordering::Less => { + let shift = old.col - new.col; + match shift { + 1 => buffer.push_str("\x1b[D"), + _ => write!(buffer, "\x1b[{shift}D").map_err(err)? + } + } + std::cmp::Ordering::Equal => { /* Do nothing */ } + } + Ok(buffer) + } + pub fn move_cursor(&mut self, old: Pos, new: Pos) -> ShResult<()> { + self.buffer.clear(); + let movement = self.get_cursor_movement(old, new)?; + + write_all(self.out, &movement)?; + Ok(()) + } + + + + pub fn update_t_cols(&mut self) { + let (t_cols,_) = get_win_size(self.out); + self.t_cols = t_cols; + } + + pub fn move_cursor_at_leftmost(&mut self, rdr: &mut TermReader, use_newline: bool) -> ShResult<()> { + let result = rdr.poll(PollTimeout::ZERO)?; + if result { + // The terminals reply is going to be stuck behind the currently buffered output + // So let's get out of here + return Ok(()) + } + + // Ping the cursor's position + self.flush_write("\x1b[6n\n")?; + + if !rdr.poll(PollTimeout::from(255u8))? { + return Ok(()) + } + + if rdr.next_byte()? as char != '\x1b' { + return Ok(()) + } + + if rdr.next_byte()? as char != '[' { + return Ok(()) + } + + if read_digits_until(rdr, ';')?.is_none() { + return Ok(()) + } + + // We just consumed everything up to the column number, so let's get that now + let col = read_digits_until(rdr, 'R')?; + + // The cursor is not at the leftmost, so let's fix that + if col != Some(1) { + if use_newline { + // We use '\n' instead of '\r' sometimes because if there's a bunch of garbage on this line, + // It might pollute the prompt/line buffer if those are shorter than said garbage + self.flush_write("\n")?; + } else { + // Sometimes though, we know that there's nothing to the right of the cursor after moving + // So we just move to the left. + self.flush_write("\r")?; + } + } + + Ok(()) + } +} + +impl LineWriter for TermWriter { + fn clear_rows(&mut self, layout: &Layout) -> ShResult<()> { + self.buffer.clear(); + let rows_to_clear = layout.end.row; + let cursor_row = layout.cursor.row; + + let cursor_motion = rows_to_clear.saturating_sub(cursor_row); + if cursor_motion > 0 { + write!(self.buffer, "\x1b[{cursor_motion}B").unwrap() + } + + for _ in 0..rows_to_clear { + self.buffer.push_str("\x1b[2K\x1b[A"); + } + self.buffer.push_str("\x1b[2K"); + write_all(self.out,self.buffer.as_str())?; + self.buffer.clear(); + Ok(()) + } + + fn redraw( + &mut self, + prompt: &str, + line: &LineBuf, + new_layout: &Layout, + ) -> ShResult<()> { + let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer"); + self.buffer.clear(); + + let end = new_layout.end; + let cursor = new_layout.cursor; + + self.buffer.push_str(prompt); + self.buffer.push_str(&line.to_string()); + + if end.col == 0 && end.row > 0 && !self.buffer.ends_with('\n') { + // The line has wrapped. We need to use our own line break. + self.buffer.push('\n'); + } + + let movement = self.get_cursor_movement(end, cursor)?; + write!(self.buffer, "{}", &movement).map_err(err)?; + + write_all(self.out, self.buffer.as_str())?; + Ok(()) + } + + fn flush_write(&mut self, buf: &str) -> ShResult<()> { + write_all(self.out, buf)?; + Ok(()) } } diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs index d5de91a..afd9f39 100644 --- a/src/prompt/readline/vicmd.rs +++ b/src/prompt/readline/vicmd.rs @@ -1,5 +1,9 @@ +use bitflags::bitflags; + use super::register::{append_register, read_register, write_register}; +//TODO: write tests that take edit results and cursor positions from actual neovim edits and test them against the behavior of this editor + #[derive(Clone,Copy,Debug)] pub struct RegisterName { name: Option, @@ -52,12 +56,22 @@ impl Default for RegisterName { } } +bitflags! { + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] + pub struct CmdFlags: u32 { + const VISUAL = 1<<0; + const VISUAL_LINE = 1<<1; + const VISUAL_BLOCK = 1<<2; + } +} + #[derive(Clone,Default,Debug)] pub struct ViCmd { pub register: RegisterName, pub verb: Option, pub motion: Option, pub raw_seq: String, + pub flags: CmdFlags, } impl ViCmd { @@ -82,6 +96,15 @@ impl ViCmd { pub fn motion_count(&self) -> usize { self.motion.as_ref().map(|m| m.0).unwrap_or(1) } + pub fn normalize_counts(&mut self) { + let Some(verb) = self.verb.as_mut() else { return }; + let Some(motion) = self.motion.as_mut() else { return }; + let VerbCmd(v_count, _) = verb; + let MotionCmd(m_count, _) = motion; + let product = *v_count * *m_count; + verb.0 = 1; + motion.0 = product; + } pub fn is_repeatable(&self) -> bool { self.verb.as_ref().is_some_and(|v| v.1.is_repeatable()) } @@ -95,13 +118,36 @@ impl ViCmd { self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::CharSearch(..))) } pub fn should_submit(&self) -> bool { - self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLine)) + self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline)) } pub fn is_undo_op(&self) -> bool { self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo)) } + pub fn is_inplace_edit(&self) -> bool { + self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceCharInplace(_,_) | Verb::ToggleCaseInplace(_))) && + self.motion.is_none() + } pub fn is_line_motion(&self) -> bool { - self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::LineUp | Motion::LineDown)) + self.motion.as_ref().is_some_and(|m| { + matches!(m.1, + Motion::LineUp | + Motion::LineDown | + Motion::LineUpCharwise | + Motion::LineDownCharwise + ) + }) + } + /// If a ViCmd has a linewise motion, but no verb, we change it to charwise + pub fn alter_line_motion_if_no_verb(&mut self) { + if self.is_line_motion() && self.verb.is_none() { + if let Some(motion) = self.motion.as_mut() { + match motion.1 { + Motion::LineUp => motion.1 = Motion::LineUpCharwise, + Motion::LineDown => motion.1 = Motion::LineDownCharwise, + _ => unreachable!() + } + } + } } pub fn is_mode_transition(&self) -> bool { self.verb.as_ref().is_some_and(|v| { @@ -140,12 +186,13 @@ impl MotionCmd { #[non_exhaustive] pub enum Verb { Delete, - DeleteChar(Anchor), Change, Yank, - ReplaceChar(char), - Substitute, - ToggleCase, + Rot13, // lol + ReplaceChar(char), // char to replace with, number of chars to replace + ReplaceCharInplace(char,u16), // char to replace with, number of chars to replace + ToggleCaseInplace(u16), // Number of chars to toggle + ToggleCaseRange, ToLower, ToUpper, Complete, @@ -166,47 +213,31 @@ pub enum Verb { JoinLines, InsertChar(char), Insert(String), - Breakline(Anchor), Indent, Dedent, Equalize, - AcceptLine, - Rot13, // lol - Builder(VerbBuilder), + AcceptLineOrNewline, 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 - ) - } pub fn is_repeatable(&self) -> bool { matches!(self, Self::Delete | - Self::DeleteChar(_) | Self::Change | Self::ReplaceChar(_) | - Self::Substitute | + Self::ReplaceCharInplace(_,_) | Self::ToLower | Self::ToUpper | - Self::ToggleCase | + Self::ToggleCaseRange | + Self::ToggleCaseInplace(_) | Self::Put(_) | Self::ReplaceMode | Self::InsertModeLineBreak(_) | Self::JoinLines | Self::InsertChar(_) | Self::Insert(_) | - Self::Breakline(_) | Self::Indent | Self::Dedent | Self::Equalize @@ -215,11 +246,11 @@ impl Verb { pub fn is_edit(&self) -> bool { matches!(self, Self::Delete | - Self::DeleteChar(_) | Self::Change | Self::ReplaceChar(_) | - Self::Substitute | - Self::ToggleCase | + Self::ReplaceCharInplace(_,_) | + Self::ToggleCaseRange | + Self::ToggleCaseInplace(_) | Self::ToLower | Self::ToUpper | Self::RepeatLast | @@ -229,7 +260,6 @@ impl Verb { Self::JoinLines | Self::InsertChar(_) | Self::Insert(_) | - Self::Breakline(_) | Self::Rot13 | Self::EndOfFile ) @@ -238,7 +268,8 @@ impl Verb { matches!(self, Self::Change | Self::InsertChar(_) | - Self::ReplaceChar(_) + Self::ReplaceChar(_) | + Self::ReplaceCharInplace(_,_) ) } } @@ -251,15 +282,20 @@ pub enum Motion { BeginningOfFirstWord, BeginningOfLine, EndOfLine, - BackwardWord(To, Word), - ForwardWord(To, Word), + WordMotion(To,Word,Direction), CharSearch(Direction,Dest,char), BackwardChar, ForwardChar, + BackwardCharForced, // These two variants can cross line boundaries + ForwardCharForced, LineUp, + LineUpCharwise, ScreenLineUp, + ScreenLineUpCharwise, LineDown, + LineDownCharwise, ScreenLineDown, + ScreenLineDownCharwise, BeginningOfScreenLine, FirstGraphicalOnScreenLine, HalfOfScreen, @@ -267,23 +303,65 @@ pub enum Motion { WholeBuffer, BeginningOfBuffer, EndOfBuffer, - ToColumn(usize), + ToColumn, + ToDelimMatch, + ToBrace(Direction), + ToBracket(Direction), + ToParen(Direction), Range(usize,usize), - Builder(MotionBuilder), RepeatMotion, RepeatMotionRev, Null } -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum MotionBuilder { - CharSearch(Option,Option,Option), - TextObj(Option,Option) +#[derive(Clone,Copy,PartialEq,Eq,Debug)] +pub enum MotionBehavior { + Exclusive, + Inclusive, + Linewise } impl Motion { - pub fn needs_verb(&self) -> bool { - matches!(self, Self::TextObj(_, _)) + pub fn behavior(&self) -> MotionBehavior { + if self.is_linewise() { + MotionBehavior::Linewise + } else if self.is_exclusive() { + MotionBehavior::Exclusive + } else { + MotionBehavior::Inclusive + } + } + pub fn is_exclusive(&self) -> bool { + matches!(&self, + Self::BeginningOfLine | + Self::BeginningOfFirstWord | + Self::BeginningOfScreenLine | + Self::FirstGraphicalOnScreenLine | + Self::LineDownCharwise | + Self::LineUpCharwise | + Self::ScreenLineUpCharwise | + Self::ScreenLineDownCharwise | + Self::ToColumn | + Self::TextObj(TextObj::Sentence(_),_) | + Self::TextObj(TextObj::Paragraph(_),_) | + Self::CharSearch(Direction::Backward, _, _) | + Self::WordMotion(To::Start,_,_) | + Self::ToBrace(_) | + Self::ToBracket(_) | + Self::ToParen(_) | + Self::ScreenLineDown | + Self::ScreenLineUp | + Self::Range(_, _) + ) + } + pub fn is_linewise(&self) -> bool { + matches!(self, + Self::WholeLine | + Self::LineUp | + Self::LineDown | + Self::ScreenLineDown | + Self::ScreenLineUp + ) } } @@ -297,14 +375,11 @@ pub enum TextObj { /// `iw`, `aw` — inner word, around word Word(Word), - /// for stuff like 'dd' - Line, - /// `is`, `as` — inner sentence, around sentence - Sentence, + Sentence(Direction), /// `ip`, `ap` — inner paragraph, around paragraph - Paragraph, + Paragraph(Direction), /// `i"`, `a"` — inner/around double quotes DoubleQuote, diff --git a/src/prompt/readline/mode.rs b/src/prompt/readline/vimode.rs similarity index 73% rename from src/prompt/readline/mode.rs rename to src/prompt/readline/vimode.rs index a379972..ebc05f4 100644 --- a/src/prompt/readline/mode.rs +++ b/src/prompt/readline/vimode.rs @@ -2,9 +2,11 @@ use std::iter::Peekable; use std::str::Chars; use nix::NixPath; +use unicode_segmentation::UnicodeSegmentation; -use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; -use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, MotionCmd, RegisterName, TextObj, To, Verb, VerbBuilder, VerbCmd, ViCmd, Word}; +use super::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; +use super::linebuf::CharClass; +use super::vicmd::{Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, VerbCmd, ViCmd, Word}; use crate::prelude::*; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -51,6 +53,17 @@ pub trait ViMode { fn clamp_cursor(&self) -> bool; fn hist_scroll_start_pos(&self) -> Option; fn report_mode(&self) -> ModeReport; + fn cmds_from_raw(&mut self, raw: &str) -> Vec { + let mut cmds = vec![]; + for ch in raw.graphemes(true) { + let key = E::new(ch, M::NONE); + let Some(cmd) = self.handle_key(key) else { + continue + }; + cmds.push(cmd) + } + cmds + } } #[derive(Default,Debug)] @@ -69,7 +82,8 @@ impl ViInsert { self } pub fn register_and_return(&mut self) -> Option { - let cmd = self.take_cmd(); + let mut cmd = self.take_cmd(); + cmd.normalize_counts(); self.register_cmd(&cmd); Some(cmd) } @@ -99,20 +113,14 @@ impl ViMode for ViInsert { self.register_and_return() } E(K::Char('W'), M::CTRL) => { - if self.ctrl_w_is_undo() { - self.pending_cmd.set_verb(VerbCmd(1,Verb::Undo)); - self.cmds.clear(); - Some(self.take_cmd()) - } else { - self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete)); - self.pending_cmd.set_motion(MotionCmd(1, Motion::BackwardWord(To::Start, Word::Normal))); - self.register_and_return() - } + self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete)); + self.pending_cmd.set_motion(MotionCmd(1, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward))); + self.register_and_return() } E(K::Char('H'), M::CTRL) | E(K::Backspace, M::NONE) => { self.pending_cmd.set_verb(VerbCmd(1,Verb::Delete)); - self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); + self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardCharForced)); self.register_and_return() } @@ -136,6 +144,7 @@ impl ViMode for ViInsert { } } + fn is_repeatable(&self) -> bool { true } @@ -180,19 +189,11 @@ impl ViReplace { self } pub fn register_and_return(&mut self) -> Option { - let cmd = self.take_cmd(); + let mut cmd = self.take_cmd(); + cmd.normalize_counts(); self.register_cmd(&cmd); Some(cmd) } - pub fn ctrl_w_is_undo(&self) -> bool { - let insert_count = self.cmds.iter().filter(|cmd| { - matches!(cmd.verb(),Some(VerbCmd(1, Verb::ReplaceChar(_)))) - }).count(); - let backspace_count = self.cmds.iter().filter(|cmd| { - matches!(cmd.verb(),Some(VerbCmd(1, Verb::Delete))) - }).count(); - insert_count > backspace_count - } pub fn register_cmd(&mut self, cmd: &ViCmd) { self.cmds.push(cmd.clone()) } @@ -210,14 +211,9 @@ impl ViMode for ViReplace { self.register_and_return() } E(K::Char('W'), M::CTRL) => { - if self.ctrl_w_is_undo() { - self.pending_cmd.set_verb(VerbCmd(1,Verb::Undo)); - self.cmds.clear(); - Some(self.take_cmd()) - } else { - self.pending_cmd.set_motion(MotionCmd(1, Motion::BackwardWord(To::Start, Word::Normal))); - self.register_and_return() - } + self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete)); + self.pending_cmd.set_motion(MotionCmd(1, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward))); + self.register_and_return() } E(K::Char('H'), M::CTRL) | E(K::Backspace, M::NONE) => { @@ -272,6 +268,7 @@ impl ViMode for ViReplace { #[derive(Default,Debug)] pub struct ViNormal { pending_seq: String, + pending_flags: CmdFlags, } impl ViNormal { @@ -284,6 +281,10 @@ impl ViNormal { pub fn take_cmd(&mut self) -> String { std::mem::take(&mut self.pending_seq) } + pub fn flags(&self) -> CmdFlags { + self.pending_flags + } + #[allow(clippy::unnecessary_unwrap)] fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState { if verb.is_none() { match motion { @@ -294,8 +295,7 @@ impl ViNormal { } if verb.is_some() && motion.is_none() { match verb.unwrap() { - Verb::Put(_) | - Verb::DeleteChar(_) => CmdState::Complete, + Verb::Put(_) => CmdState::Complete, _ => CmdState::Pending } } else { @@ -329,6 +329,12 @@ impl ViNormal { self.pending_seq.push(ch); let mut chars = self.pending_seq.chars().peekable(); + /* + * Parse the register + * + * Registers can be any letter a-z or A-Z. + * While uncommon, it is possible to give a count to a register name. + */ let register = 'reg_parse: { let mut chars_clone = chars.clone(); let count = self.parse_count(&mut chars_clone); @@ -337,7 +343,7 @@ impl ViNormal { break 'reg_parse RegisterName::default() }; - let Some(reg_name) = chars_clone.next() else { + let Some(reg_name) = chars_clone.next() else { return None // Pending register name }; match reg_name { @@ -350,6 +356,17 @@ impl ViNormal { RegisterName::new(Some(reg_name), count) }; + /* + * We will now parse the verb + * If we hit an invalid sequence, we will call 'return self.quit_parse()' + * self.quit_parse() will clear the pending command and return None + * + * If we hit an incomplete sequence, we will simply return None. + * returning None leaves the pending sequence where it is + * + * Note that we do use a label here for the block and 'return' values from this scope + * using "break 'verb_parse " + */ let verb = 'verb_parse: { let mut chars_clone = chars.clone(); let count = self.parse_count(&mut chars_clone).unwrap_or(1); @@ -367,10 +384,26 @@ impl ViNormal { register, verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags(), } ) } + '~' => { + chars_clone.next(); + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::ToggleCaseRange)); + } + 'u' => { + chars_clone.next(); + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::ToLower)); + } + 'U' => { + chars_clone.next(); + chars = chars_clone; + break 'verb_parse Some(VerbCmd(count, Verb::ToUpper)); + } '?' => { chars_clone.next(); chars = chars_clone; @@ -389,16 +422,53 @@ impl ViNormal { verb: Some(VerbCmd(count, Verb::RepeatLast)), motion: None, raw_seq: self.take_cmd(), + flags: self.flags() } ) } 'x' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::DeleteChar(Anchor::After))); + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Delete)), + motion: Some(MotionCmd(1, Motion::ForwardChar)), + raw_seq: self.take_cmd(), + flags: self.flags() + } + ) } 'X' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::DeleteChar(Anchor::Before))); + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Delete)), + motion: Some(MotionCmd(1, Motion::BackwardChar)), + raw_seq: self.take_cmd(), + flags: self.flags() + } + ) + } + 's' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Change)), + motion: Some(MotionCmd(1, Motion::ForwardChar)), + raw_seq: self.take_cmd(), + flags: self.flags() + }, + ) + } + 'S' => { + return Some( + ViCmd { + register, + verb: Some(VerbCmd(count, Verb::Change)), + motion: Some(MotionCmd(1, Motion::WholeLine)), + raw_seq: self.take_cmd(), + flags: self.flags() + } + ) } 'p' => { chars = chars_clone; @@ -421,9 +491,10 @@ impl ViNormal { return Some( ViCmd { register, - verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))), - motion: Some(MotionCmd(count, Motion::ForwardChar)), - raw_seq: self.take_cmd() + verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch,count as u16))), + motion: None, + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -433,7 +504,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::ReplaceMode)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -441,9 +513,10 @@ impl ViNormal { return Some( ViCmd { register, - verb: Some(VerbCmd(1, Verb::ToggleCase)), - motion: Some(MotionCmd(count, Motion::ForwardChar)), - raw_seq: self.take_cmd() + verb: Some(VerbCmd(1, Verb::ToggleCaseInplace(count as u16))), + motion: None, + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -453,7 +526,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::Undo)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -463,7 +537,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::VisualMode)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -473,7 +548,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::VisualModeLine)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -483,7 +559,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::After))), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -493,7 +570,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::Before))), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -503,7 +581,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::InsertMode)), motion: Some(MotionCmd(1, Motion::ForwardChar)), - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -513,7 +592,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::InsertMode)), motion: Some(MotionCmd(1, Motion::EndOfLine)), - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -523,7 +603,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::InsertMode)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -533,7 +614,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::InsertMode)), motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)), - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -543,7 +625,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::JoinLines)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -565,7 +648,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::Yank)), motion: Some(MotionCmd(1, Motion::EndOfLine)), - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -575,7 +659,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::Delete)), motion: Some(MotionCmd(1, Motion::EndOfLine)), - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -585,7 +670,8 @@ impl ViNormal { register, verb: Some(VerbCmd(count, Verb::Change)), motion: Some(MotionCmd(1, Motion::EndOfLine)), - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -604,59 +690,90 @@ impl ViNormal { let Some(ch) = chars_clone.next() else { break 'motion_parse None }; + // Double inputs like 'dd' and 'cc', and some special cases match (ch, &verb) { + // Double inputs ('?', Some(VerbCmd(_,Verb::Rot13))) | ('d', Some(VerbCmd(_,Verb::Delete))) | ('c', Some(VerbCmd(_,Verb::Change))) | ('y', Some(VerbCmd(_,Verb::Yank))) | ('=', Some(VerbCmd(_,Verb::Equalize))) | + ('u', Some(VerbCmd(_,Verb::ToLower))) | + ('U', Some(VerbCmd(_,Verb::ToUpper))) | + ('~', Some(VerbCmd(_,Verb::ToggleCaseRange))) | ('>', Some(VerbCmd(_,Verb::Indent))) | ('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)), - _ => {} + ('W', Some(VerbCmd(_, Verb::Change))) => { + // Same with 'W' + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward))); + } + _ => { /* Nothing weird, so let's continue */ } } match ch { 'g' => { - if let Some(ch) = chars_clone.peek() { - match ch { - 'g' => { - chars_clone.next(); - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer)) - } - 'e' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Normal))); - } - 'E' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Big))); - } - 'k' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp)); - } - 'j' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown)); - } - '_' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::EndOfLastWord)); - } - '0' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfScreenLine)); - } - '^' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::FirstGraphicalOnScreenLine)); - } - _ => return self.quit_parse() - } - } else { + let Some(ch) = chars_clone.peek() else { break 'motion_parse None + }; + match ch { + 'g' => { + chars_clone.next(); + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer)) + } + 'e' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Backward))); + } + 'E' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Backward))); + } + 'k' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp)); + } + 'j' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown)); + } + '_' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::EndOfLastWord)); + } + '0' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfScreenLine)); + } + '^' => { + chars = chars_clone; + break 'motion_parse Some(MotionCmd(count, Motion::FirstGraphicalOnScreenLine)); + } + _ => return self.quit_parse() } } + 'v' => { + // We got 'v' after a verb + // Instead of normal operations, we will calculate the span based on how visual mode would see it + if self.flags().intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) { + // We can't have more than one of these + return self.quit_parse(); + } + self.pending_flags |= CmdFlags::VISUAL; + break 'motion_parse None + } + 'V' => { + // We got 'V' after a verb + // Instead of normal operations, we will calculate the span based on how visual line mode would see it + if self.flags().intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) { + // We can't have more than one of these + // I know vim can technically do this, but it doesn't really make sense to allow it + // since even in vim only the first one given is used + return self.quit_parse(); + } + self.pending_flags |= CmdFlags::VISUAL; + break 'motion_parse None + } + // TODO: figure out how to include 'Ctrl+V' here, might need a refactor 'G' => { chars = chars_clone; break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer)); @@ -699,7 +816,7 @@ impl ViNormal { } '|' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count))); + break 'motion_parse Some(MotionCmd(count, Motion::ToColumn)); } '^' => { chars = chars_clone; @@ -731,27 +848,27 @@ impl ViNormal { } 'w' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Normal))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Forward))); } 'W' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Big))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Forward))); } 'e' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Normal))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Forward))); } 'E' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Big))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward))); } 'b' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Normal))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward))); } 'B' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Big))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Backward))); } ch if ch == 'i' || ch == 'a' => { let bound = match ch { @@ -767,6 +884,7 @@ impl ViNormal { 'W' => TextObj::Word(Word::Big), '"' => TextObj::DoubleQuote, '\'' => TextObj::SingleQuote, + '`' => TextObj::BacktickQuote, '(' | ')' | 'b' => TextObj::Paren, '{' | '}' | 'B' => TextObj::Brace, '[' | ']' => TextObj::Bracket, @@ -795,7 +913,8 @@ impl ViNormal { register, verb, motion, - raw_seq: std::mem::take(&mut self.pending_seq) + raw_seq: std::mem::take(&mut self.pending_seq), + flags: self.flags() } ) } @@ -812,7 +931,7 @@ impl ViNormal { impl ViMode for ViNormal { fn handle_key(&mut self, key: E) -> Option { - match key { + let mut cmd = match key { E(K::Char(ch), M::NONE) => self.try_parse(ch), E(K::Backspace, M::NONE) => { Some(ViCmd { @@ -820,6 +939,7 @@ impl ViMode for ViNormal { verb: None, motion: Some(MotionCmd(1, Motion::BackwardChar)), raw_seq: "".into(), + flags: self.flags() }) } E(K::Char('R'), M::CTRL) => { @@ -830,7 +950,8 @@ impl ViMode for ViNormal { register: RegisterName::default(), verb: Some(VerbCmd(count,Verb::Redo)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: self.flags() } ) } @@ -846,7 +967,12 @@ impl ViMode for ViNormal { None } } - } + }; + + if let Some(cmd) = cmd.as_mut() { + cmd.normalize_counts(); + }; + cmd } fn is_repeatable(&self) -> bool { @@ -894,6 +1020,8 @@ impl ViVisual { pub fn take_cmd(&mut self) -> String { std::mem::take(&mut self.pending_seq) } + + #[allow(clippy::unnecessary_unwrap)] fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState { if verb.is_none() { match motion { @@ -902,10 +1030,9 @@ impl ViVisual { None => return CmdState::Pending } } - if verb.is_some() && motion.is_none() { + if motion.is_none() && verb.is_some() { match verb.unwrap() { - Verb::Put(_) | - Verb::DeleteChar(_) => CmdState::Complete, + Verb::Put(_) => CmdState::Complete, _ => CmdState::Pending } } else { @@ -977,7 +1104,8 @@ impl ViVisual { register, verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -987,7 +1115,8 @@ impl ViVisual { register, verb: Some(VerbCmd(1, Verb::Rot13)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1004,6 +1133,7 @@ impl ViVisual { verb: Some(VerbCmd(count, Verb::RepeatLast)), motion: None, raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1018,6 +1148,7 @@ impl ViVisual { verb: Some(VerbCmd(1, Verb::Delete)), motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1027,7 +1158,8 @@ impl ViVisual { register, verb: Some(VerbCmd(1, Verb::Yank)), motion: Some(MotionCmd(1, Motion::WholeLine)), - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1037,7 +1169,8 @@ impl ViVisual { register, verb: Some(VerbCmd(1, Verb::Delete)), motion: Some(MotionCmd(1, Motion::WholeLine)), - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1049,6 +1182,7 @@ impl ViVisual { verb: Some(VerbCmd(1, Verb::Change)), motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1059,6 +1193,7 @@ impl ViVisual { verb: Some(VerbCmd(1, Verb::Indent)), motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1069,6 +1204,7 @@ impl ViVisual { verb: Some(VerbCmd(1, Verb::Dedent)), motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1079,6 +1215,7 @@ impl ViVisual { verb: Some(VerbCmd(1, Verb::Equalize)), motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1094,7 +1231,8 @@ impl ViVisual { register, verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1102,9 +1240,10 @@ impl ViVisual { return Some( ViCmd { register, - verb: Some(VerbCmd(1, Verb::ToggleCase)), + verb: Some(VerbCmd(1, Verb::ToggleCaseRange)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1114,7 +1253,8 @@ impl ViVisual { register, verb: Some(VerbCmd(count, Verb::ToLower)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1124,7 +1264,8 @@ impl ViVisual { register, verb: Some(VerbCmd(count, Verb::ToUpper)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1135,7 +1276,8 @@ impl ViVisual { register, verb: Some(VerbCmd(count, Verb::SwapVisualAnchor)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1145,7 +1287,8 @@ impl ViVisual { register, verb: Some(VerbCmd(count, Verb::InsertMode)), motion: Some(MotionCmd(1, Motion::ForwardChar)), - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1155,7 +1298,8 @@ impl ViVisual { register, verb: Some(VerbCmd(count, Verb::InsertMode)), motion: Some(MotionCmd(1, Motion::BeginningOfLine)), - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1165,7 +1309,8 @@ impl ViVisual { register, verb: Some(VerbCmd(count, Verb::JoinLines)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1190,7 +1335,8 @@ impl ViVisual { register, verb: Some(verb), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() }) } @@ -1220,18 +1366,22 @@ impl ViVisual { break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer)) } 'e' => { + chars_clone.next(); chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Normal))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Backward))); } 'E' => { + chars_clone.next(); chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::End, Word::Big))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Backward))); } 'k' => { + chars_clone.next(); chars = chars_clone; break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp)); } 'j' => { + chars_clone.next(); chars = chars_clone; break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown)); } @@ -1246,28 +1396,28 @@ impl ViVisual { break 'motion_parse None }; - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, (*ch).into()))) + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, *ch))) } 'F' => { let Some(ch) = chars_clone.peek() else { break 'motion_parse None }; - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, (*ch).into()))) + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, *ch))) } 't' => { let Some(ch) = chars_clone.peek() else { break 'motion_parse None }; - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, (*ch).into()))) + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, *ch))) } 'T' => { let Some(ch) = chars_clone.peek() else { break 'motion_parse None }; - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, (*ch).into()))) + break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, *ch))) } ';' => { chars = chars_clone; @@ -1279,7 +1429,7 @@ impl ViVisual { } '|' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count))); + break 'motion_parse Some(MotionCmd(count, Motion::ToColumn)); } '0' => { chars = chars_clone; @@ -1307,27 +1457,27 @@ impl ViVisual { } 'w' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Normal))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Forward))); } 'W' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Big))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Forward))); } 'e' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Normal))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Normal, Direction::Forward))); } 'E' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Big))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::End, Word::Big, Direction::Forward))); } 'b' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Normal))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Normal, Direction::Backward))); } 'B' => { chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Big))); + break 'motion_parse Some(MotionCmd(count, Motion::WordMotion(To::Start, Word::Big, Direction::Backward))); } ch if ch == 'i' || ch == 'a' => { let bound = match ch { @@ -1343,6 +1493,7 @@ impl ViVisual { 'W' => TextObj::Word(Word::Big), '"' => TextObj::DoubleQuote, '\'' => TextObj::SingleQuote, + '`' => TextObj::BacktickQuote, '(' | ')' | 'b' => TextObj::Paren, '{' | '}' | 'B' => TextObj::Brace, '[' | ']' => TextObj::Bracket, @@ -1366,15 +1517,15 @@ impl ViVisual { match self.validate_combination(verb_ref, motion_ref) { CmdState::Complete => { - let cmd = Some( + Some( ViCmd { register, verb, motion, - raw_seq: std::mem::take(&mut self.pending_seq) + raw_seq: std::mem::take(&mut self.pending_seq), + flags: CmdFlags::empty() } - ); - cmd + ) } CmdState::Pending => { None @@ -1389,7 +1540,7 @@ impl ViVisual { impl ViMode for ViVisual { fn handle_key(&mut self, key: E) -> Option { - match key { + let mut cmd = match key { E(K::Char(ch), M::NONE) => self.try_parse(ch), E(K::Backspace, M::NONE) => { Some(ViCmd { @@ -1397,6 +1548,7 @@ impl ViMode for ViVisual { verb: None, motion: Some(MotionCmd(1, Motion::BackwardChar)), raw_seq: "".into(), + flags: CmdFlags::empty() }) } E(K::Char('R'), M::CTRL) => { @@ -1407,7 +1559,8 @@ impl ViMode for ViVisual { register: RegisterName::default(), verb: Some(VerbCmd(count,Verb::Redo)), motion: None, - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() } ) } @@ -1417,7 +1570,8 @@ impl ViMode for ViVisual { register: Default::default(), verb: Some(VerbCmd(1, Verb::NormalMode)), motion: Some(MotionCmd(1, Motion::Null)), - raw_seq: self.take_cmd() + raw_seq: self.take_cmd(), + flags: CmdFlags::empty() }) } _ => { @@ -1428,7 +1582,12 @@ impl ViMode for ViVisual { None } } - } + }; + + if let Some(cmd) = cmd.as_mut() { + cmd.normalize_counts(); + }; + cmd } fn is_repeatable(&self) -> bool { @@ -1473,11 +1632,17 @@ pub fn common_cmds(key: E) -> Option { E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)), E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineUp)), E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineDown)), - E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::AcceptLine)), + E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::AcceptLineOrNewline)), E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1,Verb::EndOfFile)), - E(K::Delete, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::DeleteChar(Anchor::After))), + E(K::Delete, M::NONE) => { + pending_cmd.set_verb(VerbCmd(1,Verb::Delete)); + pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar)); + } E(K::Backspace, M::NONE) | - E(K::Char('H'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1,Verb::DeleteChar(Anchor::Before))), + E(K::Char('H'), M::CTRL) => { + pending_cmd.set_verb(VerbCmd(1,Verb::Delete)); + pending_cmd.set_motion(MotionCmd(1, Motion::BackwardChar)); + } _ => return None } Some(pending_cmd) diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 2c5083b..9dfe00c 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -26,6 +26,7 @@ pub mod error; pub mod getopt; pub mod script; pub mod highlight; +pub mod readline; /// Unsafe to use outside of tests pub fn get_nodes(input: &str, filter: F1) -> Vec diff --git a/src/tests/readline.rs b/src/tests/readline.rs new file mode 100644 index 0000000..0efc9f3 --- /dev/null +++ b/src/tests/readline.rs @@ -0,0 +1,654 @@ +use std::collections::VecDeque; + +use crate::{libsh::term::{Style, Styled}, prompt::readline::{history::History, keys::{KeyCode, KeyEvent, ModKeys}, linebuf::LineBuf, term::{raw_mode, KeyReader, LineWriter}, vimode::{ViInsert, ViMode, ViNormal}, FernVi, Readline}}; + +use pretty_assertions::assert_eq; + +use super::super::*; + +#[derive(Default,Debug)] +struct TestReader { + pub bytes: VecDeque +} + +impl TestReader { + pub fn new() -> Self { + Self::default() + } + pub fn with_initial(mut self, bytes: &[u8]) -> Self { + let bytes = bytes.iter(); + self.bytes.extend(bytes); + self + } + + pub fn parse_esc_seq_from_bytes(&mut self) -> Option { + let mut seq = vec![0x1b]; + let b1 = self.bytes.pop_front()?; + seq.push(b1); + + match b1 { + b'[' => { + let b2 = self.bytes.pop_front()?; + seq.push(b2); + + match b2 { + b'A' => Some(KeyEvent(KeyCode::Up, ModKeys::empty())), + b'B' => Some(KeyEvent(KeyCode::Down, ModKeys::empty())), + b'C' => Some(KeyEvent(KeyCode::Right, ModKeys::empty())), + b'D' => Some(KeyEvent(KeyCode::Left, ModKeys::empty())), + b'1'..=b'9' => { + let mut digits = vec![b2]; + + while let Some(&b) = self.bytes.front() { + seq.push(b); + self.bytes.pop_front(); + + if b == b'~' || b == b';' { + break; + } else if b.is_ascii_digit() { + digits.push(b); + } else { + break; + } + } + + let key = match digits.as_slice() { + [b'1'] => KeyCode::Home, + [b'3'] => KeyCode::Delete, + [b'4'] => KeyCode::End, + [b'5'] => KeyCode::PageUp, + [b'6'] => KeyCode::PageDown, + [b'7'] => KeyCode::Home, // xterm alternate + [b'8'] => KeyCode::End, // xterm alternate + + [b'1', b'5'] => KeyCode::F(5), + [b'1', b'7'] => KeyCode::F(6), + [b'1', b'8'] => KeyCode::F(7), + [b'1', b'9'] => KeyCode::F(8), + [b'2', b'0'] => KeyCode::F(9), + [b'2', b'1'] => KeyCode::F(10), + [b'2', b'3'] => KeyCode::F(11), + [b'2', b'4'] => KeyCode::F(12), + _ => KeyCode::Esc, + }; + + Some(KeyEvent(key, ModKeys::empty())) + } + _ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())), + } + } + + b'O' => { + let b2 = self.bytes.pop_front()?; + seq.push(b2); + + let key = match b2 { + b'P' => KeyCode::F(1), + b'Q' => KeyCode::F(2), + b'R' => KeyCode::F(3), + b'S' => KeyCode::F(4), + _ => KeyCode::Esc, + }; + + Some(KeyEvent(key, ModKeys::empty())) + } + + _ => Some(KeyEvent(KeyCode::Esc, ModKeys::empty())), + } + } +} + +impl KeyReader for TestReader { + fn read_key(&mut self) -> Option { + use core::str; + + let mut collected = Vec::with_capacity(4); + + loop { + let byte = self.bytes.pop_front()?; + collected.push(byte); + + // If it's an escape sequence, delegate + if collected[0] == 0x1b && collected.len() == 1 { + if let Some(&_next @ (b'[' | b'0')) = self.bytes.front() { + println!("found escape seq"); + let seq = self.parse_esc_seq_from_bytes(); + println!("{seq:?}"); + return seq + } + } + + // Try parse as valid UTF-8 + if let Ok(s) = str::from_utf8(&collected) { + return Some(KeyEvent::new(s, ModKeys::empty())); + } + + if collected.len() >= 4 { + break; + } + } + + None + } +} + +pub struct TestWriter { +} + +impl TestWriter { + pub fn new() -> Self { + Self {} + } +} + +impl LineWriter for TestWriter { + fn clear_rows(&mut self, _layout: &prompt::readline::term::Layout) -> libsh::error::ShResult<()> { + Ok(()) + } + + fn redraw( + &mut self, + _prompt: &str, + _line: &LineBuf, + _new_layout: &prompt::readline::term::Layout, + ) -> libsh::error::ShResult<()> { + Ok(()) + } + + fn flush_write(&mut self, _buf: &str) -> libsh::error::ShResult<()> { + Ok(()) + } +} + +impl FernVi { + pub fn new_test(prompt: Option,input: &str, initial: &str) -> Self { + Self { + reader: Box::new(TestReader::new().with_initial(input.as_bytes())), + writer: Box::new(TestWriter::new()), + prompt: prompt.unwrap_or("$ ".styled(Style::Green)), + mode: Box::new(ViInsert::new()), + old_layout: None, + repeat_action: None, + repeat_motion: None, + history: History::new().unwrap(), + editor: LineBuf::new().with_initial(initial, 0) + } + } +} + +fn fernvi_test(input: &str, initial: &str) -> String { + let mut fernvi = FernVi::new_test(None,input,initial); + let raw_mode = raw_mode(); + let line = fernvi.readline().unwrap(); + std::mem::drop(raw_mode); + line +} + +fn normal_cmd(cmd: &str, buf: &str, cursor: usize) -> (String,usize) { + let cmd = ViNormal::new() + .cmds_from_raw(cmd) + .pop() + .unwrap(); + let mut buf = LineBuf::new().with_initial(buf, cursor); + buf.exec_cmd(cmd).unwrap(); + (buf.as_str().to_string(),buf.cursor.get()) +} + +#[test] +fn vimode_insert_cmds() { + let raw = "abcdefghijklmnopqrstuvwxyz1234567890-=[];'<>/\\x1b"; + let mut mode = ViInsert::new(); + let cmds = mode.cmds_from_raw(raw); + insta::assert_debug_snapshot!(cmds) +} + +#[test] +fn vimode_normal_cmds() { + let raw = "d2wg?5b2P5x"; + let mut mode = ViNormal::new(); + let cmds = mode.cmds_from_raw(raw); + insta::assert_debug_snapshot!(cmds) +} + +#[test] +fn linebuf_empty_linebuf() { + let mut buf = LineBuf::new(); + assert_eq!(buf.as_str(), ""); + buf.update_graphemes_lazy(); + assert_eq!(buf.grapheme_indices(), &[]); + assert!(buf.slice(0..0).is_none()); +} + +#[test] +fn linebuf_ascii_content() { + let mut buf = LineBuf::new().with_initial("hello", 0); + + buf.update_graphemes_lazy(); + assert_eq!(buf.grapheme_indices(), &[0, 1, 2, 3, 4]); + + assert_eq!(buf.grapheme_at(0), Some("h")); + assert_eq!(buf.grapheme_at(4), Some("o")); + assert_eq!(buf.slice(1..4), Some("ell")); + assert_eq!(buf.slice_to(2), Some("he")); + assert_eq!(buf.slice_from(2), Some("llo")); +} + +#[test] +fn linebuf_unicode_graphemes() { + let mut buf = LineBuf::new().with_initial("a🇺🇸b́c", 0); + + buf.update_graphemes_lazy(); + let indices = buf.grapheme_indices(); + assert_eq!(indices.len(), 4); // 4 graphemes + 1 end marker + + assert_eq!(buf.grapheme_at(0), Some("a")); + assert_eq!(buf.grapheme_at(1), Some("🇺🇸")); + assert_eq!(buf.grapheme_at(2), Some("b́")); // b + combining accent + assert_eq!(buf.grapheme_at(3), Some("c")); + assert_eq!(buf.grapheme_at(4), None); // out of bounds + + assert_eq!(buf.slice(0..2), Some("a🇺🇸")); + assert_eq!(buf.slice(1..3), Some("🇺🇸b́")); + assert_eq!(buf.slice(2..4), Some("b́c")); +} + +#[test] +fn linebuf_slice_to_from_cursor() { + let mut buf = LineBuf::new().with_initial("abçd", 2); + + buf.update_graphemes_lazy(); + assert_eq!(buf.slice_to_cursor(), Some("ab")); + assert_eq!(buf.slice_from_cursor(), Some("çd")); +} + +#[test] +fn linebuf_out_of_bounds_slices() { + let mut buf = LineBuf::new().with_initial("test", 0); + + buf.update_graphemes_lazy(); + + assert_eq!(buf.grapheme_at(5), None); // out of bounds + assert_eq!(buf.slice(2..5), None); // end out of bounds + assert_eq!(buf.slice(4..4), None); // valid but empty +} + +#[test] +fn linebuf_this_line() { + let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line"; + let mut buf = LineBuf::new().with_initial(initial, 57); + let (start,end) = buf.this_line(); + assert_eq!(buf.slice(start..end), Some("This is the third line\n")) +} + +#[test] +fn linebuf_prev_line() { + let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line"; + let mut buf = LineBuf::new().with_initial(initial, 57); + let (start,end) = buf.nth_prev_line(1).unwrap(); + assert_eq!(buf.slice(start..end), Some("This is the second line\n")) +} + +#[test] +fn linebuf_prev_line_first_line_is_empty() { + let initial = "\nThis is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line"; + let mut buf = LineBuf::new().with_initial(initial, 36); + let (start,end) = buf.nth_prev_line(1).unwrap(); + assert_eq!(buf.slice(start..end), Some("This is the first line\n")) +} + +#[test] +fn linebuf_next_line() { + let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line"; + let mut buf = LineBuf::new().with_initial(initial, 57); + let (start,end) = buf.nth_next_line(1).unwrap(); + assert_eq!(buf.slice(start..end), Some("This is the fourth line")) +} + +#[test] +fn linebuf_next_line_last_line_is_empty() { + let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n"; + let mut buf = LineBuf::new().with_initial(initial, 57); + let (start,end) = buf.nth_next_line(1).unwrap(); + assert_eq!(buf.slice(start..end), Some("This is the fourth line\n")) +} + +#[test] +fn linebuf_next_line_several_trailing_newlines() { + let initial = "This is the first line\nThis is the second line\nThis is the third line\nThis is the fourth line\n\n\n\n"; + let mut buf = LineBuf::new().with_initial(initial, 81); + let (start,end) = buf.nth_next_line(1).unwrap(); + assert_eq!(buf.slice(start..end), Some("\n")) +} + +#[test] +fn linebuf_next_line_only_newlines() { + let initial = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; + let mut buf = LineBuf::new().with_initial(initial, 7); + let (start,end) = buf.nth_next_line(1).unwrap(); + assert_eq!(start, 8); + assert_eq!(buf.slice(start..end), Some("\n")) +} + +#[test] +fn linebuf_prev_line_only_newlines() { + let initial = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"; + let mut buf = LineBuf::new().with_initial(initial, 7); + let (start,end) = buf.nth_prev_line(1).unwrap(); + assert_eq!(buf.slice(start..end), Some("\n")); + assert_eq!(start, 6); +} + +#[test] +fn linebuf_cursor_motion() { + let mut buf = LineBuf::new().with_initial("Thé quíck 🦊 bröwn fóx jumpś óver the 💤 lázy dóg 🐶", 0); + + buf.update_graphemes_lazy(); + let total = buf.grapheme_indices.as_ref().unwrap().len(); + + for i in 0..total { + buf.cursor.set(i); + + let expected_to = buf.buffer.get(..buf.grapheme_indices_owned()[i]).unwrap_or("").to_string(); + let expected_from = if i + 1 < total { + buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string() + } else { + // last grapheme, ends at buffer end + buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string() + }; + + let expected_at = { + let start = buf.grapheme_indices_owned()[i]; + let end = buf.grapheme_indices_owned().get(i + 1).copied().unwrap_or(buf.buffer.len()); + buf.buffer.get(start..end).map(|slice| slice.to_string()) + }; + + assert_eq!( + buf.slice_to_cursor(), + Some(expected_to.as_str()), + "Failed at cursor position {i}: slice_to_cursor" + ); + assert_eq!( + buf.slice_from_cursor(), + Some(expected_from.as_str()), + "Failed at cursor position {i}: slice_from_cursor" + ); + assert_eq!( + buf.grapheme_at(i).map(|slice| slice.to_string()), + expected_at, + "Failed at cursor position {i}: grapheme_at" + ); + } +} + +#[test] +fn editor_delete_word() { + assert_eq!(normal_cmd( + "dw", + "The quick brown fox jumps over the lazy dog", + 16), + ("The quick brown jumps over the lazy dog".into(), 16) + ); +} + +#[test] +fn editor_delete_backwards() { + assert_eq!(normal_cmd( + "2db", + "The quick brown fox jumps over the lazy dog", + 16), + ("The fox jumps over the lazy dog".into(), 4) + ); +} + +#[test] +fn editor_rot13_five_words_backwards() { + assert_eq!(normal_cmd( + "g?5b", + "The quick brown fox jumps over the lazy dog", + 31), + ("The dhvpx oebja sbk whzcf bire the lazy dog".into(), 4) + ); +} + +#[test] +fn editor_delete_word_on_whitespace() { + assert_eq!(normal_cmd( + "dw", + "The quick brown fox", + 10), //on the whitespace between "quick" and "brown" + ("The quick brown fox".into(), 10) + ); +} + +#[test] +fn editor_delete_5_words() { + assert_eq!(normal_cmd( + "5dw", + "The quick brown fox jumps over the lazy dog", + 16,), + ("The quick brown dog".into(), 16) + ); +} + +#[test] +fn editor_delete_end_includes_last() { + assert_eq!(normal_cmd( + "de", + "The quick brown fox::::jumps over the lazy dog", + 16), + ("The quick brown ::::jumps over the lazy dog".into(), 16) + ); +} + +#[test] +fn editor_delete_end_unicode_word() { + assert_eq!(normal_cmd( + "de", + "naïve café world", + 0), + (" café world".into(), 0) + ); +} + +#[test] +fn editor_inplace_edit_cursor_position() { + assert_eq!(normal_cmd( + "5~", + "foobar", + 0), + ("FOOBAr".into(), 4) + ); + assert_eq!(normal_cmd( + "5rg", + "foobar", + 0), + ("gggggr".into(), 4) + ); +} + +#[test] +fn editor_insert_mode_not_clamped() { + assert_eq!(normal_cmd( + "a", + "foobar", + 5), + ("foobar".into(), 6) + ) +} + +#[test] +fn editor_overshooting_motions() { + assert_eq!(normal_cmd( + "5dw", + "foo bar", + 0), + ("".into(), 0) + ); + assert_eq!(normal_cmd( + "3db", + "foo bar", + 0), + ("foo bar".into(), 0) + ); + assert_eq!(normal_cmd( + "3dj", + "foo bar", + 0), + ("foo bar".into(), 0) + ); + assert_eq!(normal_cmd( + "3dk", + "foo bar", + 0), + ("foo bar".into(), 0) + ); +} + +#[test] +fn editor_textobj_quoted() { + assert_eq!(normal_cmd( + "di\"", + "this buffer has \"some \\\"quoted\" text", + 0), + ("this buffer has \"\" text".into(), 17) + ); + assert_eq!(normal_cmd( + "da\"", + "this buffer has \"some \\\"quoted\" text", + 0), + ("this buffer has text".into(), 16) + ); + assert_eq!(normal_cmd( + "di'", + "this buffer has 'some \\'quoted' text", + 0), + ("this buffer has '' text".into(), 17) + ); + assert_eq!(normal_cmd( + "da'", + "this buffer has 'some \\'quoted' text", + 0), + ("this buffer has text".into(), 16) + ); + assert_eq!(normal_cmd( + "di`", + "this buffer has `some \\`quoted` text", + 0), + ("this buffer has `` text".into(), 17) + ); + assert_eq!(normal_cmd( + "da`", + "this buffer has `some \\`quoted` text", + 0), + ("this buffer has text".into(), 16) + ); +} + +#[test] +fn editor_textobj_delimited() { + assert_eq!(normal_cmd( + "di)", + "this buffer has (some \\(\\)(inner) \\(\\)delimited) text", + 0), + ("this buffer has () text".into(), 17) + ); + assert_eq!(normal_cmd( + "da)", + "this buffer has (some \\(\\)(inner) \\(\\)delimited) text", + 0), + ("this buffer has text".into(), 16) + ); + assert_eq!(normal_cmd( + "di]", + "this buffer has [some \\[\\][inner] \\[\\]delimited] text", + 0), + ("this buffer has [] text".into(), 17) + ); + assert_eq!(normal_cmd( + "da]", + "this buffer has [some \\[\\][inner] \\[\\]delimited] text", + 0), + ("this buffer has text".into(), 16) + ); + assert_eq!(normal_cmd( + "di}", + "this buffer has {some \\{\\}{inner} \\{\\}delimited} text", + 0), + ("this buffer has {} text".into(), 17) + ); + assert_eq!(normal_cmd( + "da}", + "this buffer has {some \\{\\}{inner} \\{\\}delimited} text", + 0), + ("this buffer has text".into(), 16) + ); + assert_eq!(normal_cmd( + "di>", + "this buffer has \\<\\>delimited> text", + 0), + ("this buffer has <> text".into(), 17) + ); + assert_eq!(normal_cmd( + "da>", + "this buffer has \\<\\>delimited> text", + 0), + ("this buffer has text".into(), 16) + ); +} + +const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."; + +#[test] +fn editor_delete_line_up() { + assert_eq!(normal_cmd( + "dk", + LOREM_IPSUM, + 237), + ("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.".into(), 240,) + ) +} + +#[test] +fn fernvi_test_simple() { + assert_eq!(fernvi_test( + "foo bar\x1bbdw\r", + ""), + "foo " + ) +} + +#[test] +fn fernvi_test_mode_change() { + assert_eq!(fernvi_test( + "foo bar biz buzz\x1bbbb2cwbiz buzz bar\r", + ""), + "foo biz buzz bar buzz" + ) +} + +#[test] +fn fernvi_test_lorem_ipsum_1() { + assert_eq!(fernvi_test( + "\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r", + LOREM_IPSUM), + "Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in repin voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur asdasdasdasd occaecat cupinon proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra." + ) +} + +#[test] +fn fernvi_test_lorem_ipsum_undo() { + assert_eq!(fernvi_test( + "\x1bwwwwwwwwainserting some characters now...\x1bu\r", + LOREM_IPSUM), + LOREM_IPSUM + ) +} + +#[test] +fn fernvi_test_lorem_ipsum_ctrl_w() { + assert_eq!(fernvi_test( + "\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r", + LOREM_IPSUM), + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim am, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra." + ) +}