Compare commits
2 Commits
a43f8a6dde
...
bc30521e47
| Author | SHA1 | Date | |
|---|---|---|---|
| bc30521e47 | |||
| 490ce4571d |
@@ -250,6 +250,7 @@ impl Dispatcher {
|
||||
NdRule::CaseNode { .. } => self.exec_case(node)?,
|
||||
NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?,
|
||||
NdRule::FuncDef { .. } => self.exec_func_def(node)?,
|
||||
NdRule::Negate { .. } => self.exec_negated(node)?,
|
||||
NdRule::Command { .. } => self.dispatch_cmd(node)?,
|
||||
NdRule::Test { .. } => self.exec_test(node)?,
|
||||
_ => unreachable!(),
|
||||
@@ -284,6 +285,16 @@ impl Dispatcher {
|
||||
self.exec_cmd(node)
|
||||
}
|
||||
}
|
||||
pub fn exec_negated(&mut self, node: Node) -> ShResult<()> {
|
||||
let NdRule::Negate { cmd } = node.class else {
|
||||
unreachable!()
|
||||
};
|
||||
self.dispatch_node(*cmd)?;
|
||||
let status = state::get_status();
|
||||
state::set_status(if status == 0 { 1 } else { 0 });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> {
|
||||
let NdRule::Conjunction { elements } = conjunction.class else {
|
||||
unreachable!()
|
||||
@@ -578,6 +589,7 @@ impl Dispatcher {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state::set_status(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -714,9 +726,13 @@ impl Dispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
if !matched && !else_block.is_empty() {
|
||||
for node in else_block {
|
||||
s.dispatch_node(node)?;
|
||||
if !matched {
|
||||
if !else_block.is_empty() {
|
||||
for node in else_block {
|
||||
s.dispatch_node(node)?;
|
||||
}
|
||||
} else {
|
||||
state::set_status(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1195,3 +1211,191 @@ pub fn is_func(tk: Option<Tk>) -> bool {
|
||||
pub fn is_subsh(tk: Option<Tk>) -> bool {
|
||||
tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state;
|
||||
use crate::testutil::{TestGuard, test_input};
|
||||
|
||||
// ===================== while/until status =====================
|
||||
|
||||
#[test]
|
||||
fn while_loop_status_zero_after_completion() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("while false; do :; done").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn while_loop_status_zero_after_iterations() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("X=0; while [[ $X -lt 3 ]]; do X=$((X+1)); done").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn until_loop_status_zero_after_completion() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("until true; do :; done").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn until_loop_status_zero_after_iterations() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("X=3; until [[ $X -le 0 ]]; do X=$((X-1)); done").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn while_break_preserves_status() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("while true; do break; done").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn while_body_status_propagates() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("X=0; while [[ $X -lt 1 ]]; do X=$((X+1)); false; done").unwrap();
|
||||
// Loop body ended with `false` (status 1), but the loop itself
|
||||
// completed normally when the condition failed, so status should be 0
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== if/elif/else status =====================
|
||||
|
||||
#[test]
|
||||
fn if_true_body_status() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("if true; then echo ok; fi").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_false_no_else_status() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("if false; then echo ok; fi").unwrap();
|
||||
// No branch taken, POSIX says status is 0
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_else_branch_status() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("if false; then true; else false; fi").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
// ===================== for loop status =====================
|
||||
|
||||
#[test]
|
||||
fn for_loop_empty_list_status() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("for x in; do echo $x; done").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_loop_body_status() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("for x in a b c; do true; done").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== case status =====================
|
||||
|
||||
#[test]
|
||||
fn case_match_status() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("case foo in foo) true;; esac").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn case_no_match_status() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("case foo in bar) true;; esac").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
// ===================== other stuff =====================
|
||||
|
||||
#[test]
|
||||
fn for_loop_var_zip() {
|
||||
let g = TestGuard::new();
|
||||
test_input("for a b in 1 2 3 4 5 6; do echo $a $b; done").unwrap();
|
||||
let out = g.read_output();
|
||||
assert_eq!(out, "1 2\n3 4\n5 6\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_loop_unsets_zipped() {
|
||||
let g = TestGuard::new();
|
||||
test_input("for a b c d in 1 2 3 4 5 6; do echo $a $b $c $d; done").unwrap();
|
||||
let out = g.read_output();
|
||||
assert_eq!(out, "1 2 3 4\n5 6\n");
|
||||
}
|
||||
|
||||
// ===================== negation (!) status =====================
|
||||
|
||||
#[test]
|
||||
fn negate_true() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("! true").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negate_false() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("! false").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_negate_true() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("! ! true").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_negate_false() {
|
||||
let _g = TestGuard::new();
|
||||
test_input("! ! false").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negate_pipeline_last_cmd() {
|
||||
let _g = TestGuard::new();
|
||||
// pipeline status = last cmd (false) = 1, negated → 0
|
||||
test_input("! true | false").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negate_pipeline_last_cmd_true() {
|
||||
let _g = TestGuard::new();
|
||||
// pipeline status = last cmd (true) = 0, negated → 1
|
||||
test_input("! false | true").unwrap();
|
||||
assert_eq!(state::get_status(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negate_in_conjunction() {
|
||||
let _g = TestGuard::new();
|
||||
// ! binds to pipeline, not conjunction: (! (true && false)) && true
|
||||
test_input("! (true && false) && true").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negate_in_if_condition() {
|
||||
let g = TestGuard::new();
|
||||
test_input("if ! false; then echo yes; fi").unwrap();
|
||||
assert_eq!(state::get_status(), 0);
|
||||
assert_eq!(g.read_output(), "yes\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
pub const KEYWORDS: [&str; 16] = [
|
||||
pub const KEYWORDS: [&str; 17] = [
|
||||
"if", "then", "elif", "else", "fi", "while", "until", "select", "for", "in", "do", "done",
|
||||
"case", "esac", "[[", "]]",
|
||||
"case", "esac", "[[", "]]", "!"
|
||||
];
|
||||
|
||||
pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"];
|
||||
@@ -166,6 +166,7 @@ pub enum TkRule {
|
||||
ErrPipe,
|
||||
And,
|
||||
Or,
|
||||
Bang,
|
||||
Bg,
|
||||
Sep,
|
||||
Redir,
|
||||
@@ -250,6 +251,7 @@ pub struct LexStream {
|
||||
quote_state: QuoteState,
|
||||
brc_grp_depth: usize,
|
||||
brc_grp_start: Option<usize>,
|
||||
case_depth: usize,
|
||||
flags: LexFlags,
|
||||
}
|
||||
|
||||
@@ -271,7 +273,6 @@ bitflags! {
|
||||
/// The lexer has no more tokens to produce
|
||||
const STALE = 0b0001000000;
|
||||
const EXPECTING_IN = 0b0010000000;
|
||||
const IN_CASE = 0b0100000000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,6 +307,7 @@ impl LexStream {
|
||||
quote_state: QuoteState::default(),
|
||||
brc_grp_depth: 0,
|
||||
brc_grp_start: None,
|
||||
case_depth: 0,
|
||||
}
|
||||
}
|
||||
/// Returns a slice of the source input using the given range
|
||||
@@ -453,7 +455,7 @@ impl LexStream {
|
||||
let mut chars = slice.chars().peekable();
|
||||
let can_be_subshell = chars.peek() == Some(&'(');
|
||||
|
||||
if self.flags.contains(LexFlags::IN_CASE)
|
||||
if self.case_depth > 0
|
||||
&& let Some(count) = case_pat_lookahead(chars.clone())
|
||||
{
|
||||
pos += count;
|
||||
@@ -731,7 +733,7 @@ impl LexStream {
|
||||
"case" | "select" | "for" => {
|
||||
new_tk.mark(TkFlags::KEYWORD);
|
||||
self.flags |= LexFlags::EXPECTING_IN;
|
||||
self.flags |= LexFlags::IN_CASE;
|
||||
self.case_depth += 1;
|
||||
self.set_next_is_cmd(false);
|
||||
}
|
||||
"in" if self.flags.contains(LexFlags::EXPECTING_IN) => {
|
||||
@@ -739,8 +741,8 @@ impl LexStream {
|
||||
self.flags &= !LexFlags::EXPECTING_IN;
|
||||
}
|
||||
_ if is_keyword(text) => {
|
||||
if text == "esac" && self.flags.contains(LexFlags::IN_CASE) {
|
||||
self.flags &= !LexFlags::IN_CASE;
|
||||
if text == "esac" && self.case_depth > 0 {
|
||||
self.case_depth -= 1;
|
||||
}
|
||||
new_tk.mark(TkFlags::KEYWORD);
|
||||
}
|
||||
@@ -881,6 +883,14 @@ impl Iterator for LexStream {
|
||||
return self.next();
|
||||
}
|
||||
}
|
||||
'!' if self.next_is_cmd() => {
|
||||
self.cursor += 1;
|
||||
let tk_type = TkRule::Bang;
|
||||
|
||||
let mut tk = self.get_token((self.cursor - 1)..self.cursor, tk_type);
|
||||
tk.flags |= TkFlags::KEYWORD;
|
||||
tk
|
||||
}
|
||||
'|' => {
|
||||
let ch_idx = self.cursor;
|
||||
self.cursor += 1;
|
||||
|
||||
666
src/parse/mod.rs
666
src/parse/mod.rs
File diff suppressed because one or more lines are too long
@@ -217,18 +217,36 @@ pub struct History {
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
path: PathBuf::new(),
|
||||
pending: None,
|
||||
entries: Vec::new(),
|
||||
search_mask: Vec::new(),
|
||||
fuzzy_finder: FuzzySelector::new("History").number_candidates(true),
|
||||
no_matches: false,
|
||||
cursor: 0,
|
||||
//search_direction: Direction::Backward,
|
||||
ignore_dups: false,
|
||||
max_size: None,
|
||||
}
|
||||
}
|
||||
pub fn new() -> ShResult<Self> {
|
||||
let ignore_dups = crate::state::read_shopts(|s| s.core.hist_ignore_dupes);
|
||||
let max_hist = crate::state::read_shopts(|s| s.core.max_hist);
|
||||
|
||||
let path = PathBuf::from(env::var("SHEDHIST").unwrap_or({
|
||||
let home = env::var("HOME").unwrap();
|
||||
format!("{home}/.shed_history")
|
||||
}));
|
||||
|
||||
let mut entries = read_hist_file(&path)?;
|
||||
|
||||
// Enforce max_hist limit on loaded entries (negative = unlimited)
|
||||
if max_hist >= 0 && entries.len() > max_hist as usize {
|
||||
entries = entries.split_off(entries.len() - max_hist as usize);
|
||||
}
|
||||
|
||||
let search_mask = dedupe_entries(&entries);
|
||||
let cursor = search_mask.len();
|
||||
let max_size = if max_hist < 0 {
|
||||
@@ -236,6 +254,7 @@ impl History {
|
||||
} else {
|
||||
Some(max_hist as u32)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
entries,
|
||||
|
||||
@@ -823,7 +823,7 @@ impl LineBuf {
|
||||
}
|
||||
Some(self.line_bounds(line_no))
|
||||
}
|
||||
pub fn this_word(&mut self, word: Word) -> (usize, usize) {
|
||||
pub fn word_at(&mut self, pos: usize, word: Word) -> (usize,usize) {
|
||||
let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) {
|
||||
self.cursor.get()
|
||||
} else {
|
||||
@@ -835,7 +835,35 @@ impl LineBuf {
|
||||
self.end_of_word_forward(self.cursor.get(), word)
|
||||
};
|
||||
(start, end)
|
||||
}
|
||||
pub fn this_word(&mut self, word: Word) -> (usize, usize) {
|
||||
self.word_at(self.cursor.get(), word)
|
||||
}
|
||||
|
||||
pub fn number_at_cursor(&mut self) -> Option<(usize,usize)> {
|
||||
self.number_at(self.cursor.get())
|
||||
}
|
||||
pub fn number_at(&mut self, pos: usize) -> Option<(usize,usize)> {
|
||||
// A number is a sequence of digits, possibly containing one dot, and possibly starting with a minus sign
|
||||
let is_number_char = |c: &str| c == "." || c == "-" || c.chars().all(|c| c.is_ascii_digit());
|
||||
let is_digit = |gr: &str| gr.chars().all(|c| c.is_ascii_digit());
|
||||
if self.grapheme_at(pos).is_some_and(|gr| !is_number_char(gr)) {
|
||||
return None;
|
||||
}
|
||||
let mut fwd_indices = self.directional_indices_iter_from(pos, Direction::Forward);
|
||||
let mut bkwd_indices = self.directional_indices_iter_from(pos, Direction::Backward);
|
||||
|
||||
// Find the digit span, then check if preceded by '-'
|
||||
let mut start = bkwd_indices.find(|i| !self.grapheme_at(*i).is_some_and(is_digit))
|
||||
.map(|i| i + 1).unwrap_or(0);
|
||||
let end = fwd_indices.find(|i| !self.grapheme_at(*i).is_some_and(is_digit))
|
||||
.map(|i| i - 1).unwrap_or(self.cursor.max); // inclusive end
|
||||
|
||||
// Check for leading minus
|
||||
if start > 0 && self.grapheme_at(start - 1) == Some("-") { start -= 1; }
|
||||
|
||||
Some((start, end))
|
||||
}
|
||||
pub fn this_line_exclusive(&mut self) -> (usize, usize) {
|
||||
let line_no = self.cursor_line_number();
|
||||
let (start, mut end) = self.line_bounds(line_no);
|
||||
@@ -947,17 +975,14 @@ impl LineBuf {
|
||||
dir: Direction,
|
||||
) -> Box<dyn Iterator<Item = usize>> {
|
||||
self.update_graphemes_lazy();
|
||||
let skip = pos + 1;
|
||||
let len = self.grapheme_indices().len();
|
||||
match dir {
|
||||
Direction::Forward => Box::new(self.grapheme_indices().to_vec().into_iter().skip(skip))
|
||||
as Box<dyn Iterator<Item = usize>>,
|
||||
Direction::Backward => Box::new(self.grapheme_indices().to_vec().into_iter().take(pos).rev())
|
||||
as Box<dyn Iterator<Item = usize>>,
|
||||
Direction::Forward => Box::new(pos + 1..len) as Box<dyn Iterator<Item = usize>>,
|
||||
Direction::Backward => Box::new((0..pos).rev()) as Box<dyn Iterator<Item = usize>>,
|
||||
}
|
||||
}
|
||||
pub fn is_word_bound(&mut self, pos: usize, word: Word, dir: Direction) -> bool {
|
||||
let clamped_pos = ClampedUsize::new(pos, self.cursor.max, true);
|
||||
log::debug!("clamped_pos: {}", clamped_pos.get());
|
||||
let cur_char = self
|
||||
.grapheme_at(clamped_pos.get())
|
||||
.map(|c| c.to_string())
|
||||
@@ -1193,20 +1218,6 @@ impl LineBuf {
|
||||
Bound::Around => {
|
||||
// End excludes the quote, so push it forward
|
||||
end += 1;
|
||||
|
||||
// We also need to include any trailing whitespace
|
||||
let end_of_line = self.end_of_line();
|
||||
let remainder = end..end_of_line;
|
||||
for idx in remainder {
|
||||
let Some(gr) = self.grapheme_at(idx) else {
|
||||
break;
|
||||
};
|
||||
if is_whitespace(gr) {
|
||||
end += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1793,6 +1804,11 @@ impl LineBuf {
|
||||
/// Find the start of the current/previous word backward
|
||||
pub fn start_of_word_backward(&mut self, mut pos: usize, word: Word) -> usize {
|
||||
let default = 0;
|
||||
// In insert mode, cursor can be one past the last grapheme; step back so
|
||||
// grapheme_at(pos) doesn't return None and bail to 0
|
||||
if pos > 0 && self.grapheme_at(pos).is_none() {
|
||||
pos -= 1;
|
||||
}
|
||||
let mut indices_iter = (0..pos).rev().peekable();
|
||||
|
||||
match word {
|
||||
@@ -2000,10 +2016,12 @@ impl LineBuf {
|
||||
return;
|
||||
}
|
||||
let start = self.index_byte_pos(pos);
|
||||
let end = start + gr.len();
|
||||
let end = start + (new.len().max(gr.len()));
|
||||
self.buffer.replace_range(start..end, new);
|
||||
}
|
||||
pub fn calc_indent_level(&mut self) {
|
||||
// FIXME: This implementation is extremely naive but it kind of sort of works for now
|
||||
// Need to re-implement it and write tests
|
||||
let to_cursor = self
|
||||
.slice_to_cursor()
|
||||
.map(|s| s.to_string())
|
||||
@@ -2377,15 +2395,28 @@ impl LineBuf {
|
||||
MotionCmd(_count, Motion::WholeBuffer) => {
|
||||
MotionKind::Exclusive((0, self.grapheme_indices().len()))
|
||||
}
|
||||
MotionCmd(_count, Motion::BeginningOfBuffer) => MotionKind::On(0),
|
||||
MotionCmd(_count, Motion::EndOfBuffer) => {
|
||||
if self.cursor.exclusive {
|
||||
MotionKind::On(self.grapheme_indices().len().saturating_sub(1))
|
||||
} else {
|
||||
MotionKind::On(self.grapheme_indices().len())
|
||||
}
|
||||
MotionCmd(_count, Motion::StartOfBuffer) => {
|
||||
MotionKind::InclusiveWithTargetCol((0, self.end_of_line()), 0)
|
||||
}
|
||||
MotionCmd(_count, Motion::ToColumn) => todo!(),
|
||||
MotionCmd(_count, Motion::EndOfBuffer) => {
|
||||
let end = self.grapheme_indices().len();
|
||||
MotionKind::InclusiveWithTargetCol((self.start_of_line(), end), 0)
|
||||
}
|
||||
MotionCmd(count, Motion::ToColumn) => {
|
||||
let s = self.start_of_line();
|
||||
let mut end = s;
|
||||
for _ in 0..count {
|
||||
let Some(gr) = self.grapheme_at(end) else {
|
||||
end = self.grapheme_indices().len();
|
||||
break;
|
||||
};
|
||||
if gr == "\n" {
|
||||
break;
|
||||
}
|
||||
end += 1;
|
||||
}
|
||||
MotionKind::On(end.saturating_sub(1)) // count starts at 1, columns are "zero-indexed", so we subtract one
|
||||
}
|
||||
MotionCmd(count, Motion::Range(start, end)) => {
|
||||
let mut final_end = end;
|
||||
if self.cursor.exclusive {
|
||||
@@ -2607,8 +2638,8 @@ impl LineBuf {
|
||||
MotionKind::InclusiveWithTargetCol(..) | MotionKind::ExclusiveWithTargetCol(..)
|
||||
) || matches!(self.select_mode, Some(SelectMode::Line(_)));
|
||||
let register_content = if is_linewise {
|
||||
if !text.ends_with('\n') && !text.is_empty() {
|
||||
text.push('\n');
|
||||
if text.ends_with('\n') && !text.is_empty() {
|
||||
text = text.strip_suffix('\n').unwrap().to_string();
|
||||
}
|
||||
RegisterContent::Line(text)
|
||||
} else {
|
||||
@@ -2645,19 +2676,27 @@ impl LineBuf {
|
||||
self.apply_motion(motion);
|
||||
}
|
||||
Verb::ReplaceCharInplace(ch, count) => {
|
||||
for i in 0..count {
|
||||
let mut buf = [0u8; 4];
|
||||
let new = ch.encode_utf8(&mut buf);
|
||||
self.replace_at_cursor(new);
|
||||
if let Some((start,end)) = self.select_range() {
|
||||
let end = (end + 1).min(self.grapheme_indices().len()); // inclusive
|
||||
let replaced = ch.to_string().repeat(end.saturating_sub(start));
|
||||
self.replace_at(start, &replaced);
|
||||
self.cursor.set(start);
|
||||
} else {
|
||||
for i in 0..count {
|
||||
let mut buf = [0u8; 4];
|
||||
let new = ch.encode_utf8(&mut buf);
|
||||
self.replace_at_cursor(new);
|
||||
|
||||
// try to increment the cursor until we are on the last iteration
|
||||
// or until we hit the end of the buffer
|
||||
if i != count.saturating_sub(1) && !self.cursor.inc() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// try to increment the cursor until we are on the last iteration
|
||||
// or until we hit the end of the buffer
|
||||
if i != count.saturating_sub(1) && !self.cursor.inc() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Verb::ToggleCaseInplace(count) => {
|
||||
let mut did_something = false;
|
||||
for i in 0..count {
|
||||
let Some(gr) = self.grapheme_at_cursor() else {
|
||||
return Ok(());
|
||||
@@ -2679,10 +2718,14 @@ impl LineBuf {
|
||||
|
||||
// try to increment the cursor until we are on the last iteration
|
||||
// or until we hit the end of the buffer
|
||||
did_something = true;
|
||||
if i != count.saturating_sub(1) && !self.cursor.inc() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if did_something {
|
||||
self.cursor.inc();
|
||||
}
|
||||
}
|
||||
Verb::ToggleCaseRange => {
|
||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
||||
@@ -2707,6 +2750,7 @@ impl LineBuf {
|
||||
};
|
||||
self.replace_at(i, new);
|
||||
}
|
||||
self.cursor.set(start);
|
||||
}
|
||||
Verb::ToLower => {
|
||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
||||
@@ -2731,6 +2775,7 @@ impl LineBuf {
|
||||
};
|
||||
self.replace_at(i, new);
|
||||
}
|
||||
self.cursor.set(start);
|
||||
}
|
||||
Verb::ToUpper => {
|
||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
||||
@@ -2755,6 +2800,7 @@ impl LineBuf {
|
||||
};
|
||||
self.replace_at(i, new);
|
||||
}
|
||||
self.cursor.set(start);
|
||||
}
|
||||
Verb::Redo | Verb::Undo => {
|
||||
let (edit_provider, edit_receiver) = match verb {
|
||||
@@ -2810,33 +2856,50 @@ impl LineBuf {
|
||||
}
|
||||
match content {
|
||||
RegisterContent::Span(ref text) => {
|
||||
let insert_idx = match anchor {
|
||||
Anchor::After => self
|
||||
.cursor
|
||||
.get()
|
||||
.saturating_add(1)
|
||||
.min(self.grapheme_indices().len()),
|
||||
Anchor::Before => self.cursor.get(),
|
||||
match anchor {
|
||||
Anchor::After => {
|
||||
let insert_idx = self
|
||||
.cursor
|
||||
.get()
|
||||
.saturating_add(1)
|
||||
.min(self.grapheme_indices().len());
|
||||
let offset = text.len().max(1);
|
||||
|
||||
self.insert_str_at(insert_idx, text);
|
||||
self.cursor.add(offset);
|
||||
},
|
||||
Anchor::Before => {
|
||||
let insert_idx = self.cursor.get();
|
||||
self.insert_str_at(insert_idx, text);
|
||||
self.cursor.add(text.len().saturating_sub(1));
|
||||
},
|
||||
};
|
||||
self.insert_str_at(insert_idx, text);
|
||||
self.cursor.add(text.len().saturating_sub(1));
|
||||
}
|
||||
RegisterContent::Line(ref text) => {
|
||||
let insert_idx = match anchor {
|
||||
Anchor::After => self.end_of_line(),
|
||||
Anchor::Before => self.start_of_line(),
|
||||
};
|
||||
let needs_newline = self
|
||||
.grapheme_before(insert_idx)
|
||||
.is_some_and(|gr| gr != "\n");
|
||||
if needs_newline {
|
||||
let full = format!("\n{}", text);
|
||||
self.insert_str_at(insert_idx, &full);
|
||||
self.cursor.set(insert_idx + 1);
|
||||
} else {
|
||||
self.insert_str_at(insert_idx, text);
|
||||
self.cursor.set(insert_idx);
|
||||
}
|
||||
let mut full = text.to_string();
|
||||
let mut offset = 0;
|
||||
|
||||
match anchor {
|
||||
Anchor::After => {
|
||||
if self.grapheme_before(insert_idx).is_none_or(|gr| gr != "\n") {
|
||||
full = format!("\n{text}");
|
||||
offset += 1;
|
||||
}
|
||||
if self.grapheme_at(insert_idx).is_some_and(|gr| gr != "\n") {
|
||||
full = format!("{full}\n");
|
||||
}
|
||||
}
|
||||
Anchor::Before => {
|
||||
full = format!("{full}\n");
|
||||
}
|
||||
}
|
||||
|
||||
self.insert_str_at(insert_idx, &full);
|
||||
self.cursor.set(insert_idx + offset);
|
||||
}
|
||||
RegisterContent::Empty => {}
|
||||
}
|
||||
@@ -2872,6 +2935,7 @@ impl LineBuf {
|
||||
self.force_replace_at(i, " ");
|
||||
}
|
||||
last_was_whitespace = false;
|
||||
self.cursor.set(i);
|
||||
continue;
|
||||
}
|
||||
last_was_whitespace = is_whitespace(gr);
|
||||
@@ -2907,8 +2971,6 @@ impl LineBuf {
|
||||
Verb::Insert(string) => {
|
||||
self.insert_str_at_cursor(&string);
|
||||
let graphemes = string.graphemes(true).count();
|
||||
log::debug!("Inserted string: {string:?}, graphemes: {graphemes}");
|
||||
log::debug!("buffer after insert: {:?}", self.buffer);
|
||||
self.cursor.add(graphemes);
|
||||
}
|
||||
Verb::Indent => {
|
||||
@@ -3067,7 +3129,13 @@ impl LineBuf {
|
||||
} else {
|
||||
-(n as i64)
|
||||
};
|
||||
let (s, e) = self.select_range().unwrap_or(self.this_word(Word::Normal));
|
||||
let (s, e) = if let Some(r) = self.select_range() {
|
||||
r
|
||||
} else if let Some(r) = self.number_at_cursor() {
|
||||
r
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
let end = if self.select_range().is_some() {
|
||||
if e < self.grapheme_indices().len() - 1 {
|
||||
e
|
||||
@@ -3122,9 +3190,21 @@ impl LineBuf {
|
||||
} else if let Ok(num) = word.parse::<i64>() {
|
||||
let width = word.len();
|
||||
let new_num = num + inc;
|
||||
let num_fmt = if new_num < 0 {
|
||||
let abs = new_num.unsigned_abs();
|
||||
let digit_width = if num < 0 { width - 1 } else { width };
|
||||
format!("-{abs:0>digit_width$}")
|
||||
} else if num < 0 {
|
||||
// Was negative, now positive — pad to width-1 since
|
||||
// the minus sign is gone (e.g. -001 + 2 = 00001)
|
||||
let digit_width = width - 1;
|
||||
format!("{new_num:0>digit_width$}")
|
||||
} else {
|
||||
format!("{new_num:0>width$}")
|
||||
};
|
||||
self
|
||||
.buffer
|
||||
.replace_range(byte_start..byte_end, &format!("{new_num:0>width$}"));
|
||||
.replace_range(byte_start..byte_end, &num_fmt);
|
||||
self.update_graphemes();
|
||||
self.cursor.set(s);
|
||||
}
|
||||
@@ -3144,7 +3224,6 @@ impl LineBuf {
|
||||
| Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
|
||||
|
||||
Verb::ShellCmd(cmd) => {
|
||||
log::debug!("Executing ex-mode command from widget: {cmd}");
|
||||
let mut vars = HashSet::new();
|
||||
vars.insert("_BUFFER".into());
|
||||
vars.insert("_CURSOR".into());
|
||||
@@ -3187,17 +3266,10 @@ impl LineBuf {
|
||||
self.update_graphemes();
|
||||
self.cursor.set_max(self.buffer.graphemes(true).count());
|
||||
self.cursor.set(cursor);
|
||||
log::debug!(
|
||||
"[ShellCmd] post-widget: cursor={}, anchor={}, select_range={:?}",
|
||||
cursor,
|
||||
anchor,
|
||||
self.select_range
|
||||
);
|
||||
if anchor != cursor && self.select_range.is_some() {
|
||||
self.select_range = Some(ordered(cursor, anchor));
|
||||
}
|
||||
if !keys.is_empty() {
|
||||
log::debug!("Pending widget keys from shell command: {keys}");
|
||||
write_meta(|m| m.set_pending_widget_keys(&keys))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ pub mod term;
|
||||
pub mod vicmd;
|
||||
pub mod vimode;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
pub mod markers {
|
||||
use super::Marker;
|
||||
|
||||
@@ -289,6 +292,37 @@ impl ShedVi {
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
pub fn new_no_hist(prompt: Prompt, tty: RawFd) -> ShResult<Self> {
|
||||
let mut new = Self {
|
||||
reader: PollReader::new(),
|
||||
writer: TermWriter::new(tty),
|
||||
prompt,
|
||||
completer: Box::new(FuzzyCompleter::default()),
|
||||
highlighter: Highlighter::new(),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
next_is_escaped: false,
|
||||
saved_mode: None,
|
||||
pending_keymap: Vec::new(),
|
||||
old_layout: None,
|
||||
repeat_action: None,
|
||||
repeat_motion: None,
|
||||
editor: LineBuf::new(),
|
||||
history: History::empty(),
|
||||
needs_redraw: true,
|
||||
};
|
||||
write_vars(|v| {
|
||||
v.set_var(
|
||||
"SHED_VI_MODE",
|
||||
VarKind::Str(new.mode.report_mode().to_string()),
|
||||
VarFlags::NONE,
|
||||
)
|
||||
})?;
|
||||
new.prompt.refresh();
|
||||
new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline
|
||||
new.print_line(false)?;
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
pub fn with_initial(mut self, initial: &str) -> Self {
|
||||
self.editor = LineBuf::new().with_initial(initial, 0);
|
||||
self
|
||||
@@ -696,7 +730,8 @@ impl ShedVi {
|
||||
|
||||
self.needs_redraw = true;
|
||||
return Ok(None);
|
||||
} else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key {
|
||||
} else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key
|
||||
&& self.mode.report_mode() == ModeReport::Insert {
|
||||
let initial = self.editor.as_str();
|
||||
match self.history.start_search(initial) {
|
||||
Some(entry) => {
|
||||
@@ -814,7 +849,7 @@ impl ShedVi {
|
||||
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
|
||||
|
||||
let before = self.editor.buffer.clone();
|
||||
self.exec_cmd(cmd)?;
|
||||
self.exec_cmd(cmd, false)?;
|
||||
if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) {
|
||||
for key in keys {
|
||||
self.handle_key(key)?;
|
||||
@@ -1072,99 +1107,70 @@ impl ShedVi {
|
||||
post_mode_change.exec();
|
||||
}
|
||||
|
||||
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
|
||||
fn exec_mode_transition(&mut self, cmd: ViCmd, from_replay: bool) -> ShResult<()> {
|
||||
let mut select_mode = None;
|
||||
let mut is_insert_mode = false;
|
||||
if cmd.is_mode_transition() {
|
||||
let count = cmd.verb_count();
|
||||
let count = cmd.verb_count();
|
||||
|
||||
let mut mode: Box<dyn ViMode> = if matches!(
|
||||
self.mode.report_mode(),
|
||||
ModeReport::Ex | ModeReport::Verbatim
|
||||
) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE)
|
||||
{
|
||||
if let Some(saved) = self.saved_mode.take() {
|
||||
saved
|
||||
} else {
|
||||
Box::new(ViNormal::new())
|
||||
let mut mode: Box<dyn ViMode> = if matches!(
|
||||
self.mode.report_mode(),
|
||||
ModeReport::Ex | ModeReport::Verbatim
|
||||
) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE)
|
||||
{
|
||||
if let Some(saved) = self.saved_mode.take() {
|
||||
saved
|
||||
} else {
|
||||
Box::new(ViNormal::new())
|
||||
}
|
||||
} else {
|
||||
match cmd.verb().unwrap().1 {
|
||||
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
|
||||
is_insert_mode = true;
|
||||
Box::new(ViInsert::new().with_count(count as u16).record_cmd(cmd.clone()))
|
||||
}
|
||||
} else {
|
||||
match cmd.verb().unwrap().1 {
|
||||
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
|
||||
is_insert_mode = true;
|
||||
Box::new(ViInsert::new().with_count(count as u16))
|
||||
}
|
||||
|
||||
Verb::ExMode => Box::new(ViEx::new()),
|
||||
Verb::ExMode => Box::new(ViEx::new()),
|
||||
|
||||
Verb::VerbatimMode => Box::new(ViVerbatim::read_one().with_count(count as u16)),
|
||||
|
||||
Verb::NormalMode => Box::new(ViNormal::new()),
|
||||
|
||||
Verb::ReplaceMode => Box::new(ViReplace::new()),
|
||||
|
||||
Verb::VisualModeSelectLast => {
|
||||
if self.mode.report_mode() != ModeReport::Visual {
|
||||
self
|
||||
.editor
|
||||
.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
}
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
|
||||
self.swap_mode(&mut mode);
|
||||
|
||||
return self.editor.exec_cmd(cmd);
|
||||
}
|
||||
Verb::VisualMode => {
|
||||
select_mode = Some(SelectMode::Char(SelectAnchor::End));
|
||||
Box::new(ViVisual::new())
|
||||
}
|
||||
Verb::VisualModeLine => {
|
||||
select_mode = Some(SelectMode::Line(SelectAnchor::End));
|
||||
Box::new(ViVisual::new())
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
Verb::VerbatimMode => {
|
||||
self.reader.verbatim_single = true;
|
||||
Box::new(ViVerbatim::new().with_count(count as u16))
|
||||
}
|
||||
};
|
||||
|
||||
self.swap_mode(&mut mode);
|
||||
Verb::NormalMode => Box::new(ViNormal::new()),
|
||||
|
||||
if matches!(
|
||||
self.mode.report_mode(),
|
||||
ModeReport::Ex | ModeReport::Verbatim
|
||||
) {
|
||||
self.saved_mode = Some(mode);
|
||||
write_vars(|v| {
|
||||
v.set_var(
|
||||
"SHED_VI_MODE",
|
||||
VarKind::Str(self.mode.report_mode().to_string()),
|
||||
VarFlags::NONE,
|
||||
)
|
||||
})?;
|
||||
self.prompt.refresh();
|
||||
return Ok(());
|
||||
Verb::ReplaceMode => Box::new(ViReplace::new()),
|
||||
|
||||
Verb::VisualModeSelectLast => {
|
||||
if self.mode.report_mode() != ModeReport::Visual {
|
||||
self
|
||||
.editor
|
||||
.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||
}
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
|
||||
self.swap_mode(&mut mode);
|
||||
|
||||
return self.editor.exec_cmd(cmd);
|
||||
}
|
||||
Verb::VisualMode => {
|
||||
select_mode = Some(SelectMode::Char(SelectAnchor::End));
|
||||
Box::new(ViVisual::new())
|
||||
}
|
||||
Verb::VisualModeLine => {
|
||||
select_mode = Some(SelectMode::Line(SelectAnchor::End));
|
||||
Box::new(ViVisual::new())
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
};
|
||||
|
||||
if mode.is_repeatable() {
|
||||
self.repeat_action = mode.as_replay();
|
||||
}
|
||||
|
||||
// Set cursor clamp BEFORE executing the command so that motions
|
||||
// (like EndOfLine for 'A') can reach positions valid in the new mode
|
||||
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 {
|
||||
self.editor.clear_insert_mode_start_pos();
|
||||
}
|
||||
self.swap_mode(&mut mode);
|
||||
|
||||
if matches!(
|
||||
self.mode.report_mode(),
|
||||
ModeReport::Ex | ModeReport::Verbatim
|
||||
) {
|
||||
self.saved_mode = Some(mode);
|
||||
write_vars(|v| {
|
||||
v.set_var(
|
||||
"SHED_VI_MODE",
|
||||
@@ -1173,8 +1179,56 @@ impl ShedVi {
|
||||
)
|
||||
})?;
|
||||
self.prompt.refresh();
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if mode.is_repeatable() && !from_replay {
|
||||
self.repeat_action = mode.as_replay();
|
||||
}
|
||||
|
||||
// Set cursor clamp BEFORE executing the command so that motions
|
||||
// (like EndOfLine for 'A') can reach positions valid in the new mode
|
||||
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 {
|
||||
self.editor.clear_insert_mode_start_pos();
|
||||
}
|
||||
|
||||
write_vars(|v| {
|
||||
v.set_var(
|
||||
"SHED_VI_MODE",
|
||||
VarKind::Str(self.mode.report_mode().to_string()),
|
||||
VarFlags::NONE,
|
||||
)
|
||||
})?;
|
||||
self.prompt.refresh();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn clone_mode(&self) -> Box<dyn ViMode> {
|
||||
match self.mode.report_mode() {
|
||||
ModeReport::Normal => Box::new(ViNormal::new()),
|
||||
ModeReport::Insert => Box::new(ViInsert::new()),
|
||||
ModeReport::Visual => Box::new(ViVisual::new()),
|
||||
ModeReport::Ex => Box::new(ViEx::new()),
|
||||
ModeReport::Replace => Box::new(ViReplace::new()),
|
||||
ModeReport::Verbatim => Box::new(ViVerbatim::new()),
|
||||
ModeReport::Unknown => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exec_cmd(&mut self, mut cmd: ViCmd, from_replay: bool) -> ShResult<()> {
|
||||
if cmd.is_mode_transition() {
|
||||
return self.exec_mode_transition(cmd, from_replay);
|
||||
} else if cmd.is_cmd_repeat() {
|
||||
let Some(replay) = self.repeat_action.clone() else {
|
||||
return Ok(());
|
||||
@@ -1186,11 +1240,36 @@ impl ShedVi {
|
||||
if count > 1 {
|
||||
repeat = count as u16;
|
||||
}
|
||||
|
||||
let old_mode = self.mode.report_mode();
|
||||
|
||||
for _ in 0..repeat {
|
||||
let cmds = cmds.clone();
|
||||
for cmd in cmds {
|
||||
self.editor.exec_cmd(cmd)?
|
||||
for (i, cmd) in cmds.iter().enumerate() {
|
||||
log::debug!("Replaying command {cmd:?} in mode {:?}, replay {i}/{repeat}", self.mode.report_mode());
|
||||
self.exec_cmd(cmd.clone(), true)?;
|
||||
// After the first command, start merging so all subsequent
|
||||
// edits fold into one undo entry (e.g. cw + inserted chars)
|
||||
if i == 0
|
||||
&& let Some(edit) = self.editor.undo_stack.last_mut() {
|
||||
edit.start_merge();
|
||||
}
|
||||
}
|
||||
// Stop merging at the end of the replay
|
||||
if let Some(edit) = self.editor.undo_stack.last_mut() {
|
||||
edit.stop_merge();
|
||||
}
|
||||
|
||||
let old_mode_clone = match old_mode {
|
||||
ModeReport::Normal => Box::new(ViNormal::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Insert => Box::new(ViInsert::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Visual => Box::new(ViVisual::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Ex => Box::new(ViEx::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Replace => Box::new(ViReplace::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Verbatim => Box::new(ViVerbatim::new()) as Box<dyn ViMode>,
|
||||
ModeReport::Unknown => unreachable!(),
|
||||
};
|
||||
self.mode = old_mode_clone;
|
||||
}
|
||||
}
|
||||
CmdReplay::Single(mut cmd) => {
|
||||
@@ -1253,7 +1332,7 @@ impl ShedVi {
|
||||
self.swap_mode(&mut mode);
|
||||
}
|
||||
|
||||
if cmd.is_repeatable() {
|
||||
if cmd.is_repeatable() && !from_replay {
|
||||
if self.mode.report_mode() == ModeReport::Visual {
|
||||
// The motion is assigned in the line buffer execution, so we also have to
|
||||
// assign it here in order to be able to repeat it
|
||||
@@ -1272,7 +1351,7 @@ impl ShedVi {
|
||||
|
||||
self.editor.exec_cmd(cmd.clone())?;
|
||||
|
||||
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
|
||||
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit() || v.1 == Verb::Yank) {
|
||||
self.editor.stop_selecting();
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||
self.swap_mode(&mut mode);
|
||||
@@ -1421,6 +1500,7 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
|
||||
pub fn marker_for(class: &TkRule) -> Option<Marker> {
|
||||
match class {
|
||||
TkRule::Pipe
|
||||
| TkRule::Bang
|
||||
| TkRule::ErrPipe
|
||||
| TkRule::And
|
||||
| TkRule::Or
|
||||
|
||||
@@ -2,6 +2,24 @@ use std::{fmt::Display, sync::Mutex};
|
||||
|
||||
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
|
||||
|
||||
#[cfg(test)]
|
||||
pub static SAVED_REGISTERS: Mutex<Option<Registers>> = Mutex::new(None);
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn save_registers() {
|
||||
let mut saved = SAVED_REGISTERS.lock().unwrap();
|
||||
*saved = Some(REGISTERS.lock().unwrap().clone());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn restore_registers() {
|
||||
let mut saved = SAVED_REGISTERS.lock().unwrap();
|
||||
if let Some(ref registers) = *saved {
|
||||
*REGISTERS.lock().unwrap() = registers.clone();
|
||||
}
|
||||
*saved = None;
|
||||
}
|
||||
|
||||
pub fn read_register(ch: Option<char>) -> Option<RegisterContent> {
|
||||
let lock = REGISTERS.lock().unwrap();
|
||||
lock.get_reg(ch).map(|r| r.content().clone())
|
||||
@@ -79,7 +97,7 @@ impl RegisterContent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct Registers {
|
||||
default: Register,
|
||||
a: Register,
|
||||
|
||||
@@ -499,6 +499,7 @@ pub struct PollReader {
|
||||
parser: Parser,
|
||||
collector: KeyCollector,
|
||||
byte_buf: VecDeque<u8>,
|
||||
pub verbatim_single: bool,
|
||||
pub verbatim: bool,
|
||||
}
|
||||
|
||||
@@ -508,6 +509,7 @@ impl PollReader {
|
||||
parser: Parser::new(),
|
||||
collector: KeyCollector::new(),
|
||||
byte_buf: VecDeque::new(),
|
||||
verbatim_single: false,
|
||||
verbatim: false,
|
||||
}
|
||||
}
|
||||
@@ -531,6 +533,15 @@ impl PollReader {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn read_one_verbatim(&mut self) -> Option<KeyEvent> {
|
||||
if self.byte_buf.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let bytes: Vec<u8> = self.byte_buf.drain(..).collect();
|
||||
let verbatim_str = String::from_utf8_lossy(&bytes).to_string();
|
||||
Some(KeyEvent(KeyCode::Verbatim(verbatim_str.into()), ModKeys::empty()))
|
||||
}
|
||||
|
||||
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||
self.byte_buf.extend(bytes);
|
||||
}
|
||||
@@ -544,17 +555,28 @@ impl Default for PollReader {
|
||||
|
||||
impl KeyReader for PollReader {
|
||||
fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr> {
|
||||
if self.verbatim_single {
|
||||
if let Some(key) = self.read_one_verbatim() {
|
||||
self.verbatim_single = false;
|
||||
return Ok(Some(key));
|
||||
}
|
||||
return Ok(None);
|
||||
}
|
||||
if self.verbatim {
|
||||
if let Some(paste) = self.handle_bracket_paste() {
|
||||
return Ok(Some(paste));
|
||||
}
|
||||
// If we're in verbatim mode but haven't seen the end marker yet, don't attempt to parse keys
|
||||
return Ok(None);
|
||||
} else if self.byte_buf.len() == 1
|
||||
&& self.byte_buf.front() == Some(&b'\x1b') {
|
||||
// User pressed escape
|
||||
self.byte_buf.pop_front(); // Consume the escape byte
|
||||
return Ok(Some(KeyEvent(KeyCode::Esc, ModKeys::empty())));
|
||||
} else if self.byte_buf.front() == Some(&b'\x1b') {
|
||||
// Escape: if it's the only byte, or the next byte isn't a valid
|
||||
// escape sequence prefix ([ or O), emit a standalone Escape
|
||||
if self.byte_buf.len() == 1
|
||||
|| !matches!(self.byte_buf.get(1), Some(b'[') | Some(b'O'))
|
||||
{
|
||||
self.byte_buf.pop_front();
|
||||
return Ok(Some(KeyEvent(KeyCode::Esc, ModKeys::empty())));
|
||||
}
|
||||
}
|
||||
while let Some(byte) = self.byte_buf.pop_front() {
|
||||
self.parser.advance(&mut self.collector, &[byte]);
|
||||
|
||||
229
src/readline/tests.rs
Normal file
229
src/readline/tests.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
#![allow(non_snake_case)]
|
||||
use std::os::fd::AsRawFd;
|
||||
|
||||
use crate::{readline::{Prompt, ShedVi}, testutil::TestGuard};
|
||||
|
||||
/// Tests for our vim logic emulation. Each test consists of an initial text, a sequence of keys to feed, and the expected final text and cursor position.
|
||||
macro_rules! vi_test {
|
||||
{ $($name:ident: $input:expr => $op:expr => $expected_text:expr,$expected_cursor:expr);* } => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() {
|
||||
let (mut vi, _g) = test_vi($input);
|
||||
vi.feed_bytes(b"\x1b"); // Start in normal mode
|
||||
vi.process_input().unwrap();
|
||||
|
||||
vi.feed_bytes($op.as_bytes());
|
||||
vi.process_input().unwrap();
|
||||
assert_eq!(vi.editor.as_str(), $expected_text);
|
||||
assert_eq!(vi.editor.cursor.get(), $expected_cursor);
|
||||
}
|
||||
)*
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
fn test_vi(initial: &str) -> (ShedVi, TestGuard) {
|
||||
let g = TestGuard::new();
|
||||
let prompt = Prompt::default();
|
||||
let vi = ShedVi::new_no_hist(prompt, g.pty_slave().as_raw_fd())
|
||||
.unwrap()
|
||||
.with_initial(initial);
|
||||
|
||||
(vi, g)
|
||||
}
|
||||
|
||||
// Why can't I marry a programming language
|
||||
vi_test! {
|
||||
vi_dw_basic : "hello world" => "dw" => "world", 0;
|
||||
vi_dw_middle : "one two three" => "wdw" => "one three", 4;
|
||||
vi_dd_whole_line : "hello world" => "dd" => "", 0;
|
||||
vi_x_single : "hello" => "x" => "ello", 0;
|
||||
vi_x_middle : "hello" => "llx" => "helo", 2;
|
||||
vi_X_backdelete : "hello" => "llX" => "hllo", 1;
|
||||
vi_h_motion : "hello" => "$h" => "hello", 3;
|
||||
vi_l_motion : "hello" => "l" => "hello", 1;
|
||||
vi_h_at_start : "hello" => "h" => "hello", 0;
|
||||
vi_l_at_end : "hello" => "$l" => "hello", 4;
|
||||
vi_w_forward : "one two three" => "w" => "one two three", 4;
|
||||
vi_b_backward : "one two three" => "$b" => "one two three", 8;
|
||||
vi_e_end : "one two three" => "e" => "one two three", 2;
|
||||
vi_ge_back_end : "one two three" => "$ge" => "one two three", 6;
|
||||
vi_w_punctuation : "foo.bar baz" => "w" => "foo.bar baz", 3;
|
||||
vi_e_punctuation : "foo.bar baz" => "e" => "foo.bar baz", 2;
|
||||
vi_b_punctuation : "foo.bar baz" => "$b" => "foo.bar baz", 8;
|
||||
vi_w_at_eol : "hello" => "$w" => "hello", 4;
|
||||
vi_b_at_bol : "hello" => "b" => "hello", 0;
|
||||
vi_W_forward : "foo.bar baz" => "W" => "foo.bar baz", 8;
|
||||
vi_B_backward : "foo.bar baz" => "$B" => "foo.bar baz", 8;
|
||||
vi_E_end : "foo.bar baz" => "E" => "foo.bar baz", 6;
|
||||
vi_gE_back_end : "one two three" => "$gE" => "one two three", 6;
|
||||
vi_W_skip_punct : "one-two three" => "W" => "one-two three", 8;
|
||||
vi_B_skip_punct : "one two-three" => "$B" => "one two-three", 4;
|
||||
vi_E_skip_punct : "one-two three" => "E" => "one-two three", 6;
|
||||
vi_dW_big : "foo.bar baz" => "dW" => "baz", 0;
|
||||
vi_cW_big : "foo.bar baz" => "cWx\x1b" => "x baz", 0;
|
||||
vi_zero_bol : " hello" => "$0" => " hello", 0;
|
||||
vi_caret_first_char : " hello" => "$^" => " hello", 2;
|
||||
vi_dollar_eol : "hello world" => "$" => "hello world", 10;
|
||||
vi_g_last_nonws : "hello " => "g_" => "hello ", 4;
|
||||
vi_g_no_trailing : "hello" => "g_" => "hello", 4;
|
||||
vi_pipe_column : "hello world" => "6|" => "hello world", 5;
|
||||
vi_pipe_col1 : "hello world" => "1|" => "hello world", 0;
|
||||
vi_I_insert_front : " hello" => "Iworld \x1b" => " world hello", 7;
|
||||
vi_A_append_end : "hello" => "A world\x1b" => "hello world", 10;
|
||||
vi_f_find : "hello world" => "fo" => "hello world", 4;
|
||||
vi_F_find_back : "hello world" => "$Fo" => "hello world", 7;
|
||||
vi_t_till : "hello world" => "tw" => "hello world", 5;
|
||||
vi_T_till_back : "hello world" => "$To" => "hello world", 8;
|
||||
vi_f_no_match : "hello" => "fz" => "hello", 0;
|
||||
vi_semicolon_repeat : "abcabc" => "fa;;" => "abcabc", 3;
|
||||
vi_comma_reverse : "abcabc" => "fa;;," => "abcabc", 0;
|
||||
vi_df_semicolon : "abcabc" => "fa;;dfa" => "abcabc", 3;
|
||||
vi_t_at_target : "aab" => "lta" => "aab", 1;
|
||||
vi_D_to_end : "hello world" => "wD" => "hello ", 5;
|
||||
vi_d_dollar : "hello world" => "wd$" => "hello ", 5;
|
||||
vi_d0_to_start : "hello world" => "$d0" => "d", 0;
|
||||
vi_dw_multiple : "one two three" => "d2w" => "three", 0;
|
||||
vi_dt_char : "hello world" => "dtw" => "world", 0;
|
||||
vi_df_char : "hello world" => "dfw" => "orld", 0;
|
||||
vi_dh_back : "hello" => "lldh" => "hllo", 1;
|
||||
vi_dl_forward : "hello" => "dl" => "ello", 0;
|
||||
vi_dge_back_end : "one two three" => "$dge" => "one tw", 5;
|
||||
vi_dG_to_end : "hello world" => "dG" => "", 0;
|
||||
vi_dgg_to_start : "hello world" => "$dgg" => "", 0;
|
||||
vi_d_semicolon : "abcabc" => "fad;" => "abcabc", 3;
|
||||
vi_cw_basic : "hello world" => "cwfoo\x1b" => "foo world", 2;
|
||||
vi_C_to_end : "hello world" => "wCfoo\x1b" => "hello foo", 8;
|
||||
vi_cc_whole : "hello world" => "ccfoo\x1b" => "foo", 2;
|
||||
vi_ct_char : "hello world" => "ctwfoo\x1b" => "fooworld", 2;
|
||||
vi_s_single : "hello" => "sfoo\x1b" => "fooello", 2;
|
||||
vi_S_whole_line : "hello world" => "Sfoo\x1b" => "foo", 2;
|
||||
vi_cl_forward : "hello" => "clX\x1b" => "Xello", 0;
|
||||
vi_ch_backward : "hello" => "llchX\x1b" => "hXllo", 1;
|
||||
vi_cb_word_back : "hello world" => "$cbfoo\x1b" => "hello food", 8;
|
||||
vi_ce_word_end : "hello world" => "cefoo\x1b" => "foo world", 2;
|
||||
vi_c0_to_start : "hello world" => "wc0foo\x1b" => "fooworld", 2;
|
||||
vi_yw_p_basic : "hello world" => "ywwP" => "hello hello world", 11;
|
||||
vi_dw_p_paste : "hello world" => "dwP" => "hello world", 5;
|
||||
vi_dd_p_paste : "hello world" => "ddp" => "\nhello world", 1;
|
||||
vi_y_dollar_p : "hello world" => "wy$P" => "hello worldworld", 10;
|
||||
vi_ye_p : "hello world" => "yewP" => "hello helloworld", 10;
|
||||
vi_yy_p : "hello world" => "yyp" => "hello world\nhello world", 12;
|
||||
vi_Y_p : "hello world" => "Yp" => "hhello worldello world", 11;
|
||||
vi_p_after_x : "hello" => "xp" => "ehllo", 1;
|
||||
vi_P_before : "hello" => "llxP" => "hello", 2;
|
||||
vi_paste_empty : "hello" => "p" => "hello", 0;
|
||||
vi_r_replace : "hello" => "ra" => "aello", 0;
|
||||
vi_r_middle : "hello" => "llra" => "healo", 2;
|
||||
vi_r_at_end : "hello" => "$ra" => "hella", 4;
|
||||
vi_r_space : "hello" => "r " => " ello", 0;
|
||||
vi_r_with_count : "hello" => "3rx" => "xxxlo", 2;
|
||||
vi_tilde_single : "hello" => "~" => "Hello", 1;
|
||||
vi_tilde_count : "hello" => "3~" => "HELlo", 3;
|
||||
vi_tilde_at_end : "HELLO" => "$~" => "HELLo", 4;
|
||||
vi_tilde_mixed : "hElLo" => "5~" => "HeLlO", 4;
|
||||
vi_gu_word : "HELLO world" => "guw" => "hello world", 0;
|
||||
vi_gU_word : "hello WORLD" => "gUw" => "HELLO WORLD", 0;
|
||||
vi_gu_dollar : "HELLO WORLD" => "gu$" => "hello world", 0;
|
||||
vi_gU_dollar : "hello world" => "gU$" => "HELLO WORLD", 0;
|
||||
vi_gu_0 : "HELLO WORLD" => "$gu0" => "hello worlD", 0;
|
||||
vi_gU_0 : "hello world" => "$gU0" => "HELLO WORLd", 0;
|
||||
vi_gtilde_word : "hello WORLD" => "g~w" => "HELLO WORLD", 0;
|
||||
vi_gtilde_dollar : "hello WORLD" => "g~$" => "HELLO world", 0;
|
||||
vi_diw_inner : "one two three" => "wdiw" => "one three", 4;
|
||||
vi_ciw_replace : "hello world" => "ciwfoo\x1b" => "foo world", 2;
|
||||
vi_daw_around : "one two three" => "wdaw" => "one three", 4;
|
||||
vi_yiw_p : "hello world" => "yiwAp \x1bp" => "hello worldp hello", 17;
|
||||
vi_diW_big_inner : "one-two three" => "diW" => " three", 0;
|
||||
vi_daW_big_around : "one two-three end" => "wdaW" => "one end", 4;
|
||||
vi_ciW_big : "one-two three" => "ciWx\x1b" => "x three", 0;
|
||||
vi_di_dquote : "one \"two\" three" => "f\"di\"" => "one \"\" three", 5;
|
||||
vi_da_dquote : "one \"two\" three" => "f\"da\"" => "one three", 4;
|
||||
vi_ci_dquote : "one \"two\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5;
|
||||
vi_di_squote : "one 'two' three" => "f'di'" => "one '' three", 5;
|
||||
vi_da_squote : "one 'two' three" => "f'da'" => "one three", 4;
|
||||
vi_di_backtick : "one `two` three" => "f`di`" => "one `` three", 5;
|
||||
vi_da_backtick : "one `two` three" => "f`da`" => "one three", 4;
|
||||
vi_ci_dquote_empty : "one \"\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5;
|
||||
vi_di_paren : "one (two) three" => "f(di(" => "one () three", 5;
|
||||
vi_da_paren : "one (two) three" => "f(da(" => "one three", 4;
|
||||
vi_ci_paren : "one (two) three" => "f(ci(x\x1b" => "one (x) three", 5;
|
||||
vi_di_brace : "one {two} three" => "f{di{" => "one {} three", 5;
|
||||
vi_da_brace : "one {two} three" => "f{da{" => "one three", 4;
|
||||
vi_di_bracket : "one [two] three" => "f[di[" => "one [] three", 5;
|
||||
vi_da_bracket : "one [two] three" => "f[da[" => "one three", 4;
|
||||
vi_di_angle : "one <two> three" => "f<di<" => "one <> three", 5;
|
||||
vi_da_angle : "one <two> three" => "f<da<" => "one three", 4;
|
||||
vi_di_paren_nested : "fn(a, (b, c))" => "f(di(" => "fn()", 3;
|
||||
vi_di_paren_empty : "fn() end" => "f(di(" => "fn() end", 3;
|
||||
vi_dib_alias : "one (two) three" => "f(dib" => "one () three", 5;
|
||||
vi_diB_alias : "one {two} three" => "f{diB" => "one {} three", 5;
|
||||
vi_percent_paren : "(hello) world" => "%" => "(hello) world", 6;
|
||||
vi_percent_brace : "{hello} world" => "%" => "{hello} world", 6;
|
||||
vi_percent_bracket : "[hello] world" => "%" => "[hello] world", 6;
|
||||
vi_percent_from_close: "(hello) world" => "f)%" => "(hello) world", 0;
|
||||
vi_d_percent_paren : "(hello) world" => "d%" => " world", 0;
|
||||
vi_i_insert : "hello" => "iX\x1b" => "Xhello", 0;
|
||||
vi_a_append : "hello" => "aX\x1b" => "hXello", 1;
|
||||
vi_I_front : " hello" => "IX\x1b" => " Xhello", 2;
|
||||
vi_A_end : "hello" => "AX\x1b" => "helloX", 5;
|
||||
vi_o_open_below : "hello" => "oworld\x1b" => "hello\nworld", 10;
|
||||
vi_O_open_above : "hello" => "Oworld\x1b" => "world\nhello", 4;
|
||||
vi_empty_input : "" => "i hello\x1b" => " hello", 5;
|
||||
vi_insert_escape : "hello" => "aX\x1b" => "hXello", 1;
|
||||
vi_ctrl_w_del_word : "hello world" => "A\x17\x1b" => "hello ", 5;
|
||||
vi_ctrl_h_backspace : "hello" => "A\x08\x1b" => "hell", 3;
|
||||
vi_u_undo_delete : "hello world" => "dwu" => "hello world", 0;
|
||||
vi_u_undo_change : "hello world" => "ciwfoo\x1bu" => "hello world", 0;
|
||||
vi_u_undo_x : "hello" => "xu" => "hello", 0;
|
||||
vi_ctrl_r_redo : "hello" => "xu\x12" => "ello", 0;
|
||||
vi_u_multiple : "hello world" => "xdwu" => "ello world", 0;
|
||||
vi_redo_after_undo : "hello world" => "dwu\x12" => "world", 0;
|
||||
vi_dot_repeat_x : "hello" => "x." => "llo", 0;
|
||||
vi_dot_repeat_dw : "one two three" => "dw." => "three", 0;
|
||||
vi_dot_repeat_cw : "one two three" => "cwfoo\x1bw." => "foo foo three", 6;
|
||||
vi_dot_repeat_r : "hello" => "ra.." => "aello", 0;
|
||||
vi_dot_repeat_s : "hello" => "sX\x1bl." => "XXllo", 1;
|
||||
vi_count_h : "hello world" => "$3h" => "hello world", 7;
|
||||
vi_count_l : "hello world" => "3l" => "hello world", 3;
|
||||
vi_count_w : "one two three four" => "2w" => "one two three four", 8;
|
||||
vi_count_b : "one two three four" => "$2b" => "one two three four", 8;
|
||||
vi_count_x : "hello" => "3x" => "lo", 0;
|
||||
vi_count_dw : "one two three four" => "2dw" => "three four", 0;
|
||||
vi_verb_count_motion : "one two three four" => "d2w" => "three four", 0;
|
||||
vi_count_s : "hello" => "3sX\x1b" => "Xlo", 0;
|
||||
vi_indent_line : "hello" => ">>" => "\thello", 0;
|
||||
vi_dedent_line : "\thello" => "<<" => "hello", 0;
|
||||
vi_indent_double : "hello" => ">>>>" => "\t\thello", 0;
|
||||
vi_J_join_lines : "hello\nworld" => "J" => "hello world", 5;
|
||||
vi_v_u_lower : "HELLO" => "vlllu" => "hellO", 0;
|
||||
vi_v_U_upper : "hello" => "vlllU" => "HELLo", 0;
|
||||
vi_v_d_delete : "hello world" => "vwwd" => "", 0;
|
||||
vi_v_x_delete : "hello world" => "vwwx" => "", 0;
|
||||
vi_v_c_change : "hello world" => "vwcfoo\x1b" => "fooorld", 2;
|
||||
vi_v_y_p_yank : "hello world" => "vwyAp \x1bp" => "hello worldp hello w", 19;
|
||||
vi_v_dollar_d : "hello world" => "wv$d" => "hello ", 5;
|
||||
vi_v_0_d : "hello world" => "$v0d" => "", 0;
|
||||
vi_ve_d : "hello world" => "ved" => " world", 0;
|
||||
vi_v_o_swap : "hello world" => "vllod" => "lo world", 0;
|
||||
vi_v_r_replace : "hello" => "vlllrx" => "xxxxo", 0;
|
||||
vi_v_tilde_case : "hello" => "vlll~" => "HELLo", 0;
|
||||
vi_V_d_delete : "hello world" => "Vd" => "", 0;
|
||||
vi_V_y_p : "hello world" => "Vyp" => "hello world\nhello world", 12;
|
||||
vi_V_S_change : "hello world" => "VSfoo\x1b" => "foo", 2;
|
||||
vi_ctrl_a_inc : "num 5 end" => "w\x01" => "num 6 end", 4;
|
||||
vi_ctrl_x_dec : "num 5 end" => "w\x18" => "num 4 end", 4;
|
||||
vi_ctrl_a_negative : "num -3 end" => "w\x01" => "num -2 end", 4;
|
||||
vi_ctrl_x_to_neg : "num 0 end" => "w\x18" => "num -1 end", 4;
|
||||
vi_ctrl_a_count : "num 5 end" => "w3\x01" => "num 8 end", 4;
|
||||
vi_ctrl_a_width : "num -00001 end" => "w\x01" => "num 00000 end", 4;
|
||||
vi_delete_empty : "" => "x" => "", 0;
|
||||
vi_undo_on_empty : "" => "u" => "", 0;
|
||||
vi_w_single_char : "a b c" => "w" => "a b c", 2;
|
||||
vi_dw_last_word : "hello" => "dw" => "", 0;
|
||||
vi_dollar_single : "h" => "$" => "h", 0;
|
||||
vi_caret_no_ws : "hello" => "$^" => "hello", 0;
|
||||
vi_f_last_char : "hello" => "fo" => "hello", 4;
|
||||
vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4
|
||||
}
|
||||
@@ -343,7 +343,7 @@ pub enum Motion {
|
||||
HalfOfScreen,
|
||||
HalfOfScreenLineText,
|
||||
WholeBuffer,
|
||||
BeginningOfBuffer,
|
||||
StartOfBuffer,
|
||||
EndOfBuffer,
|
||||
ToColumn,
|
||||
ToDelimMatch,
|
||||
|
||||
@@ -13,6 +13,10 @@ impl ViInsert {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn record_cmd(mut self, cmd: ViCmd) -> Self {
|
||||
self.cmds.push(cmd);
|
||||
self
|
||||
}
|
||||
pub fn with_count(mut self, repeat_count: u16) -> Self {
|
||||
self.repeat_count = repeat_count;
|
||||
self
|
||||
|
||||
@@ -434,7 +434,7 @@ impl ViNormal {
|
||||
'g' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::StartOfBuffer));
|
||||
}
|
||||
'e' => {
|
||||
chars = chars_clone;
|
||||
|
||||
@@ -4,19 +4,11 @@ use crate::readline::vicmd::{CmdFlags, RegisterName, To, Verb, VerbCmd, ViCmd};
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct ViVerbatim {
|
||||
pending_seq: String,
|
||||
sent_cmd: Vec<ViCmd>,
|
||||
repeat_count: u16,
|
||||
read_one: bool
|
||||
}
|
||||
|
||||
impl ViVerbatim {
|
||||
pub fn read_one() -> Self {
|
||||
Self {
|
||||
read_one: true,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
@@ -31,7 +23,7 @@ impl ViVerbatim {
|
||||
impl ViMode for ViVerbatim {
|
||||
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||
match key {
|
||||
E(K::Verbatim(seq), _mods) if self.read_one => {
|
||||
E(K::Verbatim(seq), _mods) => {
|
||||
log::debug!("Received verbatim key sequence: {:?}", seq);
|
||||
let cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
@@ -43,22 +35,6 @@ impl ViMode for ViVerbatim {
|
||||
self.sent_cmd.push(cmd.clone());
|
||||
Some(cmd)
|
||||
}
|
||||
E(K::Verbatim(seq), _mods) => {
|
||||
self.pending_seq.push_str(&seq);
|
||||
None
|
||||
}
|
||||
E(K::BracketedPasteEnd, _mods) => {
|
||||
log::debug!("Received verbatim paste: {:?}", self.pending_seq);
|
||||
let cmd = ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: Some(VerbCmd(1, Verb::Insert(self.pending_seq.clone()))),
|
||||
motion: None,
|
||||
raw_seq: std::mem::take(&mut self.pending_seq),
|
||||
flags: CmdFlags::EXIT_CUR_MODE,
|
||||
};
|
||||
self.sent_cmd.push(cmd.clone());
|
||||
Some(cmd)
|
||||
}
|
||||
_ => common_cmds(key),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ impl ViVisual {
|
||||
let ch = chars_clone.next()?;
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
|
||||
verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch,1))),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
@@ -237,6 +237,24 @@ impl ViVisual {
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
's' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Delete)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'S' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Change)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'U' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
@@ -283,8 +301,13 @@ impl ViVisual {
|
||||
});
|
||||
}
|
||||
'y' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Yank));
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Yank)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'd' => {
|
||||
chars = chars_clone;
|
||||
@@ -335,7 +358,7 @@ impl ViVisual {
|
||||
'g' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::StartOfBuffer));
|
||||
}
|
||||
'e' => {
|
||||
chars_clone.next();
|
||||
|
||||
115
src/testutil.rs
115
src/testutil.rs
@@ -1,9 +1,9 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{HashMap, HashSet},
|
||||
env,
|
||||
os::fd::{AsRawFd, OwnedFd},
|
||||
os::fd::{AsRawFd, BorrowedFd, OwnedFd},
|
||||
path::PathBuf,
|
||||
sync::{self, MutexGuard},
|
||||
sync::{self, Arc, MutexGuard},
|
||||
};
|
||||
|
||||
use nix::{
|
||||
@@ -14,10 +14,7 @@ use nix::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
libsh::error::ShResult,
|
||||
parse::{Redir, RedirType, execute::exec_input},
|
||||
procio::{IoFrame, IoMode, RedirGuard},
|
||||
state::{MetaTab, SHED},
|
||||
expand::expand_aliases, libsh::error::ShResult, parse::{ParsedSrc, Redir, RedirType, execute::exec_input, lex::LexFlags}, procio::{IoFrame, IoMode, RedirGuard}, readline::register::{restore_registers, save_registers}, state::{MetaTab, SHED, read_logic}
|
||||
};
|
||||
|
||||
static TEST_MUTEX: sync::Mutex<()> = sync::Mutex::new(());
|
||||
@@ -41,7 +38,7 @@ pub struct TestGuard {
|
||||
old_cwd: PathBuf,
|
||||
saved_env: HashMap<String, String>,
|
||||
pty_master: OwnedFd,
|
||||
_pty_slave: OwnedFd,
|
||||
pty_slave: OwnedFd,
|
||||
|
||||
cleanups: Vec<Box<dyn FnOnce()>>
|
||||
}
|
||||
@@ -90,17 +87,22 @@ impl TestGuard {
|
||||
let old_cwd = env::current_dir().unwrap();
|
||||
let saved_env = env::vars().collect();
|
||||
SHED.with(|s| s.save());
|
||||
save_registers();
|
||||
Self {
|
||||
_lock,
|
||||
_redir_guard,
|
||||
old_cwd,
|
||||
saved_env,
|
||||
pty_master,
|
||||
_pty_slave: pty_slave,
|
||||
pty_slave,
|
||||
cleanups: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pty_slave(&self) -> BorrowedFd {
|
||||
unsafe { BorrowedFd::borrow_raw(self.pty_slave.as_raw_fd()) }
|
||||
}
|
||||
|
||||
pub fn add_cleanup(&mut self, f: impl FnOnce() + 'static) {
|
||||
self.cleanups.push(Box::new(f));
|
||||
}
|
||||
@@ -151,5 +153,100 @@ impl Drop for TestGuard {
|
||||
cleanup();
|
||||
}
|
||||
SHED.with(|s| s.restore());
|
||||
restore_registers();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ast(input: &str) -> ShResult<Vec<crate::parse::Node>> {
|
||||
let log_tab = read_logic(|l| l.clone());
|
||||
let input = expand_aliases(input.into(), HashSet::new(), &log_tab);
|
||||
|
||||
let source_name = "test_input".to_string();
|
||||
let mut parser = ParsedSrc::new(Arc::new(input))
|
||||
.with_lex_flags(LexFlags::empty())
|
||||
.with_name(source_name.clone());
|
||||
|
||||
parser.parse_src().map_err(|e| e.into_iter().next().unwrap())?;
|
||||
|
||||
Ok(parser.extract_nodes())
|
||||
}
|
||||
|
||||
impl crate::parse::Node {
|
||||
pub fn assert_structure(&mut self, expected: &mut impl Iterator<Item = NdKind>) -> Result<(), String> {
|
||||
let mut full_structure = vec![];
|
||||
let mut before = vec![];
|
||||
let mut after = vec![];
|
||||
let mut offender = None;
|
||||
|
||||
self.walk_tree(&mut |s| {
|
||||
let expected_rule = expected.next();
|
||||
full_structure.push(s.class.as_nd_kind());
|
||||
|
||||
if offender.is_none() && expected_rule.as_ref().map_or(true, |e| *e != s.class.as_nd_kind()) {
|
||||
offender = Some((s.class.as_nd_kind(), expected_rule));
|
||||
} else if offender.is_none() {
|
||||
before.push(s.class.as_nd_kind());
|
||||
} else {
|
||||
after.push(s.class.as_nd_kind());
|
||||
}
|
||||
});
|
||||
|
||||
assert!(expected.next().is_none(), "Expected structure has more nodes than actual structure");
|
||||
|
||||
if let Some((nd_kind, expected_rule)) = offender {
|
||||
let expected_rule = expected_rule.map_or("(none — expected array too short)".into(), |e| format!("{e:?}"));
|
||||
let full_structure_hint = full_structure.into_iter()
|
||||
.map(|s| format!("\tNdKind::{s:?},"))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
let full_structure_hint = format!("let expected = &mut [\n{full_structure_hint}\n].into_iter();");
|
||||
|
||||
let output = [
|
||||
"Structure assertion failed!\n".into(),
|
||||
format!("Expected node type '{:?}', found '{:?}'", expected_rule, nd_kind),
|
||||
format!("Before offender: {:?}", before),
|
||||
format!("After offender: {:?}\n", after),
|
||||
format!("hint: here is the full structure as an array\n {full_structure_hint}"),
|
||||
].join("\n");
|
||||
|
||||
Err(output)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum NdKind {
|
||||
IfNode,
|
||||
LoopNode,
|
||||
ForNode,
|
||||
CaseNode,
|
||||
Command,
|
||||
Pipeline,
|
||||
Conjunction,
|
||||
Assignment,
|
||||
BraceGrp,
|
||||
Negate,
|
||||
Test,
|
||||
FuncDef,
|
||||
}
|
||||
|
||||
impl crate::parse::NdRule {
|
||||
pub fn as_nd_kind(&self) -> NdKind {
|
||||
match self {
|
||||
Self::Negate { .. } => NdKind::Negate,
|
||||
Self::IfNode { .. } => NdKind::IfNode,
|
||||
Self::LoopNode { .. } => NdKind::LoopNode,
|
||||
Self::ForNode { .. } => NdKind::ForNode,
|
||||
Self::CaseNode { .. } => NdKind::CaseNode,
|
||||
Self::Command { .. } => NdKind::Command,
|
||||
Self::Pipeline { .. } => NdKind::Pipeline,
|
||||
Self::Conjunction { .. } => NdKind::Conjunction,
|
||||
Self::Assignment { .. } => NdKind::Assignment,
|
||||
Self::BraceGrp { .. } => NdKind::BraceGrp,
|
||||
Self::Test { .. } => NdKind::Test,
|
||||
Self::FuncDef { .. } => NdKind::FuncDef,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
319
tests/gen_vi_tests.lua
Normal file
319
tests/gen_vi_tests.lua
Normal file
@@ -0,0 +1,319 @@
|
||||
-- Generate Rust vi_test! macro invocations using neovim as oracle
|
||||
-- Usage: nvim --headless --clean -l tests/gen_vi_tests.lua
|
||||
--
|
||||
-- Define test cases as { name, input_text, key_sequence }
|
||||
-- Key sequences use vim notation: <Esc>, <CR>, <C-w>, etc.
|
||||
-- The script executes each in a fresh buffer and captures the result.
|
||||
|
||||
local tests = {
|
||||
-- ===================== basic char motions =====================
|
||||
{ "dw_basic", "hello world", "dw" },
|
||||
{ "dw_middle", "one two three", "wdw" },
|
||||
{ "dd_whole_line", "hello world", "dd" },
|
||||
{ "x_single", "hello", "x" },
|
||||
{ "x_middle", "hello", "llx" },
|
||||
{ "X_backdelete", "hello", "llX" },
|
||||
{ "h_motion", "hello", "$h" },
|
||||
{ "l_motion", "hello", "l" },
|
||||
{ "h_at_start", "hello", "h" },
|
||||
{ "l_at_end", "hello", "$l" },
|
||||
|
||||
-- ===================== word motions (small) =====================
|
||||
{ "w_forward", "one two three", "w" },
|
||||
{ "b_backward", "one two three", "$b" },
|
||||
{ "e_end", "one two three", "e" },
|
||||
{ "ge_back_end", "one two three", "$ge" },
|
||||
{ "w_punctuation", "foo.bar baz", "w" },
|
||||
{ "e_punctuation", "foo.bar baz", "e" },
|
||||
{ "b_punctuation", "foo.bar baz", "$b" },
|
||||
{ "w_at_eol", "hello", "$w" },
|
||||
{ "b_at_bol", "hello", "b" },
|
||||
|
||||
-- ===================== word motions (big) =====================
|
||||
{ "W_forward", "foo.bar baz", "W" },
|
||||
{ "B_backward", "foo.bar baz", "$B" },
|
||||
{ "E_end", "foo.bar baz", "E" },
|
||||
{ "gE_back_end", "one two three", "$gE" },
|
||||
{ "W_skip_punct", "one-two three", "W" },
|
||||
{ "B_skip_punct", "one two-three", "$B" },
|
||||
{ "E_skip_punct", "one-two three", "E" },
|
||||
{ "dW_big", "foo.bar baz", "dW" },
|
||||
{ "cW_big", "foo.bar baz", "cWx<Esc>" },
|
||||
|
||||
-- ===================== line motions =====================
|
||||
{ "zero_bol", " hello", "$0" },
|
||||
{ "caret_first_char", " hello", "$^" },
|
||||
{ "dollar_eol", "hello world", "$" },
|
||||
{ "g_last_nonws", "hello ", "g_" },
|
||||
{ "g_no_trailing", "hello", "g_" },
|
||||
{ "pipe_column", "hello world", "6|" },
|
||||
{ "pipe_col1", "hello world", "1|" },
|
||||
{ "I_insert_front", " hello", "Iworld <Esc>" },
|
||||
{ "A_append_end", "hello", "A world<Esc>" },
|
||||
|
||||
-- ===================== find motions =====================
|
||||
{ "f_find", "hello world", "fo" },
|
||||
{ "F_find_back", "hello world", "$Fo" },
|
||||
{ "t_till", "hello world", "tw" },
|
||||
{ "T_till_back", "hello world", "$To" },
|
||||
{ "f_no_match", "hello", "fz" },
|
||||
{ "semicolon_repeat", "abcabc", "fa;;" },
|
||||
{ "comma_reverse", "abcabc", "fa;;," },
|
||||
{ "df_semicolon", "abcabc", "fa;;dfa" },
|
||||
{ "t_at_target", "aab", "lta" },
|
||||
|
||||
-- ===================== delete operations =====================
|
||||
{ "D_to_end", "hello world", "wD" },
|
||||
{ "d_dollar", "hello world", "wd$" },
|
||||
{ "d0_to_start", "hello world", "$d0" },
|
||||
{ "dw_multiple", "one two three", "d2w" },
|
||||
{ "dt_char", "hello world", "dtw" },
|
||||
{ "df_char", "hello world", "dfw" },
|
||||
{ "dh_back", "hello", "lldh" },
|
||||
{ "dl_forward", "hello", "dl" },
|
||||
{ "dge_back_end", "one two three", "$dge" },
|
||||
{ "dG_to_end", "hello world", "dG" },
|
||||
{ "dgg_to_start", "hello world", "$dgg" },
|
||||
{ "d_semicolon", "abcabc", "fad;" },
|
||||
|
||||
-- ===================== change operations =====================
|
||||
{ "cw_basic", "hello world", "cwfoo<Esc>" },
|
||||
{ "C_to_end", "hello world", "wCfoo<Esc>" },
|
||||
{ "cc_whole", "hello world", "ccfoo<Esc>" },
|
||||
{ "ct_char", "hello world", "ctwfoo<Esc>" },
|
||||
{ "s_single", "hello", "sfoo<Esc>" },
|
||||
{ "S_whole_line", "hello world", "Sfoo<Esc>" },
|
||||
{ "cl_forward", "hello", "clX<Esc>" },
|
||||
{ "ch_backward", "hello", "llchX<Esc>" },
|
||||
{ "cb_word_back", "hello world", "$cbfoo<Esc>" },
|
||||
{ "ce_word_end", "hello world", "cefoo<Esc>" },
|
||||
{ "c0_to_start", "hello world", "wc0foo<Esc>" },
|
||||
|
||||
-- ===================== yank and paste =====================
|
||||
{ "yw_p_basic", "hello world", "ywwP" },
|
||||
{ "dw_p_paste", "hello world", "dwP" },
|
||||
{ "dd_p_paste", "hello world", "ddp" },
|
||||
{ "y_dollar_p", "hello world", "wy$P" },
|
||||
{ "ye_p", "hello world", "yewP" },
|
||||
{ "yy_p", "hello world", "yyp" },
|
||||
{ "Y_p", "hello world", "Yp" },
|
||||
{ "p_after_x", "hello", "xp" },
|
||||
{ "P_before", "hello", "llxP" },
|
||||
{ "paste_empty", "hello", "p" },
|
||||
|
||||
-- ===================== replace =====================
|
||||
{ "r_replace", "hello", "ra" },
|
||||
{ "r_middle", "hello", "llra" },
|
||||
{ "r_at_end", "hello", "$ra" },
|
||||
{ "r_space", "hello", "r " },
|
||||
{ "r_with_count", "hello", "3rx" },
|
||||
|
||||
-- ===================== case operations =====================
|
||||
{ "tilde_single", "hello", "~" },
|
||||
{ "tilde_count", "hello", "3~" },
|
||||
{ "tilde_at_end", "HELLO", "$~" },
|
||||
{ "tilde_mixed", "hElLo", "5~" },
|
||||
{ "gu_word", "HELLO world", "guw" },
|
||||
{ "gU_word", "hello WORLD", "gUw" },
|
||||
{ "gu_dollar", "HELLO WORLD", "gu$" },
|
||||
{ "gU_dollar", "hello world", "gU$" },
|
||||
{ "gu_0", "HELLO WORLD", "$gu0" },
|
||||
{ "gU_0", "hello world", "$gU0" },
|
||||
{ "gtilde_word", "hello WORLD", "g~w" },
|
||||
{ "gtilde_dollar", "hello WORLD", "g~$" },
|
||||
|
||||
-- ===================== text objects: word =====================
|
||||
{ "diw_inner", "one two three", "wdiw" },
|
||||
{ "ciw_replace", "hello world", "ciwfoo<Esc>" },
|
||||
{ "daw_around", "one two three", "wdaw" },
|
||||
{ "yiw_p", "hello world", "yiwAp <Esc>p" },
|
||||
{ "diW_big_inner", "one-two three", "diW" },
|
||||
{ "daW_big_around", "one two-three end", "wdaW" },
|
||||
{ "ciW_big", "one-two three", "ciWx<Esc>" },
|
||||
|
||||
-- ===================== text objects: quotes =====================
|
||||
{ "di_dquote", 'one "two" three', 'f"di"' },
|
||||
{ "da_dquote", 'one "two" three', 'f"da"' },
|
||||
{ "ci_dquote", 'one "two" three', 'f"ci"x<Esc>' },
|
||||
{ "di_squote", "one 'two' three", "f'di'" },
|
||||
{ "da_squote", "one 'two' three", "f'da'" },
|
||||
{ "di_backtick", "one `two` three", "f`di`" },
|
||||
{ "da_backtick", "one `two` three", "f`da`" },
|
||||
{ "ci_dquote_empty", 'one "" three', 'f"ci"x<Esc>' },
|
||||
|
||||
-- ===================== text objects: delimiters =====================
|
||||
{ "di_paren", "one (two) three", "f(di(" },
|
||||
{ "da_paren", "one (two) three", "f(da(" },
|
||||
{ "ci_paren", "one (two) three", "f(ci(x<Esc>" },
|
||||
{ "di_brace", "one {two} three", "f{di{" },
|
||||
{ "da_brace", "one {two} three", "f{da{" },
|
||||
{ "di_bracket", "one [two] three", "f[di[" },
|
||||
{ "da_bracket", "one [two] three", "f[da[" },
|
||||
{ "di_angle", "one <two> three", "f<di<" },
|
||||
{ "da_angle", "one <two> three", "f<da<" },
|
||||
{ "di_paren_nested", "fn(a, (b, c))", "f(di(" },
|
||||
{ "di_paren_empty", "fn() end", "f(di(" },
|
||||
{ "dib_alias", "one (two) three", "f(dib" },
|
||||
{ "diB_alias", "one {two} three", "f{diB" },
|
||||
|
||||
-- ===================== delimiter matching =====================
|
||||
{ "percent_paren", "(hello) world", "%" },
|
||||
{ "percent_brace", "{hello} world", "%" },
|
||||
{ "percent_bracket", "[hello] world", "%" },
|
||||
{ "percent_from_close", "(hello) world", "f)%" },
|
||||
{ "d_percent_paren", "(hello) world", "d%" },
|
||||
|
||||
-- ===================== insert mode entry =====================
|
||||
{ "i_insert", "hello", "iX<Esc>" },
|
||||
{ "a_append", "hello", "aX<Esc>" },
|
||||
{ "I_front", " hello", "IX<Esc>" },
|
||||
{ "A_end", "hello", "AX<Esc>" },
|
||||
{ "o_open_below", "hello", "oworld<Esc>" },
|
||||
{ "O_open_above", "hello", "Oworld<Esc>" },
|
||||
|
||||
-- ===================== insert mode operations =====================
|
||||
{ "empty_input", "", "i hello<Esc>" },
|
||||
{ "insert_escape", "hello", "aX<Esc>" },
|
||||
{ "ctrl_w_del_word", "hello world", "A<C-w><Esc>" },
|
||||
{ "ctrl_h_backspace", "hello", "A<C-h><Esc>" },
|
||||
|
||||
-- ===================== undo / redo =====================
|
||||
{ "u_undo_delete", "hello world", "dwu" },
|
||||
{ "u_undo_change", "hello world", "ciwfoo<Esc>u" },
|
||||
{ "u_undo_x", "hello", "xu" },
|
||||
{ "ctrl_r_redo", "hello", "xu<C-r>" },
|
||||
{ "u_multiple", "hello world", "xdwu" },
|
||||
{ "redo_after_undo", "hello world", "dwu<C-r>" },
|
||||
|
||||
-- ===================== dot repeat =====================
|
||||
{ "dot_repeat_x", "hello", "x." },
|
||||
{ "dot_repeat_dw", "one two three", "dw." },
|
||||
{ "dot_repeat_cw", "one two three", "cwfoo<Esc>w." },
|
||||
{ "dot_repeat_r", "hello", "ra.." },
|
||||
{ "dot_repeat_s", "hello", "sX<Esc>l." },
|
||||
|
||||
-- ===================== counts =====================
|
||||
{ "count_h", "hello world", "$3h" },
|
||||
{ "count_l", "hello world", "3l" },
|
||||
{ "count_w", "one two three four", "2w" },
|
||||
{ "count_b", "one two three four", "$2b" },
|
||||
{ "count_x", "hello", "3x" },
|
||||
{ "count_dw", "one two three four", "2dw" },
|
||||
{ "verb_count_motion", "one two three four", "d2w" },
|
||||
{ "count_s", "hello", "3sX<Esc>" },
|
||||
|
||||
-- ===================== indent / dedent =====================
|
||||
{ "indent_line", "hello", ">>" },
|
||||
{ "dedent_line", "\thello", "<<" },
|
||||
{ "indent_double", "hello", ">>>>" },
|
||||
|
||||
-- ===================== join =====================
|
||||
{ "J_join_lines", "hello\nworld", "J" },
|
||||
|
||||
-- ===================== case in visual =====================
|
||||
{ "v_u_lower", "HELLO", "vlllu" },
|
||||
{ "v_U_upper", "hello", "vlllU" },
|
||||
|
||||
-- ===================== visual mode =====================
|
||||
{ "v_d_delete", "hello world", "vwwd" },
|
||||
{ "v_x_delete", "hello world", "vwwx" },
|
||||
{ "v_c_change", "hello world", "vwcfoo<Esc>" },
|
||||
{ "v_y_p_yank", "hello world", "vwyAp <Esc>p" },
|
||||
{ "v_dollar_d", "hello world", "wv$d" },
|
||||
{ "v_0_d", "hello world", "$v0d" },
|
||||
{ "ve_d", "hello world", "ved" },
|
||||
{ "v_o_swap", "hello world", "vllod" },
|
||||
{ "v_r_replace", "hello", "vlllrx" },
|
||||
{ "v_tilde_case", "hello", "vlll~" },
|
||||
|
||||
-- ===================== visual line mode =====================
|
||||
{ "V_d_delete", "hello world", "Vd" },
|
||||
{ "V_y_p", "hello world", "Vyp" },
|
||||
{ "V_S_change", "hello world", "VSfoo<Esc>" },
|
||||
|
||||
-- ===================== increment / decrement =====================
|
||||
{ "ctrl_a_inc", "num 5 end", "w<C-a>" },
|
||||
{ "ctrl_x_dec", "num 5 end", "w<C-x>" },
|
||||
{ "ctrl_a_negative", "num -3 end", "w<C-a>" },
|
||||
{ "ctrl_x_to_neg", "num 0 end", "w<C-x>" },
|
||||
{ "ctrl_a_count", "num 5 end", "w3<C-a>" },
|
||||
|
||||
-- ===================== misc / edge cases =====================
|
||||
{ "delete_empty", "", "x" },
|
||||
{ "undo_on_empty", "", "u" },
|
||||
{ "w_single_char", "a b c", "w" },
|
||||
{ "dw_last_word", "hello", "dw" },
|
||||
{ "dollar_single", "h", "$" },
|
||||
{ "caret_no_ws", "hello", "$^" },
|
||||
{ "f_last_char", "hello", "fo" },
|
||||
{ "r_on_space", "hello world", "5|r-" },
|
||||
}
|
||||
|
||||
-- Map vim special key names to Rust string escape sequences
|
||||
local key_to_bytes = {
|
||||
["<Esc>"] = "\\x1b",
|
||||
["<CR>"] = "\\r",
|
||||
["<BS>"] = "\\x7f",
|
||||
["<Tab>"] = "\\t",
|
||||
["<Del>"] = "\\x1b[3~",
|
||||
["<Up>"] = "\\x1b[A",
|
||||
["<Down>"] = "\\x1b[B",
|
||||
["<Right>"] = "\\x1b[C",
|
||||
["<Left>"] = "\\x1b[D",
|
||||
["<Home>"] = "\\x1b[H",
|
||||
["<End>"] = "\\x1b[F",
|
||||
}
|
||||
|
||||
-- Convert vim key notation to Rust string escape sequences
|
||||
local function keys_to_rust(keys)
|
||||
local result = keys
|
||||
result = result:gsub("<C%-(.)>", function(ch)
|
||||
local byte = string.byte(ch:lower()) - string.byte('a') + 1
|
||||
return string.format("\\x%02x", byte)
|
||||
end)
|
||||
for name, bytes in pairs(key_to_bytes) do
|
||||
result = result:gsub(vim.pesc(name), bytes)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Escape a string for use in a Rust string literal
|
||||
local function rust_escape(s)
|
||||
return s:gsub("\\", "\\\\"):gsub('"', '\\"'):gsub("\n", "\\n"):gsub("\t", "\\t")
|
||||
end
|
||||
|
||||
io.write("vi_test! {\n")
|
||||
|
||||
for i, test in ipairs(tests) do
|
||||
local name, input, keys = test[1], test[2], test[3]
|
||||
|
||||
-- Fresh buffer and register state
|
||||
local input_lines = vim.split(input, "\n", { plain = true })
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, input_lines)
|
||||
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||
vim.fn.setreg('"', '')
|
||||
|
||||
-- Execute the key sequence synchronously
|
||||
local translated = vim.api.nvim_replace_termcodes(keys, true, false, true)
|
||||
vim.api.nvim_feedkeys(translated, "ntx", false)
|
||||
vim.api.nvim_exec_autocmds("CursorMoved", {})
|
||||
|
||||
-- Capture result
|
||||
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||
local result = table.concat(lines, "\n")
|
||||
local cursor_col = vim.api.nvim_win_get_cursor(0)[2]
|
||||
|
||||
local rust_keys = keys_to_rust(keys)
|
||||
local rust_input = rust_escape(input)
|
||||
local rust_result = rust_escape(result)
|
||||
|
||||
local sep = ";"
|
||||
if i == #tests then sep = "" end
|
||||
|
||||
io.write(string.format('\tvi_%s: "%s" => "%s" => "%s", %d%s\n',
|
||||
name, rust_input, rust_keys, rust_result, cursor_col, sep))
|
||||
end
|
||||
|
||||
io.write("}\n")
|
||||
|
||||
vim.cmd("qa!")
|
||||
Reference in New Issue
Block a user