rustfmt'd the codebase

This commit is contained in:
2026-03-04 19:52:29 -05:00
parent 79cb34246b
commit 0e3f2afe99
51 changed files with 4926 additions and 4131 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ pub struct Highlighter {
style_stack: Vec<StyleSet>,
last_was_reset: bool,
in_selection: bool,
only_hl_visual: bool
only_hl_visual: bool,
}
impl Highlighter {
@@ -39,12 +39,12 @@ impl Highlighter {
style_stack: Vec::new(),
last_was_reset: true, // start as true so we don't emit a leading reset
in_selection: false,
only_hl_visual: false
only_hl_visual: false,
}
}
pub fn only_visual(&mut self, only_visual: bool) {
self.only_hl_visual = only_visual;
}
pub fn only_visual(&mut self, only_visual: bool) {
self.only_hl_visual = only_visual;
}
/// Loads raw input text and annotates it with syntax markers
///
@@ -66,25 +66,25 @@ impl Highlighter {
out
}
pub fn expand_control_chars(&mut self) {
let mut expanded = String::new();
let mut chars = self.input.chars().peekable();
pub fn expand_control_chars(&mut self) {
let mut expanded = String::new();
let mut chars = self.input.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'\n' | '\t' | '\r' => expanded.push(ch),
c if c as u32 <= 0x1F => {
let display = (c as u8 + b'@') as char;
expanded.push_str("\x1b[7m^");
expanded.push(display);
expanded.push_str("\x1b[0m");
}
_ => expanded.push(ch),
}
}
while let Some(ch) = chars.next() {
match ch {
'\n' | '\t' | '\r' => expanded.push(ch),
c if c as u32 <= 0x1F => {
let display = (c as u8 + b'@') as char;
expanded.push_str("\x1b[7m^");
expanded.push(display);
expanded.push_str("\x1b[0m");
}
_ => expanded.push(ch),
}
}
self.input = expanded;
}
self.input = expanded;
}
/// Processes the annotated input and generates ANSI-styled output
///
@@ -104,9 +104,9 @@ impl Highlighter {
self.reapply_style();
self.in_selection = false;
}
_ if self.only_hl_visual => {
self.output.push(ch);
}
_ if self.only_hl_visual => {
self.output.push(ch);
}
markers::STRING_DQ_END
| markers::STRING_SQ_END
| markers::VAR_SUB_END
@@ -471,7 +471,7 @@ impl Highlighter {
}
impl Default for Highlighter {
fn default() -> Self {
Self::new()
}
fn default() -> Self {
Self::new()
}
}

View File

