rustfmt'd the codebase
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(_)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user