reimplemented ex-mode widget/function execution

This commit is contained in:
2026-03-20 12:36:57 -04:00
parent 392506d414
commit 6f44759deb
8 changed files with 139 additions and 89 deletions

12
Cargo.lock generated
View File

@@ -224,6 +224,17 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 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]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.1.5" version = "0.1.5"
@@ -568,6 +579,7 @@ dependencies = [
"bitflags", "bitflags",
"clap", "clap",
"env_logger", "env_logger",
"fnmatch-regex",
"glob", "glob",
"itertools", "itertools",
"log", "log",

View File

@@ -14,6 +14,7 @@ ariadne = "0.6.0"
bitflags = "2.8.0" bitflags = "2.8.0"
clap = { version = "4.5.38", features = ["derive"] } clap = { version = "4.5.38", features = ["derive"] }
env_logger = "0.11.9" env_logger = "0.11.9"
fnmatch-regex = "0.3.0"
glob = "0.3.2" glob = "0.3.2"
itertools = "0.14.0" itertools = "0.14.0"
log = "0.4.29" log = "0.4.29"

View File

@@ -865,15 +865,15 @@ impl QueryEditor {
self.available_width = width; self.available_width = width;
} }
pub fn update_scroll_offset(&mut self) { 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 { 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) { if cursor_pos >= self.scroll_offset + self.available_width.saturating_sub(1) {
self.scroll_offset = self self.scroll_offset = self
.linebuf .linebuf
.cursor .cursor_to_flat()
.ret_sub(self.available_width.saturating_sub(1)); .saturating_sub(self.available_width.saturating_sub(1));
} }
let max_offset = self let max_offset = self
.linebuf .linebuf
@@ -1257,8 +1257,7 @@ impl FuzzySelector {
let cursor_in_window = self let cursor_in_window = self
.query .query
.linebuf .linebuf
.cursor .cursor_to_flat()
.get()
.saturating_sub(self.query.scroll_offset); .saturating_sub(self.query.scroll_offset);
let cursor_col = (cursor_in_window + 4) as u16; let cursor_col = (cursor_in_window + 4) as u16;
write!(buf, "\x1b[{}A\r\x1b[{}C", lines_below_prompt, cursor_col).unwrap(); write!(buf, "\x1b[{}A\r\x1b[{}C", lines_below_prompt, cursor_col).unwrap();

View File

@@ -317,7 +317,7 @@ impl History {
pub fn update_pending_cmd(&mut self, buf: (&str, usize)) { pub fn update_pending_cmd(&mut self, buf: (&str, usize)) {
let cursor_pos = if let Some(pending) = &self.pending { let cursor_pos = if let Some(pending) = &self.pending {
pending.cursor.get() pending.cursor_to_flat()
} else { } else {
buf.1 buf.1
}; };
@@ -329,7 +329,7 @@ impl History {
if let Some(pending) = &mut self.pending { if let Some(pending) = &mut self.pending {
pending.set_buffer(cmd); pending.set_buffer(cmd);
pending.cursor.set(cursor_pos); pending.set_cursor_from_flat(cursor_pos);
} else { } else {
self.pending = Some(LineBuf::new().with_initial(&cmd, cursor_pos)); self.pending = Some(LineBuf::new().with_initial(&cmd, cursor_pos));
} }

View File

@@ -1,7 +1,5 @@
use std::{ use std::{
fmt::Display, collections::HashSet, fmt::Display, ops::{Index, IndexMut}, slice::SliceIndex
ops::{Index, IndexMut},
slice::SliceIndex,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -14,11 +12,11 @@ use super::vicmd::{
}; };
use crate::{ use crate::{
expand::expand_cmd_sub, expand::expand_cmd_sub,
libsh::error::ShResult, libsh::{error::ShResult, guards::{RawModeGuard, var_ctx_guard}},
parse::{ parse::{
Redir, RedirType, Redir, RedirType,
execute::exec_input, execute::exec_input,
lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, lex::{LexFlags, LexStream, Tk, TkFlags},
}, },
prelude::*, prelude::*,
procio::{IoFrame, IoMode, IoStack}, procio::{IoFrame, IoMode, IoStack},
@@ -26,7 +24,7 @@ use crate::{
markers, markers,
register::RegisterContent, vicmd::{ReadSrc, VerbCmd, WriteDest}, register::RegisterContent, vicmd::{ReadSrc, VerbCmd, WriteDest},
}, },
state::{read_vars, write_meta}, state::{VarFlags, VarKind, read_vars, write_meta, write_vars},
}; };
const PUNCTUATION: [&str; 3] = ["?", "!", "."]; 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)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SelectMode { pub enum SelectMode {
Char(Pos), Char(Pos),
@@ -343,7 +315,10 @@ impl Pos {
row: usize::MAX, row: usize::MAX,
col: 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 { pub fn row_col_add(&self, row: isize, col: isize) -> Self {
Self { Self {
@@ -412,21 +387,6 @@ pub struct Cursor {
pub exclusive: bool, 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)] #[derive(Default, Clone, Debug)]
pub struct Edit { pub struct Edit {
pub old_cursor: Pos, pub old_cursor: Pos,
@@ -715,7 +675,66 @@ impl LineBuf {
col, 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("<ex-mode-cmd>".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(()) Ok(())
} }
fn insert_at(&mut self, pos: Pos, gr: Grapheme) { fn insert_at(&mut self, pos: Pos, gr: Grapheme) {
@@ -1064,10 +1083,15 @@ impl LineBuf {
match obj { match obj {
// text structures // text structures
TextObj::Word(word, bound) => self.text_obj_word(count, word, obj, bound), TextObj::Word(word, bound) => self.text_obj_word(count, word, obj, bound),
TextObj::Sentence(direction) => todo!(), TextObj::Sentence(_) |
TextObj::Paragraph(direction) => todo!(), TextObj::Paragraph(_) |
TextObj::WholeSentence(bound) => todo!(), TextObj::WholeSentence(_) |
TextObj::WholeParagraph(bound) => todo!(), TextObj::Tag(_) |
TextObj::Custom(_) |
TextObj::WholeParagraph(_) => {
log::warn!("{:?} text objects are not implemented yet", obj);
None
}
// quote stuff // quote stuff
TextObj::DoubleQuote(bound) | TextObj::DoubleQuote(bound) |
@@ -1081,9 +1105,6 @@ impl LineBuf {
| TextObj::Bracket(bound) | TextObj::Bracket(bound)
| TextObj::Brace(bound) | TextObj::Brace(bound)
| TextObj::Angle(bound) => self.text_obj_delim(count, obj, bound), | TextObj::Angle(bound) => self.text_obj_delim(count, obj, bound),
TextObj::Tag(bound) => todo!(),
TextObj::Custom(_) => todo!(),
} }
} }
fn text_obj_word( fn text_obj_word(
@@ -1736,10 +1757,13 @@ impl LineBuf {
let (s, e) = ordered(*s, *e); let (s, e) = ordered(*s, *e);
Some(MotionKind::Block { start: s, end: e }) Some(MotionKind::Block { start: s, end: e })
} }
Motion::RepeatMotion => todo!(), Motion::RepeatMotion |
Motion::RepeatMotionRev => todo!(), Motion::RepeatMotionRev => unreachable!("Repeat motions should have been resolved in readline/mod.rs"),
Motion::Global(val) => todo!(), Motion::Global(val) |
Motion::NotGlobal(val) => todo!(), Motion::NotGlobal(val) => {
log::warn!("Global motions are not implemented yet (val: {:?})", val);
None
}
Motion::Null => None, Motion::Null => None,
}; };
@@ -1748,11 +1772,11 @@ impl LineBuf {
} }
fn move_to_start(&mut self, motion: MotionKind) { fn move_to_start(&mut self, motion: MotionKind) {
match motion { match motion {
MotionKind::Char { start, end, inclusive } => { MotionKind::Char { start, end, .. } => {
let (s,_) = ordered(start, end); let (s,_) = ordered(start, end);
self.set_cursor(s); self.set_cursor(s);
} }
MotionKind::Line { start, end, inclusive } => { MotionKind::Line { start, end, .. } => {
let (s,_) = ordered(start, end); let (s,_) = ordered(start, end);
self.set_cursor(Pos { row: s, col: 0 }); self.set_cursor(Pos { row: s, col: 0 });
} }
@@ -2250,7 +2274,7 @@ impl LineBuf {
let line_len = self.line(row).len(); 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() // 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 // 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_start > level_end, the line has an closer
// if level_end > level_start, the line has a opener // if level_end > level_start, the line has a opener
@@ -2758,10 +2782,28 @@ impl LineBuf {
offset + pos.col.min(self.lines[row].len()) 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 { pub fn cursor_to_flat(&self) -> usize {
self.pos_to_flat(self.cursor.pos) 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. /// Compat shim: attempt history expansion. Stub that returns false.
pub fn attempt_history_expansion(&mut self, _history: &super::history::History) -> bool { pub fn attempt_history_expansion(&mut self, _history: &super::history::History) -> bool {
// TODO: implement history expansion for 2D buffer // TODO: implement history expansion for 2D buffer

View File

@@ -343,7 +343,7 @@ impl ShedVi {
self.editor = LineBuf::new().with_initial(initial, 0); self.editor = LineBuf::new().with_initial(initial, 0);
{ {
let s = self.editor.joined(); 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.history.update_pending_cmd((&s, c));
} }
self self
@@ -486,7 +486,7 @@ impl ShedVi {
self self
.history .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); self.editor.set_hint(None);
{ {
let mut writer = std::mem::take(&mut self.writer); let mut writer = std::mem::take(&mut self.writer);
@@ -548,7 +548,7 @@ impl ShedVi {
let new_cursor = span_start + candidate.len(); let new_cursor = span_start + candidate.len();
let line = self.completer.get_completed_line(&candidate); let line = self.completer.get_completed_line(&candidate);
self.focused_editor().set_buffer(line); 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. // Don't reset yet — clear() needs old_layout to erase the selector.
if !self.history.at_pending() { if !self.history.at_pending() {
@@ -556,7 +556,7 @@ impl ShedVi {
} }
self self
.history .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(); 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)?;
@@ -684,7 +684,7 @@ impl ShedVi {
} }
self self
.history .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; self.needs_redraw = true;
return Ok(None); return Ok(None);
} }
@@ -728,14 +728,14 @@ impl ShedVi {
.unwrap_or_default(); .unwrap_or_default();
self.focused_editor().set_buffer(line.clone()); 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() { if !self.history.at_pending() {
self.history.reset_to_pending(); self.history.reset_to_pending();
} }
self self
.history .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(); let hint = self.history.get_hint();
self.editor.set_hint(hint); self.editor.set_hint(hint);
write_vars(|v| { write_vars(|v| {
@@ -804,7 +804,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.joined(), self.editor.cursor.get())); .update_pending_cmd((&self.editor.joined(), self.editor.cursor_to_flat()));
self.editor.set_hint(None); self.editor.set_hint(None);
} }
None => { None => {
@@ -880,7 +880,7 @@ impl ShedVi {
} }
self.editor.set_hint(None); 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.print_line(true)?;
self.writer.flush_write("\n")?; self.writer.flush_write("\n")?;
let buf = self.editor.take_buf(); let buf = self.editor.take_buf();
@@ -931,7 +931,7 @@ impl ShedVi {
if before != after { if before != after {
self self
.history .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 { } else if before == after && has_edit_verb {
self.writer.send_bell().ok(); self.writer.send_bell().ok();
} }

View File

@@ -497,7 +497,7 @@ vi_test! {
vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4; vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4;
vi_vw_doesnt_crash : "" => "vw" => "", 0; vi_vw_doesnt_crash : "" => "vw" => "", 0;
vi_indent_cursor_pos : "echo foo" => ">>" => "\techo foo", 1; 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] #[test]

View File

@@ -42,11 +42,10 @@ struct ExEditor {
impl ExEditor { impl ExEditor {
pub fn new(history: History) -> Self { pub fn new(history: History) -> Self {
let mut new = Self { Self {
history, history,
..Default::default() ..Default::default()
}; }
new
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
*self = Self::default() *self = Self::default()
@@ -169,7 +168,7 @@ impl ViMode for ViEx {
} }
fn pending_cursor(&self) -> Option<usize> { fn pending_cursor(&self) -> Option<usize> {
Some(self.pending_cmd.buf.cursor.get()) Some(self.pending_cmd.buf.cursor_to_flat())
} }
fn move_cursor_on_undo(&self) -> bool { fn move_cursor_on_undo(&self) -> bool {
@@ -229,9 +228,6 @@ fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>, Option<String>> {
/// Unescape shell command arguments /// Unescape shell command arguments
fn unescape_shell_cmd(cmd: &str) -> String { 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 result = String::new();
let mut chars = cmd.chars().peekable(); let mut chars = cmd.chars().peekable();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {