Compare commits

..

2 Commits

23 changed files with 3150 additions and 2016 deletions

18
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -14,7 +14,7 @@
{
packages.default = pkgs.rustPlatform.buildRustPackage {
pname = "shed";
version = "0.4.0";
version = "0.5.0";
src = self;

150
src/builtin/keymap.rs Normal file
View File

@@ -0,0 +1,150 @@
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<String>,
flags: KeyMapFlags,
}
impl KeyMapOpts {
pub fn from_opts(opts: &[Opt]) -> ShResult<Self> {
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<KeyEvent> {
expand_keymap(&self.keys)
}
pub fn action_expanded(&self) -> Vec<KeyEvent> {
expand_keymap(&self.action)
}
pub fn compare(&self, other: &[KeyEvent]) -> KeyMapMatch {
log::debug!("Comparing keymap keys {:?} with input {:?}", self.keys_expanded(), other);
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(())
}

View File

@@ -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<()> {

View File

@@ -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,88 @@ pub fn expand_aliases(
expand_aliases(result, already_expanded, log_tab)
}
}
pub fn expand_keymap(s: &str) -> Vec<KeyEvent> {
log::debug!("Expanding keymap for '{}'", s);
let mut keys = Vec::new();
let mut chars = s.chars().collect::<VecDeque<char>>();
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);
}
}
'>' => {
log::debug!("Found key alias '{}'", alias);
if alias.eq_ignore_ascii_case("leader") {
let mut leader = read_shopts(|o| o.prompt.leader.clone());
if leader == "\\" {
leader.push('\\');
}
log::debug!("Expanding leader key to '{}'", leader);
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));
}
}
}
log::debug!("Expanded keymap '{}' to {:?}", s, keys);
keys
}
pub fn parse_key_alias(alias: &str) -> Option<KeyEvent> {
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))
}

View File

@@ -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 => "",
};

View File

