diff --git a/src/expand.rs b/src/expand.rs index a1d510d..6c4b406 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -1223,16 +1223,13 @@ pub fn unescape_str(raw: &str) -> String { result.push(markers::SNG_QUOTE); while let Some(q_ch) = chars.next() { match q_ch { - '\\' => { - match chars.peek() { - Some(&'\\') | - Some(&'\'') => { - let ch = chars.next().unwrap(); - result.push(ch); - } - _ => result.push(q_ch), - } - } + '\\' => match chars.peek() { + Some(&'\\') | Some(&'\'') => { + let ch = chars.next().unwrap(); + result.push(ch); + } + _ => result.push(q_ch), + }, '\'' => { result.push(markers::SNG_QUOTE); break; diff --git a/src/main.rs b/src/main.rs index 286e783..b24e6d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -161,10 +161,10 @@ fn main() -> ExitCode { write_jobs(|j| j.hang_up()); - let code = QUIT_CODE.load(Ordering::SeqCst) as u8; - if code == 0 && isatty(STDIN_FILENO).unwrap_or_default() { - write(borrow_fd(STDERR_FILENO), b"\nexit\n").ok(); - } + let code = QUIT_CODE.load(Ordering::SeqCst) as u8; + if code == 0 && isatty(STDIN_FILENO).unwrap_or_default() { + write(borrow_fd(STDERR_FILENO), b"\nexit\n").ok(); + } ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) } diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 7805f54..155a916 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -29,100 +29,144 @@ use crate::{ }, }; +/// Compat shim: replaces the old ClampedUsize type that was removed in the linebuf refactor. +/// A simple wrapper around usize with wrapping arithmetic and a max bound. +#[derive(Clone, Default, Debug)] +pub struct ClampedUsize { + val: usize, + max: usize, + wrap: bool, +} + +impl ClampedUsize { + pub fn new(val: usize, max: usize, wrap: bool) -> Self { + Self { val, max, wrap } + } + pub fn get(&self) -> usize { + self.val + } + pub fn set_max(&mut self, max: usize) { + self.max = max; + if self.val >= self.max && self.max > 0 { + self.val = self.max - 1; + } + } + pub fn wrap_add(&mut self, n: usize) { + if self.max == 0 { + return; + } + if self.wrap { + self.val = (self.val + n) % self.max; + } else { + self.val = (self.val + n).min(self.max.saturating_sub(1)); + } + } + pub fn wrap_sub(&mut self, n: usize) { + if self.max == 0 { + return; + } + if self.wrap { + self.val = (self.val + self.max - (n % self.max)) % self.max; + } else { + self.val = self.val.saturating_sub(n); + } + } +} + #[derive(Debug, Clone)] pub struct Candidate(pub String); impl Eq for Candidate {} impl PartialEq for Candidate { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } } impl PartialOrd for Candidate { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } } impl Ord for Candidate { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.0.cmp(&other.0) - } + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.cmp(&other.0) + } } impl From for Candidate { - fn from(value: String) -> Self { - Self(value) - } + fn from(value: String) -> Self { + Self(value) + } } impl From<&String> for Candidate { - fn from(value: &String) -> Self { - Self(value.clone()) - } + fn from(value: &String) -> Self { + Self(value.clone()) + } } impl From<&str> for Candidate { - fn from(value: &str) -> Self { - Self(value.to_string()) - } + fn from(value: &str) -> Self { + Self(value.to_string()) + } } impl Display for Candidate { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self.0) - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } } impl AsRef for Candidate { - fn as_ref(&self) -> &str { - &self.0 - } + fn as_ref(&self) -> &str { + &self.0 + } } impl std::ops::Deref for Candidate { - type Target = str; - fn deref(&self) -> &str { - &self.0 - } + type Target = str; + fn deref(&self) -> &str { + &self.0 + } } impl Candidate { - pub fn is_match(&self, other: &str) -> bool { - let ignore_case = read_shopts(|o| o.prompt.completion_ignore_case); - if ignore_case { - let other_lower = other.to_lowercase(); - let self_lower = self.0.to_lowercase(); - self_lower.starts_with(&other_lower) - } else { - self.0.starts_with(other) - } - } - pub fn as_str(&self) -> &str { - &self.0 - } - pub fn as_bytes(&self) -> &[u8] { - self.0.as_bytes() - } - pub fn starts_with(&self, pat: char) -> bool { - self.0.starts_with(pat) - } - pub fn strip_prefix(&self, prefix: &str) -> Option { - let ignore_case = read_shopts(|o| o.prompt.completion_ignore_case); - if ignore_case { - let old_len = self.0.len(); - let prefix_lower = prefix.to_lowercase(); - let self_lower = self.0.to_lowercase(); - let stripped = self_lower.strip_prefix(&prefix_lower)?; - let new_len = stripped.len(); - let delta = old_len - new_len; - Some(self.0[delta..].to_string()) - } else { - self.0.strip_prefix(prefix).map(|s| s.to_string()) - } - } + pub fn is_match(&self, other: &str) -> bool { + let ignore_case = read_shopts(|o| o.prompt.completion_ignore_case); + if ignore_case { + let other_lower = other.to_lowercase(); + let self_lower = self.0.to_lowercase(); + self_lower.starts_with(&other_lower) + } else { + self.0.starts_with(other) + } + } + pub fn as_str(&self) -> &str { + &self.0 + } + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + pub fn starts_with(&self, pat: char) -> bool { + self.0.starts_with(pat) + } + pub fn strip_prefix(&self, prefix: &str) -> Option { + let ignore_case = read_shopts(|o| o.prompt.completion_ignore_case); + if ignore_case { + let old_len = self.0.len(); + let prefix_lower = prefix.to_lowercase(); + let self_lower = self.0.to_lowercase(); + let stripped = self_lower.strip_prefix(&prefix_lower)?; + let new_len = stripped.len(); + let delta = old_len - new_len; + Some(self.0[delta..].to_string()) + } else { + self.0.strip_prefix(prefix).map(|s| s.to_string()) + } + } } pub fn complete_signals(start: &str) -> Vec { @@ -133,7 +177,7 @@ pub fn complete_signals(start: &str) -> Vec { .unwrap_or(s.as_ref()) .to_string() }) - .map(Candidate::from) + .map(Candidate::from) .filter(|s| s.is_match(start)) .collect() } @@ -141,8 +185,8 @@ pub fn complete_signals(start: &str) -> Vec { pub fn complete_aliases(start: &str) -> Vec { read_logic(|l| { l.aliases() - .keys() - .map(Candidate::from) + .keys() + .map(Candidate::from) .filter(|a| a.is_match(start)) .collect() }) @@ -155,7 +199,7 @@ pub fn complete_jobs(start: &str) -> Vec { .iter() .filter_map(|j| j.as_ref()) .filter_map(|j| j.name()) - .map(Candidate::from) + .map(Candidate::from) .filter(|name| name.is_match(prefix)) .map(|name| format!("%{name}").into()) .collect() @@ -179,7 +223,7 @@ pub fn complete_users(start: &str) -> Vec { passwd .lines() .filter_map(|line| line.split(':').next()) - .map(Candidate::from) + .map(Candidate::from) .filter(|username| username.is_match(start)) .collect() } @@ -199,7 +243,7 @@ pub fn complete_vars(start: &str) -> Vec { .keys() .filter(|k| k.starts_with(&var_name) && *k != &var_name) .map(|k| format!("{prefix}{k}")) - .map(Candidate::from) + .map(Candidate::from) .collect::>() }) } @@ -271,7 +315,7 @@ fn complete_commands(start: &str) -> Vec { let mut candidates: Vec = read_meta(|m| { m.cached_cmds() .iter() - .map(Candidate::from) + .map(Candidate::from) .filter(|c| c.is_match(start)) .collect() }); @@ -325,8 +369,6 @@ fn complete_filename(start: &str) -> Vec { let file_name = entry.file_name(); let file_str: Candidate = file_name.to_string_lossy().to_string().into(); - - // Skip hidden files unless explicitly requested if !prefix.starts_with('.') && file_str.0.starts_with('.') { continue; @@ -528,11 +570,11 @@ impl BashCompSpec { ); exec_input(input, None, false, Some("comp_function".into()))?; - let comp_reply = read_vars(|v| v.get_arr_elems("COMPREPLY")) - .unwrap_or_default() - .into_iter() - .map(Candidate::from) - .collect(); + let comp_reply = read_vars(|v| v.get_arr_elems("COMPREPLY")) + .unwrap_or_default() + .into_iter() + .map(Candidate::from) + .collect(); Ok(comp_reply) } @@ -569,7 +611,12 @@ impl CompSpec for BashCompSpec { candidates.extend(complete_signals(&expanded)); } if let Some(words) = &self.wordlist { - candidates.extend(words.iter().map(Candidate::from).filter(|w| w.is_match(&expanded))); + candidates.extend( + words + .iter() + .map(Candidate::from) + .filter(|w| w.is_match(&expanded)), + ); } if self.function.is_some() { candidates.extend(self.exec_comp_func(ctx)?); @@ -645,7 +692,7 @@ impl CompResult { Self::NoMatch } else if candidates.len() == 1 { Self::Single { - result: candidates.remove(0) + result: candidates.remove(0), } } else { Self::Many { candidates } @@ -828,22 +875,22 @@ impl QueryEditor { .cursor .ret_sub(self.available_width.saturating_sub(1)); } - let max_offset = self.linebuf - .count_graphemes() + let max_offset = self + .linebuf + .count_graphemes() .saturating_sub(self.available_width); self.scroll_offset = self.scroll_offset.min(max_offset); } pub fn get_window(&mut self) -> String { - self.linebuf.update_graphemes(); - let buf_len = self.linebuf.grapheme_indices().len(); + let buf_len = self.linebuf.count_graphemes(); if buf_len <= self.available_width { - return self.linebuf.as_str().to_string(); + return self.linebuf.joined(); } let start = self .scroll_offset .min(buf_len.saturating_sub(self.available_width)); let end = (start + self.available_width).min(buf_len); - self.linebuf.slice(start..end).unwrap_or("").to_string() + self.linebuf.slice(start..end).unwrap_or_default() } pub fn handle_key(&mut self, key: K) -> ShResult<()> { let Some(cmd) = self.mode.handle_key(key) else { @@ -1028,7 +1075,7 @@ impl FuzzySelector { .into_iter() .filter_map(|c| { let mut sc = ScoredCandidate::new(c.to_string()); - let score = sc.fuzzy_score(self.query.linebuf.as_str()); + let score = sc.fuzzy_score(&self.query.linebuf.joined()); if score > i32::MIN { Some(sc) } else { None } }) .collect(); @@ -1319,12 +1366,18 @@ impl Completer for FuzzyCompleter { basename, ) } else { - (self.completer.original_input[..start].to_string(), selected.clone()) + ( + self.completer.original_input[..start].to_string(), + selected.clone(), + ) } } else { start += slice.width(); let completion = selected.strip_prefix(slice).unwrap_or(&selected); - (self.completer.original_input[..start].to_string(), completion.to_string()) + ( + self.completer.original_input[..start].to_string(), + completion.to_string(), + ) }; let escaped = escape_str(&completion, false); let ret = format!( @@ -1435,7 +1488,10 @@ impl Completer for SimpleCompleter { } fn selected_candidate(&self) -> Option { - self.candidates.get(self.selected_idx).map(|c| c.to_string()) + self + .candidates + .get(self.selected_idx) + .map(|c| c.to_string()) } fn token_span(&self) -> (usize, usize) { @@ -1591,16 +1647,20 @@ impl SimpleCompleter { let prefix_end = start + last_sep + 1; let trailing_slash = selected.ends_with('/'); let trimmed = selected.trim_end_matches('/'); - let mut basename = trimmed.rsplit('/').next().unwrap_or(selected.as_str()).to_string(); + let mut basename = trimmed + .rsplit('/') + .next() + .unwrap_or(selected.as_str()) + .to_string(); if trailing_slash { basename.push('/'); } - ( - self.original_input[..prefix_end].to_string(), - basename, - ) + (self.original_input[..prefix_end].to_string(), basename) } else { - (self.original_input[..start].to_string(), selected.to_string()) + ( + self.original_input[..start].to_string(), + selected.to_string(), + ) } } else { start += slice.width(); @@ -1608,12 +1668,7 @@ impl SimpleCompleter { (self.original_input[..start].to_string(), completion) }; let escaped = escape_str(&completion, false); - format!( - "{}{}{}", - prefix, - escaped, - &self.original_input[end..] - ) + format!("{}{}{}", prefix, escaped, &self.original_input[end..]) } pub fn build_comp_ctx(&self, tks: &[Tk], line: &str, cursor_pos: usize) -> ShResult { @@ -2180,7 +2235,7 @@ mod tests { vi.feed_bytes(b"echo hello\t"); let _ = vi.process_input(); - let line = vi.editor.as_str().to_string(); + let line = vi.editor.joined(); assert!( line.contains("hello\\ world.txt"), "expected escaped space in completion: {line:?}" @@ -2202,7 +2257,7 @@ mod tests { vi.feed_bytes(b"echo my\\ \t"); let _ = vi.process_input(); - let line = vi.editor.as_str().to_string(); + let line = vi.editor.joined(); // The user's "my\ " should be preserved, not double-escaped to "my\\\ " assert!( !line.contains("my\\\\ "), @@ -2231,7 +2286,7 @@ mod tests { vi.feed_bytes(b"echo unique_shed_test\t"); let _ = vi.process_input(); - let line = vi.editor.as_str().to_string(); + let line = vi.editor.joined(); assert!( line.contains("unique_shed_test_file.txt"), "expected completion in line: {line:?}" @@ -2251,7 +2306,7 @@ mod tests { vi.feed_bytes(b"cd mysub\t"); let _ = vi.process_input(); - let line = vi.editor.as_str().to_string(); + let line = vi.editor.joined(); assert!( line.contains("mysubdir/"), "expected dir completion with trailing slash: {line:?}" @@ -2272,7 +2327,7 @@ mod tests { vi.feed_bytes(b"cmd --opt=eqf\t"); let _ = vi.process_input(); - let line = vi.editor.as_str().to_string(); + let line = vi.editor.joined(); assert!( line.contains("--opt=eqfile.txt"), "expected completion after '=': {line:?}" diff --git a/src/readline/history.rs b/src/readline/history.rs index 421b4f8..0552690 100644 --- a/src/readline/history.rs +++ b/src/readline/history.rs @@ -414,7 +414,12 @@ impl History { } pub fn get_hint(&self) -> Option { - if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.buffer.is_empty()) { + if self.at_pending() + && self + .pending + .as_ref() + .is_some_and(|p| !p.joined().is_empty()) + { let entry = self.hint_entry()?; Some(entry.command().to_string()) } else { diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index ee7c16f..a0f90b2 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -1,9 +1,11 @@ use std::{ collections::HashSet, fmt::Display, - ops::{Index, Range, RangeBounds, RangeFull, RangeInclusive}, slice::SliceIndex, + ops::{Index, IndexMut, Range, RangeBounds, RangeFull, RangeInclusive}, + slice::SliceIndex, }; +use itertools::Itertools; use smallvec::SmallVec; use unicode_segmentation::UnicodeSegmentation; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; @@ -38,143 +40,170 @@ const PUNCTUATION: [&str; 3] = ["?", "!", "."]; pub struct Grapheme(SmallVec<[char; 4]>); impl Grapheme { - pub fn chars(&self) -> &[char] { - &self.0 - } - /// Returns the display width of the Grapheme, treating unprintable chars as width 0 - pub fn width(&self) -> usize { - self.0.iter().map(|c| c.width().unwrap_or(0)).sum() - } - /// Returns true if the Grapheme is wrapping a linefeed ('\n') - pub fn is_lf(&self) -> bool { - self.is_char('\n') - } - /// Returns true if the Grapheme consists of exactly one char and that char is `c` - pub fn is_char(&self, c: char) -> bool { - self.0.len() == 1 && self.0[0] == c - } - /// Returns the CharClass of the Grapheme, which is determined by the properties of its chars - pub fn class(&self) -> CharClass { - CharClass::from(self) + pub fn chars(&self) -> &[char] { + &self.0 + } + /// Returns the display width of the Grapheme, treating unprintable chars as width 0 + pub fn width(&self) -> usize { + self.0.iter().map(|c| c.width().unwrap_or(0)).sum() + } + /// Returns true if the Grapheme is wrapping a linefeed ('\n') + pub fn is_lf(&self) -> bool { + self.is_char('\n') + } + /// Returns true if the Grapheme consists of exactly one char and that char is `c` + pub fn is_char(&self, c: char) -> bool { + self.0.len() == 1 && self.0[0] == c + } + /// Returns the CharClass of the Grapheme, which is determined by the properties of its chars + pub fn class(&self) -> CharClass { + CharClass::from(self) + } + + pub fn as_char(&self) -> Option { + if self.0.len() == 1 { + Some(self.0[0]) + } else { + None + } } + + /// Returns true if the Grapheme is classified as whitespace (i.e. all chars are whitespace) + pub fn is_ws(&self) -> bool { + self.class() == CharClass::Whitespace + } } impl From for Grapheme { - fn from(value: char) -> Self { - let mut new = SmallVec::<[char; 4]>::new(); - new.push(value); - Self(new) - } + fn from(value: char) -> Self { + let mut new = SmallVec::<[char; 4]>::new(); + new.push(value); + Self(new) + } } impl From<&str> for Grapheme { - fn from(value: &str) -> Self { - assert_eq!(value.graphemes(true).count(), 1); - let mut new = SmallVec::<[char; 4]>::new(); - for char in value.chars() { - new.push(char); - } - Self(new) - } + fn from(value: &str) -> Self { + assert_eq!(value.graphemes(true).count(), 1); + let mut new = SmallVec::<[char; 4]>::new(); + for char in value.chars() { + new.push(char); + } + Self(new) + } } impl From for Grapheme { - fn from(value: String) -> Self { - Into::::into(value.as_str()) - } + fn from(value: String) -> Self { + Into::::into(value.as_str()) + } } impl From<&String> for Grapheme { - fn from(value: &String) -> Self { - Into::::into(value.as_str()) - } + fn from(value: &String) -> Self { + Into::::into(value.as_str()) + } } impl Display for Grapheme { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for ch in &self.0 { - write!(f, "{ch}")?; - } - Ok(()) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for ch in &self.0 { + write!(f, "{ch}")?; + } + Ok(()) + } } pub fn to_graphemes(s: impl ToString) -> Vec { - let s = s.to_string(); - s.graphemes(true) - .map(Grapheme::from) - .collect() + let s = s.to_string(); + s.graphemes(true).map(Grapheme::from).collect() } pub fn to_lines(s: impl ToString) -> Vec { - let s = s.to_string(); - s.split("\n") - .map(to_graphemes) - .map(Line::from) - .collect() + let s = s.to_string(); + s.split("\n").map(to_graphemes).map(Line::from).collect() +} + +pub fn trim_lines(lines: &mut Vec) { + while lines.last().is_some_and(|line| line.is_empty()) { + lines.pop(); + } } #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct Line(Vec); impl Line { - pub fn graphemes(&self) -> &[Grapheme] { - &self.0 - } - pub fn len(&self) -> usize { - self.0.len() - } - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - pub fn push_str(&mut self, s: &str) { - for g in s.graphemes(true) { - self.0.push(Grapheme::from(g)); - } - } - pub fn push_char(&mut self, c: char) { - self.0.push(Grapheme::from(c)); - } - pub fn split_off(&mut self, at: usize) -> Line { - if at > self.0.len() { - return Line::default(); - } - Line(self.0.split_off(at)) - } - pub fn append(&mut self, other: &mut Line) { - self.0.append(&mut other.0); - } - pub fn insert_char(&mut self, at: usize, c: char) { - self.0.insert(at, Grapheme::from(c)); - } - pub fn insert(&mut self, at: usize, g: Grapheme) { - self.0.insert(at, g); - } - pub fn width(&self) -> usize { - self.0.iter().map(|g| g.width()).sum() + pub fn graphemes(&self) -> &[Grapheme] { + &self.0 + } + pub fn len(&self) -> usize { + self.0.len() + } + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + pub fn push_str(&mut self, s: &str) { + for g in s.graphemes(true) { + self.0.push(Grapheme::from(g)); + } + } + pub fn push_char(&mut self, c: char) { + self.0.push(Grapheme::from(c)); + } + pub fn split_off(&mut self, at: usize) -> Line { + if at > self.0.len() { + return Line::default(); + } + Line(self.0.split_off(at)) + } + pub fn append(&mut self, other: &mut Line) { + self.0.append(&mut other.0); + } + pub fn insert_char(&mut self, at: usize, c: char) { + self.0.insert(at, Grapheme::from(c)); + } + pub fn insert(&mut self, at: usize, g: Grapheme) { + self.0.insert(at, g); + } + pub fn width(&self) -> usize { + self.0.iter().map(|g| g.width()).sum() + } + pub fn trim_start(&mut self) -> Line { + let mut clone = self.clone(); + while clone.0.first().is_some_and(|g| g.is_ws()) { + clone.0.remove(0); + } + clone + } +} + +impl IndexMut for Line { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.0[index] } } -impl> Index for Line { - type Output = T::Output; - fn index(&self, index: T) -> &Self::Output { - &self.0[index] - } +impl> Index for Line { + type Output = T::Output; + fn index(&self, index: T) -> &Self::Output { + &self.0[index] + } } impl From> for Line { - fn from(value: Vec) -> Self { - Self(value) - } + fn from(value: Vec) -> Self { + Self(value) + } } impl Display for Line { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for gr in &self.0 { - write!(f, "{gr}")?; - } - Ok(()) - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for gr in &self.0 { + write!(f, "{gr}")?; + } + Ok(()) + } } #[derive(PartialEq, Eq, Debug, Clone, Copy)] @@ -194,28 +223,54 @@ pub enum CharClass { Other, } -impl From<&Grapheme> for CharClass { - fn from(g: &Grapheme) -> Self { - let Some(&first) = g.0.first() else { - return Self::Other - }; - - if first.is_alphanumeric() - && g.0[1..].iter().all(|&c| c.is_ascii_punctuation() || c == '\u{0301}' || c == '\u{0308}') { - // Handles things like `ï`, `é`, etc., by manually allowing common diacritics - return CharClass::Alphanum; - } - - if g.0.iter().all(|&c| c.is_alphanumeric() || c == '_') { - CharClass::Alphanum - } else if g.0.iter().all(|c| c.is_whitespace()) { - CharClass::Whitespace - } else if g.0.iter().all(|c| !c.is_alphanumeric()) { - CharClass::Symbol +impl CharClass { + pub fn is_other_class(&self, other: &CharClass) -> bool { + !self.eq(other) + } + pub fn is_other_class_not_ws(&self, other: &CharClass) -> bool { + if self.is_ws() || other.is_ws() { + false } else { - CharClass::Other + self.is_other_class(other) } } + pub fn is_other_class_or_ws(&self, other: &CharClass) -> bool { + if self.is_ws() || other.is_ws() { + true + } else { + self.is_other_class(other) + } + } + pub fn is_ws(&self) -> bool { + *self == CharClass::Whitespace + } +} + +impl From<&Grapheme> for CharClass { + fn from(g: &Grapheme) -> Self { + let Some(&first) = g.0.first() else { + return Self::Other; + }; + + if first.is_alphanumeric() + && g.0[1..] + .iter() + .all(|&c| c.is_ascii_punctuation() || c == '\u{0301}' || c == '\u{0308}') + { + // Handles things like `ï`, `é`, etc., by manually allowing common diacritics + return CharClass::Alphanum; + } + + if g.0.iter().all(|&c| c.is_alphanumeric() || c == '_') { + CharClass::Alphanum + } else if g.0.iter().all(|c| c.is_whitespace()) { + CharClass::Whitespace + } else if g.0.iter().all(|c| !c.is_alphanumeric()) { + CharClass::Symbol + } else { + CharClass::Other + } + } } fn is_whitespace(a: &Grapheme) -> bool { @@ -247,7 +302,7 @@ fn is_other_class_or_is_ws(a: &Grapheme, b: &Grapheme) -> bool { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum SelectAnchor { Pos(Pos), - LineNo(usize) + LineNo(usize), } #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -259,21 +314,14 @@ pub enum SelectMode { impl SelectMode { pub fn invert_anchor(&mut self, new_anchor: SelectAnchor) { - match self { - SelectMode::Block(select_anchor) | - SelectMode::Char(select_anchor) => { - let SelectAnchor::Pos(_) = new_anchor else { - panic!("Cannot switch to a Pos anchor when the new anchor is a LineNo, or vice versa"); - }; - *select_anchor = new_anchor; - } - SelectMode::Line(select_anchor) => { - let SelectAnchor::LineNo(_) = new_anchor else { - panic!("Cannot switch to a LineNo anchor when the new anchor is a Pos, or vice versa"); - }; - *select_anchor = new_anchor; - } - } + match self { + SelectMode::Block(select_anchor) | SelectMode::Char(select_anchor) => { + *select_anchor = new_anchor; + } + SelectMode::Line(select_anchor) => { + *select_anchor = new_anchor; + } + } } } @@ -286,120 +334,97 @@ enum CaseTransform { #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Pos { - pub row: usize, - pub col: usize + pub row: usize, + pub col: usize, } impl Pos { - pub fn clamp_row(&mut self, other: &[T]) { - self.row = self.row.clamp(0, other.len().saturating_sub(1)); - } - pub fn clamp_col(&mut self, other: &[T], inclusive: bool) { - let mut max = other.len(); - if inclusive && max > 0 { - max = max.saturating_sub(1); - } - self.col = self.col.clamp(0, max); - } + /// make sure you clamp this + pub const MAX: Self = Pos { row: usize::MAX, col: usize::MAX }; + pub const MIN: Self = Pos { row: 0, col: 0 }; + + pub fn clamp_row(&mut self, other: &[T]) { + self.row = self.row.clamp(0, other.len().saturating_sub(1)); + } + pub fn clamp_col(&mut self, other: &[T], exclusive: bool) { + let mut max = other.len(); + if exclusive && max > 0 { + max = max.saturating_sub(1); + } + self.col = self.col.clamp(0, max); + } } +#[derive(Debug, Clone)] pub enum MotionKind { - Char { target: Pos }, - Line(usize), - LineRange(Range), - LineOffset(isize), - Block { start: Pos, end: Pos }, + Char { target: Pos, inclusive: bool }, + Line(usize), + LineRange(Range), + LineOffset(isize), + Block { start: Pos, end: Pos }, } impl MotionKind { - /// Normalizes any given max-bounded range (1..2, 2..=5, ..10 etc) into a Range - /// - /// Examples: - /// ```rust - /// let range = MotionKind::line(1..=5); - /// assert_eq!(range, 1..6); - /// ``` - /// - /// ```rust - /// let range = MotionKind::line(..10); - /// assert_eq!(range, 0..10); - /// ``` - /// - /// Panics if the given range is max-unbounded (e.g. '5..'). - pub fn line>(range: R) -> Range { - 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::Excluded(&end) => end, - std::ops::Bound::Included(&end) => end + 1, - std::ops::Bound::Unbounded => panic!("Unbounded end is not allowed for MotionKind::Line"), - }; - start..end - } + /// Normalizes any given max-bounded range (1..2, 2..=5, ..10 etc) into a Range + /// + /// Examples: + /// ```rust + /// let range = MotionKind::line(1..=5); + /// assert_eq!(range, 1..6); + /// ``` + /// + /// ```rust + /// let range = MotionKind::line(..10); + /// assert_eq!(range, 0..10); + /// ``` + /// + /// Panics if the given range is max-unbounded (e.g. '5..'). + pub fn line>(range: R) -> Range { + 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::Excluded(&end) => end, + std::ops::Bound::Included(&end) => end + 1, + std::ops::Bound::Unbounded => panic!("Unbounded end is not allowed for MotionKind::Line"), + }; + start..end + } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Cursor { - pub pos: Pos, - pub inclusive: bool + pub pos: Pos, + pub exclusive: bool, +} + +impl Cursor { + /// Compat shim: returns the flat column position (col on row 0 in single-line mode) + pub fn get(&self) -> usize { + self.pos.col + } + /// Compat shim: sets the flat column position + pub fn set(&mut self, col: usize) { + self.pos.col = col; + } + /// Compat shim: returns cursor.col - n without mutating, clamped to 0 + pub fn ret_sub(&self, n: usize) -> usize { + self.pos.col.saturating_sub(n) + } } #[derive(Default, Clone, Debug)] pub struct Edit { - pub pos: usize, - pub cursor_pos: usize, - pub old: String, - pub new: String, + pub old_cursor: Pos, + pub new_cursor: Pos, + pub old: Vec, + pub new: Vec, 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 } @@ -407,7 +432,7 @@ impl Edit { self.merging = false } pub fn is_empty(&self) -> bool { - self.new.is_empty() && self.old.is_empty() + self.old == self.new } } @@ -482,358 +507,972 @@ impl IndentCtx { } } -#[derive(Debug, Default, Clone)] +fn extract_range_contiguous(buf: &mut Vec, start: Pos, end: Pos) -> Vec { + let start_col = start.col.min(buf[start.row].len()); + let end_col = end.col.min(buf[end.row].len()); + + if start.row == end.row { + // single line case + let line = &mut buf[start.row]; + let removed: Vec = line.0 + .drain(start_col..end_col) + .collect(); + return vec![Line(removed)]; + } + + // multi line case + // tail of first line + let first_tail: Line = buf[start.row].split_off(start_col); + + // all inbetween lines. extracts nothing if only two rows + let middle: Vec = buf.drain(start.row + 1..end.row).collect(); + + // head of last line + let last_col = end_col.min(buf[start.row + 1].len()); + let last_head: Line = Line::from(buf[start.row + 1].0.drain(..last_col).collect::>()); + + // tail of last line + let mut last_remainder = buf.remove(start.row + 1); + + // attach tail of last line to head of first line + buf[start.row].append(&mut last_remainder); + + // construct vector of extracted content + let mut extracts = vec![first_tail]; + extracts.extend(middle); + extracts.push(last_head); + extracts +} + +#[derive(Debug, Clone)] pub struct LineBuf { - pub lines: Vec, - pub hint: Vec, - pub cursor: Cursor, + pub lines: Vec, + pub hint: Vec, + pub cursor: Cursor, - pub select_mode: Option, - pub last_selection: Option<(SelectMode,SelectAnchor)>, + pub select_mode: Option, + pub last_selection: Option<(SelectMode, SelectAnchor)>, - pub insert_mode_start_pos: Option, - pub saved_col: Option, - pub indent_ctx: IndentCtx, + pub insert_mode_start_pos: Option, + pub saved_col: Option, + pub indent_ctx: IndentCtx, - pub undo_stack: Vec, - pub redo_stack: Vec, + pub undo_stack: Vec, + pub redo_stack: Vec, +} + +impl Default for LineBuf { + fn default() -> Self { + Self { + lines: vec![Line::from(vec![])], + hint: vec![], + cursor: Cursor { + pos: Pos { row: 0, col: 0 }, + exclusive: false, + }, + select_mode: None, + last_selection: None, + insert_mode_start_pos: None, + saved_col: None, + indent_ctx: IndentCtx::new(), + undo_stack: vec![], + redo_stack: vec![], + } + } } impl LineBuf { - pub fn new() -> Self { - Self::default() + pub fn new() -> Self { + Self::default() + } + pub fn count_graphemes(&self) -> usize { + self.lines.iter().map(|line| line.len()).sum() + } + fn cur_line(&self) -> &Line { + &self.lines[self.cursor.pos.row] + } + fn cur_line_mut(&mut self) -> &mut Line { + &mut self.lines[self.cursor.pos.row] + } + fn line_mut(&mut self, row: usize) -> &mut Line { + &mut self.lines[row] } - pub fn count_graphemes(&self) -> usize { - self.lines.iter().map(|line| line.len()).sum() - } - fn cur_line(&self) -> &Line { - &self.lines[self.cursor.pos.row] - } - fn cur_line_mut(&mut self) -> &mut Line { - &mut self.lines[self.cursor.pos.row] - } - fn line_to_cursor(&self) -> &[Grapheme] { - let line = self.cur_line(); - let col = self.cursor.pos.col.min(line.len()); - &line[..col] - } - fn line_from_cursor(&self) -> &[Grapheme] { - let line = self.cur_line(); - let col = self.cursor.pos.col.min(line.len()); - &line[col..] - } - fn row_col(&self) -> (usize,usize) { - (self.row(), self.col()) - } - fn row(&self) -> usize { - self.cursor.pos.row - } - fn offset_row(&self, offset: isize) -> usize { - let mut row = self.cursor.pos.row.saturating_add_signed(offset); - row = row.clamp(0, self.lines.len().saturating_sub(1)); - row - } - fn col(&self) -> usize { - self.cursor.pos.col - } - fn offset_col(&self, row: usize, offset: isize) -> usize { - let mut col = self.cursor.pos.col.saturating_add_signed(offset); - col = col.clamp(0, self.lines[row].len()); - col - } - fn offset_col_wrapping(&self, row: usize, offset: isize) -> (usize, usize) { - let mut row = row; - let mut col = self.cursor.pos.col as isize + offset; + fn line_to_cursor(&self) -> &[Grapheme] { + let line = self.cur_line(); + let col = self.cursor.pos.col.min(line.len()); + &line[..col] + } + fn line_from_cursor(&self) -> &[Grapheme] { + let line = self.cur_line(); + let col = self.cursor.pos.col.min(line.len()); + &line[col..] + } + fn row_col(&self) -> (usize, usize) { + (self.row(), self.col()) + } + fn row(&self) -> usize { + self.cursor.pos.row + } + fn offset_row(&self, offset: isize) -> usize { + let mut row = self.cursor.pos.row.saturating_add_signed(offset); + row = row.clamp(0, self.lines.len().saturating_sub(1)); + row + } + fn col(&self) -> usize { + self.cursor.pos.col + } + fn offset_col(&self, row: usize, offset: isize) -> usize { + let mut col = self.cursor.pos.col.saturating_add_signed(offset); + let max = if self.cursor.exclusive { + self.lines[row].len().saturating_sub(1) + } else { + self.lines[row].len() + }; + col = col.clamp(0, max); + col + } + fn offset_col_wrapping(&self, row: usize, offset: isize) -> (usize, usize) { + let mut row = row; + let mut col = self.cursor.pos.col as isize + offset; - while col < 0 { - if row == 0 { - col = 0; - break; - } - row -= 1; - col += self.lines[row].len() as isize + 1; - } - while col > self.lines[row].len() as isize { - if row >= self.lines.len() - 1 { - col = self.lines[row].len() as isize; - break; - } - col -= self.lines[row].len() as isize + 1; - row += 1; - } + while col < 0 { + if row == 0 { + col = 0; + break; + } + row -= 1; + col += self.lines[row].len() as isize + 1; + } + while col > self.lines[row].len() as isize { + if row >= self.lines.len() - 1 { + col = self.lines[row].len() as isize; + break; + } + col -= self.lines[row].len() as isize + 1; + row += 1; + } - (row, col as usize) - } - fn offset_cursor(&self, row_offset: isize, col_offset: isize) -> Pos { - let row = self.offset_row(row_offset); - let col = self.offset_col(row, col_offset); - Pos { row, col } - } - fn offset_cursor_wrapping(&self, row_offset: isize, col_offset: isize) -> Pos { - let row = self.offset_row(row_offset); - let (row, col) = self.offset_col_wrapping(row, col_offset); - Pos { row, col } - } - fn verb_shell_cmd(&self, cmd: &str) -> ShResult<()> { + (row, col as usize) + } + fn set_cursor(&mut self, mut pos: Pos) { + pos.clamp_row(&self.lines); + pos.clamp_col(&self.lines[pos.row].0, false); + self.cursor.pos = pos; + } + fn set_row(&mut self, row: usize) { + let target_col = self.saved_col.unwrap_or(self.cursor.pos.col); - Ok(()) - } - fn insert(&mut self, gr: Grapheme) { - if gr.is_lf() { - let (row,col) = self.row_col(); - let rest = self.lines[row].split_off(col); - self.lines.insert(row + 1, rest); - self.cursor.pos = Pos { row: row + 1, col: 0 }; - } else { - let (row,col) = self.row_col(); - self.lines[row].insert(col, gr); - self.cursor.pos = self.offset_cursor(0, 1); - } - } - fn insert_str(&mut self, s: &str) { - for gr in s.graphemes(true) { - let gr = Grapheme::from(gr); - self.insert(gr); - } - } - fn scan_forward bool>(&self, f: F) -> Option { - self.scan_forward_from(self.cursor.pos, f) - } - fn scan_forward_from bool>(&self, mut pos: Pos, f: F) -> Option { - pos.clamp_row(&self.lines); - pos.clamp_col(&self.lines[pos.row].0, false); - let Pos { mut row, mut col } = pos; + self.set_cursor(Pos { + row, + col: self.saved_col.unwrap(), + }); + } + fn set_col(&mut self, col: usize) { + self.set_cursor(Pos { + row: self.cursor.pos.row, + col, + }); + } + fn offset_cursor(&self, row_offset: isize, col_offset: isize) -> Pos { + let row = self.offset_row(row_offset); + let col = self.offset_col(row, col_offset); + Pos { row, col } + } + fn offset_cursor_wrapping(&self, row_offset: isize, col_offset: isize) -> Pos { + let row = self.offset_row(row_offset); + let (row, col) = self.offset_col_wrapping(row, col_offset); + Pos { row, col } + } + fn break_line(&mut self) { + let (row, col) = self.row_col(); + let rest = self.lines[row].split_off(col); + self.lines.insert(row + 1, rest); + self.cursor.pos = Pos { + row: row + 1, + col: 0, + }; + } + fn verb_shell_cmd(&self, cmd: &str) -> ShResult<()> { + Ok(()) + } + fn insert(&mut self, gr: Grapheme) { + if gr.is_lf() { + self.break_line(); + } else { + let (row, col) = self.row_col(); + self.lines[row].insert(col, gr); + self.cursor.pos = self.offset_cursor(0, 1); + } + } + fn insert_str(&mut self, s: &str) { + for gr in s.graphemes(true) { + let gr = Grapheme::from(gr); + self.insert(gr); + } + } + fn push_str(&mut self, s: &str) { + let lines = to_lines(s); + self.lines.extend(lines); + } + fn push(&mut self, gr: Grapheme) { + let last = self.lines.last_mut(); + if let Some(last) = last { + last.push_str(&gr.to_string()); + } else { + self.lines.push(Line::from(vec![gr])); + } + } + fn scan_forward bool>(&self, f: F) -> Option { + self.scan_forward_from(self.cursor.pos, f) + } + fn scan_forward_from bool>(&self, mut pos: Pos, f: F) -> Option { + pos.clamp_row(&self.lines); + pos.clamp_col(&self.lines[pos.row].0, false); + let Pos { mut row, mut col } = pos; - loop { - let line = &self.lines[row]; - if !line.is_empty() && f(&line[col]) { - return Some(Pos { row, col }); - } - if col < self.lines[row].len().saturating_sub(1) { - col += 1; - } else if row < self.lines.len().saturating_sub(1) { - row += 1; - col = 0; - } else{ - return None - } - } - } - fn scan_backward bool>(&self, f: F) -> Option { - self.scan_backward_from(self.cursor.pos, f) - } - fn scan_backward_from bool>(&self, mut pos: Pos, f: F) -> Option { - pos.clamp_row(&self.lines); - pos.clamp_col(&self.lines[pos.row].0, false); - let Pos { mut row, mut col } = pos; + loop { + let line = &self.lines[row]; + if !line.is_empty() && f(&line[col]) { + return Some(Pos { row, col }); + } + if col < self.lines[row].len().saturating_sub(1) { + col += 1; + } else if row < self.lines.len().saturating_sub(1) { + row += 1; + col = 0; + } else { + return None; + } + } + } + fn scan_backward bool>(&self, f: F) -> Option { + self.scan_backward_from(self.cursor.pos, f) + } + fn scan_backward_from bool>(&self, mut pos: Pos, f: F) -> Option { + pos.clamp_row(&self.lines); + pos.clamp_col(&self.lines[pos.row].0, false); + let Pos { mut row, mut col } = pos; - loop { - let line = &self.lines[row]; - if !line.is_empty() && f(&line[col]) { - return Some(Pos { row, col }); - } - if col > 0 { - col -= 1; - } else if row > 0 { - row -= 1; - col = self.lines[row].len().saturating_sub(1); - } else { - return None - } - } - } - fn search_char(&self, dir: &Direction, dest: &Dest, char: &Grapheme) -> isize { - match dir { - Direction::Forward => { - let slice = self.line_from_cursor(); - for (i, gr) in slice.iter().enumerate().skip(1) { - if gr == char { - match dest { - Dest::On => return i as isize, - Dest::Before => return (i as isize - 1).max(0), - Dest::After => unreachable!() - } - } + loop { + let line = &self.lines[row]; + if !line.is_empty() && f(&line[col]) { + return Some(Pos { row, col }); + } + if col > 0 { + col -= 1; + } else if row > 0 { + row -= 1; + col = self.lines[row].len().saturating_sub(1); + } else { + return None; + } + } + } + fn search_char(&self, dir: &Direction, dest: &Dest, char: &Grapheme) -> isize { + match dir { + Direction::Forward => { + let slice = self.line_from_cursor(); + for (i, gr) in slice.iter().enumerate().skip(1) { + if gr == char { + match dest { + Dest::On => return i as isize, + Dest::Before => return (i as isize - 1).max(0), + Dest::After => unreachable!(), + } + } + } + } + Direction::Backward => { + let slice = self.line_to_cursor(); + for (i, gr) in slice.iter().rev().enumerate().skip(1) { + if gr == char { + match dest { + Dest::On => return -(i as isize) - 1, + Dest::Before => return -(i as isize), + Dest::After => unreachable!(), + } + } + } + } + } + + 0 + } + fn eval_word_motion( + &self, + count: usize, + to: &To, + word: &Word, + dir: &Direction, + ignore_trailing_ws: bool, + mut inclusive: bool + ) -> Option { + let mut target = self.cursor.pos; + + for _ in 0..count { + match (to, dir) { + (To::Start, Direction::Forward) => { + target = self.word_motion_w(word, target, ignore_trailing_ws).unwrap_or_else(|| { + // we set inclusive to true so that we catch the entire word + // instead of ignoring the last character + inclusive = true; + Pos::MAX + }); } - } - Direction::Backward => { - let slice = self.line_to_cursor(); - for (i,gr) in slice.iter().rev().enumerate().skip(1) { - if gr == char { - match dest { - Dest::On => return -(i as isize), - Dest::After => return -(i as isize) + 1, - Dest::Before => unreachable!() - } - } + (To::End, Direction::Forward) => { + inclusive = true; + target = self.word_motion_e(word, target).unwrap_or(Pos::MAX); + } + (To::Start, Direction::Backward) => { + target = self.word_motion_b(word, target).unwrap_or(Pos::MIN); + } + (To::End, Direction::Backward) => { + inclusive = true; + target = self.word_motion_ge(word, target).unwrap_or(Pos::MIN); } } } - 0 - } - fn eval_word_motion( - &self, - count: usize, - to: &To, - word: &Word, - dir: &Direction, - include_last_char: bool - ) -> Option { - todo!() - } - fn eval_motion(&mut self, cmd: &ViCmd) -> Option { - let ViCmd { verb, motion, .. } = cmd; - let MotionCmd(count, motion) = motion.as_ref()?; + target.clamp_row(&self.lines); + target.clamp_col(&self.lines[target.row].0, true); - match motion { - Motion::WholeLine => { - Some(MotionKind::Line(self.row())) + Some(MotionKind::Char { target, inclusive }) + } + fn word_motion_w(&self, word: &Word, start: Pos, ignore_trailing_ws: bool) -> Option { + use CharClass as C; + + // get our iterator of char classes + // we dont actually care what the chars are + // just what they look like. + // we are going to use .find() a lot to advance the iterator + let mut classes = self.char_classes_forward_from(start).peekable(); + + match word { + Word::Big => { + if let Some((_,C::Whitespace)) = classes.peek() { + // we are on whitespace. advance to the next non-ws char class + return classes.find(|(_,c)| !c.is_ws()).map(|(p,_)| p); + } + + let last_non_ws = classes.find(|(_,c)| c.is_ws()); + if ignore_trailing_ws { + return last_non_ws.map(|(p,_)| p); + } + classes.find(|(_,c)| !c.is_ws()).map(|(p,_)| p) } - Motion::TextObj(text_obj) => todo!(), - Motion::EndOfLastWord => todo!(), - Motion::BeginningOfFirstWord => todo!(), - dir @ (Motion::BeginningOfLine | Motion::EndOfLine) => { - let off = match dir { - Motion::BeginningOfLine => isize::MIN, - Motion::EndOfLine => isize::MAX, - _ => unreachable!(), + Word::Normal => { + if let Some((_,C::Whitespace)) = classes.peek() { + // we are on whitespace. advance to the next non-ws char class + return classes.find(|(_,c)| !c.is_ws()).map(|(p,_)| p); + } + + // go forward until we find some char class that isnt this one + let first_c = classes.next()?.1; + + + match classes.find(|(_,c)| c.is_other_class_or_ws(&first_c))? { + (pos, C::Whitespace) if ignore_trailing_ws => return Some(pos), + (_, C::Whitespace) => { /* fall through */ } + (pos, _) => return Some(pos) + } + + // we found whitespace previously, look for the next non-whitespace char class + classes.find(|(_,c)| !c.is_ws()).map(|(p,_)| p) + } + } + } + fn word_motion_b(&self, word: &Word, start: Pos) -> Option { + use CharClass as C; + // get our iterator again + let mut classes = self.char_classes_backward_from(start).peekable(); + + match word { + Word::Big => { + classes.next(); + // for 'b', we handle starting on whitespace differently than 'w' + // we don't return immediately if find() returns Some() here. + let first_non_ws = if let Some((_,C::Whitespace)) = classes.peek() { + // we use find() to advance the iterator as usual + // but we can also be clever and use the question mark + // to return early if we don't find a word backwards + classes.find(|(_,c)| !c.is_ws())? + } else { + classes.next()? }; - let target = self.offset_cursor(0, off); - (target != self.cursor.pos).then_some(MotionKind::Char { target }) + + // ok now we are off that whitespace + // now advance backwards until we find more whitespace, or next() is None + + let mut last = first_non_ws; + while let Some((_,c)) = classes.peek() { + if c.is_ws() { break; } + last = classes.next()?; + } + Some(last.0) } - Motion::WordMotion(to, word, dir) => { + Word::Normal => { + classes.next(); + let first_non_ws = if let Some((_,C::Whitespace)) = classes.peek() { + classes.find(|(_,c)| !c.is_ws())? + } else { + classes.next()? + }; + + // ok, off the whitespace + // now advance until we find any different char class at all + let mut last = first_non_ws; + while let Some((_,c)) = classes.peek() { + if c.is_other_class(&last.1) { break; } + last = classes.next()?; + } + + Some(last.0) + } + } + } + fn word_motion_e(&self, word: &Word, start: Pos) -> Option { + use CharClass as C; + let mut classes = self.char_classes_forward_from(start).peekable(); + + match word { + Word::Big => { + classes.next(); // unconditionally skip first position for 'e' + let first_non_ws = if let Some((_,C::Whitespace)) = classes.peek() { + classes.find(|(_,c)| !c.is_ws())? + } else { + classes.next()? + }; + + let mut last = first_non_ws; + while let Some((_, c)) = classes.peek() { + if c.is_other_class_or_ws(&first_non_ws.1) { return Some(last.0); } + last = classes.next()?; + } + None + } + Word::Normal => { + classes.next(); + let first_non_ws = if let Some((_,C::Whitespace)) = classes.peek() { + classes.find(|(_,c)| !c.is_ws())? + } else { + classes.next()? + }; + + let mut last = first_non_ws; + while let Some((_, c)) = classes.peek() { + if c.is_other_class_or_ws(&first_non_ws.1) { return Some(last.0); } + last = classes.next()?; + } + None + } + } + } + fn word_motion_ge(&self, word: &Word, start: Pos) -> Option { + use CharClass as C; + let mut classes = self.char_classes_backward_from(start).peekable(); + + match word { + Word::Big => { + classes.next(); // unconditionally skip first position for 'ge' + if matches!(classes.peek(), Some((_, c)) if !c.is_ws()) { + classes.find(|(_,c)| c.is_ws()); + } + + classes.find(|(_,c)| !c.is_ws()).map(|(p,_)| p) + } + Word::Normal => { + classes.next(); + if let Some((_,C::Whitespace)) = classes.peek() { + return classes.find(|(_,c)| !c.is_ws()).map(|(p,_)| p); + } + + let cur_class = classes.peek()?.1; + let bound = classes.find(|(_,c)| c.is_other_class(&cur_class))?; + + if bound.1.is_ws() { + classes.find(|(_,c)| !c.is_ws()).map(|(p,_)| p) + } else { + Some(bound.0) + } + } + } + } + fn char_classes_forward_from(&self, pos: Pos) -> impl Iterator { + CharClassIter::new(&self.lines, pos) + } + fn char_classes_forward(&self) -> impl Iterator { + self.char_classes_forward_from(self.cursor.pos) + } + fn char_classes_backward_from(&self, pos: Pos) -> impl Iterator { + CharClassIterRev::new(&self.lines, pos) + } + fn char_classes_backward(&self) -> impl Iterator { + self.char_classes_backward_from(self.cursor.pos) + } + fn eval_motion(&mut self, cmd: &ViCmd) -> Option { + let ViCmd { verb, motion, .. } = cmd; + let MotionCmd(count, motion) = motion.as_ref()?; + + match motion { + Motion::WholeLine => Some(MotionKind::Line(self.row())), + Motion::TextObj(text_obj) => todo!(), + Motion::EndOfLastWord => todo!(), + Motion::BeginningOfFirstWord => todo!(), + dir @ (Motion::BeginningOfLine | Motion::EndOfLine) => { + let off = match dir { + Motion::BeginningOfLine => isize::MIN, + Motion::EndOfLine => isize::MAX, + _ => unreachable!(), + }; + let target = self.offset_cursor(0, off); + (target != self.cursor.pos).then_some(MotionKind::Char { target, inclusive: true }) + } + Motion::WordMotion(to, word, dir) => { // 'cw' is a weird case // if you are on the word's left boundary, it will not delete whitespace after // the end of the word - let include_last_char = matches!( - verb, - Some(VerbCmd(_, Verb::Change)), - ) - && matches!(motion, Motion::WordMotion( - To::Start, - _, - Direction::Forward, - )); + let ignore_trailing_ws = matches!(verb, Some(VerbCmd(_, Verb::Change)),) + && matches!( + motion, + Motion::WordMotion(To::Start, _, Direction::Forward,) + ); + let inclusive = verb.is_none(); - self.eval_word_motion(*count, to, word, dir, include_last_char) - } - Motion::CharSearch(dir, dest, char) => { - let off = self.search_char(dir, dest, char); - let target = self.offset_cursor(0, off); - (target != self.cursor.pos).then_some(MotionKind::Char { target }) - } - dir @ (Motion::BackwardChar | Motion::ForwardChar) | - dir @ (Motion::BackwardCharForced | Motion::ForwardCharForced) => { - let (off,wrap) = match dir { - Motion::BackwardChar => (-(*count as isize), false), - Motion::ForwardChar => (*count as isize, false), - Motion::BackwardCharForced => (-(*count as isize), true), - Motion::ForwardCharForced => (*count as isize, true), - _ => unreachable!(), - }; - let target = if wrap { - self.offset_cursor_wrapping(0, off) - } else { - self.offset_cursor(0, off) - }; + self.eval_word_motion(*count, to, word, dir, ignore_trailing_ws, inclusive) + } + Motion::CharSearch(dir, dest, char) => { + let off = self.search_char(dir, dest, char); + let target = self.offset_cursor(0, off); + let inclusive = matches!(dest, Dest::On); + (target != self.cursor.pos).then_some(MotionKind::Char { target, inclusive }) + } + dir @ (Motion::BackwardChar | Motion::ForwardChar) + | dir @ (Motion::BackwardCharForced | Motion::ForwardCharForced) => { + let (off, wrap) = match dir { + Motion::BackwardChar => (-(*count as isize), false), + Motion::ForwardChar => (*count as isize, false), + Motion::BackwardCharForced => (-(*count as isize), true), + Motion::ForwardCharForced => (*count as isize, true), + _ => unreachable!(), + }; + let target = if wrap { + self.offset_cursor_wrapping(0, off) + } else { + self.offset_cursor(0, off) + }; - (target != self.cursor.pos).then_some(MotionKind::Char { target }) - } - dir @ (Motion::LineDown | Motion::LineUp) => { - let off = match dir { - Motion::LineUp => -(*count as isize), - Motion::LineDown => *count as isize, - _ => unreachable!(), - }; - if verb.is_some() { - Some(MotionKind::LineOffset(off)) + (target != self.cursor.pos).then_some(MotionKind::Char { target, inclusive: false }) + } + dir @ (Motion::LineDown | Motion::LineUp) => { + let off = match dir { + Motion::LineUp => -(*count as isize), + Motion::LineDown => *count as isize, + _ => unreachable!(), + }; + if verb.is_some() { + Some(MotionKind::LineOffset(off)) + } else { + if self.saved_col.is_none() { + self.saved_col = Some(self.cursor.pos.col); + } + let row = self.offset_row(off); + let col = self.saved_col.unwrap().min(self.lines[row].len()); + let target = Pos { row, col }; + (target != self.cursor.pos).then_some(MotionKind::Char { target, inclusive: true }) + } + } + dir @ (Motion::EndOfBuffer | Motion::StartOfBuffer) => { + let off = match dir { + Motion::StartOfBuffer => isize::MIN, + Motion::EndOfBuffer => isize::MAX, + _ => unreachable!(), + }; + if verb.is_some() { + Some(MotionKind::LineOffset(off)) + } else { + let target = self.offset_cursor(off, 0); + (target != self.cursor.pos).then_some(MotionKind::Char { target, inclusive: true }) + } + } + Motion::WholeBuffer => Some(MotionKind::LineRange(0..self.lines.len())), + Motion::ToColumn => todo!(), + Motion::ToDelimMatch => todo!(), + Motion::ToBrace(direction) => todo!(), + Motion::ToBracket(direction) => todo!(), + Motion::ToParen(direction) => todo!(), + Motion::Range(_, _) => todo!(), + Motion::RepeatMotion => todo!(), + Motion::RepeatMotionRev => todo!(), + Motion::Global(val) => todo!(), + Motion::NotGlobal(val) => todo!(), + Motion::Null => None, + } + } + fn apply_motion(&mut self, motion: MotionKind) -> ShResult<()> { + match motion { + MotionKind::Char { target, inclusive: _ } => { + self.set_cursor(target); + } + MotionKind::Line(ln) => { + self.set_row(ln); + } + MotionKind::LineRange(range) => { + let pos = Pos { + row: range.start, + col: 0, + }; + self.set_cursor(pos); + } + MotionKind::LineOffset(off) => { + self.set_row(self.offset_row(off)); + } + MotionKind::Block { start, end } => todo!(), + } + Ok(()) + } + fn extract_range(&mut self, motion: &MotionKind) -> Vec { + let extracted = match motion { + MotionKind::Char { target, inclusive } => { + let (s, e) = ordered(self.cursor.pos, *target); + let end = if *inclusive { + Pos { row: e.row, col: e.col + 1 } } else { - let target = self.offset_cursor(off, 0); - (target != self.cursor.pos).then_some(MotionKind::Char { target }) + e + }; + let mut buf = std::mem::take(&mut self.lines); + let extracted = extract_range_contiguous(&mut buf, s, end); + self.lines = buf; + extracted + } + MotionKind::Line(lineno) => { + vec![self.lines.remove(*lineno)] + } + MotionKind::LineRange(range) => { + self.lines.drain(range.clone()).collect() + } + MotionKind::LineOffset(off) => { + let row = self.row(); + let end = row.saturating_add_signed(*off); + let (s, e) = ordered(row, end); + self.lines.drain(s..=e).collect() + } + MotionKind::Block { start, end } => { + let (s, e) = ordered(*start, *end); + (s.row..=e.row).map(|row| { + let sc = s.col.min(self.lines[row].len()); + let ec = (e.col + 1).min(self.lines[row].len()); + Line(self.lines[row].0.drain(sc..ec).collect()) + }).collect() + } + }; + if self.lines.is_empty() { + self.lines.push(Line::default()); + } + extracted + } + fn yank_range(&self, motion: &MotionKind) -> Vec { + let mut tmp = Self { + lines: self.lines.clone(), + cursor: self.cursor, + ..Default::default() + }; + tmp.extract_range(motion) + } + fn delete_range(&mut self, motion: &MotionKind) -> Vec { + self.extract_range(motion) + } + fn motion_mutation(&mut self, motion: MotionKind, f: impl Fn(&Grapheme) -> Grapheme) { + match motion { + MotionKind::Char { target, inclusive } => { + let (s,e) = ordered(self.cursor.pos,target); + if s.row == e.row { + let range = if inclusive { s.col..e.col + 1 } else { s.col..e.col }; + for col in range { + if col >= self.lines[s.row].len() { + break; + } + self.lines[s.row][col] = f(&self.lines[s.row][col]); + } + return + } + let end = if inclusive { e.col + 1 } else { e.col }; + + for col in s.col..self.lines[s.row].len() { + self.lines[s.row][col] = f(&self.lines[s.row][col]); + } + for row in s.row + 1..e.row { + for col in 0..self.lines[row].len() { + self.lines[row][col] = f(&self.lines[row][col]); + } + } + for col in 0..end { + if col >= self.lines[e.row].len() { + break; + } + self.lines[e.row][col] = f(&self.lines[e.row][col]); } } - dir @ (Motion::EndOfBuffer | Motion::StartOfBuffer) => { - let off = match dir { - Motion::StartOfBuffer => isize::MIN, - Motion::EndOfBuffer => isize::MAX, - _ => unreachable!(), - }; - if verb.is_some() { - Some(MotionKind::LineOffset(off)) - } else { - let target = self.offset_cursor(off, 0); - (target != self.cursor.pos).then_some(MotionKind::Char { target }) + MotionKind::Line(lineno) => { + if lineno >= self.lines.len() { + return; + } + let line = self.line_mut(lineno); + for col in 0..line.len() { + line[col] = f(&line[col]); } } - Motion::WholeBuffer => { - Some(MotionKind::LineRange(0..self.lines.len())) + MotionKind::LineRange(range) => { + for line in range { + if line >= self.lines.len() { + break; + } + let line = self.line_mut(line); + for col in 0..line.len() { + line[col] = f(&line[col]); + } + } } - Motion::ToColumn => todo!(), - Motion::ToDelimMatch => todo!(), - Motion::ToBrace(direction) => todo!(), - Motion::ToBracket(direction) => todo!(), - Motion::ToParen(direction) => todo!(), - Motion::Range(_, _) => todo!(), - Motion::RepeatMotion => todo!(), - Motion::RepeatMotionRev => todo!(), - Motion::Global(val) => todo!(), - Motion::NotGlobal(val) => todo!(), - Motion::Null => None + MotionKind::LineOffset(off) => { + let row = self.row(); + let end = row.saturating_add_signed(off); + let (s,mut e) = ordered(row, end); + e = e.min(self.lines.len().saturating_sub(1)); + + for line in s..=e { + let line = self.line_mut(line); + for col in 0..line.len() { + line[col] = f(&line[col]); + } + } + } + MotionKind::Block { start, end } => todo!(), } } - fn apply_motion(&mut self, motion: MotionKind) -> ShResult<()> { - todo!() - } - fn exec_verb(&mut self, cmd: &ViCmd) -> ShResult<()> { - let ViCmd { register, verb, motion, .. } = cmd; - let Some(VerbCmd(_, verb)) = verb else { - let Some(motion_kind) = self.eval_motion(cmd) else { - return Ok(()) + fn inplace_mutation(&mut self, count: u16, f: impl Fn(&Grapheme) -> Grapheme) { + let mut first = true; + for i in 0..count { + let motion = MotionKind::Char { + target: self.cursor.pos, + inclusive: false, }; - return self.apply_motion(motion_kind); - }; - let count = motion.as_ref().map(|m| m.0).unwrap_or(1); + self.motion_mutation(motion, &f); + if !first { + first = false + } else { + self.cursor.pos = self.offset_cursor(0, 1); + } + } + } + fn exec_verb(&mut self, cmd: &ViCmd) -> ShResult<()> { + let ViCmd { + register, + verb, + motion, + .. + } = cmd; + let Some(VerbCmd(count, verb)) = verb else { + let Some(motion_kind) = self.eval_motion(cmd) else { + return Ok(()); + }; + return self.apply_motion(motion_kind); + }; + let count = motion.as_ref().map(|m| m.0).unwrap_or(1); - match verb { - Verb::Delete => todo!(), - Verb::Change => todo!(), - Verb::Yank => todo!(), - Verb::Rot13 => todo!(), - Verb::ReplaceChar(_) => todo!(), - Verb::ReplaceCharInplace(_, _) => todo!(), - Verb::ToggleCaseInplace(_) => todo!(), - Verb::ToggleCaseRange => todo!(), - Verb::IncrementNumber(_) => todo!(), - Verb::DecrementNumber(_) => todo!(), - Verb::ToLower => todo!(), - Verb::ToUpper => todo!(), - Verb::Undo => todo!(), - Verb::Redo => todo!(), - Verb::RepeatLast => todo!(), - Verb::Put(anchor) => todo!(), - Verb::InsertModeLineBreak(anchor) => { - match anchor { - Anchor::After => { - let row = self.row(); - self.lines.insert(row + 1, Line::default()); - self.cursor.pos = Pos { row: row + 1, col: 0 }; + match verb { + Verb::Delete | + Verb::Change | + Verb::Yank => { + let Some(motion) = self.eval_motion(cmd) else { + return Ok(()) + }; + let content = if *verb == Verb::Yank { + self.yank_range(&motion) + } else { + self.delete_range(&motion) + }; + let reg_content = match &motion { + MotionKind::Char { .. } => RegisterContent::Span(content), + MotionKind::Line(_) | MotionKind::LineRange(_) | MotionKind::LineOffset(_) => RegisterContent::Line(content), + MotionKind::Block { .. } => RegisterContent::Block(content), + }; + register.write_to_register(reg_content); + + match motion { + MotionKind::Char { target, .. } => { + let (start, _) = ordered(self.cursor.pos, target); + self.set_cursor(start); } - Anchor::Before => { - let row = self.row(); - self.lines.insert(row, Line::default()); - self.cursor.pos = Pos { row, col: 0 }; + MotionKind::Line(line_no) => { + self.set_cursor_clamp(self.cursor.exclusive); + } + MotionKind::LineRange(_) | MotionKind::LineOffset(_) => { + self.set_cursor_clamp(self.cursor.exclusive); + } + MotionKind::Block { start, .. } => { + let (s, _) = ordered(self.cursor.pos, start); + self.set_cursor(s); } } } - Verb::SwapVisualAnchor => todo!(), - Verb::JoinLines => todo!(), - Verb::InsertChar(ch) => self.insert(Grapheme::from(*ch)), - Verb::Insert(s) => self.insert_str(s), - Verb::Indent => todo!(), - Verb::Dedent => todo!(), - Verb::Equalize => todo!(), - Verb::AcceptLineOrNewline => todo!(), + Verb::Rot13 => { + let Some(motion) = self.eval_motion(cmd) else { return Ok(()) }; + self.motion_mutation(motion, |gr| { + gr.as_char() + .map(rot13_char) + .map(Grapheme::from) + .unwrap_or_else(|| gr.clone()) + }); + } + Verb::ReplaceChar(ch) => { + let Some(motion) = self.eval_motion(cmd) else { return Ok(()) }; + self.motion_mutation(motion, |_| Grapheme::from(*ch)); + } + Verb::ReplaceCharInplace(ch, count) => self.inplace_mutation(*count, |_| Grapheme::from(*ch)), + Verb::ToggleCaseInplace(count) => { + self.inplace_mutation(*count, |gr| { + gr.as_char() + .map(toggle_case_char) + .map(Grapheme::from) + .unwrap_or_else(|| gr.clone()) + }); + } + Verb::ToggleCaseRange => { + let Some(motion) = self.eval_motion(cmd) else { return Ok(()) }; + self.motion_mutation(motion, |gr| { + gr.as_char() + .map(toggle_case_char) + .map(Grapheme::from) + .unwrap_or_else(|| gr.clone()) + }); + } + Verb::IncrementNumber(_) => todo!(), + Verb::DecrementNumber(_) => todo!(), + Verb::ToLower => { + let Some(motion) = self.eval_motion(cmd) else { return Ok(()) }; + self.motion_mutation(motion, |gr| { + gr.as_char() + .map(|c| c.to_ascii_uppercase()) + .map(Grapheme::from) + .unwrap_or_else(|| gr.clone()) + }) + } + Verb::ToUpper => { + let Some(motion) = self.eval_motion(cmd) else { return Ok(()) }; + self.motion_mutation(motion, |gr| { + gr.as_char() + .map(|c| c.to_ascii_uppercase()) + .map(Grapheme::from) + .unwrap_or_else(|| gr.clone()) + }) + } + Verb::Undo => { + if let Some(edit) = self.undo_stack.pop() { + self.lines = edit.old.clone(); + self.cursor.pos = edit.old_cursor; + self.redo_stack.push(edit); + } + } + Verb::Redo => if let Some(edit) = self.redo_stack.pop() { + self.lines = edit.new.clone(); + self.cursor.pos = edit.new_cursor; + self.undo_stack.push(edit); + } + Verb::RepeatLast => todo!(), + Verb::Put(anchor) => { + let Some(content) = register.read_from_register() else { + return Ok(()) + }; + match content { + RegisterContent::Span(lines) => { + let row = self.row(); + let col = match anchor { + Anchor::After => (self.col() + 1).min(self.cur_line().len()), + Anchor::Before => self.col(), + }; + let mut right = self.lines[row].split_off(col); + + let mut lines = lines.clone(); + let last = lines.len() - 1; + + // First line appends to current line + self.lines[row].append(&mut lines[0]); + + // Middle + last lines get inserted after + for (i, line) in lines[1..].iter().cloned().enumerate() { + self.lines.insert(row + 1 + i, line); + } + + // Reattach right half to the last inserted line + self.lines[row + last].append(&mut right); + } + RegisterContent::Line(lines) => { + let row = match anchor { + Anchor::After => self.row() + 1, + Anchor::Before => self.row(), + }; + for (i,line) in lines.iter().cloned().enumerate() { + self.lines.insert(row + i, line); + } + } + RegisterContent::Block(lines) => todo!(), + RegisterContent::Empty => {} + } + } + Verb::InsertModeLineBreak(anchor) => match anchor { + Anchor::After => { + let row = self.row(); + let target = (row + 1).min(self.lines.len()); + self.lines.insert(target, Line::default()); + self.cursor.pos = Pos { + row: row + 1, + col: 0, + }; + } + Anchor::Before => { + let row = self.row(); + self.lines.insert(row, Line::default()); + self.cursor.pos = Pos { row, col: 0 }; + } + }, + Verb::SwapVisualAnchor => todo!(), + Verb::JoinLines => { + let old_exclusive = self.cursor.exclusive; + self.cursor.exclusive = false; + for _ in 0..count { + let row = self.row(); + let target_pos = Pos { + row, + col: self.offset_col(row, isize::MAX), + }; + if row == self.lines.len() - 1 { + break; + } + + let mut next_line = self.lines.remove(row + 1).trim_start(); + let this_line = self.cur_line_mut(); + let this_has_ws = this_line.0.last().is_some_and(|g| g.is_ws()); + let join_with_space = !this_has_ws && !this_line.is_empty() && !next_line.is_empty(); + + if join_with_space { + next_line.insert_char(0, ' '); + } + + this_line.append(&mut next_line); + self.set_cursor(target_pos); + } + + self.cursor.exclusive = old_exclusive; + } + Verb::InsertChar(ch) => self.insert(Grapheme::from(*ch)), + Verb::Insert(s) => self.insert_str(s), + Verb::Indent => todo!(), + Verb::Dedent => todo!(), + Verb::Equalize => todo!(), + Verb::AcceptLineOrNewline => { + // If we are here, we did not accept the line + // so we break to a new line + self.break_line(); + } Verb::ShellCmd(cmd) => self.verb_shell_cmd(cmd)?, Verb::Read(src) => match src { ReadSrc::File(path_buf) => { @@ -869,19 +1508,13 @@ impl LineBuf { .truncate(true) .write(true) .open(path_buf) - } else { - OpenOptions::new() - .create(true) - .append(true) - .open(path_buf) - + OpenOptions::new().create(true).append(true).open(path_buf) }) else { write_meta(|m| { m.post_system_message(format!("Failed to open file {}", path_buf.display())) }); return Ok(()); - }; if let Err(e) = file.write_all(self.joined().as_bytes()) { @@ -928,24 +1561,23 @@ impl LineBuf { | Verb::VisualModeBlock | Verb::CompleteBackward | Verb::VisualModeSelectLast => { - let Some(motion_kind) = self.eval_motion(cmd) else { - return Ok(()) - }; - self.apply_motion(motion_kind)?; - } - Verb::Normal(_) | - Verb::Substitute(..) | - Verb::RepeatSubstitute | - Verb::Quit | - Verb::RepeatGlobal => { - log::warn!("Verb {:?} is not implemented yet", verb); - } - } + let Some(motion_kind) = self.eval_motion(cmd) else { + return Ok(()); + }; + self.apply_motion(motion_kind)?; + } + Verb::Normal(_) + | Verb::Substitute(..) + | Verb::RepeatSubstitute + | Verb::Quit + | Verb::RepeatGlobal => { + log::warn!("Verb {:?} is not implemented yet", verb); + } + } - Ok(()) - } - pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { - let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit()); + Ok(()) + } + pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { 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() || cmd @@ -953,17 +1585,484 @@ impl LineBuf { .as_ref() .is_some_and(|v| v.1 == Verb::AcceptLineOrNewline); let is_undo_op = cmd.is_undo_op(); - let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging); + let is_vertical = matches!( + cmd.motion().map(|m| &m.1), + Some(Motion::LineUp | Motion::LineDown) + ); - self.exec_verb(&cmd) + if !is_vertical { + self.saved_col = None; + } + + let before = self.lines.clone(); + let old_cursor = self.cursor.pos; + + let res = self.exec_verb(&cmd); + + let new_cursor = self.cursor.pos; + + if self.lines != before && !is_undo_op { + self.redo_stack.clear(); + if is_char_insert { + // Merge consecutive char inserts into one undo entry + if let Some(edit) = self.undo_stack.last_mut().filter(|e| e.merging) { + edit.new = self.lines.clone(); + edit.new_cursor = new_cursor; + } else { + self.undo_stack.push(Edit { + old_cursor, + new_cursor, + old: before, + new: self.lines.clone(), + merging: true, + }); + } + } else { + // Stop merging on any non-insert edit + if let Some(edit) = self.undo_stack.last_mut() { + edit.merging = false; + } + self.handle_edit(before, new_cursor, old_cursor); + } + } + + self.fix_cursor(); + res + } + + pub fn handle_edit(&mut self, old: Vec, new_cursor: Pos, old_cursor: Pos) { + let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging); + if edit_is_merging { + // Update the `new` snapshot on the existing edit + if let Some(edit) = self.undo_stack.last_mut() { + edit.new = self.lines.clone(); + } + } else { + self.undo_stack.push(Edit { + new_cursor, + old_cursor, + old, + new: self.lines.clone(), + merging: false, + }); + } + } + + fn fix_cursor(&mut self) { + if self.cursor.exclusive { + let line = self.cur_line(); + let col = self.col(); + if col > 0 && col >= line.len() { + self.cursor.pos.col = line.len().saturating_sub(1); + } + } else { + let line = self.cur_line(); + let col = self.col(); + if col > 0 && col > line.len() { + self.cursor.pos.col = line.len(); + } + } } - pub fn joined(&self) -> String { - let mut lines = vec![]; - for line in &self.lines { - lines.push(line.to_string()); + pub fn joined(&self) -> String { + let mut lines = vec![]; + for line in &self.lines { + lines.push(line.to_string()); + } + lines.join("\n") + } + + // ───── Compatibility shims for old flat-string interface ───── + + /// Compat shim: replace buffer contents from a string, parsing into lines. + pub fn set_buffer(&mut self, s: String) { + self.lines = to_lines(&s); + if self.lines.is_empty() { + self.lines.push(Line::default()); + } + // Clamp cursor to valid position + self.cursor.pos.row = self.cursor.pos.row.min(self.lines.len().saturating_sub(1)); + let max_col = self.lines[self.cursor.pos.row].len(); + self.cursor.pos.col = self.cursor.pos.col.min(max_col); + } + + /// Compat shim: set hint text. None clears the hint. + pub fn set_hint(&mut self, hint: Option) { + match hint { + Some(s) => self.hint = to_lines(&s), + None => self.hint.clear(), + } + } + + /// Compat shim: returns true if there is a non-empty hint. + pub fn has_hint(&self) -> bool { + !self.hint.is_empty() && self.hint.iter().any(|l| !l.is_empty()) + } + + /// Compat shim: get hint text as a string. + pub fn get_hint_text(&self) -> String { + let mut lines = vec![]; + let mut hint = self.hint.clone(); + trim_lines(&mut hint); + for line in hint { + lines.push(line.to_string()); + } + lines.join("\n") + } + + /// Compat shim: accept the current hint by appending it to the buffer. + pub fn accept_hint(&mut self) { + if self.hint.is_empty() { + return; + } + let hint_str = self.get_hint_text(); + self.push_str(&hint_str); + self.hint.clear(); + } + + /// Compat shim: return a constructor that sets initial buffer contents and cursor. + pub fn with_initial(mut self, s: &str, cursor_pos: usize) -> Self { + self.set_buffer(s.to_string()); + // In the flat model, cursor_pos was a flat offset. Map to col on row 0. + self.cursor.pos = Pos { + row: 0, + col: cursor_pos.min(s.len()), + }; + self + } + + /// Compat shim: move cursor to end of buffer. + pub fn move_cursor_to_end(&mut self) { + let last_row = self.lines.len().saturating_sub(1); + let last_col = self.lines[last_row].len(); + self.cursor.pos = Pos { + row: last_row, + col: last_col, + }; + } + + /// Compat shim: returns the maximum cursor position (flat grapheme count). + pub fn cursor_max(&self) -> usize { + // In single-line mode this is the length of the first line + // In multi-line mode this returns total grapheme count (for flat compat) + if self.lines.len() == 1 { + self.lines[0].len() + } else { + self.count_graphemes() + } + } + + /// Compat shim: returns true if cursor is at the max position. + pub fn cursor_at_max(&self) -> bool { + let last_row = self.lines.len().saturating_sub(1); + self.cursor.pos.row == last_row && self.cursor.pos.col >= self.lines[last_row].len() + } + + /// Compat shim: set cursor with clamping. + pub fn set_cursor_clamp(&mut self, exclusive: bool) { + self.cursor.exclusive = exclusive; + } + + /// Compat shim: returns the flat column of the start of the current line. + /// In the old flat model this returned 0 for single-line; for multi-line it's the + /// flat offset of the beginning of the current row. + pub fn start_of_line(&self) -> usize { + // Return 0-based flat offset of start of current row + let mut offset = 0; + for i in 0..self.cursor.pos.row { + offset += self.lines[i].len() + 1; // +1 for '\n' + } + offset + } + + pub fn on_last_line(&self) -> bool { + self.cursor.pos.row == self.lines.len().saturating_sub(1) + } + + /// Compat shim: returns slice of joined buffer from grapheme indices. + pub fn slice(&self, range: std::ops::Range) -> Option { + let joined = self.joined(); + let graphemes: Vec<&str> = joined.graphemes(true).collect(); + if range.start > graphemes.len() || range.end > graphemes.len() { + return None; + } + Some(graphemes[range].join("")) + } + + /// Compat shim: returns the string from buffer start to cursor position. + pub fn slice_to_cursor(&self) -> Option { + let mut result = String::new(); + for i in 0..self.cursor.pos.row { + result.push_str(&self.lines[i].to_string()); + result.push('\n'); + } + let line = &self.lines[self.cursor.pos.row]; + let col = self.cursor.pos.col.min(line.len()); + for g in &line.graphemes()[..col] { + result.push_str(&g.to_string()); + } + Some(result) + } + + /// Compat shim: returns cursor byte position in the joined string. + pub fn cursor_byte_pos(&self) -> usize { + let mut pos = 0; + for i in 0..self.cursor.pos.row { + pos += self.lines[i].to_string().len() + 1; // +1 for '\n' + } + let line_str = self.lines[self.cursor.pos.row].to_string(); + let col = self + .cursor + .pos + .col + .min(self.lines[self.cursor.pos.row].len()); + // Sum bytes of graphemes up to col + let mut byte_count = 0; + for (i, g) in line_str.graphemes(true).enumerate() { + if i >= col { + break; + } + byte_count += g.len(); + } + pos + byte_count + } + + pub fn start_char_select(&mut self) { + self.select_mode = Some(SelectMode::Char(SelectAnchor::Pos(self.cursor.pos))); + } + + pub fn start_line_select(&mut self) { + self.select_mode = Some(SelectMode::Line(SelectAnchor::LineNo(self.cursor.pos.row))); + } + + pub fn start_block_select(&mut self) { + self.select_mode = Some(SelectMode::Block(SelectAnchor::Pos(self.cursor.pos))); + } + + /// Compat shim: stop visual selection. + pub fn stop_selecting(&mut self) { + if self.select_mode.is_some() { + self.last_selection = self.select_mode.map(|m| { + let anchor = match m { + SelectMode::Char(a) | SelectMode::Block(a) | SelectMode::Line(a) => a, + }; + (m, anchor) + }); + } + self.select_mode = None; + } + + /// Compat shim: return current selection range as flat (start, end) offsets. + pub fn select_range(&self) -> Option<(usize, usize)> { + let mode = self.select_mode.as_ref()?; + let anchor_pos = match mode { + SelectMode::Char(SelectAnchor::Pos(p)) => *p, + SelectMode::Line(SelectAnchor::LineNo(l)) => Pos { row: *l, col: 0 }, + SelectMode::Block(SelectAnchor::Pos(p)) => *p, + _ => return None, + }; + let cursor_pos = self.cursor.pos; + // Convert both to flat offsets + let flat_anchor = self.pos_to_flat(anchor_pos); + let flat_cursor = self.pos_to_flat(cursor_pos); + let (start, end) = ordered(flat_anchor, flat_cursor); + Some((start, end)) + } + + /// Helper: convert a Pos to a flat grapheme offset. + fn pos_to_flat(&self, pos: Pos) -> usize { + let mut offset = 0; + let row = pos.row.min(self.lines.len().saturating_sub(1)); + for i in 0..row { + offset += self.lines[i].len() + 1; // +1 for '\n' + } + offset + pos.col.min(self.lines[row].len()) + } + + /// Compat shim: attempt history expansion. Stub that returns false. + pub fn attempt_history_expansion(&mut self, _history: &super::history::History) -> bool { + // TODO: implement history expansion for 2D buffer + false + } + + /// Compat shim: check if cursor is on an escaped char. + pub fn cursor_is_escaped(&self) -> bool { + if self.cursor.pos.col == 0 { + return false; + } + let line = &self.lines[self.cursor.pos.row]; + if self.cursor.pos.col > line.len() { + return false; + } + line + .graphemes() + .get(self.cursor.pos.col.saturating_sub(1)) + .is_some_and(|g| g.is_char('\\')) + } + + /// Compat shim: take buffer contents and reset. + pub fn take_buf(&mut self) -> String { + let result = self.joined(); + self.lines = vec![Line::default()]; + self.cursor.pos = Pos { row: 0, col: 0 }; + result + } + + /// Compat shim: calculate indent level. + pub fn calc_indent_level(&mut self) { + let joined = self.joined(); + self.indent_ctx.calculate(&joined); + } + + /// Compat shim: mark where insert mode started. + pub fn mark_insert_mode_start_pos(&mut self) { + self.insert_mode_start_pos = Some(self.cursor.pos); + } + + /// Compat shim: clear insert mode start position. + pub fn clear_insert_mode_start_pos(&mut self) { + self.insert_mode_start_pos = None; + } +} + +impl std::fmt::Display for LineBuf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.joined()) + } +} + +struct CharClassIter<'a> { + lines: &'a [Line], + row: usize, + col: usize, + exhausted: bool, + at_boundary: bool, +} + +impl<'a> CharClassIter<'a> { + pub fn new(lines: &'a [Line], start_pos: Pos) -> Self { + Self { + lines, + row: start_pos.row, + col: start_pos.col, + exhausted: false, + at_boundary: false, } - lines.join("\n") + } + fn get_pos(&self) -> Pos { + Pos { row: self.row, col: self.col } + } +} + +impl<'a> Iterator for CharClassIter<'a> { + type Item = (Pos, CharClass); + fn next(&mut self) -> Option<(Pos, CharClass)> { + if self.exhausted { return None; } + + // Synthetic whitespace for line boundary + if self.at_boundary { + self.at_boundary = false; + let pos = self.get_pos(); + return Some((pos, CharClass::Whitespace)); + } + + if self.row >= self.lines.len() { + self.exhausted = true; + return None; + } + + let line = &self.lines[self.row]; + // Empty line = whitespace + if line.is_empty() { + let pos = Pos { row: self.row, col: 0 }; + self.row += 1; + self.col = 0; + return Some((pos, CharClass::Whitespace)); + } + + let pos = self.get_pos(); + let class = line[self.col].class(); + + self.col += 1; + if self.col >= line.len() { + self.row += 1; + self.col = 0; + self.at_boundary = self.row < self.lines.len(); + } + + Some((pos, class)) + } +} + +struct CharClassIterRev<'a> { + lines: &'a [Line], + row: usize, + col: usize, + exhausted: bool, + at_boundary: bool, +} + +impl<'a> CharClassIterRev<'a> { + pub fn new(lines: &'a [Line], start_pos: Pos) -> Self { + Self { + lines, + row: start_pos.row, + col: start_pos.col, + exhausted: false, + at_boundary: false, + } + } + fn get_pos(&self) -> Pos { + Pos { row: self.row, col: self.col } + } +} + +impl<'a> Iterator for CharClassIterRev<'a> { + type Item = (Pos, CharClass); + fn next(&mut self) -> Option<(Pos, CharClass)> { + if self.exhausted { return None; } + + // Synthetic whitespace for line boundary + if self.at_boundary { + self.at_boundary = false; + let pos = self.get_pos(); + return Some((pos, CharClass::Whitespace)); + } + + if self.row >= self.lines.len() { + self.exhausted = true; + return None; + } + + let line = &self.lines[self.row]; + // Empty line = whitespace + if line.is_empty() { + let pos = Pos { row: self.row, col: 0 }; + if self.row == 0 { + self.exhausted = true; + } else { + self.row -= 1; + self.col = self.lines[self.row].len().saturating_sub(1); + } + return Some((pos, CharClass::Whitespace)); + } + + let pos = self.get_pos(); + let class = line[self.col].class(); + + if self.col == 0 { + if self.row == 0 { + self.exhausted = true; + } else { + self.row -= 1; + self.col = self.lines[self.row].len().saturating_sub(1); + self.at_boundary = true; + } + } else { + self.col -= 1; + } + + Some((pos, class)) } } @@ -985,7 +2084,28 @@ pub fn rot13(input: &str) -> String { .collect() } -pub fn ordered(start: usize, end: usize) -> (usize, usize) { +pub fn rot13_char(c: char) -> char { + let offset = if c.is_ascii_lowercase() { + b'a' + } else if c.is_ascii_uppercase() { + b'A' + } else { + return c; + }; + (((c as u8 - offset + 13) % 26) + offset) as char +} + +pub fn toggle_case_char(c: char) -> char { + if c.is_ascii_lowercase() { + c.to_ascii_uppercase() + } else if c.is_ascii_uppercase() { + c.to_ascii_lowercase() + } else { + c + } +} + +pub fn ordered(start: T, end: T) -> (T, T) { if start > end { (end, start) } else { diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 9940d97..5cd2274 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -341,23 +341,25 @@ impl ShedVi { pub fn with_initial(mut self, initial: &str) -> Self { self.editor = LineBuf::new().with_initial(initial, 0); - self - .history - .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); + { + let s = self.editor.joined(); + let c = self.editor.cursor.get(); + self.history.update_pending_cmd((&s, c)); + } self } - /// A mutable reference to the currently focused editor - /// This includes the main LineBuf, and sub-editors for modes like Ex mode. - pub fn focused_editor(&mut self) -> &mut LineBuf { - self.mode.editor().unwrap_or(&mut self.editor) - } + /// A mutable reference to the currently focused editor + /// This includes the main LineBuf, and sub-editors for modes like Ex mode. + pub fn focused_editor(&mut self) -> &mut LineBuf { + self.mode.editor().unwrap_or(&mut self.editor) + } - /// A mutable reference to the currently focused history, if any. - /// This includes the main history struct, and history for sub-editors like Ex mode. - pub fn focused_history(&mut self) -> &mut History { - self.mode.history().unwrap_or(&mut self.history) - } + /// A mutable reference to the currently focused history, if any. + /// This includes the main history struct, and history for sub-editors like Ex mode. + pub fn focused_history(&mut self) -> &mut History { + self.mode.history().unwrap_or(&mut self.history) + } /// Feed raw bytes from stdin into the reader's buffer pub fn feed_bytes(&mut self, bytes: &[u8]) { @@ -436,7 +438,7 @@ impl ShedVi { if self.mode.report_mode() == ModeReport::Normal { return Ok(true); } - let input = Arc::new(self.editor.buffer.clone()); + let input = Arc::new(self.editor.joined()); self.editor.calc_indent_level(); let lex_result1 = LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::>>(); @@ -476,21 +478,21 @@ impl ShedVi { SelectorResponse::Accept(cmd) => { let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect)); - { - let editor = self.focused_editor(); - editor.set_buffer(cmd.to_string()); - editor.move_cursor_to_end(); - } + { + let editor = self.focused_editor(); + editor.set_buffer(cmd.to_string()); + editor.move_cursor_to_end(); + } self .history - .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); - self.editor.set_hint(None); - { - let mut writer = std::mem::take(&mut self.writer); - self.focused_history().fuzzy_finder.clear(&mut writer)?; - self.writer = writer; - } + .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); + self.editor.set_hint(None); + { + let mut writer = std::mem::take(&mut self.writer); + self.focused_history().fuzzy_finder.clear(&mut writer)?; + self.writer = writer; + } self.focused_history().fuzzy_finder.reset(); with_vars([("_HIST_ENTRY".into(), cmd.clone())], || { @@ -514,11 +516,11 @@ impl ShedVi { post_cmds.exec(); self.editor.set_hint(None); - { - let mut writer = std::mem::take(&mut self.writer); - self.focused_history().fuzzy_finder.clear(&mut writer)?; - self.writer = writer; - } + { + let mut writer = std::mem::take(&mut self.writer); + self.focused_history().fuzzy_finder.clear(&mut writer)?; + self.writer = writer; + } write_vars(|v| { v.set_var( "SHED_VI_MODE", @@ -554,7 +556,7 @@ impl ShedVi { } self .history - .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); + .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); let hint = self.history.get_hint(); self.editor.set_hint(hint); self.completer.clear(&mut self.writer)?; @@ -669,14 +671,15 @@ impl ShedVi { } self .history - .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); + .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); self.needs_redraw = true; return Ok(None); } if let KeyEvent(KeyCode::Tab, mod_keys) = key { - if self.mode.report_mode() != ModeReport::Ex - && self.editor.attempt_history_expansion(&self.history) { + if self.mode.report_mode() != ModeReport::Ex + && self.editor.attempt_history_expansion(&self.history) + { // If history expansion occurred, don't attempt completion yet // allow the user to see the expanded command and accept or edit it before completing return Ok(None); @@ -686,7 +689,7 @@ impl ShedVi { ModKeys::SHIFT => -1, _ => 1, }; - let line = self.focused_editor().as_str().to_string(); + let line = self.focused_editor().joined(); let cursor_pos = self.focused_editor().cursor_byte_pos(); match self.completer.complete(line, cursor_pos, direction) { @@ -719,7 +722,7 @@ impl ShedVi { } self .history - .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); + .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); let hint = self.history.get_hint(); self.editor.set_hint(hint); write_vars(|v| { @@ -776,7 +779,7 @@ impl ShedVi { } else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key && matches!(self.mode.report_mode(), ModeReport::Insert | ModeReport::Ex) { - let initial = self.focused_editor().as_str().to_string(); + let initial = self.focused_editor().joined(); match self.focused_history().start_search(&initial) { Some(entry) => { let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect)); @@ -788,7 +791,7 @@ impl ShedVi { self.focused_editor().move_cursor_to_end(); self .history - .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); + .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); self.editor.set_hint(None); } None => { @@ -847,8 +850,6 @@ impl ShedVi { let Some(mut cmd) = cmd else { return Ok(None); }; - cmd.alter_line_motion_if_no_verb(); - if self.should_grab_history(&cmd) { self.scroll_history(cmd); self.needs_redraw = true; @@ -875,7 +876,7 @@ impl ShedVi { } if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) { - if self.focused_editor().buffer.is_empty() { + if self.focused_editor().joined().is_empty() { return Ok(Some(ReadlineEvent::Eof)); } else { *self.focused_editor() = LineBuf::new(); @@ -884,23 +885,21 @@ impl ShedVi { return Ok(None); } } else if cmd.verb().is_some_and(|v| v.1 == Verb::Quit) { - return Ok(Some(ReadlineEvent::Eof)); - } + return Ok(Some(ReadlineEvent::Eof)); + } let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit()); let is_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_))); let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD); - log::debug!("is_ex_cmd: {is_ex_cmd}"); if is_shell_cmd { self.old_layout = None; } if is_ex_cmd { self.ex_history.push(cmd.raw_seq.clone()); self.ex_history.reset(); - log::debug!("ex_history: {:?}", self.ex_history.entries()); } - let before = self.editor.buffer.clone(); + let before = self.editor.joined(); self.exec_cmd(cmd, false)?; @@ -909,12 +908,12 @@ impl ShedVi { self.handle_key(key)?; } } - let after = self.editor.as_str(); + let after = self.editor.joined(); if before != after { self .history - .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); + .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); } else if before == after && has_edit_verb { self.writer.send_bell().ok(); } @@ -929,7 +928,7 @@ impl ShedVi { pub fn get_layout(&mut self, line: &str) -> Layout { let to_cursor = self.editor.slice_to_cursor().unwrap_or_default(); let (cols, _) = get_win_size(self.tty); - Layout::from_parts(cols, self.prompt.get_ps1(), to_cursor, line) + Layout::from_parts(cols, self.prompt.get_ps1(), &to_cursor, line) } pub fn scroll_history(&mut self, cmd: ViCmd) { /* @@ -941,8 +940,8 @@ impl ShedVi { let count = &cmd.motion().unwrap().0; let motion = &cmd.motion().unwrap().1; let count = match motion { - Motion::LineUpCharwise => -(*count as isize), - Motion::LineDownCharwise => *count as isize, + Motion::LineUp => -(*count as isize), + Motion::LineDown => *count as isize, _ => unreachable!(), }; let entry = self.history.scroll(count); @@ -985,12 +984,12 @@ impl ShedVi { cmd.verb().is_none() && (cmd .motion() - .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise))) + .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUp))) && self.editor.start_of_line() == 0) || (cmd .motion() - .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) - && self.editor.end_of_line() == self.editor.cursor_max()) + .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDown))) + && self.editor.on_last_line()) } pub fn line_text(&mut self) -> String { @@ -1004,7 +1003,8 @@ impl ShedVi { self.highlighter.expand_control_chars(); self.highlighter.highlight(); let highlighted = self.highlighter.take(); - format!("{highlighted}{hint}") + let res = format!("{highlighted}{hint}"); + res } pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> { @@ -1035,11 +1035,11 @@ impl ShedVi { let one_line = new_layout.end.row == 0; self.completer.clear(&mut self.writer)?; - { - let mut writer = std::mem::take(&mut self.writer); - self.focused_history().fuzzy_finder.clear(&mut writer)?; - self.writer = writer; - } + { + let mut writer = std::mem::take(&mut self.writer); + self.focused_history().fuzzy_finder.clear(&mut writer)?; + self.writer = writer; + } if let Some(layout) = self.old_layout.as_ref() { self.writer.clear_rows(layout)?; @@ -1136,11 +1136,11 @@ impl ShedVi { .fuzzy_finder .set_prompt_line_context(preceding_width, new_layout.cursor.col); - { - let mut writer = std::mem::take(&mut self.writer); - self.focused_history().fuzzy_finder.draw(&mut writer)?; - self.writer = writer; - } + { + let mut writer = std::mem::take(&mut self.writer); + self.focused_history().fuzzy_finder.draw(&mut writer)?; + self.writer = writer; + } self.old_layout = Some(new_layout); self.needs_redraw = false; @@ -1172,7 +1172,6 @@ impl ShedVi { } fn exec_mode_transition(&mut self, cmd: ViCmd, from_replay: bool) -> ShResult<()> { - let mut select_mode = None; let mut is_insert_mode = false; let count = cmd.verb_count(); @@ -1210,9 +1209,7 @@ impl ShedVi { Verb::VisualModeSelectLast => { if self.mode.report_mode() != ModeReport::Visual { - self - .editor - .start_selecting(SelectMode::Char(SelectAnchor::End)); + self.editor.start_char_select(); } let mut mode: Box = Box::new(ViVisual::new()); self.swap_mode(&mut mode); @@ -1220,11 +1217,11 @@ impl ShedVi { return self.editor.exec_cmd(cmd); } Verb::VisualMode => { - select_mode = Some(SelectMode::Char(SelectAnchor::End)); + self.editor.start_char_select(); Box::new(ViVisual::new()) } Verb::VisualModeLine => { - select_mode = Some(SelectMode::Line(SelectAnchor::End)); + self.editor.start_line_select(); Box::new(ViVisual::new()) } @@ -1232,6 +1229,8 @@ impl ShedVi { } }; + // The mode we just created swaps places with our current mode + // After this line, 'mode' contains our previous mode. self.swap_mode(&mut mode); if matches!( @@ -1259,11 +1258,6 @@ impl ShedVi { self.editor.set_cursor_clamp(self.mode.clamp_cursor()); self.editor.exec_cmd(cmd)?; - if let Some(sel_mode) = select_mode { - self.editor.start_selecting(sel_mode); - } else { - self.editor.stop_selecting(); - } if is_insert_mode { self.editor.mark_insert_mode_start_pos(); } else { diff --git a/src/readline/register.rs b/src/readline/register.rs index dde4329..df913f9 100644 --- a/src/readline/register.rs +++ b/src/readline/register.rs @@ -1,5 +1,7 @@ use std::{fmt::Display, sync::Mutex}; +use crate::readline::linebuf::Line; + pub static REGISTERS: Mutex = Mutex::new(Registers::new()); #[cfg(test)] @@ -41,8 +43,9 @@ pub fn append_register(ch: Option, buf: RegisterContent) { #[derive(Default, Clone, Debug)] pub enum RegisterContent { - Span(String), - Line(String), + Span(Vec), + Line(Vec), + Block(Vec), #[default] Empty, } @@ -50,8 +53,11 @@ pub enum RegisterContent { impl Display for RegisterContent { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Span(s) => write!(f, "{}", s), - Self::Line(s) => write!(f, "{}", s), + Self::Block(s) | + Self::Line(s) | + Self::Span(s) => { + write!(f, "{}", s.iter().map(|l| l.to_string()).collect::>().join("\n")) + } Self::Empty => write!(f, ""), } } @@ -59,16 +65,13 @@ impl Display for RegisterContent { impl RegisterContent { pub fn clear(&mut self) { - match self { - Self::Span(s) => s.clear(), - Self::Line(s) => s.clear(), - Self::Empty => {} - } + *self = Self::Empty } pub fn len(&self) -> usize { match self { - Self::Span(s) => s.len(), - Self::Line(s) => s.len(), + Self::Span(s) | + Self::Line(s) | + Self::Block(s) => s.len(), Self::Empty => 0, } } @@ -76,24 +79,21 @@ impl RegisterContent { match self { Self::Span(s) => s.is_empty(), Self::Line(s) => s.is_empty(), + Self::Block(s) => s.is_empty(), Self::Empty => true, } } + pub fn is_block(&self) -> bool { + matches!(self, Self::Block(_)) + } pub fn is_line(&self) -> bool { matches!(self, Self::Line(_)) } pub fn is_span(&self) -> bool { matches!(self, Self::Span(_)) } - pub fn as_str(&self) -> &str { - match self { - Self::Span(s) => s, - Self::Line(s) => s, - Self::Empty => "", - } - } pub fn char_count(&self) -> usize { - self.as_str().chars().count() + self.to_string().chars().count() } } @@ -238,7 +238,7 @@ pub struct Register { impl Register { pub const fn new() -> Self { Self { - content: RegisterContent::Span(String::new()), + content: RegisterContent::Empty, } } pub fn content(&self) -> &RegisterContent { @@ -247,13 +247,16 @@ impl Register { pub fn write(&mut self, buf: RegisterContent) { self.content = buf } - pub fn append(&mut self, buf: RegisterContent) { + pub fn append(&mut self, mut buf: RegisterContent) { match buf { RegisterContent::Empty => {} - RegisterContent::Span(ref s) | RegisterContent::Line(ref s) => match &mut self.content { + RegisterContent::Span(ref mut s) | + RegisterContent::Block(ref mut s) | + RegisterContent::Line(ref mut s) => match &mut self.content { RegisterContent::Empty => self.content = buf, - RegisterContent::Span(existing) => existing.push_str(s), - RegisterContent::Line(existing) => existing.push_str(s), + RegisterContent::Span(existing) | + RegisterContent::Line(existing) | + RegisterContent::Block(existing) => existing.append(s), }, } } diff --git a/src/readline/term.rs b/src/readline/term.rs index fea81d2..4add73f 100644 --- a/src/readline/term.rs +++ b/src/readline/term.rs @@ -70,9 +70,11 @@ pub fn get_win_size(fd: RawFd) -> (Col, Row) { } fn enumerate_lines(s: &str, left_pad: usize, show_numbers: bool) -> String { - let total_lines = s.lines().count(); + let lines: Vec<&str> = s.split('\n').collect(); + let total_lines = lines.len(); let max_num_len = total_lines.to_string().len(); - s.lines() + lines + .into_iter() .enumerate() .fold(String::new(), |mut acc, (i, ln)| { if i == 0 { diff --git a/src/readline/tests.rs b/src/readline/tests.rs index 88b01ff..71a6d44 100644 --- a/src/readline/tests.rs +++ b/src/readline/tests.rs @@ -23,7 +23,7 @@ macro_rules! vi_test { vi.feed_bytes($op.as_bytes()); vi.process_input().unwrap(); - assert_eq!(vi.editor.as_str(), $expected_text); + assert_eq!(vi.editor.joined(), $expected_text); assert_eq!(vi.editor.cursor.get(), $expected_cursor); } )* @@ -512,7 +512,7 @@ fn vi_auto_indent() { } assert_eq!( - vi.editor.as_str(), + vi.editor.joined(), "func() {\n\tcase foo in\n\t\tbar)\n\t\t\twhile true; do\n\t\t\t\techo foo \\\n\t\t\t\t\tbar \\\n\t\t\t\t\tbiz \\\n\t\t\t\t\tbazz\n\t\t\t\tbreak\n\t\t\tdone\n\t\t;;\n\tesac\n}" ); } diff --git a/src/readline/vicmd.rs b/src/readline/vicmd.rs index 4216de4..7fb1a5a 100644 --- a/src/readline/vicmd.rs +++ b/src/readline/vicmd.rs @@ -158,12 +158,10 @@ impl ViCmd { }) && self.motion.is_none() } pub fn is_line_motion(&self) -> bool { - self.motion.as_ref().is_some_and(|m| { - matches!( - m.1, - Motion::LineUp | Motion::LineDown - ) - }) + self + .motion + .as_ref() + .is_some_and(|m| matches!(m.1, Motion::LineUp | Motion::LineDown)) } /// If a ViCmd has a linewise motion, but no verb, we change it to charwise pub fn is_mode_transition(&self) -> bool { @@ -249,7 +247,7 @@ pub enum Verb { Read(ReadSrc), Write(WriteDest), Edit(PathBuf), - Quit, + Quit, Substitute(String, String, SubFlags), RepeatSubstitute, RepeatGlobal, @@ -296,9 +294,9 @@ impl Verb { | Self::JoinLines | Self::InsertChar(_) | Self::Insert(_) - | Self::Dedent - | Self::Indent - | Self::Equalize + | Self::Dedent + | Self::Indent + | Self::Equalize | Self::Rot13 | Self::EndOfFile | Self::IncrementNumber(_) @@ -380,10 +378,7 @@ impl Motion { ) } pub fn is_linewise(&self) -> bool { - matches!( - self, - Self::WholeLine | Self::LineUp | Self::LineDown - ) + matches!(self, Self::WholeLine | Self::LineUp | Self::LineDown) } } diff --git a/src/readline/vimode/ex.rs b/src/readline/vimode/ex.rs index 7466f2f..0e861de 100644 --- a/src/readline/vimode/ex.rs +++ b/src/readline/vimode/ex.rs @@ -46,7 +46,6 @@ impl ExEditor { history, ..Default::default() }; - new.buf.update_graphemes(); new } pub fn clear(&mut self) { @@ -56,19 +55,19 @@ impl ExEditor { cmd.verb().is_none() && (cmd .motion() - .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise))) + .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUp))) && self.buf.start_of_line() == 0) || (cmd .motion() - .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) - && self.buf.end_of_line() == self.buf.cursor_max()) + .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDown))) + && self.buf.on_last_line()) } pub fn scroll_history(&mut self, cmd: ViCmd) { let count = &cmd.motion().unwrap().0; let motion = &cmd.motion().unwrap().1; let count = match motion { - Motion::LineUpCharwise => -(*count as isize), - Motion::LineDownCharwise => *count as isize, + Motion::LineUp => -(*count as isize), + Motion::LineDown => *count as isize, _ => unreachable!(), }; let entry = self.history.scroll(count); @@ -88,7 +87,6 @@ impl ExEditor { let Some(mut cmd) = self.mode.handle_key(key) else { return Ok(()); }; - cmd.alter_line_motion_if_no_verb(); log::debug!("ExEditor got cmd: {:?}", cmd); if self.should_grab_history(&cmd) { log::debug!("Grabbing history for cmd: {:?}", cmd); @@ -118,11 +116,11 @@ impl ViMode for ViEx { use crate::readline::keys::{KeyCode as C, KeyEvent as E, ModKeys as M}; match key { E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => { - let input = self.pending_cmd.buf.as_str(); - match parse_ex_cmd(input) { + let input = self.pending_cmd.buf.joined(); + match parse_ex_cmd(&input) { Ok(cmd) => Ok(cmd), Err(e) => { - let msg = e.unwrap_or(format!("Not an editor command: {}", input)); + let msg = e.unwrap_or(format!("Not an editor command: {}", &input)); write_meta(|m| m.post_system_message(msg.clone())); Err(ShErr::simple(ShErrKind::ParseErr, msg)) } @@ -167,7 +165,7 @@ impl ViMode for ViEx { } fn pending_seq(&self) -> Option { - Some(self.pending_cmd.buf.as_str().to_string()) + Some(self.pending_cmd.buf.joined()) } fn pending_cursor(&self) -> Option { @@ -280,7 +278,7 @@ fn parse_ex_command(chars: &mut Peekable>) -> Result, Opt _ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)), _ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)), _ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))), - _ if "quit".starts_with(&cmd_name) => Ok(Some(Verb::Quit)), + _ if "quit".starts_with(&cmd_name) => Ok(Some(Verb::Quit)), _ if "read".starts_with(&cmd_name) => parse_read(chars), _ if "write".starts_with(&cmd_name) => parse_write(chars), _ if "edit".starts_with(&cmd_name) => parse_edit(chars), diff --git a/src/readline/vimode/normal.rs b/src/readline/vimode/normal.rs index 2e32c31..847df89 100644 --- a/src/readline/vimode/normal.rs +++ b/src/readline/vimode/normal.rs @@ -3,6 +3,7 @@ use std::str::Chars; use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; +use crate::readline::linebuf::Grapheme; use crate::readline::vicmd::{ Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, VerbCmd, ViCmd, Word, @@ -197,7 +198,7 @@ impl ViNormal { return Some(ViCmd { register, verb: Some(VerbCmd(count, Verb::Change)), - motion: Some(MotionCmd(1, Motion::WholeLineExclusive)), + motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: self.flags(), }); @@ -411,10 +412,10 @@ impl ViNormal { | ('~', Some(VerbCmd(_, Verb::ToggleCaseRange))) | ('>', Some(VerbCmd(_, Verb::Indent))) | ('<', Some(VerbCmd(_, Verb::Dedent))) => { - break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive)); + break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)); } ('c', Some(VerbCmd(_, Verb::Change))) => { - break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive)); + break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)); } ('W', Some(VerbCmd(_, Verb::Change))) => { // Same with 'W' @@ -535,7 +536,7 @@ impl ViNormal { break 'motion_parse Some(MotionCmd( count, - Motion::CharSearch(Direction::Forward, Dest::On, *ch), + Motion::CharSearch(Direction::Forward, Dest::On, Grapheme::from(*ch)), )); } 'F' => { @@ -545,7 +546,7 @@ impl ViNormal { break 'motion_parse Some(MotionCmd( count, - Motion::CharSearch(Direction::Backward, Dest::On, *ch), + Motion::CharSearch(Direction::Backward, Dest::On, Grapheme::from(*ch)), )); } 't' => { @@ -555,7 +556,7 @@ impl ViNormal { break 'motion_parse Some(MotionCmd( count, - Motion::CharSearch(Direction::Forward, Dest::Before, *ch), + Motion::CharSearch(Direction::Forward, Dest::Before, Grapheme::from(*ch)), )); } 'T' => { @@ -565,7 +566,7 @@ impl ViNormal { break 'motion_parse Some(MotionCmd( count, - Motion::CharSearch(Direction::Backward, Dest::Before, *ch), + Motion::CharSearch(Direction::Backward, Dest::Before, Grapheme::from(*ch)), )); } ';' => { diff --git a/src/readline/vimode/visual.rs b/src/readline/vimode/visual.rs index 8c7f782..f66c9f6 100644 --- a/src/readline/vimode/visual.rs +++ b/src/readline/vimode/visual.rs @@ -3,6 +3,7 @@ use std::str::Chars; use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; +use crate::readline::linebuf::Grapheme; use crate::readline::vicmd::{ Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, VerbCmd, ViCmd, Word, @@ -146,7 +147,7 @@ impl ViVisual { return Some(ViCmd { register, verb: Some(VerbCmd(1, Verb::Delete)), - motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), + motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), }); @@ -155,7 +156,7 @@ impl ViVisual { return Some(ViCmd { register, verb: Some(VerbCmd(1, Verb::Yank)), - motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), + motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), }); @@ -164,7 +165,7 @@ impl ViVisual { return Some(ViCmd { register, verb: Some(VerbCmd(1, Verb::Delete)), - motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), + motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), }); @@ -173,7 +174,7 @@ impl ViVisual { return Some(ViCmd { register, verb: Some(VerbCmd(1, Verb::Change)), - motion: Some(MotionCmd(1, Motion::WholeLineExclusive)), + motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), }); @@ -182,7 +183,7 @@ impl ViVisual { return Some(ViCmd { register, verb: Some(VerbCmd(1, Verb::Indent)), - motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), + motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), }); @@ -191,7 +192,7 @@ impl ViVisual { return Some(ViCmd { register, verb: Some(VerbCmd(1, Verb::Dedent)), - motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), + motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), }); @@ -200,7 +201,7 @@ impl ViVisual { return Some(ViCmd { register, verb: Some(VerbCmd(1, Verb::Equalize)), - motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), + motion: Some(MotionCmd(1, Motion::WholeLine)), raw_seq: self.take_cmd(), flags: CmdFlags::empty(), }); @@ -344,10 +345,10 @@ impl ViVisual { | ('=', Some(VerbCmd(_, Verb::Equalize))) | ('>', Some(VerbCmd(_, Verb::Indent))) | ('<', Some(VerbCmd(_, Verb::Dedent))) => { - break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive)); + break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)); } ('c', Some(VerbCmd(_, Verb::Change))) => { - break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive)); + break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)); } _ => {} } @@ -425,7 +426,7 @@ impl ViVisual { break 'motion_parse Some(MotionCmd( count, - Motion::CharSearch(Direction::Forward, Dest::On, *ch), + Motion::CharSearch(Direction::Forward, Dest::On, Grapheme::from(*ch)), )); } 'F' => { @@ -435,7 +436,7 @@ impl ViVisual { break 'motion_parse Some(MotionCmd( count, - Motion::CharSearch(Direction::Backward, Dest::On, *ch), + Motion::CharSearch(Direction::Backward, Dest::On, Grapheme::from(*ch)), )); } 't' => { @@ -445,7 +446,7 @@ impl ViVisual { break 'motion_parse Some(MotionCmd( count, - Motion::CharSearch(Direction::Forward, Dest::Before, *ch), + Motion::CharSearch(Direction::Forward, Dest::Before, Grapheme::from(*ch)), )); } 'T' => { @@ -455,7 +456,7 @@ impl ViVisual { break 'motion_parse Some(MotionCmd( count, - Motion::CharSearch(Direction::Backward, Dest::Before, *ch), + Motion::CharSearch(Direction::Backward, Dest::Before, Grapheme::from(*ch)), )); } ';' => { diff --git a/src/state.rs b/src/state.rs index f6e6e86..211e607 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1002,10 +1002,10 @@ impl From> for Var { } impl From> for Var { - fn from(value: Vec) -> Self { - let as_strs = value.into_iter().map(|c| c.0).collect::>(); - Self::new(VarKind::Arr(as_strs.into()), VarFlags::NONE) - } + fn from(value: Vec) -> Self { + let as_strs = value.into_iter().map(|c| c.0).collect::>(); + Self::new(VarKind::Arr(as_strs.into()), VarFlags::NONE) + } } impl From<&[String]> for Var {