Compare commits
2 Commits
a43f8a6dde
...
bc30521e47
| Author | SHA1 | Date | |
|---|---|---|---|
| bc30521e47 | |||
| 490ce4571d |
@@ -250,6 +250,7 @@ impl Dispatcher {
|
|||||||
NdRule::CaseNode { .. } => self.exec_case(node)?,
|
NdRule::CaseNode { .. } => self.exec_case(node)?,
|
||||||
NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?,
|
NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?,
|
||||||
NdRule::FuncDef { .. } => self.exec_func_def(node)?,
|
NdRule::FuncDef { .. } => self.exec_func_def(node)?,
|
||||||
|
NdRule::Negate { .. } => self.exec_negated(node)?,
|
||||||
NdRule::Command { .. } => self.dispatch_cmd(node)?,
|
NdRule::Command { .. } => self.dispatch_cmd(node)?,
|
||||||
NdRule::Test { .. } => self.exec_test(node)?,
|
NdRule::Test { .. } => self.exec_test(node)?,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
@@ -284,6 +285,16 @@ impl Dispatcher {
|
|||||||
self.exec_cmd(node)
|
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<()> {
|
pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> {
|
||||||
let NdRule::Conjunction { elements } = conjunction.class else {
|
let NdRule::Conjunction { elements } = conjunction.class else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
@@ -578,6 +589,7 @@ impl Dispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
state::set_status(0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -714,9 +726,13 @@ impl Dispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matched && !else_block.is_empty() {
|
if !matched {
|
||||||
for node in else_block {
|
if !else_block.is_empty() {
|
||||||
s.dispatch_node(node)?;
|
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 {
|
pub fn is_subsh(tk: Option<Tk>) -> bool {
|
||||||
tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH))
|
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",
|
"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"];
|
pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"];
|
||||||
@@ -166,6 +166,7 @@ pub enum TkRule {
|
|||||||
ErrPipe,
|
ErrPipe,
|
||||||
And,
|
And,
|
||||||
Or,
|
Or,
|
||||||
|
Bang,
|
||||||
Bg,
|
Bg,
|
||||||
Sep,
|
Sep,
|
||||||
Redir,
|
Redir,
|
||||||
@@ -250,6 +251,7 @@ pub struct LexStream {
|
|||||||
quote_state: QuoteState,
|
quote_state: QuoteState,
|
||||||
brc_grp_depth: usize,
|
brc_grp_depth: usize,
|
||||||
brc_grp_start: Option<usize>,
|
brc_grp_start: Option<usize>,
|
||||||
|
case_depth: usize,
|
||||||
flags: LexFlags,
|
flags: LexFlags,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +273,6 @@ bitflags! {
|
|||||||
/// The lexer has no more tokens to produce
|
/// The lexer has no more tokens to produce
|
||||||
const STALE = 0b0001000000;
|
const STALE = 0b0001000000;
|
||||||
const EXPECTING_IN = 0b0010000000;
|
const EXPECTING_IN = 0b0010000000;
|
||||||
const IN_CASE = 0b0100000000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,6 +307,7 @@ impl LexStream {
|
|||||||
quote_state: QuoteState::default(),
|
quote_state: QuoteState::default(),
|
||||||
brc_grp_depth: 0,
|
brc_grp_depth: 0,
|
||||||
brc_grp_start: None,
|
brc_grp_start: None,
|
||||||
|
case_depth: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Returns a slice of the source input using the given range
|
/// Returns a slice of the source input using the given range
|
||||||
@@ -453,7 +455,7 @@ impl LexStream {
|
|||||||
let mut chars = slice.chars().peekable();
|
let mut chars = slice.chars().peekable();
|
||||||
let can_be_subshell = chars.peek() == Some(&'(');
|
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())
|
&& let Some(count) = case_pat_lookahead(chars.clone())
|
||||||
{
|
{
|
||||||
pos += count;
|
pos += count;
|
||||||
@@ -731,7 +733,7 @@ impl LexStream {
|
|||||||
"case" | "select" | "for" => {
|
"case" | "select" | "for" => {
|
||||||
new_tk.mark(TkFlags::KEYWORD);
|
new_tk.mark(TkFlags::KEYWORD);
|
||||||
self.flags |= LexFlags::EXPECTING_IN;
|
self.flags |= LexFlags::EXPECTING_IN;
|
||||||
self.flags |= LexFlags::IN_CASE;
|
self.case_depth += 1;
|
||||||
self.set_next_is_cmd(false);
|
self.set_next_is_cmd(false);
|
||||||
}
|
}
|
||||||
"in" if self.flags.contains(LexFlags::EXPECTING_IN) => {
|
"in" if self.flags.contains(LexFlags::EXPECTING_IN) => {
|
||||||
@@ -739,8 +741,8 @@ impl LexStream {
|
|||||||
self.flags &= !LexFlags::EXPECTING_IN;
|
self.flags &= !LexFlags::EXPECTING_IN;
|
||||||
}
|
}
|
||||||
_ if is_keyword(text) => {
|
_ if is_keyword(text) => {
|
||||||
if text == "esac" && self.flags.contains(LexFlags::IN_CASE) {
|
if text == "esac" && self.case_depth > 0 {
|
||||||
self.flags &= !LexFlags::IN_CASE;
|
self.case_depth -= 1;
|
||||||
}
|
}
|
||||||
new_tk.mark(TkFlags::KEYWORD);
|
new_tk.mark(TkFlags::KEYWORD);
|
||||||
}
|
}
|
||||||
@@ -881,6 +883,14 @@ impl Iterator for LexStream {
|
|||||||
return self.next();
|
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;
|
let ch_idx = self.cursor;
|
||||||
self.cursor += 1;
|
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 {
|
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> {
|
pub fn new() -> ShResult<Self> {
|
||||||
let ignore_dups = crate::state::read_shopts(|s| s.core.hist_ignore_dupes);
|
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 max_hist = crate::state::read_shopts(|s| s.core.max_hist);
|
||||||
|
|
||||||
let path = PathBuf::from(env::var("SHEDHIST").unwrap_or({
|
let path = PathBuf::from(env::var("SHEDHIST").unwrap_or({
|
||||||
let home = env::var("HOME").unwrap();
|
let home = env::var("HOME").unwrap();
|
||||||
format!("{home}/.shed_history")
|
format!("{home}/.shed_history")
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let mut entries = read_hist_file(&path)?;
|
let mut entries = read_hist_file(&path)?;
|
||||||
|
|
||||||
// Enforce max_hist limit on loaded entries (negative = unlimited)
|
// Enforce max_hist limit on loaded entries (negative = unlimited)
|
||||||
if max_hist >= 0 && entries.len() > max_hist as usize {
|
if max_hist >= 0 && entries.len() > max_hist as usize {
|
||||||
entries = entries.split_off(entries.len() - max_hist as usize);
|
entries = entries.split_off(entries.len() - max_hist as usize);
|
||||||
}
|
}
|
||||||
|
|
||||||
let search_mask = dedupe_entries(&entries);
|
let search_mask = dedupe_entries(&entries);
|
||||||
let cursor = search_mask.len();
|
let cursor = search_mask.len();
|
||||||
let max_size = if max_hist < 0 {
|
let max_size = if max_hist < 0 {
|
||||||
@@ -236,6 +254,7 @@ impl History {
|
|||||||
} else {
|
} else {
|
||||||
Some(max_hist as u32)
|
Some(max_hist as u32)
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path,
|
path,
|
||||||
entries,
|
entries,
|
||||||
|
|||||||
@@ -823,7 +823,7 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
Some(self.line_bounds(line_no))
|
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) {
|
let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) {
|
||||||
self.cursor.get()
|
self.cursor.get()
|
||||||
} else {
|
} else {
|
||||||
@@ -835,7 +835,35 @@ impl LineBuf {
|
|||||||
self.end_of_word_forward(self.cursor.get(), word)
|
self.end_of_word_forward(self.cursor.get(), word)
|
||||||
};
|
};
|
||||||
(start, end)
|
(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) {
|
pub fn this_line_exclusive(&mut self) -> (usize, usize) {
|
||||||
let line_no = self.cursor_line_number();
|
let line_no = self.cursor_line_number();
|
||||||
let (start, mut end) = self.line_bounds(line_no);
|
let (start, mut end) = self.line_bounds(line_no);
|
||||||
@@ -947,17 +975,14 @@ impl LineBuf {
|
|||||||
dir: Direction,
|
dir: Direction,
|
||||||
) -> Box<dyn Iterator<Item = usize>> {
|
) -> Box<dyn Iterator<Item = usize>> {
|
||||||
self.update_graphemes_lazy();
|
self.update_graphemes_lazy();
|
||||||
let skip = pos + 1;
|
let len = self.grapheme_indices().len();
|
||||||
match dir {
|
match dir {
|
||||||
Direction::Forward => Box::new(self.grapheme_indices().to_vec().into_iter().skip(skip))
|
Direction::Forward => Box::new(pos + 1..len) as Box<dyn Iterator<Item = usize>>,
|
||||||
as Box<dyn Iterator<Item = usize>>,
|
Direction::Backward => Box::new((0..pos).rev()) 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>>,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn is_word_bound(&mut self, pos: usize, word: Word, dir: Direction) -> bool {
|
pub fn is_word_bound(&mut self, pos: usize, word: Word, dir: Direction) -> bool {
|
||||||
let clamped_pos = ClampedUsize::new(pos, self.cursor.max, true);
|
let clamped_pos = ClampedUsize::new(pos, self.cursor.max, true);
|
||||||
log::debug!("clamped_pos: {}", clamped_pos.get());
|
|
||||||
let cur_char = self
|
let cur_char = self
|
||||||
.grapheme_at(clamped_pos.get())
|
.grapheme_at(clamped_pos.get())
|
||||||
.map(|c| c.to_string())
|
.map(|c| c.to_string())
|
||||||
@@ -1193,20 +1218,6 @@ impl LineBuf {
|
|||||||
Bound::Around => {
|
Bound::Around => {
|
||||||
// End excludes the quote, so push it forward
|
// End excludes the quote, so push it forward
|
||||||
end += 1;
|
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
|
/// Find the start of the current/previous word backward
|
||||||
pub fn start_of_word_backward(&mut self, mut pos: usize, word: Word) -> usize {
|
pub fn start_of_word_backward(&mut self, mut pos: usize, word: Word) -> usize {
|
||||||
let default = 0;
|
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();
|
let mut indices_iter = (0..pos).rev().peekable();
|
||||||
|
|
||||||
match word {
|
match word {
|
||||||
@@ -2000,10 +2016,12 @@ impl LineBuf {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let start = self.index_byte_pos(pos);
|
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);
|
self.buffer.replace_range(start..end, new);
|
||||||
}
|
}
|
||||||
pub fn calc_indent_level(&mut self) {
|
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
|
let to_cursor = self
|
||||||
.slice_to_cursor()
|
.slice_to_cursor()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
@@ -2377,15 +2395,28 @@ impl LineBuf {
|
|||||||
MotionCmd(_count, Motion::WholeBuffer) => {
|
MotionCmd(_count, Motion::WholeBuffer) => {
|
||||||
MotionKind::Exclusive((0, self.grapheme_indices().len()))
|
MotionKind::Exclusive((0, self.grapheme_indices().len()))
|
||||||
}
|
}
|
||||||
MotionCmd(_count, Motion::BeginningOfBuffer) => MotionKind::On(0),
|
MotionCmd(_count, Motion::StartOfBuffer) => {
|
||||||
MotionCmd(_count, Motion::EndOfBuffer) => {
|
MotionKind::InclusiveWithTargetCol((0, self.end_of_line()), 0)
|
||||||
if self.cursor.exclusive {
|
|
||||||
MotionKind::On(self.grapheme_indices().len().saturating_sub(1))
|
|
||||||
} else {
|
|
||||||
MotionKind::On(self.grapheme_indices().len())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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)) => {
|
MotionCmd(count, Motion::Range(start, end)) => {
|
||||||
let mut final_end = end;
|
let mut final_end = end;
|
||||||
if self.cursor.exclusive {
|
if self.cursor.exclusive {
|
||||||
@@ -2607,8 +2638,8 @@ impl LineBuf {
|
|||||||
MotionKind::InclusiveWithTargetCol(..) | MotionKind::ExclusiveWithTargetCol(..)
|
MotionKind::InclusiveWithTargetCol(..) | MotionKind::ExclusiveWithTargetCol(..)
|
||||||
) || matches!(self.select_mode, Some(SelectMode::Line(_)));
|
) || matches!(self.select_mode, Some(SelectMode::Line(_)));
|
||||||
let register_content = if is_linewise {
|
let register_content = if is_linewise {
|
||||||
if !text.ends_with('\n') && !text.is_empty() {
|
if text.ends_with('\n') && !text.is_empty() {
|
||||||
text.push('\n');
|
text = text.strip_suffix('\n').unwrap().to_string();
|
||||||
}
|
}
|
||||||
RegisterContent::Line(text)
|
RegisterContent::Line(text)
|
||||||
} else {
|
} else {
|
||||||
@@ -2645,19 +2676,27 @@ impl LineBuf {
|
|||||||
self.apply_motion(motion);
|
self.apply_motion(motion);
|
||||||
}
|
}
|
||||||
Verb::ReplaceCharInplace(ch, count) => {
|
Verb::ReplaceCharInplace(ch, count) => {
|
||||||
for i in 0..count {
|
if let Some((start,end)) = self.select_range() {
|
||||||
let mut buf = [0u8; 4];
|
let end = (end + 1).min(self.grapheme_indices().len()); // inclusive
|
||||||
let new = ch.encode_utf8(&mut buf);
|
let replaced = ch.to_string().repeat(end.saturating_sub(start));
|
||||||
self.replace_at_cursor(new);
|
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
|
// try to increment the cursor until we are on the last iteration
|
||||||
// or until we hit the end of the buffer
|
// or until we hit the end of the buffer
|
||||||
if i != count.saturating_sub(1) && !self.cursor.inc() {
|
if i != count.saturating_sub(1) && !self.cursor.inc() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Verb::ToggleCaseInplace(count) => {
|
Verb::ToggleCaseInplace(count) => {
|
||||||
|
let mut did_something = false;
|
||||||
for i in 0..count {
|
for i in 0..count {
|
||||||
let Some(gr) = self.grapheme_at_cursor() else {
|
let Some(gr) = self.grapheme_at_cursor() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -2679,10 +2718,14 @@ impl LineBuf {
|
|||||||
|
|
||||||
// try to increment the cursor until we are on the last iteration
|
// try to increment the cursor until we are on the last iteration
|
||||||
// or until we hit the end of the buffer
|
// or until we hit the end of the buffer
|
||||||
|
did_something = true;
|
||||||
if i != count.saturating_sub(1) && !self.cursor.inc() {
|
if i != count.saturating_sub(1) && !self.cursor.inc() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if did_something {
|
||||||
|
self.cursor.inc();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Verb::ToggleCaseRange => {
|
Verb::ToggleCaseRange => {
|
||||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
let Some((start, end)) = self.range_from_motion(&motion) else {
|
||||||
@@ -2707,6 +2750,7 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
self.replace_at(i, new);
|
self.replace_at(i, new);
|
||||||
}
|
}
|
||||||
|
self.cursor.set(start);
|
||||||
}
|
}
|
||||||
Verb::ToLower => {
|
Verb::ToLower => {
|
||||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
let Some((start, end)) = self.range_from_motion(&motion) else {
|
||||||
@@ -2731,6 +2775,7 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
self.replace_at(i, new);
|
self.replace_at(i, new);
|
||||||
}
|
}
|
||||||
|
self.cursor.set(start);
|
||||||
}
|
}
|
||||||
Verb::ToUpper => {
|
Verb::ToUpper => {
|
||||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
let Some((start, end)) = self.range_from_motion(&motion) else {
|
||||||
@@ -2755,6 +2800,7 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
self.replace_at(i, new);
|
self.replace_at(i, new);
|
||||||
}
|
}
|
||||||
|
self.cursor.set(start);
|
||||||
}
|
}
|
||||||
Verb::Redo | Verb::Undo => {
|
Verb::Redo | Verb::Undo => {
|
||||||
let (edit_provider, edit_receiver) = match verb {
|
let (edit_provider, edit_receiver) = match verb {
|
||||||
@@ -2810,33 +2856,50 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
match content {
|
match content {
|
||||||
RegisterContent::Span(ref text) => {
|
RegisterContent::Span(ref text) => {
|
||||||
let insert_idx = match anchor {
|
match anchor {
|
||||||
Anchor::After => self
|
Anchor::After => {
|
||||||
.cursor
|
let insert_idx = self
|
||||||
.get()
|
.cursor
|
||||||
.saturating_add(1)
|
.get()
|
||||||
.min(self.grapheme_indices().len()),
|
.saturating_add(1)
|
||||||
Anchor::Before => self.cursor.get(),
|
.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) => {
|
RegisterContent::Line(ref text) => {
|
||||||
let insert_idx = match anchor {
|
let insert_idx = match anchor {
|
||||||
Anchor::After => self.end_of_line(),
|
Anchor::After => self.end_of_line(),
|
||||||
Anchor::Before => self.start_of_line(),
|
Anchor::Before => self.start_of_line(),
|
||||||
};
|
};
|
||||||
let needs_newline = self
|
let mut full = text.to_string();
|
||||||
.grapheme_before(insert_idx)
|
let mut offset = 0;
|
||||||
.is_some_and(|gr| gr != "\n");
|
|
||||||
if needs_newline {
|
match anchor {
|
||||||
let full = format!("\n{}", text);
|
Anchor::After => {
|
||||||
self.insert_str_at(insert_idx, &full);
|
if self.grapheme_before(insert_idx).is_none_or(|gr| gr != "\n") {
|
||||||
self.cursor.set(insert_idx + 1);
|
full = format!("\n{text}");
|
||||||
} else {
|
offset += 1;
|
||||||
self.insert_str_at(insert_idx, text);
|
}
|
||||||
self.cursor.set(insert_idx);
|
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 => {}
|
RegisterContent::Empty => {}
|
||||||
}
|
}
|
||||||
@@ -2872,6 +2935,7 @@ impl LineBuf {
|
|||||||
self.force_replace_at(i, " ");
|
self.force_replace_at(i, " ");
|
||||||
}
|
}
|
||||||
last_was_whitespace = false;
|
last_was_whitespace = false;
|
||||||
|
self.cursor.set(i);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
last_was_whitespace = is_whitespace(gr);
|
last_was_whitespace = is_whitespace(gr);
|
||||||
@@ -2907,8 +2971,6 @@ impl LineBuf {
|
|||||||
Verb::Insert(string) => {
|
Verb::Insert(string) => {
|
||||||
self.insert_str_at_cursor(&string);
|
self.insert_str_at_cursor(&string);
|
||||||
let graphemes = string.graphemes(true).count();
|
let graphemes = string.graphemes(true).count();
|
||||||
log::debug!("Inserted string: {string:?}, graphemes: {graphemes}");
|
|
||||||
log::debug!("buffer after insert: {:?}", self.buffer);
|
|
||||||
self.cursor.add(graphemes);
|
self.cursor.add(graphemes);
|
||||||
}
|
}
|
||||||
Verb::Indent => {
|
Verb::Indent => {
|
||||||
@@ -3067,7 +3129,13 @@ impl LineBuf {
|
|||||||
} else {
|
} else {
|
||||||
-(n as i64)
|
-(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() {
|
let end = if self.select_range().is_some() {
|
||||||
if e < self.grapheme_indices().len() - 1 {
|
if e < self.grapheme_indices().len() - 1 {
|
||||||
e
|
e
|
||||||
@@ -3122,9 +3190,21 @@ impl LineBuf {
|
|||||||
} else if let Ok(num) = word.parse::<i64>() {
|
} else if let Ok(num) = word.parse::<i64>() {
|
||||||
let width = word.len();
|
let width = word.len();
|
||||||
let new_num = num + inc;
|
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
|
self
|
||||||
.buffer
|
.buffer
|
||||||
.replace_range(byte_start..byte_end, &format!("{new_num:0>width$}"));
|
.replace_range(byte_start..byte_end, &num_fmt);
|
||||||
self.update_graphemes();
|
self.update_graphemes();
|
||||||
self.cursor.set(s);
|
self.cursor.set(s);
|
||||||
}
|
}
|
||||||
@@ -3144,7 +3224,6 @@ impl LineBuf {
|
|||||||
| Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
|
| Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
|
||||||
|
|
||||||
Verb::ShellCmd(cmd) => {
|
Verb::ShellCmd(cmd) => {
|
||||||
log::debug!("Executing ex-mode command from widget: {cmd}");
|
|
||||||
let mut vars = HashSet::new();
|
let mut vars = HashSet::new();
|
||||||
vars.insert("_BUFFER".into());
|
vars.insert("_BUFFER".into());
|
||||||
vars.insert("_CURSOR".into());
|
vars.insert("_CURSOR".into());
|
||||||
@@ -3187,17 +3266,10 @@ impl LineBuf {
|
|||||||
self.update_graphemes();
|
self.update_graphemes();
|
||||||
self.cursor.set_max(self.buffer.graphemes(true).count());
|
self.cursor.set_max(self.buffer.graphemes(true).count());
|
||||||
self.cursor.set(cursor);
|
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() {
|
if anchor != cursor && self.select_range.is_some() {
|
||||||
self.select_range = Some(ordered(cursor, anchor));
|
self.select_range = Some(ordered(cursor, anchor));
|
||||||
}
|
}
|
||||||
if !keys.is_empty() {
|
if !keys.is_empty() {
|
||||||
log::debug!("Pending widget keys from shell command: {keys}");
|
|
||||||
write_meta(|m| m.set_pending_widget_keys(&keys))
|
write_meta(|m| m.set_pending_widget_keys(&keys))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ pub mod term;
|
|||||||
pub mod vicmd;
|
pub mod vicmd;
|
||||||
pub mod vimode;
|
pub mod vimode;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests;
|
||||||
|
|
||||||
pub mod markers {
|
pub mod markers {
|
||||||
use super::Marker;
|
use super::Marker;
|
||||||
|
|
||||||
@@ -289,6 +292,37 @@ impl ShedVi {
|
|||||||
Ok(new)
|
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 {
|
pub fn with_initial(mut self, initial: &str) -> Self {
|
||||||
self.editor = LineBuf::new().with_initial(initial, 0);
|
self.editor = LineBuf::new().with_initial(initial, 0);
|
||||||
self
|
self
|
||||||
@@ -696,7 +730,8 @@ impl ShedVi {
|
|||||||
|
|
||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
return Ok(None);
|
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();
|
let initial = self.editor.as_str();
|
||||||
match self.history.start_search(initial) {
|
match self.history.start_search(initial) {
|
||||||
Some(entry) => {
|
Some(entry) => {
|
||||||
@@ -814,7 +849,7 @@ impl ShedVi {
|
|||||||
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
|
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
|
||||||
|
|
||||||
let before = self.editor.buffer.clone();
|
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()) {
|
if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) {
|
||||||
for key in keys {
|
for key in keys {
|
||||||
self.handle_key(key)?;
|
self.handle_key(key)?;
|
||||||
@@ -1072,99 +1107,70 @@ impl ShedVi {
|
|||||||
post_mode_change.exec();
|
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 select_mode = None;
|
||||||
let mut is_insert_mode = false;
|
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!(
|
let mut mode: Box<dyn ViMode> = if matches!(
|
||||||
self.mode.report_mode(),
|
self.mode.report_mode(),
|
||||||
ModeReport::Ex | ModeReport::Verbatim
|
ModeReport::Ex | ModeReport::Verbatim
|
||||||
) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE)
|
) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE)
|
||||||
{
|
{
|
||||||
if let Some(saved) = self.saved_mode.take() {
|
if let Some(saved) = self.saved_mode.take() {
|
||||||
saved
|
saved
|
||||||
} else {
|
} else {
|
||||||
Box::new(ViNormal::new())
|
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::VerbatimMode => {
|
||||||
|
self.reader.verbatim_single = true;
|
||||||
Verb::NormalMode => Box::new(ViNormal::new()),
|
Box::new(ViVerbatim::new().with_count(count as u16))
|
||||||
|
|
||||||
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!(),
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
self.swap_mode(&mut mode);
|
Verb::NormalMode => Box::new(ViNormal::new()),
|
||||||
|
|
||||||
if matches!(
|
Verb::ReplaceMode => Box::new(ViReplace::new()),
|
||||||
self.mode.report_mode(),
|
|
||||||
ModeReport::Ex | ModeReport::Verbatim
|
Verb::VisualModeSelectLast => {
|
||||||
) {
|
if self.mode.report_mode() != ModeReport::Visual {
|
||||||
self.saved_mode = Some(mode);
|
self
|
||||||
write_vars(|v| {
|
.editor
|
||||||
v.set_var(
|
.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||||
"SHED_VI_MODE",
|
}
|
||||||
VarKind::Str(self.mode.report_mode().to_string()),
|
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
|
||||||
VarFlags::NONE,
|
self.swap_mode(&mut mode);
|
||||||
)
|
|
||||||
})?;
|
return self.editor.exec_cmd(cmd);
|
||||||
self.prompt.refresh();
|
}
|
||||||
return Ok(());
|
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.swap_mode(&mut mode);
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
self.mode.report_mode(),
|
||||||
|
ModeReport::Ex | ModeReport::Verbatim
|
||||||
|
) {
|
||||||
|
self.saved_mode = Some(mode);
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var(
|
v.set_var(
|
||||||
"SHED_VI_MODE",
|
"SHED_VI_MODE",
|
||||||
@@ -1173,8 +1179,56 @@ impl ShedVi {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
self.prompt.refresh();
|
self.prompt.refresh();
|
||||||
|
|
||||||
return Ok(());
|
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() {
|
} else if cmd.is_cmd_repeat() {
|
||||||
let Some(replay) = self.repeat_action.clone() else {
|
let Some(replay) = self.repeat_action.clone() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -1186,11 +1240,36 @@ impl ShedVi {
|
|||||||
if count > 1 {
|
if count > 1 {
|
||||||
repeat = count as u16;
|
repeat = count as u16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let old_mode = self.mode.report_mode();
|
||||||
|
|
||||||
for _ in 0..repeat {
|
for _ in 0..repeat {
|
||||||
let cmds = cmds.clone();
|
let cmds = cmds.clone();
|
||||||
for cmd in cmds {
|
for (i, cmd) in cmds.iter().enumerate() {
|
||||||
self.editor.exec_cmd(cmd)?
|
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) => {
|
CmdReplay::Single(mut cmd) => {
|
||||||
@@ -1253,7 +1332,7 @@ impl ShedVi {
|
|||||||
self.swap_mode(&mut mode);
|
self.swap_mode(&mut mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.is_repeatable() {
|
if cmd.is_repeatable() && !from_replay {
|
||||||
if self.mode.report_mode() == ModeReport::Visual {
|
if self.mode.report_mode() == ModeReport::Visual {
|
||||||
// The motion is assigned in the line buffer execution, so we also have to
|
// 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
|
// assign it here in order to be able to repeat it
|
||||||
@@ -1272,7 +1351,7 @@ impl ShedVi {
|
|||||||
|
|
||||||
self.editor.exec_cmd(cmd.clone())?;
|
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();
|
self.editor.stop_selecting();
|
||||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||||
self.swap_mode(&mut mode);
|
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> {
|
pub fn marker_for(class: &TkRule) -> Option<Marker> {
|
||||||
match class {
|
match class {
|
||||||
TkRule::Pipe
|
TkRule::Pipe
|
||||||
|
| TkRule::Bang
|
||||||
| TkRule::ErrPipe
|
| TkRule::ErrPipe
|
||||||
| TkRule::And
|
| TkRule::And
|
||||||
| TkRule::Or
|
| TkRule::Or
|
||||||
|
|||||||
@@ -2,6 +2,24 @@ use std::{fmt::Display, sync::Mutex};
|
|||||||
|
|
||||||
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
|
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> {
|
pub fn read_register(ch: Option<char>) -> Option<RegisterContent> {
|
||||||
let lock = REGISTERS.lock().unwrap();
|
let lock = REGISTERS.lock().unwrap();
|
||||||
lock.get_reg(ch).map(|r| r.content().clone())
|
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 {
|
pub struct Registers {
|
||||||
default: Register,
|
default: Register,
|
||||||
a: Register,
|
a: Register,
|
||||||
|
|||||||
@@ -499,6 +499,7 @@ pub struct PollReader {
|
|||||||
parser: Parser,
|
parser: Parser,
|
||||||
collector: KeyCollector,
|
collector: KeyCollector,
|
||||||
byte_buf: VecDeque<u8>,
|
byte_buf: VecDeque<u8>,
|
||||||
|
pub verbatim_single: bool,
|
||||||
pub verbatim: bool,
|
pub verbatim: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,6 +509,7 @@ impl PollReader {
|
|||||||
parser: Parser::new(),
|
parser: Parser::new(),
|
||||||
collector: KeyCollector::new(),
|
collector: KeyCollector::new(),
|
||||||
byte_buf: VecDeque::new(),
|
byte_buf: VecDeque::new(),
|
||||||
|
verbatim_single: false,
|
||||||
verbatim: false,
|
verbatim: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,6 +533,15 @@ impl PollReader {
|
|||||||
None
|
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]) {
|
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||||
self.byte_buf.extend(bytes);
|
self.byte_buf.extend(bytes);
|
||||||
}
|
}
|
||||||
@@ -544,17 +555,28 @@ impl Default for PollReader {
|
|||||||
|
|
||||||
impl KeyReader for PollReader {
|
impl KeyReader for PollReader {
|
||||||
fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr> {
|
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 self.verbatim {
|
||||||
if let Some(paste) = self.handle_bracket_paste() {
|
if let Some(paste) = self.handle_bracket_paste() {
|
||||||
return Ok(Some(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
|
// If we're in verbatim mode but haven't seen the end marker yet, don't attempt to parse keys
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
} else if self.byte_buf.len() == 1
|
} else if self.byte_buf.front() == Some(&b'\x1b') {
|
||||||
&& self.byte_buf.front() == Some(&b'\x1b') {
|
// Escape: if it's the only byte, or the next byte isn't a valid
|
||||||
// User pressed escape
|
// escape sequence prefix ([ or O), emit a standalone Escape
|
||||||
self.byte_buf.pop_front(); // Consume the escape byte
|
if self.byte_buf.len() == 1
|
||||||
return Ok(Some(KeyEvent(KeyCode::Esc, ModKeys::empty())));
|
|| !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() {
|
while let Some(byte) = self.byte_buf.pop_front() {
|
||||||
self.parser.advance(&mut self.collector, &[byte]);
|
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,
|
HalfOfScreen,
|
||||||
HalfOfScreenLineText,
|
HalfOfScreenLineText,
|
||||||
WholeBuffer,
|
WholeBuffer,
|
||||||
BeginningOfBuffer,
|
StartOfBuffer,
|
||||||
EndOfBuffer,
|
EndOfBuffer,
|
||||||
ToColumn,
|
ToColumn,
|
||||||
ToDelimMatch,
|
ToDelimMatch,
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ impl ViInsert {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
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 {
|
pub fn with_count(mut self, repeat_count: u16) -> Self {
|
||||||
self.repeat_count = repeat_count;
|
self.repeat_count = repeat_count;
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ impl ViNormal {
|
|||||||
'g' => {
|
'g' => {
|
||||||
chars_clone.next();
|
chars_clone.next();
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
|
break 'motion_parse Some(MotionCmd(count, Motion::StartOfBuffer));
|
||||||
}
|
}
|
||||||
'e' => {
|
'e' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
|
|||||||
@@ -4,19 +4,11 @@ use crate::readline::vicmd::{CmdFlags, RegisterName, To, Verb, VerbCmd, ViCmd};
|
|||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct ViVerbatim {
|
pub struct ViVerbatim {
|
||||||
pending_seq: String,
|
|
||||||
sent_cmd: Vec<ViCmd>,
|
sent_cmd: Vec<ViCmd>,
|
||||||
repeat_count: u16,
|
repeat_count: u16,
|
||||||
read_one: bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViVerbatim {
|
impl ViVerbatim {
|
||||||
pub fn read_one() -> Self {
|
|
||||||
Self {
|
|
||||||
read_one: true,
|
|
||||||
..Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
@@ -31,7 +23,7 @@ impl ViVerbatim {
|
|||||||
impl ViMode for ViVerbatim {
|
impl ViMode for ViVerbatim {
|
||||||
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||||
match key {
|
match key {
|
||||||
E(K::Verbatim(seq), _mods) if self.read_one => {
|
E(K::Verbatim(seq), _mods) => {
|
||||||
log::debug!("Received verbatim key sequence: {:?}", seq);
|
log::debug!("Received verbatim key sequence: {:?}", seq);
|
||||||
let cmd = ViCmd {
|
let cmd = ViCmd {
|
||||||
register: RegisterName::default(),
|
register: RegisterName::default(),
|
||||||
@@ -43,22 +35,6 @@ impl ViMode for ViVerbatim {
|
|||||||
self.sent_cmd.push(cmd.clone());
|
self.sent_cmd.push(cmd.clone());
|
||||||
Some(cmd)
|
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),
|
_ => common_cmds(key),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ impl ViVisual {
|
|||||||
let ch = chars_clone.next()?;
|
let ch = chars_clone.next()?;
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
register,
|
register,
|
||||||
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
|
verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch,1))),
|
||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
@@ -237,6 +237,24 @@ impl ViVisual {
|
|||||||
flags: CmdFlags::empty(),
|
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' => {
|
'U' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
register,
|
register,
|
||||||
@@ -283,8 +301,13 @@ impl ViVisual {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
'y' => {
|
'y' => {
|
||||||
chars = chars_clone;
|
return Some(ViCmd {
|
||||||
break 'verb_parse Some(VerbCmd(count, Verb::Yank));
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Yank)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
'd' => {
|
'd' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -335,7 +358,7 @@ impl ViVisual {
|
|||||||
'g' => {
|
'g' => {
|
||||||
chars_clone.next();
|
chars_clone.next();
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
|
break 'motion_parse Some(MotionCmd(count, Motion::StartOfBuffer));
|
||||||
}
|
}
|
||||||
'e' => {
|
'e' => {
|
||||||
chars_clone.next();
|
chars_clone.next();
|
||||||
|
|||||||
115
src/testutil.rs
115
src/testutil.rs
@@ -1,9 +1,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, HashSet},
|
||||||
env,
|
env,
|
||||||
os::fd::{AsRawFd, OwnedFd},
|
os::fd::{AsRawFd, BorrowedFd, OwnedFd},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{self, MutexGuard},
|
sync::{self, Arc, MutexGuard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use nix::{
|
use nix::{
|
||||||
@@ -14,10 +14,7 @@ use nix::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::ShResult,
|
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}
|
||||||
parse::{Redir, RedirType, execute::exec_input},
|
|
||||||
procio::{IoFrame, IoMode, RedirGuard},
|
|
||||||
state::{MetaTab, SHED},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static TEST_MUTEX: sync::Mutex<()> = sync::Mutex::new(());
|
static TEST_MUTEX: sync::Mutex<()> = sync::Mutex::new(());
|
||||||
@@ -41,7 +38,7 @@ pub struct TestGuard {
|
|||||||
old_cwd: PathBuf,
|
old_cwd: PathBuf,
|
||||||
saved_env: HashMap<String, String>,
|
saved_env: HashMap<String, String>,
|
||||||
pty_master: OwnedFd,
|
pty_master: OwnedFd,
|
||||||
_pty_slave: OwnedFd,
|
pty_slave: OwnedFd,
|
||||||
|
|
||||||
cleanups: Vec<Box<dyn FnOnce()>>
|
cleanups: Vec<Box<dyn FnOnce()>>
|
||||||
}
|
}
|
||||||
@@ -90,17 +87,22 @@ impl TestGuard {
|
|||||||
let old_cwd = env::current_dir().unwrap();
|
let old_cwd = env::current_dir().unwrap();
|
||||||
let saved_env = env::vars().collect();
|
let saved_env = env::vars().collect();
|
||||||
SHED.with(|s| s.save());
|
SHED.with(|s| s.save());
|
||||||
|
save_registers();
|
||||||
Self {
|
Self {
|
||||||
_lock,
|
_lock,
|
||||||
_redir_guard,
|
_redir_guard,
|
||||||
old_cwd,
|
old_cwd,
|
||||||
saved_env,
|
saved_env,
|
||||||
pty_master,
|
pty_master,
|
||||||
_pty_slave: pty_slave,
|
pty_slave,
|
||||||
cleanups: vec![],
|
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) {
|
pub fn add_cleanup(&mut self, f: impl FnOnce() + 'static) {
|
||||||
self.cleanups.push(Box::new(f));
|
self.cleanups.push(Box::new(f));
|
||||||
}
|
}
|
||||||
@@ -151,5 +153,100 @@ impl Drop for TestGuard {
|
|||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
SHED.with(|s| s.restore());
|
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