@@ -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("<stdin>".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("<stdin>".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()

View File

@@ -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(())

View File

@@ -520,12 +520,14 @@ pub enum CompResponse {
pub trait Completer {
fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>>;
fn reset(&mut self);
fn reset_stay_active(&mut self);
fn is_active(&self) -> bool;
fn selected_candidate(&self) -> Option<String>;
fn token_span(&self) -> (usize, usize);
fn original_input(&self) -> &str;
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>;
fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> { Ok(()) }
fn set_prompt_line_context(&mut self, _line_width: u16, _cursor_col: u16) {}
fn handle_key(&mut self, key: K) -> ShResult<CompResponse>;
fn get_completed_line(&self, candidate: &str) -> String;
}
@@ -610,7 +612,14 @@ impl From<String> for ScoredCandidate {
#[derive(Debug, Clone)]
pub struct FuzzyLayout {
rows: u16
rows: u16,
cols: u16,
cursor_col: u16,
/// Width of the prompt line above the `\n` that starts the fuzzy window.
/// If PSR was drawn, this is `t_cols`; otherwise the content width.
preceding_line_width: u16,
/// Cursor column on the prompt line before the fuzzy window was drawn.
preceding_cursor_col: u16,
}
#[derive(Default, Debug, Clone)]
@@ -671,7 +680,11 @@ pub struct FuzzyCompleter {
old_layout: Option<FuzzyLayout>,
max_height: usize,
scroll_offset: usize,
active: bool
active: bool,
/// Context from the prompt: width of the line above the fuzzy window
prompt_line_width: u16,
/// Context from the prompt: cursor column on the line above the fuzzy window
prompt_cursor_col: u16,
}
impl FuzzyCompleter {
@@ -740,11 +753,23 @@ impl Default for FuzzyCompleter {
old_layout: None,
scroll_offset: 0,
active: false,
prompt_line_width: 0,
prompt_cursor_col: 0,
}
}
}
impl Completer for FuzzyCompleter {
fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) {
self.prompt_line_width = line_width;
self.prompt_cursor_col = cursor_col;
}
fn reset_stay_active(&mut self) {
if self.is_active() {
self.query.clear();
self.score_candidates();
}
}
fn get_completed_line(&self, _candidate: &str) -> String {
log::debug!("Getting completed line for candidate: {}", _candidate);
@@ -782,6 +807,7 @@ impl Completer for FuzzyCompleter {
fn handle_key(&mut self, key: K) -> ShResult<CompResponse> {
match key {
K(C::Char('D'), M::CTRL) |
K(C::Esc, M::NONE) => {
self.active = false;
self.filtered.clear();
@@ -816,18 +842,48 @@ impl Completer for FuzzyCompleter {
}
fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
if let Some(layout) = self.old_layout.take() {
let (new_cols, _) = get_win_size(*TTY_FILENO);
// The fuzzy window is one continuous auto-wrapped block (no hard
// newlines between rows). After a resize the terminal re-joins
// soft wraps and re-wraps as a flat buffer.
let total_cells = layout.rows as u32 * layout.cols as u32;
let physical_rows = if new_cols > 0 {
((total_cells + new_cols as u32 - 1) / new_cols as u32) as u16
} else {
layout.rows
};
let cursor_offset = layout.cols as u32 + layout.cursor_col as u32;
let cursor_phys_row = if new_cols > 0 {
(cursor_offset / new_cols as u32) as u16
} else {
1
};
let lines_below = physical_rows.saturating_sub(cursor_phys_row + 1);
// The prompt line above the \n may have wrapped (e.g. due to PSR
// filling to t_cols). Compute how many extra rows that adds
// between the prompt cursor and the fuzzy content.
let gap_extra = if new_cols > 0 && layout.preceding_line_width > new_cols {
let wrap_rows = ((layout.preceding_line_width as u32 + new_cols as u32 - 1)
/ new_cols as u32) as u16;
let cursor_wrap_row = layout.preceding_cursor_col / new_cols;
wrap_rows.saturating_sub(cursor_wrap_row + 1)
} else {
0
};
let mut buf = String::new();
// Cursor is on the prompt line. Move down to the bottom border.
let lines_below_prompt = layout.rows.saturating_sub(2);
if lines_below_prompt > 0 {
write!(buf, "\x1b[{}B", lines_below_prompt).unwrap();
if lines_below > 0 {
write!(buf, "\x1b[{}B", lines_below).unwrap();
}
// Erase each line moving up, back to the top border
for _ in 0..layout.rows {
for _ in 0..physical_rows {
buf.push_str("\x1b[2K\x1b[A");
}
// Erase the top border line
buf.push_str("\x1b[2K");
// Clear extra rows from prompt line wrapping (PSR)
for _ in 0..gap_extra {
buf.push_str("\x1b[A\x1b[2K");
}
writer.flush_write(&buf)?;
}
Ok(())
@@ -847,10 +903,11 @@ impl Completer for FuzzyCompleter {
let num_filtered = format!("\x1b[33m{}\x1b[0m",self.filtered.len());
let num_candidates = format!("\x1b[33m{}\x1b[0m",self.candidates.len());
let visible = self.get_window();
let mut rows = 0;
let top_bar = format!("\n{}{}{}",
let mut rows: u16 = 0;
let top_bar = format!("\n{}{} \x1b[1mComplete\x1b[0m {}{}",
Self::TOP_LEFT,
Self::HOR_LINE.to_string().repeat(cols.saturating_sub(2) as usize),
Self::HOR_LINE,
Self::HOR_LINE.repeat(cols.saturating_sub(13) as usize),
Self::TOP_RIGHT
);
buf.push_str(&top_bar);
@@ -910,15 +967,19 @@ impl Completer for FuzzyCompleter {
buf.push_str(&bot_bar);
rows += 1;
let new_layout = FuzzyLayout {
rows, // +1 for the query line
};
// Move cursor back up to the prompt line (skip: separator + candidates + bottom border)
let lines_below_prompt = new_layout.rows.saturating_sub(2); // total rows minus top_bar and prompt
let lines_below_prompt = rows.saturating_sub(2); // total rows minus top_bar and prompt
let cursor_in_window = self.query.linebuf.cursor.get().saturating_sub(self.query.scroll_offset);
let cursor_col = cursor_in_window + 4; // "| > ".len() == 4
let cursor_col = (cursor_in_window + 4) as u16; // "| > ".len() == 4
write!(buf, "\x1b[{}A\r\x1b[{}C", lines_below_prompt, cursor_col).unwrap();
let new_layout = FuzzyLayout {
rows,
cols: cols as u16,
cursor_col,
preceding_line_width: self.prompt_line_width,
preceding_cursor_col: self.prompt_cursor_col,
};
writer.flush_write(&buf)?;
self.old_layout = Some(new_layout);
@@ -953,6 +1014,11 @@ pub struct SimpleCompleter {
}
impl Completer for SimpleCompleter {
fn reset_stay_active(&mut self) {
let active = self.is_active();
self.reset();
self.active = active;
}
fn get_completed_line(&self, _candidate: &str) -> String {
self.get_completed_line()
}

View File

@@ -1,6 +1,5 @@
use std::{
fmt::Display,
ops::{Range, RangeInclusive},
collections::HashSet, fmt::Display, ops::{Range, RangeInclusive}
};
use unicode_segmentation::UnicodeSegmentation;
@@ -11,14 +10,15 @@ use super::vicmd::{
ViCmd, Word,
};
use crate::{
libsh::error::ShResult,
parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule},
libsh::{error::ShResult, guards::var_ctx_guard},
parse::{execute::exec_input, lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}},
prelude::*,
readline::{
markers,
register::{RegisterContent, write_register},
term::RawModeGuard,
},
state::read_shopts,
state::{VarFlags, VarKind, read_shopts, read_vars, write_vars},
};
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
@@ -2336,7 +2336,13 @@ impl LineBuf {
MotionKind::Exclusive((0, self.grapheme_indices().len()))
}
MotionCmd(_count, Motion::BeginningOfBuffer) => MotionKind::On(0),
MotionCmd(_count, Motion::EndOfBuffer) => MotionKind::To(self.grapheme_indices().len()),
MotionCmd(_count, Motion::EndOfBuffer) => {
if self.cursor.exclusive {
MotionKind::On(self.grapheme_indices().len().saturating_sub(1))
} else {
MotionKind::On(self.grapheme_indices().len())
}
},
MotionCmd(_count, Motion::ToColumn) => todo!(),
MotionCmd(count, Motion::Range(start, end)) => {
let mut final_end = end;
@@ -2355,7 +2361,9 @@ impl LineBuf {
}
MotionCmd(_count, Motion::RepeatMotion) => todo!(),
MotionCmd(_count, Motion::RepeatMotionRev) => todo!(),
MotionCmd(_count, Motion::Null) => MotionKind::Null,
MotionCmd(_count, Motion::Null)
| MotionCmd(_count, Motion::Global(_))
| MotionCmd(_count, Motion::NotGlobal(_)) => MotionKind::Null,
};
self.set_buffer(buffer);
@@ -2528,16 +2536,9 @@ impl LineBuf {
) -> ShResult<()> {
match verb {
Verb::Delete | Verb::Yank | Verb::Change => {
log::debug!("Executing verb: {verb:?} with motion: {motion:?}");
let Some((start, end)) = self.range_from_motion(&motion) else {
log::debug!("No range from motion, nothing to do");
return Ok(());
};
log::debug!("Initial range from motion: ({start}, {end})");
log::debug!(
"self.grapheme_indices().len(): {}",
self.grapheme_indices().len()
);
let mut do_indent = false;
if verb == Verb::Change && (start, end) == self.this_line_exclusive() {
@@ -3014,8 +3015,16 @@ impl LineBuf {
Verb::IncrementNumber(n) |
Verb::DecrementNumber(n) => {
let inc = if matches!(verb, Verb::IncrementNumber(_)) { n as i64 } else { -(n as i64) };
let (s, e) = self.this_word(Word::Normal);
let end = (e + 1).min(self.grapheme_indices().len()); // inclusive → exclusive, capped at buffer len
let (s, e) = self.select_range().unwrap_or(self.this_word(Word::Normal));
let end = if self.select_range().is_some() {
if e < self.grapheme_indices().len() - 1 {
e
} else {
e + 1
}
} else {
(e + 1).min(self.grapheme_indices().len())
}; // inclusive → exclusive, capped at buffer len
let word = self.slice(s..end).unwrap_or_default().to_lowercase();
let byte_start = self.index_byte_pos(s);
@@ -3062,6 +3071,7 @@ impl LineBuf {
}
Verb::Complete
| Verb::ExMode
| Verb::EndOfFile
| Verb::InsertMode
| Verb::NormalMode
@@ -3071,6 +3081,38 @@ impl LineBuf {
| Verb::VisualModeBlock
| Verb::CompleteBackward
| Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
Verb::ShellCmd(cmd) => {
let mut vars = HashSet::new();
vars.insert("BUFFER".into());
vars.insert("CURSOR".into());
let _guard = var_ctx_guard(vars);
let mut buf = self.as_str().to_string();
let mut cursor = self.cursor.get();
write_vars(|v| {
v.set_var("BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?;
v.set_var("CURSOR", VarKind::Str(cursor.to_string()), VarFlags::EXPORT)
})?;
RawModeGuard::with_cooked_mode(|| exec_input(cmd, None, true, Some("<ex-mode-cmd>".into())))?;
read_vars(|v| {
buf = v.get_var("BUFFER");
cursor = v.get_var("CURSOR").parse().unwrap_or(cursor);
});
self.set_buffer(buf);
self.cursor.set_max(self.buffer.graphemes(true).count());
self.cursor.set(cursor);
}
Verb::Normal(_)
| Verb::Read(_)
| Verb::Write(_)
| Verb::Substitute(..)
| Verb::RepeatSubstitute
| Verb::RepeatGlobal => {} // Ex-mode verbs handled elsewhere
}
Ok(())
}

View File

@@ -1,3 +1,4 @@
use std::fmt::Write;
use history::History;
use keys::{KeyCode, KeyEvent, ModKeys};
use linebuf::{LineBuf, SelectAnchor, SelectMode};
@@ -6,13 +7,15 @@ use unicode_width::UnicodeWidthStr;
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
use crate::builtin::keymap::{KeyMapFlags, KeyMapMatch};
use crate::expand::expand_prompt;
use crate::libsh::sys::TTY_FILENO;
use crate::parse::lex::{LexStream, QuoteState};
use crate::prelude::*;
use crate::readline::complete::FuzzyCompleter;
use crate::readline::term::{Pos, TermReader, calc_str_width};
use crate::state::{ShellParam, read_shopts};
use crate::readline::vimode::ViEx;
use crate::state::{ShellParam, read_logic, read_shopts};
use crate::{
libsh::error::ShResult,
parse::lex::{self, LexFlags, Tk, TkFlags, TkRule},
@@ -210,6 +213,7 @@ pub struct ShedVi {
pub completer: Box<dyn Completer>,
pub mode: Box<dyn ViMode>,
pub pending_keymap: Vec<KeyEvent>,
pub repeat_action: Option<CmdReplay>,
pub repeat_motion: Option<MotionCmd>,
pub editor: LineBuf,
@@ -229,6 +233,7 @@ impl ShedVi {
completer: Box::new(FuzzyCompleter::default()),
highlighter: Highlighter::new(),
mode: Box::new(ViInsert::new()),
pending_keymap: Vec::new(),
old_layout: None,
repeat_action: None,
repeat_motion: None,
@@ -263,6 +268,16 @@ impl ShedVi {
self.writer.fix_cursor_column(&mut TermReader::new(*TTY_FILENO))
}
pub fn reset_active_widget(&mut self, full_redraw: bool) -> ShResult<()> {
if self.completer.is_active() {
self.completer.reset_stay_active();
self.needs_redraw = true;
Ok(())
} else {
self.reset(full_redraw)
}
}
/// Reset readline state for a new prompt
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
// Clear old display before resetting state — old_layout must survive
@@ -287,6 +302,24 @@ impl ShedVi {
&mut self.prompt
}
pub fn curr_keymap_flags(&self) -> KeyMapFlags {
let mut flags = KeyMapFlags::empty();
match self.mode.report_mode() {
ModeReport::Insert => flags |= KeyMapFlags::INSERT,
ModeReport::Normal => flags |= KeyMapFlags::NORMAL,
ModeReport::Ex => flags |= KeyMapFlags::EX,
ModeReport::Visual => flags |= KeyMapFlags::VISUAL,
ModeReport::Replace => flags |= KeyMapFlags::REPLACE,
ModeReport::Unknown => todo!(),
}
if self.mode.pending_seq().is_some_and(|seq| !seq.is_empty()) {
flags |= KeyMapFlags::OP_PENDING;
}
flags
}
fn should_submit(&mut self) -> ShResult<bool> {
if self.mode.report_mode() == ModeReport::Normal {
return Ok(true);
@@ -326,6 +359,7 @@ impl ShedVi {
while let Some(key) = self.reader.read_key()? {
// If completer is active, delegate input to it
if self.completer.is_active() {
self.print_line(false)?;
match self.completer.handle_key(key.clone())? {
CompResponse::Accept(candidate) => {
let span_start = self.completer.token_span().0;
@@ -351,6 +385,8 @@ impl ShedVi {
continue;
}
CompResponse::Dismiss => {
let hint = self.history.get_hint();
self.editor.set_hint(hint);
self.completer.clear(&mut self.writer)?;
self.completer.reset();
continue;
@@ -362,127 +398,49 @@ 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] self.pending_keymap={:?}", self.pending_keymap);
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 +452,143 @@ impl ShedVi {
Ok(ReadlineEvent::Pending)
}
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<Option<ReadlineEvent>> {
if self.should_accept_hint(&key) {
self.editor.accept_hint();
if !self.history.at_pending() {
self.history.reset_to_pending();
}
self
.history
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
self.needs_redraw = true;
return Ok(None);
}
if let KeyEvent(KeyCode::Tab, mod_keys) = key {
let direction = match mod_keys {
ModKeys::SHIFT => -1,
_ => 1,
};
let line = self.editor.as_str().to_string();
let cursor_pos = self.editor.cursor_byte_pos();
match self.completer.complete(line, cursor_pos, direction) {
Err(e) => {
e.print_error();
// Printing the error invalidates the layout
self.old_layout = None;
}
Ok(Some(line)) => {
let span_start = self.completer.token_span().0;
let new_cursor = span_start
+ self
.completer
.selected_candidate()
.map(|c| c.len())
.unwrap_or_default();
self.editor.set_buffer(line);
self.editor.cursor.set(new_cursor);
if !self.history.at_pending() {
self.history.reset_to_pending();
}
self
.history
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
let hint = self.history.get_hint();
self.editor.set_hint(hint);
}
Ok(None) => {
self.writer.send_bell().ok();
if self.completer.is_active() {
self.editor.set_hint(None);
}
}
}
self.needs_redraw = true;
return Ok(None);
}
let Ok(cmd) = self.mode.handle_key_fallible(key) else {
// it's an ex mode error
self.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>;
return Ok(None);
};
let Some(mut cmd) = cmd else {
log::debug!("[readline] mode.handle_key returned None");
return Ok(None);
};
log::debug!("[readline] got cmd: verb={:?} motion={:?} flags={:?}", cmd.verb, cmd.motion, cmd.flags);
cmd.alter_line_motion_if_no_verb();
if self.should_grab_history(&cmd) {
self.scroll_history(cmd);
self.needs_redraw = true;
return Ok(None);
}
if cmd.is_submit_action()
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
{
self.editor.set_hint(None);
self.editor.cursor.set(self.editor.cursor_max());
self.print_line(true)?;
self.writer.flush_write("\n")?;
let buf = self.editor.take_buf();
if read_shopts(|s| s.core.auto_hist) && !buf.is_empty() {
self.history.push(buf.clone());
if let Err(e) = self.history.save() {
eprintln!("Failed to save history: {e}");
}
}
self.history.reset();
return Ok(Some(ReadlineEvent::Line(buf)));
}
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
if self.editor.buffer.is_empty() {
return Ok(Some(ReadlineEvent::Eof));
} else {
self.editor = LineBuf::new();
self.mode = Box::new(ViInsert::new());
self.needs_redraw = true;
return Ok(None);
}
}
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
let before = self.editor.buffer.clone();
self.exec_cmd(cmd)?;
let after = self.editor.as_str();
if before != after {
self
.history
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
} else if before == after && has_edit_verb {
self.writer.send_bell().ok();
}
let hint = self.history.get_hint();
self.editor.set_hint(hint);
self.needs_redraw = true;
Ok(None)
}
pub fn update_layout(&mut self) {
let text = self.line_text();
let new = self.get_layout(&text);
if let Some(old) = self.old_layout.as_mut() {
*old = new;
}
}
pub fn get_layout(&mut self, line: &str) -> Layout {
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
let (cols, _) = get_win_size(*TTY_FILENO);
@@ -578,7 +673,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 +685,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 +719,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 +731,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 +743,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 +782,46 @@ impl ShedVi {
let mut is_insert_mode = false;
if cmd.is_mode_transition() {
let count = cmd.verb_count();
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
is_insert_mode = true;
Box::new(ViInsert::new().with_count(count as u16))
}
let mut mode: Box<dyn ViMode> = if let ModeReport::Ex = self.mode.report_mode() && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
Box::new(ViNormal::new())
} else {
match cmd.verb().unwrap().1 {
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
is_insert_mode = true;
Box::new(ViInsert::new().with_count(count as u16))
}
Verb::NormalMode => Box::new(ViNormal::new()),
Verb::ExMode => Box::new(ViEx::new()),
Verb::ReplaceMode => Box::new(ViReplace::new()),
Verb::NormalMode => Box::new(ViNormal::new()),
Verb::VisualModeSelectLast => {
if self.mode.report_mode() != ModeReport::Visual {
self
.editor
.start_selecting(SelectMode::Char(SelectAnchor::End));
}
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
std::mem::swap(&mut mode, &mut self.mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
Verb::ReplaceMode => Box::new(ViReplace::new()),
return self.editor.exec_cmd(cmd);
}
Verb::VisualMode => {
select_mode = Some(SelectMode::Char(SelectAnchor::End));
Box::new(ViVisual::new())
}
Verb::VisualModeLine => {
select_mode = Some(SelectMode::Line(SelectAnchor::End));
Box::new(ViVisual::new())
}
Verb::VisualModeSelectLast => {
if self.mode.report_mode() != ModeReport::Visual {
self
.editor
.start_selecting(SelectMode::Char(SelectAnchor::End));
}
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
std::mem::swap(&mut mode, &mut self.mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
return self.editor.exec_cmd(cmd);
}
Verb::VisualMode => {
select_mode = Some(SelectMode::Char(SelectAnchor::End));
Box::new(ViVisual::new())
}
Verb::VisualModeLine => {
select_mode = Some(SelectMode::Line(SelectAnchor::End));
Box::new(ViVisual::new())
}
_ => unreachable!(),
}
};
_ => unreachable!(),
};
std::mem::swap(&mut mode, &mut self.mode);
@@ -818,6 +938,13 @@ impl ShedVi {
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
std::mem::swap(&mut mode, &mut self.mode);
}
if cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
std::mem::swap(&mut mode, &mut self.mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
}
Ok(())
}
}

View File

@@ -698,6 +698,8 @@ pub struct Layout {
pub prompt_end: Pos,
pub cursor: Pos,
pub end: Pos,
pub psr_end: Option<Pos>,
pub t_cols: u16,
}
impl Layout {
@@ -706,6 +708,8 @@ impl Layout {
prompt_end: Pos::default(),
cursor: Pos::default(),
end: Pos::default(),
psr_end: None,
t_cols: 0,
}
}
pub fn from_parts(term_width: u16, prompt: &str, to_cursor: &str, to_end: &str) -> Self {
@@ -716,6 +720,8 @@ impl Layout {
prompt_end,
cursor,
end,
psr_end: None,
t_cols: term_width,
}
}
@@ -925,7 +931,14 @@ impl TermWriter {
impl LineWriter for TermWriter {
fn clear_rows(&mut self, layout: &Layout) -> ShResult<()> {
self.buffer.clear();
let rows_to_clear = layout.end.row;
// Account for lines that may have wrapped due to terminal resize.
// If a PSR was drawn, the last row extended to the old terminal width.
// When the terminal shrinks, that row wraps into extra physical rows.
let mut rows_to_clear = layout.end.row;
if layout.psr_end.is_some() && layout.t_cols > self.t_cols && self.t_cols > 0 {
let extra = (layout.t_cols.saturating_sub(1)) / self.t_cols;
rows_to_clear += extra;
}
let cursor_row = layout.cursor.row;
let cursor_motion = rows_to_clear.saturating_sub(cursor_row);
@@ -950,6 +963,7 @@ impl LineWriter for TermWriter {
)
};
self.buffer.clear();
self.buffer.push_str("\x1b[J"); // Clear from cursor to end of screen to erase any remnants of the old line after the prompt
let end = new_layout.end;
let cursor = new_layout.cursor;

View File

@@ -63,6 +63,7 @@ bitflags! {
const VISUAL = 1<<0;
const VISUAL_LINE = 1<<1;
const VISUAL_BLOCK = 1<<2;
const EXIT_CUR_MODE = 1<<3;
}
}
@@ -177,6 +178,7 @@ impl ViCmd {
matches!(
v.1,
Verb::Change
| Verb::ExMode
| Verb::InsertMode
| Verb::InsertModeLineBreak(_)
| Verb::NormalMode
@@ -184,7 +186,7 @@ impl ViCmd {
| Verb::VisualMode
| Verb::VisualModeLine
| Verb::ReplaceMode
)
) || self.flags.contains(CmdFlags::EXIT_CUR_MODE)
})
}
}
@@ -245,6 +247,15 @@ pub enum Verb {
Equalize,
AcceptLineOrNewline,
EndOfFile,
// Ex-mode verbs
ExMode,
ShellCmd(String),
Normal(String),
Read(ReadSrc),
Write(WriteDest),
Substitute(String, String, super::vimode::ex::SubFlags),
RepeatSubstitute,
RepeatGlobal,
}
impl Verb {
@@ -290,6 +301,8 @@ impl Verb {
| Self::Insert(_)
| Self::Rot13
| Self::EndOfFile
| Self::IncrementNumber(_)
| Self::DecrementNumber(_)
)
}
pub fn is_char_insert(&self) -> bool {
@@ -339,6 +352,9 @@ pub enum Motion {
RepeatMotion,
RepeatMotionRev,
Null,
// Ex-mode motions
Global(Val),
NotGlobal(Val),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
@@ -467,3 +483,30 @@ pub enum To {
Start,
End,
}
// Ex-mode types
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ReadSrc {
File(std::path::PathBuf),
Cmd(String),
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum WriteDest {
File(std::path::PathBuf),
FileAppend(std::path::PathBuf),
Cmd(String),
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Val {
Str(String),
Regex(String),
}
impl Val {
pub fn new_str(s: String) -> Self {
Self::Str(s)
}
}

File diff suppressed because it is too large Load Diff

381
src/readline/vimode/ex.rs Normal file
View File

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

View File

@@ -0,0 +1,124 @@
use super::{common_cmds, CmdReplay, ModeReport, ViMode};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{
Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word,
};
#[derive(Default, Clone, Debug)]
pub struct ViInsert {
cmds: Vec<ViCmd>,
pending_cmd: ViCmd,
repeat_count: u16,
}
impl ViInsert {
pub fn new() -> Self {
Self::default()
}
pub fn with_count(mut self, repeat_count: u16) -> Self {
self.repeat_count = repeat_count;
self
}
pub fn register_and_return(&mut self) -> Option<ViCmd> {
let mut cmd = self.take_cmd();
cmd.normalize_counts();
self.register_cmd(&cmd);
Some(cmd)
}
pub fn ctrl_w_is_undo(&self) -> bool {
let insert_count = self
.cmds
.iter()
.filter(|cmd: &&ViCmd| matches!(cmd.verb(), Some(VerbCmd(1, Verb::InsertChar(_)))))
.count();
let backspace_count = self
.cmds
.iter()
.filter(|cmd: &&ViCmd| matches!(cmd.verb(), Some(VerbCmd(1, Verb::Delete))))
.count();
insert_count > backspace_count
}
pub fn register_cmd(&mut self, cmd: &ViCmd) {
self.cmds.push(cmd.clone())
}
pub fn take_cmd(&mut self) -> ViCmd {
std::mem::take(&mut self.pending_cmd)
}
}
impl ViMode for ViInsert {
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
match key {
E(K::Char(ch), M::NONE) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::InsertChar(ch)));
self
.pending_cmd
.set_motion(MotionCmd(1, Motion::ForwardChar));
self.register_and_return()
}
E(K::Char('W'), M::CTRL) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
self.pending_cmd.set_motion(MotionCmd(
1,
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
));
self.register_and_return()
}
E(K::Char('H'), M::CTRL) | E(K::Backspace, M::NONE) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
self
.pending_cmd
.set_motion(MotionCmd(1, Motion::BackwardCharForced));
self.register_and_return()
}
E(K::BackTab, M::NONE) => {
self
.pending_cmd
.set_verb(VerbCmd(1, Verb::CompleteBackward));
self.register_and_return()
}
E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Complete));
self.register_and_return()
}
E(K::Esc, M::NONE) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::NormalMode));
self
.pending_cmd
.set_motion(MotionCmd(1, Motion::BackwardChar));
self.register_and_return()
}
_ => common_cmds(key),
}
}
fn is_repeatable(&self) -> bool {
true
}
fn as_replay(&self) -> Option<CmdReplay> {
Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count))
}
fn cursor_style(&self) -> String {
"\x1b[6 q".to_string()
}
fn pending_seq(&self) -> Option<String> {
None
}
fn move_cursor_on_undo(&self) -> bool {
true
}
fn clamp_cursor(&self) -> bool {
false
}
fn hist_scroll_start_pos(&self) -> Option<To> {
Some(To::End)
}
fn report_mode(&self) -> ModeReport {
ModeReport::Insert
}
}

103
src/readline/vimode/mod.rs Normal file
View File

@@ -0,0 +1,103 @@
use unicode_segmentation::UnicodeSegmentation;
use crate::libsh::error::ShResult;
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{
Motion, MotionCmd, To, Verb, VerbCmd, ViCmd,
};
pub mod insert;
pub mod normal;
pub mod replace;
pub mod visual;
pub mod ex;
pub use ex::ViEx;
pub use insert::ViInsert;
pub use normal::ViNormal;
pub use replace::ViReplace;
pub use visual::ViVisual;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ModeReport {
Insert,
Normal,
Ex,
Visual,
Replace,
Unknown,
}
#[derive(Debug, Clone)]
pub enum CmdReplay {
ModeReplay { cmds: Vec<ViCmd>, repeat: u16 },
Single(ViCmd),
Motion(Motion),
}
impl CmdReplay {
pub fn mode(cmds: Vec<ViCmd>, repeat: u16) -> Self {
Self::ModeReplay { cmds, repeat }
}
pub fn single(cmd: ViCmd) -> Self {
Self::Single(cmd)
}
pub fn motion(motion: Motion) -> Self {
Self::Motion(motion)
}
}
pub enum CmdState {
Pending,
Complete,
Invalid,
}
pub trait ViMode {
fn handle_key_fallible(&mut self, key: E) -> ShResult<Option<ViCmd>> { Ok(self.handle_key(key)) }
fn handle_key(&mut self, key: E) -> Option<ViCmd>;
fn is_repeatable(&self) -> bool;
fn as_replay(&self) -> Option<CmdReplay>;
fn cursor_style(&self) -> String;
fn pending_seq(&self) -> Option<String>;
fn pending_cursor(&self) -> Option<usize> { None }
fn move_cursor_on_undo(&self) -> bool;
fn clamp_cursor(&self) -> bool;
fn hist_scroll_start_pos(&self) -> Option<To>;
fn report_mode(&self) -> ModeReport;
fn cmds_from_raw(&mut self, raw: &str) -> Vec<ViCmd> {
let mut cmds = vec![];
for ch in raw.graphemes(true) {
let key = E::new(ch, M::NONE);
let Some(cmd) = self.handle_key(key) else {
continue;
};
cmds.push(cmd)
}
cmds
}
}
pub fn common_cmds(key: E) -> Option<ViCmd> {
let mut pending_cmd = ViCmd::new();
match key {
E(K::Home, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::BeginningOfLine)),
E(K::End, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::EndOfLine)),
E(K::Left, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::BackwardChar)),
E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar)),
E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineUp)),
E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineDown)),
E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1, Verb::AcceptLineOrNewline)),
E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1, Verb::EndOfFile)),
E(K::Delete, M::NONE) => {
pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar));
}
E(K::Backspace, M::NONE) | E(K::Char('H'), M::CTRL) => {
pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
pending_cmd.set_motion(MotionCmd(1, Motion::BackwardChar));
}
_ => return None,
}
Some(pending_cmd)
}

