From 6f44759deb20c879554cb9dcc32eae127d435374 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Fri, 20 Mar 2026 12:36:57 -0400 Subject: [PATCH] reimplemented ex-mode widget/function execution --- Cargo.lock | 12 +++ Cargo.toml | 1 + src/readline/complete.rs | 11 ++- src/readline/history.rs | 4 +- src/readline/linebuf.rs | 168 ++++++++++++++++++++++++-------------- src/readline/mod.rs | 20 ++--- src/readline/tests.rs | 2 +- src/readline/vimode/ex.rs | 10 +-- 8 files changed, 139 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f73516..cb162b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,6 +224,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fnmatch-regex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f319c7da34eac5f0b8c7220a4afb2e1ddde0c24ae87c7435a8e36dcd62a43a3" +dependencies = [ + "anyhow", + "itertools", + "regex", +] + [[package]] name = "foldhash" version = "0.1.5" @@ -568,6 +579,7 @@ dependencies = [ "bitflags", "clap", "env_logger", + "fnmatch-regex", "glob", "itertools", "log", diff --git a/Cargo.toml b/Cargo.toml index 4c10567..11d8ec0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ ariadne = "0.6.0" bitflags = "2.8.0" clap = { version = "4.5.38", features = ["derive"] } env_logger = "0.11.9" +fnmatch-regex = "0.3.0" glob = "0.3.2" itertools = "0.14.0" log = "0.4.29" diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 155a916..c617334 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -865,15 +865,15 @@ impl QueryEditor { self.available_width = width; } pub fn update_scroll_offset(&mut self) { - let cursor_pos = self.linebuf.cursor.get(); + let cursor_pos = self.linebuf.cursor_to_flat(); if cursor_pos < self.scroll_offset + 1 { - self.scroll_offset = self.linebuf.cursor.ret_sub(1); + self.scroll_offset = self.linebuf.cursor_to_flat().saturating_sub(1) } if cursor_pos >= self.scroll_offset + self.available_width.saturating_sub(1) { self.scroll_offset = self .linebuf - .cursor - .ret_sub(self.available_width.saturating_sub(1)); + .cursor_to_flat() + .saturating_sub(self.available_width.saturating_sub(1)); } let max_offset = self .linebuf @@ -1257,8 +1257,7 @@ impl FuzzySelector { let cursor_in_window = self .query .linebuf - .cursor - .get() + .cursor_to_flat() .saturating_sub(self.query.scroll_offset); let cursor_col = (cursor_in_window + 4) as u16; write!(buf, "\x1b[{}A\r\x1b[{}C", lines_below_prompt, cursor_col).unwrap(); diff --git a/src/readline/history.rs b/src/readline/history.rs index 0552690..2deeebc 100644 --- a/src/readline/history.rs +++ b/src/readline/history.rs @@ -317,7 +317,7 @@ impl History { pub fn update_pending_cmd(&mut self, buf: (&str, usize)) { let cursor_pos = if let Some(pending) = &self.pending { - pending.cursor.get() + pending.cursor_to_flat() } else { buf.1 }; @@ -329,7 +329,7 @@ impl History { if let Some(pending) = &mut self.pending { pending.set_buffer(cmd); - pending.cursor.set(cursor_pos); + pending.set_cursor_from_flat(cursor_pos); } else { self.pending = Some(LineBuf::new().with_initial(&cmd, cursor_pos)); } diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index 0b0f253..411c118 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -1,7 +1,5 @@ use std::{ - fmt::Display, - ops::{Index, IndexMut}, - slice::SliceIndex, + collections::HashSet, fmt::Display, ops::{Index, IndexMut}, slice::SliceIndex }; use smallvec::SmallVec; @@ -14,11 +12,11 @@ use super::vicmd::{ }; use crate::{ expand::expand_cmd_sub, - libsh::error::ShResult, + libsh::{error::ShResult, guards::{RawModeGuard, var_ctx_guard}}, parse::{ Redir, RedirType, execute::exec_input, - lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, + lex::{LexFlags, LexStream, Tk, TkFlags}, }, prelude::*, procio::{IoFrame, IoMode, IoStack}, @@ -26,7 +24,7 @@ use crate::{ markers, register::RegisterContent, vicmd::{ReadSrc, VerbCmd, WriteDest}, }, - state::{read_vars, write_meta}, + state::{VarFlags, VarKind, read_vars, write_meta, write_vars}, }; const PUNCTUATION: [&str; 3] = ["?", "!", "."]; @@ -298,32 +296,6 @@ impl From<&Grapheme> for CharClass { } } -fn is_whitespace(a: &Grapheme) -> bool { - CharClass::from(a) == CharClass::Whitespace -} - -fn is_other_class(a: &Grapheme, b: &Grapheme) -> bool { - let a = CharClass::from(a); - let b = CharClass::from(b); - a != b -} - -fn is_other_class_not_ws(a: &Grapheme, b: &Grapheme) -> bool { - if is_whitespace(a) || is_whitespace(b) { - false - } else { - is_other_class(a, b) - } -} - -fn is_other_class_or_is_ws(a: &Grapheme, b: &Grapheme) -> bool { - if is_whitespace(a) || is_whitespace(b) { - true - } else { - is_other_class(a, b) - } -} - #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum SelectMode { Char(Pos), @@ -343,7 +315,10 @@ impl Pos { row: usize::MAX, col: usize::MAX, }; - pub const MIN: Self = Pos { row: 0, col: 0 }; + pub const MIN: Self = Pos { + row: usize::MIN, // just in case we discover something smaller than '0' + col: usize::MIN, + }; pub fn row_col_add(&self, row: isize, col: isize) -> Self { Self { @@ -412,21 +387,6 @@ pub struct Cursor { 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 old_cursor: Pos, @@ -715,7 +675,66 @@ impl LineBuf { col, }; } - fn verb_shell_cmd(&self, cmd: &str) -> ShResult<()> { + fn verb_shell_cmd(&mut self, cmd: &str) -> ShResult<()> { + let mut vars = HashSet::new(); + vars.insert("_BUFFER".into()); + vars.insert("_CURSOR".into()); + vars.insert("_ANCHOR".into()); + let _guard = var_ctx_guard(vars); + + let mut buf = self.joined(); + let mut cursor = self.cursor_to_flat(); + let mut anchor = self.select_mode.map(|r| { + match r { + SelectMode::Char(pos) | + SelectMode::Block(pos) | + SelectMode::Line(pos) => { + self.pos_to_flat(pos).to_string() + } + } + }).unwrap_or_default(); + + write_vars(|v| { + v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?; + v.set_var( + "_CURSOR", + VarKind::Str(cursor.to_string()), + VarFlags::EXPORT, + )?; + v.set_var( + "_ANCHOR", + VarKind::Str(anchor.clone()), + VarFlags::EXPORT, + ) + })?; + + RawModeGuard::with_cooked_mode(|| exec_input(cmd.to_string(), None, true, Some("".into())))?; + + let keys = write_vars(|v| { + buf = v.take_var("_BUFFER"); + cursor = v.take_var("_CURSOR").parse().unwrap_or(cursor); + anchor = v.take_var("_ANCHOR"); + v.take_var("_KEYS") + }); + + self.set_buffer(buf); + self.set_cursor_from_flat(cursor); + if let Ok(pos) = anchor.parse() + && pos != cursor + && self.select_mode.is_some() { + let new_pos = self.pos_from_flat(pos); + match self.select_mode.as_mut() { + Some(SelectMode::Line(pos)) | + Some(SelectMode::Block(pos)) | + Some(SelectMode::Char(pos)) => { + *pos = new_pos + } + None => unreachable!() + } + } + if !keys.is_empty() { + write_meta(|m| m.set_pending_widget_keys(&keys)) + } Ok(()) } fn insert_at(&mut self, pos: Pos, gr: Grapheme) { @@ -1064,10 +1083,15 @@ impl LineBuf { match obj { // text structures TextObj::Word(word, bound) => self.text_obj_word(count, word, obj, bound), - TextObj::Sentence(direction) => todo!(), - TextObj::Paragraph(direction) => todo!(), - TextObj::WholeSentence(bound) => todo!(), - TextObj::WholeParagraph(bound) => todo!(), + TextObj::Sentence(_) | + TextObj::Paragraph(_) | + TextObj::WholeSentence(_) | + TextObj::Tag(_) | + TextObj::Custom(_) | + TextObj::WholeParagraph(_) => { + log::warn!("{:?} text objects are not implemented yet", obj); + None + } // quote stuff TextObj::DoubleQuote(bound) | @@ -1081,9 +1105,6 @@ impl LineBuf { | TextObj::Bracket(bound) | TextObj::Brace(bound) | TextObj::Angle(bound) => self.text_obj_delim(count, obj, bound), - - TextObj::Tag(bound) => todo!(), - TextObj::Custom(_) => todo!(), } } fn text_obj_word( @@ -1736,10 +1757,13 @@ impl LineBuf { let (s, e) = ordered(*s, *e); Some(MotionKind::Block { start: s, end: e }) } - Motion::RepeatMotion => todo!(), - Motion::RepeatMotionRev => todo!(), - Motion::Global(val) => todo!(), - Motion::NotGlobal(val) => todo!(), + Motion::RepeatMotion | + Motion::RepeatMotionRev => unreachable!("Repeat motions should have been resolved in readline/mod.rs"), + Motion::Global(val) | + Motion::NotGlobal(val) => { + log::warn!("Global motions are not implemented yet (val: {:?})", val); + None + } Motion::Null => None, }; @@ -1748,11 +1772,11 @@ impl LineBuf { } fn move_to_start(&mut self, motion: MotionKind) { match motion { - MotionKind::Char { start, end, inclusive } => { + MotionKind::Char { start, end, .. } => { let (s,_) = ordered(start, end); self.set_cursor(s); } - MotionKind::Line { start, end, inclusive } => { + MotionKind::Line { start, end, .. } => { let (s,_) = ordered(start, end); self.set_cursor(Pos { row: s, col: 0 }); } @@ -2250,7 +2274,7 @@ impl LineBuf { let line_len = self.line(row).len(); // we are going to calculate the level twice, once at column = 0 and once at column = line.len() - // "b-b-b-b-but the performance" i dont care. open a pull request genius + // "b-b-b-b-but the performance" i dont care // the number of tabs we use for the line is the lesser of these two calculations // if level_start > level_end, the line has an closer // if level_end > level_start, the line has a opener @@ -2758,10 +2782,28 @@ impl LineBuf { offset + pos.col.min(self.lines[row].len()) } + fn pos_from_flat(&self, mut flat: usize) -> Pos { + for (i, line) in self.lines.iter().enumerate() { + if flat <= line.len() { + return Pos { row: i, col: flat }; + } + flat = flat.saturating_sub(line.len() + 1); // +1 for '\n' + } + // If we exceed the total length, clamp to end + let last_row = self.lines.len().saturating_sub(1); + let last_col = self.lines[last_row].len(); + Pos { row: last_row, col: last_col } + } + pub fn cursor_to_flat(&self) -> usize { self.pos_to_flat(self.cursor.pos) } + pub fn set_cursor_from_flat(&mut self, flat: usize) { + self.cursor.pos = self.pos_from_flat(flat); + self.fix_cursor(); + } + /// 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 diff --git a/src/readline/mod.rs b/src/readline/mod.rs index a341e2a..67bf0be 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -343,7 +343,7 @@ impl ShedVi { self.editor = LineBuf::new().with_initial(initial, 0); { let s = self.editor.joined(); - let c = self.editor.cursor.get(); + let c = self.editor.cursor_to_flat(); self.history.update_pending_cmd((&s, c)); } self @@ -486,7 +486,7 @@ impl ShedVi { self .history - .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); + .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat())); self.editor.set_hint(None); { let mut writer = std::mem::take(&mut self.writer); @@ -548,7 +548,7 @@ impl ShedVi { let new_cursor = span_start + candidate.len(); let line = self.completer.get_completed_line(&candidate); self.focused_editor().set_buffer(line); - self.focused_editor().cursor.set(new_cursor); + self.focused_editor().set_cursor_from_flat(new_cursor); // Don't reset yet — clear() needs old_layout to erase the selector. if !self.history.at_pending() { @@ -556,7 +556,7 @@ impl ShedVi { } self .history - .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); + .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat())); let hint = self.history.get_hint(); self.editor.set_hint(hint); self.completer.clear(&mut self.writer)?; @@ -684,7 +684,7 @@ impl ShedVi { } self .history - .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); + .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat())); self.needs_redraw = true; return Ok(None); } @@ -728,14 +728,14 @@ impl ShedVi { .unwrap_or_default(); self.focused_editor().set_buffer(line.clone()); - self.focused_editor().cursor.set(new_cursor); + self.focused_editor().set_cursor_from_flat(new_cursor); if !self.history.at_pending() { self.history.reset_to_pending(); } self .history - .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); + .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat())); let hint = self.history.get_hint(); self.editor.set_hint(hint); write_vars(|v| { @@ -804,7 +804,7 @@ impl ShedVi { self.focused_editor().move_cursor_to_end(); self .history - .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); + .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat())); self.editor.set_hint(None); } None => { @@ -880,7 +880,7 @@ impl ShedVi { } self.editor.set_hint(None); - self.editor.cursor.set(self.editor.cursor_max()); + self.editor.set_cursor_from_flat(self.editor.cursor_max()); self.print_line(true)?; self.writer.flush_write("\n")?; let buf = self.editor.take_buf(); @@ -931,7 +931,7 @@ impl ShedVi { if before != after { self .history - .update_pending_cmd((&self.editor.joined(), self.editor.cursor.get())); + .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat())); } else if before == after && has_edit_verb { self.writer.send_bell().ok(); } diff --git a/src/readline/tests.rs b/src/readline/tests.rs index b50487e..95ac4c7 100644 --- a/src/readline/tests.rs +++ b/src/readline/tests.rs @@ -497,7 +497,7 @@ vi_test! { vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4; vi_vw_doesnt_crash : "" => "vw" => "", 0; vi_indent_cursor_pos : "echo foo" => ">>" => "\techo foo", 1; - vi_join_indent_lines : "echo foo\n\t\techo bar" => "J" => "echo foo echo bar", 8 + vi_join_indent_lines : "echo foo\n\t\techo bar" => "J" => "echo foo echo bar", 8 } #[test] diff --git a/src/readline/vimode/ex.rs b/src/readline/vimode/ex.rs index 0e861de..9dfed65 100644 --- a/src/readline/vimode/ex.rs +++ b/src/readline/vimode/ex.rs @@ -42,11 +42,10 @@ struct ExEditor { impl ExEditor { pub fn new(history: History) -> Self { - let mut new = Self { + Self { history, ..Default::default() - }; - new + } } pub fn clear(&mut self) { *self = Self::default() @@ -169,7 +168,7 @@ impl ViMode for ViEx { } fn pending_cursor(&self) -> Option { - Some(self.pending_cmd.buf.cursor.get()) + Some(self.pending_cmd.buf.cursor_to_flat()) } fn move_cursor_on_undo(&self) -> bool { @@ -229,9 +228,6 @@ fn parse_ex_cmd(raw: &str) -> Result, Option> { /// Unescape shell command arguments fn unescape_shell_cmd(cmd: &str) -> String { - // The pest grammar uses double quotes for vicut commands - // So shell commands need to escape double quotes - // We will be removing a single layer of escaping from double quotes let mut result = String::new(); let mut chars = cmd.chars().peekable(); while let Some(ch) = chars.next() {