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"
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",

View File

@@ -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"

View File

@@ -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();

View File

@@ -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));
}

View File

@@ -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("<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(())
}
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

View File

@@ -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();
}

View File

@@ -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]

View File

@@ -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<usize> {
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<ViCmd>, Option<String>> {
/// 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() {