View File

@@ -0,0 +1,849 @@
use std::iter::Peekable;
use std::str::Chars;
use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
VerbCmd, ViCmd, Word,
};
#[derive(Default, Debug)]
pub struct ViNormal {
pending_seq: String,
pending_flags: CmdFlags,
}
impl ViNormal {
pub fn new() -> Self {
Self::default()
}
pub fn clear_cmd(&mut self) {
self.pending_seq = String::new();
}
pub fn take_cmd(&mut self) -> String {
std::mem::take(&mut self.pending_seq)
}
pub fn flags(&self) -> CmdFlags {
self.pending_flags
}
#[allow(clippy::unnecessary_unwrap)]
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
if verb.is_none() {
match motion {
Some(Motion::TextObj(obj)) => {
return match obj {
TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete,
_ => CmdState::Invalid,
};
}
Some(_) => return CmdState::Complete,
None => return CmdState::Pending,
}
}
if verb.is_some() && motion.is_none() {
match verb.unwrap() {
Verb::Put(_) => CmdState::Complete,
_ => CmdState::Pending,
}
} else {
CmdState::Complete
}
}
pub fn parse_count(&self, chars: &mut Peekable<Chars<'_>>) -> Option<usize> {
let mut count = String::new();
let Some(_digit @ '1'..='9') = chars.peek() else {
return None;
};
count.push(chars.next().unwrap());
while let Some(_digit @ '0'..='9') = chars.peek() {
count.push(chars.next().unwrap());
}
if !count.is_empty() {
count.parse::<usize>().ok()
} else {
None
}
}
/// End the parse and clear the pending sequence
pub fn quit_parse(&mut self) -> Option<ViCmd> {
self.clear_cmd();
None
}
pub fn try_parse(&mut self, ch: char) -> Option<ViCmd> {
self.pending_seq.push(ch);
let mut chars = self.pending_seq.chars().peekable();
/*
* Parse the register
*
* Registers can be any letter a-z or A-Z.
* While uncommon, it is possible to give a count to a register name.
*/
let register = 'reg_parse: {
let mut chars_clone = chars.clone();
let count = self.parse_count(&mut chars_clone);
let Some('"') = chars_clone.next() else {
break 'reg_parse RegisterName::default();
};
let Some(reg_name) = chars_clone.next() else {
return None; // Pending register name
};
match reg_name {
'a'..='z' | 'A'..='Z' => { /* proceed */ }
_ => return self.quit_parse(),
}
chars = chars_clone;
RegisterName::new(Some(reg_name), count)
};
/*
* We will now parse the verb
* If we hit an invalid sequence, we will call 'return self.quit_parse()'
* self.quit_parse() will clear the pending command and return None
*
* If we hit an incomplete sequence, we will simply return None.
* returning None leaves the pending sequence where it is
*
* Note that we do use a label here for the block and 'return' values from
* this scope using "break 'verb_parse <value>"
*/
let verb = 'verb_parse: {
let mut chars_clone = chars.clone();
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
let Some(ch) = chars_clone.next() else {
break 'verb_parse None;
};
match ch {
'g' => {
if let Some(ch) = chars_clone.peek() {
match ch {
'v' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'~' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToggleCaseRange));
}
'u' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToLower));
}
'U' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::ToUpper));
}
'?' => {
chars_clone.next();
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Rot13));
}
_ => break 'verb_parse None,
}
} else {
break 'verb_parse None;
}
}
'.' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::RepeatLast)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'x' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::ForwardCharForced)),
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'X' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
's' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'S' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::WholeLineExclusive)),
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'p' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::After)));
}
'P' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before)));
}
'>' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Indent));
}
'<' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Dedent));
}
'r' => {
let ch = chars_clone.next()?;
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch, count as u16))),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'R' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::ReplaceMode)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'~' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ToggleCaseInplace(count as u16))),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'u' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Undo)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'v' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::VisualMode)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'V' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::VisualModeLine)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'o' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::After))),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'O' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::Before))),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'a' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'A' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
':' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::ExMode)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
}
'i' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'I' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'J' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::JoinLines)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'y' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Yank));
}
'd' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
}
'c' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Change));
}
'Y' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Yank)),
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'D' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'C' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::Change)),
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(),
flags: self.flags(),
});
}
'=' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Equalize));
}
_ => break 'verb_parse None,
}
};
let motion = 'motion_parse: {
let mut chars_clone = chars.clone();
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
let Some(ch) = chars_clone.next() else {
break 'motion_parse None;
};
// Double inputs like 'dd' and 'cc', and some special cases
match (ch, &verb) {
// Double inputs
('?', Some(VerbCmd(_, Verb::Rot13)))
| ('d', Some(VerbCmd(_, Verb::Delete)))
| ('y', Some(VerbCmd(_, Verb::Yank)))
| ('=', Some(VerbCmd(_, Verb::Equalize)))
| ('u', Some(VerbCmd(_, Verb::ToLower)))
| ('U', Some(VerbCmd(_, Verb::ToUpper)))
| ('~', Some(VerbCmd(_, Verb::ToggleCaseRange)))
| ('>', Some(VerbCmd(_, Verb::Indent)))
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive));
}
('c', Some(VerbCmd(_, Verb::Change))) => {
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive));
}
('W', Some(VerbCmd(_, Verb::Change))) => {
// Same with 'W'
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::End, Word::Big, Direction::Forward),
));
}
_ => { /* Nothing weird, so let's continue */ }
}
match ch {
'g' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
match ch {
'g' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
}
'e' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::End, Word::Normal, Direction::Backward),
));
}
'E' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::End, Word::Big, Direction::Backward),
));
}
'k' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp));
}
'j' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown));
}
'_' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLastWord));
}
'0' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfScreenLine));
}
'^' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::FirstGraphicalOnScreenLine));
}
_ => return self.quit_parse(),
}
}
']' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
match ch {
')' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Forward)));
}
'}' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Forward)));
}
_ => return self.quit_parse(),
}
}
'[' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
match ch {
'(' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Backward)));
}
'{' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Backward)));
}
_ => return self.quit_parse(),
}
}
'%' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToDelimMatch));
}
'v' => {
// We got 'v' after a verb
// Instead of normal operations, we will calculate the span based on how visual
// mode would see it
if self
.flags()
.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK)
{
// We can't have more than one of these
return self.quit_parse();
}
self.pending_flags |= CmdFlags::VISUAL;
break 'motion_parse None;
}
'V' => {
// We got 'V' after a verb
// Instead of normal operations, we will calculate the span based on how visual
// line mode would see it
if self
.flags()
.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK)
{
// We can't have more than one of these
// I know vim can technically do this, but it doesn't really make sense to allow
// it since even in vim only the first one given is used
return self.quit_parse();
}
self.pending_flags |= CmdFlags::VISUAL;
break 'motion_parse None;
}
// TODO: figure out how to include 'Ctrl+V' here, might need a refactor
'G' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer));
}
'f' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
break 'motion_parse Some(MotionCmd(
count,
Motion::CharSearch(Direction::Forward, Dest::On, *ch),
));
}
'F' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
break 'motion_parse Some(MotionCmd(
count,
Motion::CharSearch(Direction::Backward, Dest::On, *ch),
));
}
't' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
break 'motion_parse Some(MotionCmd(
count,
Motion::CharSearch(Direction::Forward, Dest::Before, *ch),
));
}
'T' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
break 'motion_parse Some(MotionCmd(
count,
Motion::CharSearch(Direction::Backward, Dest::Before, *ch),
));
}
';' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion));
}
',' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev));
}
'|' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToColumn));
}
'^' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfFirstWord));
}
'0' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine));
}
'$' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine));
}
'k' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::LineUp));
}
'j' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::LineDown));
}
'h' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar));
}
'l' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar));
}
'w' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::Start, Word::Normal, Direction::Forward),
));
}
'W' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::Start, Word::Big, Direction::Forward),
));
}
'e' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::End, Word::Normal, Direction::Forward),
));
}
'E' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::End, Word::Big, Direction::Forward),
));
}
'b' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
));
}
'B' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::Start, Word::Big, Direction::Backward),
));
}
')' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::TextObj(TextObj::Sentence(Direction::Forward)),
));
}
'(' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::TextObj(TextObj::Sentence(Direction::Backward)),
));
}
'}' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::TextObj(TextObj::Paragraph(Direction::Forward)),
));
}
'{' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::TextObj(TextObj::Paragraph(Direction::Backward)),
));
}
ch if ch == 'i' || ch == 'a' => {
let bound = match ch {
'i' => Bound::Inside,
'a' => Bound::Around,
_ => unreachable!(),
};
if chars_clone.peek().is_none() {
break 'motion_parse None;
}
let obj = match chars_clone.next().unwrap() {
'w' => TextObj::Word(Word::Normal, bound),
'W' => TextObj::Word(Word::Big, bound),
's' => TextObj::WholeSentence(bound),
'p' => TextObj::WholeParagraph(bound),
'"' => TextObj::DoubleQuote(bound),
'\'' => TextObj::SingleQuote(bound),
'`' => TextObj::BacktickQuote(bound),
'(' | ')' | 'b' => TextObj::Paren(bound),
'{' | '}' | 'B' => TextObj::Brace(bound),
'[' | ']' => TextObj::Bracket(bound),
'<' | '>' => TextObj::Angle(bound),
_ => return self.quit_parse(),
};
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj)));
}
_ => return self.quit_parse(),
}
};
let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later
let verb_ref = verb.as_ref().map(|v| &v.1);
let motion_ref = motion.as_ref().map(|m| &m.1);
match self.validate_combination(verb_ref, motion_ref) {
CmdState::Complete => Some(ViCmd {
register,
verb,
motion,
raw_seq: std::mem::take(&mut self.pending_seq),
flags: self.flags(),
}),
CmdState::Pending => None,
CmdState::Invalid => {
self.pending_seq.clear();
None
}
}
}
}
impl ViMode for ViNormal {
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
let mut cmd: Option<ViCmd> = match key {
E(K::Char('V'), M::NONE) => Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::VisualModeLine)),
motion: None,
raw_seq: "".into(),
flags: self.flags(),
}),
E(K::Char('A'), M::CTRL) => {
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::IncrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: self.flags(),
})
},
E(K::Char('X'), M::CTRL) => {
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::DecrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: self.flags(),
})
},
E(K::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => Some(ViCmd {
register: Default::default(),
verb: None,
motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: "".into(),
flags: self.flags(),
}),
E(K::Char('R'), M::CTRL) => {
let mut chars = self.pending_seq.chars().peekable();
let count = self.parse_count(&mut chars).unwrap_or(1);
Some(ViCmd {
register: RegisterName::default(),
verb: Some(VerbCmd(count, Verb::Redo)),
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
}
E(K::Esc, M::NONE) => {
self.clear_cmd();
None
}
_ => {
if let Some(cmd) = common_cmds(key) {
self.clear_cmd();
Some(cmd)
} else {
None
}
}
};
if let Some(cmd) = cmd.as_mut() {
cmd.normalize_counts();
};
cmd
}
fn is_repeatable(&self) -> bool {
false
}
fn as_replay(&self) -> Option<CmdReplay> {
None
}
fn cursor_style(&self) -> String {
"\x1b[2 q".to_string()
}
fn pending_seq(&self) -> Option<String> {
Some(self.pending_seq.clone())
}
fn move_cursor_on_undo(&self) -> bool {
false
}
fn clamp_cursor(&self) -> bool {
true
}
fn hist_scroll_start_pos(&self) -> Option<To> {
None
}
fn report_mode(&self) -> ModeReport {
ModeReport::Normal
}
}

