diff --git a/Cargo.lock b/Cargo.lock index 6d71a2f..0813c44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,6 +185,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -331,6 +337,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" @@ -589,7 +604,7 @@ dependencies = [ [[package]] name = "shed" -version = "0.4.0" +version = "0.5.0" dependencies = [ "ariadne", "bitflags", @@ -597,6 +612,7 @@ dependencies = [ "env_logger", "glob", "insta", + "itertools", "log", "nix", "pretty_assertions", diff --git a/Cargo.toml b/Cargo.toml index acd84b8..3ff04c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "shed" description = "A linux shell written in rust" publish = false -version = "0.4.0" +version = "0.5.0" edition = "2024" @@ -15,6 +15,7 @@ bitflags = "2.8.0" clap = { version = "4.5.38", features = ["derive"] } env_logger = "0.11.9" glob = "0.3.2" +itertools = "0.14.0" log = "0.4.29" nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] } rand = "0.10.0" diff --git a/flake.nix b/flake.nix index ae00fa9..58b1100 100644 --- a/flake.nix +++ b/flake.nix @@ -14,7 +14,7 @@ { packages.default = pkgs.rustPlatform.buildRustPackage { pname = "shed"; - version = "0.4.0"; + version = "0.5.0"; src = self; diff --git a/src/builtin/keymap.rs b/src/builtin/keymap.rs new file mode 100644 index 0000000..3bb63bb --- /dev/null +++ b/src/builtin/keymap.rs @@ -0,0 +1,149 @@ +use crate::{ + expand::expand_keymap, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, readline::{keys::KeyEvent, vimode::ModeReport}, state::{self, write_logic} +}; + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct KeyMapFlags: u32 { + const NORMAL = 0b0000001; + const INSERT = 0b0000010; + const VISUAL = 0b0000100; + const EX = 0b0001000; + const OP_PENDING = 0b0010000; + const REPLACE = 0b0100000; + } +} + +pub struct KeyMapOpts { + remove: Option, + flags: KeyMapFlags, +} +impl KeyMapOpts { + pub fn from_opts(opts: &[Opt]) -> ShResult { + let mut flags = KeyMapFlags::empty(); + let mut remove = None; + for opt in opts { + match opt { + Opt::Short('n') => flags |= KeyMapFlags::NORMAL, + Opt::Short('i') => flags |= KeyMapFlags::INSERT, + Opt::Short('v') => flags |= KeyMapFlags::VISUAL, + Opt::Short('x') => flags |= KeyMapFlags::EX, + Opt::Short('o') => flags |= KeyMapFlags::OP_PENDING, + Opt::Short('r') => flags |= KeyMapFlags::REPLACE, + Opt::LongWithArg(name, arg) if name == "remove" => { + if remove.is_some() { + return Err(ShErr::simple(ShErrKind::ExecFail, "Duplicate --remove option for keymap".to_string())); + } + remove = Some(arg.clone()); + }, + _ => return Err(ShErr::simple(ShErrKind::ExecFail, format!("Invalid option for keymap: {:?}", opt))), + } + } + if flags.is_empty() { + return Err(ShErr::simple(ShErrKind::ExecFail, "At least one mode option must be specified for keymap".to_string()).with_note("Use -n for normal mode, -i for insert mode, -v for visual mode, -x for ex mode, and -o for operator-pending mode".to_string())); + } + Ok(Self { remove, flags }) + } + pub fn keymap_opts() -> [OptSpec;6] { + [ + OptSpec { + opt: Opt::Short('n'), // normal mode + takes_arg: false + }, + OptSpec { + opt: Opt::Short('i'), // insert mode + takes_arg: false + }, + OptSpec { + opt: Opt::Short('v'), // visual mode + takes_arg: false + }, + OptSpec { + opt: Opt::Short('x'), // ex mode + takes_arg: false + }, + OptSpec { + opt: Opt::Short('o'), // operator-pending mode + takes_arg: false + }, + OptSpec { + opt: Opt::Short('r'), // replace mode + takes_arg: false + }, + ] + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum KeyMapMatch { + NoMatch, + IsPrefix, + IsExact +} + +#[derive(Debug, Clone)] +pub struct KeyMap { + pub flags: KeyMapFlags, + pub keys: String, + pub action: String +} + +impl KeyMap { + pub fn keys_expanded(&self) -> Vec { + expand_keymap(&self.keys) + } + pub fn action_expanded(&self) -> Vec { + expand_keymap(&self.action) + } + pub fn compare(&self, other: &[KeyEvent]) -> KeyMapMatch { + let ours = self.keys_expanded(); + if other == ours { + KeyMapMatch::IsExact + } else if ours.starts_with(other) { + KeyMapMatch::IsPrefix + } else { + KeyMapMatch::NoMatch + } + } +} + +pub fn keymap(node: Node) -> ShResult<()> { + let span = node.get_span(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; + + let (argv, opts) = get_opts_from_tokens(argv, &KeyMapOpts::keymap_opts())?; + let opts = KeyMapOpts::from_opts(&opts).promote_err(span.clone())?; + if let Some(to_rm) = opts.remove { + write_logic(|l| l.remove_keymap(&to_rm)); + state::set_status(0); + return Ok(()); + } + + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } + + let Some((keys,_)) = argv.first() else { + return Err(ShErr::at(ShErrKind::ExecFail, span, "missing keys argument".to_string())); + }; + + let Some((action,_)) = argv.get(1) else { + return Err(ShErr::at(ShErrKind::ExecFail, span, "missing action argument".to_string())); + }; + + let keymap = KeyMap { + flags: opts.flags, + keys: keys.clone(), + action: action.clone(), + }; + + write_logic(|l| l.insert_keymap(keymap)); + + state::set_status(0); + Ok(()) +} diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 2b93aa4..4dd16e2 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -25,13 +25,14 @@ pub mod map; pub mod arrops; pub mod intro; pub mod getopts; +pub mod keymap; -pub const BUILTINS: [&str; 44] = [ +pub const BUILTINS: [&str; 45] = [ "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type", - "getopts" + "getopts", "keymap" ]; pub fn true_builtin() -> ShResult<()> { diff --git a/src/expand.rs b/src/expand.rs index ec7acde..53fb256 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashSet, VecDeque}; use std::iter::Peekable; use std::str::{Chars, FromStr}; @@ -11,10 +11,10 @@ use crate::parse::execute::exec_input; use crate::parse::lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule, is_hard_sep}; use crate::parse::{Redir, RedirType}; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; +use crate::readline::keys::{KeyCode, KeyEvent, ModKeys}; use crate::readline::markers; use crate::state::{ - ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_vars, write_jobs, write_meta, - write_vars, + ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_shopts, read_vars, write_jobs, write_meta, write_vars }; use crate::prelude::*; @@ -2143,3 +2143,81 @@ pub fn expand_aliases( expand_aliases(result, already_expanded, log_tab) } } + +pub fn expand_keymap(s: &str) -> Vec { + let mut keys = Vec::new(); + let mut chars = s.chars().collect::>(); + while let Some(ch) = chars.pop_front() { + match ch { + '\\' => { + if let Some(next_ch) = chars.pop_front() { + keys.push(KeyEvent(KeyCode::Char(next_ch), ModKeys::NONE)); + } + } + '<' => { + let mut alias = String::new(); + while let Some(a_ch) = chars.pop_front() { + match a_ch { + '\\' => { + if let Some(esc_ch) = chars.pop_front() { + alias.push(esc_ch); + } + } + '>' => { + if alias.eq_ignore_ascii_case("leader") { + let leader = read_shopts(|o| o.prompt.leader.clone()); + keys.extend(expand_keymap(&leader)); + } else if let Some(key) = parse_key_alias(&alias) { + keys.push(key); + } + break; + } + _ => alias.push(a_ch), + } + } + } + _ => { + keys.push(KeyEvent(KeyCode::Char(ch), ModKeys::NONE)); + } + } + } + + keys +} + +pub fn parse_key_alias(alias: &str) -> Option { + let parts: Vec<&str> = alias.split('-').collect(); + let (mods_parts, key_name) = parts.split_at(parts.len() - 1); + let mut mods = ModKeys::NONE; + for m in mods_parts { + match m.to_uppercase().as_str() { + "C" => mods |= ModKeys::CTRL, + "A" | "M" => mods |= ModKeys::ALT, + "S" => mods |= ModKeys::SHIFT, + _ => return None, + } + } + + let key = match *key_name.first()? { + "CR" => KeyCode::Char('\r'), + "ENTER" | "RETURN" => KeyCode::Enter, + "ESC" | "ESCAPE" => KeyCode::Esc, + "TAB" => KeyCode::Tab, + "BS" | "BACKSPACE" => KeyCode::Backspace, + "DEL" | "DELETE" => KeyCode::Delete, + "INS" | "INSERT" => KeyCode::Insert, + "SPACE" => KeyCode::Char(' '), + "UP" => KeyCode::Up, + "DOWN" => KeyCode::Down, + "LEFT" => KeyCode::Left, + "RIGHT" => KeyCode::Right, + "HOME" => KeyCode::Home, + "END" => KeyCode::End, + "PGUP" | "PAGEUP" => KeyCode::PageUp, + "PGDN" | "PAGEDOWN" => KeyCode::PageDown, + k if k.len() == 1 => KeyCode::Char(k.chars().next().unwrap()), + _ => return None + }; + + Some(KeyEvent(key, mods)) +} diff --git a/src/libsh/error.rs b/src/libsh/error.rs index a78e6de..6a5db4f 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -180,10 +180,15 @@ impl ShErr { Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()], io_guards: vec![] } } pub fn promote(mut self, span: Span) -> Self { - if let Some(note) = self.notes.pop() { - self = self.labeled(span, note) + if self.notes.is_empty() { + return self } - self + let first = self.notes[0].clone(); + if self.notes.len() > 1 { + self.notes = self.notes[1..].to_vec(); + } + + self.labeled(span, first) } pub fn with_redirs(mut self, guard: RedirGuard) -> Self { self.io_guards.push(guard); @@ -340,6 +345,7 @@ pub enum ShErrKind { Errno(Errno), NotFound, ReadlineErr, + ExCommand, // Not really errors, more like internal signals CleanExit(i32), @@ -369,6 +375,7 @@ impl Display for ShErrKind { Self::LoopContinue(_) => "Syntax Error", Self::LoopBreak(_) => "Syntax Error", Self::ReadlineErr => "Readline Error", + Self::ExCommand => "Ex Command Error", Self::ClearReadline => "", Self::Null => "", }; diff --git a/src/main.rs b/src/main.rs index b217361..4385a2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ use nix::errno::Errno; use nix::poll::{PollFd, PollFlags, PollTimeout, poll}; use nix::unistd::read; +use crate::builtin::keymap::KeyMapMatch; use crate::builtin::trap::TrapTarget; use crate::libsh::error::{self, ShErr, ShErrKind, ShResult}; use crate::libsh::sys::TTY_FILENO; @@ -193,8 +194,8 @@ fn shed_interactive() -> ShResult<()> { if let Err(e) = check_signals() { match e.kind() { ShErrKind::ClearReadline => { - // Ctrl+C - clear current input and show new prompt - readline.reset(false)?; + // Ctrl+C - clear current input and redraw + readline.reset_active_widget(false)?; } ShErrKind::CleanExit(code) => { QUIT_CODE.store(*code, Ordering::SeqCst); @@ -207,8 +208,11 @@ fn shed_interactive() -> ShResult<()> { if GOT_SIGWINCH.swap(false, Ordering::SeqCst) { log::info!("Window size change detected, updating readline dimensions"); + // Restore cursor to saved row before clearing, since the terminal + // may have moved it during resize/rewrap readline.writer.update_t_cols(); readline.prompt_mut().refresh()?; + readline.mark_dirty(); } if JOB_DONE.swap(false, Ordering::SeqCst) { @@ -224,7 +228,13 @@ fn shed_interactive() -> ShResult<()> { PollFlags::POLLIN, )]; - match poll(&mut fds, PollTimeout::MAX) { + let timeout = if readline.pending_keymap.is_empty() { + PollTimeout::MAX + } else { + PollTimeout::from(1000u16) + }; + + match poll(&mut fds, timeout) { Ok(_) => {} Err(Errno::EINTR) => { // Interrupted by signal, loop back to handle it @@ -236,6 +246,89 @@ fn shed_interactive() -> ShResult<()> { } } + // Timeout — resolve pending keymap ambiguity + if !readline.pending_keymap.is_empty() + && fds[0].revents().is_none_or(|r| !r.contains(PollFlags::POLLIN)) + { + log::debug!("[keymap timeout] resolving pending={:?}", readline.pending_keymap); + let keymap_flags = readline.curr_keymap_flags(); + let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &readline.pending_keymap)); + // If there's an exact match, fire it; otherwise flush as normal keys + let exact = matches.iter().find(|km| km.compare(&readline.pending_keymap) == KeyMapMatch::IsExact); + if let Some(km) = exact { + log::debug!("[keymap timeout] firing exact match: {:?} -> {:?}", km.keys, km.action); + let action = km.action_expanded(); + readline.pending_keymap.clear(); + for key in action { + if let Some(event) = readline.handle_key(key)? { + match event { + ReadlineEvent::Line(input) => { + let start = Instant::now(); + write_meta(|m| m.start_timer()); + if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true, Some("".into()))) { + match e.kind() { + ShErrKind::CleanExit(code) => { + QUIT_CODE.store(*code, Ordering::SeqCst); + return Ok(()); + } + _ => e.print_error(), + } + } + let command_run_time = start.elapsed(); + log::info!("Command executed in {:.2?}", command_run_time); + write_meta(|m| m.stop_timer()); + readline.fix_column()?; + readline.writer.flush_write("\n\r")?; + readline.reset(true)?; + break; + } + ReadlineEvent::Eof => { + QUIT_CODE.store(0, Ordering::SeqCst); + return Ok(()); + } + ReadlineEvent::Pending => {} + } + } + } + } else { + log::debug!("[keymap timeout] no exact match, flushing {} keys as normal input", readline.pending_keymap.len()); + let buffered = std::mem::take(&mut readline.pending_keymap); + for key in buffered { + if let Some(event) = readline.handle_key(key)? { + match event { + ReadlineEvent::Line(input) => { + let start = Instant::now(); + write_meta(|m| m.start_timer()); + if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true, Some("".into()))) { + match e.kind() { + ShErrKind::CleanExit(code) => { + QUIT_CODE.store(*code, Ordering::SeqCst); + return Ok(()); + } + _ => e.print_error(), + } + } + let command_run_time = start.elapsed(); + log::info!("Command executed in {:.2?}", command_run_time); + write_meta(|m| m.stop_timer()); + readline.fix_column()?; + readline.writer.flush_write("\n\r")?; + readline.reset(true)?; + break; + } + ReadlineEvent::Eof => { + QUIT_CODE.store(0, Ordering::SeqCst); + return Ok(()); + } + ReadlineEvent::Pending => {} + } + } + } + } + readline.print_line(false)?; + continue; + } + // Check if stdin has data if fds[0] .revents() diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 4c08d09..65b9505 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -7,7 +7,7 @@ use ariadne::Fmt; use crate::{ builtin::{ - alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, map, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak + alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak }, expand::{expand_aliases, glob_to_regex}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, @@ -823,6 +823,7 @@ impl Dispatcher { "wait" => jobctl::wait(cmd), "type" => intro::type_builtin(cmd), "getopts" => getopts(cmd), + "keymap" => keymap::keymap(cmd), "true" | ":" => { state::set_status(0); Ok(()) diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 2df25dd..317f11a 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -520,12 +520,14 @@ pub enum CompResponse { pub trait Completer { fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult>; fn reset(&mut self); + fn reset_stay_active(&mut self); fn is_active(&self) -> bool; fn selected_candidate(&self) -> Option; 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; fn get_completed_line(&self, candidate: &str) -> String; } @@ -610,7 +612,14 @@ impl From 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, 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 { 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() } diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index fef6ba5..3692c43 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -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("".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(()) } diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 407bdcc..3096b11 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -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, pub mode: Box, + pub pending_keymap: Vec, pub repeat_action: Option, pub repeat_motion: Option, 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 { 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> { + 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; + 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 = 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 = 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 = 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 = 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 = Box::new(ViNormal::new()); std::mem::swap(&mut mode, &mut self.mode); } + + if cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) { + let mut mode: Box = Box::new(ViNormal::new()); + std::mem::swap(&mut mode, &mut self.mode); + self.editor.set_cursor_clamp(self.mode.clamp_cursor()); + } + Ok(()) } } diff --git a/src/readline/term.rs b/src/readline/term.rs index 3a1b4df..f3a2504 100644 --- a/src/readline/term.rs +++ b/src/readline/term.rs @@ -698,6 +698,8 @@ pub struct Layout { pub prompt_end: Pos, pub cursor: Pos, pub end: Pos, + pub psr_end: Option, + 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; diff --git a/src/readline/vicmd.rs b/src/readline/vicmd.rs index 23688ac..a9ca6fa 100644 --- a/src/readline/vicmd.rs +++ b/src/readline/vicmd.rs @@ -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) + } +} diff --git a/src/readline/vimode.rs b/src/readline/vimode.rs deleted file mode 100644 index 5d9e081..0000000 --- a/src/readline/vimode.rs +++ /dev/null @@ -1,1806 +0,0 @@ -use std::iter::Peekable; -use std::str::Chars; - -use unicode_segmentation::UnicodeSegmentation; - -use super::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; -use super::vicmd::{ - Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, - VerbCmd, ViCmd, Word, -}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ModeReport { - Insert, - Normal, - Visual, - Replace, - Unknown, -} - -#[derive(Debug, Clone)] -pub enum CmdReplay { - ModeReplay { cmds: Vec, repeat: u16 }, - Single(ViCmd), - Motion(Motion), -} - -impl CmdReplay { - pub fn mode(cmds: Vec, 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(&mut self, key: E) -> Option; - fn is_repeatable(&self) -> bool; - fn as_replay(&self) -> Option; - fn cursor_style(&self) -> String; - fn pending_seq(&self) -> Option; - fn move_cursor_on_undo(&self) -> bool; - fn clamp_cursor(&self) -> bool; - fn hist_scroll_start_pos(&self) -> Option; - fn report_mode(&self) -> ModeReport; - fn cmds_from_raw(&mut self, raw: &str) -> Vec { - 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 - } -} - -#[derive(Default, Clone, Debug)] -pub struct ViInsert { - cmds: Vec, - 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 { - 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| matches!(cmd.verb(), Some(VerbCmd(1, Verb::InsertChar(_))))) - .count(); - let backspace_count = self - .cmds - .iter() - .filter(|cmd| 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 { - 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 { - Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count)) - } - - fn cursor_style(&self) -> String { - "\x1b[6 q".to_string() - } - fn pending_seq(&self) -> Option { - None - } - fn move_cursor_on_undo(&self) -> bool { - true - } - fn clamp_cursor(&self) -> bool { - false - } - fn hist_scroll_start_pos(&self) -> Option { - Some(To::End) - } - fn report_mode(&self) -> ModeReport { - ModeReport::Insert - } -} - -#[derive(Default, Debug)] -pub struct ViReplace { - cmds: Vec, - 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 { - 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 { - 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 { - None - } - fn as_replay(&self) -> Option { - 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 { - Some(To::End) - } - fn report_mode(&self) -> ModeReport { - ModeReport::Replace - } -} -#[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>) -> Option { - 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::().ok() - } else { - None - } - } - /// End the parse and clear the pending sequence - pub fn quit_parse(&mut self) -> Option { - self.clear_cmd(); - None - } - pub fn try_parse(&mut self, ch: char) -> Option { - 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 " - */ - 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(), - }); - } - '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 { - let mut cmd = 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 { - None - } - - fn cursor_style(&self) -> String { - "\x1b[2 q".to_string() - } - - fn pending_seq(&self) -> Option { - 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 { - None - } - fn report_mode(&self) -> ModeReport { - ModeReport::Normal - } -} - -#[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>) -> Option { - 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::().ok() - } else { - None - } - } - /// End the parse and clear the pending sequence - pub fn quit_parse(&mut self) -> Option { - self.clear_cmd(); - None - } - pub fn try_parse(&mut self, ch: char) -> Option { - 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 { - let mut cmd = 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('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 { - None - } - - fn cursor_style(&self) -> String { - "\x1b[2 q".to_string() - } - - fn pending_seq(&self) -> Option { - 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 { - None - } - - fn report_mode(&self) -> ModeReport { - ModeReport::Visual - } -} - -pub fn common_cmds(key: E) -> Option { - 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) -} diff --git a/src/readline/vimode/ex.rs b/src/readline/vimode/ex.rs new file mode 100644 index 0000000..2dda7f9 --- /dev/null +++ b/src/readline/vimode/ex.rs @@ -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> { + 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 { + 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 { + None + } + + fn cursor_style(&self) -> String { + "\x1b[3 q".to_string() + } + + fn pending_seq(&self) -> Option { + Some(self.pending_cmd.buf.as_str().to_string()) + } + + fn pending_cursor(&self) -> Option { + 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 { + None + } + + fn report_mode(&self) -> super::ModeReport { + ModeReport::Ex + } +} + +fn parse_ex_cmd(raw: &str) -> Result,Option> { + 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>) -> Result,Option> { + 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::(); + 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>) -> Result,Option> { + 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>) -> Result,Option> { + 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>) -> Result,Option> { + 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>) -> Result,Option> { + 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>) -> Result,Option> { + 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>, delimiter: char) -> Result> { + 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) + } +} diff --git a/src/readline/vimode/insert.rs b/src/readline/vimode/insert.rs new file mode 100644 index 0000000..4338435 --- /dev/null +++ b/src/readline/vimode/insert.rs @@ -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, + 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 { + 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 { + 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 { + Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count)) + } + + fn cursor_style(&self) -> String { + "\x1b[6 q".to_string() + } + fn pending_seq(&self) -> Option { + None + } + fn move_cursor_on_undo(&self) -> bool { + true + } + fn clamp_cursor(&self) -> bool { + false + } + fn hist_scroll_start_pos(&self) -> Option { + Some(To::End) + } + fn report_mode(&self) -> ModeReport { + ModeReport::Insert + } +} diff --git a/src/readline/vimode/mod.rs b/src/readline/vimode/mod.rs new file mode 100644 index 0000000..49767c5 --- /dev/null +++ b/src/readline/vimode/mod.rs @@ -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, repeat: u16 }, + Single(ViCmd), + Motion(Motion), +} + +impl CmdReplay { + pub fn mode(cmds: Vec, 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> { Ok(self.handle_key(key)) } + fn handle_key(&mut self, key: E) -> Option; + fn is_repeatable(&self) -> bool; + fn as_replay(&self) -> Option; + fn cursor_style(&self) -> String; + fn pending_seq(&self) -> Option; + fn pending_cursor(&self) -> Option { None } + fn move_cursor_on_undo(&self) -> bool; + fn clamp_cursor(&self) -> bool; + fn hist_scroll_start_pos(&self) -> Option; + fn report_mode(&self) -> ModeReport; + fn cmds_from_raw(&mut self, raw: &str) -> Vec { + 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 { + 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) +} diff --git a/src/readline/vimode/normal.rs b/src/readline/vimode/normal.rs new file mode 100644 index 0000000..051693f --- /dev/null +++ b/src/readline/vimode/normal.rs @@ -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>) -> Option { + 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::().ok() + } else { + None + } + } + /// End the parse and clear the pending sequence + pub fn quit_parse(&mut self) -> Option { + self.clear_cmd(); + None + } + pub fn try_parse(&mut self, ch: char) -> Option { + 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 " + */ + 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 { + let mut cmd: Option = 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 { + None + } + + fn cursor_style(&self) -> String { + "\x1b[2 q".to_string() + } + + fn pending_seq(&self) -> Option { + 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 { + None + } + fn report_mode(&self) -> ModeReport { + ModeReport::Normal + } +} diff --git a/src/readline/vimode/replace.rs b/src/readline/vimode/replace.rs new file mode 100644 index 0000000..b9c277c --- /dev/null +++ b/src/readline/vimode/replace.rs @@ -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, + 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 { + 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 { + 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 { + None + } + fn as_replay(&self) -> Option { + 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 { + Some(To::End) + } + fn report_mode(&self) -> ModeReport { + ModeReport::Replace + } +} diff --git a/src/readline/vimode/visual.rs b/src/readline/vimode/visual.rs new file mode 100644 index 0000000..c3aa76e --- /dev/null +++ b/src/readline/vimode/visual.rs @@ -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>) -> Option { + 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::().ok() + } else { + None + } + } + /// End the parse and clear the pending sequence + pub fn quit_parse(&mut self) -> Option { + self.clear_cmd(); + None + } + pub fn try_parse(&mut self, ch: char) -> Option { + 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 { + let mut cmd: Option = 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 { + None + } + + fn cursor_style(&self) -> String { + "\x1b[2 q".to_string() + } + + fn pending_seq(&self) -> Option { + 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 { + None + } + + fn report_mode(&self) -> ModeReport { + ModeReport::Visual + } +} diff --git a/src/shopt.rs b/src/shopt.rs index 320b7a8..f76dd0e 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -343,6 +343,7 @@ pub struct ShOptPrompt { pub highlight: bool, pub auto_indent: bool, pub linebreak_on_incomplete: bool, + pub leader: String, } impl ShOptPrompt { @@ -402,6 +403,9 @@ impl ShOptPrompt { }; self.linebreak_on_incomplete = val; } + "leader" => { + self.leader = val.to_string(); + } "custom" => { todo!() } @@ -459,6 +463,12 @@ impl ShOptPrompt { output.push_str(&format!("{}", self.linebreak_on_incomplete)); Ok(Some(output)) } + "leader" => { + let mut output = + String::from("The leader key sequence used in keymap bindings\n"); + output.push_str(&self.leader); + Ok(Some(output)) + } _ => Err( ShErr::simple( ShErrKind::SyntaxErr, @@ -482,6 +492,7 @@ impl Display for ShOptPrompt { "linebreak_on_incomplete = {}", self.linebreak_on_incomplete )); + output.push(format!("leader = {}", self.leader)); let final_output = output.join("\n"); @@ -498,6 +509,7 @@ impl Default for ShOptPrompt { highlight: true, auto_indent: true, linebreak_on_incomplete: true, + leader: "\\".to_string(), } } } diff --git a/src/state.rs b/src/state.rs index e8400ff..2ece57c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -11,7 +11,7 @@ use std::{ use nix::unistd::{User, gethostname, getppid}; use crate::{ - builtin::{BUILTINS, map::MapNode, trap::TrapTarget}, + builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget}, exec_input, jobs::JobTab, libsh::{ @@ -24,8 +24,7 @@ use crate::{ }, prelude::*, readline::{ - complete::{BashCompSpec, CompSpec}, - markers, + complete::{BashCompSpec, CompSpec}, keys::KeyEvent, markers }, shopt::ShOpts, }; @@ -533,12 +532,36 @@ pub struct LogTab { functions: HashMap, aliases: HashMap, traps: HashMap, + keymaps: Vec } impl LogTab { pub fn new() -> Self { Self::default() } + pub fn insert_keymap(&mut self, keymap: KeyMap) { + let mut found_dup = false; + for map in self.keymaps.iter_mut() { + if map.keys == keymap.keys { + *map = keymap.clone(); + found_dup = true; + break; + } + } + if !found_dup { + self.keymaps.push(keymap); + } + } + pub fn remove_keymap(&mut self, keys: &str) { + self.keymaps.retain(|km| km.keys != keys); + } + pub fn keymaps_filtered(&self, flags: KeyMapFlags, pending: &[KeyEvent]) -> Vec { + self.keymaps + .iter() + .filter(|km| km.flags.intersects(flags) && km.compare(pending) != KeyMapMatch::NoMatch) + .cloned() + .collect() + } pub fn insert_func(&mut self, name: &str, src: ShFunc) { self.functions.insert(name.into(), src); }