@@ -70,10 +70,10 @@ impl HistEntry {
impl FromStr for HistEntry {
type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let err = Err(ShErr::simple(
ShErrKind::HistoryReadErr,
format!("Bad formatting on history entry '{s}'"),
));
let err = Err(ShErr::simple(
ShErrKind::HistoryReadErr,
format!("Bad formatting on history entry '{s}'"),
));
//: 248972349;148;echo foo; echo bar
let Some(cleaned) = s.strip_prefix(": ") else {
@@ -132,10 +132,10 @@ impl FromStr for HistEntries {
while let Some((i, line)) = lines.next() {
if !line.starts_with(": ") {
return Err(ShErr::simple(
ShErrKind::HistoryReadErr,
format!("Bad formatting on line {i}"),
));
return Err(ShErr::simple(
ShErrKind::HistoryReadErr,
format!("Bad formatting on line {i}"),
));
}
let mut chars = line.chars().peekable();
let mut feeding_lines = true;
@@ -161,10 +161,10 @@ impl FromStr for HistEntries {
}
if feeding_lines {
let Some((_, line)) = lines.next() else {
return Err(ShErr::simple(
ShErrKind::HistoryReadErr,
format!("Bad formatting on line {i}"),
));
return Err(ShErr::simple(
ShErrKind::HistoryReadErr,
format!("Bad formatting on line {i}"),
));
};
chars = line.chars().peekable();
}

View File

@@ -89,110 +89,112 @@ impl KeyEvent {
}
}
}
pub fn as_vim_seq(&self) -> ShResult<String> {
let mut seq = String::new();
let KeyEvent(event, mods) = self;
let mut needs_angle_bracket = false;
pub fn as_vim_seq(&self) -> ShResult<String> {
let mut seq = String::new();
let KeyEvent(event, mods) = self;
let mut needs_angle_bracket = false;
if mods.contains(ModKeys::CTRL) {
seq.push_str("C-");
needs_angle_bracket = true;
}
if mods.contains(ModKeys::ALT) {
seq.push_str("A-");
needs_angle_bracket = true;
}
if mods.contains(ModKeys::SHIFT) {
seq.push_str("S-");
needs_angle_bracket = true;
}
if mods.contains(ModKeys::CTRL) {
seq.push_str("C-");
needs_angle_bracket = true;
}
if mods.contains(ModKeys::ALT) {
seq.push_str("A-");
needs_angle_bracket = true;
}
if mods.contains(ModKeys::SHIFT) {
seq.push_str("S-");
needs_angle_bracket = true;
}
match event {
KeyCode::UnknownEscSeq => return Err(ShErr::simple(
ShErrKind::ParseErr,
"Cannot convert unknown escape sequence to Vim key sequence".to_string(),
)),
KeyCode::Backspace => {
seq.push_str("BS");
needs_angle_bracket = true;
}
KeyCode::BackTab => {
seq.push_str("S-Tab");
needs_angle_bracket = true;
}
KeyCode::BracketedPasteStart => todo!(),
KeyCode::BracketedPasteEnd => todo!(),
KeyCode::Delete => {
seq.push_str("Del");
needs_angle_bracket = true;
}
KeyCode::Down => {
seq.push_str("Down");
needs_angle_bracket = true;
}
KeyCode::End => {
seq.push_str("End");
needs_angle_bracket = true;
}
KeyCode::Enter => {
seq.push_str("Enter");
needs_angle_bracket = true;
}
KeyCode::Esc => {
seq.push_str("Esc");
needs_angle_bracket = true;
}
match event {
KeyCode::UnknownEscSeq => {
return Err(ShErr::simple(
ShErrKind::ParseErr,
"Cannot convert unknown escape sequence to Vim key sequence".to_string(),
));
}
KeyCode::Backspace => {
seq.push_str("BS");
needs_angle_bracket = true;
}
KeyCode::BackTab => {
seq.push_str("S-Tab");
needs_angle_bracket = true;
}
KeyCode::BracketedPasteStart => todo!(),
KeyCode::BracketedPasteEnd => todo!(),
KeyCode::Delete => {
seq.push_str("Del");
needs_angle_bracket = true;
}
KeyCode::Down => {
seq.push_str("Down");
needs_angle_bracket = true;
}
KeyCode::End => {
seq.push_str("End");
needs_angle_bracket = true;
}
KeyCode::Enter => {
seq.push_str("Enter");
needs_angle_bracket = true;
}
KeyCode::Esc => {
seq.push_str("Esc");
needs_angle_bracket = true;
}
KeyCode::F(f) => {
seq.push_str(&format!("F{}", f));
needs_angle_bracket = true;
}
KeyCode::Home => {
seq.push_str("Home");
needs_angle_bracket = true;
}
KeyCode::Insert => {
seq.push_str("Insert");
needs_angle_bracket = true;
}
KeyCode::Left => {
seq.push_str("Left");
needs_angle_bracket = true;
}
KeyCode::Null => todo!(),
KeyCode::PageDown => {
seq.push_str("PgDn");
needs_angle_bracket = true;
}
KeyCode::PageUp => {
seq.push_str("PgUp");
needs_angle_bracket = true;
}
KeyCode::Right => {
seq.push_str("Right");
needs_angle_bracket = true;
}
KeyCode::Tab => {
seq.push_str("Tab");
needs_angle_bracket = true;
}
KeyCode::Up => {
seq.push_str("Up");
needs_angle_bracket = true;
}
KeyCode::Char(ch) => {
seq.push(*ch);
}
KeyCode::Grapheme(gr) => seq.push_str(gr),
KeyCode::Verbatim(s) => seq.push_str(s),
}
KeyCode::F(f) => {
seq.push_str(&format!("F{}", f));
needs_angle_bracket = true;
}
KeyCode::Home => {
seq.push_str("Home");
needs_angle_bracket = true;
}
KeyCode::Insert => {
seq.push_str("Insert");
needs_angle_bracket = true;
}
KeyCode::Left => {
seq.push_str("Left");
needs_angle_bracket = true;
}
KeyCode::Null => todo!(),
KeyCode::PageDown => {
seq.push_str("PgDn");
needs_angle_bracket = true;
}
KeyCode::PageUp => {
seq.push_str("PgUp");
needs_angle_bracket = true;
}
KeyCode::Right => {
seq.push_str("Right");
needs_angle_bracket = true;
}
KeyCode::Tab => {
seq.push_str("Tab");
needs_angle_bracket = true;
}
KeyCode::Up => {
seq.push_str("Up");
needs_angle_bracket = true;
}
KeyCode::Char(ch) => {
seq.push(*ch);
}
KeyCode::Grapheme(gr) => seq.push_str(gr),
KeyCode::Verbatim(s) => seq.push_str(s),
}
if needs_angle_bracket {
Ok(format!("<{}>", seq))
} else {
Ok(seq)
}
}
if needs_angle_bracket {
Ok(format!("<{}>", seq))
} else {
Ok(seq)
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
@@ -204,7 +206,7 @@ pub enum KeyCode {
BracketedPasteEnd,
Char(char),
Grapheme(Arc<str>),
Verbatim(Arc<str>), // For sequences that should be treated as literal input, not parsed into a KeyCode
Verbatim(Arc<str>), // For sequences that should be treated as literal input, not parsed into a KeyCode
Delete,
Down,
End,

View File

@@ -1,5 +1,7 @@
use std::{
collections::HashSet, fmt::Display, ops::{Range, RangeInclusive}
collections::HashSet,
fmt::Display,
ops::{Range, RangeInclusive},
};
use unicode_segmentation::UnicodeSegmentation;
@@ -11,7 +13,10 @@ use super::vicmd::{
};
use crate::{
libsh::{error::ShResult, guards::var_ctx_guard},
parse::{execute::exec_input, lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}},
parse::{
execute::exec_input,
lex::{LexFlags, LexStream, Tk, TkFlags, TkRule},
},
prelude::*,
readline::{
markers,
@@ -297,20 +302,20 @@ impl ClampedUsize {
pub fn sub(&mut self, value: usize) {
self.value = self.value.saturating_sub(value)
}
pub fn wrap_add(&mut self, value: usize) {
self.value = self.ret_wrap_add(value);
}
pub fn wrap_sub(&mut self, value: usize) {
self.value = self.ret_wrap_sub(value);
}
pub fn ret_wrap_add(&self, value: usize) -> usize {
let max = self.upper_bound();
(self.value + value) % (max + 1)
}
pub fn ret_wrap_sub(&self, value: usize) -> usize {
let max = self.upper_bound();
(self.value + (max + 1) - (value % (max + 1))) % (max + 1)
}
pub fn wrap_add(&mut self, value: usize) {
self.value = self.ret_wrap_add(value);
}
pub fn wrap_sub(&mut self, value: usize) {
self.value = self.ret_wrap_sub(value);
}
pub fn ret_wrap_add(&self, value: usize) -> usize {
let max = self.upper_bound();
(self.value + value) % (max + 1)
}
pub fn ret_wrap_sub(&self, value: usize) -> usize {
let max = self.upper_bound();
(self.value + (max + 1) - (value % (max + 1))) % (max + 1)
}
/// Add a value to the wrapped usize, return the result
///
/// Returns the result instead of mutating the inner value
@@ -352,12 +357,12 @@ pub struct LineBuf {
impl LineBuf {
pub fn new() -> Self {
let mut new = Self {
grapheme_indices: Some(vec![]), // We know the buffer is empty, so this keeps us safe from unwrapping None
..Default::default()
};
new.update_graphemes();
new
let mut new = Self {
grapheme_indices: Some(vec![]), // We know the buffer is empty, so this keeps us safe from unwrapping None
..Default::default()
};
new.update_graphemes();
new
}
/// Only update self.grapheme_indices if it is None
pub fn update_graphemes_lazy(&mut self) {
@@ -437,17 +442,17 @@ impl LineBuf {
self.cursor.set_max(indices.len());
self.grapheme_indices = Some(indices)
}
#[track_caller]
#[track_caller]
pub fn grapheme_indices(&self) -> &[usize] {
if self.grapheme_indices.is_none() {
let caller = std::panic::Location::caller();
panic!(
"grapheme_indices is None. This likely means you forgot to call update_graphemes() before calling a method that relies on grapheme_indices, or you called a method that relies on grapheme_indices from another method that also relies on grapheme_indices without updating graphemes in between. Caller: {}:{}:{}",
caller.file(),
caller.line(),
caller.column(),
);
}
if self.grapheme_indices.is_none() {
let caller = std::panic::Location::caller();
panic!(
"grapheme_indices is None. This likely means you forgot to call update_graphemes() before calling a method that relies on grapheme_indices, or you called a method that relies on grapheme_indices from another method that also relies on grapheme_indices without updating graphemes in between. Caller: {}:{}:{}",
caller.file(),
caller.line(),
caller.column(),
);
}
self.grapheme_indices.as_ref().unwrap()
}
pub fn grapheme_indices_owned(&self) -> Vec<usize> {
@@ -806,19 +811,19 @@ impl LineBuf {
}
Some(self.line_bounds(line_no))
}
pub fn this_word(&mut self, word: Word) -> (usize, usize) {
let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) {
self.cursor.get()
} else {
self.start_of_word_backward(self.cursor.get(), word)
};
let end = if self.is_word_bound(self.cursor.get(), word, Direction::Forward) {
self.cursor.get()
} else {
self.end_of_word_forward(self.cursor.get(), word)
};
(start, end)
}
pub fn this_word(&mut self, word: Word) -> (usize, usize) {
let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) {
self.cursor.get()
} else {
self.start_of_word_backward(self.cursor.get(), word)
};
let end = if self.is_word_bound(self.cursor.get(), word, Direction::Forward) {
self.cursor.get()
} else {
self.end_of_word_forward(self.cursor.get(), word)
};
(start, end)
}
pub fn this_line_exclusive(&mut self) -> (usize, usize) {
let line_no = self.cursor_line_number();
let (start, mut end) = self.line_bounds(line_no);
@@ -827,10 +832,10 @@ impl LineBuf {
}
(start, end)
}
pub fn this_line_content(&mut self) -> Option<&str> {
let (start,end) = self.this_line_exclusive();
self.slice(start..end)
}
pub fn this_line_content(&mut self) -> Option<&str> {
let (start, end) = self.this_line_exclusive();
self.slice(start..end)
}
pub fn this_line(&mut self) -> (usize, usize) {
let line_no = self.cursor_line_number();
self.line_bounds(line_no)
@@ -940,7 +945,7 @@ impl LineBuf {
}
pub fn is_word_bound(&mut self, pos: usize, word: Word, dir: Direction) -> bool {
let clamped_pos = ClampedUsize::new(pos, self.cursor.max, true);
log::debug!("clamped_pos: {}", clamped_pos.get());
log::debug!("clamped_pos: {}", clamped_pos.get());
let cur_char = self
.grapheme_at(clamped_pos.get())
.map(|c| c.to_string())
@@ -1011,11 +1016,11 @@ impl LineBuf {
} else {
self.start_of_word_backward(self.cursor.get(), word)
};
let end = if self.is_word_bound(self.cursor.get(), word, Direction::Forward) {
self.cursor.get()
} else {
self.end_of_word_forward(self.cursor.get(), word)
};
let end = if self.is_word_bound(self.cursor.get(), word, Direction::Forward) {
self.cursor.get()
} else {
self.end_of_word_forward(self.cursor.get(), word)
};
Some((start, end))
}
Bound::Around => {
@@ -1994,9 +1999,9 @@ impl LineBuf {
let mut level: usize = 0;
if to_cursor.ends_with("\\\n") {
level += 1; // Line continuation, so we need to add an extra level
}
if to_cursor.ends_with("\\\n") {
level += 1; // Line continuation, so we need to add an extra level
}
let input = Arc::new(to_cursor);
let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()
@@ -2004,23 +2009,23 @@ impl LineBuf {
log::error!("Failed to lex buffer for indent calculation");
return;
};
let mut last_keyword: Option<String> = None;
let mut last_keyword: Option<String> = None;
for tk in tokens {
if tk.flags.contains(TkFlags::KEYWORD) {
match tk.as_str() {
"in" => {
if last_keyword.as_deref() == Some("case") {
level += 1;
} else {
// 'in' is also used in for loops, but we already increment level on 'do' for those
// so we just skip it here
}
}
"in" => {
if last_keyword.as_deref() == Some("case") {
level += 1;
} else {
// 'in' is also used in for loops, but we already increment level on 'do' for those
// so we just skip it here
}
}
"then" | "do" => level += 1,
"done" | "fi" | "esac" => level = level.saturating_sub(1),
_ => { /* Continue */ }
}
last_keyword = Some(tk.to_string());
last_keyword = Some(tk.to_string());
} else if tk.class == TkRule::BraceGrpStart {
level += 1;
} else if tk.class == TkRule::BraceGrpEnd {
@@ -2362,12 +2367,12 @@ impl LineBuf {
}
MotionCmd(_count, Motion::BeginningOfBuffer) => MotionKind::On(0),
MotionCmd(_count, Motion::EndOfBuffer) => {
if self.cursor.exclusive {
MotionKind::On(self.grapheme_indices().len().saturating_sub(1))
} else {
MotionKind::On(self.grapheme_indices().len())
}
},
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::Range(start, end)) => {
let mut final_end = end;
@@ -2794,7 +2799,11 @@ impl LineBuf {
match content {
RegisterContent::Span(ref text) => {
let insert_idx = match anchor {
Anchor::After => self.cursor.get().saturating_add(1).min(self.grapheme_indices().len()),
Anchor::After => self
.cursor
.get()
.saturating_add(1)
.min(self.grapheme_indices().len()),
Anchor::Before => self.cursor.get(),
};
self.insert_str_at(insert_idx, text);
@@ -2859,34 +2868,35 @@ impl LineBuf {
Verb::InsertChar(ch) => {
self.insert_at_cursor(ch);
self.cursor.add(1);
let before = self.auto_indent_level;
if read_shopts(|o| o.prompt.auto_indent)
&& let Some(line_content) = self.this_line_content() {
match line_content.trim() {
"esac" | "done" | "fi" | "}" => {
self.calc_indent_level();
if self.auto_indent_level < before {
let delta = before - self.auto_indent_level;
let line_start = self.start_of_line();
for _ in 0..delta {
if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") {
self.remove(line_start);
if !self.cursor_at_max() {
self.cursor.sub(1);
}
}
}
}
}
_ => { /* nothing to see here */ }
}
}
let before = self.auto_indent_level;
if read_shopts(|o| o.prompt.auto_indent)
&& let Some(line_content) = self.this_line_content()
{
match line_content.trim() {
"esac" | "done" | "fi" | "}" => {
self.calc_indent_level();
if self.auto_indent_level < before {
let delta = before - self.auto_indent_level;
let line_start = self.start_of_line();
for _ in 0..delta {
if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") {
self.remove(line_start);
if !self.cursor_at_max() {
self.cursor.sub(1);
}
}
}
}
}
_ => { /* nothing to see here */ }
}
}
}
Verb::Insert(string) => {
self.push_str(&string);
let graphemes = string.graphemes(true).count();
log::debug!("Inserted string: {string:?}, graphemes: {graphemes}");
log::debug!("buffer after insert: {:?}", self.buffer);
log::debug!("Inserted string: {string:?}, graphemes: {graphemes}");
log::debug!("buffer after insert: {:?}", self.buffer);
self.cursor.add(graphemes);
}
Verb::Indent => {
@@ -3039,71 +3049,82 @@ impl LineBuf {
}
}
}
Verb::IncrementNumber(n) |
Verb::DecrementNumber(n) => {
let inc = if matches!(verb, Verb::IncrementNumber(_)) { n as i64 } else { -(n as i64) };
let (s, e) = self.select_range().unwrap_or(self.this_word(Word::Normal));
let end = if self.select_range().is_some() {
if e < self.grapheme_indices().len() - 1 {
e
} else {
e + 1
}
} else {
(e + 1).min(self.grapheme_indices().len())
}; // inclusive → exclusive, capped at buffer len
let word = self.slice(s..end).unwrap_or_default().to_lowercase();
Verb::IncrementNumber(n) | Verb::DecrementNumber(n) => {
let inc = if matches!(verb, Verb::IncrementNumber(_)) {
n as i64
} else {
-(n as i64)
};
let (s, e) = self.select_range().unwrap_or(self.this_word(Word::Normal));
let end = if self.select_range().is_some() {
if e < self.grapheme_indices().len() - 1 {
e
} else {
e + 1
}
} else {
(e + 1).min(self.grapheme_indices().len())
}; // inclusive → exclusive, capped at buffer len
let word = self.slice(s..end).unwrap_or_default().to_lowercase();
let byte_start = self.index_byte_pos(s);
let byte_end = if end >= self.grapheme_indices().len() {
self.buffer.len()
} else {
self.index_byte_pos(end)
};
let byte_start = self.index_byte_pos(s);
let byte_end = if end >= self.grapheme_indices().len() {
self.buffer.len()
} else {
self.index_byte_pos(end)
};
if word.starts_with("0x") {
let body = word.strip_prefix("0x").unwrap();
let width = body.len();
if let Ok(num) = i64::from_str_radix(body, 16) {
let new_num = num + inc;
self.buffer.replace_range(byte_start..byte_end, &format!("0x{new_num:0>width$x}"));
self.update_graphemes();
self.cursor.set(s);
}
} else if word.starts_with("0b") {
let body = word.strip_prefix("0b").unwrap();
let width = body.len();
if let Ok(num) = i64::from_str_radix(body, 2) {
let new_num = num + inc;
self.buffer.replace_range(byte_start..byte_end, &format!("0b{new_num:0>width$b}"));
self.update_graphemes();
self.cursor.set(s);
}
} else if word.starts_with("0o") {
let body = word.strip_prefix("0o").unwrap();
let width = body.len();
if let Ok(num) = i64::from_str_radix(body, 8) {
let new_num = num + inc;
self.buffer.replace_range(byte_start..byte_end, &format!("0o{new_num:0>width$o}"));
self.update_graphemes();
self.cursor.set(s);
}
} else if let Ok(num) = word.parse::<i64>() {
let width = word.len();
let new_num = num + inc;
self.buffer.replace_range(byte_start..byte_end, &format!("{new_num:0>width$}"));
self.update_graphemes();
self.cursor.set(s);
}
}
if word.starts_with("0x") {
let body = word.strip_prefix("0x").unwrap();
let width = body.len();
if let Ok(num) = i64::from_str_radix(body, 16) {
let new_num = num + inc;
self
.buffer
.replace_range(byte_start..byte_end, &format!("0x{new_num:0>width$x}"));
self.update_graphemes();
self.cursor.set(s);
}
} else if word.starts_with("0b") {
let body = word.strip_prefix("0b").unwrap();
let width = body.len();
if let Ok(num) = i64::from_str_radix(body, 2) {
let new_num = num + inc;
self
.buffer
.replace_range(byte_start..byte_end, &format!("0b{new_num:0>width$b}"));
self.update_graphemes();
self.cursor.set(s);
}
} else if word.starts_with("0o") {
let body = word.strip_prefix("0o").unwrap();
let width = body.len();
if let Ok(num) = i64::from_str_radix(body, 8) {
let new_num = num + inc;
self
.buffer
.replace_range(byte_start..byte_end, &format!("0o{new_num:0>width$o}"));
self.update_graphemes();
self.cursor.set(s);
}
} else if let Ok(num) = word.parse::<i64>() {
let width = word.len();
let new_num = num + inc;
self
.buffer
.replace_range(byte_start..byte_end, &format!("{new_num:0>width$}"));
self.update_graphemes();
self.cursor.set(s);
}
}
Verb::Complete
| Verb::ExMode
| Verb::ExMode
| Verb::EndOfFile
| Verb::InsertMode
| Verb::NormalMode
| Verb::VisualMode
| Verb::VerbatimMode
| Verb::VerbatimMode
| Verb::ReplaceMode
| Verb::VisualModeLine
| Verb::VisualModeBlock
@@ -3111,46 +3132,63 @@ impl LineBuf {
| Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
Verb::ShellCmd(cmd) => {
log::debug!("Executing ex-mode command from widget: {cmd}");
let mut vars = HashSet::new();
vars.insert("_BUFFER".into());
vars.insert("_CURSOR".into());
vars.insert("_ANCHOR".into());
let _guard = var_ctx_guard(vars);
log::debug!("Executing ex-mode command from widget: {cmd}");
let mut vars = HashSet::new();
vars.insert("_BUFFER".into());
vars.insert("_CURSOR".into());
vars.insert("_ANCHOR".into());
let _guard = var_ctx_guard(vars);
let mut buf = self.as_str().to_string();
let mut cursor = self.cursor.get();
let mut anchor = self.select_range().map(|r| if r.0 != cursor { r.0 } else { r.1 }).unwrap_or(cursor);
let mut buf = self.as_str().to_string();
let mut cursor = self.cursor.get();
let mut anchor = self
.select_range()
.map(|r| if r.0 != cursor { r.0 } else { r.1 })
.unwrap_or(cursor);
write_vars(|v| {
v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?;
v.set_var("_CURSOR", VarKind::Str(cursor.to_string()), VarFlags::EXPORT)?;
v.set_var("_ANCHOR", VarKind::Str(anchor.to_string()), VarFlags::EXPORT)
})?;
write_vars(|v| {
v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?;
v.set_var(
"_CURSOR",
VarKind::Str(cursor.to_string()),
VarFlags::EXPORT,
)?;
v.set_var(
"_ANCHOR",
VarKind::Str(anchor.to_string()),
VarFlags::EXPORT,
)
})?;
RawModeGuard::with_cooked_mode(|| exec_input(cmd, None, true, Some("<ex-mode-cmd>".into())))?;
RawModeGuard::with_cooked_mode(|| {
exec_input(cmd, None, true, Some("<ex-mode-cmd>".into()))
})?;
let keys = write_vars(|v| {
buf = v.take_var("_BUFFER");
cursor = v.take_var("_CURSOR").parse().unwrap_or(cursor);
anchor = v.take_var("_ANCHOR").parse().unwrap_or(anchor);
v.take_var("_KEYS")
});
let keys = write_vars(|v| {
buf = v.take_var("_BUFFER");
cursor = v.take_var("_CURSOR").parse().unwrap_or(cursor);
anchor = v.take_var("_ANCHOR").parse().unwrap_or(anchor);
v.take_var("_KEYS")
});
self.set_buffer(buf);
self.update_graphemes();
self.cursor.set_max(self.buffer.graphemes(true).count());
self.cursor.set(cursor);
log::debug!("[ShellCmd] post-widget: cursor={}, anchor={}, select_range={:?}", cursor, anchor, self.select_range);
if anchor != cursor && self.select_range.is_some() {
self.select_range = Some(ordered(cursor, anchor));
}
if !keys.is_empty() {
log::debug!("Pending widget keys from shell command: {keys}");
write_meta(|m| m.set_pending_widget_keys(&keys))
}
}
self.set_buffer(buf);
self.update_graphemes();
self.cursor.set_max(self.buffer.graphemes(true).count());
self.cursor.set(cursor);
log::debug!(
"[ShellCmd] post-widget: cursor={}, anchor={}, select_range={:?}",
cursor,
anchor,
self.select_range
);
if anchor != cursor && self.select_range.is_some() {
self.select_range = Some(ordered(cursor, anchor));
}
if !keys.is_empty() {
log::debug!("Pending widget keys from shell command: {keys}");
write_meta(|m| m.set_pending_widget_keys(&keys))
}
}
Verb::Normal(_)
| Verb::Read(_)
| Verb::Write(_)

View File

@@ -1,7 +1,7 @@
use std::fmt::Write;
use history::History;
use keys::{KeyCode, KeyEvent, ModKeys};
use linebuf::{LineBuf, SelectAnchor, SelectMode};
use std::fmt::Write;
use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size};
use unicode_width::UnicodeWidthStr;
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
@@ -12,16 +12,21 @@ use crate::expand::expand_prompt;
use crate::libsh::sys::TTY_FILENO;
use crate::libsh::utils::AutoCmdVecUtils;
use crate::parse::lex::{LexStream, QuoteState};
use crate::{prelude::*, state};
use crate::readline::complete::FuzzyCompleter;
use crate::readline::term::{Pos, TermReader, calc_str_width};
use crate::readline::vimode::{ViEx, ViVerbatim};
use crate::state::{AutoCmdKind, ShellParam, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta, write_vars};
use crate::state::{
AutoCmdKind, ShellParam, VarFlags, VarKind, read_logic, read_shopts, write_meta, write_vars,
};
use crate::{
libsh::error::ShResult,
parse::lex::{self, LexFlags, Tk, TkFlags, TkRule},
readline::{complete::{CompResponse, Completer}, highlight::Highlighter},
readline::{
complete::{CompResponse, Completer},
highlight::Highlighter,
},
};
use crate::{prelude::*, state};
pub mod complete;
pub mod highlight;
@@ -150,8 +155,8 @@ impl Prompt {
let Ok(ps1_raw) = env::var("PS1") else {
return Self::default();
};
// PS1 expansion may involve running commands (e.g., for \h or \W), which can modify shell state
let saved_status = state::get_status();
// PS1 expansion may involve running commands (e.g., for \h or \W), which can modify shell state
let saved_status = state::get_status();
let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else {
return Self::default();
@@ -164,8 +169,8 @@ impl Prompt {
.ok()
.flatten();
// Restore shell state after prompt expansion, since it may have been modified by command substitutions in the prompt
state::set_status(saved_status);
// Restore shell state after prompt expansion, since it may have been modified by command substitutions in the prompt
state::set_status(saved_status);
Self {
ps1_expanded,
ps1_raw,
@@ -208,10 +213,10 @@ impl Prompt {
if let Ok(expanded) = expand_prompt(&self.ps1_raw) {
self.ps1_expanded = expanded;
}
if let Some(psr_raw) = &self.psr_raw {
if let Ok(expanded) = expand_prompt(psr_raw) {
self.psr_expanded = Some(expanded);
}
if let Some(psr_raw) = &self.psr_raw
&& let Ok(expanded) = expand_prompt(psr_raw)
{
self.psr_expanded = Some(expanded);
}
state::set_status(saved_status);
self.dirty = false;
@@ -244,12 +249,12 @@ pub struct ShedVi {
pub completer: Box<dyn Completer>,
pub mode: Box<dyn ViMode>,
pub saved_mode: Option<Box<dyn ViMode>>,
pub pending_keymap: Vec<KeyEvent>,
pub saved_mode: Option<Box<dyn ViMode>>,
pub pending_keymap: Vec<KeyEvent>,
pub repeat_action: Option<CmdReplay>,
pub repeat_motion: Option<MotionCmd>,
pub editor: LineBuf,
pub next_is_escaped: bool,
pub next_is_escaped: bool,
pub old_layout: Option<Layout>,
pub history: History,
@@ -266,9 +271,9 @@ impl ShedVi {
completer: Box::new(FuzzyCompleter::default()),
highlighter: Highlighter::new(),
mode: Box::new(ViInsert::new()),
next_is_escaped: false,
saved_mode: None,
pending_keymap: Vec::new(),
next_is_escaped: false,
saved_mode: None,
pending_keymap: Vec::new(),
old_layout: None,
repeat_action: None,
repeat_motion: None,
@@ -276,8 +281,14 @@ impl ShedVi {
history: History::new()?,
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();
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)
@@ -293,7 +304,7 @@ impl ShedVi {
/// Feed raw bytes from stdin into the reader's buffer
pub fn feed_bytes(&mut self, bytes: &[u8]) {
let verbatim = self.mode.report_mode() == ModeReport::Verbatim;
let verbatim = self.mode.report_mode() == ModeReport::Verbatim;
self.reader.feed_bytes(bytes, verbatim);
}
@@ -302,19 +313,21 @@ impl ShedVi {
self.needs_redraw = true;
}
pub fn fix_column(&mut self) -> ShResult<()> {
self.writer.fix_cursor_column(&mut TermReader::new(*TTY_FILENO))
}
pub fn fix_column(&mut self) -> ShResult<()> {
self
.writer
.fix_cursor_column(&mut TermReader::new(*TTY_FILENO))
}
pub fn reset_active_widget(&mut self, full_redraw: bool) -> ShResult<()> {
if self.completer.is_active() {
self.completer.reset_stay_active();
self.needs_redraw = true;
Ok(())
} else {
self.reset(full_redraw)
}
}
pub fn reset_active_widget(&mut self, full_redraw: bool) -> ShResult<()> {
if self.completer.is_active() {
self.completer.reset_stay_active();
self.needs_redraw = true;
Ok(())
} else {
self.reset(full_redraw)
}
}
/// Reset readline state for a new prompt
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
@@ -322,7 +335,7 @@ impl ShedVi {
// so print_line can call clear_rows with the full multi-line layout
self.prompt.refresh();
self.editor = Default::default();
self.swap_mode(&mut (Box::new(ViInsert::new()) as Box<dyn ViMode>));
self.swap_mode(&mut (Box::new(ViInsert::new()) as Box<dyn ViMode>));
self.needs_redraw = true;
if full_redraw {
self.old_layout = None;
@@ -340,24 +353,24 @@ impl ShedVi {
&mut self.prompt
}
pub fn curr_keymap_flags(&self) -> KeyMapFlags {
let mut flags = KeyMapFlags::empty();
match self.mode.report_mode() {
ModeReport::Insert => flags |= KeyMapFlags::INSERT,
ModeReport::Normal => flags |= KeyMapFlags::NORMAL,
ModeReport::Ex => flags |= KeyMapFlags::EX,
ModeReport::Visual => flags |= KeyMapFlags::VISUAL,
ModeReport::Replace => flags |= KeyMapFlags::REPLACE,
ModeReport::Verbatim => flags |= KeyMapFlags::VERBATIM,
ModeReport::Unknown => todo!(),
}
pub fn curr_keymap_flags(&self) -> KeyMapFlags {
let mut flags = KeyMapFlags::empty();
match self.mode.report_mode() {
ModeReport::Insert => flags |= KeyMapFlags::INSERT,
ModeReport::Normal => flags |= KeyMapFlags::NORMAL,
ModeReport::Ex => flags |= KeyMapFlags::EX,
ModeReport::Visual => flags |= KeyMapFlags::VISUAL,
ModeReport::Replace => flags |= KeyMapFlags::REPLACE,
ModeReport::Verbatim => flags |= KeyMapFlags::VERBATIM,
ModeReport::Unknown => todo!(),
}
if self.mode.pending_seq().is_some_and(|seq| !seq.is_empty()) {
flags |= KeyMapFlags::OP_PENDING;
}
if self.mode.pending_seq().is_some_and(|seq| !seq.is_empty()) {
flags |= KeyMapFlags::OP_PENDING;
}
flags
}
flags
}
fn should_submit(&mut self) -> ShResult<bool> {
if self.mode.report_mode() == ModeReport::Normal {
@@ -398,7 +411,7 @@ impl ShedVi {
while let Some(key) = self.reader.read_key()? {
// If completer is active, delegate input to it
if self.completer.is_active() {
self.print_line(false)?;
self.print_line(false)?;
match self.completer.handle_key(key.clone())? {
CompResponse::Accept(candidate) => {
let span_start = self.completer.token_span().0;
@@ -416,57 +429,58 @@ impl ShedVi {
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
let hint = self.history.get_hint();
self.editor.set_hint(hint);
self.completer.clear(&mut self.writer)?;
self.needs_redraw = true;
self.completer.reset();
continue;
self.completer.clear(&mut self.writer)?;
self.needs_redraw = true;
self.completer.reset();
continue;
}
CompResponse::Dismiss => {
let hint = self.history.get_hint();
self.editor.set_hint(hint);
self.completer.clear(&mut self.writer)?;
self.completer.reset();
continue;
self.completer.clear(&mut self.writer)?;
self.completer.reset();
continue;
}
CompResponse::Consumed => {
/* just redraw */
self.needs_redraw = true;
continue;
}
/* just redraw */
self.needs_redraw = true;
continue;
}
CompResponse::Passthrough => { /* fall through to normal handling below */ }
}
} else {
let keymap_flags = self.curr_keymap_flags();
self.pending_keymap.push(key.clone());
let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap));
if matches.is_empty() {
// No matches. Drain the buffered keys and execute them.
for key in std::mem::take(&mut self.pending_keymap) {
if let Some(event) = self.handle_key(key)? {
return Ok(event);
}
}
self.needs_redraw = true;
continue;
} else if matches.len() == 1 && matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact {
// We have a single exact match. Execute it.
let keymap = matches[0].clone();
self.pending_keymap.clear();
let action = keymap.action_expanded();
for key in action {
if let Some(event) = self.handle_key(key)? {
return Ok(event);
}
}
self.needs_redraw = true;
continue;
} else {
// There is ambiguity. Allow the timeout in the main loop to handle this.
continue;
}
}
} else {
let keymap_flags = self.curr_keymap_flags();
self.pending_keymap.push(key.clone());
let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap));
if matches.is_empty() {
// No matches. Drain the buffered keys and execute them.
for key in std::mem::take(&mut self.pending_keymap) {
if let Some(event) = self.handle_key(key)? {
return Ok(event);
}
}
self.needs_redraw = true;
continue;
} else if matches.len() == 1
&& matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact
{
// We have a single exact match. Execute it.
let keymap = matches[0].clone();
self.pending_keymap.clear();
let action = keymap.action_expanded();
for key in action {
if let Some(event) = self.handle_key(key)? {
return Ok(event);
}
}
self.needs_redraw = true;
continue;
} else {
// There is ambiguity. Allow the timeout in the main loop to handle this.
continue;
}
}
if let Some(event) = self.handle_key(key)? {
return Ok(event);
@@ -542,12 +556,13 @@ impl ShedVi {
return Ok(None);
}
if let KeyEvent(KeyCode::Char('\\'), ModKeys::NONE) = key
&& !self.next_is_escaped {
self.next_is_escaped = true;
} else {
self.next_is_escaped = false;
}
if let KeyEvent(KeyCode::Char('\\'), ModKeys::NONE) = key
&& !self.next_is_escaped
{
self.next_is_escaped = true;
} else {
self.next_is_escaped = false;
}
let Ok(cmd) = self.mode.handle_key_fallible(key) else {
// it's an ex mode error
@@ -567,9 +582,10 @@ impl ShedVi {
}
if cmd.is_submit_action()
&& !self.next_is_escaped
&& !self.editor.buffer.ends_with('\\')
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) {
&& !self.next_is_escaped
&& !self.editor.buffer.ends_with('\\')
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
{
self.editor.set_hint(None);
self.editor.cursor.set(self.editor.cursor_max());
self.print_line(true)?;
@@ -600,11 +616,11 @@ impl ShedVi {
let before = self.editor.buffer.clone();
self.exec_cmd(cmd)?;
if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) {
for key in keys {
self.handle_key(key)?;
}
}
if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) {
for key in keys {
self.handle_key(key)?;
}
}
let after = self.editor.as_str();
if before != after {
@@ -670,7 +686,7 @@ impl ShedVi {
|| (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty()
&& matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE)))
}
ModeReport::Ex | ModeReport::Verbatim | ModeReport::Unknown => false,
ModeReport::Ex | ModeReport::Verbatim | ModeReport::Unknown => false,
}
} else {
false
@@ -692,13 +708,15 @@ impl ShedVi {
pub fn line_text(&mut self) -> String {
let line = self.editor.to_string();
let hint = self.editor.get_hint_text();
let do_hl = state::read_shopts(|s| s.prompt.highlight);
self.highlighter.only_visual(!do_hl);
self.highlighter.load_input(&line, self.editor.cursor_byte_pos());
self.highlighter.expand_control_chars();
self.highlighter.highlight();
let highlighted = self.highlighter.take();
format!("{highlighted}{hint}")
let do_hl = state::read_shopts(|s| s.prompt.highlight);
self.highlighter.only_visual(!do_hl);
self
.highlighter
.load_input(&line, self.editor.cursor_byte_pos());
self.highlighter.expand_control_chars();
self.highlighter.highlight();
let highlighted = self.highlighter.take();
format!("{highlighted}{hint}")
}
pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> {
@@ -716,7 +734,7 @@ impl ShedVi {
prompt_string_right =
prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string());
}
let mut buf = String::new();
let mut buf = String::new();
let row0_used = self
.prompt
@@ -734,8 +752,8 @@ impl ShedVi {
self.writer.clear_rows(layout)?;
}
let pre_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PrePrompt));
pre_prompt.exec();
let pre_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PrePrompt));
pre_prompt.exec();
self
.writer
@@ -753,7 +771,8 @@ impl ShedVi {
&& !seq.is_empty()
&& !(prompt_string_right.is_some() && one_line)
&& seq_fits
&& self.mode.report_mode() != ModeReport::Ex {
&& self.mode.report_mode() != ModeReport::Ex
{
let to_col = self.writer.t_cols - calc_str_width(&seq);
let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt
@@ -765,10 +784,11 @@ impl ShedVi {
// Save cursor, move up to top row, move right to column, write sequence,
// restore cursor
write!(buf, "\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8").unwrap();
write!(buf, "\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8").unwrap();
} else if !final_draw
&& let Some(psr) = prompt_string_right
&& psr_fits {
&& psr_fits
{
let to_col = self.writer.t_cols - calc_str_width(&psr);
let down = new_layout.end.row - new_layout.cursor.row;
let move_down = if down > 0 {
@@ -781,19 +801,28 @@ impl ShedVi {
// Record where the PSR ends so clear_rows can account for wrapping
// if the terminal shrinks.
let psr_start = Pos { row: new_layout.end.row, col: to_col };
new_layout.psr_end = Some(Layout::calc_pos(self.writer.t_cols, &psr, psr_start, 0, false));
let psr_start = Pos {
row: new_layout.end.row,
col: to_col,
};
new_layout.psr_end = Some(Layout::calc_pos(
self.writer.t_cols,
&psr,
psr_start,
0,
false,
));
}
if let ModeReport::Ex = self.mode.report_mode() {
let pending_seq = self.mode.pending_seq().unwrap_or_default();
write!(buf, "\n: {pending_seq}").unwrap();
new_layout.end.row += 1;
}
if let ModeReport::Ex = self.mode.report_mode() {
let pending_seq = self.mode.pending_seq().unwrap_or_default();
write!(buf, "\n: {pending_seq}").unwrap();
new_layout.end.row += 1;
}
write!(buf, "{}", &self.mode.cursor_style()).unwrap();
self.writer.flush_write(&buf)?;
self.writer.flush_write(&buf)?;
// Tell the completer the width of the prompt line above its \n so it can
// account for wrapping when clearing after a resize.
@@ -803,30 +832,39 @@ impl ShedVi {
// Without PSR, use the content width on the cursor's row
(new_layout.end.col + 1).max(new_layout.cursor.col + 1)
};
self.completer.set_prompt_line_context(preceding_width, new_layout.cursor.col);
self
.completer
.set_prompt_line_context(preceding_width, new_layout.cursor.col);
self.completer.draw(&mut self.writer)?;
self.old_layout = Some(new_layout);
self.needs_redraw = false;
let post_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PostPrompt));
post_prompt.exec();
let post_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PostPrompt));
post_prompt.exec();
Ok(())
}
pub fn swap_mode(&mut self, mode: &mut Box<dyn ViMode>) {
let pre_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PreModeChange));
pre_mode_change.exec();
pub fn swap_mode(&mut self, mode: &mut Box<dyn ViMode>) {
let pre_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PreModeChange));
pre_mode_change.exec();
std::mem::swap(&mut self.mode, mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok();
self.prompt.refresh();
std::mem::swap(&mut self.mode, mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})
.ok();
self.prompt.refresh();
let post_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PostModeChange));
post_mode_change.exec();
}
let post_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PostModeChange));
post_mode_change.exec();
}
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
let mut select_mode = None;
@@ -834,63 +872,72 @@ impl ShedVi {
if cmd.is_mode_transition() {
let count = cmd.verb_count();
let mut mode: Box<dyn ViMode> = if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
if let Some(saved) = self.saved_mode.take() {
saved
} else {
Box::new(ViNormal::new())
}
} 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))
}
let mut mode: Box<dyn ViMode> = if matches!(
self.mode.report_mode(),
ModeReport::Ex | ModeReport::Verbatim
) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE)
{
if let Some(saved) = self.saved_mode.take() {
saved
} else {
Box::new(ViNormal::new())
}
} else {
match cmd.verb().unwrap().1 {
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
is_insert_mode = true;
Box::new(ViInsert::new().with_count(count as u16))
}
Verb::ExMode => {
Box::new(ViEx::new())
}
Verb::ExMode => Box::new(ViEx::new()),
Verb::VerbatimMode => {
Box::new(ViVerbatim::new().with_count(count as u16))
}
Verb::VerbatimMode => Box::new(ViVerbatim::new().with_count(count as u16)),
Verb::NormalMode => Box::new(ViNormal::new()),
Verb::NormalMode => Box::new(ViNormal::new()),
Verb::ReplaceMode => Box::new(ViReplace::new()),
Verb::ReplaceMode => Box::new(ViReplace::new()),
Verb::VisualModeSelectLast => {
if self.mode.report_mode() != ModeReport::Visual {
self
.editor
.start_selecting(SelectMode::Char(SelectAnchor::End));
}
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
self.swap_mode(&mut mode);
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())
}
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!(),
}
};
_ => unreachable!(),
}
};
self.swap_mode(&mut mode);
self.swap_mode(&mut mode);
if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) {
self.saved_mode = Some(mode);
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?;
self.prompt.refresh();
return Ok(());
}
if matches!(
self.mode.report_mode(),
ModeReport::Ex | ModeReport::Verbatim
) {
self.saved_mode = Some(mode);
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})?;
self.prompt.refresh();
return Ok(());
}
if mode.is_repeatable() {
self.repeat_action = mode.as_replay();
@@ -912,9 +959,14 @@ impl ShedVi {
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();
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})?;
self.prompt.refresh();
return Ok(());
} else if cmd.is_cmd_repeat() {
@@ -989,22 +1041,21 @@ impl ShedVi {
}
}
if self.mode.report_mode() == ModeReport::Visual
&& self.editor.select_range().is_none() {
self.editor.stop_selecting();
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
self.swap_mode(&mut mode);
}
if self.mode.report_mode() == ModeReport::Visual && self.editor.select_range().is_none() {
self.editor.stop_selecting();
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
self.swap_mode(&mut mode);
}
if cmd.is_repeatable() {
if self.mode.report_mode() == ModeReport::Visual {
// The motion is assigned in the line buffer execution, so we also have to
// assign it here in order to be able to repeat it
if let Some(range) = self.editor.select_range() {
cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1)))
} else {
log::warn!("You're in visual mode with no select range??");
};
cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1)))
} else {
log::warn!("You're in visual mode with no select range??");
};
}
self.repeat_action = Some(CmdReplay::Single(cmd.clone()));
}
@@ -1018,24 +1069,27 @@ impl ShedVi {
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
self.editor.stop_selecting();
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
self.swap_mode(&mut mode);
self.swap_mode(&mut mode);
}
if self.mode.report_mode() != ModeReport::Visual && self.editor.select_range().is_some() {
self.editor.stop_selecting();
}
if self.mode.report_mode() != ModeReport::Visual && self.editor.select_range().is_some() {
self.editor.stop_selecting();
}
if cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
let mut mode: Box<dyn ViMode> = if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) {
if let Some(saved) = self.saved_mode.take() {
saved
} else {
Box::new(ViNormal::new())
}
} else {
Box::new(ViNormal::new())
};
self.swap_mode(&mut mode);
let mut mode: Box<dyn ViMode> = if matches!(
self.mode.report_mode(),
ModeReport::Ex | ModeReport::Verbatim
) {
if let Some(saved) = self.saved_mode.take() {
saved
} else {
Box::new(ViNormal::new())
}
} else {
Box::new(ViNormal::new())
};
self.swap_mode(&mut mode);
}
Ok(())
@@ -1069,7 +1123,6 @@ pub fn annotate_input(input: &str) -> String {
.filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null))
.collect();
for tk in tokens.into_iter().rev() {
let insertions = annotate_token(tk);
for (pos, marker) in insertions {
@@ -1112,9 +1165,7 @@ pub fn annotate_input_recursive(input: &str) -> String {
markers::PROC_SUB => match chars.peek().map(|(_, c)| *c) {
Some('>') => ">(",
Some('<') => "<(",
_ => {
"<("
}
_ => "<(",
},
markers::CMD_SUB => "$(",
markers::SUBSH => "(",
@@ -1256,7 +1307,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
let mut insertions: Vec<(usize, Marker)> = vec![];
if token.class != TkRule::Str
&& let Some(marker) = marker_for(&token.class) {
&& let Some(marker) = marker_for(&token.class)
{
insertions.push((token.span.range().end, markers::RESET));
insertions.push((token.span.range().start, marker));
return insertions;
@@ -1279,7 +1331,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
let span_start = token.span.range().start;
let mut qt_state = QuoteState::default();
let mut qt_state = QuoteState::default();
let mut cmd_sub_depth = 0;
let mut proc_sub_depth = 0;
@@ -1350,7 +1402,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|| *br_ch == '='
|| *br_ch == '/' // parameter expansion symbols
|| *br_ch == '?'
|| *br_ch == '$' // we're in some expansion like $foo$bar or ${foo$bar}
|| *br_ch == '$'
// we're in some expansion like $foo$bar or ${foo$bar}
{
token_chars.next();
} else if *br_ch == '}' {
@@ -1370,8 +1423,9 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
// consume the var name
while let Some((cur_i, var_ch)) = token_chars.peek() {
if var_ch.is_ascii_alphanumeric()
|| ShellParam::from_char(var_ch).is_some()
|| *var_ch == '_' {
|| ShellParam::from_char(var_ch).is_some()
|| *var_ch == '_'
{
end_pos = *cur_i + 1;
token_chars.next();
} else {
@@ -1397,12 +1451,12 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
token_chars.next(); // consume the escaped char
}
}
'\\' if qt_state.in_single() => {
token_chars.next();
if let Some(&(_,'\'')) = token_chars.peek() {
token_chars.next(); // consume the escaped single quote
}
}
'\\' if qt_state.in_single() => {
token_chars.next();
if let Some(&(_, '\'')) = token_chars.peek() {
token_chars.next(); // consume the escaped single quote
}
}
'<' | '>' if !qt_state.in_quote() && cmd_sub_depth == 0 && proc_sub_depth == 0 => {
token_chars.next();
if let Some((_, proc_sub_ch)) = token_chars.peek()
@@ -1421,7 +1475,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
} else {
insertions.push((span_start + *i, markers::STRING_DQ));
}
qt_state.toggle_double();
qt_state.toggle_double();
token_chars.next(); // consume the quote
}
'\'' if !qt_state.in_double() => {
@@ -1430,7 +1484,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
} else {
insertions.push((span_start + *i, markers::STRING_SQ));
}
qt_state.toggle_single();
qt_state.toggle_single();
token_chars.next(); // consume the quote
}
'[' if !qt_state.in_quote() && !token.flags.contains(TkFlags::ASSIGN) => {

View File

@@ -3,7 +3,8 @@ use std::{
env,
fmt::{Debug, Write},
io::{BufRead, BufReader, Read},
os::fd::{AsFd, BorrowedFd, RawFd}, sync::Arc,
os::fd::{AsFd, BorrowedFd, RawFd},
sync::Arc,
};
use nix::{
@@ -17,14 +18,12 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use vte::{Parser, Perform};
pub use crate::libsh::guards::{RawModeGuard, raw_mode};
use crate::state::{read_meta, write_meta};
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
readline::keys::{KeyCode, ModKeys},
state::read_shopts,
};
use crate::{
state::{read_meta, write_meta},
};
use super::keys::KeyEvent;
@@ -165,13 +164,13 @@ fn width(s: &str, esc_seq: &mut u8) -> u16 {
0
} else if *esc_seq == 2 {
if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') {
/*} else if s == "m" {
// last
*esc_seq = 0;*/
} else {
// not supported
*esc_seq = 0;
}
/*} else if s == "m" {
// last
*esc_seq = 0;*/
} else {
// not supported
*esc_seq = 0;
}
0
} else if s == "\x1b" {
@@ -457,27 +456,27 @@ impl Perform for KeyCollector {
};
KeyEvent(key, mods)
}
([],'u') => {
let codepoint = params.first().copied().unwrap_or(0);
let mods = params
.get(1)
.map(|&m| Self::parse_modifiers(m))
.unwrap_or(ModKeys::empty());
let key = match codepoint {
9 => KeyCode::Tab,
13 => KeyCode::Enter,
27 => KeyCode::Esc,
127 => KeyCode::Backspace,
_ => {
if let Some(ch) = char::from_u32(codepoint as u32) {
KeyCode::Char(ch)
} else {
return
}
}
};
KeyEvent(key, mods)
}
([], 'u') => {
let codepoint = params.first().copied().unwrap_or(0);
let mods = params
.get(1)
.map(|&m| Self::parse_modifiers(m))
.unwrap_or(ModKeys::empty());
let key = match codepoint {
9 => KeyCode::Tab,
13 => KeyCode::Enter,
27 => KeyCode::Esc,
127 => KeyCode::Backspace,
_ => {
if let Some(ch) = char::from_u32(codepoint as u32) {
KeyCode::Char(ch)
} else {
return;
}
}
};
KeyEvent(key, mods)
}
// SGR mouse: CSI < button;x;y M/m (ignore mouse events for now)
([b'<'], 'M') | ([b'<'], 'm') => {
return;
@@ -516,18 +515,21 @@ impl PollReader {
}
pub fn feed_bytes(&mut self, bytes: &[u8], verbatim: bool) {
if verbatim {
let seq = String::from_utf8_lossy(bytes).to_string();
self.collector.push(KeyEvent(KeyCode::Verbatim(Arc::from(seq.as_str())), ModKeys::empty()));
} else if bytes == [b'\x1b'] {
if verbatim {
let seq = String::from_utf8_lossy(bytes).to_string();
self.collector.push(KeyEvent(
KeyCode::Verbatim(Arc::from(seq.as_str())),
ModKeys::empty(),
));
} else if bytes == [b'\x1b'] {
// Single escape byte - user pressed ESC key
self
.collector
.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
} else {
// Feed all bytes through vte parser
self.parser.advance(&mut self.collector, bytes);
}
// Feed all bytes through vte parser
self.parser.advance(&mut self.collector, bytes);
}
}
}
@@ -748,13 +750,9 @@ impl Layout {
}
}
fn is_ctl_char(gr: &str) -> bool {
gr.len() > 0 &&
gr.as_bytes()[0] <= 0x1F &&
gr != "\n" &&
gr != "\t" &&
gr != "\r"
}
fn is_ctl_char(gr: &str) -> bool {
!gr.is_empty() && gr.as_bytes()[0] <= 0x1F && gr != "\n" && gr != "\t" && gr != "\r"
}
pub fn calc_pos(term_width: u16, s: &str, orig: Pos, left_margin: u16, raw_calc: bool) -> Pos {
const TAB_STOP: u16 = 8;
@@ -767,8 +765,8 @@ impl Layout {
}
let c_width = if c == "\t" {
TAB_STOP - (pos.col % TAB_STOP)
} else if raw_calc && Self::is_ctl_char(c) {
2
} else if raw_calc && Self::is_ctl_char(c) {
2
} else {
width(c, &mut esc_seq)
};
@@ -867,21 +865,21 @@ impl TermWriter {
self.t_cols = t_cols;
}
/// Called before the prompt is drawn. If we are not on column 1, push a vid-inverted '%' and then a '\n\r'.
///
/// Aping zsh with this but it's a nice feature.
pub fn fix_cursor_column(&mut self, rdr: &mut TermReader) -> ShResult<()> {
let Some((_,c)) = self.get_cursor_pos(rdr)? else {
return Ok(());
};
/// Called before the prompt is drawn. If we are not on column 1, push a vid-inverted '%' and then a '\n\r'.
///
/// Aping zsh with this but it's a nice feature.
pub fn fix_cursor_column(&mut self, rdr: &mut TermReader) -> ShResult<()> {
let Some((_, c)) = self.get_cursor_pos(rdr)? else {
return Ok(());
};
if c != 1 {
self.flush_write("\x1b[7m%\x1b[0m\n\r")?;
}
Ok(())
}
if c != 1 {
self.flush_write("\x1b[7m%\x1b[0m\n\r")?;
}
Ok(())
}
pub fn get_cursor_pos(&mut self, rdr: &mut TermReader) -> ShResult<Option<(usize, usize)>> {
pub fn get_cursor_pos(&mut self, rdr: &mut TermReader) -> ShResult<Option<(usize, usize)>> {
// Ping the cursor's position
self.flush_write("\x1b[6n")?;
@@ -900,14 +898,16 @@ impl TermWriter {
let row = read_digits_until(rdr, ';')?;
let col = read_digits_until(rdr, 'R')?;
let pos = if let Some(row) = row && let Some(col) = col {
Some((row as usize, col as usize))
} else {
None
};
let pos = if let Some(row) = row
&& let Some(col) = col
{
Some((row as usize, col as usize))
} else {
None
};
Ok(pos)
}
Ok(pos)
}
pub fn move_cursor_at_leftmost(
&mut self,
@@ -996,7 +996,7 @@ impl LineWriter for TermWriter {
)
};
self.buffer.clear();
self.buffer.push_str("\x1b[J"); // Clear from cursor to end of screen to erase any remnants of the old line after the prompt
self.buffer.push_str("\x1b[J"); // Clear from cursor to end of screen to erase any remnants of the old line after the prompt
let end = new_layout.end;
let cursor = new_layout.cursor;

View File

@@ -178,8 +178,8 @@ impl ViCmd {
matches!(
v.1,
Verb::Change
| Verb::VerbatimMode
| Verb::ExMode
| Verb::VerbatimMode
| Verb::ExMode
| Verb::InsertMode
| Verb::InsertModeLineBreak(_)
| Verb::NormalMode
@@ -221,8 +221,8 @@ pub enum Verb {
ReplaceCharInplace(char, u16), // char to replace with, number of chars to replace
ToggleCaseInplace(u16), // Number of chars to toggle
ToggleCaseRange,
IncrementNumber(u16),
DecrementNumber(u16),
IncrementNumber(u16),
DecrementNumber(u16),
ToLower,
ToUpper,
Complete,
@@ -232,7 +232,7 @@ pub enum Verb {
RepeatLast,
Put(Anchor),
ReplaceMode,
VerbatimMode,
VerbatimMode,
InsertMode,
InsertModeLineBreak(Anchor),
NormalMode,
@@ -303,8 +303,8 @@ impl Verb {
| Self::Insert(_)
| Self::Rot13
| Self::EndOfFile
| Self::IncrementNumber(_)
| Self::DecrementNumber(_)
| Self::IncrementNumber(_)
| Self::DecrementNumber(_)
)
}
pub fn is_char_insert(&self) -> bool {

View File

@@ -9,373 +9,399 @@ use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::readline::keys::KeyEvent;
use crate::readline::linebuf::LineBuf;
use crate::readline::vicmd::{
Anchor, CmdFlags, Motion, MotionCmd, ReadSrc, RegisterName, To, Val, Verb, VerbCmd,
ViCmd, WriteDest,
Anchor, CmdFlags, Motion, MotionCmd, ReadSrc, RegisterName, To, Val, Verb, VerbCmd, ViCmd,
WriteDest,
};
use crate::readline::vimode::{ModeReport, ViInsert, ViMode};
use crate::state::write_meta;
bitflags! {
#[derive(Debug,Clone,Copy,PartialEq,Eq)]
pub struct SubFlags: u16 {
const GLOBAL = 1 << 0; // g
const CONFIRM = 1 << 1; // c (probably not implemented)
const IGNORE_CASE = 1 << 2; // i
const NO_IGNORE_CASE = 1 << 3; // I
const SHOW_COUNT = 1 << 4; // n
const PRINT_RESULT = 1 << 5; // p
const PRINT_NUMBERED = 1 << 6; // #
const PRINT_LEFT_ALIGN = 1 << 7; // l
}
#[derive(Debug,Clone,Copy,PartialEq,Eq)]
pub struct SubFlags: u16 {
const GLOBAL = 1 << 0; // g
const CONFIRM = 1 << 1; // c (probably not implemented)
const IGNORE_CASE = 1 << 2; // i
const NO_IGNORE_CASE = 1 << 3; // I
const SHOW_COUNT = 1 << 4; // n
const PRINT_RESULT = 1 << 5; // p
const PRINT_NUMBERED = 1 << 6; // #
const PRINT_LEFT_ALIGN = 1 << 7; // l
}
}
#[derive(Default, Clone, Debug)]
struct ExEditor {
buf: LineBuf,
mode: ViInsert
buf: LineBuf,
mode: ViInsert,
}
impl ExEditor {
pub fn clear(&mut self) {
*self = Self::default()
}
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<()> {
let Some(cmd) = self.mode.handle_key(key) else {
return Ok(())
};
self.buf.exec_cmd(cmd)
}
pub fn clear(&mut self) {
*self = Self::default()
}
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<()> {
let Some(cmd) = self.mode.handle_key(key) else {
return Ok(());
};
self.buf.exec_cmd(cmd)
}
}
#[derive(Default, Clone, Debug)]
pub struct ViEx {
pending_cmd: ExEditor,
pending_cmd: ExEditor,
}
impl ViEx {
pub fn new() -> Self {
Self::default()
}
pub fn new() -> Self {
Self::default()
}
}
impl ViMode for ViEx {
// Ex mode can return errors, so we use this fallible method instead of the normal one
fn handle_key_fallible(&mut self, key: KeyEvent) -> ShResult<Option<ViCmd>> {
use crate::readline::keys::{KeyEvent as E, KeyCode as C, ModKeys as M};
log::debug!("[ViEx] handle_key_fallible: key={:?}", key);
match key {
E(C::Char('\r'), M::NONE) |
E(C::Enter, M::NONE) => {
let input = self.pending_cmd.buf.as_str();
log::debug!("[ViEx] Enter pressed, pending_cmd={:?}", input);
match parse_ex_cmd(input) {
Ok(cmd) => {
log::debug!("[ViEx] parse_ex_cmd Ok: {:?}", cmd);
Ok(cmd)
}
Err(e) => {
log::debug!("[ViEx] parse_ex_cmd Err: {:?}", e);
let msg = e.unwrap_or(format!("Not an editor command: {}", input));
write_meta(|m| m.post_system_message(msg.clone()));
Err(ShErr::simple(ShErrKind::ParseErr, msg))
}
}
}
E(C::Char('C'), M::CTRL) => {
log::debug!("[ViEx] Ctrl-C, clearing");
self.pending_cmd.clear();
Ok(None)
}
E(C::Esc, M::NONE) => {
log::debug!("[ViEx] Esc, returning to normal mode");
Ok(Some(ViCmd {
register: RegisterName::default(),
verb: Some(VerbCmd(1, Verb::NormalMode)),
motion: None,
flags: CmdFlags::empty(),
raw_seq: "".into(),
}))
}
_ => {
log::debug!("[ViEx] forwarding key to ExEditor");
self.pending_cmd.handle_key(key).map(|_| None)
}
}
}
fn handle_key(&mut self, key: KeyEvent) -> Option<ViCmd> {
let result = self.handle_key_fallible(key);
log::debug!("[ViEx] handle_key result: {:?}", result);
result.ok().flatten()
}
fn is_repeatable(&self) -> bool {
false
}
// Ex mode can return errors, so we use this fallible method instead of the normal one
fn handle_key_fallible(&mut self, key: KeyEvent) -> ShResult<Option<ViCmd>> {
use crate::readline::keys::{KeyCode as C, KeyEvent as E, ModKeys as M};
log::debug!("[ViEx] handle_key_fallible: key={:?}", key);
match key {
E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => {
let input = self.pending_cmd.buf.as_str();
log::debug!("[ViEx] Enter pressed, pending_cmd={:?}", input);
match parse_ex_cmd(input) {
Ok(cmd) => {
log::debug!("[ViEx] parse_ex_cmd Ok: {:?}", cmd);
Ok(cmd)
}
Err(e) => {
log::debug!("[ViEx] parse_ex_cmd Err: {:?}", e);
let msg = e.unwrap_or(format!("Not an editor command: {}", input));
write_meta(|m| m.post_system_message(msg.clone()));
Err(ShErr::simple(ShErrKind::ParseErr, msg))
}
}
}
E(C::Char('C'), M::CTRL) => {
log::debug!("[ViEx] Ctrl-C, clearing");
self.pending_cmd.clear();
Ok(None)
}
E(C::Esc, M::NONE) => {
log::debug!("[ViEx] Esc, returning to normal mode");
Ok(Some(ViCmd {
register: RegisterName::default(),
verb: Some(VerbCmd(1, Verb::NormalMode)),
motion: None,
flags: CmdFlags::empty(),
raw_seq: "".into(),
}))
}
_ => {
log::debug!("[ViEx] forwarding key to ExEditor");
self.pending_cmd.handle_key(key).map(|_| None)
}
}
}
fn handle_key(&mut self, key: KeyEvent) -> Option<ViCmd> {
let result = self.handle_key_fallible(key);
log::debug!("[ViEx] handle_key result: {:?}", result);
result.ok().flatten()
}
fn is_repeatable(&self) -> bool {
false
}
fn as_replay(&self) -> Option<super::CmdReplay> {
None
}
fn as_replay(&self) -> Option<super::CmdReplay> {
None
}
fn cursor_style(&self) -> String {
"\x1b[3 q".to_string()
}
fn cursor_style(&self) -> String {
"\x1b[3 q".to_string()
}
fn pending_seq(&self) -> Option<String> {
Some(self.pending_cmd.buf.as_str().to_string())
}
fn pending_seq(&self) -> Option<String> {
Some(self.pending_cmd.buf.as_str().to_string())
}
fn pending_cursor(&self) -> Option<usize> {
Some(self.pending_cmd.buf.cursor.get())
}
fn pending_cursor(&self) -> Option<usize> {
Some(self.pending_cmd.buf.cursor.get())
}
fn move_cursor_on_undo(&self) -> bool {
false
}
fn move_cursor_on_undo(&self) -> bool {
false
}
fn clamp_cursor(&self) -> bool {
true
}
fn clamp_cursor(&self) -> bool {
true
}
fn hist_scroll_start_pos(&self) -> Option<To> {
None
}
fn hist_scroll_start_pos(&self) -> Option<To> {
None
}
fn report_mode(&self) -> super::ModeReport {
ModeReport::Ex
}
fn report_mode(&self) -> super::ModeReport {
ModeReport::Ex
}
}
fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>,Option<String>> {
let raw = raw.trim();
if raw.is_empty() {
return Ok(None)
}
let mut chars = raw.chars().peekable();
let (verb, motion) = {
if chars.peek() == Some(&'g') {
let mut cmd_name = String::new();
while let Some(ch) = chars.peek() {
if ch.is_alphanumeric() {
cmd_name.push(*ch);
chars.next();
} else {
break
}
}
if !"global".starts_with(&cmd_name) {
return Err(None)
}
let Some(result) = parse_global(&mut chars)? else { return Ok(None) };
(Some(VerbCmd(1,result.1)), Some(MotionCmd(1,result.0)))
} else {
(parse_ex_command(&mut chars)?.map(|v| VerbCmd(1, v)), None)
}
};
fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>, Option<String>> {
let raw = raw.trim();
if raw.is_empty() {
return Ok(None);
}
let mut chars = raw.chars().peekable();
let (verb, motion) = {
if chars.peek() == Some(&'g') {
let mut cmd_name = String::new();
while let Some(ch) = chars.peek() {
if ch.is_alphanumeric() {
cmd_name.push(*ch);
chars.next();
} else {
break;
}
}
if !"global".starts_with(&cmd_name) {
return Err(None);
}
let Some(result) = parse_global(&mut chars)? else {
return Ok(None);
};
(Some(VerbCmd(1, result.1)), Some(MotionCmd(1, result.0)))
} else {
(parse_ex_command(&mut chars)?.map(|v| VerbCmd(1, v)), None)
}
};
Ok(Some(ViCmd {
register: RegisterName::default(),
verb,
motion,
raw_seq: raw.to_string(),
flags: CmdFlags::EXIT_CUR_MODE,
}))
Ok(Some(ViCmd {
register: RegisterName::default(),
verb,
motion,
raw_seq: raw.to_string(),
flags: CmdFlags::EXIT_CUR_MODE,
}))
}
/// Unescape shell command arguments
fn unescape_shell_cmd(cmd: &str) -> String {
// The pest grammar uses double quotes for vicut commands
// So shell commands need to escape double quotes
// We will be removing a single layer of escaping from double quotes
let mut result = String::new();
let mut chars = cmd.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
if let Some(&'"') = chars.peek() {
chars.next();
result.push('"');
} else {
result.push(ch);
}
} else {
result.push(ch);
}
}
result
// The pest grammar uses double quotes for vicut commands
// So shell commands need to escape double quotes
// We will be removing a single layer of escaping from double quotes
let mut result = String::new();
let mut chars = cmd.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
if let Some(&'"') = chars.peek() {
chars.next();
result.push('"');
} else {
result.push(ch);
}
} else {
result.push(ch);
}
}
result
}
fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> {
let mut cmd_name = String::new();
fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
let mut cmd_name = String::new();
while let Some(ch) = chars.peek() {
if ch == &'!' {
cmd_name.push(*ch);
chars.next();
break
} else if !ch.is_alphanumeric() {
break
}
cmd_name.push(*ch);
chars.next();
}
while let Some(ch) = chars.peek() {
if ch == &'!' {
cmd_name.push(*ch);
chars.next();
break;
} else if !ch.is_alphanumeric() {
break;
}
cmd_name.push(*ch);
chars.next();
}
match cmd_name.as_str() {
"!" => {
let cmd = chars.collect::<String>();
let cmd = unescape_shell_cmd(&cmd);
Ok(Some(Verb::ShellCmd(cmd)))
}
"normal!" => parse_normal(chars),
_ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)),
_ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)),
_ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))),
_ if "read".starts_with(&cmd_name) => parse_read(chars),
_ if "write".starts_with(&cmd_name) => parse_write(chars),
_ if "substitute".starts_with(&cmd_name) => parse_substitute(chars),
_ => Err(None)
}
match cmd_name.as_str() {
"!" => {
let cmd = chars.collect::<String>();
let cmd = unescape_shell_cmd(&cmd);
Ok(Some(Verb::ShellCmd(cmd)))
}
"normal!" => parse_normal(chars),
_ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)),
_ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)),
_ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))),
_ if "read".starts_with(&cmd_name) => parse_read(chars),
_ if "write".starts_with(&cmd_name) => parse_write(chars),
_ if "substitute".starts_with(&cmd_name) => parse_substitute(chars),
_ => Err(None),
}
}
fn parse_normal(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> {
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop);
fn parse_normal(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
chars
.peeking_take_while(|c| c.is_whitespace())
.for_each(drop);
let seq: String = chars.collect();
Ok(Some(Verb::Normal(seq)))
let seq: String = chars.collect();
Ok(Some(Verb::Normal(seq)))
}
fn parse_read(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> {
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop);
fn parse_read(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
chars
.peeking_take_while(|c| c.is_whitespace())
.for_each(drop);
let is_shell_read = if chars.peek() == Some(&'!') { chars.next(); true } else { false };
let arg: String = chars.collect();
let is_shell_read = if chars.peek() == Some(&'!') {
chars.next();
true
} else {
false
};
let arg: String = chars.collect();
if arg.trim().is_empty() {
return Err(Some("Expected file path or shell command after ':r'".into()))
}
if arg.trim().is_empty() {
return Err(Some(
"Expected file path or shell command after ':r'".into(),
));
}
if is_shell_read {
Ok(Some(Verb::Read(ReadSrc::Cmd(arg))))
} else {
let arg_path = get_path(arg.trim());
Ok(Some(Verb::Read(ReadSrc::File(arg_path))))
}
if is_shell_read {
Ok(Some(Verb::Read(ReadSrc::Cmd(arg))))
} else {
let arg_path = get_path(arg.trim());
Ok(Some(Verb::Read(ReadSrc::File(arg_path))))
}
}
fn get_path(path: &str) -> PathBuf {
if let Some(stripped) = path.strip_prefix("~/")
&& let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home).join(stripped)
}
if path == "~"
&& let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home)
}
PathBuf::from(path)
if let Some(stripped) = path.strip_prefix("~/")
&& let Some(home) = std::env::var_os("HOME")
{
return PathBuf::from(home).join(stripped);
}
if path == "~"
&& let Some(home) = std::env::var_os("HOME")
{
return PathBuf::from(home);
}
PathBuf::from(path)
}
fn parse_write(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> {
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop);
fn parse_write(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
chars
.peeking_take_while(|c| c.is_whitespace())
.for_each(drop);
let is_shell_write = chars.peek() == Some(&'!');
if is_shell_write {
chars.next(); // consume '!'
let arg: String = chars.collect();
return Ok(Some(Verb::Write(WriteDest::Cmd(arg))));
}
let is_shell_write = chars.peek() == Some(&'!');
if is_shell_write {
chars.next(); // consume '!'
let arg: String = chars.collect();
return Ok(Some(Verb::Write(WriteDest::Cmd(arg))));
}
// Check for >>
let mut append_check = chars.clone();
let is_file_append = append_check.next() == Some('>') && append_check.next() == Some('>');
if is_file_append {
*chars = append_check;
}
// Check for >>
let mut append_check = chars.clone();
let is_file_append = append_check.next() == Some('>') && append_check.next() == Some('>');
if is_file_append {
*chars = append_check;
}
let arg: String = chars.collect();
let arg_path = get_path(arg.trim());
let arg: String = chars.collect();
let arg_path = get_path(arg.trim());
let dest = if is_file_append {
WriteDest::FileAppend(arg_path)
} else {
WriteDest::File(arg_path)
};
let dest = if is_file_append {
WriteDest::FileAppend(arg_path)
} else {
WriteDest::File(arg_path)
};
Ok(Some(Verb::Write(dest)))
Ok(Some(Verb::Write(dest)))
}
fn parse_global(chars: &mut Peekable<Chars<'_>>) -> Result<Option<(Motion,Verb)>,Option<String>> {
let is_negated = if chars.peek() == Some(&'!') { chars.next(); true } else { false };
fn parse_global(chars: &mut Peekable<Chars<'_>>) -> Result<Option<(Motion, Verb)>, Option<String>> {
let is_negated = if chars.peek() == Some(&'!') {
chars.next();
true
} else {
false
};
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); // Ignore whitespace
chars
.peeking_take_while(|c| c.is_whitespace())
.for_each(drop); // Ignore whitespace
let Some(delimiter) = chars.next() else {
return Ok(Some((Motion::Null,Verb::RepeatGlobal)))
};
if delimiter.is_alphanumeric() {
return Err(None)
}
let global_pat = parse_pattern(chars, delimiter)?;
let Some(command) = parse_ex_command(chars)? else {
return Err(Some("Expected a command after global pattern".into()))
};
if is_negated {
Ok(Some((Motion::NotGlobal(Val::new_str(global_pat)), command)))
} else {
Ok(Some((Motion::Global(Val::new_str(global_pat)), command)))
}
let Some(delimiter) = chars.next() else {
return Ok(Some((Motion::Null, Verb::RepeatGlobal)));
};
if delimiter.is_alphanumeric() {
return Err(None);
}
let global_pat = parse_pattern(chars, delimiter)?;
let Some(command) = parse_ex_command(chars)? else {
return Err(Some("Expected a command after global pattern".into()));
};
if is_negated {
Ok(Some((Motion::NotGlobal(Val::new_str(global_pat)), command)))
} else {
Ok(Some((Motion::Global(Val::new_str(global_pat)), command)))
}
}
fn parse_substitute(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> {
while chars.peek().is_some_and(|c| c.is_whitespace()) { chars.next(); } // Ignore whitespace
fn parse_substitute(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
} // Ignore whitespace
let Some(delimiter) = chars.next() else {
return Ok(Some(Verb::RepeatSubstitute))
};
if delimiter.is_alphanumeric() {
return Err(None)
}
let old_pat = parse_pattern(chars, delimiter)?;
let new_pat = parse_pattern(chars, delimiter)?;
let mut flags = SubFlags::empty();
while let Some(ch) = chars.next() {
match ch {
'g' => flags |= SubFlags::GLOBAL,
'i' => flags |= SubFlags::IGNORE_CASE,
'I' => flags |= SubFlags::NO_IGNORE_CASE,
'n' => flags |= SubFlags::SHOW_COUNT,
_ => return Err(None)
}
}
Ok(Some(Verb::Substitute(old_pat, new_pat, flags)))
let Some(delimiter) = chars.next() else {
return Ok(Some(Verb::RepeatSubstitute));
};
if delimiter.is_alphanumeric() {
return Err(None);
}
let old_pat = parse_pattern(chars, delimiter)?;
let new_pat = parse_pattern(chars, delimiter)?;
let mut flags = SubFlags::empty();
while let Some(ch) = chars.next() {
match ch {
'g' => flags |= SubFlags::GLOBAL,
'i' => flags |= SubFlags::IGNORE_CASE,
'I' => flags |= SubFlags::NO_IGNORE_CASE,
'n' => flags |= SubFlags::SHOW_COUNT,
_ => return Err(None),
}
}
Ok(Some(Verb::Substitute(old_pat, new_pat, flags)))
}
fn parse_pattern(chars: &mut Peekable<Chars<'_>>, delimiter: char) -> Result<String,Option<String>> {
let mut pat = String::new();
let mut closed = false;
while let Some(ch) = chars.next() {
match ch {
'\\' => {
if chars.peek().is_some_and(|c| *c == delimiter) {
// We escaped the delimiter, so we consume the escape char and continue
pat.push(chars.next().unwrap());
continue
} else {
// The escape char is probably for the regex in the pattern
pat.push(ch);
if let Some(esc_ch) = chars.next() {
pat.push(esc_ch)
}
}
}
_ if ch == delimiter => {
closed = true;
break
}
_ => pat.push(ch)
}
}
if !closed {
Err(Some("Unclosed pattern in ex command".into()))
} else {
Ok(pat)
}
fn parse_pattern(
chars: &mut Peekable<Chars<'_>>,
delimiter: char,
) -> Result<String, Option<String>> {
let mut pat = String::new();
let mut closed = false;
while let Some(ch) = chars.next() {
match ch {
'\\' => {
if chars.peek().is_some_and(|c| *c == delimiter) {
// We escaped the delimiter, so we consume the escape char and continue
pat.push(chars.next().unwrap());
continue;
} else {
// The escape char is probably for the regex in the pattern
pat.push(ch);
if let Some(esc_ch) = chars.next() {
pat.push(esc_ch)
}
}
}
_ if ch == delimiter => {
closed = true;
break;
}
_ => pat.push(ch),
}
}
if !closed {
Err(Some("Unclosed pattern in ex command".into()))
} else {
Ok(pat)
}
}

View File

@@ -1,8 +1,6 @@
use super::{common_cmds, CmdReplay, ModeReport, ViMode};
use super::{CmdReplay, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{
Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word,
};
use crate::readline::vicmd::{Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word};
#[derive(Default, Clone, Debug)]
pub struct ViInsert {

View File

@@ -4,47 +4,45 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::libsh::error::ShResult;
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{
Motion, MotionCmd, To, Verb, VerbCmd, ViCmd,
};
use crate::readline::vicmd::{Motion, MotionCmd, To, Verb, VerbCmd, ViCmd};
pub mod ex;
pub mod insert;
pub mod normal;
pub mod replace;
pub mod visual;
pub mod ex;
pub mod verbatim;
pub mod visual;
pub use ex::ViEx;
pub use insert::ViInsert;
pub use normal::ViNormal;
pub use replace::ViReplace;
pub use visual::ViVisual;
pub use verbatim::ViVerbatim;
pub use visual::ViVisual;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ModeReport {
Insert,
Normal,
Ex,
Ex,
Visual,
Replace,
Verbatim,
Verbatim,
Unknown,
}
impl Display for ModeReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModeReport::Insert => write!(f, "INSERT"),
ModeReport::Normal => write!(f, "NORMAL"),
ModeReport::Ex => write!(f, "COMMAND"),
ModeReport::Visual => write!(f, "VISUAL"),
ModeReport::Replace => write!(f, "REPLACE"),
ModeReport::Verbatim => write!(f, "VERBATIM"),
ModeReport::Unknown => write!(f, "UNKNOWN"),
}
}
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModeReport::Insert => write!(f, "INSERT"),
ModeReport::Normal => write!(f, "NORMAL"),
ModeReport::Ex => write!(f, "COMMAND"),
ModeReport::Visual => write!(f, "VISUAL"),
ModeReport::Replace => write!(f, "REPLACE"),
ModeReport::Verbatim => write!(f, "VERBATIM"),
ModeReport::Unknown => write!(f, "UNKNOWN"),
}
}
}
#[derive(Debug, Clone)]
@@ -73,13 +71,17 @@ pub enum CmdState {
}
pub trait ViMode {
fn handle_key_fallible(&mut self, key: E) -> ShResult<Option<ViCmd>> { Ok(self.handle_key(key)) }
fn handle_key_fallible(&mut self, key: E) -> ShResult<Option<ViCmd>> {
Ok(self.handle_key(key))
}
fn handle_key(&mut self, key: E) -> Option<ViCmd>;
fn is_repeatable(&self) -> bool;
fn as_replay(&self) -> Option<CmdReplay>;
fn cursor_style(&self) -> String;
fn pending_seq(&self) -> Option<String>;
fn pending_cursor(&self) -> Option<usize> { None }
fn pending_cursor(&self) -> Option<usize> {
None
}
fn move_cursor_on_undo(&self) -> bool;
fn clamp_cursor(&self) -> bool;
fn hist_scroll_start_pos(&self) -> Option<To>;

View File

@@ -1,7 +1,7 @@
use std::iter::Peekable;
use std::str::Chars;
use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode};
use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
@@ -309,15 +309,15 @@ impl ViNormal {
flags: self.flags(),
});
}
':' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::ExMode)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
}
':' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::ExMode)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'i' => {
return Some(ViCmd {
register,
@@ -724,7 +724,7 @@ impl ViNormal {
}
};
let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later
let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later
let verb_ref = verb.as_ref().map(|v| &v.1);
let motion_ref = motion.as_ref().map(|m| &m.1);
@@ -756,28 +756,32 @@ impl ViMode for ViNormal {
raw_seq: "".into(),
flags: self.flags(),
}),
E(K::Char('A'), M::CTRL) => {
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::IncrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: self.flags(),
})
},
E(K::Char('X'), M::CTRL) => {
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::DecrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: self.flags(),
})
},
E(K::Char('A'), M::CTRL) => {
let count = self
.parse_count(&mut self.pending_seq.chars().peekable())
.unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::IncrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: self.flags(),
})
}
E(K::Char('X'), M::CTRL) => {
let count = self
.parse_count(&mut self.pending_seq.chars().peekable())
.unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::DecrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: self.flags(),
})
}
E(K::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => Some(ViCmd {

View File

@@ -1,8 +1,6 @@
use super::{common_cmds, CmdReplay, ModeReport, ViMode};
use super::{CmdReplay, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{
Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word,
};
use crate::readline::vicmd::{Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word};
#[derive(Default, Debug)]
pub struct ViReplace {

View File

@@ -1,39 +1,40 @@
use super::{common_cmds, CmdReplay, ModeReport, ViMode};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::register::Register;
use crate::readline::vicmd::{
CmdFlags, Direction, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd, Word
};
use super::{CmdReplay, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E};
use crate::readline::vicmd::{CmdFlags, RegisterName, To, Verb, VerbCmd, ViCmd};
#[derive(Default, Clone, Debug)]
pub struct ViVerbatim {
sent_cmd: Vec<ViCmd>,
repeat_count: u16
sent_cmd: Vec<ViCmd>,
repeat_count: u16,
}
impl ViVerbatim {
pub fn new() -> Self {
Self::default()
}
pub fn with_count(self, repeat_count: u16) -> Self {
Self { repeat_count, ..self }
}
pub fn with_count(self, repeat_count: u16) -> Self {
Self {
repeat_count,
..self
}
}
}
impl ViMode for ViVerbatim {
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
match key {
E(K::Verbatim(seq),_mods) => {
log::debug!("Received verbatim key sequence: {:?}", seq);
let cmd = ViCmd { register: RegisterName::default(),
verb: Some(VerbCmd(1,Verb::Insert(seq.to_string()))),
motion: None,
raw_seq: seq.to_string(),
flags: CmdFlags::EXIT_CUR_MODE
};
self.sent_cmd.push(cmd.clone());
Some(cmd)
}
E(K::Verbatim(seq), _mods) => {
log::debug!("Received verbatim key sequence: {:?}", seq);
let cmd = ViCmd {
register: RegisterName::default(),
verb: Some(VerbCmd(1, Verb::Insert(seq.to_string()))),
motion: None,
raw_seq: seq.to_string(),
flags: CmdFlags::EXIT_CUR_MODE,
};
self.sent_cmd.push(cmd.clone());
Some(cmd)
}
_ => common_cmds(key),
}
}

View File

@@ -1,7 +1,7 @@
use std::iter::Peekable;
use std::str::Chars;
use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode};
use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
@@ -129,15 +129,15 @@ impl ViVisual {
flags: CmdFlags::empty(),
});
}
':' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::ExMode)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
}
':' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::ExMode)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'x' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
@@ -581,7 +581,7 @@ impl ViVisual {
}
};
let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later
let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later
let verb_ref = verb.as_ref().map(|v| &v.1);
let motion_ref = motion.as_ref().map(|m| &m.1);
@@ -614,28 +614,32 @@ impl ViMode for ViVisual {
raw_seq: "".into(),
flags: CmdFlags::empty(),
}),
E(K::Char('A'), M::CTRL) => {
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::IncrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: CmdFlags::empty(),
})
},
E(K::Char('X'), M::CTRL) => {
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::DecrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: CmdFlags::empty(),
})
}
E(K::Char('A'), M::CTRL) => {
let count = self
.parse_count(&mut self.pending_seq.chars().peekable())
.unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::IncrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: CmdFlags::empty(),
})
}
E(K::Char('X'), M::CTRL) => {
let count = self
.parse_count(&mut self.pending_seq.chars().peekable())
.unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::DecrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: CmdFlags::empty(),
})
}
E(K::Char('R'), M::CTRL) => {
let mut chars = self.pending_seq.chars().peekable();
let count = self.parse_count(&mut chars).unwrap_or(1);