Added ex mode to line editor, a 'keymap' builtin, and a zsh-like widget system using ':!<shellcmd>' ex mode commands
This commit is contained in:
@@ -520,12 +520,14 @@ pub enum CompResponse {
|
||||
pub trait Completer {
|
||||
fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>>;
|
||||
fn reset(&mut self);
|
||||
fn reset_stay_active(&mut self);
|
||||
fn is_active(&self) -> bool;
|
||||
fn selected_candidate(&self) -> Option<String>;
|
||||
fn token_span(&self) -> (usize, usize);
|
||||
fn original_input(&self) -> &str;
|
||||
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>;
|
||||
fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> { Ok(()) }
|
||||
fn set_prompt_line_context(&mut self, _line_width: u16, _cursor_col: u16) {}
|
||||
fn handle_key(&mut self, key: K) -> ShResult<CompResponse>;
|
||||
fn get_completed_line(&self, candidate: &str) -> String;
|
||||
}
|
||||
@@ -610,7 +612,14 @@ impl From<String> for ScoredCandidate {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FuzzyLayout {
|
||||
rows: u16
|
||||
rows: u16,
|
||||
cols: u16,
|
||||
cursor_col: u16,
|
||||
/// Width of the prompt line above the `\n` that starts the fuzzy window.
|
||||
/// If PSR was drawn, this is `t_cols`; otherwise the content width.
|
||||
preceding_line_width: u16,
|
||||
/// Cursor column on the prompt line before the fuzzy window was drawn.
|
||||
preceding_cursor_col: u16,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
@@ -671,7 +680,11 @@ pub struct FuzzyCompleter {
|
||||
old_layout: Option<FuzzyLayout>,
|
||||
max_height: usize,
|
||||
scroll_offset: usize,
|
||||
active: bool
|
||||
active: bool,
|
||||
/// Context from the prompt: width of the line above the fuzzy window
|
||||
prompt_line_width: u16,
|
||||
/// Context from the prompt: cursor column on the line above the fuzzy window
|
||||
prompt_cursor_col: u16,
|
||||
}
|
||||
|
||||
impl FuzzyCompleter {
|
||||
@@ -740,11 +753,23 @@ impl Default for FuzzyCompleter {
|
||||
old_layout: None,
|
||||
scroll_offset: 0,
|
||||
active: false,
|
||||
prompt_line_width: 0,
|
||||
prompt_cursor_col: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for FuzzyCompleter {
|
||||
fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) {
|
||||
self.prompt_line_width = line_width;
|
||||
self.prompt_cursor_col = cursor_col;
|
||||
}
|
||||
fn reset_stay_active(&mut self) {
|
||||
if self.is_active() {
|
||||
self.query.clear();
|
||||
self.score_candidates();
|
||||
}
|
||||
}
|
||||
fn get_completed_line(&self, _candidate: &str) -> String {
|
||||
log::debug!("Getting completed line for candidate: {}", _candidate);
|
||||
|
||||
@@ -782,6 +807,7 @@ impl Completer for FuzzyCompleter {
|
||||
|
||||
fn handle_key(&mut self, key: K) -> ShResult<CompResponse> {
|
||||
match key {
|
||||
K(C::Char('D'), M::CTRL) |
|
||||
K(C::Esc, M::NONE) => {
|
||||
self.active = false;
|
||||
self.filtered.clear();
|
||||
@@ -816,18 +842,48 @@ impl Completer for FuzzyCompleter {
|
||||
}
|
||||
fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||
if let Some(layout) = self.old_layout.take() {
|
||||
let (new_cols, _) = get_win_size(*TTY_FILENO);
|
||||
// The fuzzy window is one continuous auto-wrapped block (no hard
|
||||
// newlines between rows). After a resize the terminal re-joins
|
||||
// soft wraps and re-wraps as a flat buffer.
|
||||
let total_cells = layout.rows as u32 * layout.cols as u32;
|
||||
let physical_rows = if new_cols > 0 {
|
||||
((total_cells + new_cols as u32 - 1) / new_cols as u32) as u16
|
||||
} else {
|
||||
layout.rows
|
||||
};
|
||||
let cursor_offset = layout.cols as u32 + layout.cursor_col as u32;
|
||||
let cursor_phys_row = if new_cols > 0 {
|
||||
(cursor_offset / new_cols as u32) as u16
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let lines_below = physical_rows.saturating_sub(cursor_phys_row + 1);
|
||||
|
||||
// The prompt line above the \n may have wrapped (e.g. due to PSR
|
||||
// filling to t_cols). Compute how many extra rows that adds
|
||||
// between the prompt cursor and the fuzzy content.
|
||||
let gap_extra = if new_cols > 0 && layout.preceding_line_width > new_cols {
|
||||
let wrap_rows = ((layout.preceding_line_width as u32 + new_cols as u32 - 1)
|
||||
/ new_cols as u32) as u16;
|
||||
let cursor_wrap_row = layout.preceding_cursor_col / new_cols;
|
||||
wrap_rows.saturating_sub(cursor_wrap_row + 1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let mut buf = String::new();
|
||||
// Cursor is on the prompt line. Move down to the bottom border.
|
||||
let lines_below_prompt = layout.rows.saturating_sub(2);
|
||||
if lines_below_prompt > 0 {
|
||||
write!(buf, "\x1b[{}B", lines_below_prompt).unwrap();
|
||||
if lines_below > 0 {
|
||||
write!(buf, "\x1b[{}B", lines_below).unwrap();
|
||||
}
|
||||
// Erase each line moving up, back to the top border
|
||||
for _ in 0..layout.rows {
|
||||
for _ in 0..physical_rows {
|
||||
buf.push_str("\x1b[2K\x1b[A");
|
||||
}
|
||||
// Erase the top border line
|
||||
buf.push_str("\x1b[2K");
|
||||
// Clear extra rows from prompt line wrapping (PSR)
|
||||
for _ in 0..gap_extra {
|
||||
buf.push_str("\x1b[A\x1b[2K");
|
||||
}
|
||||
writer.flush_write(&buf)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -847,10 +903,11 @@ impl Completer for FuzzyCompleter {
|
||||
let num_filtered = format!("\x1b[33m{}\x1b[0m",self.filtered.len());
|
||||
let num_candidates = format!("\x1b[33m{}\x1b[0m",self.candidates.len());
|
||||
let visible = self.get_window();
|
||||
let mut rows = 0;
|
||||
let top_bar = format!("\n{}{}{}",
|
||||
let mut rows: u16 = 0;
|
||||
let top_bar = format!("\n{}{} \x1b[1mComplete\x1b[0m {}{}",
|
||||
Self::TOP_LEFT,
|
||||
Self::HOR_LINE.to_string().repeat(cols.saturating_sub(2) as usize),
|
||||
Self::HOR_LINE,
|
||||
Self::HOR_LINE.repeat(cols.saturating_sub(13) as usize),
|
||||
Self::TOP_RIGHT
|
||||
);
|
||||
buf.push_str(&top_bar);
|
||||
@@ -910,15 +967,19 @@ impl Completer for FuzzyCompleter {
|
||||
buf.push_str(&bot_bar);
|
||||
rows += 1;
|
||||
|
||||
let new_layout = FuzzyLayout {
|
||||
rows, // +1 for the query line
|
||||
};
|
||||
|
||||
// Move cursor back up to the prompt line (skip: separator + candidates + bottom border)
|
||||
let lines_below_prompt = new_layout.rows.saturating_sub(2); // total rows minus top_bar and prompt
|
||||
let lines_below_prompt = rows.saturating_sub(2); // total rows minus top_bar and prompt
|
||||
let cursor_in_window = self.query.linebuf.cursor.get().saturating_sub(self.query.scroll_offset);
|
||||
let cursor_col = cursor_in_window + 4; // "| > ".len() == 4
|
||||
let cursor_col = (cursor_in_window + 4) as u16; // "| > ".len() == 4
|
||||
write!(buf, "\x1b[{}A\r\x1b[{}C", lines_below_prompt, cursor_col).unwrap();
|
||||
|
||||
let new_layout = FuzzyLayout {
|
||||
rows,
|
||||
cols: cols as u16,
|
||||
cursor_col,
|
||||
preceding_line_width: self.prompt_line_width,
|
||||
preceding_cursor_col: self.prompt_cursor_col,
|
||||
};
|
||||
writer.flush_write(&buf)?;
|
||||
self.old_layout = Some(new_layout);
|
||||
|
||||
@@ -953,6 +1014,11 @@ pub struct SimpleCompleter {
|
||||
}
|
||||
|
||||
impl Completer for SimpleCompleter {
|
||||
fn reset_stay_active(&mut self) {
|
||||
let active = self.is_active();
|
||||
self.reset();
|
||||
self.active = active;
|
||||
}
|
||||
fn get_completed_line(&self, _candidate: &str) -> String {
|
||||
self.get_completed_line()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
ops::{Range, RangeInclusive},
|
||||
collections::HashSet, fmt::Display, ops::{Range, RangeInclusive}
|
||||
};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
@@ -11,14 +10,15 @@ use super::vicmd::{
|
||||
ViCmd, Word,
|
||||
};
|
||||
use crate::{
|
||||
libsh::error::ShResult,
|
||||
parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule},
|
||||
libsh::{error::ShResult, guards::var_ctx_guard},
|
||||
parse::{execute::exec_input, lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}},
|
||||
prelude::*,
|
||||
readline::{
|
||||
markers,
|
||||
register::{RegisterContent, write_register},
|
||||
term::RawModeGuard,
|
||||
},
|
||||
state::read_shopts,
|
||||
state::{VarFlags, VarKind, read_shopts, read_vars, write_vars},
|
||||
};
|
||||
|
||||
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
|
||||
@@ -2336,7 +2336,13 @@ impl LineBuf {
|
||||
MotionKind::Exclusive((0, self.grapheme_indices().len()))
|
||||
}
|
||||
MotionCmd(_count, Motion::BeginningOfBuffer) => MotionKind::On(0),
|
||||
MotionCmd(_count, Motion::EndOfBuffer) => MotionKind::To(self.grapheme_indices().len()),
|
||||
MotionCmd(_count, Motion::EndOfBuffer) => {
|
||||
if self.cursor.exclusive {
|
||||
MotionKind::On(self.grapheme_indices().len().saturating_sub(1))
|
||||
} else {
|
||||
MotionKind::On(self.grapheme_indices().len())
|
||||
}
|
||||
},
|
||||
MotionCmd(_count, Motion::ToColumn) => todo!(),
|
||||
MotionCmd(count, Motion::Range(start, end)) => {
|
||||
let mut final_end = end;
|
||||
@@ -2355,7 +2361,9 @@ impl LineBuf {
|
||||
}
|
||||
MotionCmd(_count, Motion::RepeatMotion) => todo!(),
|
||||
MotionCmd(_count, Motion::RepeatMotionRev) => todo!(),
|
||||
MotionCmd(_count, Motion::Null) => MotionKind::Null,
|
||||
MotionCmd(_count, Motion::Null)
|
||||
| MotionCmd(_count, Motion::Global(_))
|
||||
| MotionCmd(_count, Motion::NotGlobal(_)) => MotionKind::Null,
|
||||
};
|
||||
|
||||
self.set_buffer(buffer);
|
||||
@@ -2528,16 +2536,9 @@ impl LineBuf {
|
||||
) -> ShResult<()> {
|
||||
match verb {
|
||||
Verb::Delete | Verb::Yank | Verb::Change => {
|
||||
log::debug!("Executing verb: {verb:?} with motion: {motion:?}");
|
||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
||||
log::debug!("No range from motion, nothing to do");
|
||||
return Ok(());
|
||||
};
|
||||
log::debug!("Initial range from motion: ({start}, {end})");
|
||||
log::debug!(
|
||||
"self.grapheme_indices().len(): {}",
|
||||
self.grapheme_indices().len()
|
||||
);
|
||||
|
||||
let mut do_indent = false;
|
||||
if verb == Verb::Change && (start, end) == self.this_line_exclusive() {
|
||||
@@ -3014,8 +3015,16 @@ 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.this_word(Word::Normal);
|
||||
let end = (e + 1).min(self.grapheme_indices().len()); // inclusive → exclusive, capped at buffer len
|
||||
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);
|
||||
@@ -3062,6 +3071,7 @@ impl LineBuf {
|
||||
}
|
||||
|
||||
Verb::Complete
|
||||
| Verb::ExMode
|
||||
| Verb::EndOfFile
|
||||
| Verb::InsertMode
|
||||
| Verb::NormalMode
|
||||
@@ -3071,6 +3081,38 @@ impl LineBuf {
|
||||
| Verb::VisualModeBlock
|
||||
| Verb::CompleteBackward
|
||||
| Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
|
||||
|
||||
Verb::ShellCmd(cmd) => {
|
||||
let mut vars = HashSet::new();
|
||||
vars.insert("BUFFER".into());
|
||||
vars.insert("CURSOR".into());
|
||||
let _guard = var_ctx_guard(vars);
|
||||
|
||||
let mut buf = self.as_str().to_string();
|
||||
let mut cursor = self.cursor.get();
|
||||
|
||||
write_vars(|v| {
|
||||
v.set_var("BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?;
|
||||
v.set_var("CURSOR", VarKind::Str(cursor.to_string()), VarFlags::EXPORT)
|
||||
})?;
|
||||
|
||||
RawModeGuard::with_cooked_mode(|| exec_input(cmd, None, true, Some("<ex-mode-cmd>".into())))?;
|
||||
|
||||
read_vars(|v| {
|
||||
buf = v.get_var("BUFFER");
|
||||
cursor = v.get_var("CURSOR").parse().unwrap_or(cursor);
|
||||
});
|
||||
|
||||
self.set_buffer(buf);
|
||||
self.cursor.set_max(self.buffer.graphemes(true).count());
|
||||
self.cursor.set(cursor);
|
||||
}
|
||||
Verb::Normal(_)
|
||||
| Verb::Read(_)
|
||||
| Verb::Write(_)
|
||||
| Verb::Substitute(..)
|
||||
| Verb::RepeatSubstitute
|
||||
| Verb::RepeatGlobal => {} // Ex-mode verbs handled elsewhere
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::fmt::Write;
|
||||
use history::History;
|
||||
use keys::{KeyCode, KeyEvent, ModKeys};
|
||||
use linebuf::{LineBuf, SelectAnchor, SelectMode};
|
||||
@@ -6,13 +7,15 @@ use unicode_width::UnicodeWidthStr;
|
||||
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
|
||||
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
||||
|
||||
use crate::builtin::keymap::{KeyMapFlags, KeyMapMatch};
|
||||
use crate::expand::expand_prompt;
|
||||
use crate::libsh::sys::TTY_FILENO;
|
||||
use crate::parse::lex::{LexStream, QuoteState};
|
||||
use crate::prelude::*;
|
||||
use crate::readline::complete::FuzzyCompleter;
|
||||
use crate::readline::term::{Pos, TermReader, calc_str_width};
|
||||
use crate::state::{ShellParam, read_shopts};
|
||||
use crate::readline::vimode::ViEx;
|
||||
use crate::state::{ShellParam, read_logic, read_shopts};
|
||||
use crate::{
|
||||
libsh::error::ShResult,
|
||||
parse::lex::{self, LexFlags, Tk, TkFlags, TkRule},
|
||||
@@ -210,6 +213,7 @@ pub struct ShedVi {
|
||||
pub completer: Box<dyn Completer>,
|
||||
|
||||
pub mode: Box<dyn ViMode>,
|
||||
pub pending_keymap: Vec<KeyEvent>,
|
||||
pub repeat_action: Option<CmdReplay>,
|
||||
pub repeat_motion: Option<MotionCmd>,
|
||||
pub editor: LineBuf,
|
||||
@@ -229,6 +233,7 @@ impl ShedVi {
|
||||
completer: Box::new(FuzzyCompleter::default()),
|
||||
highlighter: Highlighter::new(),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
pending_keymap: Vec::new(),
|
||||
old_layout: None,
|
||||
repeat_action: None,
|
||||
repeat_motion: None,
|
||||
@@ -263,6 +268,16 @@ impl ShedVi {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset readline state for a new prompt
|
||||
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
|
||||
// Clear old display before resetting state — old_layout must survive
|
||||
@@ -287,6 +302,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::Unknown => todo!(),
|
||||
}
|
||||
|
||||
if self.mode.pending_seq().is_some_and(|seq| !seq.is_empty()) {
|
||||
flags |= KeyMapFlags::OP_PENDING;
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
|
||||
fn should_submit(&mut self) -> ShResult<bool> {
|
||||
if self.mode.report_mode() == ModeReport::Normal {
|
||||
return Ok(true);
|
||||
@@ -326,6 +359,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)?;
|
||||
match self.completer.handle_key(key.clone())? {
|
||||
CompResponse::Accept(candidate) => {
|
||||
let span_start = self.completer.token_span().0;
|
||||
@@ -351,6 +385,8 @@ impl ShedVi {
|
||||
continue;
|
||||
}
|
||||
CompResponse::Dismiss => {
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
self.completer.clear(&mut self.writer)?;
|
||||
self.completer.reset();
|
||||
continue;
|
||||
@@ -362,127 +398,48 @@ impl ShedVi {
|
||||
}
|
||||
CompResponse::Passthrough => { /* fall through to normal handling below */ }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let keymap_flags = self.curr_keymap_flags();
|
||||
self.pending_keymap.push(key.clone());
|
||||
log::debug!("[keymap] pending={:?} flags={:?}", self.pending_keymap, keymap_flags);
|
||||
|
||||
if self.should_accept_hint(&key) {
|
||||
self.editor.accept_hint();
|
||||
if !self.history.at_pending() {
|
||||
self.history.reset_to_pending();
|
||||
}
|
||||
self
|
||||
.history
|
||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let KeyEvent(KeyCode::Tab, mod_keys) = key {
|
||||
let direction = match mod_keys {
|
||||
ModKeys::SHIFT => -1,
|
||||
_ => 1,
|
||||
};
|
||||
let line = self.editor.as_str().to_string();
|
||||
let cursor_pos = self.editor.cursor_byte_pos();
|
||||
|
||||
match self.completer.complete(line, cursor_pos, direction) {
|
||||
Err(e) => {
|
||||
e.print_error();
|
||||
|
||||
// Printing the error invalidates the layout
|
||||
self.old_layout = None;
|
||||
let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap));
|
||||
log::debug!("[keymap] {} matches found", matches.len());
|
||||
if matches.is_empty() {
|
||||
// No matches. Drain the buffered keys and execute them.
|
||||
log::debug!("[keymap] no matches, flushing {} buffered keys", self.pending_keymap.len());
|
||||
for key in std::mem::take(&mut self.pending_keymap) {
|
||||
if let Some(event) = self.handle_key(key)? {
|
||||
return Ok(event);
|
||||
}
|
||||
}
|
||||
Ok(Some(line)) => {
|
||||
let span_start = self.completer.token_span().0;
|
||||
let new_cursor = span_start
|
||||
+ self
|
||||
.completer
|
||||
.selected_candidate()
|
||||
.map(|c| c.len())
|
||||
.unwrap_or_default();
|
||||
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();
|
||||
log::debug!("[keymap] exact match: {:?} -> {:?}", keymap.keys, keymap.action);
|
||||
self.pending_keymap.clear();
|
||||
let action = keymap.action_expanded();
|
||||
log::debug!("[keymap] expanded action: {:?}", action);
|
||||
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.
|
||||
log::debug!("[keymap] ambiguous: {} matches, waiting for more input", matches.len());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
self.editor.set_buffer(line);
|
||||
self.editor.cursor.set(new_cursor);
|
||||
|
||||
if !self.history.at_pending() {
|
||||
self.history.reset_to_pending();
|
||||
}
|
||||
self
|
||||
.history
|
||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
}
|
||||
Ok(None) => {
|
||||
self.writer.send_bell().ok();
|
||||
}
|
||||
}
|
||||
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
if let Some(event) = self.handle_key(key)? {
|
||||
return Ok(event);
|
||||
}
|
||||
|
||||
// if we are here, we didnt press tab
|
||||
// so we should reset the completer state
|
||||
self.completer.reset();
|
||||
|
||||
let Some(mut cmd) = self.mode.handle_key(key) else {
|
||||
continue;
|
||||
};
|
||||
cmd.alter_line_motion_if_no_verb();
|
||||
|
||||
if self.should_grab_history(&cmd) {
|
||||
self.scroll_history(cmd);
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if cmd.is_submit_action()
|
||||
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
|
||||
{
|
||||
self.editor.set_hint(None);
|
||||
self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end
|
||||
self.print_line(true)?; // Redraw
|
||||
self.writer.flush_write("\n")?;
|
||||
let buf = self.editor.take_buf();
|
||||
// Save command to history if auto_hist is enabled
|
||||
if read_shopts(|s| s.core.auto_hist) && !buf.is_empty() {
|
||||
self.history.push(buf.clone());
|
||||
if let Err(e) = self.history.save() {
|
||||
eprintln!("Failed to save history: {e}");
|
||||
}
|
||||
}
|
||||
self.history.reset();
|
||||
return Ok(ReadlineEvent::Line(buf));
|
||||
}
|
||||
|
||||
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
|
||||
if self.editor.buffer.is_empty() {
|
||||
return Ok(ReadlineEvent::Eof);
|
||||
} else {
|
||||
self.editor = LineBuf::new();
|
||||
self.mode = Box::new(ViInsert::new());
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
|
||||
|
||||
let before = self.editor.buffer.clone();
|
||||
self.exec_cmd(cmd)?;
|
||||
let after = self.editor.as_str();
|
||||
|
||||
if before != after {
|
||||
self
|
||||
.history
|
||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
} else if before == after && has_edit_verb {
|
||||
self.writer.send_bell().ok(); // bell on no-op commands with a verb (e.g., 'x' on empty line)
|
||||
}
|
||||
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
|
||||
// Redraw if we processed any input
|
||||
@@ -494,6 +451,143 @@ impl ShedVi {
|
||||
Ok(ReadlineEvent::Pending)
|
||||
}
|
||||
|
||||
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<Option<ReadlineEvent>> {
|
||||
if self.should_accept_hint(&key) {
|
||||
self.editor.accept_hint();
|
||||
if !self.history.at_pending() {
|
||||
self.history.reset_to_pending();
|
||||
}
|
||||
self
|
||||
.history
|
||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
self.needs_redraw = true;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let KeyEvent(KeyCode::Tab, mod_keys) = key {
|
||||
let direction = match mod_keys {
|
||||
ModKeys::SHIFT => -1,
|
||||
_ => 1,
|
||||
};
|
||||
let line = self.editor.as_str().to_string();
|
||||
let cursor_pos = self.editor.cursor_byte_pos();
|
||||
|
||||
match self.completer.complete(line, cursor_pos, direction) {
|
||||
Err(e) => {
|
||||
e.print_error();
|
||||
// Printing the error invalidates the layout
|
||||
self.old_layout = None;
|
||||
}
|
||||
Ok(Some(line)) => {
|
||||
let span_start = self.completer.token_span().0;
|
||||
let new_cursor = span_start
|
||||
+ self
|
||||
.completer
|
||||
.selected_candidate()
|
||||
.map(|c| c.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
self.editor.set_buffer(line);
|
||||
self.editor.cursor.set(new_cursor);
|
||||
|
||||
if !self.history.at_pending() {
|
||||
self.history.reset_to_pending();
|
||||
}
|
||||
self
|
||||
.history
|
||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
}
|
||||
Ok(None) => {
|
||||
self.writer.send_bell().ok();
|
||||
if self.completer.is_active() {
|
||||
self.editor.set_hint(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.needs_redraw = true;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Ok(cmd) = self.mode.handle_key_fallible(key) else {
|
||||
// it's an ex mode error
|
||||
self.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>;
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Some(mut cmd) = cmd else {
|
||||
log::debug!("[readline] mode.handle_key returned None");
|
||||
return Ok(None);
|
||||
};
|
||||
log::debug!("[readline] got cmd: verb={:?} motion={:?} flags={:?}", cmd.verb, cmd.motion, cmd.flags);
|
||||
cmd.alter_line_motion_if_no_verb();
|
||||
|
||||
if self.should_grab_history(&cmd) {
|
||||
self.scroll_history(cmd);
|
||||
self.needs_redraw = true;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if cmd.is_submit_action()
|
||||
&& (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)?;
|
||||
self.writer.flush_write("\n")?;
|
||||
let buf = self.editor.take_buf();
|
||||
if read_shopts(|s| s.core.auto_hist) && !buf.is_empty() {
|
||||
self.history.push(buf.clone());
|
||||
if let Err(e) = self.history.save() {
|
||||
eprintln!("Failed to save history: {e}");
|
||||
}
|
||||
}
|
||||
self.history.reset();
|
||||
return Ok(Some(ReadlineEvent::Line(buf)));
|
||||
}
|
||||
|
||||
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
|
||||
if self.editor.buffer.is_empty() {
|
||||
return Ok(Some(ReadlineEvent::Eof));
|
||||
} else {
|
||||
self.editor = LineBuf::new();
|
||||
self.mode = Box::new(ViInsert::new());
|
||||
self.needs_redraw = true;
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
|
||||
|
||||
let before = self.editor.buffer.clone();
|
||||
self.exec_cmd(cmd)?;
|
||||
let after = self.editor.as_str();
|
||||
|
||||
if before != after {
|
||||
self
|
||||
.history
|
||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
} else if before == after && has_edit_verb {
|
||||
self.writer.send_bell().ok();
|
||||
}
|
||||
|
||||
let hint = self.history.get_hint();
|
||||
|
||||
self.editor.set_hint(hint);
|
||||
self.needs_redraw = true;
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn update_layout(&mut self) {
|
||||
let text = self.line_text();
|
||||
let new = self.get_layout(&text);
|
||||
if let Some(old) = self.old_layout.as_mut() {
|
||||
*old = new;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_layout(&mut self, line: &str) -> Layout {
|
||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||
let (cols, _) = get_win_size(*TTY_FILENO);
|
||||
@@ -578,7 +672,7 @@ impl ShedVi {
|
||||
|
||||
pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> {
|
||||
let line = self.line_text();
|
||||
let new_layout = self.get_layout(&line);
|
||||
let mut new_layout = self.get_layout(&line);
|
||||
let pending_seq = self.mode.pending_seq();
|
||||
let mut prompt_string_right = self.prompt.psr_expanded.clone();
|
||||
|
||||
@@ -590,6 +684,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 row0_used = self
|
||||
.prompt
|
||||
@@ -623,7 +718,7 @@ impl ShedVi {
|
||||
&& !seq.is_empty()
|
||||
&& !(prompt_string_right.is_some() && one_line)
|
||||
&& seq_fits
|
||||
{
|
||||
&& 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
|
||||
|
||||
@@ -635,13 +730,10 @@ impl ShedVi {
|
||||
|
||||
// Save cursor, move up to top row, move right to column, write sequence,
|
||||
// restore cursor
|
||||
self
|
||||
.writer
|
||||
.flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?;
|
||||
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 {
|
||||
@@ -650,17 +742,37 @@ impl ShedVi {
|
||||
String::new()
|
||||
};
|
||||
|
||||
self
|
||||
.writer
|
||||
.flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?;
|
||||
write!(buf, "\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8").unwrap();
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
self.writer.flush_write(&self.mode.cursor_style())?;
|
||||
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)?;
|
||||
// Tell the completer the width of the prompt line above its \n so it can
|
||||
// account for wrapping when clearing after a resize.
|
||||
let preceding_width = if new_layout.psr_end.is_some() {
|
||||
self.writer.t_cols
|
||||
} else {
|
||||
// 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.draw(&mut self.writer)?;
|
||||
|
||||
self.old_layout = Some(new_layout);
|
||||
self.needs_redraw = false;
|
||||
// Save physical cursor row so SIGWINCH can restore it
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -669,39 +781,46 @@ impl ShedVi {
|
||||
let mut is_insert_mode = false;
|
||||
if cmd.is_mode_transition() {
|
||||
let count = cmd.verb_count();
|
||||
let mut mode: Box<dyn ViMode> = 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 let ModeReport::Ex = self.mode.report_mode() && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
|
||||
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::NormalMode => Box::new(ViNormal::new()),
|
||||
Verb::ExMode => Box::new(ViEx::new()),
|
||||
|
||||
Verb::ReplaceMode => Box::new(ViReplace::new()),
|
||||
Verb::NormalMode => Box::new(ViNormal::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());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
Verb::ReplaceMode => Box::new(ViReplace::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())
|
||||
}
|
||||
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());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
|
||||
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!(),
|
||||
};
|
||||
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
|
||||
@@ -818,6 +937,13 @@ impl ShedVi {
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
}
|
||||
|
||||
if cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
|
||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||
std::mem::swap(&mut mode, &mut self.mode);
|
||||
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,6 +698,8 @@ pub struct Layout {
|
||||
pub prompt_end: Pos,
|
||||
pub cursor: Pos,
|
||||
pub end: Pos,
|
||||
pub psr_end: Option<Pos>,
|
||||
pub t_cols: u16,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
@@ -706,6 +708,8 @@ impl Layout {
|
||||
prompt_end: Pos::default(),
|
||||
cursor: Pos::default(),
|
||||
end: Pos::default(),
|
||||
psr_end: None,
|
||||
t_cols: 0,
|
||||
}
|
||||
}
|
||||
pub fn from_parts(term_width: u16, prompt: &str, to_cursor: &str, to_end: &str) -> Self {
|
||||
@@ -716,6 +720,8 @@ impl Layout {
|
||||
prompt_end,
|
||||
cursor,
|
||||
end,
|
||||
psr_end: None,
|
||||
t_cols: term_width,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -925,7 +931,14 @@ impl TermWriter {
|
||||
impl LineWriter for TermWriter {
|
||||
fn clear_rows(&mut self, layout: &Layout) -> ShResult<()> {
|
||||
self.buffer.clear();
|
||||
let rows_to_clear = layout.end.row;
|
||||
// Account for lines that may have wrapped due to terminal resize.
|
||||
// If a PSR was drawn, the last row extended to the old terminal width.
|
||||
// When the terminal shrinks, that row wraps into extra physical rows.
|
||||
let mut rows_to_clear = layout.end.row;
|
||||
if layout.psr_end.is_some() && layout.t_cols > self.t_cols && self.t_cols > 0 {
|
||||
let extra = (layout.t_cols.saturating_sub(1)) / self.t_cols;
|
||||
rows_to_clear += extra;
|
||||
}
|
||||
let cursor_row = layout.cursor.row;
|
||||
|
||||
let cursor_motion = rows_to_clear.saturating_sub(cursor_row);
|
||||
@@ -950,6 +963,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
|
||||
|
||||
let end = new_layout.end;
|
||||
let cursor = new_layout.cursor;
|
||||
|
||||
@@ -63,6 +63,7 @@ bitflags! {
|
||||
const VISUAL = 1<<0;
|
||||
const VISUAL_LINE = 1<<1;
|
||||
const VISUAL_BLOCK = 1<<2;
|
||||
const EXIT_CUR_MODE = 1<<3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +178,7 @@ impl ViCmd {
|
||||
matches!(
|
||||
v.1,
|
||||
Verb::Change
|
||||
| Verb::ExMode
|
||||
| Verb::InsertMode
|
||||
| Verb::InsertModeLineBreak(_)
|
||||
| Verb::NormalMode
|
||||
@@ -184,7 +186,7 @@ impl ViCmd {
|
||||
| Verb::VisualMode
|
||||
| Verb::VisualModeLine
|
||||
| Verb::ReplaceMode
|
||||
)
|
||||
) || self.flags.contains(CmdFlags::EXIT_CUR_MODE)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -245,6 +247,15 @@ pub enum Verb {
|
||||
Equalize,
|
||||
AcceptLineOrNewline,
|
||||
EndOfFile,
|
||||
// Ex-mode verbs
|
||||
ExMode,
|
||||
ShellCmd(String),
|
||||
Normal(String),
|
||||
Read(ReadSrc),
|
||||
Write(WriteDest),
|
||||
Substitute(String, String, super::vimode::ex::SubFlags),
|
||||
RepeatSubstitute,
|
||||
RepeatGlobal,
|
||||
}
|
||||
|
||||
impl Verb {
|
||||
@@ -290,6 +301,8 @@ impl Verb {
|
||||
| Self::Insert(_)
|
||||
| Self::Rot13
|
||||
| Self::EndOfFile
|
||||
| Self::IncrementNumber(_)
|
||||
| Self::DecrementNumber(_)
|
||||
)
|
||||
}
|
||||
pub fn is_char_insert(&self) -> bool {
|
||||
@@ -339,6 +352,9 @@ pub enum Motion {
|
||||
RepeatMotion,
|
||||
RepeatMotionRev,
|
||||
Null,
|
||||
// Ex-mode motions
|
||||
Global(Val),
|
||||
NotGlobal(Val),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
@@ -467,3 +483,30 @@ pub enum To {
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
// Ex-mode types
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum ReadSrc {
|
||||
File(std::path::PathBuf),
|
||||
Cmd(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum WriteDest {
|
||||
File(std::path::PathBuf),
|
||||
FileAppend(std::path::PathBuf),
|
||||
Cmd(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum Val {
|
||||
Str(String),
|
||||
Regex(String),
|
||||
}
|
||||
|
||||
impl Val {
|
||||
pub fn new_str(s: String) -> Self {
|
||||
Self::Str(s)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
381
src/readline/vimode/ex.rs
Normal file
381
src/readline/vimode/ex.rs
Normal file
@@ -0,0 +1,381 @@
|
||||
use std::iter::Peekable;
|
||||
use std::path::PathBuf;
|
||||
use std::str::Chars;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::bitflags;
|
||||
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,
|
||||
};
|
||||
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(Default, Clone, Debug)]
|
||||
struct ExEditor {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct ViEx {
|
||||
pending_cmd: ExEditor,
|
||||
}
|
||||
|
||||
impl ViEx {
|
||||
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
|
||||
}
|
||||
|
||||
fn as_replay(&self) -> Option<super::CmdReplay> {
|
||||
None
|
||||
}
|
||||
|
||||
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_cursor(&self) -> Option<usize> {
|
||||
Some(self.pending_cmd.buf.cursor.get())
|
||||
}
|
||||
|
||||
fn move_cursor_on_undo(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn clamp_cursor(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hist_scroll_start_pos(&self) -> Option<To> {
|
||||
None
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
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))));
|
||||
}
|
||||
|
||||
// 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 dest = if is_file_append {
|
||||
WriteDest::FileAppend(arg_path)
|
||||
} else {
|
||||
WriteDest::File(arg_path)
|
||||
};
|
||||
|
||||
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 };
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
124
src/readline/vimode/insert.rs
Normal file
124
src/readline/vimode/insert.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use super::{common_cmds, CmdReplay, ModeReport, ViMode};
|
||||
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,
|
||||
};
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct ViInsert {
|
||||
cmds: Vec<ViCmd>,
|
||||
pending_cmd: ViCmd,
|
||||
repeat_count: u16,
|
||||
}
|
||||
|
||||
impl ViInsert {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn with_count(mut self, repeat_count: u16) -> Self {
|
||||
self.repeat_count = repeat_count;
|
||||
self
|
||||
}
|
||||
pub fn register_and_return(&mut self) -> Option<ViCmd> {
|
||||
let mut cmd = self.take_cmd();
|
||||
cmd.normalize_counts();
|
||||
self.register_cmd(&cmd);
|
||||
Some(cmd)
|
||||
}
|
||||
pub fn ctrl_w_is_undo(&self) -> bool {
|
||||
let insert_count = self
|
||||
.cmds
|
||||
.iter()
|
||||
.filter(|cmd: &&ViCmd| matches!(cmd.verb(), Some(VerbCmd(1, Verb::InsertChar(_)))))
|
||||
.count();
|
||||
let backspace_count = self
|
||||
.cmds
|
||||
.iter()
|
||||
.filter(|cmd: &&ViCmd| matches!(cmd.verb(), Some(VerbCmd(1, Verb::Delete))))
|
||||
.count();
|
||||
insert_count > backspace_count
|
||||
}
|
||||
pub fn register_cmd(&mut self, cmd: &ViCmd) {
|
||||
self.cmds.push(cmd.clone())
|
||||
}
|
||||
pub fn take_cmd(&mut self) -> ViCmd {
|
||||
std::mem::take(&mut self.pending_cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl ViMode for ViInsert {
|
||||
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||
match key {
|
||||
E(K::Char(ch), M::NONE) => {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::InsertChar(ch)));
|
||||
self
|
||||
.pending_cmd
|
||||
.set_motion(MotionCmd(1, Motion::ForwardChar));
|
||||
self.register_and_return()
|
||||
}
|
||||
E(K::Char('W'), M::CTRL) => {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||
self.pending_cmd.set_motion(MotionCmd(
|
||||
1,
|
||||
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
|
||||
));
|
||||
self.register_and_return()
|
||||
}
|
||||
E(K::Char('H'), M::CTRL) | E(K::Backspace, M::NONE) => {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||
self
|
||||
.pending_cmd
|
||||
.set_motion(MotionCmd(1, Motion::BackwardCharForced));
|
||||
self.register_and_return()
|
||||
}
|
||||
|
||||
E(K::BackTab, M::NONE) => {
|
||||
self
|
||||
.pending_cmd
|
||||
.set_verb(VerbCmd(1, Verb::CompleteBackward));
|
||||
self.register_and_return()
|
||||
}
|
||||
|
||||
E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Complete));
|
||||
self.register_and_return()
|
||||
}
|
||||
|
||||
E(K::Esc, M::NONE) => {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::NormalMode));
|
||||
self
|
||||
.pending_cmd
|
||||
.set_motion(MotionCmd(1, Motion::BackwardChar));
|
||||
self.register_and_return()
|
||||
}
|
||||
_ => common_cmds(key),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_repeatable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn as_replay(&self) -> Option<CmdReplay> {
|
||||
Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count))
|
||||
}
|
||||
|
||||
fn cursor_style(&self) -> String {
|
||||
"\x1b[6 q".to_string()
|
||||
}
|
||||
fn pending_seq(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
fn move_cursor_on_undo(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn clamp_cursor(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn hist_scroll_start_pos(&self) -> Option<To> {
|
||||
Some(To::End)
|
||||
}
|
||||
fn report_mode(&self) -> ModeReport {
|
||||
ModeReport::Insert
|
||||
}
|
||||
}
|
||||
103
src/readline/vimode/mod.rs
Normal file
103
src/readline/vimode/mod.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
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,
|
||||
};
|
||||
|
||||
pub mod insert;
|
||||
pub mod normal;
|
||||
pub mod replace;
|
||||
pub mod visual;
|
||||
pub mod ex;
|
||||
|
||||
pub use ex::ViEx;
|
||||
pub use insert::ViInsert;
|
||||
pub use normal::ViNormal;
|
||||
pub use replace::ViReplace;
|
||||
pub use visual::ViVisual;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ModeReport {
|
||||
Insert,
|
||||
Normal,
|
||||
Ex,
|
||||
Visual,
|
||||
Replace,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CmdReplay {
|
||||
ModeReplay { cmds: Vec<ViCmd>, repeat: u16 },
|
||||
Single(ViCmd),
|
||||
Motion(Motion),
|
||||
}
|
||||
|
||||
impl CmdReplay {
|
||||
pub fn mode(cmds: Vec<ViCmd>, repeat: u16) -> Self {
|
||||
Self::ModeReplay { cmds, repeat }
|
||||
}
|
||||
pub fn single(cmd: ViCmd) -> Self {
|
||||
Self::Single(cmd)
|
||||
}
|
||||
pub fn motion(motion: Motion) -> Self {
|
||||
Self::Motion(motion)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum CmdState {
|
||||
Pending,
|
||||
Complete,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
pub trait ViMode {
|
||||
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 move_cursor_on_undo(&self) -> bool;
|
||||
fn clamp_cursor(&self) -> bool;
|
||||
fn hist_scroll_start_pos(&self) -> Option<To>;
|
||||
fn report_mode(&self) -> ModeReport;
|
||||
fn cmds_from_raw(&mut self, raw: &str) -> Vec<ViCmd> {
|
||||
let mut cmds = vec![];
|
||||
for ch in raw.graphemes(true) {
|
||||
let key = E::new(ch, M::NONE);
|
||||
let Some(cmd) = self.handle_key(key) else {
|
||||
continue;
|
||||
};
|
||||
cmds.push(cmd)
|
||||
}
|
||||
cmds
|
||||
}
|
||||
}
|
||||
|
||||
pub fn common_cmds(key: E) -> Option<ViCmd> {
|
||||
let mut pending_cmd = ViCmd::new();
|
||||
match key {
|
||||
E(K::Home, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::BeginningOfLine)),
|
||||
E(K::End, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::EndOfLine)),
|
||||
E(K::Left, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::BackwardChar)),
|
||||
E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar)),
|
||||
E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineUp)),
|
||||
E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineDown)),
|
||||
E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1, Verb::AcceptLineOrNewline)),
|
||||
E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1, Verb::EndOfFile)),
|
||||
E(K::Delete, M::NONE) => {
|
||||
pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||
pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar));
|
||||
}
|
||||
E(K::Backspace, M::NONE) | E(K::Char('H'), M::CTRL) => {
|
||||
pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||
pending_cmd.set_motion(MotionCmd(1, Motion::BackwardChar));
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
Some(pending_cmd)
|
||||
}
|
||||
849
src/readline/vimode/normal.rs
Normal file
849
src/readline/vimode/normal.rs
Normal file
@@ -0,0 +1,849 @@
|
||||
use std::iter::Peekable;
|
||||
use std::str::Chars;
|
||||
|
||||
use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode};
|
||||
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,
|
||||
VerbCmd, ViCmd, Word,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ViNormal {
|
||||
pending_seq: String,
|
||||
pending_flags: CmdFlags,
|
||||
}
|
||||
|
||||
impl ViNormal {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn clear_cmd(&mut self) {
|
||||
self.pending_seq = String::new();
|
||||
}
|
||||
pub fn take_cmd(&mut self) -> String {
|
||||
std::mem::take(&mut self.pending_seq)
|
||||
}
|
||||
pub fn flags(&self) -> CmdFlags {
|
||||
self.pending_flags
|
||||
}
|
||||
#[allow(clippy::unnecessary_unwrap)]
|
||||
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
|
||||
if verb.is_none() {
|
||||
match motion {
|
||||
Some(Motion::TextObj(obj)) => {
|
||||
return match obj {
|
||||
TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete,
|
||||
_ => CmdState::Invalid,
|
||||
};
|
||||
}
|
||||
Some(_) => return CmdState::Complete,
|
||||
None => return CmdState::Pending,
|
||||
}
|
||||
}
|
||||
if verb.is_some() && motion.is_none() {
|
||||
match verb.unwrap() {
|
||||
Verb::Put(_) => CmdState::Complete,
|
||||
_ => CmdState::Pending,
|
||||
}
|
||||
} else {
|
||||
CmdState::Complete
|
||||
}
|
||||
}
|
||||
pub fn parse_count(&self, chars: &mut Peekable<Chars<'_>>) -> Option<usize> {
|
||||
let mut count = String::new();
|
||||
let Some(_digit @ '1'..='9') = chars.peek() else {
|
||||
return None;
|
||||
};
|
||||
count.push(chars.next().unwrap());
|
||||
while let Some(_digit @ '0'..='9') = chars.peek() {
|
||||
count.push(chars.next().unwrap());
|
||||
}
|
||||
if !count.is_empty() {
|
||||
count.parse::<usize>().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// End the parse and clear the pending sequence
|
||||
pub fn quit_parse(&mut self) -> Option<ViCmd> {
|
||||
self.clear_cmd();
|
||||
None
|
||||
}
|
||||
pub fn try_parse(&mut self, ch: char) -> Option<ViCmd> {
|
||||
self.pending_seq.push(ch);
|
||||
let mut chars = self.pending_seq.chars().peekable();
|
||||
|
||||
/*
|
||||
* Parse the register
|
||||
*
|
||||
* Registers can be any letter a-z or A-Z.
|
||||
* While uncommon, it is possible to give a count to a register name.
|
||||
*/
|
||||
let register = 'reg_parse: {
|
||||
let mut chars_clone = chars.clone();
|
||||
let count = self.parse_count(&mut chars_clone);
|
||||
|
||||
let Some('"') = chars_clone.next() else {
|
||||
break 'reg_parse RegisterName::default();
|
||||
};
|
||||
|
||||
let Some(reg_name) = chars_clone.next() else {
|
||||
return None; // Pending register name
|
||||
};
|
||||
match reg_name {
|
||||
'a'..='z' | 'A'..='Z' => { /* proceed */ }
|
||||
_ => return self.quit_parse(),
|
||||
}
|
||||
|
||||
chars = chars_clone;
|
||||
RegisterName::new(Some(reg_name), count)
|
||||
};
|
||||
|
||||
/*
|
||||
* We will now parse the verb
|
||||
* If we hit an invalid sequence, we will call 'return self.quit_parse()'
|
||||
* self.quit_parse() will clear the pending command and return None
|
||||
*
|
||||
* If we hit an incomplete sequence, we will simply return None.
|
||||
* returning None leaves the pending sequence where it is
|
||||
*
|
||||
* Note that we do use a label here for the block and 'return' values from
|
||||
* this scope using "break 'verb_parse <value>"
|
||||
*/
|
||||
let verb = 'verb_parse: {
|
||||
let mut chars_clone = chars.clone();
|
||||
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
|
||||
|
||||
let Some(ch) = chars_clone.next() else {
|
||||
break 'verb_parse None;
|
||||
};
|
||||
match ch {
|
||||
'g' => {
|
||||
if let Some(ch) = chars_clone.peek() {
|
||||
match ch {
|
||||
'v' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'~' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::ToggleCaseRange));
|
||||
}
|
||||
'u' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::ToLower));
|
||||
}
|
||||
'U' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::ToUpper));
|
||||
}
|
||||
'?' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Rot13));
|
||||
}
|
||||
_ => break 'verb_parse None,
|
||||
}
|
||||
} else {
|
||||
break 'verb_parse None;
|
||||
}
|
||||
}
|
||||
'.' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::RepeatLast)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'x' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Delete)),
|
||||
motion: Some(MotionCmd(1, Motion::ForwardCharForced)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'X' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Delete)),
|
||||
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
's' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Change)),
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'S' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Change)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineExclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'p' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::After)));
|
||||
}
|
||||
'P' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before)));
|
||||
}
|
||||
'>' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Indent));
|
||||
}
|
||||
'<' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Dedent));
|
||||
}
|
||||
'r' => {
|
||||
let ch = chars_clone.next()?;
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch, count as u16))),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'R' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::ReplaceMode)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'~' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::ToggleCaseInplace(count as u16))),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'u' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Undo)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'v' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::VisualMode)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'V' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::VisualModeLine)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'o' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::After))),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'O' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::Before))),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'a' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'A' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
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,
|
||||
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'I' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'J' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::JoinLines)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'y' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Yank));
|
||||
}
|
||||
'd' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
|
||||
}
|
||||
'c' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Change));
|
||||
}
|
||||
'Y' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Yank)),
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'D' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Delete)),
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'C' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::Change)),
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
});
|
||||
}
|
||||
'=' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Equalize));
|
||||
}
|
||||
_ => break 'verb_parse None,
|
||||
}
|
||||
};
|
||||
|
||||
let motion = 'motion_parse: {
|
||||
let mut chars_clone = chars.clone();
|
||||
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
|
||||
|
||||
let Some(ch) = chars_clone.next() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
// Double inputs like 'dd' and 'cc', and some special cases
|
||||
match (ch, &verb) {
|
||||
// Double inputs
|
||||
('?', Some(VerbCmd(_, Verb::Rot13)))
|
||||
| ('d', Some(VerbCmd(_, Verb::Delete)))
|
||||
| ('y', Some(VerbCmd(_, Verb::Yank)))
|
||||
| ('=', Some(VerbCmd(_, Verb::Equalize)))
|
||||
| ('u', Some(VerbCmd(_, Verb::ToLower)))
|
||||
| ('U', Some(VerbCmd(_, Verb::ToUpper)))
|
||||
| ('~', Some(VerbCmd(_, Verb::ToggleCaseRange)))
|
||||
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
||||
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive));
|
||||
}
|
||||
('c', Some(VerbCmd(_, Verb::Change))) => {
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive));
|
||||
}
|
||||
('W', Some(VerbCmd(_, Verb::Change))) => {
|
||||
// Same with 'W'
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::End, Word::Big, Direction::Forward),
|
||||
));
|
||||
}
|
||||
_ => { /* Nothing weird, so let's continue */ }
|
||||
}
|
||||
match ch {
|
||||
'g' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
match ch {
|
||||
'g' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
|
||||
}
|
||||
'e' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::End, Word::Normal, Direction::Backward),
|
||||
));
|
||||
}
|
||||
'E' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::End, Word::Big, Direction::Backward),
|
||||
));
|
||||
}
|
||||
'k' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp));
|
||||
}
|
||||
'j' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown));
|
||||
}
|
||||
'_' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLastWord));
|
||||
}
|
||||
'0' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfScreenLine));
|
||||
}
|
||||
'^' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::FirstGraphicalOnScreenLine));
|
||||
}
|
||||
_ => return self.quit_parse(),
|
||||
}
|
||||
}
|
||||
']' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
match ch {
|
||||
')' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Forward)));
|
||||
}
|
||||
'}' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Forward)));
|
||||
}
|
||||
_ => return self.quit_parse(),
|
||||
}
|
||||
}
|
||||
'[' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
match ch {
|
||||
'(' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Backward)));
|
||||
}
|
||||
'{' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Backward)));
|
||||
}
|
||||
_ => return self.quit_parse(),
|
||||
}
|
||||
}
|
||||
'%' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToDelimMatch));
|
||||
}
|
||||
'v' => {
|
||||
// We got 'v' after a verb
|
||||
// Instead of normal operations, we will calculate the span based on how visual
|
||||
// mode would see it
|
||||
if self
|
||||
.flags()
|
||||
.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK)
|
||||
{
|
||||
// We can't have more than one of these
|
||||
return self.quit_parse();
|
||||
}
|
||||
self.pending_flags |= CmdFlags::VISUAL;
|
||||
break 'motion_parse None;
|
||||
}
|
||||
'V' => {
|
||||
// We got 'V' after a verb
|
||||
// Instead of normal operations, we will calculate the span based on how visual
|
||||
// line mode would see it
|
||||
if self
|
||||
.flags()
|
||||
.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK)
|
||||
{
|
||||
// We can't have more than one of these
|
||||
// I know vim can technically do this, but it doesn't really make sense to allow
|
||||
// it since even in vim only the first one given is used
|
||||
return self.quit_parse();
|
||||
}
|
||||
self.pending_flags |= CmdFlags::VISUAL;
|
||||
break 'motion_parse None;
|
||||
}
|
||||
// TODO: figure out how to include 'Ctrl+V' here, might need a refactor
|
||||
'G' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer));
|
||||
}
|
||||
'f' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::CharSearch(Direction::Forward, Dest::On, *ch),
|
||||
));
|
||||
}
|
||||
'F' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::CharSearch(Direction::Backward, Dest::On, *ch),
|
||||
));
|
||||
}
|
||||
't' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::CharSearch(Direction::Forward, Dest::Before, *ch),
|
||||
));
|
||||
}
|
||||
'T' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::CharSearch(Direction::Backward, Dest::Before, *ch),
|
||||
));
|
||||
}
|
||||
';' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion));
|
||||
}
|
||||
',' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev));
|
||||
}
|
||||
'|' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToColumn));
|
||||
}
|
||||
'^' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfFirstWord));
|
||||
}
|
||||
'0' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine));
|
||||
}
|
||||
'$' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine));
|
||||
}
|
||||
'k' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::LineUp));
|
||||
}
|
||||
'j' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::LineDown));
|
||||
}
|
||||
'h' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar));
|
||||
}
|
||||
'l' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar));
|
||||
}
|
||||
'w' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::Start, Word::Normal, Direction::Forward),
|
||||
));
|
||||
}
|
||||
'W' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::Start, Word::Big, Direction::Forward),
|
||||
));
|
||||
}
|
||||
'e' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::End, Word::Normal, Direction::Forward),
|
||||
));
|
||||
}
|
||||
'E' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::End, Word::Big, Direction::Forward),
|
||||
));
|
||||
}
|
||||
'b' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
|
||||
));
|
||||
}
|
||||
'B' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::Start, Word::Big, Direction::Backward),
|
||||
));
|
||||
}
|
||||
')' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::TextObj(TextObj::Sentence(Direction::Forward)),
|
||||
));
|
||||
}
|
||||
'(' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::TextObj(TextObj::Sentence(Direction::Backward)),
|
||||
));
|
||||
}
|
||||
'}' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::TextObj(TextObj::Paragraph(Direction::Forward)),
|
||||
));
|
||||
}
|
||||
'{' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::TextObj(TextObj::Paragraph(Direction::Backward)),
|
||||
));
|
||||
}
|
||||
ch if ch == 'i' || ch == 'a' => {
|
||||
let bound = match ch {
|
||||
'i' => Bound::Inside,
|
||||
'a' => Bound::Around,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if chars_clone.peek().is_none() {
|
||||
break 'motion_parse None;
|
||||
}
|
||||
let obj = match chars_clone.next().unwrap() {
|
||||
'w' => TextObj::Word(Word::Normal, bound),
|
||||
'W' => TextObj::Word(Word::Big, bound),
|
||||
's' => TextObj::WholeSentence(bound),
|
||||
'p' => TextObj::WholeParagraph(bound),
|
||||
'"' => TextObj::DoubleQuote(bound),
|
||||
'\'' => TextObj::SingleQuote(bound),
|
||||
'`' => TextObj::BacktickQuote(bound),
|
||||
'(' | ')' | 'b' => TextObj::Paren(bound),
|
||||
'{' | '}' | 'B' => TextObj::Brace(bound),
|
||||
'[' | ']' => TextObj::Bracket(bound),
|
||||
'<' | '>' => TextObj::Angle(bound),
|
||||
_ => return self.quit_parse(),
|
||||
};
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj)));
|
||||
}
|
||||
_ => return self.quit_parse(),
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
match self.validate_combination(verb_ref, motion_ref) {
|
||||
CmdState::Complete => Some(ViCmd {
|
||||
register,
|
||||
verb,
|
||||
motion,
|
||||
raw_seq: std::mem::take(&mut self.pending_seq),
|
||||
flags: self.flags(),
|
||||
}),
|
||||
CmdState::Pending => None,
|
||||
CmdState::Invalid => {
|
||||
self.pending_seq.clear();
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViMode for ViNormal {
|
||||
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||
let mut cmd: Option<ViCmd> = match key {
|
||||
E(K::Char('V'), M::NONE) => Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: Some(VerbCmd(1, Verb::VisualModeLine)),
|
||||
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 {
|
||||
register: Default::default(),
|
||||
verb: None,
|
||||
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
||||
raw_seq: "".into(),
|
||||
flags: self.flags(),
|
||||
}),
|
||||
E(K::Char('R'), M::CTRL) => {
|
||||
let mut chars = self.pending_seq.chars().peekable();
|
||||
let count = self.parse_count(&mut chars).unwrap_or(1);
|
||||
Some(ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: Some(VerbCmd(count, Verb::Redo)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
}
|
||||
E(K::Esc, M::NONE) => {
|
||||
self.clear_cmd();
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
if let Some(cmd) = common_cmds(key) {
|
||||
self.clear_cmd();
|
||||
Some(cmd)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(cmd) = cmd.as_mut() {
|
||||
cmd.normalize_counts();
|
||||
};
|
||||
cmd
|
||||
}
|
||||
|
||||
fn is_repeatable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn as_replay(&self) -> Option<CmdReplay> {
|
||||
None
|
||||
}
|
||||
|
||||
fn cursor_style(&self) -> String {
|
||||
"\x1b[2 q".to_string()
|
||||
}
|
||||
|
||||
fn pending_seq(&self) -> Option<String> {
|
||||
Some(self.pending_seq.clone())
|
||||
}
|
||||
|
||||
fn move_cursor_on_undo(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn clamp_cursor(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn hist_scroll_start_pos(&self) -> Option<To> {
|
||||
None
|
||||
}
|
||||
fn report_mode(&self) -> ModeReport {
|
||||
ModeReport::Normal
|
||||
}
|
||||
}
|
||||
107
src/readline/vimode/replace.rs
Normal file
107
src/readline/vimode/replace.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use super::{common_cmds, CmdReplay, ModeReport, ViMode};
|
||||
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,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ViReplace {
|
||||
cmds: Vec<ViCmd>,
|
||||
pending_cmd: ViCmd,
|
||||
repeat_count: u16,
|
||||
}
|
||||
|
||||
impl ViReplace {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn with_count(mut self, repeat_count: u16) -> Self {
|
||||
self.repeat_count = repeat_count;
|
||||
self
|
||||
}
|
||||
pub fn register_and_return(&mut self) -> Option<ViCmd> {
|
||||
let mut cmd = self.take_cmd();
|
||||
cmd.normalize_counts();
|
||||
self.register_cmd(&cmd);
|
||||
Some(cmd)
|
||||
}
|
||||
pub fn register_cmd(&mut self, cmd: &ViCmd) {
|
||||
self.cmds.push(cmd.clone())
|
||||
}
|
||||
pub fn take_cmd(&mut self) -> ViCmd {
|
||||
std::mem::take(&mut self.pending_cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl ViMode for ViReplace {
|
||||
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||
match key {
|
||||
E(K::Char(ch), M::NONE) => {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::ReplaceChar(ch)));
|
||||
self
|
||||
.pending_cmd
|
||||
.set_motion(MotionCmd(1, Motion::ForwardChar));
|
||||
self.register_and_return()
|
||||
}
|
||||
E(K::Char('W'), M::CTRL) => {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||
self.pending_cmd.set_motion(MotionCmd(
|
||||
1,
|
||||
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
|
||||
));
|
||||
self.register_and_return()
|
||||
}
|
||||
E(K::Char('H'), M::CTRL) | E(K::Backspace, M::NONE) => {
|
||||
self
|
||||
.pending_cmd
|
||||
.set_motion(MotionCmd(1, Motion::BackwardChar));
|
||||
self.register_and_return()
|
||||
}
|
||||
|
||||
E(K::BackTab, M::NONE) => {
|
||||
self
|
||||
.pending_cmd
|
||||
.set_verb(VerbCmd(1, Verb::CompleteBackward));
|
||||
self.register_and_return()
|
||||
}
|
||||
|
||||
E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Complete));
|
||||
self.register_and_return()
|
||||
}
|
||||
|
||||
E(K::Esc, M::NONE) => {
|
||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::NormalMode));
|
||||
self
|
||||
.pending_cmd
|
||||
.set_motion(MotionCmd(1, Motion::BackwardChar));
|
||||
self.register_and_return()
|
||||
}
|
||||
_ => common_cmds(key),
|
||||
}
|
||||
}
|
||||
fn is_repeatable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn cursor_style(&self) -> String {
|
||||
"\x1b[4 q".to_string()
|
||||
}
|
||||
fn pending_seq(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
fn as_replay(&self) -> Option<CmdReplay> {
|
||||
Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count))
|
||||
}
|
||||
fn move_cursor_on_undo(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn clamp_cursor(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn hist_scroll_start_pos(&self) -> Option<To> {
|
||||
Some(To::End)
|
||||
}
|
||||
fn report_mode(&self) -> ModeReport {
|
||||
ModeReport::Replace
|
||||
}
|
||||
}
|
||||
695
src/readline/vimode/visual.rs
Normal file
695
src/readline/vimode/visual.rs
Normal file
@@ -0,0 +1,695 @@
|
||||
use std::iter::Peekable;
|
||||
use std::str::Chars;
|
||||
|
||||
use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode};
|
||||
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,
|
||||
VerbCmd, ViCmd, Word,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ViVisual {
|
||||
pending_seq: String,
|
||||
}
|
||||
|
||||
impl ViVisual {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn clear_cmd(&mut self) {
|
||||
self.pending_seq = String::new();
|
||||
}
|
||||
pub fn take_cmd(&mut self) -> String {
|
||||
std::mem::take(&mut self.pending_seq)
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_unwrap)]
|
||||
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
|
||||
if verb.is_none() {
|
||||
match motion {
|
||||
Some(_) => return CmdState::Complete,
|
||||
None => return CmdState::Pending,
|
||||
}
|
||||
}
|
||||
if motion.is_none() && verb.is_some() {
|
||||
match verb.unwrap() {
|
||||
Verb::Put(_) => CmdState::Complete,
|
||||
_ => CmdState::Pending,
|
||||
}
|
||||
} else {
|
||||
CmdState::Complete
|
||||
}
|
||||
}
|
||||
pub fn parse_count(&self, chars: &mut Peekable<Chars<'_>>) -> Option<usize> {
|
||||
let mut count = String::new();
|
||||
let Some(_digit @ '1'..='9') = chars.peek() else {
|
||||
return None;
|
||||
};
|
||||
count.push(chars.next().unwrap());
|
||||
while let Some(_digit @ '0'..='9') = chars.peek() {
|
||||
count.push(chars.next().unwrap());
|
||||
}
|
||||
if !count.is_empty() {
|
||||
count.parse::<usize>().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// End the parse and clear the pending sequence
|
||||
pub fn quit_parse(&mut self) -> Option<ViCmd> {
|
||||
self.clear_cmd();
|
||||
None
|
||||
}
|
||||
pub fn try_parse(&mut self, ch: char) -> Option<ViCmd> {
|
||||
self.pending_seq.push(ch);
|
||||
let mut chars = self.pending_seq.chars().peekable();
|
||||
|
||||
let register = 'reg_parse: {
|
||||
let mut chars_clone = chars.clone();
|
||||
let count = self.parse_count(&mut chars_clone);
|
||||
|
||||
let Some('"') = chars_clone.next() else {
|
||||
break 'reg_parse RegisterName::default();
|
||||
};
|
||||
|
||||
let Some(reg_name) = chars_clone.next() else {
|
||||
return None; // Pending register name
|
||||
};
|
||||
match reg_name {
|
||||
'a'..='z' | 'A'..='Z' => { /* proceed */ }
|
||||
_ => return self.quit_parse(),
|
||||
}
|
||||
|
||||
chars = chars_clone;
|
||||
RegisterName::new(Some(reg_name), count)
|
||||
};
|
||||
|
||||
let verb = 'verb_parse: {
|
||||
let mut chars_clone = chars.clone();
|
||||
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
|
||||
|
||||
let Some(ch) = chars_clone.next() else {
|
||||
break 'verb_parse None;
|
||||
};
|
||||
match ch {
|
||||
'g' => {
|
||||
if let Some(ch) = chars_clone.peek() {
|
||||
match ch {
|
||||
'v' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'?' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Rot13)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
_ => break 'verb_parse None,
|
||||
}
|
||||
} else {
|
||||
break 'verb_parse None;
|
||||
}
|
||||
}
|
||||
'.' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::RepeatLast)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'x' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
|
||||
}
|
||||
'X' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Delete)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'Y' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Yank)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'D' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Delete)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'R' | 'C' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Change)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineExclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'>' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Indent)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'<' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Dedent)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'=' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::Equalize)),
|
||||
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'p' | 'P' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before)));
|
||||
}
|
||||
'r' => {
|
||||
let ch = chars_clone.next()?;
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'~' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(1, Verb::ToggleCaseRange)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'u' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::ToLower)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'U' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::ToUpper)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'O' | 'o' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::SwapVisualAnchor)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'A' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'I' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||
motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'J' => {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(VerbCmd(count, Verb::JoinLines)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
'y' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Yank));
|
||||
}
|
||||
'd' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
|
||||
}
|
||||
'c' => {
|
||||
chars = chars_clone;
|
||||
break 'verb_parse Some(VerbCmd(count, Verb::Change));
|
||||
}
|
||||
_ => break 'verb_parse None,
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(verb) = verb {
|
||||
return Some(ViCmd {
|
||||
register,
|
||||
verb: Some(verb),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
});
|
||||
}
|
||||
|
||||
let motion = 'motion_parse: {
|
||||
let mut chars_clone = chars.clone();
|
||||
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
|
||||
|
||||
let Some(ch) = chars_clone.next() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
match (ch, &verb) {
|
||||
('d', Some(VerbCmd(_, Verb::Delete)))
|
||||
| ('y', Some(VerbCmd(_, Verb::Yank)))
|
||||
| ('=', Some(VerbCmd(_, Verb::Equalize)))
|
||||
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
||||
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive));
|
||||
}
|
||||
('c', Some(VerbCmd(_, Verb::Change))) => {
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match ch {
|
||||
'g' => {
|
||||
if let Some(ch) = chars_clone.peek() {
|
||||
match ch {
|
||||
'g' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
|
||||
}
|
||||
'e' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::End, Word::Normal, Direction::Backward),
|
||||
));
|
||||
}
|
||||
'E' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::End, Word::Big, Direction::Backward),
|
||||
));
|
||||
}
|
||||
'k' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp));
|
||||
}
|
||||
'j' => {
|
||||
chars_clone.next();
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown));
|
||||
}
|
||||
_ => return self.quit_parse(),
|
||||
}
|
||||
} else {
|
||||
break 'motion_parse None;
|
||||
}
|
||||
}
|
||||
']' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
match ch {
|
||||
')' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Forward)));
|
||||
}
|
||||
'}' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Forward)));
|
||||
}
|
||||
_ => return self.quit_parse(),
|
||||
}
|
||||
}
|
||||
'[' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
match ch {
|
||||
'(' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Backward)));
|
||||
}
|
||||
'{' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Backward)));
|
||||
}
|
||||
_ => return self.quit_parse(),
|
||||
}
|
||||
}
|
||||
'%' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToDelimMatch));
|
||||
}
|
||||
'f' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::CharSearch(Direction::Forward, Dest::On, *ch),
|
||||
));
|
||||
}
|
||||
'F' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::CharSearch(Direction::Backward, Dest::On, *ch),
|
||||
));
|
||||
}
|
||||
't' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::CharSearch(Direction::Forward, Dest::Before, *ch),
|
||||
));
|
||||
}
|
||||
'T' => {
|
||||
let Some(ch) = chars_clone.peek() else {
|
||||
break 'motion_parse None;
|
||||
};
|
||||
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::CharSearch(Direction::Backward, Dest::Before, *ch),
|
||||
));
|
||||
}
|
||||
';' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion));
|
||||
}
|
||||
',' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev));
|
||||
}
|
||||
'|' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ToColumn));
|
||||
}
|
||||
'0' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine));
|
||||
}
|
||||
'$' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine));
|
||||
}
|
||||
'k' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::LineUp));
|
||||
}
|
||||
'j' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::LineDown));
|
||||
}
|
||||
'h' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar));
|
||||
}
|
||||
'l' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar));
|
||||
}
|
||||
'w' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::Start, Word::Normal, Direction::Forward),
|
||||
));
|
||||
}
|
||||
'W' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::Start, Word::Big, Direction::Forward),
|
||||
));
|
||||
}
|
||||
'e' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::End, Word::Normal, Direction::Forward),
|
||||
));
|
||||
}
|
||||
'E' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::End, Word::Big, Direction::Forward),
|
||||
));
|
||||
}
|
||||
'b' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
|
||||
));
|
||||
}
|
||||
'B' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::WordMotion(To::Start, Word::Big, Direction::Backward),
|
||||
));
|
||||
}
|
||||
')' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::TextObj(TextObj::Sentence(Direction::Forward)),
|
||||
));
|
||||
}
|
||||
'(' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::TextObj(TextObj::Sentence(Direction::Backward)),
|
||||
));
|
||||
}
|
||||
'}' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::TextObj(TextObj::Paragraph(Direction::Forward)),
|
||||
));
|
||||
}
|
||||
'{' => {
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(
|
||||
count,
|
||||
Motion::TextObj(TextObj::Paragraph(Direction::Backward)),
|
||||
));
|
||||
}
|
||||
ch if ch == 'i' || ch == 'a' => {
|
||||
let bound = match ch {
|
||||
'i' => Bound::Inside,
|
||||
'a' => Bound::Around,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if chars_clone.peek().is_none() {
|
||||
break 'motion_parse None;
|
||||
}
|
||||
let obj = match chars_clone.next().unwrap() {
|
||||
'w' => TextObj::Word(Word::Normal, bound),
|
||||
'W' => TextObj::Word(Word::Big, bound),
|
||||
's' => TextObj::WholeSentence(bound),
|
||||
'p' => TextObj::WholeParagraph(bound),
|
||||
'"' => TextObj::DoubleQuote(bound),
|
||||
'\'' => TextObj::SingleQuote(bound),
|
||||
'`' => TextObj::BacktickQuote(bound),
|
||||
'(' | ')' | 'b' => TextObj::Paren(bound),
|
||||
'{' | '}' | 'B' => TextObj::Brace(bound),
|
||||
'[' | ']' => TextObj::Bracket(bound),
|
||||
'<' | '>' => TextObj::Angle(bound),
|
||||
_ => return self.quit_parse(),
|
||||
};
|
||||
chars = chars_clone;
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj)));
|
||||
}
|
||||
_ => return self.quit_parse(),
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
match self.validate_combination(verb_ref, motion_ref) {
|
||||
CmdState::Complete => Some(ViCmd {
|
||||
register,
|
||||
verb,
|
||||
motion,
|
||||
raw_seq: std::mem::take(&mut self.pending_seq),
|
||||
flags: CmdFlags::empty(),
|
||||
}),
|
||||
CmdState::Pending => None,
|
||||
CmdState::Invalid => {
|
||||
self.pending_seq.clear();
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViMode for ViVisual {
|
||||
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||
let mut cmd: Option<ViCmd> = match key {
|
||||
E(K::Char(ch), M::NONE) => self.try_parse(ch),
|
||||
E(K::Backspace, M::NONE) => Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: None,
|
||||
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
||||
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);
|
||||
Some(ViCmd {
|
||||
register: RegisterName::default(),
|
||||
verb: Some(VerbCmd(count, Verb::Redo)),
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
}
|
||||
E(K::Esc, M::NONE) => Some(ViCmd {
|
||||
register: Default::default(),
|
||||
verb: Some(VerbCmd(1, Verb::NormalMode)),
|
||||
motion: Some(MotionCmd(1, Motion::Null)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
}),
|
||||
_ => {
|
||||
if let Some(cmd) = common_cmds(key) {
|
||||
self.clear_cmd();
|
||||
Some(cmd)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(cmd) = cmd.as_mut() {
|
||||
cmd.normalize_counts();
|
||||
};
|
||||
cmd
|
||||
}
|
||||
|
||||
fn is_repeatable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn as_replay(&self) -> Option<CmdReplay> {
|
||||
None
|
||||
}
|
||||
|
||||
fn cursor_style(&self) -> String {
|
||||
"\x1b[2 q".to_string()
|
||||
}
|
||||
|
||||
fn pending_seq(&self) -> Option<String> {
|
||||
Some(self.pending_seq.clone())
|
||||
}
|
||||
|
||||
fn move_cursor_on_undo(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn clamp_cursor(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn hist_scroll_start_pos(&self) -> Option<To> {
|
||||
None
|
||||
}
|
||||
|
||||
fn report_mode(&self) -> ModeReport {
|
||||
ModeReport::Visual
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user