View File

@@ -0,0 +1,107 @@
use super::{common_cmds, CmdReplay, ModeReport, ViMode};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{
Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word,
};
#[derive(Default, Debug)]
pub struct ViReplace {
cmds: Vec<ViCmd>,
pending_cmd: ViCmd,
repeat_count: u16,
}
impl ViReplace {
pub fn new() -> Self {
Self::default()
}
pub fn with_count(mut self, repeat_count: u16) -> Self {
self.repeat_count = repeat_count;
self
}
pub fn register_and_return(&mut self) -> Option<ViCmd> {
let mut cmd = self.take_cmd();
cmd.normalize_counts();
self.register_cmd(&cmd);
Some(cmd)
}
pub fn register_cmd(&mut self, cmd: &ViCmd) {
self.cmds.push(cmd.clone())
}
pub fn take_cmd(&mut self) -> ViCmd {
std::mem::take(&mut self.pending_cmd)
}
}
impl ViMode for ViReplace {
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
match key {
E(K::Char(ch), M::NONE) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::ReplaceChar(ch)));
self
.pending_cmd
.set_motion(MotionCmd(1, Motion::ForwardChar));
self.register_and_return()
}
E(K::Char('W'), M::CTRL) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
self.pending_cmd.set_motion(MotionCmd(
1,
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
));
self.register_and_return()
}
E(K::Char('H'), M::CTRL) | E(K::Backspace, M::NONE) => {
self
.pending_cmd
.set_motion(MotionCmd(1, Motion::BackwardChar));
self.register_and_return()
}
E(K::BackTab, M::NONE) => {
self
.pending_cmd
.set_verb(VerbCmd(1, Verb::CompleteBackward));
self.register_and_return()
}
E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Complete));
self.register_and_return()
}
E(K::Esc, M::NONE) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::NormalMode));
self
.pending_cmd
.set_motion(MotionCmd(1, Motion::BackwardChar));
self.register_and_return()
}
_ => common_cmds(key),
}
}
fn is_repeatable(&self) -> bool {
true
}
fn cursor_style(&self) -> String {
"\x1b[4 q".to_string()
}
fn pending_seq(&self) -> Option<String> {
None
}
fn as_replay(&self) -> Option<CmdReplay> {
Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count))
}
fn move_cursor_on_undo(&self) -> bool {
true
}
fn clamp_cursor(&self) -> bool {
true
}
fn hist_scroll_start_pos(&self) -> Option<To> {
Some(To::End)
}
fn report_mode(&self) -> ModeReport {
ModeReport::Replace
}
}

