From 72f7788abc85d74e99397fa91cd4624d338ecc71 Mon Sep 17 00:00:00 2001 From: Kyler Clay Date: Mon, 2 Jun 2025 03:18:13 -0400 Subject: [PATCH] copied rustyline's homework --- Cargo.toml | 2 +- src/fern.rs | 2 +- src/prompt/mod.rs | 6 +- src/prompt/readline/history.rs | 345 ----- src/prompt/readline/keys.rs | 142 -- src/prompt/readline/layout.rs | 0 src/prompt/readline/linebuf.rs | 2177 +------------------------------ src/prompt/readline/mod.rs | 456 +------ src/prompt/readline/mode.rs | 1484 --------------------- src/prompt/readline/register.rs | 168 --- src/prompt/readline/term.rs | 838 ++++++------ src/prompt/readline/vicmd.rs | 361 ----- 12 files changed, 441 insertions(+), 5540 deletions(-) delete mode 100644 src/prompt/readline/history.rs delete mode 100644 src/prompt/readline/keys.rs create mode 100644 src/prompt/readline/layout.rs delete mode 100644 src/prompt/readline/mode.rs delete mode 100644 src/prompt/readline/register.rs delete mode 100644 src/prompt/readline/vicmd.rs diff --git a/Cargo.toml b/Cargo.toml index 5d29568..4f8fe23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ 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"] } +nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] } pretty_assertions = "1.4.1" regex = "1.11.1" unicode-segmentation = "1.12.0" 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..30b4f6d 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -22,11 +22,11 @@ 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))?), + FernEditMode::Vi => Box::new(FernVi::new()), FernEditMode::Emacs => todo!() }; - reader.readline() + reader.readline(Some(prompt)) } diff --git a/src/prompt/readline/history.rs b/src/prompt/readline/history.rs deleted file mode 100644 index f12898d..0000000 --- a/src/prompt/readline/history.rs +++ /dev/null @@ -1,345 +0,0 @@ -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::prelude::*; - -use super::vicmd::Direction; // surprisingly useful - -#[derive(Default,Clone,Copy,Debug)] -pub enum SearchKind { - Fuzzy, - #[default] - Prefix -} - -#[derive(Default,Clone,Debug)] -pub struct SearchConstraint { - kind: SearchKind, - term: String, -} - -impl SearchConstraint { - pub fn new(kind: SearchKind, term: String) -> Self { - Self { kind, term } - } -} - -#[derive(Debug,Clone)] -pub struct HistEntry { - id: u32, - timestamp: SystemTime, - command: String, - new: bool -} - -impl HistEntry { - pub fn id(&self) -> u32 { - self.id - } - pub fn timestamp(&self) -> &SystemTime { - &self.timestamp - } - pub fn command(&self) -> &str { - &self.command - } - fn with_escaped_newlines(&self) -> String { - let mut escaped = String::new(); - let mut chars = self.command.chars(); - while let Some(ch) = chars.next() { - match ch { - '\\' => { - escaped.push(ch); - if let Some(ch) = chars.next() { - escaped.push(ch) - } - } - '\n' => { - escaped.push_str("\\\n"); - } - _ => escaped.push(ch), - } - } - escaped - } - pub fn is_new(&self) -> bool { - self.new - } -} - -impl FromStr for HistEntry { - type Err = ShErr; - fn from_str(s: &str) -> Result { - let err = Err( - ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on history entry '{s}'"), notes: vec![] } - ); - - //: 248972349;148;echo foo; echo bar - let Some(cleaned) = s.strip_prefix(": ") else { return err }; - //248972349;148;echo foo; echo bar - let Some((timestamp,id_and_command)) = cleaned.split_once(';') else { return err }; - //("248972349","148;echo foo; echo bar") - let Some((id,command)) = id_and_command.split_once(';') else { return err }; - //("148","echo foo; echo bar") - let Ok(ts_seconds) = timestamp.parse::() else { return err }; - let Ok(id) = id.parse::() else { return err }; - let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds); - let command = command.to_string(); - Ok(Self { id, timestamp, command, new: false }) - } -} - -impl Display for HistEntry { - /// Similar to zsh's history format, but not entirely - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let command = self.with_escaped_newlines(); - let HistEntry { id, timestamp, command: _, new: _ } = self; - let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs(); - writeln!(f, ": {timestamp};{id};{command}") - } -} - -pub struct HistEntries(Vec); - - -impl FromStr for HistEntries { - type Err = ShErr; - fn from_str(s: &str) -> Result { - let mut entries = vec![]; - - let mut lines = s.lines().enumerate().peekable(); - let mut cur_line = String::new(); - - while let Some((i,line)) = lines.next() { - if !line.starts_with(": ") { - return Err( - ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] } - ) - } - let mut chars = line.chars().peekable(); - let mut feeding_lines = true; - while feeding_lines { - feeding_lines = false; - while let Some(ch) = chars.next() { - match ch { - '\\' => { - if let Some(esc_ch) = chars.next() { - cur_line.push(esc_ch); - } else { - cur_line.push('\n'); - feeding_lines = true; - } - } - '\n' => { - break - } - _ => { - cur_line.push(ch); - } - } - } - if feeding_lines { - let Some((_,line)) = lines.next() else { - return Err( - ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] } - ) - }; - chars = line.chars().peekable(); - } - } - let entry = cur_line.parse::()?; - entries.push(entry); - cur_line.clear(); - } - - - Ok(Self(entries)) - } -} - -fn read_hist_file(path: &Path) -> ShResult> { - if !path.exists() { - fs::File::create(path)?; - } - let raw = fs::read_to_string(path)?; - Ok(raw.parse::()?.0) -} - -pub struct History { - path: PathBuf, - entries: Vec, - search_mask: Vec, - cursor: usize, - search_direction: Direction, - ignore_dups: bool, - max_size: Option, -} - -impl History { - pub fn new() -> ShResult { - let path = PathBuf::from(env::var("FERNHIST").unwrap_or({ - let home = env::var("HOME").unwrap(); - format!("{home}/.fern_history") - })); - let mut entries = read_hist_file(&path)?; - { - let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0); - let timestamp = SystemTime::now(); - let command = "".into(); - entries.push(HistEntry { id, timestamp, command, new: true }) - } - let search_mask = entries.clone(); - let cursor = entries.len() - 1; - let mut new = Self { - path, - entries, - search_mask, - cursor, - search_direction: Direction::Backward, - ignore_dups: true, - max_size: None, - }; - new.push_empty_entry(); // Current pending command - Ok(new) - } - - pub fn entries(&self) -> &[HistEntry] { - &self.entries - } - - pub fn push_empty_entry(&mut self) { - } - - pub fn cursor_entry(&self) -> Option<&HistEntry> { - self.search_mask.get(self.cursor) - } - - pub fn update_pending_cmd(&mut self, command: &str) { - let Some(ent) = self.last_mut() else { - return - }; - let cmd = command.to_string(); - let constraint = SearchConstraint { kind: SearchKind::Prefix, term: cmd.clone() }; - - - ent.command = cmd; - self.constrain_entries(constraint); - } - - pub fn last_mut(&mut self) -> Option<&mut HistEntry> { - self.entries.last_mut() - } - - pub fn get_new_id(&self) -> u32 { - let Some(ent) = self.entries.last() else { - return 0 - }; - ent.id + 1 - } - - pub fn ignore_dups(&mut self, yn: bool) { - self.ignore_dups = yn - } - - pub fn max_hist_size(&mut self, size: Option) { - self.max_size = size - } - - pub fn constrain_entries(&mut self, constraint: SearchConstraint) { - let SearchConstraint { kind, term } = constraint; - match kind { - SearchKind::Prefix => { - if term.is_empty() { - self.search_mask = self.entries.clone(); - } else { - let filtered = self.entries - .clone() - .into_iter() - .filter(|ent| ent.command().starts_with(&term)); - - self.search_mask = filtered.collect(); - } - self.cursor = self.search_mask.len().saturating_sub(1); - } - SearchKind::Fuzzy => todo!(), - } - } - - pub fn hint_entry(&self) -> Option<&HistEntry> { - let second_to_last = self.search_mask.len().checked_sub(2)?; - self.search_mask.get(second_to_last) - } - - pub fn get_hint(&self) -> Option { - 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()) - } else { - None - } - } - - pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> { - let new_idx = self.cursor - .saturating_add_signed(offset) - .clamp(0, self.search_mask.len().saturating_sub(1)); - let ent = self.search_mask.get(new_idx)?; - - self.cursor = new_idx; - - Some(ent) - } - - pub fn push(&mut self, command: String) { - let timestamp = SystemTime::now(); - let id = self.get_new_id(); - if self.ignore_dups && self.is_dup(&command) { - return - } - self.entries.push(HistEntry { id, timestamp, command, new: true }); - } - - pub fn is_dup(&self, other: &str) -> bool { - let Some(ent) = self.entries.last() else { - return false - }; - let ent_cmd = &ent.command; - ent_cmd == other - } - - pub fn save(&mut self) -> ShResult<()> { - let mut file = OpenOptions::new() - .create(true) - .append(true) - .open(&self.path)?; - - let last_file_entry = self.entries - .iter() - .filter(|ent| !ent.new) - .next_back() - .map(|ent| ent.command.clone()) - .unwrap_or_default(); - - let entries = self.entries - .iter_mut() - .filter(|ent| { - ent.new && - !ent.command.is_empty() && - if self.ignore_dups { - ent.command() != last_file_entry - } else { - true - } - }); - - let mut data = String::new(); - for ent in entries { - ent.new = false; - write!(data, "{ent}").unwrap(); - } - - file.write_all(data.as_bytes())?; - - Ok(()) - } -} diff --git a/src/prompt/readline/keys.rs b/src/prompt/readline/keys.rs deleted file mode 100644 index 6cd784a..0000000 --- a/src/prompt/readline/keys.rs +++ /dev/null @@ -1,142 +0,0 @@ -use std::sync::Arc; -use unicode_segmentation::UnicodeSegmentation; - -// Credit to Rustyline for the design ideas in this module -// https://github.com/kkawakam/rustyline -#[derive(Clone,Debug)] -pub struct KeyEvent(pub KeyCode, pub ModKeys); - - -impl KeyEvent { - pub fn new(ch: &str, mut mods: ModKeys) -> Self { - use {KeyCode as K, KeyEvent as E, ModKeys as M}; - - let mut graphemes = ch.graphemes(true); - - let first = match graphemes.next() { - Some(g) => g, - None => return E(K::Null, mods), - }; - - // If more than one grapheme, it's not a single key event - if graphemes.next().is_some() { - return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired - } - - let mut chars = first.chars(); - - let single_char = chars.next(); - let is_single_char = chars.next().is_none(); - - match single_char { - Some(c) if is_single_char && c.is_control() => { - match c { - '\x00' => E(K::Char('@'), mods | M::CTRL), - '\x01' => E(K::Char('A'), mods | M::CTRL), - '\x02' => E(K::Char('B'), mods | M::CTRL), - '\x03' => E(K::Char('C'), mods | M::CTRL), - '\x04' => E(K::Char('D'), mods | M::CTRL), - '\x05' => E(K::Char('E'), mods | M::CTRL), - '\x06' => E(K::Char('F'), mods | M::CTRL), - '\x07' => E(K::Char('G'), mods | M::CTRL), - '\x08' => E(K::Backspace, mods), - '\x09' => { - if mods.contains(M::SHIFT) { - mods.remove(M::SHIFT); - E(K::BackTab, mods) - } else { - E(K::Tab, mods) - } - } - '\x0a' => E(K::Char('J'), mods | M::CTRL), - '\x0b' => E(K::Char('K'), mods | M::CTRL), - '\x0c' => E(K::Char('L'), mods | M::CTRL), - '\x0d' => E(K::Enter, mods), - '\x0e' => E(K::Char('N'), mods | M::CTRL), - '\x0f' => E(K::Char('O'), mods | M::CTRL), - '\x10' => E(K::Char('P'), mods | M::CTRL), - '\x11' => E(K::Char('Q'), mods | M::CTRL), - '\x12' => E(K::Char('R'), mods | M::CTRL), - '\x13' => E(K::Char('S'), mods | M::CTRL), - '\x14' => E(K::Char('T'), mods | M::CTRL), - '\x15' => E(K::Char('U'), mods | M::CTRL), - '\x16' => E(K::Char('V'), mods | M::CTRL), - '\x17' => E(K::Char('W'), mods | M::CTRL), - '\x18' => E(K::Char('X'), mods | M::CTRL), - '\x19' => E(K::Char('Y'), mods | M::CTRL), - '\x1a' => E(K::Char('Z'), mods | M::CTRL), - '\x1b' => E(K::Esc, mods), - '\x1c' => E(K::Char('\\'), mods | M::CTRL), - '\x1d' => E(K::Char(']'), mods | M::CTRL), - '\x1e' => E(K::Char('^'), mods | M::CTRL), - '\x1f' => E(K::Char('_'), mods | M::CTRL), - '\x7f' => E(K::Backspace, mods), - '\u{9b}' => E(K::Esc, mods | M::SHIFT), - _ => E(K::Null, mods), - } - } - Some(c) if is_single_char => { - if !mods.is_empty() { - mods.remove(M::SHIFT); - } - E(K::Char(c), mods) - } - _ => { - // multi-char grapheme (emoji, accented, etc) - if !mods.is_empty() { - mods.remove(M::SHIFT); - } - E(K::Grapheme(Arc::from(first)), mods) - } - } - } -} - -#[derive(Clone,Debug)] -pub enum KeyCode { - UnknownEscSeq, - Backspace, - BackTab, - BracketedPasteStart, - BracketedPasteEnd, - Char(char), - Grapheme(Arc), - Delete, - Down, - End, - Enter, - Esc, - F(u8), - Home, - Insert, - Left, - Null, - PageDown, - PageUp, - Right, - Tab, - Up, -} - -bitflags::bitflags! { - #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] - pub struct ModKeys: u8 { - /// Control modifier - const CTRL = 1<<3; - /// Escape or Alt modifier - const ALT = 1<<2; - /// Shift modifier - const SHIFT = 1<<1; - - /// No modifier - const NONE = 0; - /// Ctrl + Shift - const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits(); - /// Alt + Shift - const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits(); - /// Ctrl + Alt - const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits(); - /// Ctrl + Alt + Shift - const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits(); - } -} diff --git a/src/prompt/readline/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..af5957b 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -1,2182 +1,9 @@ -use std::{cmp::Ordering, fmt::Display, ops::{Range, RangeBounds}}; - -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, Dest, Direction, Motion, RegisterName, TextObj, To, Verb, ViCmd, Word}; - -#[derive(Debug, PartialEq, Eq)] -pub enum CharClass { - 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 - } - - if value.chars().all(char::is_alphanumeric) { - CharClass::Alphanum - } else if value.chars().all(char::is_whitespace) { - CharClass::Whitespace - } else if !value.chars().all(char::is_alphanumeric) { - CharClass::Symbol - } else { - Self::Other - } - } -} - -fn is_whitespace(a: &str) -> bool { - CharClass::from(a) == CharClass::Whitespace -} - -fn is_other_class(a: &str, b: &str) -> bool { - let a = CharClass::from(a); - let b = CharClass::from(b); - a != b -} - -fn is_other_class_or_ws(a: &str, b: &str) -> bool { - if is_whitespace(a) || is_whitespace(b) { - true - } else { - is_other_class(a, b) - } -} - -#[derive(Default,Debug)] -pub struct Edit { - pub pos: usize, - pub cursor_pos: usize, - pub old: String, - pub new: String, - pub merging: bool, -} - -impl Edit { - pub fn diff(a: &str, b: &str, old_cursor_pos: usize) -> Edit { - use std::cmp::min; - - let mut start = 0; - let max_start = min(a.len(), b.len()); - - // Calculate the prefix of the edit - while start < max_start && a.as_bytes()[start] == b.as_bytes()[start] { - start += 1; - } - - if start == a.len() && start == b.len() { - return Edit { - pos: start, - cursor_pos: old_cursor_pos, - old: String::new(), - new: String::new(), - merging: false, - }; - } - - let mut end_a = a.len(); - let mut end_b = b.len(); - - // Calculate the suffix of the edit - while end_a > start && end_b > start && a.as_bytes()[end_a - 1] == b.as_bytes()[end_b - 1] { - end_a -= 1; - end_b -= 1; - } - - // Slice off the prefix and suffix for both (safe because start/end are byte offsets) - let old = a[start..end_a].to_string(); - let new = b[start..end_b].to_string(); - - Edit { - pos: start, - cursor_pos: old_cursor_pos, - old, - new, - merging: false - } - } - pub fn start_merge(&mut self) { - self.merging = true - } - pub fn stop_merge(&mut self) { - self.merging = false - } - pub fn is_empty(&self) -> bool { - self.new.is_empty() && - self.old.is_empty() - } -} - -#[derive(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 + buffer: String } impl LineBuf { - pub fn new() -> Self { - Self { tab_stop: 8, ..Default::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(); - } - } - 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 has_hint(&self) -> bool { - self.hint.is_some() - } - 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) - } - 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) - } - } - 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 - } - 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(); - } - } - 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()); - } - } - /// 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); - } 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(); - } - } - } - pub fn find_prev_line_pos(&mut self) -> Option { - 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); - 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 { - 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); - - self.buffer.insert(start, '\t'); - - 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 { - 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; - } - } - - 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) - } - pub fn handle_edit(&mut self, old: String, new: String, curs_pos: usize) { - if self.edit_is_merging() { - let diff = Edit::diff(&old, &new, curs_pos); - if diff.is_empty() { - return - } - let Some(mut edit) = self.undo_stack.pop() else { - self.undo_stack.push(diff); - return - }; - - edit.new.push_str(&diff.new); - edit.old.push_str(&diff.old); - - self.undo_stack.push(edit); - } else { - let diff = Edit::diff(&old, &new, curs_pos); - if !diff.is_empty() { - self.undo_stack.push(diff); - } - } - } - pub fn 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(); - - // 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 let Some(edit) = self.undo_stack.last_mut() { - edit.stop_merge(); - } - } - - let ViCmd { register, verb, motion, .. } = cmd; - - let verb_count = verb.as_ref().map(|v| v.0); - let motion_count = motion.as_ref().map(|m| m.0); - - let before = self.buffer.clone(); - let cursor_pos = self.cursor; - - for _ in 0..verb_count.unwrap_or(1) { - for _ in 0..motion_count.unwrap_or(1) { - let motion_eval = motion - .clone() - .map(|m| self.eval_motion(m.1)) - .unwrap_or({ - self.selected_range - .clone() - .map(MotionKind::range) - .unwrap_or(MotionKind::Null) - }); - - 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); - } - } - } - - 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 !is_line_motion { - self.saved_col = None; - } - - if is_char_insert { - if let Some(edit) = self.undo_stack.last_mut() { - edit.start_merge(); - } - } - - - if self.clamp_cursor { - self.clamp_cursor(); - } - self.sync_cursor(); - Ok(()) + &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(); - 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); - } - 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); - } - } - } - if let Some(hint) = self.hint.as_ref() { - full_buf.push_str(&hint.styled(Style::BrightBlack)); - } - write!(f,"{}",full_buf) - } -} - -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 -} - -pub fn rot13(input: &str) -> String { - input.chars() - .map(|c| { - if c.is_ascii_lowercase() { - let offset = b'a'; - (((c as u8 - offset + 13) % 26) + offset) as char - } else if c.is_ascii_uppercase() { - let offset = b'A'; - (((c as u8 - offset + 13) % 26) + offset) as char - } else { - c - } - }) - .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) -} diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index fce4ce7..a8eadd6 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -1,462 +1,32 @@ -use std::time::Duration; +use linebuf::LineBuf; +use term::TermReader; -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 crate::libsh::error::ShResult; -use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, term::{Style, Styled}}; -use crate::prelude::*; - -pub mod keys; pub mod term; pub mod linebuf; -pub mod vicmd; -pub mod mode; -pub mod register; -pub mod history; +pub mod layout; -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; + fn readline(&mut self, prompt: Option) -> ShResult; } pub struct FernVi { - term: Terminal, - line: LineBuf, - history: History, - prompt: String, - mode: Box, - last_action: Option, - last_movement: Option, + reader: TermReader, + writer: TermWriter, + editor: LineBuf } 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(); - - 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)?; - continue - } - - let Some(cmd) = self.mode.handle_key(key) else { - continue - }; - - if self.should_grab_history(&cmd) { - flog!(DEBUG, "scrolling"); - self.scroll_history(cmd); - self.print_buf(true)?; - 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()?; - } - 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); - - if has_changes { - self.history.update_pending_cmd(self.line.as_str()); - } - - self.print_buf(true)?; - } + fn readline(&mut self, prompt: Option) -> ShResult { + todo!() } } 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, - mode: Box::new(ViInsert::new()), - last_action: None, - last_movement: None, - }) - } - pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { - if self.line.at_end_of_buffer() && self.line.has_hint() { - match self.mode.report_mode() { - ModeReport::Replace | - ModeReport::Insert => { - matches!( - event, - KeyEvent(KeyCode::Right, ModKeys::NONE) - ) - } - ModeReport::Visual | - ModeReport::Normal => { - matches!( - event, - KeyEvent(KeyCode::Right, ModKeys::NONE) - ) || - ( - self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() && - matches!( - event, - KeyEvent(KeyCode::Char('l'), ModKeys::NONE) - ) - ) - } - _ => unimplemented!() - } - } else { - false + pub fn new() -> Self { + Self { } } - /// 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 { - 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::LineDown))) && - self.line.end_of_line() == self.line.byte_len() - ) - } - pub fn print_buf(&mut self, refresh: bool) -> ShResult<()> { - let (height,width) = self.term.get_dimensions()?; - if refresh { - self.term.unwrite()?; - } - 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.term.recorded_write(&line_buf, offset)?; - self.term.position_cursor(self.line.cursor_display_coords(width))?; - - self.term.write(&self.mode.cursor_style()); - 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; - if cmd.is_mode_transition() { - let count = cmd.verb_count(); - let mut mode: Box = match cmd.verb().unwrap().1 { - Verb::Change | - Verb::InsertModeLineBreak(_) | - Verb::InsertMode => { - Box::new(ViInsert::new().with_count(count as u16)) - } - Verb::NormalMode => { - Box::new(ViNormal::new()) - } - Verb::ReplaceMode => { - Box::new(ViReplace::new().with_count(count as u16)) - } - Verb::VisualModeSelectLast => { - if self.mode.report_mode() != ModeReport::Visual { - self.line.start_selecting(SelectionMode::Char(SelectionAnchor::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) - } - 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.line.exec_cmd(cmd)?; - if selecting { - self.line.start_selecting(SelectionMode::Char(SelectionAnchor::End)); - } else { - self.line.stop_selecting(); - } - return Ok(()) - } else if cmd.is_cmd_repeat() { - let Some(replay) = self.last_action.clone() else { - return Ok(()) - }; - let ViCmd { verb, .. } = cmd; - let VerbCmd(count,_) = verb.unwrap(); - match replay { - CmdReplay::ModeReplay { cmds, mut repeat } => { - if count > 1 { - repeat = count as u16; - } - for _ in 0..repeat { - let cmds = cmds.clone(); - for cmd in cmds { - self.line.exec_cmd(cmd)? - } - } - } - CmdReplay::Single(mut cmd) => { - if count > 1 { - // Override the counts with the one passed to the '.' command - if cmd.verb.is_some() { - if let Some(v_mut) = cmd.verb.as_mut() { - v_mut.0 = count - } - if let Some(m_mut) = cmd.motion.as_mut() { - m_mut.0 = 1 - } - } else { - return Ok(()) // it has to have a verb to be repeatable, something weird happened - } - } - self.line.exec_cmd(cmd)?; - } - _ => unreachable!("motions should be handled in the other branch") - } - return Ok(()) - } else if cmd.is_motion_repeat() { - match cmd.motion.as_ref().unwrap() { - MotionCmd(count,Motion::RepeatMotion) => { - let Some(motion) = self.last_movement.clone() else { - return Ok(()) - }; - let repeat_cmd = ViCmd { - register: RegisterName::default(), - verb: None, - motion: Some(motion), - raw_seq: format!("{count};") - }; - return self.line.exec_cmd(repeat_cmd); - } - MotionCmd(count,Motion::RepeatMotionRev) => { - let Some(motion) = self.last_movement.clone() else { - return Ok(()) - }; - let mut new_motion = motion.invert_char_motion(); - new_motion.0 = *count; - let repeat_cmd = ViCmd { - register: RegisterName::default(), - verb: None, - motion: Some(new_motion), - raw_seq: format!("{count},") - }; - return self.line.exec_cmd(repeat_cmd); - } - _ => unreachable!() - } - } - - if cmd.is_repeatable() { - if self.mode.report_mode() == ModeReport::Visual { - // The motion is assigned in the line buffer execution, so we also have to assign it here - // in order to be able to repeat it - let range = self.line.selected_range().unwrap(); - cmd.motion = Some(MotionCmd(1,Motion::Range(range.start, range.end))) - } - self.last_action = Some(CmdReplay::Single(cmd.clone())); - } - - if cmd.is_char_search() { - self.last_movement = cmd.motion.clone() - } - - self.line.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(); - let mut mode: Box = Box::new(ViNormal::new()); - std::mem::swap(&mut mode, &mut self.mode); - } - Ok(()) - } } + diff --git a/src/prompt/readline/mode.rs b/src/prompt/readline/mode.rs deleted file mode 100644 index a379972..0000000 --- a/src/prompt/readline/mode.rs +++ /dev/null @@ -1,1484 +0,0 @@ -use std::iter::Peekable; -use std::str::Chars; - -use nix::NixPath; - -use super::keys::{KeyEvent as E, KeyCode as K, ModKeys as M}; -use super::vicmd::{Anchor, Bound, Dest, Direction, Motion, MotionBuilder, MotionCmd, RegisterName, TextObj, To, Verb, VerbBuilder, VerbCmd, ViCmd, Word}; -use crate::prelude::*; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ModeReport { - Insert, - Normal, - Visual, - Replace, - Unknown -} - -#[derive(Debug,Clone)] -pub enum CmdReplay { - ModeReplay { cmds: Vec, repeat: u16 }, - Single(ViCmd), - Motion(Motion) -} - -impl CmdReplay { - pub fn mode(cmds: Vec, repeat: u16) -> Self { - Self::ModeReplay { cmds, repeat } - } - pub fn single(cmd: ViCmd) -> Self { - Self::Single(cmd) - } - pub fn motion(motion: Motion) -> Self { - Self::Motion(motion) - } -} - -pub enum CmdState { - Pending, - Complete, - Invalid -} - -pub trait ViMode { - fn handle_key(&mut self, key: E) -> Option; - fn is_repeatable(&self) -> bool; - fn as_replay(&self) -> Option; - fn cursor_style(&self) -> String; - fn pending_seq(&self) -> Option; - fn move_cursor_on_undo(&self) -> bool; - fn clamp_cursor(&self) -> bool; - fn hist_scroll_start_pos(&self) -> Option; - fn report_mode(&self) -> ModeReport; -} - -#[derive(Default,Debug)] -pub struct ViInsert { - cmds: Vec, - pending_cmd: ViCmd, - repeat_count: u16 -} - -impl ViInsert { - pub fn new() -> Self { - Self::default() - } - pub fn with_count(mut self, repeat_count: u16) -> Self { - self.repeat_count = repeat_count; - self - } - pub fn register_and_return(&mut self) -> Option { - let cmd = self.take_cmd(); - self.register_cmd(&cmd); - Some(cmd) - } - pub fn ctrl_w_is_undo(&self) -> bool { - let insert_count = self.cmds.iter().filter(|cmd| { - matches!(cmd.verb(),Some(VerbCmd(1, Verb::InsertChar(_)))) - }).count(); - let backspace_count = self.cmds.iter().filter(|cmd| { - matches!(cmd.verb(),Some(VerbCmd(1, Verb::Delete))) - }).count(); - insert_count > backspace_count - } - pub fn register_cmd(&mut self, cmd: &ViCmd) { - self.cmds.push(cmd.clone()) - } - pub fn take_cmd(&mut self) -> ViCmd { - std::mem::take(&mut self.pending_cmd) - } -} - -impl ViMode for ViInsert { - fn handle_key(&mut self, key: E) -> Option { - match key { - E(K::Char(ch), M::NONE) => { - self.pending_cmd.set_verb(VerbCmd(1,Verb::InsertChar(ch))); - self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)); - self.register_and_return() - } - E(K::Char('W'), M::CTRL) => { - if self.ctrl_w_is_undo() { - self.pending_cmd.set_verb(VerbCmd(1,Verb::Undo)); - self.cmds.clear(); - Some(self.take_cmd()) - } else { - self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete)); - self.pending_cmd.set_motion(MotionCmd(1, Motion::BackwardWord(To::Start, Word::Normal))); - self.register_and_return() - } - } - E(K::Char('H'), M::CTRL) | - E(K::Backspace, M::NONE) => { - self.pending_cmd.set_verb(VerbCmd(1,Verb::Delete)); - self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); - self.register_and_return() - } - - E(K::BackTab, M::NONE) => { - self.pending_cmd.set_verb(VerbCmd(1,Verb::CompleteBackward)); - self.register_and_return() - } - - E(K::Char('I'), M::CTRL) | - E(K::Tab, M::NONE) => { - self.pending_cmd.set_verb(VerbCmd(1,Verb::Complete)); - self.register_and_return() - } - - E(K::Esc, M::NONE) => { - self.pending_cmd.set_verb(VerbCmd(1,Verb::NormalMode)); - self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); - self.register_and_return() - } - _ => common_cmds(key) - } - } - - fn is_repeatable(&self) -> bool { - true - } - - fn as_replay(&self) -> Option { - Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count)) - } - - fn cursor_style(&self) -> String { - "\x1b[6 q".to_string() - } - fn pending_seq(&self) -> Option { - None - } - fn move_cursor_on_undo(&self) -> bool { - true - } - fn clamp_cursor(&self) -> bool { - false - } - fn hist_scroll_start_pos(&self) -> Option { - Some(To::End) - } - fn report_mode(&self) -> ModeReport { - ModeReport::Insert - } -} - -#[derive(Default,Debug)] -pub struct ViReplace { - cmds: Vec, - pending_cmd: ViCmd, - repeat_count: u16 -} - -impl ViReplace { - pub fn new() -> Self { - Self::default() - } - pub fn with_count(mut self, repeat_count: u16) -> Self { - self.repeat_count = repeat_count; - self - } - pub fn register_and_return(&mut self) -> Option { - let cmd = self.take_cmd(); - self.register_cmd(&cmd); - Some(cmd) - } - pub fn ctrl_w_is_undo(&self) -> bool { - let insert_count = self.cmds.iter().filter(|cmd| { - matches!(cmd.verb(),Some(VerbCmd(1, Verb::ReplaceChar(_)))) - }).count(); - let backspace_count = self.cmds.iter().filter(|cmd| { - matches!(cmd.verb(),Some(VerbCmd(1, Verb::Delete))) - }).count(); - insert_count > backspace_count - } - pub fn register_cmd(&mut self, cmd: &ViCmd) { - self.cmds.push(cmd.clone()) - } - pub fn take_cmd(&mut self) -> ViCmd { - std::mem::take(&mut self.pending_cmd) - } -} - -impl ViMode for ViReplace { - fn handle_key(&mut self, key: E) -> Option { - match key { - E(K::Char(ch), M::NONE) => { - self.pending_cmd.set_verb(VerbCmd(1,Verb::ReplaceChar(ch))); - self.pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)); - self.register_and_return() - } - E(K::Char('W'), M::CTRL) => { - if self.ctrl_w_is_undo() { - self.pending_cmd.set_verb(VerbCmd(1,Verb::Undo)); - self.cmds.clear(); - Some(self.take_cmd()) - } else { - self.pending_cmd.set_motion(MotionCmd(1, Motion::BackwardWord(To::Start, Word::Normal))); - self.register_and_return() - } - } - E(K::Char('H'), M::CTRL) | - E(K::Backspace, M::NONE) => { - self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); - self.register_and_return() - } - - E(K::BackTab, M::NONE) => { - self.pending_cmd.set_verb(VerbCmd(1,Verb::CompleteBackward)); - self.register_and_return() - } - - E(K::Char('I'), M::CTRL) | - E(K::Tab, M::NONE) => { - self.pending_cmd.set_verb(VerbCmd(1,Verb::Complete)); - self.register_and_return() - } - - E(K::Esc, M::NONE) => { - self.pending_cmd.set_verb(VerbCmd(1,Verb::NormalMode)); - self.pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)); - self.register_and_return() - } - _ => common_cmds(key) - } - } - fn is_repeatable(&self) -> bool { - true - } - fn cursor_style(&self) -> String { - "\x1b[4 q".to_string() - } - fn pending_seq(&self) -> Option { - None - } - fn as_replay(&self) -> Option { - Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count)) - } - fn move_cursor_on_undo(&self) -> bool { - true - } - fn clamp_cursor(&self) -> bool { - true - } - fn hist_scroll_start_pos(&self) -> Option { - Some(To::End) - } - fn report_mode(&self) -> ModeReport { - ModeReport::Replace - } -} -#[derive(Default,Debug)] -pub struct ViNormal { - pending_seq: String, -} - -impl ViNormal { - pub fn new() -> Self { - Self::default() - } - pub fn clear_cmd(&mut self) { - self.pending_seq = String::new(); - } - pub fn take_cmd(&mut self) -> String { - std::mem::take(&mut self.pending_seq) - } - fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState { - if verb.is_none() { - match motion { - Some(Motion::TextObj(_,_)) => return CmdState::Invalid, - Some(_) => return CmdState::Complete, - None => return CmdState::Pending - } - } - if verb.is_some() && motion.is_none() { - match verb.unwrap() { - Verb::Put(_) | - Verb::DeleteChar(_) => CmdState::Complete, - _ => CmdState::Pending - } - } else { - CmdState::Complete - } - } - pub fn parse_count(&self, chars: &mut Peekable>) -> Option { - let mut count = String::new(); - let Some(_digit @ '1'..='9') = chars.peek() else { - return None - }; - count.push(chars.next().unwrap()); - while let Some(_digit @ '0'..='9') = chars.peek() { - count.push(chars.next().unwrap()); - } - if !count.is_empty() { - count.parse::().ok() - } else { - None - } - } - /// End the parse and clear the pending sequence - #[track_caller] - pub fn quit_parse(&mut self) -> Option { - flog!(DEBUG, std::panic::Location::caller()); - flog!(WARN, "exiting parse early with sequence: {}",self.pending_seq); - self.clear_cmd(); - None - } - pub fn try_parse(&mut self, ch: char) -> Option { - self.pending_seq.push(ch); - let mut chars = self.pending_seq.chars().peekable(); - - let register = 'reg_parse: { - let mut chars_clone = chars.clone(); - let count = self.parse_count(&mut chars_clone); - - let Some('"') = chars_clone.next() else { - break 'reg_parse RegisterName::default() - }; - - let Some(reg_name) = chars_clone.next() else { - return None // Pending register name - }; - match reg_name { - 'a'..='z' | - 'A'..='Z' => { /* proceed */ } - _ => return self.quit_parse() - } - - chars = chars_clone; - RegisterName::new(Some(reg_name), count) - }; - - let verb = 'verb_parse: { - let mut chars_clone = chars.clone(); - let count = self.parse_count(&mut chars_clone).unwrap_or(1); - - let Some(ch) = chars_clone.next() else { - break 'verb_parse None - }; - match ch { - 'g' => { - if let Some(ch) = chars_clone.peek() { - match ch { - 'v' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - '?' => { - chars_clone.next(); - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Rot13)); - } - _ => break 'verb_parse None - } - } else { - break 'verb_parse None - } - } - '.' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::RepeatLast)), - motion: None, - raw_seq: self.take_cmd(), - } - ) - } - 'x' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::DeleteChar(Anchor::After))); - } - 'X' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::DeleteChar(Anchor::Before))); - } - 'p' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::After))); - } - 'P' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before))); - } - '>' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Indent)); - } - '<' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Dedent)); - } - 'r' => { - let ch = chars_clone.next()?; - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))), - motion: Some(MotionCmd(count, Motion::ForwardChar)), - raw_seq: self.take_cmd() - } - ) - } - 'R' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::ReplaceMode)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - '~' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::ToggleCase)), - motion: Some(MotionCmd(count, Motion::ForwardChar)), - raw_seq: self.take_cmd() - } - ) - } - 'u' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::Undo)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'v' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::VisualMode)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'V' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::VisualModeLine)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'o' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::After))), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'O' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::Before))), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'a' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::InsertMode)), - motion: Some(MotionCmd(1, Motion::ForwardChar)), - raw_seq: self.take_cmd() - } - ) - } - 'A' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::InsertMode)), - motion: Some(MotionCmd(1, Motion::EndOfLine)), - raw_seq: self.take_cmd() - } - ) - } - 'i' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::InsertMode)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'I' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::InsertMode)), - motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)), - raw_seq: self.take_cmd() - } - ) - } - 'J' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::JoinLines)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'y' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Yank)) - } - 'd' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Delete)) - } - 'c' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Change)) - } - 'Y' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::Yank)), - motion: Some(MotionCmd(1, Motion::EndOfLine)), - raw_seq: self.take_cmd() - } - ) - } - 'D' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::Delete)), - motion: Some(MotionCmd(1, Motion::EndOfLine)), - raw_seq: self.take_cmd() - } - ) - } - 'C' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::Change)), - motion: Some(MotionCmd(1, Motion::EndOfLine)), - raw_seq: self.take_cmd() - } - ) - } - '=' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Equalize)) - } - _ => break 'verb_parse None - } - }; - - let motion = 'motion_parse: { - let mut chars_clone = chars.clone(); - let count = self.parse_count(&mut chars_clone).unwrap_or(1); - - let Some(ch) = chars_clone.next() else { - break 'motion_parse None - }; - match (ch, &verb) { - ('?', Some(VerbCmd(_,Verb::Rot13))) | - ('d', Some(VerbCmd(_,Verb::Delete))) | - ('c', Some(VerbCmd(_,Verb::Change))) | - ('y', Some(VerbCmd(_,Verb::Yank))) | - ('=', Some(VerbCmd(_,Verb::Equalize))) | - ('>', Some(VerbCmd(_,Verb::Indent))) | - ('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)), - _ => {} - } - match ch { - 'g' => { - if let Some(ch) = chars_clone.peek() { - match ch { - 'g' => { - chars_clone.next(); - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer)) - } - 'e' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::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 { - break 'motion_parse None - } - } - 'G' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer)); - } - 'f' => { - let Some(ch) = chars_clone.peek() else { - break 'motion_parse None - }; - - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, *ch))) - } - 'F' => { - let Some(ch) = chars_clone.peek() else { - break 'motion_parse None - }; - - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, *ch))) - } - 't' => { - let Some(ch) = chars_clone.peek() else { - break 'motion_parse None - }; - - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, *ch))) - } - 'T' => { - let Some(ch) = chars_clone.peek() else { - break 'motion_parse None - }; - - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, *ch))) - } - ';' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion)); - } - ',' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev)); - } - '|' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count))); - } - '^' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfFirstWord)); - } - '0' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine)); - } - '$' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine)); - } - 'k' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::LineUp)); - } - 'j' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::LineDown)); - } - 'h' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar)); - } - 'l' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar)); - } - 'w' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Normal))); - } - 'W' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Big))); - } - 'e' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Normal))); - } - 'E' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Big))); - } - 'b' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Normal))); - } - 'B' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Big))); - } - ch if ch == 'i' || ch == 'a' => { - let bound = match ch { - 'i' => Bound::Inside, - 'a' => Bound::Around, - _ => unreachable!() - }; - if chars_clone.peek().is_none() { - break 'motion_parse None - } - let obj = match chars_clone.next().unwrap() { - 'w' => TextObj::Word(Word::Normal), - 'W' => TextObj::Word(Word::Big), - '"' => TextObj::DoubleQuote, - '\'' => TextObj::SingleQuote, - '(' | ')' | 'b' => TextObj::Paren, - '{' | '}' | 'B' => TextObj::Brace, - '[' | ']' => TextObj::Bracket, - '<' | '>' => TextObj::Angle, - _ => return self.quit_parse() - }; - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj, bound))) - } - _ => return self.quit_parse(), - } - }; - - if chars.peek().is_some() { - flog!(WARN, "Unused characters in Vi command parse!"); - flog!(WARN, "{:?}",chars) - } - - let verb_ref = verb.as_ref().map(|v| &v.1); - let motion_ref = motion.as_ref().map(|m| &m.1); - - match self.validate_combination(verb_ref, motion_ref) { - CmdState::Complete => { - Some( - ViCmd { - register, - verb, - motion, - raw_seq: std::mem::take(&mut self.pending_seq) - } - ) - } - CmdState::Pending => { - None - } - CmdState::Invalid => { - self.pending_seq.clear(); - None - } - } - } -} - -impl ViMode for ViNormal { - fn handle_key(&mut self, key: E) -> Option { - match key { - E(K::Char(ch), M::NONE) => self.try_parse(ch), - E(K::Backspace, M::NONE) => { - Some(ViCmd { - register: Default::default(), - verb: None, - motion: Some(MotionCmd(1, Motion::BackwardChar)), - raw_seq: "".into(), - }) - } - E(K::Char('R'), M::CTRL) => { - let mut chars = self.pending_seq.chars().peekable(); - let count = self.parse_count(&mut chars).unwrap_or(1); - Some( - ViCmd { - register: RegisterName::default(), - verb: Some(VerbCmd(count,Verb::Redo)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - E(K::Esc, M::NONE) => { - self.clear_cmd(); - None - } - _ => { - if let Some(cmd) = common_cmds(key) { - self.clear_cmd(); - Some(cmd) - } else { - None - } - } - } - } - - fn is_repeatable(&self) -> bool { - false - } - - fn as_replay(&self) -> Option { - None - } - - fn cursor_style(&self) -> String { - "\x1b[2 q".to_string() - } - - fn pending_seq(&self) -> Option { - Some(self.pending_seq.clone()) - } - - fn move_cursor_on_undo(&self) -> bool { - false - } - fn clamp_cursor(&self) -> bool { - true - } - fn hist_scroll_start_pos(&self) -> Option { - None - } - fn report_mode(&self) -> ModeReport { - ModeReport::Normal - } -} - -#[derive(Default,Debug)] -pub struct ViVisual { - pending_seq: String, -} - -impl ViVisual { - pub fn new() -> Self { - Self::default() - } - pub fn clear_cmd(&mut self) { - self.pending_seq = String::new(); - } - pub fn take_cmd(&mut self) -> String { - std::mem::take(&mut self.pending_seq) - } - fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState { - if verb.is_none() { - match motion { - Some(Motion::TextObj(_,_)) => return CmdState::Invalid, - Some(_) => return CmdState::Complete, - None => return CmdState::Pending - } - } - if verb.is_some() && motion.is_none() { - match verb.unwrap() { - Verb::Put(_) | - Verb::DeleteChar(_) => CmdState::Complete, - _ => CmdState::Pending - } - } else { - CmdState::Complete - } - } - pub fn parse_count(&self, chars: &mut Peekable>) -> Option { - let mut count = String::new(); - let Some(_digit @ '1'..='9') = chars.peek() else { - return None - }; - count.push(chars.next().unwrap()); - while let Some(_digit @ '0'..='9') = chars.peek() { - count.push(chars.next().unwrap()); - } - if !count.is_empty() { - count.parse::().ok() - } else { - None - } - } - /// End the parse and clear the pending sequence - #[track_caller] - pub fn quit_parse(&mut self) -> Option { - flog!(DEBUG, std::panic::Location::caller()); - flog!(WARN, "exiting parse early with sequence: {}",self.pending_seq); - self.clear_cmd(); - None - } - pub fn try_parse(&mut self, ch: char) -> Option { - self.pending_seq.push(ch); - let mut chars = self.pending_seq.chars().peekable(); - - let register = 'reg_parse: { - let mut chars_clone = chars.clone(); - let count = self.parse_count(&mut chars_clone); - - let Some('"') = chars_clone.next() else { - break 'reg_parse RegisterName::default() - }; - - let Some(reg_name) = chars_clone.next() else { - return None // Pending register name - }; - match reg_name { - 'a'..='z' | - 'A'..='Z' => { /* proceed */ } - _ => return self.quit_parse() - } - - chars = chars_clone; - RegisterName::new(Some(reg_name), count) - }; - - let verb = 'verb_parse: { - let mut chars_clone = chars.clone(); - let count = self.parse_count(&mut chars_clone).unwrap_or(1); - - let Some(ch) = chars_clone.next() else { - break 'verb_parse None - }; - match ch { - 'g' => { - if let Some(ch) = chars_clone.peek() { - match ch { - 'v' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - '?' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::Rot13)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - _ => break 'verb_parse None - } - } else { - break 'verb_parse None - } - } - '.' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::RepeatLast)), - motion: None, - raw_seq: self.take_cmd(), - } - ) - } - 'x' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Delete)); - } - 'X' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::Delete)), - motion: Some(MotionCmd(1, Motion::WholeLine)), - raw_seq: self.take_cmd(), - } - ) - } - 'Y' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::Yank)), - motion: Some(MotionCmd(1, Motion::WholeLine)), - raw_seq: self.take_cmd() - } - ) - } - 'D' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::Delete)), - motion: Some(MotionCmd(1, Motion::WholeLine)), - raw_seq: self.take_cmd() - } - ) - } - 'R' | - 'C' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::Change)), - motion: Some(MotionCmd(1, Motion::WholeLine)), - raw_seq: self.take_cmd(), - } - ) - } - '>' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::Indent)), - motion: Some(MotionCmd(1, Motion::WholeLine)), - raw_seq: self.take_cmd(), - } - ) - } - '<' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::Dedent)), - motion: Some(MotionCmd(1, Motion::WholeLine)), - raw_seq: self.take_cmd(), - } - ) - } - '=' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::Equalize)), - motion: Some(MotionCmd(1, Motion::WholeLine)), - raw_seq: self.take_cmd(), - } - ) - } - 'p' | - 'P' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before))); - } - 'r' => { - let ch = chars_clone.next()?; - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - '~' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(1, Verb::ToggleCase)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'u' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::ToLower)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'U' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::ToUpper)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'O' | - 'o' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::SwapVisualAnchor)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'A' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::InsertMode)), - motion: Some(MotionCmd(1, Motion::ForwardChar)), - raw_seq: self.take_cmd() - } - ) - } - 'I' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::InsertMode)), - motion: Some(MotionCmd(1, Motion::BeginningOfLine)), - raw_seq: self.take_cmd() - } - ) - } - 'J' => { - return Some( - ViCmd { - register, - verb: Some(VerbCmd(count, Verb::JoinLines)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - 'y' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Yank)) - } - 'd' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Delete)) - } - 'c' => { - chars = chars_clone; - break 'verb_parse Some(VerbCmd(count, Verb::Change)) - } - _ => break 'verb_parse None - } - }; - - if let Some(verb) = verb { - return Some(ViCmd { - register, - verb: Some(verb), - motion: None, - raw_seq: self.take_cmd() - }) - } - - let motion = 'motion_parse: { - let mut chars_clone = chars.clone(); - let count = self.parse_count(&mut chars_clone).unwrap_or(1); - - let Some(ch) = chars_clone.next() else { - break 'motion_parse None - }; - match (ch, &verb) { - ('d', Some(VerbCmd(_,Verb::Delete))) | - ('c', Some(VerbCmd(_,Verb::Change))) | - ('y', Some(VerbCmd(_,Verb::Yank))) | - ('=', Some(VerbCmd(_,Verb::Equalize))) | - ('>', Some(VerbCmd(_,Verb::Indent))) | - ('<', Some(VerbCmd(_,Verb::Dedent))) => break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)), - _ => {} - } - match ch { - 'g' => { - if let Some(ch) = chars_clone.peek() { - match ch { - 'g' => { - chars_clone.next(); - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer)) - } - 'e' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::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)); - } - _ => return self.quit_parse() - } - } else { - break 'motion_parse None - } - } - 'f' => { - let Some(ch) = chars_clone.peek() else { - break 'motion_parse None - }; - - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::On, (*ch).into()))) - } - 'F' => { - let Some(ch) = chars_clone.peek() else { - break 'motion_parse None - }; - - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::On, (*ch).into()))) - } - 't' => { - let Some(ch) = chars_clone.peek() else { - break 'motion_parse None - }; - - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Forward, Dest::Before, (*ch).into()))) - } - 'T' => { - let Some(ch) = chars_clone.peek() else { - break 'motion_parse None - }; - - break 'motion_parse Some(MotionCmd(count, Motion::CharSearch(Direction::Backward, Dest::Before, (*ch).into()))) - } - ';' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion)); - } - ',' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev)); - } - '|' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(1, Motion::ToColumn(count))); - } - '0' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine)); - } - '$' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine)); - } - 'k' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::LineUp)); - } - 'j' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::LineDown)); - } - 'h' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar)); - } - 'l' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar)); - } - 'w' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Normal))); - } - 'W' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::Start, Word::Big))); - } - 'e' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Normal))); - } - 'E' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::ForwardWord(To::End, Word::Big))); - } - 'b' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Normal))); - } - 'B' => { - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::BackwardWord(To::Start, Word::Big))); - } - ch if ch == 'i' || ch == 'a' => { - let bound = match ch { - 'i' => Bound::Inside, - 'a' => Bound::Around, - _ => unreachable!() - }; - if chars_clone.peek().is_none() { - break 'motion_parse None - } - let obj = match chars_clone.next().unwrap() { - 'w' => TextObj::Word(Word::Normal), - 'W' => TextObj::Word(Word::Big), - '"' => TextObj::DoubleQuote, - '\'' => TextObj::SingleQuote, - '(' | ')' | 'b' => TextObj::Paren, - '{' | '}' | 'B' => TextObj::Brace, - '[' | ']' => TextObj::Bracket, - '<' | '>' => TextObj::Angle, - _ => return self.quit_parse() - }; - chars = chars_clone; - break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj, bound))) - } - _ => return self.quit_parse(), - } - }; - - if chars.peek().is_some() { - flog!(WARN, "Unused characters in Vi command parse!"); - flog!(WARN, "{:?}",chars) - } - - let verb_ref = verb.as_ref().map(|v| &v.1); - let motion_ref = motion.as_ref().map(|m| &m.1); - - match self.validate_combination(verb_ref, motion_ref) { - CmdState::Complete => { - let cmd = Some( - ViCmd { - register, - verb, - motion, - raw_seq: std::mem::take(&mut self.pending_seq) - } - ); - cmd - } - CmdState::Pending => { - None - } - CmdState::Invalid => { - self.pending_seq.clear(); - None - } - } - } -} - -impl ViMode for ViVisual { - fn handle_key(&mut self, key: E) -> Option { - match key { - E(K::Char(ch), M::NONE) => self.try_parse(ch), - E(K::Backspace, M::NONE) => { - Some(ViCmd { - register: Default::default(), - verb: None, - motion: Some(MotionCmd(1, Motion::BackwardChar)), - raw_seq: "".into(), - }) - } - E(K::Char('R'), M::CTRL) => { - let mut chars = self.pending_seq.chars().peekable(); - let count = self.parse_count(&mut chars).unwrap_or(1); - Some( - ViCmd { - register: RegisterName::default(), - verb: Some(VerbCmd(count,Verb::Redo)), - motion: None, - raw_seq: self.take_cmd() - } - ) - } - E(K::Esc, M::NONE) => { - Some( - ViCmd { - register: Default::default(), - verb: Some(VerbCmd(1, Verb::NormalMode)), - motion: Some(MotionCmd(1, Motion::Null)), - raw_seq: self.take_cmd() - }) - } - _ => { - if let Some(cmd) = common_cmds(key) { - self.clear_cmd(); - Some(cmd) - } else { - None - } - } - } - } - - fn is_repeatable(&self) -> bool { - true - } - - fn as_replay(&self) -> Option { - None - } - - fn cursor_style(&self) -> String { - "\x1b[2 q".to_string() - } - - fn pending_seq(&self) -> Option { - Some(self.pending_seq.clone()) - } - - fn move_cursor_on_undo(&self) -> bool { - true - } - - fn clamp_cursor(&self) -> bool { - true - } - - fn hist_scroll_start_pos(&self) -> Option { - None - } - - fn report_mode(&self) -> ModeReport { - ModeReport::Visual - } -} - -pub fn common_cmds(key: E) -> Option { - let mut pending_cmd = ViCmd::new(); - match key { - E(K::Home, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::BeginningOfLine)), - E(K::End, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::EndOfLine)), - E(K::Left, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::BackwardChar)), - E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::ForwardChar)), - E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineUp)), - E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1,Motion::LineDown)), - E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::AcceptLine)), - E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1,Verb::EndOfFile)), - E(K::Delete, M::NONE) => pending_cmd.set_verb(VerbCmd(1,Verb::DeleteChar(Anchor::After))), - E(K::Backspace, M::NONE) | - E(K::Char('H'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1,Verb::DeleteChar(Anchor::Before))), - _ => return None - } - Some(pending_cmd) -} diff --git a/src/prompt/readline/register.rs b/src/prompt/readline/register.rs deleted file mode 100644 index 5fdc12d..0000000 --- a/src/prompt/readline/register.rs +++ /dev/null @@ -1,168 +0,0 @@ -use std::sync::Mutex; - -pub static REGISTERS: Mutex = Mutex::new(Registers::new()); - -pub fn read_register(ch: Option) -> Option { - let lock = REGISTERS.lock().unwrap(); - lock.get_reg(ch).map(|r| r.buf().clone()) -} - -pub fn write_register(ch: Option, buf: String) { - let mut lock = REGISTERS.lock().unwrap(); - if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) } -} - -pub fn append_register(ch: Option, buf: String) { - let mut lock = REGISTERS.lock().unwrap(); - if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) } -} - -#[derive(Default,Debug)] -pub struct Registers { - default: Register, - a: Register, - b: Register, - c: Register, - d: Register, - e: Register, - f: Register, - g: Register, - h: Register, - i: Register, - j: Register, - k: Register, - l: Register, - m: Register, - n: Register, - o: Register, - p: Register, - q: Register, - r: Register, - s: Register, - t: Register, - u: Register, - v: Register, - w: Register, - x: Register, - y: Register, - z: Register, -} - -impl Registers { - pub const fn new() -> Self { - Self { - default: Register(String::new()), - a: Register(String::new()), - b: Register(String::new()), - c: Register(String::new()), - d: Register(String::new()), - e: Register(String::new()), - f: Register(String::new()), - g: Register(String::new()), - h: Register(String::new()), - i: Register(String::new()), - j: Register(String::new()), - k: Register(String::new()), - l: Register(String::new()), - m: Register(String::new()), - n: Register(String::new()), - o: Register(String::new()), - p: Register(String::new()), - q: Register(String::new()), - r: Register(String::new()), - s: Register(String::new()), - t: Register(String::new()), - u: Register(String::new()), - v: Register(String::new()), - w: Register(String::new()), - x: Register(String::new()), - y: Register(String::new()), - z: Register(String::new()), - } - } - pub fn get_reg(&self, ch: Option) -> Option<&Register> { - let Some(ch) = ch else { - return Some(&self.default) - }; - match ch { - 'a' => Some(&self.a), - 'b' => Some(&self.b), - 'c' => Some(&self.c), - 'd' => Some(&self.d), - 'e' => Some(&self.e), - 'f' => Some(&self.f), - 'g' => Some(&self.g), - 'h' => Some(&self.h), - 'i' => Some(&self.i), - 'j' => Some(&self.j), - 'k' => Some(&self.k), - 'l' => Some(&self.l), - 'm' => Some(&self.m), - 'n' => Some(&self.n), - 'o' => Some(&self.o), - 'p' => Some(&self.p), - 'q' => Some(&self.q), - 'r' => Some(&self.r), - 's' => Some(&self.s), - 't' => Some(&self.t), - 'u' => Some(&self.u), - 'v' => Some(&self.v), - 'w' => Some(&self.w), - 'x' => Some(&self.x), - 'y' => Some(&self.y), - 'z' => Some(&self.z), - _ => None - } - } - pub fn get_reg_mut(&mut self, ch: Option) -> Option<&mut Register> { - let Some(ch) = ch else { - return Some(&mut self.default) - }; - match ch { - 'a' => Some(&mut self.a), - 'b' => Some(&mut self.b), - 'c' => Some(&mut self.c), - 'd' => Some(&mut self.d), - 'e' => Some(&mut self.e), - 'f' => Some(&mut self.f), - 'g' => Some(&mut self.g), - 'h' => Some(&mut self.h), - 'i' => Some(&mut self.i), - 'j' => Some(&mut self.j), - 'k' => Some(&mut self.k), - 'l' => Some(&mut self.l), - 'm' => Some(&mut self.m), - 'n' => Some(&mut self.n), - 'o' => Some(&mut self.o), - 'p' => Some(&mut self.p), - 'q' => Some(&mut self.q), - 'r' => Some(&mut self.r), - 's' => Some(&mut self.s), - 't' => Some(&mut self.t), - 'u' => Some(&mut self.u), - 'v' => Some(&mut self.v), - 'w' => Some(&mut self.w), - 'x' => Some(&mut self.x), - 'y' => Some(&mut self.y), - 'z' => Some(&mut self.z), - _ => None - } - } -} - -#[derive(Clone,Default,Debug)] -pub struct Register(String); -impl Register { - pub fn buf(&self) -> &String { - &self.0 - } - pub fn write(&mut self, buf: String) { - self.0 = buf - } - pub fn append(&mut self, buf: String) { - self.0.push_str(&buf) - } - pub fn clear(&mut self) { - self.0.clear() - } -} diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 2526929..007a1d0 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -1,459 +1,463 @@ -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::Write, io::{BufRead, BufReader, Read}, ops::{Deref, DerefMut}, os::fd::{AsFd, BorrowedFd, RawFd}}; -use crate::libsh::error::ShResult; -use crate::prelude::*; +use nix::{errno::Errno, libc, poll::{self, PollFlags, PollTimeout}, unistd::isatty}; +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; -use super::keys::{KeyCode, KeyEvent, ModKeys}; +use crate::libsh::error::{ShErr, ShErrKind, ShResult}; -#[derive(Default,Debug)] -struct WriteMap { - lines: usize, - cols: usize, - offset: usize +use super::linebuf::LineBuf; + +pub type Row = u16; +pub type Col = u16; + +#[derive(Default,Clone,Copy,PartialEq,Eq,PartialOrd,Ord,Debug)] +pub struct Pos { + col: Col, + row: Row } -#[derive(Debug)] -pub struct Terminal { - stdin: RawFd, - stdout: RawFd, - recording: bool, - write_records: WriteMap, - cursor_records: WriteMap +// 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) + } + + 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.into(), rows.into()) + } + _ => (80,24) + } + } } -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(), +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(()) +} - 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)); - } - }) - } - - /// 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)); - } - }) - } - - 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; +} + +#[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 == true)); + 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 == true)); + 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 TermReader { + buffer: BufReader +} + +impl TermReader { + pub fn new() -> Self { + Self { + buffer: BufReader::new(TermBuffer::new(1)) + } + } + + pub fn poll(&mut self, timeout: PollTimeout) -> ShResult { + if self.buffer.buffer().len() > 0 { + 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); + } +} + +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 Layout { + pub fn new() -> Self { + let w_calc = width_calculator(); + Self { + w_calc, + prompt_end: Pos::default(), + cursor: Pos::default(), + end: Pos::default(), + } + } +} + +pub struct LineWriter { + out: RawFd, + t_cols: Col, // terminal width + buffer: String, + w_calc: Box, + tab_stop: u16, +} + +impl LineWriter { + 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 flush_write(&mut self, buf: &str) -> ShResult<()> { + write_all(self.out, buf)?; + Ok(()) + } + pub fn clear_rows(&mut self, layout: &Layout) { + 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[K\x1b[A"); + } + self.buffer.push_str("\x1b[K"); + } + pub fn move_cursor(&mut self, old: Pos, new: Pos) -> ShResult<()> { + self.buffer.clear(); + let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer"); + + match new.row.cmp(&old.row) { + std::cmp::Ordering::Greater => { + let shift = new.row - old.row; + match shift { + 1 => self.buffer.push_str("\x1b[B"), + _ => write!(self.buffer, "\x1b[{shift}B").map_err(err)? } } - - // Try parse valid UTF-8 from collected bytes - if let Ok(s) = str::from_utf8(&collected) { - return KeyEvent::new(s, ModKeys::empty()); - } - - // If it's not valid UTF-8 yet, loop to collect more bytes - if collected.len() >= 4 { - // UTF-8 max char length is 4; if it's still invalid, give up - break; + std::cmp::Ordering::Less => { + let shift = old.row - new.row; + match shift { + 1 => self.buffer.push_str("\x1b[A"), + _ => write!(self.buffer, "\x1b[{shift}A").map_err(err)? + } } + std::cmp::Ordering::Equal => { /* Do nothing */ } } - KeyEvent(KeyCode::Null, ModKeys::empty()) + match new.col.cmp(&old.col) { + std::cmp::Ordering::Greater => { + let shift = new.col - old.col; + match shift { + 1 => self.buffer.push_str("\x1b[C"), + _ => write!(self.buffer, "\x1b[{shift}C").map_err(err)? + } + } + std::cmp::Ordering::Less => { + let shift = old.col - new.col; + match shift { + 1 => self.buffer.push_str("\x1b[D"), + _ => write!(self.buffer, "\x1b[{shift}D").map_err(err)? + } + } + std::cmp::Ordering::Equal => { /* Do nothing */ } + } + write_all(self.out, self.buffer.as_str())?; + Ok(()) } - pub fn parse_esc_seq(&self, buf: &mut [u8]) -> Option { - let mut collected = vec![0x1b]; + pub fn redraw( + &mut self, + prompt: &str, + line: &LineBuf, + old_layout: &Layout, + new_layout: &Layout, + ) -> ShResult<()> { + let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer"); + self.buffer.clear(); - // Peek next byte - let _ = self.peek_byte(&mut buf[..1]); - let b1 = buf[0]; - collected.push(b1); + self.clear_rows(old_layout); - match b1 { - b'[' => { - // Next byte(s) determine the sequence - let _ = self.peek_byte(&mut buf[..1]); - let b2 = buf[0]; - collected.push(b2); + let end = new_layout.end; + let cursor = new_layout.cursor; - 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]; + self.buffer.push_str(prompt); + self.buffer.push_str(line.as_str()); - // Keep reading until we hit `~` or `;` (modifiers) - loop { - let _ = self.peek_byte(&mut buf[..1]); - let b = buf[0]; - collected.push(b); + if end.col == 0 + && end.row > 0 + { + // The line has wrapped. We need to use our own line break. + self.buffer.push('\n') + } - 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 cursor_row_offset = end.row - cursor.row; - 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 + match cursor_row_offset { + 0 => { /* Do nothing */ } + 1 => self.buffer.push_str("\x1b[A"), + _ => write!(self.buffer, "\x1b[{cursor_row_offset}A").map_err(err)? + } - // 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, - }; + let cursor_col = cursor.col; + match cursor_col { + 0 => self.buffer.push('\r'), + 1 => self.buffer.push_str("\x1b[C"), + _ => write!(self.buffer, "\x1b[{cursor_col}C").map_err(err)? + } - 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())), - } + write_all(self.out, self.buffer.as_str())?; + Ok(()) } - 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(&self, 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" { + self.tab_stop - (pos.col % self.tab_stop) + } else { + width(c, &mut esc_seq) + }; + pos.col += c_width; + if pos.col > self.t_cols { + pos.row += 1; + pos.col = c_width; } } - (row,col) - } -} + if pos.col > self.t_cols { + pos.row += 1; + pos.col = 0; + } -impl Default for Terminal { - fn default() -> Self { - Self::new() + pos + } + + 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) -> ShResult<()> { + if rdr.poll(PollTimeout::ZERO)? { + // 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")?; + + // Excessively paranoid invariant checking + if !rdr.poll(PollTimeout::from(100u8))? + || rdr.next_byte()? as char != '\x1b' + || rdr.next_byte()? as char != '[' + || read_digits_until(rdr, ';')?.is_none() { + // Invariant is broken, get out + return Ok(()) + } + // We just consumed everything up to the column number, so let's get that now + let col = read_digits_until(rdr, 'R')?; + + // The cursor is not at the leftmost, so let's fix that + if col != Some(1) { + // We use '\n' instead of '\r' because if there's a bunch of garbage on this line, + // It might pollute the prompt/line buffer if those are shorter than said garbage + self.flush_write("\n")?; + } + + Ok(()) } } diff --git a/src/prompt/readline/vicmd.rs b/src/prompt/readline/vicmd.rs deleted file mode 100644 index d5de91a..0000000 --- a/src/prompt/readline/vicmd.rs +++ /dev/null @@ -1,361 +0,0 @@ -use super::register::{append_register, read_register, write_register}; - -#[derive(Clone,Copy,Debug)] -pub struct RegisterName { - name: Option, - count: usize, - append: bool -} - -impl RegisterName { - pub fn new(name: Option, count: Option) -> Self { - let Some(ch) = name else { - return Self::default() - }; - - let append = ch.is_uppercase(); - let name = ch.to_ascii_lowercase(); - Self { - name: Some(name), - count: count.unwrap_or(1), - append - } - } - pub fn name(&self) -> Option { - self.name - } - pub fn is_append(&self) -> bool { - self.append - } - pub fn count(&self) -> usize { - self.count - } - pub fn write_to_register(&self, buf: String) { - if self.append { - append_register(self.name, buf); - } else { - write_register(self.name, buf); - } - } - pub fn read_from_register(&self) -> Option { - read_register(self.name) - } -} - -impl Default for RegisterName { - fn default() -> Self { - Self { - name: None, - count: 1, - append: false - } - } -} - -#[derive(Clone,Default,Debug)] -pub struct ViCmd { - pub register: RegisterName, - pub verb: Option, - pub motion: Option, - pub raw_seq: String, -} - -impl ViCmd { - pub fn new() -> Self { - Self::default() - } - pub fn set_motion(&mut self, motion: MotionCmd) { - self.motion = Some(motion) - } - pub fn set_verb(&mut self, verb: VerbCmd) { - self.verb = Some(verb) - } - pub fn verb(&self) -> Option<&VerbCmd> { - self.verb.as_ref() - } - pub fn motion(&self) -> Option<&MotionCmd> { - self.motion.as_ref() - } - pub fn verb_count(&self) -> usize { - self.verb.as_ref().map(|v| v.0).unwrap_or(1) - } - pub fn motion_count(&self) -> usize { - self.motion.as_ref().map(|m| m.0).unwrap_or(1) - } - pub fn is_repeatable(&self) -> bool { - self.verb.as_ref().is_some_and(|v| v.1.is_repeatable()) - } - pub fn is_cmd_repeat(&self) -> bool { - self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatLast)) - } - pub fn is_motion_repeat(&self) -> bool { - self.motion.as_ref().is_some_and(|m| matches!(m.1,Motion::RepeatMotion | Motion::RepeatMotionRev)) - } - pub fn is_char_search(&self) -> bool { - self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::CharSearch(..))) - } - pub fn should_submit(&self) -> bool { - self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLine)) - } - pub fn is_undo_op(&self) -> bool { - self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo)) - } - pub fn is_line_motion(&self) -> bool { - self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::LineUp | Motion::LineDown)) - } - pub fn is_mode_transition(&self) -> bool { - self.verb.as_ref().is_some_and(|v| { - matches!(v.1, - Verb::Change | - Verb::InsertMode | - Verb::InsertModeLineBreak(_) | - Verb::NormalMode | - Verb::VisualModeSelectLast | - Verb::VisualMode | - Verb::ReplaceMode - ) - }) - } -} - -#[derive(Clone,Debug)] -pub struct VerbCmd(pub usize,pub Verb); -#[derive(Clone,Debug)] -pub struct MotionCmd(pub usize,pub Motion); - -impl MotionCmd { - pub fn invert_char_motion(self) -> Self { - let MotionCmd(count,Motion::CharSearch(dir, dest, ch)) = self else { - unreachable!() - }; - let new_dir = match dir { - Direction::Forward => Direction::Backward, - Direction::Backward => Direction::Forward, - }; - MotionCmd(count,Motion::CharSearch(new_dir, dest, ch)) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -#[non_exhaustive] -pub enum Verb { - Delete, - DeleteChar(Anchor), - Change, - Yank, - ReplaceChar(char), - Substitute, - ToggleCase, - ToLower, - ToUpper, - Complete, - CompleteBackward, - Undo, - Redo, - RepeatLast, - Put(Anchor), - ReplaceMode, - InsertMode, - InsertModeLineBreak(Anchor), - NormalMode, - VisualMode, - VisualModeLine, - VisualModeBlock, // dont even know if im going to implement this - VisualModeSelectLast, - SwapVisualAnchor, - JoinLines, - InsertChar(char), - Insert(String), - Breakline(Anchor), - Indent, - Dedent, - Equalize, - AcceptLine, - Rot13, // lol - Builder(VerbBuilder), - EndOfFile -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum VerbBuilder { -} - -impl Verb { - pub fn needs_motion(&self) -> bool { - matches!(self, - Self::Indent | - Self::Dedent | - Self::Delete | - Self::Change | - Self::Yank - ) - } - pub fn is_repeatable(&self) -> bool { - matches!(self, - Self::Delete | - Self::DeleteChar(_) | - Self::Change | - Self::ReplaceChar(_) | - Self::Substitute | - Self::ToLower | - Self::ToUpper | - Self::ToggleCase | - Self::Put(_) | - Self::ReplaceMode | - Self::InsertModeLineBreak(_) | - Self::JoinLines | - Self::InsertChar(_) | - Self::Insert(_) | - Self::Breakline(_) | - Self::Indent | - Self::Dedent | - Self::Equalize - ) - } - pub fn is_edit(&self) -> bool { - matches!(self, - Self::Delete | - Self::DeleteChar(_) | - Self::Change | - Self::ReplaceChar(_) | - Self::Substitute | - Self::ToggleCase | - Self::ToLower | - Self::ToUpper | - Self::RepeatLast | - Self::Put(_) | - Self::ReplaceMode | - Self::InsertModeLineBreak(_) | - Self::JoinLines | - Self::InsertChar(_) | - Self::Insert(_) | - Self::Breakline(_) | - Self::Rot13 | - Self::EndOfFile - ) - } - pub fn is_char_insert(&self) -> bool { - matches!(self, - Self::Change | - Self::InsertChar(_) | - Self::ReplaceChar(_) - ) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum Motion { - WholeLine, - TextObj(TextObj, Bound), - EndOfLastWord, - BeginningOfFirstWord, - BeginningOfLine, - EndOfLine, - BackwardWord(To, Word), - ForwardWord(To, Word), - CharSearch(Direction,Dest,char), - BackwardChar, - ForwardChar, - LineUp, - ScreenLineUp, - LineDown, - ScreenLineDown, - BeginningOfScreenLine, - FirstGraphicalOnScreenLine, - HalfOfScreen, - HalfOfScreenLineText, - WholeBuffer, - BeginningOfBuffer, - EndOfBuffer, - ToColumn(usize), - Range(usize,usize), - Builder(MotionBuilder), - RepeatMotion, - RepeatMotionRev, - Null -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum MotionBuilder { - CharSearch(Option,Option,Option), - TextObj(Option,Option) -} - -impl Motion { - pub fn needs_verb(&self) -> bool { - matches!(self, Self::TextObj(_, _)) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum Anchor { - After, - Before -} -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum TextObj { - /// `iw`, `aw` — inner word, around word - Word(Word), - - /// for stuff like 'dd' - Line, - - /// `is`, `as` — inner sentence, around sentence - Sentence, - - /// `ip`, `ap` — inner paragraph, around paragraph - Paragraph, - - /// `i"`, `a"` — inner/around double quotes - DoubleQuote, - /// `i'`, `a'` - SingleQuote, - /// `i\``, `a\`` - BacktickQuote, - - /// `i)`, `a)` — round parens - Paren, - /// `i]`, `a]` - Bracket, - /// `i}`, `a}` - Brace, - /// `i<`, `a<` - Angle, - - /// `it`, `at` — HTML/XML tags - Tag, - - /// Custom user-defined objects maybe? - Custom(char), -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Word { - Big, - Normal -} -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum Bound { - Inside, - Around -} - -#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] -pub enum Direction { - #[default] - Forward, - Backward -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum Dest { - On, - Before, - After -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub enum To { - Start, - End -}