progress on linebuf refactor

This commit is contained in:
2026-03-18 23:52:23 -04:00
parent 7c8a418f96
commit 4a82f29231
14 changed files with 2018 additions and 847 deletions

View File

@@ -1223,16 +1223,13 @@ pub fn unescape_str(raw: &str) -> String {
result.push(markers::SNG_QUOTE); result.push(markers::SNG_QUOTE);
while let Some(q_ch) = chars.next() { while let Some(q_ch) = chars.next() {
match q_ch { match q_ch {
'\\' => { '\\' => match chars.peek() {
match chars.peek() { Some(&'\\') | Some(&'\'') => {
Some(&'\\') |
Some(&'\'') => {
let ch = chars.next().unwrap(); let ch = chars.next().unwrap();
result.push(ch); result.push(ch);
} }
_ => result.push(q_ch), _ => result.push(q_ch),
} },
}
'\'' => { '\'' => {
result.push(markers::SNG_QUOTE); result.push(markers::SNG_QUOTE);
break; break;

View File

@@ -29,6 +29,50 @@ 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)] #[derive(Debug, Clone)]
pub struct Candidate(pub String); pub struct Candidate(pub String);
@@ -325,8 +369,6 @@ fn complete_filename(start: &str) -> Vec<Candidate> {
let file_name = entry.file_name(); let file_name = entry.file_name();
let file_str: Candidate = file_name.to_string_lossy().to_string().into(); let file_str: Candidate = file_name.to_string_lossy().to_string().into();
// Skip hidden files unless explicitly requested // Skip hidden files unless explicitly requested
if !prefix.starts_with('.') && file_str.0.starts_with('.') { if !prefix.starts_with('.') && file_str.0.starts_with('.') {
continue; continue;
@@ -569,7 +611,12 @@ impl CompSpec for BashCompSpec {
candidates.extend(complete_signals(&expanded)); candidates.extend(complete_signals(&expanded));
} }
if let Some(words) = &self.wordlist { 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() { if self.function.is_some() {
candidates.extend(self.exec_comp_func(ctx)?); candidates.extend(self.exec_comp_func(ctx)?);
@@ -645,7 +692,7 @@ impl CompResult {
Self::NoMatch Self::NoMatch
} else if candidates.len() == 1 { } else if candidates.len() == 1 {
Self::Single { Self::Single {
result: candidates.remove(0) result: candidates.remove(0),
} }
} else { } else {
Self::Many { candidates } Self::Many { candidates }
@@ -828,22 +875,22 @@ impl QueryEditor {
.cursor .cursor
.ret_sub(self.available_width.saturating_sub(1)); .ret_sub(self.available_width.saturating_sub(1));
} }
let max_offset = self.linebuf let max_offset = self
.linebuf
.count_graphemes() .count_graphemes()
.saturating_sub(self.available_width); .saturating_sub(self.available_width);
self.scroll_offset = self.scroll_offset.min(max_offset); self.scroll_offset = self.scroll_offset.min(max_offset);
} }
pub fn get_window(&mut self) -> String { pub fn get_window(&mut self) -> String {
self.linebuf.update_graphemes(); let buf_len = self.linebuf.count_graphemes();
let buf_len = self.linebuf.grapheme_indices().len();
if buf_len <= self.available_width { if buf_len <= self.available_width {
return self.linebuf.as_str().to_string(); return self.linebuf.joined();
} }
let start = self let start = self
.scroll_offset .scroll_offset
.min(buf_len.saturating_sub(self.available_width)); .min(buf_len.saturating_sub(self.available_width));
let end = (start + self.available_width).min(buf_len); 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<()> { pub fn handle_key(&mut self, key: K) -> ShResult<()> {
let Some(cmd) = self.mode.handle_key(key) else { let Some(cmd) = self.mode.handle_key(key) else {
@@ -1028,7 +1075,7 @@ impl FuzzySelector {
.into_iter() .into_iter()
.filter_map(|c| { .filter_map(|c| {
let mut sc = ScoredCandidate::new(c.to_string()); 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 } if score > i32::MIN { Some(sc) } else { None }
}) })
.collect(); .collect();
@@ -1319,12 +1366,18 @@ impl Completer for FuzzyCompleter {
basename, basename,
) )
} else { } else {
(self.completer.original_input[..start].to_string(), selected.clone()) (
self.completer.original_input[..start].to_string(),
selected.clone(),
)
} }
} else { } else {
start += slice.width(); start += slice.width();
let completion = selected.strip_prefix(slice).unwrap_or(&selected); 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 escaped = escape_str(&completion, false);
let ret = format!( let ret = format!(
@@ -1435,7 +1488,10 @@ impl Completer for SimpleCompleter {
} }
fn selected_candidate(&self) -> Option<String> { fn selected_candidate(&self) -> Option<String> {
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) { fn token_span(&self) -> (usize, usize) {
@@ -1591,16 +1647,20 @@ impl SimpleCompleter {
let prefix_end = start + last_sep + 1; let prefix_end = start + last_sep + 1;
let trailing_slash = selected.ends_with('/'); let trailing_slash = selected.ends_with('/');
let trimmed = selected.trim_end_matches('/'); 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 { if trailing_slash {
basename.push('/'); basename.push('/');
} }
( (self.original_input[..prefix_end].to_string(), basename)
self.original_input[..prefix_end].to_string(),
basename,
)
} else { } else {
(self.original_input[..start].to_string(), selected.to_string()) (
self.original_input[..start].to_string(),
selected.to_string(),
)
} }
} else { } else {
start += slice.width(); start += slice.width();
@@ -1608,12 +1668,7 @@ impl SimpleCompleter {
(self.original_input[..start].to_string(), completion) (self.original_input[..start].to_string(), completion)
}; };
let escaped = escape_str(&completion, false); let escaped = escape_str(&completion, false);
format!( format!("{}{}{}", prefix, escaped, &self.original_input[end..])
"{}{}{}",
prefix,
escaped,
&self.original_input[end..]
)
} }
pub fn build_comp_ctx(&self, tks: &[Tk], line: &str, cursor_pos: usize) -> ShResult<CompContext> { pub fn build_comp_ctx(&self, tks: &[Tk], line: &str, cursor_pos: usize) -> ShResult<CompContext> {
@@ -2180,7 +2235,7 @@ mod tests {
vi.feed_bytes(b"echo hello\t"); vi.feed_bytes(b"echo hello\t");
let _ = vi.process_input(); let _ = vi.process_input();
let line = vi.editor.as_str().to_string(); let line = vi.editor.joined();
assert!( assert!(
line.contains("hello\\ world.txt"), line.contains("hello\\ world.txt"),
"expected escaped space in completion: {line:?}" "expected escaped space in completion: {line:?}"
@@ -2202,7 +2257,7 @@ mod tests {
vi.feed_bytes(b"echo my\\ \t"); vi.feed_bytes(b"echo my\\ \t");
let _ = vi.process_input(); 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\\\ " // The user's "my\ " should be preserved, not double-escaped to "my\\\ "
assert!( assert!(
!line.contains("my\\\\ "), !line.contains("my\\\\ "),
@@ -2231,7 +2286,7 @@ mod tests {
vi.feed_bytes(b"echo unique_shed_test\t"); vi.feed_bytes(b"echo unique_shed_test\t");
let _ = vi.process_input(); let _ = vi.process_input();
let line = vi.editor.as_str().to_string(); let line = vi.editor.joined();
assert!( assert!(
line.contains("unique_shed_test_file.txt"), line.contains("unique_shed_test_file.txt"),
"expected completion in line: {line:?}" "expected completion in line: {line:?}"
@@ -2251,7 +2306,7 @@ mod tests {
vi.feed_bytes(b"cd mysub\t"); vi.feed_bytes(b"cd mysub\t");
let _ = vi.process_input(); let _ = vi.process_input();
let line = vi.editor.as_str().to_string(); let line = vi.editor.joined();
assert!( assert!(
line.contains("mysubdir/"), line.contains("mysubdir/"),
"expected dir completion with trailing slash: {line:?}" "expected dir completion with trailing slash: {line:?}"
@@ -2272,7 +2327,7 @@ mod tests {
vi.feed_bytes(b"cmd --opt=eqf\t"); vi.feed_bytes(b"cmd --opt=eqf\t");
let _ = vi.process_input(); let _ = vi.process_input();
let line = vi.editor.as_str().to_string(); let line = vi.editor.joined();
assert!( assert!(
line.contains("--opt=eqfile.txt"), line.contains("--opt=eqfile.txt"),
"expected completion after '=': {line:?}" "expected completion after '=': {line:?}"

View File

@@ -414,7 +414,12 @@ impl History {
} }
pub fn get_hint(&self) -> Option<String> { pub fn get_hint(&self) -> Option<String> {
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()?; let entry = self.hint_entry()?;
Some(entry.command().to_string()) Some(entry.command().to_string())
} else { } else {

File diff suppressed because it is too large Load Diff

View File

@@ -341,9 +341,11 @@ impl ShedVi {
pub fn with_initial(mut self, initial: &str) -> Self { pub fn with_initial(mut self, initial: &str) -> Self {
self.editor = LineBuf::new().with_initial(initial, 0); self.editor = LineBuf::new().with_initial(initial, 0);
self {
.history let s = self.editor.joined();
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); let c = self.editor.cursor.get();
self.history.update_pending_cmd((&s, c));
}
self self
} }
@@ -436,7 +438,7 @@ impl ShedVi {
if self.mode.report_mode() == ModeReport::Normal { if self.mode.report_mode() == ModeReport::Normal {
return Ok(true); return Ok(true);
} }
let input = Arc::new(self.editor.buffer.clone()); let input = Arc::new(self.editor.joined());
self.editor.calc_indent_level(); self.editor.calc_indent_level();
let lex_result1 = let lex_result1 =
LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>(); LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
@@ -484,7 +486,7 @@ impl ShedVi {
self self
.history .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); self.editor.set_hint(None);
{ {
let mut writer = std::mem::take(&mut self.writer); let mut writer = std::mem::take(&mut self.writer);
@@ -554,7 +556,7 @@ impl ShedVi {
} }
self self
.history .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(); let hint = self.history.get_hint();
self.editor.set_hint(hint); self.editor.set_hint(hint);
self.completer.clear(&mut self.writer)?; self.completer.clear(&mut self.writer)?;
@@ -669,14 +671,15 @@ impl ShedVi {
} }
self self
.history .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; self.needs_redraw = true;
return Ok(None); return Ok(None);
} }
if let KeyEvent(KeyCode::Tab, mod_keys) = key { if let KeyEvent(KeyCode::Tab, mod_keys) = key {
if self.mode.report_mode() != ModeReport::Ex if self.mode.report_mode() != ModeReport::Ex
&& self.editor.attempt_history_expansion(&self.history) { && self.editor.attempt_history_expansion(&self.history)
{
// If history expansion occurred, don't attempt completion yet // If history expansion occurred, don't attempt completion yet
// allow the user to see the expanded command and accept or edit it before completing // allow the user to see the expanded command and accept or edit it before completing
return Ok(None); return Ok(None);
@@ -686,7 +689,7 @@ impl ShedVi {
ModKeys::SHIFT => -1, ModKeys::SHIFT => -1,
_ => 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(); let cursor_pos = self.focused_editor().cursor_byte_pos();
match self.completer.complete(line, cursor_pos, direction) { match self.completer.complete(line, cursor_pos, direction) {
@@ -719,7 +722,7 @@ impl ShedVi {
} }
self self
.history .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(); let hint = self.history.get_hint();
self.editor.set_hint(hint); self.editor.set_hint(hint);
write_vars(|v| { write_vars(|v| {
@@ -776,7 +779,7 @@ impl ShedVi {
} else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key } else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key
&& matches!(self.mode.report_mode(), ModeReport::Insert | ModeReport::Ex) && 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) { match self.focused_history().start_search(&initial) {
Some(entry) => { Some(entry) => {
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect)); 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.focused_editor().move_cursor_to_end();
self self
.history .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); self.editor.set_hint(None);
} }
None => { None => {
@@ -847,8 +850,6 @@ impl ShedVi {
let Some(mut cmd) = cmd else { let Some(mut cmd) = cmd else {
return Ok(None); return Ok(None);
}; };
cmd.alter_line_motion_if_no_verb();
if self.should_grab_history(&cmd) { if self.should_grab_history(&cmd) {
self.scroll_history(cmd); self.scroll_history(cmd);
self.needs_redraw = true; self.needs_redraw = true;
@@ -875,7 +876,7 @@ impl ShedVi {
} }
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) { 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)); return Ok(Some(ReadlineEvent::Eof));
} else { } else {
*self.focused_editor() = LineBuf::new(); *self.focused_editor() = LineBuf::new();
@@ -890,17 +891,15 @@ impl ShedVi {
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit()); 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_shell_cmd = cmd.verb().is_some_and(|v| matches!(v.1, Verb::ShellCmd(_)));
let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD); let is_ex_cmd = cmd.flags.contains(CmdFlags::IS_EX_CMD);
log::debug!("is_ex_cmd: {is_ex_cmd}");
if is_shell_cmd { if is_shell_cmd {
self.old_layout = None; self.old_layout = None;
} }
if is_ex_cmd { if is_ex_cmd {
self.ex_history.push(cmd.raw_seq.clone()); self.ex_history.push(cmd.raw_seq.clone());
self.ex_history.reset(); 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)?; self.exec_cmd(cmd, false)?;
@@ -909,12 +908,12 @@ impl ShedVi {
self.handle_key(key)?; self.handle_key(key)?;
} }
} }
let after = self.editor.as_str(); let after = self.editor.joined();
if before != after { if before != after {
self self
.history .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 { } else if before == after && has_edit_verb {
self.writer.send_bell().ok(); self.writer.send_bell().ok();
} }
@@ -929,7 +928,7 @@ impl ShedVi {
pub fn get_layout(&mut self, line: &str) -> Layout { pub fn get_layout(&mut self, line: &str) -> Layout {
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default(); let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
let (cols, _) = get_win_size(self.tty); 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) { pub fn scroll_history(&mut self, cmd: ViCmd) {
/* /*
@@ -941,8 +940,8 @@ impl ShedVi {
let count = &cmd.motion().unwrap().0; let count = &cmd.motion().unwrap().0;
let motion = &cmd.motion().unwrap().1; let motion = &cmd.motion().unwrap().1;
let count = match motion { let count = match motion {
Motion::LineUpCharwise => -(*count as isize), Motion::LineUp => -(*count as isize),
Motion::LineDownCharwise => *count as isize, Motion::LineDown => *count as isize,
_ => unreachable!(), _ => unreachable!(),
}; };
let entry = self.history.scroll(count); let entry = self.history.scroll(count);
@@ -985,12 +984,12 @@ impl ShedVi {
cmd.verb().is_none() cmd.verb().is_none()
&& (cmd && (cmd
.motion() .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) && self.editor.start_of_line() == 0)
|| (cmd || (cmd
.motion() .motion()
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDown)))
&& self.editor.end_of_line() == self.editor.cursor_max()) && self.editor.on_last_line())
} }
pub fn line_text(&mut self) -> String { pub fn line_text(&mut self) -> String {
@@ -1004,7 +1003,8 @@ impl ShedVi {
self.highlighter.expand_control_chars(); self.highlighter.expand_control_chars();
self.highlighter.highlight(); self.highlighter.highlight();
let highlighted = self.highlighter.take(); let highlighted = self.highlighter.take();
format!("{highlighted}{hint}") let res = format!("{highlighted}{hint}");
res
} }
pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> { pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> {
@@ -1172,7 +1172,6 @@ impl ShedVi {
} }
fn exec_mode_transition(&mut self, cmd: ViCmd, from_replay: bool) -> ShResult<()> { fn exec_mode_transition(&mut self, cmd: ViCmd, from_replay: bool) -> ShResult<()> {
let mut select_mode = None;
let mut is_insert_mode = false; let mut is_insert_mode = false;
let count = cmd.verb_count(); let count = cmd.verb_count();
@@ -1210,9 +1209,7 @@ impl ShedVi {
Verb::VisualModeSelectLast => { Verb::VisualModeSelectLast => {
if self.mode.report_mode() != ModeReport::Visual { if self.mode.report_mode() != ModeReport::Visual {
self self.editor.start_char_select();
.editor
.start_selecting(SelectMode::Char(SelectAnchor::End));
} }
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new()); let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
self.swap_mode(&mut mode); self.swap_mode(&mut mode);
@@ -1220,11 +1217,11 @@ impl ShedVi {
return self.editor.exec_cmd(cmd); return self.editor.exec_cmd(cmd);
} }
Verb::VisualMode => { Verb::VisualMode => {
select_mode = Some(SelectMode::Char(SelectAnchor::End)); self.editor.start_char_select();
Box::new(ViVisual::new()) Box::new(ViVisual::new())
} }
Verb::VisualModeLine => { Verb::VisualModeLine => {
select_mode = Some(SelectMode::Line(SelectAnchor::End)); self.editor.start_line_select();
Box::new(ViVisual::new()) 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); self.swap_mode(&mut mode);
if matches!( if matches!(
@@ -1259,11 +1258,6 @@ impl ShedVi {
self.editor.set_cursor_clamp(self.mode.clamp_cursor()); self.editor.set_cursor_clamp(self.mode.clamp_cursor());
self.editor.exec_cmd(cmd)?; 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 { if is_insert_mode {
self.editor.mark_insert_mode_start_pos(); self.editor.mark_insert_mode_start_pos();
} else { } else {

View File

@@ -1,5 +1,7 @@
use std::{fmt::Display, sync::Mutex}; use std::{fmt::Display, sync::Mutex};
use crate::readline::linebuf::Line;
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new()); pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
#[cfg(test)] #[cfg(test)]
@@ -41,8 +43,9 @@ pub fn append_register(ch: Option<char>, buf: RegisterContent) {
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub enum RegisterContent { pub enum RegisterContent {
Span(String), Span(Vec<Line>),
Line(String), Line(Vec<Line>),
Block(Vec<Line>),
#[default] #[default]
Empty, Empty,
} }
@@ -50,8 +53,11 @@ pub enum RegisterContent {
impl Display for RegisterContent { impl Display for RegisterContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Span(s) => write!(f, "{}", s), Self::Block(s) |
Self::Line(s) => write!(f, "{}", s), Self::Line(s) |
Self::Span(s) => {
write!(f, "{}", s.iter().map(|l| l.to_string()).collect::<Vec<_>>().join("\n"))
}
Self::Empty => write!(f, ""), Self::Empty => write!(f, ""),
} }
} }
@@ -59,16 +65,13 @@ impl Display for RegisterContent {
impl RegisterContent { impl RegisterContent {
pub fn clear(&mut self) { pub fn clear(&mut self) {
match self { *self = Self::Empty
Self::Span(s) => s.clear(),
Self::Line(s) => s.clear(),
Self::Empty => {}
}
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
match self { match self {
Self::Span(s) => s.len(), Self::Span(s) |
Self::Line(s) => s.len(), Self::Line(s) |
Self::Block(s) => s.len(),
Self::Empty => 0, Self::Empty => 0,
} }
} }
@@ -76,24 +79,21 @@ impl RegisterContent {
match self { match self {
Self::Span(s) => s.is_empty(), Self::Span(s) => s.is_empty(),
Self::Line(s) => s.is_empty(), Self::Line(s) => s.is_empty(),
Self::Block(s) => s.is_empty(),
Self::Empty => true, Self::Empty => true,
} }
} }
pub fn is_block(&self) -> bool {
matches!(self, Self::Block(_))
}
pub fn is_line(&self) -> bool { pub fn is_line(&self) -> bool {
matches!(self, Self::Line(_)) matches!(self, Self::Line(_))
} }
pub fn is_span(&self) -> bool { pub fn is_span(&self) -> bool {
matches!(self, Self::Span(_)) 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 { 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 { impl Register {
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
content: RegisterContent::Span(String::new()), content: RegisterContent::Empty,
} }
} }
pub fn content(&self) -> &RegisterContent { pub fn content(&self) -> &RegisterContent {
@@ -247,13 +247,16 @@ impl Register {
pub fn write(&mut self, buf: RegisterContent) { pub fn write(&mut self, buf: RegisterContent) {
self.content = buf self.content = buf
} }
pub fn append(&mut self, buf: RegisterContent) { pub fn append(&mut self, mut buf: RegisterContent) {
match buf { match buf {
RegisterContent::Empty => {} 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::Empty => self.content = buf,
RegisterContent::Span(existing) => existing.push_str(s), RegisterContent::Span(existing) |
RegisterContent::Line(existing) => existing.push_str(s), RegisterContent::Line(existing) |
RegisterContent::Block(existing) => existing.append(s),
}, },
} }
} }

View File

@@ -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 { 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(); let max_num_len = total_lines.to_string().len();
s.lines() lines
.into_iter()
.enumerate() .enumerate()
.fold(String::new(), |mut acc, (i, ln)| { .fold(String::new(), |mut acc, (i, ln)| {
if i == 0 { if i == 0 {

View File

@@ -23,7 +23,7 @@ macro_rules! vi_test {
vi.feed_bytes($op.as_bytes()); vi.feed_bytes($op.as_bytes());
vi.process_input().unwrap(); 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); assert_eq!(vi.editor.cursor.get(), $expected_cursor);
} }
)* )*
@@ -512,7 +512,7 @@ fn vi_auto_indent() {
} }
assert_eq!( 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}" "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}"
); );
} }

View File

@@ -158,12 +158,10 @@ impl ViCmd {
}) && self.motion.is_none() }) && self.motion.is_none()
} }
pub fn is_line_motion(&self) -> bool { pub fn is_line_motion(&self) -> bool {
self.motion.as_ref().is_some_and(|m| { self
matches!( .motion
m.1, .as_ref()
Motion::LineUp | Motion::LineDown .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 /// If a ViCmd has a linewise motion, but no verb, we change it to charwise
pub fn is_mode_transition(&self) -> bool { pub fn is_mode_transition(&self) -> bool {
@@ -380,10 +378,7 @@ impl Motion {
) )
} }
pub fn is_linewise(&self) -> bool { pub fn is_linewise(&self) -> bool {
matches!( matches!(self, Self::WholeLine | Self::LineUp | Self::LineDown)
self,
Self::WholeLine | Self::LineUp | Self::LineDown
)
} }
} }

View File

@@ -46,7 +46,6 @@ impl ExEditor {
history, history,
..Default::default() ..Default::default()
}; };
new.buf.update_graphemes();
new new
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
@@ -56,19 +55,19 @@ impl ExEditor {
cmd.verb().is_none() cmd.verb().is_none()
&& (cmd && (cmd
.motion() .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) && self.buf.start_of_line() == 0)
|| (cmd || (cmd
.motion() .motion()
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDown)))
&& self.buf.end_of_line() == self.buf.cursor_max()) && self.buf.on_last_line())
} }
pub fn scroll_history(&mut self, cmd: ViCmd) { pub fn scroll_history(&mut self, cmd: ViCmd) {
let count = &cmd.motion().unwrap().0; let count = &cmd.motion().unwrap().0;
let motion = &cmd.motion().unwrap().1; let motion = &cmd.motion().unwrap().1;
let count = match motion { let count = match motion {
Motion::LineUpCharwise => -(*count as isize), Motion::LineUp => -(*count as isize),
Motion::LineDownCharwise => *count as isize, Motion::LineDown => *count as isize,
_ => unreachable!(), _ => unreachable!(),
}; };
let entry = self.history.scroll(count); let entry = self.history.scroll(count);
@@ -88,7 +87,6 @@ impl ExEditor {
let Some(mut cmd) = self.mode.handle_key(key) else { let Some(mut cmd) = self.mode.handle_key(key) else {
return Ok(()); return Ok(());
}; };
cmd.alter_line_motion_if_no_verb();
log::debug!("ExEditor got cmd: {:?}", cmd); log::debug!("ExEditor got cmd: {:?}", cmd);
if self.should_grab_history(&cmd) { if self.should_grab_history(&cmd) {
log::debug!("Grabbing history for cmd: {:?}", 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}; use crate::readline::keys::{KeyCode as C, KeyEvent as E, ModKeys as M};
match key { match key {
E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => { E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => {
let input = self.pending_cmd.buf.as_str(); let input = self.pending_cmd.buf.joined();
match parse_ex_cmd(input) { match parse_ex_cmd(&input) {
Ok(cmd) => Ok(cmd), Ok(cmd) => Ok(cmd),
Err(e) => { 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())); write_meta(|m| m.post_system_message(msg.clone()));
Err(ShErr::simple(ShErrKind::ParseErr, msg)) Err(ShErr::simple(ShErrKind::ParseErr, msg))
} }
@@ -167,7 +165,7 @@ impl ViMode for ViEx {
} }
fn pending_seq(&self) -> Option<String> { fn pending_seq(&self) -> Option<String> {
Some(self.pending_cmd.buf.as_str().to_string()) Some(self.pending_cmd.buf.joined())
} }
fn pending_cursor(&self) -> Option<usize> { fn pending_cursor(&self) -> Option<usize> {

View File

@@ -3,6 +3,7 @@ use std::str::Chars;
use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds}; use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::linebuf::Grapheme;
use crate::readline::vicmd::{ use crate::readline::vicmd::{
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
VerbCmd, ViCmd, Word, VerbCmd, ViCmd, Word,
@@ -197,7 +198,7 @@ impl ViNormal {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(count, Verb::Change)), verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::WholeLineExclusive)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}); });
@@ -411,10 +412,10 @@ impl ViNormal {
| ('~', Some(VerbCmd(_, Verb::ToggleCaseRange))) | ('~', Some(VerbCmd(_, Verb::ToggleCaseRange)))
| ('>', Some(VerbCmd(_, Verb::Indent))) | ('>', Some(VerbCmd(_, Verb::Indent)))
| ('<', Some(VerbCmd(_, Verb::Dedent))) => { | ('<', 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))) => { ('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))) => { ('W', Some(VerbCmd(_, Verb::Change))) => {
// Same with 'W' // Same with 'W'
@@ -535,7 +536,7 @@ impl ViNormal {
break 'motion_parse Some(MotionCmd( break 'motion_parse Some(MotionCmd(
count, count,
Motion::CharSearch(Direction::Forward, Dest::On, *ch), Motion::CharSearch(Direction::Forward, Dest::On, Grapheme::from(*ch)),
)); ));
} }
'F' => { 'F' => {
@@ -545,7 +546,7 @@ impl ViNormal {
break 'motion_parse Some(MotionCmd( break 'motion_parse Some(MotionCmd(
count, count,
Motion::CharSearch(Direction::Backward, Dest::On, *ch), Motion::CharSearch(Direction::Backward, Dest::On, Grapheme::from(*ch)),
)); ));
} }
't' => { 't' => {
@@ -555,7 +556,7 @@ impl ViNormal {
break 'motion_parse Some(MotionCmd( break 'motion_parse Some(MotionCmd(
count, count,
Motion::CharSearch(Direction::Forward, Dest::Before, *ch), Motion::CharSearch(Direction::Forward, Dest::Before, Grapheme::from(*ch)),
)); ));
} }
'T' => { 'T' => {
@@ -565,7 +566,7 @@ impl ViNormal {
break 'motion_parse Some(MotionCmd( break 'motion_parse Some(MotionCmd(
count, count,
Motion::CharSearch(Direction::Backward, Dest::Before, *ch), Motion::CharSearch(Direction::Backward, Dest::Before, Grapheme::from(*ch)),
)); ));
} }
';' => { ';' => {

View File

@@ -3,6 +3,7 @@ use std::str::Chars;
use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds}; use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::linebuf::Grapheme;
use crate::readline::vicmd::{ use crate::readline::vicmd::{
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
VerbCmd, ViCmd, Word, VerbCmd, ViCmd, Word,
@@ -146,7 +147,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Delete)), verb: Some(VerbCmd(1, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -155,7 +156,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Yank)), verb: Some(VerbCmd(1, Verb::Yank)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -164,7 +165,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Delete)), verb: Some(VerbCmd(1, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -173,7 +174,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Change)), verb: Some(VerbCmd(1, Verb::Change)),
motion: Some(MotionCmd(1, Motion::WholeLineExclusive)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -182,7 +183,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Indent)), verb: Some(VerbCmd(1, Verb::Indent)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -191,7 +192,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Dedent)), verb: Some(VerbCmd(1, Verb::Dedent)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -200,7 +201,7 @@ impl ViVisual {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(1, Verb::Equalize)), verb: Some(VerbCmd(1, Verb::Equalize)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
@@ -344,10 +345,10 @@ impl ViVisual {
| ('=', Some(VerbCmd(_, Verb::Equalize))) | ('=', Some(VerbCmd(_, Verb::Equalize)))
| ('>', Some(VerbCmd(_, Verb::Indent))) | ('>', Some(VerbCmd(_, Verb::Indent)))
| ('<', Some(VerbCmd(_, Verb::Dedent))) => { | ('<', 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))) => { ('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( break 'motion_parse Some(MotionCmd(
count, count,
Motion::CharSearch(Direction::Forward, Dest::On, *ch), Motion::CharSearch(Direction::Forward, Dest::On, Grapheme::from(*ch)),
)); ));
} }
'F' => { 'F' => {
@@ -435,7 +436,7 @@ impl ViVisual {
break 'motion_parse Some(MotionCmd( break 'motion_parse Some(MotionCmd(
count, count,
Motion::CharSearch(Direction::Backward, Dest::On, *ch), Motion::CharSearch(Direction::Backward, Dest::On, Grapheme::from(*ch)),
)); ));
} }
't' => { 't' => {
@@ -445,7 +446,7 @@ impl ViVisual {
break 'motion_parse Some(MotionCmd( break 'motion_parse Some(MotionCmd(
count, count,
Motion::CharSearch(Direction::Forward, Dest::Before, *ch), Motion::CharSearch(Direction::Forward, Dest::Before, Grapheme::from(*ch)),
)); ));
} }
'T' => { 'T' => {
@@ -455,7 +456,7 @@ impl ViVisual {
break 'motion_parse Some(MotionCmd( break 'motion_parse Some(MotionCmd(
count, count,
Motion::CharSearch(Direction::Backward, Dest::Before, *ch), Motion::CharSearch(Direction::Backward, Dest::Before, Grapheme::from(*ch)),
)); ));
} }
';' => { ';' => {