View File

@@ -0,0 +1,695 @@
use std::iter::Peekable;
use std::str::Chars;
use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
VerbCmd, ViCmd, Word,
};
#[derive(Default, Debug)]
pub struct ViVisual {
pending_seq: String,
}
impl ViVisual {
pub fn new() -> Self {
Self::default()
}
pub fn clear_cmd(&mut self) {
self.pending_seq = String::new();
}
pub fn take_cmd(&mut self) -> String {
std::mem::take(&mut self.pending_seq)
}
#[allow(clippy::unnecessary_unwrap)]
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
if verb.is_none() {
match motion {
Some(_) => return CmdState::Complete,
None => return CmdState::Pending,
}
}
if motion.is_none() && verb.is_some() {
match verb.unwrap() {
Verb::Put(_) => CmdState::Complete,
_ => CmdState::Pending,
}
} else {
CmdState::Complete
}
}
pub fn parse_count(&self, chars: &mut Peekable<Chars<'_>>) -> Option<usize> {
let mut count = String::new();
let Some(_digit @ '1'..='9') = chars.peek() else {
return None;
};
count.push(chars.next().unwrap());
while let Some(_digit @ '0'..='9') = chars.peek() {
count.push(chars.next().unwrap());
}
if !count.is_empty() {
count.parse::<usize>().ok()
} else {
None
}
}
/// End the parse and clear the pending sequence
pub fn quit_parse(&mut self) -> Option<ViCmd> {
self.clear_cmd();
None
}
pub fn try_parse(&mut self, ch: char) -> Option<ViCmd> {
self.pending_seq.push(ch);
let mut chars = self.pending_seq.chars().peekable();
let register = 'reg_parse: {
let mut chars_clone = chars.clone();
let count = self.parse_count(&mut chars_clone);
let Some('"') = chars_clone.next() else {
break 'reg_parse RegisterName::default();
};
let Some(reg_name) = chars_clone.next() else {
return None; // Pending register name
};
match reg_name {
'a'..='z' | 'A'..='Z' => { /* proceed */ }
_ => return self.quit_parse(),
}
chars = chars_clone;
RegisterName::new(Some(reg_name), count)
};
let verb = 'verb_parse: {
let mut chars_clone = chars.clone();
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
let Some(ch) = chars_clone.next() else {
break 'verb_parse None;
};
match ch {
'g' => {
if let Some(ch) = chars_clone.peek() {
match ch {
'v' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'?' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::Rot13)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
_ => break 'verb_parse None,
}
} else {
break 'verb_parse None;
}
}
'.' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::RepeatLast)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'x' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
}
'X' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'Y' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::Yank)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'D' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::Delete)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'R' | 'C' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::Change)),
motion: Some(MotionCmd(1, Motion::WholeLineExclusive)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'>' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::Indent)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'<' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::Dedent)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'=' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::Equalize)),
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'p' | 'P' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before)));
}
'r' => {
let ch = chars_clone.next()?;
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'~' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(1, Verb::ToggleCaseRange)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'u' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::ToLower)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'U' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::ToUpper)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'O' | 'o' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::SwapVisualAnchor)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'A' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'I' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::InsertMode)),
motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'J' => {
return Some(ViCmd {
register,
verb: Some(VerbCmd(count, Verb::JoinLines)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
'y' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Yank));
}
'd' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
}
'c' => {
chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Change));
}
_ => break 'verb_parse None,
}
};
if let Some(verb) = verb {
return Some(ViCmd {
register,
verb: Some(verb),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
});
}
let motion = 'motion_parse: {
let mut chars_clone = chars.clone();
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
let Some(ch) = chars_clone.next() else {
break 'motion_parse None;
};
match (ch, &verb) {
('d', Some(VerbCmd(_, Verb::Delete)))
| ('y', Some(VerbCmd(_, Verb::Yank)))
| ('=', Some(VerbCmd(_, Verb::Equalize)))
| ('>', Some(VerbCmd(_, Verb::Indent)))
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive));
}
('c', Some(VerbCmd(_, Verb::Change))) => {
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive));
}
_ => {}
}
match ch {
'g' => {
if let Some(ch) = chars_clone.peek() {
match ch {
'g' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
}
'e' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::End, Word::Normal, Direction::Backward),
));
}
'E' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::End, Word::Big, Direction::Backward),
));
}
'k' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp));
}
'j' => {
chars_clone.next();
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown));
}
_ => return self.quit_parse(),
}
} else {
break 'motion_parse None;
}
}
']' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
match ch {
')' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Forward)));
}
'}' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Forward)));
}
_ => return self.quit_parse(),
}
}
'[' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
match ch {
'(' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Backward)));
}
'{' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Backward)));
}
_ => return self.quit_parse(),
}
}
'%' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToDelimMatch));
}
'f' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
break 'motion_parse Some(MotionCmd(
count,
Motion::CharSearch(Direction::Forward, Dest::On, *ch),
));
}
'F' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
break 'motion_parse Some(MotionCmd(
count,
Motion::CharSearch(Direction::Backward, Dest::On, *ch),
));
}
't' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
break 'motion_parse Some(MotionCmd(
count,
Motion::CharSearch(Direction::Forward, Dest::Before, *ch),
));
}
'T' => {
let Some(ch) = chars_clone.peek() else {
break 'motion_parse None;
};
break 'motion_parse Some(MotionCmd(
count,
Motion::CharSearch(Direction::Backward, Dest::Before, *ch),
));
}
';' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion));
}
',' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev));
}
'|' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ToColumn));
}
'0' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine));
}
'$' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine));
}
'k' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::LineUp));
}
'j' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::LineDown));
}
'h' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar));
}
'l' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar));
}
'w' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::Start, Word::Normal, Direction::Forward),
));
}
'W' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::Start, Word::Big, Direction::Forward),
));
}
'e' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::End, Word::Normal, Direction::Forward),
));
}
'E' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::End, Word::Big, Direction::Forward),
));
}
'b' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
));
}
'B' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::WordMotion(To::Start, Word::Big, Direction::Backward),
));
}
')' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::TextObj(TextObj::Sentence(Direction::Forward)),
));
}
'(' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::TextObj(TextObj::Sentence(Direction::Backward)),
));
}
'}' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::TextObj(TextObj::Paragraph(Direction::Forward)),
));
}
'{' => {
chars = chars_clone;
break 'motion_parse Some(MotionCmd(
count,
Motion::TextObj(TextObj::Paragraph(Direction::Backward)),
));
}
ch if ch == 'i' || ch == 'a' => {
let bound = match ch {
'i' => Bound::Inside,
'a' => Bound::Around,
_ => unreachable!(),
};
if chars_clone.peek().is_none() {
break 'motion_parse None;
}
let obj = match chars_clone.next().unwrap() {
'w' => TextObj::Word(Word::Normal, bound),
'W' => TextObj::Word(Word::Big, bound),
's' => TextObj::WholeSentence(bound),
'p' => TextObj::WholeParagraph(bound),
'"' => TextObj::DoubleQuote(bound),
'\'' => TextObj::SingleQuote(bound),
'`' => TextObj::BacktickQuote(bound),
'(' | ')' | 'b' => TextObj::Paren(bound),
'{' | '}' | 'B' => TextObj::Brace(bound),
'[' | ']' => TextObj::Bracket(bound),
'<' | '>' => TextObj::Angle(bound),
_ => return self.quit_parse(),
};
chars = chars_clone;
break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj)));
}
_ => return self.quit_parse(),
}
};
let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later
let verb_ref = verb.as_ref().map(|v| &v.1);
let motion_ref = motion.as_ref().map(|m| &m.1);
match self.validate_combination(verb_ref, motion_ref) {
CmdState::Complete => Some(ViCmd {
register,
verb,
motion,
raw_seq: std::mem::take(&mut self.pending_seq),
flags: CmdFlags::empty(),
}),
CmdState::Pending => None,
CmdState::Invalid => {
self.pending_seq.clear();
None
}
}
}
}
impl ViMode for ViVisual {
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
let mut cmd: Option<ViCmd> = match key {
E(K::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => Some(ViCmd {
register: Default::default(),
verb: None,
motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: "".into(),
flags: CmdFlags::empty(),
}),
E(K::Char('A'), M::CTRL) => {
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::IncrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: CmdFlags::empty(),
})
},
E(K::Char('X'), M::CTRL) => {
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::DecrementNumber(count))),
motion: None,
raw_seq: "".into(),
flags: CmdFlags::empty(),
})
}
E(K::Char('R'), M::CTRL) => {
let mut chars = self.pending_seq.chars().peekable();
let count = self.parse_count(&mut chars).unwrap_or(1);
Some(ViCmd {
register: RegisterName::default(),
verb: Some(VerbCmd(count, Verb::Redo)),
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
}
E(K::Esc, M::NONE) => Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::NormalMode)),
motion: Some(MotionCmd(1, Motion::Null)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
}),
_ => {
if let Some(cmd) = common_cmds(key) {
self.clear_cmd();
Some(cmd)
} else {
None
}
}
};
if let Some(cmd) = cmd.as_mut() {
cmd.normalize_counts();
};
cmd
}
fn is_repeatable(&self) -> bool {
true
}
fn as_replay(&self) -> Option<CmdReplay> {
None
}
fn cursor_style(&self) -> String {
"\x1b[2 q".to_string()
}
fn pending_seq(&self) -> Option<String> {
Some(self.pending_seq.clone())
}
fn move_cursor_on_undo(&self) -> bool {
true
}
fn clamp_cursor(&self) -> bool {
true
}
fn hist_scroll_start_pos(&self) -> Option<To> {
None
}
fn report_mode(&self) -> ModeReport {
ModeReport::Visual
}
}

View File

@@ -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(),
}
}
}

View File

@@ -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<String, ShFunc>,
aliases: HashMap<String, ShAlias>,
traps: HashMap<TrapTarget, String>,
keymaps: Vec<KeyMap>
}
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<KeyMap> {
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);
}