Added ex mode to line editor, a 'keymap' builtin, and a zsh-like widget system using ':!<shellcmd>' ex mode commands
This commit is contained in:
18
Cargo.lock
generated
18
Cargo.lock
generated
@@ -185,6 +185,12 @@ version = "0.1.13"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encode_unicode"
|
name = "encode_unicode"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -331,6 +337,15 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.17"
|
||||||
@@ -589,7 +604,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shed"
|
name = "shed"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ariadne",
|
"ariadne",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
@@ -597,6 +612,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"glob",
|
"glob",
|
||||||
"insta",
|
"insta",
|
||||||
|
"itertools",
|
||||||
"log",
|
"log",
|
||||||
"nix",
|
"nix",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "shed"
|
name = "shed"
|
||||||
description = "A linux shell written in rust"
|
description = "A linux shell written in rust"
|
||||||
publish = false
|
publish = false
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
|
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ bitflags = "2.8.0"
|
|||||||
clap = { version = "4.5.38", features = ["derive"] }
|
clap = { version = "4.5.38", features = ["derive"] }
|
||||||
env_logger = "0.11.9"
|
env_logger = "0.11.9"
|
||||||
glob = "0.3.2"
|
glob = "0.3.2"
|
||||||
|
itertools = "0.14.0"
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] }
|
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] }
|
||||||
rand = "0.10.0"
|
rand = "0.10.0"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
{
|
{
|
||||||
packages.default = pkgs.rustPlatform.buildRustPackage {
|
packages.default = pkgs.rustPlatform.buildRustPackage {
|
||||||
pname = "shed";
|
pname = "shed";
|
||||||
version = "0.4.0";
|
version = "0.5.0";
|
||||||
|
|
||||||
src = self;
|
src = self;
|
||||||
|
|
||||||
|
|||||||
149
src/builtin/keymap.rs
Normal file
149
src/builtin/keymap.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
use crate::{
|
||||||
|
expand::expand_keymap, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, readline::{keys::KeyEvent, vimode::ModeReport}, state::{self, write_logic}
|
||||||
|
};
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct KeyMapFlags: u32 {
|
||||||
|
const NORMAL = 0b0000001;
|
||||||
|
const INSERT = 0b0000010;
|
||||||
|
const VISUAL = 0b0000100;
|
||||||
|
const EX = 0b0001000;
|
||||||
|
const OP_PENDING = 0b0010000;
|
||||||
|
const REPLACE = 0b0100000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct KeyMapOpts {
|
||||||
|
remove: Option<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 {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
@@ -25,13 +25,14 @@ pub mod map;
|
|||||||
pub mod arrops;
|
pub mod arrops;
|
||||||
pub mod intro;
|
pub mod intro;
|
||||||
pub mod getopts;
|
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",
|
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown",
|
||||||
"alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin",
|
"alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin",
|
||||||
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
|
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
|
||||||
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
|
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
|
||||||
"getopts"
|
"getopts", "keymap"
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn true_builtin() -> ShResult<()> {
|
pub fn true_builtin() -> ShResult<()> {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::{HashSet, VecDeque};
|
||||||
use std::iter::Peekable;
|
use std::iter::Peekable;
|
||||||
use std::str::{Chars, FromStr};
|
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::lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule, is_hard_sep};
|
||||||
use crate::parse::{Redir, RedirType};
|
use crate::parse::{Redir, RedirType};
|
||||||
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
||||||
|
use crate::readline::keys::{KeyCode, KeyEvent, ModKeys};
|
||||||
use crate::readline::markers;
|
use crate::readline::markers;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_vars, write_jobs, write_meta,
|
ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_shopts, read_vars, write_jobs, write_meta, write_vars
|
||||||
write_vars,
|
|
||||||
};
|
};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
@@ -2143,3 +2143,81 @@ pub fn expand_aliases(
|
|||||||
expand_aliases(result, already_expanded, log_tab)
|
expand_aliases(result, already_expanded, log_tab)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn expand_keymap(s: &str) -> Vec<KeyEvent> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'>' => {
|
||||||
|
if alias.eq_ignore_ascii_case("leader") {
|
||||||
|
let leader = read_shopts(|o| o.prompt.leader.clone());
|
||||||
|
keys.extend(expand_keymap(&leader));
|
||||||
|
} else if let Some(key) = parse_key_alias(&alias) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => alias.push(a_ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
keys.push(KeyEvent(KeyCode::Char(ch), ModKeys::NONE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_key_alias(alias: &str) -> Option<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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -180,10 +180,15 @@ impl ShErr {
|
|||||||
Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()], io_guards: vec![] }
|
Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()], io_guards: vec![] }
|
||||||
}
|
}
|
||||||
pub fn promote(mut self, span: Span) -> Self {
|
pub fn promote(mut self, span: Span) -> Self {
|
||||||
if let Some(note) = self.notes.pop() {
|
if self.notes.is_empty() {
|
||||||
self = self.labeled(span, note)
|
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 {
|
pub fn with_redirs(mut self, guard: RedirGuard) -> Self {
|
||||||
self.io_guards.push(guard);
|
self.io_guards.push(guard);
|
||||||
@@ -340,6 +345,7 @@ pub enum ShErrKind {
|
|||||||
Errno(Errno),
|
Errno(Errno),
|
||||||
NotFound,
|
NotFound,
|
||||||
ReadlineErr,
|
ReadlineErr,
|
||||||
|
ExCommand,
|
||||||
|
|
||||||
// Not really errors, more like internal signals
|
// Not really errors, more like internal signals
|
||||||
CleanExit(i32),
|
CleanExit(i32),
|
||||||
@@ -369,6 +375,7 @@ impl Display for ShErrKind {
|
|||||||
Self::LoopContinue(_) => "Syntax Error",
|
Self::LoopContinue(_) => "Syntax Error",
|
||||||
Self::LoopBreak(_) => "Syntax Error",
|
Self::LoopBreak(_) => "Syntax Error",
|
||||||
Self::ReadlineErr => "Readline Error",
|
Self::ReadlineErr => "Readline Error",
|
||||||
|
Self::ExCommand => "Ex Command Error",
|
||||||
Self::ClearReadline => "",
|
Self::ClearReadline => "",
|
||||||
Self::Null => "",
|
Self::Null => "",
|
||||||
};
|
};
|
||||||
|
|||||||
99
src/main.rs
99
src/main.rs
@@ -27,6 +27,7 @@ use nix::errno::Errno;
|
|||||||
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
|
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
|
||||||
use nix::unistd::read;
|
use nix::unistd::read;
|
||||||
|
|
||||||
|
use crate::builtin::keymap::KeyMapMatch;
|
||||||
use crate::builtin::trap::TrapTarget;
|
use crate::builtin::trap::TrapTarget;
|
||||||
use crate::libsh::error::{self, ShErr, ShErrKind, ShResult};
|
use crate::libsh::error::{self, ShErr, ShErrKind, ShResult};
|
||||||
use crate::libsh::sys::TTY_FILENO;
|
use crate::libsh::sys::TTY_FILENO;
|
||||||
@@ -193,8 +194,8 @@ fn shed_interactive() -> ShResult<()> {
|
|||||||
if let Err(e) = check_signals() {
|
if let Err(e) = check_signals() {
|
||||||
match e.kind() {
|
match e.kind() {
|
||||||
ShErrKind::ClearReadline => {
|
ShErrKind::ClearReadline => {
|
||||||
// Ctrl+C - clear current input and show new prompt
|
// Ctrl+C - clear current input and redraw
|
||||||
readline.reset(false)?;
|
readline.reset_active_widget(false)?;
|
||||||
}
|
}
|
||||||
ShErrKind::CleanExit(code) => {
|
ShErrKind::CleanExit(code) => {
|
||||||
QUIT_CODE.store(*code, Ordering::SeqCst);
|
QUIT_CODE.store(*code, Ordering::SeqCst);
|
||||||
@@ -207,8 +208,11 @@ fn shed_interactive() -> ShResult<()> {
|
|||||||
|
|
||||||
if GOT_SIGWINCH.swap(false, Ordering::SeqCst) {
|
if GOT_SIGWINCH.swap(false, Ordering::SeqCst) {
|
||||||
log::info!("Window size change detected, updating readline dimensions");
|
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.writer.update_t_cols();
|
||||||
readline.prompt_mut().refresh()?;
|
readline.prompt_mut().refresh()?;
|
||||||
|
readline.mark_dirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
if JOB_DONE.swap(false, Ordering::SeqCst) {
|
if JOB_DONE.swap(false, Ordering::SeqCst) {
|
||||||
@@ -224,7 +228,13 @@ fn shed_interactive() -> ShResult<()> {
|
|||||||
PollFlags::POLLIN,
|
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(_) => {}
|
Ok(_) => {}
|
||||||
Err(Errno::EINTR) => {
|
Err(Errno::EINTR) => {
|
||||||
// Interrupted by signal, loop back to handle it
|
// 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
|
// Check if stdin has data
|
||||||
if fds[0]
|
if fds[0]
|
||||||
.revents()
|
.revents()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use ariadne::Fmt;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::{
|
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},
|
expand::{expand_aliases, glob_to_regex},
|
||||||
jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
|
jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
|
||||||
@@ -823,6 +823,7 @@ impl Dispatcher {
|
|||||||
"wait" => jobctl::wait(cmd),
|
"wait" => jobctl::wait(cmd),
|
||||||
"type" => intro::type_builtin(cmd),
|
"type" => intro::type_builtin(cmd),
|
||||||
"getopts" => getopts(cmd),
|
"getopts" => getopts(cmd),
|
||||||
|
"keymap" => keymap::keymap(cmd),
|
||||||
"true" | ":" => {
|
"true" | ":" => {
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -520,12 +520,14 @@ pub enum CompResponse {
|
|||||||
pub trait Completer {
|
pub trait Completer {
|
||||||
fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>>;
|
fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>>;
|
||||||
fn reset(&mut self);
|
fn reset(&mut self);
|
||||||
|
fn reset_stay_active(&mut self);
|
||||||
fn is_active(&self) -> bool;
|
fn is_active(&self) -> bool;
|
||||||
fn selected_candidate(&self) -> Option<String>;
|
fn selected_candidate(&self) -> Option<String>;
|
||||||
fn token_span(&self) -> (usize, usize);
|
fn token_span(&self) -> (usize, usize);
|
||||||
fn original_input(&self) -> &str;
|
fn original_input(&self) -> &str;
|
||||||
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>;
|
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>;
|
||||||
fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> { Ok(()) }
|
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 handle_key(&mut self, key: K) -> ShResult<CompResponse>;
|
||||||
fn get_completed_line(&self, candidate: &str) -> String;
|
fn get_completed_line(&self, candidate: &str) -> String;
|
||||||
}
|
}
|
||||||
@@ -610,7 +612,14 @@ impl From<String> for ScoredCandidate {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FuzzyLayout {
|
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)]
|
#[derive(Default, Debug, Clone)]
|
||||||
@@ -671,7 +680,11 @@ pub struct FuzzyCompleter {
|
|||||||
old_layout: Option<FuzzyLayout>,
|
old_layout: Option<FuzzyLayout>,
|
||||||
max_height: usize,
|
max_height: usize,
|
||||||
scroll_offset: 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 {
|
impl FuzzyCompleter {
|
||||||
@@ -740,11 +753,23 @@ impl Default for FuzzyCompleter {
|
|||||||
old_layout: None,
|
old_layout: None,
|
||||||
scroll_offset: 0,
|
scroll_offset: 0,
|
||||||
active: false,
|
active: false,
|
||||||
|
prompt_line_width: 0,
|
||||||
|
prompt_cursor_col: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for FuzzyCompleter {
|
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 {
|
fn get_completed_line(&self, _candidate: &str) -> String {
|
||||||
log::debug!("Getting completed line for candidate: {}", _candidate);
|
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> {
|
fn handle_key(&mut self, key: K) -> ShResult<CompResponse> {
|
||||||
match key {
|
match key {
|
||||||
|
K(C::Char('D'), M::CTRL) |
|
||||||
K(C::Esc, M::NONE) => {
|
K(C::Esc, M::NONE) => {
|
||||||
self.active = false;
|
self.active = false;
|
||||||
self.filtered.clear();
|
self.filtered.clear();
|
||||||
@@ -816,18 +842,48 @@ impl Completer for FuzzyCompleter {
|
|||||||
}
|
}
|
||||||
fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
fn clear(&mut self, writer: &mut TermWriter) -> ShResult<()> {
|
||||||
if let Some(layout) = self.old_layout.take() {
|
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();
|
let mut buf = String::new();
|
||||||
// Cursor is on the prompt line. Move down to the bottom border.
|
if lines_below > 0 {
|
||||||
let lines_below_prompt = layout.rows.saturating_sub(2);
|
write!(buf, "\x1b[{}B", lines_below).unwrap();
|
||||||
if lines_below_prompt > 0 {
|
|
||||||
write!(buf, "\x1b[{}B", lines_below_prompt).unwrap();
|
|
||||||
}
|
}
|
||||||
// Erase each line moving up, back to the top border
|
for _ in 0..physical_rows {
|
||||||
for _ in 0..layout.rows {
|
|
||||||
buf.push_str("\x1b[2K\x1b[A");
|
buf.push_str("\x1b[2K\x1b[A");
|
||||||
}
|
}
|
||||||
// Erase the top border line
|
|
||||||
buf.push_str("\x1b[2K");
|
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)?;
|
writer.flush_write(&buf)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -847,10 +903,11 @@ impl Completer for FuzzyCompleter {
|
|||||||
let num_filtered = format!("\x1b[33m{}\x1b[0m",self.filtered.len());
|
let num_filtered = format!("\x1b[33m{}\x1b[0m",self.filtered.len());
|
||||||
let num_candidates = format!("\x1b[33m{}\x1b[0m",self.candidates.len());
|
let num_candidates = format!("\x1b[33m{}\x1b[0m",self.candidates.len());
|
||||||
let visible = self.get_window();
|
let visible = self.get_window();
|
||||||
let mut rows = 0;
|
let mut rows: u16 = 0;
|
||||||
let top_bar = format!("\n{}{}{}",
|
let top_bar = format!("\n{}{} \x1b[1mComplete\x1b[0m {}{}",
|
||||||
Self::TOP_LEFT,
|
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
|
Self::TOP_RIGHT
|
||||||
);
|
);
|
||||||
buf.push_str(&top_bar);
|
buf.push_str(&top_bar);
|
||||||
@@ -910,15 +967,19 @@ impl Completer for FuzzyCompleter {
|
|||||||
buf.push_str(&bot_bar);
|
buf.push_str(&bot_bar);
|
||||||
rows += 1;
|
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)
|
// 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_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();
|
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)?;
|
writer.flush_write(&buf)?;
|
||||||
self.old_layout = Some(new_layout);
|
self.old_layout = Some(new_layout);
|
||||||
|
|
||||||
@@ -953,6 +1014,11 @@ pub struct SimpleCompleter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Completer for 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 {
|
fn get_completed_line(&self, _candidate: &str) -> String {
|
||||||
self.get_completed_line()
|
self.get_completed_line()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
collections::HashSet, fmt::Display, ops::{Range, RangeInclusive}
|
||||||
ops::{Range, RangeInclusive},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
@@ -11,14 +10,15 @@ use super::vicmd::{
|
|||||||
ViCmd, Word,
|
ViCmd, Word,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::ShResult,
|
libsh::{error::ShResult, guards::var_ctx_guard},
|
||||||
parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule},
|
parse::{execute::exec_input, lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
readline::{
|
readline::{
|
||||||
markers,
|
markers,
|
||||||
register::{RegisterContent, write_register},
|
register::{RegisterContent, write_register},
|
||||||
|
term::RawModeGuard,
|
||||||
},
|
},
|
||||||
state::read_shopts,
|
state::{VarFlags, VarKind, read_shopts, read_vars, write_vars},
|
||||||
};
|
};
|
||||||
|
|
||||||
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
|
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
|
||||||
@@ -2336,7 +2336,13 @@ impl LineBuf {
|
|||||||
MotionKind::Exclusive((0, self.grapheme_indices().len()))
|
MotionKind::Exclusive((0, self.grapheme_indices().len()))
|
||||||
}
|
}
|
||||||
MotionCmd(_count, Motion::BeginningOfBuffer) => MotionKind::On(0),
|
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::ToColumn) => todo!(),
|
||||||
MotionCmd(count, Motion::Range(start, end)) => {
|
MotionCmd(count, Motion::Range(start, end)) => {
|
||||||
let mut final_end = end;
|
let mut final_end = end;
|
||||||
@@ -2355,7 +2361,9 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
MotionCmd(_count, Motion::RepeatMotion) => todo!(),
|
MotionCmd(_count, Motion::RepeatMotion) => todo!(),
|
||||||
MotionCmd(_count, Motion::RepeatMotionRev) => 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);
|
self.set_buffer(buffer);
|
||||||
@@ -2528,16 +2536,9 @@ impl LineBuf {
|
|||||||
) -> ShResult<()> {
|
) -> ShResult<()> {
|
||||||
match verb {
|
match verb {
|
||||||
Verb::Delete | Verb::Yank | Verb::Change => {
|
Verb::Delete | Verb::Yank | Verb::Change => {
|
||||||
log::debug!("Executing verb: {verb:?} with motion: {motion:?}");
|
|
||||||
let Some((start, end)) = self.range_from_motion(&motion) else {
|
let Some((start, end)) = self.range_from_motion(&motion) else {
|
||||||
log::debug!("No range from motion, nothing to do");
|
|
||||||
return Ok(());
|
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;
|
let mut do_indent = false;
|
||||||
if verb == Verb::Change && (start, end) == self.this_line_exclusive() {
|
if verb == Verb::Change && (start, end) == self.this_line_exclusive() {
|
||||||
@@ -3014,8 +3015,16 @@ impl LineBuf {
|
|||||||
Verb::IncrementNumber(n) |
|
Verb::IncrementNumber(n) |
|
||||||
Verb::DecrementNumber(n) => {
|
Verb::DecrementNumber(n) => {
|
||||||
let inc = if matches!(verb, Verb::IncrementNumber(_)) { n as i64 } else { -(n as i64) };
|
let inc = if matches!(verb, Verb::IncrementNumber(_)) { n as i64 } else { -(n as i64) };
|
||||||
let (s, e) = self.this_word(Word::Normal);
|
let (s, e) = self.select_range().unwrap_or(self.this_word(Word::Normal));
|
||||||
let end = (e + 1).min(self.grapheme_indices().len()); // inclusive → exclusive, capped at buffer len
|
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 word = self.slice(s..end).unwrap_or_default().to_lowercase();
|
||||||
|
|
||||||
let byte_start = self.index_byte_pos(s);
|
let byte_start = self.index_byte_pos(s);
|
||||||
@@ -3062,6 +3071,7 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Verb::Complete
|
Verb::Complete
|
||||||
|
| Verb::ExMode
|
||||||
| Verb::EndOfFile
|
| Verb::EndOfFile
|
||||||
| Verb::InsertMode
|
| Verb::InsertMode
|
||||||
| Verb::NormalMode
|
| Verb::NormalMode
|
||||||
@@ -3071,6 +3081,38 @@ impl LineBuf {
|
|||||||
| Verb::VisualModeBlock
|
| Verb::VisualModeBlock
|
||||||
| Verb::CompleteBackward
|
| Verb::CompleteBackward
|
||||||
| Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
|
| 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::fmt::Write;
|
||||||
use history::History;
|
use history::History;
|
||||||
use keys::{KeyCode, KeyEvent, ModKeys};
|
use keys::{KeyCode, KeyEvent, ModKeys};
|
||||||
use linebuf::{LineBuf, SelectAnchor, SelectMode};
|
use linebuf::{LineBuf, SelectAnchor, SelectMode};
|
||||||
@@ -6,13 +7,15 @@ use unicode_width::UnicodeWidthStr;
|
|||||||
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
|
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
|
||||||
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
||||||
|
|
||||||
|
use crate::builtin::keymap::{KeyMapFlags, KeyMapMatch};
|
||||||
use crate::expand::expand_prompt;
|
use crate::expand::expand_prompt;
|
||||||
use crate::libsh::sys::TTY_FILENO;
|
use crate::libsh::sys::TTY_FILENO;
|
||||||
use crate::parse::lex::{LexStream, QuoteState};
|
use crate::parse::lex::{LexStream, QuoteState};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::readline::complete::FuzzyCompleter;
|
use crate::readline::complete::FuzzyCompleter;
|
||||||
use crate::readline::term::{Pos, TermReader, calc_str_width};
|
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::{
|
use crate::{
|
||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
parse::lex::{self, LexFlags, Tk, TkFlags, TkRule},
|
parse::lex::{self, LexFlags, Tk, TkFlags, TkRule},
|
||||||
@@ -210,6 +213,7 @@ pub struct ShedVi {
|
|||||||
pub completer: Box<dyn Completer>,
|
pub completer: Box<dyn Completer>,
|
||||||
|
|
||||||
pub mode: Box<dyn ViMode>,
|
pub mode: Box<dyn ViMode>,
|
||||||
|
pub pending_keymap: Vec<KeyEvent>,
|
||||||
pub repeat_action: Option<CmdReplay>,
|
pub repeat_action: Option<CmdReplay>,
|
||||||
pub repeat_motion: Option<MotionCmd>,
|
pub repeat_motion: Option<MotionCmd>,
|
||||||
pub editor: LineBuf,
|
pub editor: LineBuf,
|
||||||
@@ -229,6 +233,7 @@ impl ShedVi {
|
|||||||
completer: Box::new(FuzzyCompleter::default()),
|
completer: Box::new(FuzzyCompleter::default()),
|
||||||
highlighter: Highlighter::new(),
|
highlighter: Highlighter::new(),
|
||||||
mode: Box::new(ViInsert::new()),
|
mode: Box::new(ViInsert::new()),
|
||||||
|
pending_keymap: Vec::new(),
|
||||||
old_layout: None,
|
old_layout: None,
|
||||||
repeat_action: None,
|
repeat_action: None,
|
||||||
repeat_motion: None,
|
repeat_motion: None,
|
||||||
@@ -263,6 +268,16 @@ impl ShedVi {
|
|||||||
self.writer.fix_cursor_column(&mut TermReader::new(*TTY_FILENO))
|
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
|
/// Reset readline state for a new prompt
|
||||||
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
|
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
|
||||||
// Clear old display before resetting state — old_layout must survive
|
// Clear old display before resetting state — old_layout must survive
|
||||||
@@ -287,6 +302,24 @@ impl ShedVi {
|
|||||||
&mut self.prompt
|
&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> {
|
fn should_submit(&mut self) -> ShResult<bool> {
|
||||||
if self.mode.report_mode() == ModeReport::Normal {
|
if self.mode.report_mode() == ModeReport::Normal {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
@@ -326,6 +359,7 @@ impl ShedVi {
|
|||||||
while let Some(key) = self.reader.read_key()? {
|
while let Some(key) = self.reader.read_key()? {
|
||||||
// If completer is active, delegate input to it
|
// If completer is active, delegate input to it
|
||||||
if self.completer.is_active() {
|
if self.completer.is_active() {
|
||||||
|
self.print_line(false)?;
|
||||||
match self.completer.handle_key(key.clone())? {
|
match self.completer.handle_key(key.clone())? {
|
||||||
CompResponse::Accept(candidate) => {
|
CompResponse::Accept(candidate) => {
|
||||||
let span_start = self.completer.token_span().0;
|
let span_start = self.completer.token_span().0;
|
||||||
@@ -351,6 +385,8 @@ impl ShedVi {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
CompResponse::Dismiss => {
|
CompResponse::Dismiss => {
|
||||||
|
let hint = self.history.get_hint();
|
||||||
|
self.editor.set_hint(hint);
|
||||||
self.completer.clear(&mut self.writer)?;
|
self.completer.clear(&mut self.writer)?;
|
||||||
self.completer.reset();
|
self.completer.reset();
|
||||||
continue;
|
continue;
|
||||||
@@ -362,127 +398,48 @@ impl ShedVi {
|
|||||||
}
|
}
|
||||||
CompResponse::Passthrough => { /* fall through to normal handling below */ }
|
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) {
|
let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap));
|
||||||
self.editor.accept_hint();
|
log::debug!("[keymap] {} matches found", matches.len());
|
||||||
if !self.history.at_pending() {
|
if matches.is_empty() {
|
||||||
self.history.reset_to_pending();
|
// No matches. Drain the buffered keys and execute them.
|
||||||
}
|
log::debug!("[keymap] no matches, flushing {} buffered keys", self.pending_keymap.len());
|
||||||
self
|
for key in std::mem::take(&mut self.pending_keymap) {
|
||||||
.history
|
if let Some(event) = self.handle_key(key)? {
|
||||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
return Ok(event);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
Ok(Some(line)) => {
|
self.needs_redraw = true;
|
||||||
let span_start = self.completer.token_span().0;
|
continue;
|
||||||
let new_cursor = span_start
|
} else if matches.len() == 1 && matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact {
|
||||||
+ self
|
// We have a single exact match. Execute it.
|
||||||
.completer
|
let keymap = matches[0].clone();
|
||||||
.selected_candidate()
|
log::debug!("[keymap] exact match: {:?} -> {:?}", keymap.keys, keymap.action);
|
||||||
.map(|c| c.len())
|
self.pending_keymap.clear();
|
||||||
.unwrap_or_default();
|
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() {
|
if let Some(event) = self.handle_key(key)? {
|
||||||
self.history.reset_to_pending();
|
return Ok(event);
|
||||||
}
|
|
||||||
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 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
|
// Redraw if we processed any input
|
||||||
@@ -494,6 +451,143 @@ impl ShedVi {
|
|||||||
Ok(ReadlineEvent::Pending)
|
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 {
|
pub fn get_layout(&mut self, line: &str) -> Layout {
|
||||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||||
let (cols, _) = get_win_size(*TTY_FILENO);
|
let (cols, _) = get_win_size(*TTY_FILENO);
|
||||||
@@ -578,7 +672,7 @@ impl ShedVi {
|
|||||||
|
|
||||||
pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> {
|
pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> {
|
||||||
let line = self.line_text();
|
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 pending_seq = self.mode.pending_seq();
|
||||||
let mut prompt_string_right = self.prompt.psr_expanded.clone();
|
let mut prompt_string_right = self.prompt.psr_expanded.clone();
|
||||||
|
|
||||||
@@ -590,6 +684,7 @@ impl ShedVi {
|
|||||||
prompt_string_right =
|
prompt_string_right =
|
||||||
prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string());
|
prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string());
|
||||||
}
|
}
|
||||||
|
let mut buf = String::new();
|
||||||
|
|
||||||
let row0_used = self
|
let row0_used = self
|
||||||
.prompt
|
.prompt
|
||||||
@@ -623,7 +718,7 @@ impl ShedVi {
|
|||||||
&& !seq.is_empty()
|
&& !seq.is_empty()
|
||||||
&& !(prompt_string_right.is_some() && one_line)
|
&& !(prompt_string_right.is_some() && one_line)
|
||||||
&& seq_fits
|
&& seq_fits
|
||||||
{
|
&& self.mode.report_mode() != ModeReport::Ex {
|
||||||
let to_col = self.writer.t_cols - calc_str_width(&seq);
|
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
|
let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt
|
||||||
|
|
||||||
@@ -635,13 +730,10 @@ impl ShedVi {
|
|||||||
|
|
||||||
// Save cursor, move up to top row, move right to column, write sequence,
|
// Save cursor, move up to top row, move right to column, write sequence,
|
||||||
// restore cursor
|
// restore cursor
|
||||||
self
|
write!(buf, "\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8").unwrap();
|
||||||
.writer
|
|
||||||
.flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?;
|
|
||||||
} else if !final_draw
|
} else if !final_draw
|
||||||
&& let Some(psr) = prompt_string_right
|
&& let Some(psr) = prompt_string_right
|
||||||
&& psr_fits
|
&& psr_fits {
|
||||||
{
|
|
||||||
let to_col = self.writer.t_cols - calc_str_width(&psr);
|
let to_col = self.writer.t_cols - calc_str_width(&psr);
|
||||||
let down = new_layout.end.row - new_layout.cursor.row;
|
let down = new_layout.end.row - new_layout.cursor.row;
|
||||||
let move_down = if down > 0 {
|
let move_down = if down > 0 {
|
||||||
@@ -650,17 +742,37 @@ impl ShedVi {
|
|||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
self
|
write!(buf, "\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8").unwrap();
|
||||||
.writer
|
|
||||||
.flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?;
|
// 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.completer.draw(&mut self.writer)?;
|
||||||
|
|
||||||
self.old_layout = Some(new_layout);
|
self.old_layout = Some(new_layout);
|
||||||
self.needs_redraw = false;
|
self.needs_redraw = false;
|
||||||
|
// Save physical cursor row so SIGWINCH can restore it
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,39 +781,46 @@ impl ShedVi {
|
|||||||
let mut is_insert_mode = false;
|
let mut is_insert_mode = false;
|
||||||
if cmd.is_mode_transition() {
|
if cmd.is_mode_transition() {
|
||||||
let count = cmd.verb_count();
|
let count = cmd.verb_count();
|
||||||
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
|
let mut mode: Box<dyn ViMode> = if let ModeReport::Ex = self.mode.report_mode() && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
|
||||||
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
|
Box::new(ViNormal::new())
|
||||||
is_insert_mode = true;
|
} else {
|
||||||
Box::new(ViInsert::new().with_count(count as u16))
|
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 => {
|
Verb::ReplaceMode => Box::new(ViReplace::new()),
|
||||||
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::VisualModeSelectLast => {
|
||||||
}
|
if self.mode.report_mode() != ModeReport::Visual {
|
||||||
Verb::VisualMode => {
|
self
|
||||||
select_mode = Some(SelectMode::Char(SelectAnchor::End));
|
.editor
|
||||||
Box::new(ViVisual::new())
|
.start_selecting(SelectMode::Char(SelectAnchor::End));
|
||||||
}
|
}
|
||||||
Verb::VisualModeLine => {
|
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
|
||||||
select_mode = Some(SelectMode::Line(SelectAnchor::End));
|
std::mem::swap(&mut mode, &mut self.mode);
|
||||||
Box::new(ViVisual::new())
|
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);
|
std::mem::swap(&mut mode, &mut self.mode);
|
||||||
|
|
||||||
@@ -818,6 +937,13 @@ impl ShedVi {
|
|||||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||||
std::mem::swap(&mut mode, &mut self.mode);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -698,6 +698,8 @@ pub struct Layout {
|
|||||||
pub prompt_end: Pos,
|
pub prompt_end: Pos,
|
||||||
pub cursor: Pos,
|
pub cursor: Pos,
|
||||||
pub end: Pos,
|
pub end: Pos,
|
||||||
|
pub psr_end: Option<Pos>,
|
||||||
|
pub t_cols: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Layout {
|
impl Layout {
|
||||||
@@ -706,6 +708,8 @@ impl Layout {
|
|||||||
prompt_end: Pos::default(),
|
prompt_end: Pos::default(),
|
||||||
cursor: Pos::default(),
|
cursor: Pos::default(),
|
||||||
end: 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 {
|
pub fn from_parts(term_width: u16, prompt: &str, to_cursor: &str, to_end: &str) -> Self {
|
||||||
@@ -716,6 +720,8 @@ impl Layout {
|
|||||||
prompt_end,
|
prompt_end,
|
||||||
cursor,
|
cursor,
|
||||||
end,
|
end,
|
||||||
|
psr_end: None,
|
||||||
|
t_cols: term_width,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -925,7 +931,14 @@ impl TermWriter {
|
|||||||
impl LineWriter for TermWriter {
|
impl LineWriter for TermWriter {
|
||||||
fn clear_rows(&mut self, layout: &Layout) -> ShResult<()> {
|
fn clear_rows(&mut self, layout: &Layout) -> ShResult<()> {
|
||||||
self.buffer.clear();
|
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_row = layout.cursor.row;
|
||||||
|
|
||||||
let cursor_motion = rows_to_clear.saturating_sub(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.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 end = new_layout.end;
|
||||||
let cursor = new_layout.cursor;
|
let cursor = new_layout.cursor;
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ bitflags! {
|
|||||||
const VISUAL = 1<<0;
|
const VISUAL = 1<<0;
|
||||||
const VISUAL_LINE = 1<<1;
|
const VISUAL_LINE = 1<<1;
|
||||||
const VISUAL_BLOCK = 1<<2;
|
const VISUAL_BLOCK = 1<<2;
|
||||||
|
const EXIT_CUR_MODE = 1<<3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +178,7 @@ impl ViCmd {
|
|||||||
matches!(
|
matches!(
|
||||||
v.1,
|
v.1,
|
||||||
Verb::Change
|
Verb::Change
|
||||||
|
| Verb::ExMode
|
||||||
| Verb::InsertMode
|
| Verb::InsertMode
|
||||||
| Verb::InsertModeLineBreak(_)
|
| Verb::InsertModeLineBreak(_)
|
||||||
| Verb::NormalMode
|
| Verb::NormalMode
|
||||||
@@ -184,7 +186,7 @@ impl ViCmd {
|
|||||||
| Verb::VisualMode
|
| Verb::VisualMode
|
||||||
| Verb::VisualModeLine
|
| Verb::VisualModeLine
|
||||||
| Verb::ReplaceMode
|
| Verb::ReplaceMode
|
||||||
)
|
) || self.flags.contains(CmdFlags::EXIT_CUR_MODE)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,6 +247,15 @@ pub enum Verb {
|
|||||||
Equalize,
|
Equalize,
|
||||||
AcceptLineOrNewline,
|
AcceptLineOrNewline,
|
||||||
EndOfFile,
|
EndOfFile,
|
||||||
|
// Ex-mode verbs
|
||||||
|
ExMode,
|
||||||
|
ShellCmd(String),
|
||||||
|
Normal(String),
|
||||||
|
Read(ReadSrc),
|
||||||
|
Write(WriteDest),
|
||||||
|
Substitute(String, String, super::vimode::ex::SubFlags),
|
||||||
|
RepeatSubstitute,
|
||||||
|
RepeatGlobal,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Verb {
|
impl Verb {
|
||||||
@@ -290,6 +301,8 @@ impl Verb {
|
|||||||
| Self::Insert(_)
|
| Self::Insert(_)
|
||||||
| Self::Rot13
|
| Self::Rot13
|
||||||
| Self::EndOfFile
|
| Self::EndOfFile
|
||||||
|
| Self::IncrementNumber(_)
|
||||||
|
| Self::DecrementNumber(_)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pub fn is_char_insert(&self) -> bool {
|
pub fn is_char_insert(&self) -> bool {
|
||||||
@@ -339,6 +352,9 @@ pub enum Motion {
|
|||||||
RepeatMotion,
|
RepeatMotion,
|
||||||
RepeatMotionRev,
|
RepeatMotionRev,
|
||||||
Null,
|
Null,
|
||||||
|
// Ex-mode motions
|
||||||
|
Global(Val),
|
||||||
|
NotGlobal(Val),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
@@ -467,3 +483,30 @@ pub enum To {
|
|||||||
Start,
|
Start,
|
||||||
End,
|
End,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ex-mode types
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub enum ReadSrc {
|
||||||
|
File(std::path::PathBuf),
|
||||||
|
Cmd(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub enum WriteDest {
|
||||||
|
File(std::path::PathBuf),
|
||||||
|
FileAppend(std::path::PathBuf),
|
||||||
|
Cmd(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub enum Val {
|
||||||
|
Str(String),
|
||||||
|
Regex(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Val {
|
||||||
|
pub fn new_str(s: String) -> Self {
|
||||||
|
Self::Str(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
381
src/readline/vimode/ex.rs
Normal file
381
src/readline/vimode/ex.rs
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
use std::iter::Peekable;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::str::Chars;
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
use crate::bitflags;
|
||||||
|
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||||
|
use crate::readline::keys::KeyEvent;
|
||||||
|
use crate::readline::linebuf::LineBuf;
|
||||||
|
use crate::readline::vicmd::{
|
||||||
|
Anchor, CmdFlags, Motion, MotionCmd, ReadSrc, RegisterName, To, Val, Verb, VerbCmd,
|
||||||
|
ViCmd, WriteDest,
|
||||||
|
};
|
||||||
|
use crate::readline::vimode::{ModeReport, ViInsert, ViMode};
|
||||||
|
use crate::state::write_meta;
|
||||||
|
|
||||||
|
bitflags! {
|
||||||
|
#[derive(Debug,Clone,Copy,PartialEq,Eq)]
|
||||||
|
pub struct SubFlags: u16 {
|
||||||
|
const GLOBAL = 1 << 0; // g
|
||||||
|
const CONFIRM = 1 << 1; // c (probably not implemented)
|
||||||
|
const IGNORE_CASE = 1 << 2; // i
|
||||||
|
const NO_IGNORE_CASE = 1 << 3; // I
|
||||||
|
const SHOW_COUNT = 1 << 4; // n
|
||||||
|
const PRINT_RESULT = 1 << 5; // p
|
||||||
|
const PRINT_NUMBERED = 1 << 6; // #
|
||||||
|
const PRINT_LEFT_ALIGN = 1 << 7; // l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug)]
|
||||||
|
struct ExEditor {
|
||||||
|
buf: LineBuf,
|
||||||
|
mode: ViInsert
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExEditor {
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
*self = Self::default()
|
||||||
|
}
|
||||||
|
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<()> {
|
||||||
|
let Some(cmd) = self.mode.handle_key(key) else {
|
||||||
|
return Ok(())
|
||||||
|
};
|
||||||
|
self.buf.exec_cmd(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug)]
|
||||||
|
pub struct ViEx {
|
||||||
|
pending_cmd: ExEditor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViEx {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViMode for ViEx {
|
||||||
|
// Ex mode can return errors, so we use this fallible method instead of the normal one
|
||||||
|
fn handle_key_fallible(&mut self, key: KeyEvent) -> ShResult<Option<ViCmd>> {
|
||||||
|
use crate::readline::keys::{KeyEvent as E, KeyCode as C, ModKeys as M};
|
||||||
|
log::debug!("[ViEx] handle_key_fallible: key={:?}", key);
|
||||||
|
match key {
|
||||||
|
E(C::Char('\r'), M::NONE) |
|
||||||
|
E(C::Enter, M::NONE) => {
|
||||||
|
let input = self.pending_cmd.buf.as_str();
|
||||||
|
log::debug!("[ViEx] Enter pressed, pending_cmd={:?}", input);
|
||||||
|
match parse_ex_cmd(input) {
|
||||||
|
Ok(cmd) => {
|
||||||
|
log::debug!("[ViEx] parse_ex_cmd Ok: {:?}", cmd);
|
||||||
|
Ok(cmd)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::debug!("[ViEx] parse_ex_cmd Err: {:?}", e);
|
||||||
|
let msg = e.unwrap_or(format!("Not an editor command: {}", input));
|
||||||
|
write_meta(|m| m.post_system_message(msg.clone()));
|
||||||
|
Err(ShErr::simple(ShErrKind::ParseErr, msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
E(C::Char('C'), M::CTRL) => {
|
||||||
|
log::debug!("[ViEx] Ctrl-C, clearing");
|
||||||
|
self.pending_cmd.clear();
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
E(C::Esc, M::NONE) => {
|
||||||
|
log::debug!("[ViEx] Esc, returning to normal mode");
|
||||||
|
Ok(Some(ViCmd {
|
||||||
|
register: RegisterName::default(),
|
||||||
|
verb: Some(VerbCmd(1, Verb::NormalMode)),
|
||||||
|
motion: None,
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
raw_seq: "".into(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::debug!("[ViEx] forwarding key to ExEditor");
|
||||||
|
self.pending_cmd.handle_key(key).map(|_| None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn handle_key(&mut self, key: KeyEvent) -> Option<ViCmd> {
|
||||||
|
let result = self.handle_key_fallible(key);
|
||||||
|
log::debug!("[ViEx] handle_key result: {:?}", result);
|
||||||
|
result.ok().flatten()
|
||||||
|
}
|
||||||
|
fn is_repeatable(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_replay(&self) -> Option<super::CmdReplay> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_style(&self) -> String {
|
||||||
|
"\x1b[3 q".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pending_seq(&self) -> Option<String> {
|
||||||
|
Some(self.pending_cmd.buf.as_str().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pending_cursor(&self) -> Option<usize> {
|
||||||
|
Some(self.pending_cmd.buf.cursor.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_on_undo(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp_cursor(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hist_scroll_start_pos(&self) -> Option<To> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_mode(&self) -> super::ModeReport {
|
||||||
|
ModeReport::Ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>,Option<String>> {
|
||||||
|
let raw = raw.trim();
|
||||||
|
if raw.is_empty() {
|
||||||
|
return Ok(None)
|
||||||
|
}
|
||||||
|
let mut chars = raw.chars().peekable();
|
||||||
|
let (verb, motion) = {
|
||||||
|
if chars.peek() == Some(&'g') {
|
||||||
|
let mut cmd_name = String::new();
|
||||||
|
while let Some(ch) = chars.peek() {
|
||||||
|
if ch.is_alphanumeric() {
|
||||||
|
cmd_name.push(*ch);
|
||||||
|
chars.next();
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !"global".starts_with(&cmd_name) {
|
||||||
|
return Err(None)
|
||||||
|
}
|
||||||
|
let Some(result) = parse_global(&mut chars)? else { return Ok(None) };
|
||||||
|
(Some(VerbCmd(1,result.1)), Some(MotionCmd(1,result.0)))
|
||||||
|
} else {
|
||||||
|
(parse_ex_command(&mut chars)?.map(|v| VerbCmd(1, v)), None)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(ViCmd {
|
||||||
|
register: RegisterName::default(),
|
||||||
|
verb,
|
||||||
|
motion,
|
||||||
|
raw_seq: raw.to_string(),
|
||||||
|
flags: CmdFlags::EXIT_CUR_MODE,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unescape shell command arguments
|
||||||
|
fn unescape_shell_cmd(cmd: &str) -> String {
|
||||||
|
// The pest grammar uses double quotes for vicut commands
|
||||||
|
// So shell commands need to escape double quotes
|
||||||
|
// We will be removing a single layer of escaping from double quotes
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut chars = cmd.chars().peekable();
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
if ch == '\\' {
|
||||||
|
if let Some(&'"') = chars.peek() {
|
||||||
|
chars.next();
|
||||||
|
result.push('"');
|
||||||
|
} else {
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> {
|
||||||
|
let mut cmd_name = String::new();
|
||||||
|
|
||||||
|
while let Some(ch) = chars.peek() {
|
||||||
|
if ch == &'!' {
|
||||||
|
cmd_name.push(*ch);
|
||||||
|
chars.next();
|
||||||
|
break
|
||||||
|
} else if !ch.is_alphanumeric() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cmd_name.push(*ch);
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
match cmd_name.as_str() {
|
||||||
|
"!" => {
|
||||||
|
let cmd = chars.collect::<String>();
|
||||||
|
let cmd = unescape_shell_cmd(&cmd);
|
||||||
|
Ok(Some(Verb::ShellCmd(cmd)))
|
||||||
|
}
|
||||||
|
"normal!" => parse_normal(chars),
|
||||||
|
_ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)),
|
||||||
|
_ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)),
|
||||||
|
_ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))),
|
||||||
|
_ if "read".starts_with(&cmd_name) => parse_read(chars),
|
||||||
|
_ if "write".starts_with(&cmd_name) => parse_write(chars),
|
||||||
|
_ if "substitute".starts_with(&cmd_name) => parse_substitute(chars),
|
||||||
|
_ => Err(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_normal(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> {
|
||||||
|
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop);
|
||||||
|
|
||||||
|
let seq: String = chars.collect();
|
||||||
|
Ok(Some(Verb::Normal(seq)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_read(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> {
|
||||||
|
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop);
|
||||||
|
|
||||||
|
let is_shell_read = if chars.peek() == Some(&'!') { chars.next(); true } else { false };
|
||||||
|
let arg: String = chars.collect();
|
||||||
|
|
||||||
|
if arg.trim().is_empty() {
|
||||||
|
return Err(Some("Expected file path or shell command after ':r'".into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_shell_read {
|
||||||
|
Ok(Some(Verb::Read(ReadSrc::Cmd(arg))))
|
||||||
|
} else {
|
||||||
|
let arg_path = get_path(arg.trim());
|
||||||
|
Ok(Some(Verb::Read(ReadSrc::File(arg_path))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_path(path: &str) -> PathBuf {
|
||||||
|
if let Some(stripped) = path.strip_prefix("~/")
|
||||||
|
&& let Some(home) = std::env::var_os("HOME") {
|
||||||
|
return PathBuf::from(home).join(stripped)
|
||||||
|
}
|
||||||
|
if path == "~"
|
||||||
|
&& let Some(home) = std::env::var_os("HOME") {
|
||||||
|
return PathBuf::from(home)
|
||||||
|
}
|
||||||
|
PathBuf::from(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_write(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> {
|
||||||
|
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop);
|
||||||
|
|
||||||
|
let is_shell_write = chars.peek() == Some(&'!');
|
||||||
|
if is_shell_write {
|
||||||
|
chars.next(); // consume '!'
|
||||||
|
let arg: String = chars.collect();
|
||||||
|
return Ok(Some(Verb::Write(WriteDest::Cmd(arg))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for >>
|
||||||
|
let mut append_check = chars.clone();
|
||||||
|
let is_file_append = append_check.next() == Some('>') && append_check.next() == Some('>');
|
||||||
|
if is_file_append {
|
||||||
|
*chars = append_check;
|
||||||
|
}
|
||||||
|
|
||||||
|
let arg: String = chars.collect();
|
||||||
|
let arg_path = get_path(arg.trim());
|
||||||
|
|
||||||
|
let dest = if is_file_append {
|
||||||
|
WriteDest::FileAppend(arg_path)
|
||||||
|
} else {
|
||||||
|
WriteDest::File(arg_path)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(Verb::Write(dest)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_global(chars: &mut Peekable<Chars<'_>>) -> Result<Option<(Motion,Verb)>,Option<String>> {
|
||||||
|
let is_negated = if chars.peek() == Some(&'!') { chars.next(); true } else { false };
|
||||||
|
|
||||||
|
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); // Ignore whitespace
|
||||||
|
|
||||||
|
let Some(delimiter) = chars.next() else {
|
||||||
|
return Ok(Some((Motion::Null,Verb::RepeatGlobal)))
|
||||||
|
};
|
||||||
|
if delimiter.is_alphanumeric() {
|
||||||
|
return Err(None)
|
||||||
|
}
|
||||||
|
let global_pat = parse_pattern(chars, delimiter)?;
|
||||||
|
let Some(command) = parse_ex_command(chars)? else {
|
||||||
|
return Err(Some("Expected a command after global pattern".into()))
|
||||||
|
};
|
||||||
|
if is_negated {
|
||||||
|
Ok(Some((Motion::NotGlobal(Val::new_str(global_pat)), command)))
|
||||||
|
} else {
|
||||||
|
Ok(Some((Motion::Global(Val::new_str(global_pat)), command)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_substitute(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> {
|
||||||
|
while chars.peek().is_some_and(|c| c.is_whitespace()) { chars.next(); } // Ignore whitespace
|
||||||
|
|
||||||
|
let Some(delimiter) = chars.next() else {
|
||||||
|
return Ok(Some(Verb::RepeatSubstitute))
|
||||||
|
};
|
||||||
|
if delimiter.is_alphanumeric() {
|
||||||
|
return Err(None)
|
||||||
|
}
|
||||||
|
let old_pat = parse_pattern(chars, delimiter)?;
|
||||||
|
let new_pat = parse_pattern(chars, delimiter)?;
|
||||||
|
let mut flags = SubFlags::empty();
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
match ch {
|
||||||
|
'g' => flags |= SubFlags::GLOBAL,
|
||||||
|
'i' => flags |= SubFlags::IGNORE_CASE,
|
||||||
|
'I' => flags |= SubFlags::NO_IGNORE_CASE,
|
||||||
|
'n' => flags |= SubFlags::SHOW_COUNT,
|
||||||
|
_ => return Err(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(Verb::Substitute(old_pat, new_pat, flags)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_pattern(chars: &mut Peekable<Chars<'_>>, delimiter: char) -> Result<String,Option<String>> {
|
||||||
|
let mut pat = String::new();
|
||||||
|
let mut closed = false;
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
match ch {
|
||||||
|
'\\' => {
|
||||||
|
if chars.peek().is_some_and(|c| *c == delimiter) {
|
||||||
|
// We escaped the delimiter, so we consume the escape char and continue
|
||||||
|
pat.push(chars.next().unwrap());
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
// The escape char is probably for the regex in the pattern
|
||||||
|
pat.push(ch);
|
||||||
|
if let Some(esc_ch) = chars.next() {
|
||||||
|
pat.push(esc_ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ if ch == delimiter => {
|
||||||
|
closed = true;
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_ => pat.push(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !closed {
|
||||||
|
Err(Some("Unclosed pattern in ex command".into()))
|
||||||
|
} else {
|
||||||
|
Ok(pat)
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/readline/vimode/insert.rs
Normal file
124
src/readline/vimode/insert.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use super::{common_cmds, CmdReplay, ModeReport, ViMode};
|
||||||
|
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||||
|
use crate::readline::vicmd::{
|
||||||
|
Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug)]
|
||||||
|
pub struct ViInsert {
|
||||||
|
cmds: Vec<ViCmd>,
|
||||||
|
pending_cmd: ViCmd,
|
||||||
|
repeat_count: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViInsert {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
pub fn with_count(mut self, repeat_count: u16) -> Self {
|
||||||
|
self.repeat_count = repeat_count;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn register_and_return(&mut self) -> Option<ViCmd> {
|
||||||
|
let mut cmd = self.take_cmd();
|
||||||
|
cmd.normalize_counts();
|
||||||
|
self.register_cmd(&cmd);
|
||||||
|
Some(cmd)
|
||||||
|
}
|
||||||
|
pub fn ctrl_w_is_undo(&self) -> bool {
|
||||||
|
let insert_count = self
|
||||||
|
.cmds
|
||||||
|
.iter()
|
||||||
|
.filter(|cmd: &&ViCmd| matches!(cmd.verb(), Some(VerbCmd(1, Verb::InsertChar(_)))))
|
||||||
|
.count();
|
||||||
|
let backspace_count = self
|
||||||
|
.cmds
|
||||||
|
.iter()
|
||||||
|
.filter(|cmd: &&ViCmd| matches!(cmd.verb(), Some(VerbCmd(1, Verb::Delete))))
|
||||||
|
.count();
|
||||||
|
insert_count > backspace_count
|
||||||
|
}
|
||||||
|
pub fn register_cmd(&mut self, cmd: &ViCmd) {
|
||||||
|
self.cmds.push(cmd.clone())
|
||||||
|
}
|
||||||
|
pub fn take_cmd(&mut self) -> ViCmd {
|
||||||
|
std::mem::take(&mut self.pending_cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViMode for ViInsert {
|
||||||
|
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||||
|
match key {
|
||||||
|
E(K::Char(ch), M::NONE) => {
|
||||||
|
self.pending_cmd.set_verb(VerbCmd(1, Verb::InsertChar(ch)));
|
||||||
|
self
|
||||||
|
.pending_cmd
|
||||||
|
.set_motion(MotionCmd(1, Motion::ForwardChar));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
E(K::Char('W'), M::CTRL) => {
|
||||||
|
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||||
|
self.pending_cmd.set_motion(MotionCmd(
|
||||||
|
1,
|
||||||
|
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
|
||||||
|
));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
E(K::Char('H'), M::CTRL) | E(K::Backspace, M::NONE) => {
|
||||||
|
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||||
|
self
|
||||||
|
.pending_cmd
|
||||||
|
.set_motion(MotionCmd(1, Motion::BackwardCharForced));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
|
||||||
|
E(K::BackTab, M::NONE) => {
|
||||||
|
self
|
||||||
|
.pending_cmd
|
||||||
|
.set_verb(VerbCmd(1, Verb::CompleteBackward));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
|
||||||
|
E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => {
|
||||||
|
self.pending_cmd.set_verb(VerbCmd(1, Verb::Complete));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
|
||||||
|
E(K::Esc, M::NONE) => {
|
||||||
|
self.pending_cmd.set_verb(VerbCmd(1, Verb::NormalMode));
|
||||||
|
self
|
||||||
|
.pending_cmd
|
||||||
|
.set_motion(MotionCmd(1, Motion::BackwardChar));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
_ => common_cmds(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_repeatable(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_replay(&self) -> Option<CmdReplay> {
|
||||||
|
Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_style(&self) -> String {
|
||||||
|
"\x1b[6 q".to_string()
|
||||||
|
}
|
||||||
|
fn pending_seq(&self) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn move_cursor_on_undo(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn clamp_cursor(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn hist_scroll_start_pos(&self) -> Option<To> {
|
||||||
|
Some(To::End)
|
||||||
|
}
|
||||||
|
fn report_mode(&self) -> ModeReport {
|
||||||
|
ModeReport::Insert
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/readline/vimode/mod.rs
Normal file
103
src/readline/vimode/mod.rs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
use crate::libsh::error::ShResult;
|
||||||
|
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||||
|
use crate::readline::vicmd::{
|
||||||
|
Motion, MotionCmd, To, Verb, VerbCmd, ViCmd,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod insert;
|
||||||
|
pub mod normal;
|
||||||
|
pub mod replace;
|
||||||
|
pub mod visual;
|
||||||
|
pub mod ex;
|
||||||
|
|
||||||
|
pub use ex::ViEx;
|
||||||
|
pub use insert::ViInsert;
|
||||||
|
pub use normal::ViNormal;
|
||||||
|
pub use replace::ViReplace;
|
||||||
|
pub use visual::ViVisual;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum ModeReport {
|
||||||
|
Insert,
|
||||||
|
Normal,
|
||||||
|
Ex,
|
||||||
|
Visual,
|
||||||
|
Replace,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CmdReplay {
|
||||||
|
ModeReplay { cmds: Vec<ViCmd>, repeat: u16 },
|
||||||
|
Single(ViCmd),
|
||||||
|
Motion(Motion),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CmdReplay {
|
||||||
|
pub fn mode(cmds: Vec<ViCmd>, repeat: u16) -> Self {
|
||||||
|
Self::ModeReplay { cmds, repeat }
|
||||||
|
}
|
||||||
|
pub fn single(cmd: ViCmd) -> Self {
|
||||||
|
Self::Single(cmd)
|
||||||
|
}
|
||||||
|
pub fn motion(motion: Motion) -> Self {
|
||||||
|
Self::Motion(motion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum CmdState {
|
||||||
|
Pending,
|
||||||
|
Complete,
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ViMode {
|
||||||
|
fn handle_key_fallible(&mut self, key: E) -> ShResult<Option<ViCmd>> { Ok(self.handle_key(key)) }
|
||||||
|
fn handle_key(&mut self, key: E) -> Option<ViCmd>;
|
||||||
|
fn is_repeatable(&self) -> bool;
|
||||||
|
fn as_replay(&self) -> Option<CmdReplay>;
|
||||||
|
fn cursor_style(&self) -> String;
|
||||||
|
fn pending_seq(&self) -> Option<String>;
|
||||||
|
fn pending_cursor(&self) -> Option<usize> { None }
|
||||||
|
fn move_cursor_on_undo(&self) -> bool;
|
||||||
|
fn clamp_cursor(&self) -> bool;
|
||||||
|
fn hist_scroll_start_pos(&self) -> Option<To>;
|
||||||
|
fn report_mode(&self) -> ModeReport;
|
||||||
|
fn cmds_from_raw(&mut self, raw: &str) -> Vec<ViCmd> {
|
||||||
|
let mut cmds = vec![];
|
||||||
|
for ch in raw.graphemes(true) {
|
||||||
|
let key = E::new(ch, M::NONE);
|
||||||
|
let Some(cmd) = self.handle_key(key) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
cmds.push(cmd)
|
||||||
|
}
|
||||||
|
cmds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn common_cmds(key: E) -> Option<ViCmd> {
|
||||||
|
let mut pending_cmd = ViCmd::new();
|
||||||
|
match key {
|
||||||
|
E(K::Home, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::BeginningOfLine)),
|
||||||
|
E(K::End, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::EndOfLine)),
|
||||||
|
E(K::Left, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::BackwardChar)),
|
||||||
|
E(K::Right, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar)),
|
||||||
|
E(K::Up, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineUp)),
|
||||||
|
E(K::Down, M::NONE) => pending_cmd.set_motion(MotionCmd(1, Motion::LineDown)),
|
||||||
|
E(K::Enter, M::NONE) => pending_cmd.set_verb(VerbCmd(1, Verb::AcceptLineOrNewline)),
|
||||||
|
E(K::Char('D'), M::CTRL) => pending_cmd.set_verb(VerbCmd(1, Verb::EndOfFile)),
|
||||||
|
E(K::Delete, M::NONE) => {
|
||||||
|
pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||||
|
pending_cmd.set_motion(MotionCmd(1, Motion::ForwardChar));
|
||||||
|
}
|
||||||
|
E(K::Backspace, M::NONE) | E(K::Char('H'), M::CTRL) => {
|
||||||
|
pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||||
|
pending_cmd.set_motion(MotionCmd(1, Motion::BackwardChar));
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
Some(pending_cmd)
|
||||||
|
}
|
||||||
849
src/readline/vimode/normal.rs
Normal file
849
src/readline/vimode/normal.rs
Normal file
@@ -0,0 +1,849 @@
|
|||||||
|
use std::iter::Peekable;
|
||||||
|
use std::str::Chars;
|
||||||
|
|
||||||
|
use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode};
|
||||||
|
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||||
|
use crate::readline::vicmd::{
|
||||||
|
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
|
||||||
|
VerbCmd, ViCmd, Word,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct ViNormal {
|
||||||
|
pending_seq: String,
|
||||||
|
pending_flags: CmdFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViNormal {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
pub fn clear_cmd(&mut self) {
|
||||||
|
self.pending_seq = String::new();
|
||||||
|
}
|
||||||
|
pub fn take_cmd(&mut self) -> String {
|
||||||
|
std::mem::take(&mut self.pending_seq)
|
||||||
|
}
|
||||||
|
pub fn flags(&self) -> CmdFlags {
|
||||||
|
self.pending_flags
|
||||||
|
}
|
||||||
|
#[allow(clippy::unnecessary_unwrap)]
|
||||||
|
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
|
||||||
|
if verb.is_none() {
|
||||||
|
match motion {
|
||||||
|
Some(Motion::TextObj(obj)) => {
|
||||||
|
return match obj {
|
||||||
|
TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete,
|
||||||
|
_ => CmdState::Invalid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Some(_) => return CmdState::Complete,
|
||||||
|
None => return CmdState::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if verb.is_some() && motion.is_none() {
|
||||||
|
match verb.unwrap() {
|
||||||
|
Verb::Put(_) => CmdState::Complete,
|
||||||
|
_ => CmdState::Pending,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CmdState::Complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn parse_count(&self, chars: &mut Peekable<Chars<'_>>) -> Option<usize> {
|
||||||
|
let mut count = String::new();
|
||||||
|
let Some(_digit @ '1'..='9') = chars.peek() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
count.push(chars.next().unwrap());
|
||||||
|
while let Some(_digit @ '0'..='9') = chars.peek() {
|
||||||
|
count.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
if !count.is_empty() {
|
||||||
|
count.parse::<usize>().ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// End the parse and clear the pending sequence
|
||||||
|
pub fn quit_parse(&mut self) -> Option<ViCmd> {
|
||||||
|
self.clear_cmd();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
pub fn try_parse(&mut self, ch: char) -> Option<ViCmd> {
|
||||||
|
self.pending_seq.push(ch);
|
||||||
|
let mut chars = self.pending_seq.chars().peekable();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Parse the register
|
||||||
|
*
|
||||||
|
* Registers can be any letter a-z or A-Z.
|
||||||
|
* While uncommon, it is possible to give a count to a register name.
|
||||||
|
*/
|
||||||
|
let register = 'reg_parse: {
|
||||||
|
let mut chars_clone = chars.clone();
|
||||||
|
let count = self.parse_count(&mut chars_clone);
|
||||||
|
|
||||||
|
let Some('"') = chars_clone.next() else {
|
||||||
|
break 'reg_parse RegisterName::default();
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(reg_name) = chars_clone.next() else {
|
||||||
|
return None; // Pending register name
|
||||||
|
};
|
||||||
|
match reg_name {
|
||||||
|
'a'..='z' | 'A'..='Z' => { /* proceed */ }
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
}
|
||||||
|
|
||||||
|
chars = chars_clone;
|
||||||
|
RegisterName::new(Some(reg_name), count)
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We will now parse the verb
|
||||||
|
* If we hit an invalid sequence, we will call 'return self.quit_parse()'
|
||||||
|
* self.quit_parse() will clear the pending command and return None
|
||||||
|
*
|
||||||
|
* If we hit an incomplete sequence, we will simply return None.
|
||||||
|
* returning None leaves the pending sequence where it is
|
||||||
|
*
|
||||||
|
* Note that we do use a label here for the block and 'return' values from
|
||||||
|
* this scope using "break 'verb_parse <value>"
|
||||||
|
*/
|
||||||
|
let verb = 'verb_parse: {
|
||||||
|
let mut chars_clone = chars.clone();
|
||||||
|
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
|
||||||
|
|
||||||
|
let Some(ch) = chars_clone.next() else {
|
||||||
|
break 'verb_parse None;
|
||||||
|
};
|
||||||
|
match ch {
|
||||||
|
'g' => {
|
||||||
|
if let Some(ch) = chars_clone.peek() {
|
||||||
|
match ch {
|
||||||
|
'v' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'~' => {
|
||||||
|
chars_clone.next();
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::ToggleCaseRange));
|
||||||
|
}
|
||||||
|
'u' => {
|
||||||
|
chars_clone.next();
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::ToLower));
|
||||||
|
}
|
||||||
|
'U' => {
|
||||||
|
chars_clone.next();
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::ToUpper));
|
||||||
|
}
|
||||||
|
'?' => {
|
||||||
|
chars_clone.next();
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Rot13));
|
||||||
|
}
|
||||||
|
_ => break 'verb_parse None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break 'verb_parse None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'.' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::RepeatLast)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'x' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Delete)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::ForwardCharForced)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'X' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Delete)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
's' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Change)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'S' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Change)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::WholeLineExclusive)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'p' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::After)));
|
||||||
|
}
|
||||||
|
'P' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before)));
|
||||||
|
}
|
||||||
|
'>' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Indent));
|
||||||
|
}
|
||||||
|
'<' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Dedent));
|
||||||
|
}
|
||||||
|
'r' => {
|
||||||
|
let ch = chars_clone.next()?;
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch, count as u16))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'R' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::ReplaceMode)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'~' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::ToggleCaseInplace(count as u16))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'u' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Undo)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'v' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::VisualMode)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'V' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::VisualModeLine)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'o' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::After))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'O' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertModeLineBreak(Anchor::Before))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'a' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'A' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
':' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::ExMode)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
'i' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'I' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'J' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::JoinLines)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'y' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Yank));
|
||||||
|
}
|
||||||
|
'd' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
|
||||||
|
}
|
||||||
|
'c' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Change));
|
||||||
|
}
|
||||||
|
'Y' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Yank)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'D' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Delete)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'C' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Change)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'=' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Equalize));
|
||||||
|
}
|
||||||
|
_ => break 'verb_parse None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let motion = 'motion_parse: {
|
||||||
|
let mut chars_clone = chars.clone();
|
||||||
|
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
|
||||||
|
|
||||||
|
let Some(ch) = chars_clone.next() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
// Double inputs like 'dd' and 'cc', and some special cases
|
||||||
|
match (ch, &verb) {
|
||||||
|
// Double inputs
|
||||||
|
('?', Some(VerbCmd(_, Verb::Rot13)))
|
||||||
|
| ('d', Some(VerbCmd(_, Verb::Delete)))
|
||||||
|
| ('y', Some(VerbCmd(_, Verb::Yank)))
|
||||||
|
| ('=', Some(VerbCmd(_, Verb::Equalize)))
|
||||||
|
| ('u', Some(VerbCmd(_, Verb::ToLower)))
|
||||||
|
| ('U', Some(VerbCmd(_, Verb::ToUpper)))
|
||||||
|
| ('~', Some(VerbCmd(_, Verb::ToggleCaseRange)))
|
||||||
|
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
||||||
|
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive));
|
||||||
|
}
|
||||||
|
('c', Some(VerbCmd(_, Verb::Change))) => {
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive));
|
||||||
|
}
|
||||||
|
('W', Some(VerbCmd(_, Verb::Change))) => {
|
||||||
|
// Same with 'W'
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::End, Word::Big, Direction::Forward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => { /* Nothing weird, so let's continue */ }
|
||||||
|
}
|
||||||
|
match ch {
|
||||||
|
'g' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
match ch {
|
||||||
|
'g' => {
|
||||||
|
chars_clone.next();
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
|
||||||
|
}
|
||||||
|
'e' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::End, Word::Normal, Direction::Backward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'E' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::End, Word::Big, Direction::Backward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'k' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp));
|
||||||
|
}
|
||||||
|
'j' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown));
|
||||||
|
}
|
||||||
|
'_' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLastWord));
|
||||||
|
}
|
||||||
|
'0' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfScreenLine));
|
||||||
|
}
|
||||||
|
'^' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::FirstGraphicalOnScreenLine));
|
||||||
|
}
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
']' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
match ch {
|
||||||
|
')' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Forward)));
|
||||||
|
}
|
||||||
|
'}' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Forward)));
|
||||||
|
}
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'[' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
match ch {
|
||||||
|
'(' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Backward)));
|
||||||
|
}
|
||||||
|
'{' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Backward)));
|
||||||
|
}
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'%' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToDelimMatch));
|
||||||
|
}
|
||||||
|
'v' => {
|
||||||
|
// We got 'v' after a verb
|
||||||
|
// Instead of normal operations, we will calculate the span based on how visual
|
||||||
|
// mode would see it
|
||||||
|
if self
|
||||||
|
.flags()
|
||||||
|
.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK)
|
||||||
|
{
|
||||||
|
// We can't have more than one of these
|
||||||
|
return self.quit_parse();
|
||||||
|
}
|
||||||
|
self.pending_flags |= CmdFlags::VISUAL;
|
||||||
|
break 'motion_parse None;
|
||||||
|
}
|
||||||
|
'V' => {
|
||||||
|
// We got 'V' after a verb
|
||||||
|
// Instead of normal operations, we will calculate the span based on how visual
|
||||||
|
// line mode would see it
|
||||||
|
if self
|
||||||
|
.flags()
|
||||||
|
.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK)
|
||||||
|
{
|
||||||
|
// We can't have more than one of these
|
||||||
|
// I know vim can technically do this, but it doesn't really make sense to allow
|
||||||
|
// it since even in vim only the first one given is used
|
||||||
|
return self.quit_parse();
|
||||||
|
}
|
||||||
|
self.pending_flags |= CmdFlags::VISUAL;
|
||||||
|
break 'motion_parse None;
|
||||||
|
}
|
||||||
|
// TODO: figure out how to include 'Ctrl+V' here, might need a refactor
|
||||||
|
'G' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::EndOfBuffer));
|
||||||
|
}
|
||||||
|
'f' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::CharSearch(Direction::Forward, Dest::On, *ch),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'F' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::CharSearch(Direction::Backward, Dest::On, *ch),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
't' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::CharSearch(Direction::Forward, Dest::Before, *ch),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'T' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::CharSearch(Direction::Backward, Dest::Before, *ch),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
';' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion));
|
||||||
|
}
|
||||||
|
',' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev));
|
||||||
|
}
|
||||||
|
'|' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToColumn));
|
||||||
|
}
|
||||||
|
'^' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfFirstWord));
|
||||||
|
}
|
||||||
|
'0' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine));
|
||||||
|
}
|
||||||
|
'$' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine));
|
||||||
|
}
|
||||||
|
'k' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::LineUp));
|
||||||
|
}
|
||||||
|
'j' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::LineDown));
|
||||||
|
}
|
||||||
|
'h' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar));
|
||||||
|
}
|
||||||
|
'l' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar));
|
||||||
|
}
|
||||||
|
'w' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::Start, Word::Normal, Direction::Forward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'W' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::Start, Word::Big, Direction::Forward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'e' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::End, Word::Normal, Direction::Forward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'E' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::End, Word::Big, Direction::Forward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'b' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'B' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::Start, Word::Big, Direction::Backward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
')' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::TextObj(TextObj::Sentence(Direction::Forward)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'(' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::TextObj(TextObj::Sentence(Direction::Backward)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'}' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::TextObj(TextObj::Paragraph(Direction::Forward)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'{' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::TextObj(TextObj::Paragraph(Direction::Backward)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
ch if ch == 'i' || ch == 'a' => {
|
||||||
|
let bound = match ch {
|
||||||
|
'i' => Bound::Inside,
|
||||||
|
'a' => Bound::Around,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
if chars_clone.peek().is_none() {
|
||||||
|
break 'motion_parse None;
|
||||||
|
}
|
||||||
|
let obj = match chars_clone.next().unwrap() {
|
||||||
|
'w' => TextObj::Word(Word::Normal, bound),
|
||||||
|
'W' => TextObj::Word(Word::Big, bound),
|
||||||
|
's' => TextObj::WholeSentence(bound),
|
||||||
|
'p' => TextObj::WholeParagraph(bound),
|
||||||
|
'"' => TextObj::DoubleQuote(bound),
|
||||||
|
'\'' => TextObj::SingleQuote(bound),
|
||||||
|
'`' => TextObj::BacktickQuote(bound),
|
||||||
|
'(' | ')' | 'b' => TextObj::Paren(bound),
|
||||||
|
'{' | '}' | 'B' => TextObj::Brace(bound),
|
||||||
|
'[' | ']' => TextObj::Bracket(bound),
|
||||||
|
'<' | '>' => TextObj::Angle(bound),
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
};
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj)));
|
||||||
|
}
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later
|
||||||
|
|
||||||
|
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||||
|
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||||
|
|
||||||
|
match self.validate_combination(verb_ref, motion_ref) {
|
||||||
|
CmdState::Complete => Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb,
|
||||||
|
motion,
|
||||||
|
raw_seq: std::mem::take(&mut self.pending_seq),
|
||||||
|
flags: self.flags(),
|
||||||
|
}),
|
||||||
|
CmdState::Pending => None,
|
||||||
|
CmdState::Invalid => {
|
||||||
|
self.pending_seq.clear();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViMode for ViNormal {
|
||||||
|
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||||
|
let mut cmd: Option<ViCmd> = match key {
|
||||||
|
E(K::Char('V'), M::NONE) => Some(ViCmd {
|
||||||
|
register: Default::default(),
|
||||||
|
verb: Some(VerbCmd(1, Verb::VisualModeLine)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: "".into(),
|
||||||
|
flags: self.flags(),
|
||||||
|
}),
|
||||||
|
E(K::Char('A'), M::CTRL) => {
|
||||||
|
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
|
||||||
|
self.pending_seq.clear();
|
||||||
|
Some(ViCmd {
|
||||||
|
register: Default::default(),
|
||||||
|
verb: Some(VerbCmd(1, Verb::IncrementNumber(count))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: "".into(),
|
||||||
|
flags: self.flags(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
E(K::Char('X'), M::CTRL) => {
|
||||||
|
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
|
||||||
|
self.pending_seq.clear();
|
||||||
|
Some(ViCmd {
|
||||||
|
register: Default::default(),
|
||||||
|
verb: Some(VerbCmd(1, Verb::DecrementNumber(count))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: "".into(),
|
||||||
|
flags: self.flags(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
E(K::Char(ch), M::NONE) => self.try_parse(ch),
|
||||||
|
E(K::Backspace, M::NONE) => Some(ViCmd {
|
||||||
|
register: Default::default(),
|
||||||
|
verb: None,
|
||||||
|
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
||||||
|
raw_seq: "".into(),
|
||||||
|
flags: self.flags(),
|
||||||
|
}),
|
||||||
|
E(K::Char('R'), M::CTRL) => {
|
||||||
|
let mut chars = self.pending_seq.chars().peekable();
|
||||||
|
let count = self.parse_count(&mut chars).unwrap_or(1);
|
||||||
|
Some(ViCmd {
|
||||||
|
register: RegisterName::default(),
|
||||||
|
verb: Some(VerbCmd(count, Verb::Redo)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: self.flags(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
E(K::Esc, M::NONE) => {
|
||||||
|
self.clear_cmd();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(cmd) = common_cmds(key) {
|
||||||
|
self.clear_cmd();
|
||||||
|
Some(cmd)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(cmd) = cmd.as_mut() {
|
||||||
|
cmd.normalize_counts();
|
||||||
|
};
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_repeatable(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_replay(&self) -> Option<CmdReplay> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_style(&self) -> String {
|
||||||
|
"\x1b[2 q".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pending_seq(&self) -> Option<String> {
|
||||||
|
Some(self.pending_seq.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_on_undo(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn clamp_cursor(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn hist_scroll_start_pos(&self) -> Option<To> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn report_mode(&self) -> ModeReport {
|
||||||
|
ModeReport::Normal
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/readline/vimode/replace.rs
Normal file
107
src/readline/vimode/replace.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
use super::{common_cmds, CmdReplay, ModeReport, ViMode};
|
||||||
|
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||||
|
use crate::readline::vicmd::{
|
||||||
|
Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct ViReplace {
|
||||||
|
cmds: Vec<ViCmd>,
|
||||||
|
pending_cmd: ViCmd,
|
||||||
|
repeat_count: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViReplace {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
pub fn with_count(mut self, repeat_count: u16) -> Self {
|
||||||
|
self.repeat_count = repeat_count;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
pub fn register_and_return(&mut self) -> Option<ViCmd> {
|
||||||
|
let mut cmd = self.take_cmd();
|
||||||
|
cmd.normalize_counts();
|
||||||
|
self.register_cmd(&cmd);
|
||||||
|
Some(cmd)
|
||||||
|
}
|
||||||
|
pub fn register_cmd(&mut self, cmd: &ViCmd) {
|
||||||
|
self.cmds.push(cmd.clone())
|
||||||
|
}
|
||||||
|
pub fn take_cmd(&mut self) -> ViCmd {
|
||||||
|
std::mem::take(&mut self.pending_cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViMode for ViReplace {
|
||||||
|
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||||
|
match key {
|
||||||
|
E(K::Char(ch), M::NONE) => {
|
||||||
|
self.pending_cmd.set_verb(VerbCmd(1, Verb::ReplaceChar(ch)));
|
||||||
|
self
|
||||||
|
.pending_cmd
|
||||||
|
.set_motion(MotionCmd(1, Motion::ForwardChar));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
E(K::Char('W'), M::CTRL) => {
|
||||||
|
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
|
||||||
|
self.pending_cmd.set_motion(MotionCmd(
|
||||||
|
1,
|
||||||
|
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
|
||||||
|
));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
E(K::Char('H'), M::CTRL) | E(K::Backspace, M::NONE) => {
|
||||||
|
self
|
||||||
|
.pending_cmd
|
||||||
|
.set_motion(MotionCmd(1, Motion::BackwardChar));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
|
||||||
|
E(K::BackTab, M::NONE) => {
|
||||||
|
self
|
||||||
|
.pending_cmd
|
||||||
|
.set_verb(VerbCmd(1, Verb::CompleteBackward));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
|
||||||
|
E(K::Char('I'), M::CTRL) | E(K::Tab, M::NONE) => {
|
||||||
|
self.pending_cmd.set_verb(VerbCmd(1, Verb::Complete));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
|
||||||
|
E(K::Esc, M::NONE) => {
|
||||||
|
self.pending_cmd.set_verb(VerbCmd(1, Verb::NormalMode));
|
||||||
|
self
|
||||||
|
.pending_cmd
|
||||||
|
.set_motion(MotionCmd(1, Motion::BackwardChar));
|
||||||
|
self.register_and_return()
|
||||||
|
}
|
||||||
|
_ => common_cmds(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn is_repeatable(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn cursor_style(&self) -> String {
|
||||||
|
"\x1b[4 q".to_string()
|
||||||
|
}
|
||||||
|
fn pending_seq(&self) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn as_replay(&self) -> Option<CmdReplay> {
|
||||||
|
Some(CmdReplay::mode(self.cmds.clone(), self.repeat_count))
|
||||||
|
}
|
||||||
|
fn move_cursor_on_undo(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn clamp_cursor(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn hist_scroll_start_pos(&self) -> Option<To> {
|
||||||
|
Some(To::End)
|
||||||
|
}
|
||||||
|
fn report_mode(&self) -> ModeReport {
|
||||||
|
ModeReport::Replace
|
||||||
|
}
|
||||||
|
}
|
||||||
695
src/readline/vimode/visual.rs
Normal file
695
src/readline/vimode/visual.rs
Normal file
@@ -0,0 +1,695 @@
|
|||||||
|
use std::iter::Peekable;
|
||||||
|
use std::str::Chars;
|
||||||
|
|
||||||
|
use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode};
|
||||||
|
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
|
||||||
|
use crate::readline::vicmd::{
|
||||||
|
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
|
||||||
|
VerbCmd, ViCmd, Word,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct ViVisual {
|
||||||
|
pending_seq: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViVisual {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
pub fn clear_cmd(&mut self) {
|
||||||
|
self.pending_seq = String::new();
|
||||||
|
}
|
||||||
|
pub fn take_cmd(&mut self) -> String {
|
||||||
|
std::mem::take(&mut self.pending_seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unnecessary_unwrap)]
|
||||||
|
fn validate_combination(&self, verb: Option<&Verb>, motion: Option<&Motion>) -> CmdState {
|
||||||
|
if verb.is_none() {
|
||||||
|
match motion {
|
||||||
|
Some(_) => return CmdState::Complete,
|
||||||
|
None => return CmdState::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if motion.is_none() && verb.is_some() {
|
||||||
|
match verb.unwrap() {
|
||||||
|
Verb::Put(_) => CmdState::Complete,
|
||||||
|
_ => CmdState::Pending,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CmdState::Complete
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn parse_count(&self, chars: &mut Peekable<Chars<'_>>) -> Option<usize> {
|
||||||
|
let mut count = String::new();
|
||||||
|
let Some(_digit @ '1'..='9') = chars.peek() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
count.push(chars.next().unwrap());
|
||||||
|
while let Some(_digit @ '0'..='9') = chars.peek() {
|
||||||
|
count.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
if !count.is_empty() {
|
||||||
|
count.parse::<usize>().ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// End the parse and clear the pending sequence
|
||||||
|
pub fn quit_parse(&mut self) -> Option<ViCmd> {
|
||||||
|
self.clear_cmd();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
pub fn try_parse(&mut self, ch: char) -> Option<ViCmd> {
|
||||||
|
self.pending_seq.push(ch);
|
||||||
|
let mut chars = self.pending_seq.chars().peekable();
|
||||||
|
|
||||||
|
let register = 'reg_parse: {
|
||||||
|
let mut chars_clone = chars.clone();
|
||||||
|
let count = self.parse_count(&mut chars_clone);
|
||||||
|
|
||||||
|
let Some('"') = chars_clone.next() else {
|
||||||
|
break 'reg_parse RegisterName::default();
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(reg_name) = chars_clone.next() else {
|
||||||
|
return None; // Pending register name
|
||||||
|
};
|
||||||
|
match reg_name {
|
||||||
|
'a'..='z' | 'A'..='Z' => { /* proceed */ }
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
}
|
||||||
|
|
||||||
|
chars = chars_clone;
|
||||||
|
RegisterName::new(Some(reg_name), count)
|
||||||
|
};
|
||||||
|
|
||||||
|
let verb = 'verb_parse: {
|
||||||
|
let mut chars_clone = chars.clone();
|
||||||
|
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
|
||||||
|
|
||||||
|
let Some(ch) = chars_clone.next() else {
|
||||||
|
break 'verb_parse None;
|
||||||
|
};
|
||||||
|
match ch {
|
||||||
|
'g' => {
|
||||||
|
if let Some(ch) = chars_clone.peek() {
|
||||||
|
match ch {
|
||||||
|
'v' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::VisualModeSelectLast)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'?' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::Rot13)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => break 'verb_parse None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break 'verb_parse None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'.' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::RepeatLast)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'x' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
|
||||||
|
}
|
||||||
|
'X' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::Delete)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'Y' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::Yank)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'D' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::Delete)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'R' | 'C' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::Change)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::WholeLineExclusive)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'>' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::Indent)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'<' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::Dedent)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'=' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::Equalize)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::WholeLineInclusive)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'p' | 'P' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Put(Anchor::Before)));
|
||||||
|
}
|
||||||
|
'r' => {
|
||||||
|
let ch = chars_clone.next()?;
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'~' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(1, Verb::ToggleCaseRange)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'u' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::ToLower)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'U' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::ToUpper)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'O' | 'o' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::SwapVisualAnchor)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'A' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'I' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::InsertMode)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'J' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::JoinLines)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'y' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Yank));
|
||||||
|
}
|
||||||
|
'd' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Delete));
|
||||||
|
}
|
||||||
|
'c' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'verb_parse Some(VerbCmd(count, Verb::Change));
|
||||||
|
}
|
||||||
|
_ => break 'verb_parse None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(verb) = verb {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(verb),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let motion = 'motion_parse: {
|
||||||
|
let mut chars_clone = chars.clone();
|
||||||
|
let count = self.parse_count(&mut chars_clone).unwrap_or(1);
|
||||||
|
|
||||||
|
let Some(ch) = chars_clone.next() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
match (ch, &verb) {
|
||||||
|
('d', Some(VerbCmd(_, Verb::Delete)))
|
||||||
|
| ('y', Some(VerbCmd(_, Verb::Yank)))
|
||||||
|
| ('=', Some(VerbCmd(_, Verb::Equalize)))
|
||||||
|
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
||||||
|
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive));
|
||||||
|
}
|
||||||
|
('c', Some(VerbCmd(_, Verb::Change))) => {
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
match ch {
|
||||||
|
'g' => {
|
||||||
|
if let Some(ch) = chars_clone.peek() {
|
||||||
|
match ch {
|
||||||
|
'g' => {
|
||||||
|
chars_clone.next();
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
|
||||||
|
}
|
||||||
|
'e' => {
|
||||||
|
chars_clone.next();
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::End, Word::Normal, Direction::Backward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'E' => {
|
||||||
|
chars_clone.next();
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::End, Word::Big, Direction::Backward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'k' => {
|
||||||
|
chars_clone.next();
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineUp));
|
||||||
|
}
|
||||||
|
'j' => {
|
||||||
|
chars_clone.next();
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ScreenLineDown));
|
||||||
|
}
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
']' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
match ch {
|
||||||
|
')' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Forward)));
|
||||||
|
}
|
||||||
|
'}' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Forward)));
|
||||||
|
}
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'[' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
match ch {
|
||||||
|
'(' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToParen(Direction::Backward)));
|
||||||
|
}
|
||||||
|
'{' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToBrace(Direction::Backward)));
|
||||||
|
}
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'%' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToDelimMatch));
|
||||||
|
}
|
||||||
|
'f' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::CharSearch(Direction::Forward, Dest::On, *ch),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'F' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::CharSearch(Direction::Backward, Dest::On, *ch),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
't' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::CharSearch(Direction::Forward, Dest::Before, *ch),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'T' => {
|
||||||
|
let Some(ch) = chars_clone.peek() else {
|
||||||
|
break 'motion_parse None;
|
||||||
|
};
|
||||||
|
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::CharSearch(Direction::Backward, Dest::Before, *ch),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
';' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotion));
|
||||||
|
}
|
||||||
|
',' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::RepeatMotionRev));
|
||||||
|
}
|
||||||
|
'|' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ToColumn));
|
||||||
|
}
|
||||||
|
'0' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfLine));
|
||||||
|
}
|
||||||
|
'$' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::EndOfLine));
|
||||||
|
}
|
||||||
|
'k' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::LineUp));
|
||||||
|
}
|
||||||
|
'j' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::LineDown));
|
||||||
|
}
|
||||||
|
'h' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::BackwardChar));
|
||||||
|
}
|
||||||
|
'l' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::ForwardChar));
|
||||||
|
}
|
||||||
|
'w' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::Start, Word::Normal, Direction::Forward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'W' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::Start, Word::Big, Direction::Forward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'e' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::End, Word::Normal, Direction::Forward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'E' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::End, Word::Big, Direction::Forward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'b' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::Start, Word::Normal, Direction::Backward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'B' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::WordMotion(To::Start, Word::Big, Direction::Backward),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
')' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::TextObj(TextObj::Sentence(Direction::Forward)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'(' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::TextObj(TextObj::Sentence(Direction::Backward)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'}' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::TextObj(TextObj::Paragraph(Direction::Forward)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
'{' => {
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(
|
||||||
|
count,
|
||||||
|
Motion::TextObj(TextObj::Paragraph(Direction::Backward)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
ch if ch == 'i' || ch == 'a' => {
|
||||||
|
let bound = match ch {
|
||||||
|
'i' => Bound::Inside,
|
||||||
|
'a' => Bound::Around,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
if chars_clone.peek().is_none() {
|
||||||
|
break 'motion_parse None;
|
||||||
|
}
|
||||||
|
let obj = match chars_clone.next().unwrap() {
|
||||||
|
'w' => TextObj::Word(Word::Normal, bound),
|
||||||
|
'W' => TextObj::Word(Word::Big, bound),
|
||||||
|
's' => TextObj::WholeSentence(bound),
|
||||||
|
'p' => TextObj::WholeParagraph(bound),
|
||||||
|
'"' => TextObj::DoubleQuote(bound),
|
||||||
|
'\'' => TextObj::SingleQuote(bound),
|
||||||
|
'`' => TextObj::BacktickQuote(bound),
|
||||||
|
'(' | ')' | 'b' => TextObj::Paren(bound),
|
||||||
|
'{' | '}' | 'B' => TextObj::Brace(bound),
|
||||||
|
'[' | ']' => TextObj::Bracket(bound),
|
||||||
|
'<' | '>' => TextObj::Angle(bound),
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
};
|
||||||
|
chars = chars_clone;
|
||||||
|
break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj)));
|
||||||
|
}
|
||||||
|
_ => return self.quit_parse(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later
|
||||||
|
|
||||||
|
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||||
|
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||||
|
|
||||||
|
match self.validate_combination(verb_ref, motion_ref) {
|
||||||
|
CmdState::Complete => Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb,
|
||||||
|
motion,
|
||||||
|
raw_seq: std::mem::take(&mut self.pending_seq),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
}),
|
||||||
|
CmdState::Pending => None,
|
||||||
|
CmdState::Invalid => {
|
||||||
|
self.pending_seq.clear();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViMode for ViVisual {
|
||||||
|
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||||
|
let mut cmd: Option<ViCmd> = match key {
|
||||||
|
E(K::Char(ch), M::NONE) => self.try_parse(ch),
|
||||||
|
E(K::Backspace, M::NONE) => Some(ViCmd {
|
||||||
|
register: Default::default(),
|
||||||
|
verb: None,
|
||||||
|
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
||||||
|
raw_seq: "".into(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
}),
|
||||||
|
E(K::Char('A'), M::CTRL) => {
|
||||||
|
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
|
||||||
|
self.pending_seq.clear();
|
||||||
|
Some(ViCmd {
|
||||||
|
register: Default::default(),
|
||||||
|
verb: Some(VerbCmd(1, Verb::IncrementNumber(count))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: "".into(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
E(K::Char('X'), M::CTRL) => {
|
||||||
|
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16;
|
||||||
|
self.pending_seq.clear();
|
||||||
|
Some(ViCmd {
|
||||||
|
register: Default::default(),
|
||||||
|
verb: Some(VerbCmd(1, Verb::DecrementNumber(count))),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: "".into(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
E(K::Char('R'), M::CTRL) => {
|
||||||
|
let mut chars = self.pending_seq.chars().peekable();
|
||||||
|
let count = self.parse_count(&mut chars).unwrap_or(1);
|
||||||
|
Some(ViCmd {
|
||||||
|
register: RegisterName::default(),
|
||||||
|
verb: Some(VerbCmd(count, Verb::Redo)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
E(K::Esc, M::NONE) => Some(ViCmd {
|
||||||
|
register: Default::default(),
|
||||||
|
verb: Some(VerbCmd(1, Verb::NormalMode)),
|
||||||
|
motion: Some(MotionCmd(1, Motion::Null)),
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
}),
|
||||||
|
_ => {
|
||||||
|
if let Some(cmd) = common_cmds(key) {
|
||||||
|
self.clear_cmd();
|
||||||
|
Some(cmd)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(cmd) = cmd.as_mut() {
|
||||||
|
cmd.normalize_counts();
|
||||||
|
};
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_repeatable(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_replay(&self) -> Option<CmdReplay> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_style(&self) -> String {
|
||||||
|
"\x1b[2 q".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pending_seq(&self) -> Option<String> {
|
||||||
|
Some(self.pending_seq.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_cursor_on_undo(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp_cursor(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hist_scroll_start_pos(&self) -> Option<To> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_mode(&self) -> ModeReport {
|
||||||
|
ModeReport::Visual
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/shopt.rs
12
src/shopt.rs
@@ -343,6 +343,7 @@ pub struct ShOptPrompt {
|
|||||||
pub highlight: bool,
|
pub highlight: bool,
|
||||||
pub auto_indent: bool,
|
pub auto_indent: bool,
|
||||||
pub linebreak_on_incomplete: bool,
|
pub linebreak_on_incomplete: bool,
|
||||||
|
pub leader: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShOptPrompt {
|
impl ShOptPrompt {
|
||||||
@@ -402,6 +403,9 @@ impl ShOptPrompt {
|
|||||||
};
|
};
|
||||||
self.linebreak_on_incomplete = val;
|
self.linebreak_on_incomplete = val;
|
||||||
}
|
}
|
||||||
|
"leader" => {
|
||||||
|
self.leader = val.to_string();
|
||||||
|
}
|
||||||
"custom" => {
|
"custom" => {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
@@ -459,6 +463,12 @@ impl ShOptPrompt {
|
|||||||
output.push_str(&format!("{}", self.linebreak_on_incomplete));
|
output.push_str(&format!("{}", self.linebreak_on_incomplete));
|
||||||
Ok(Some(output))
|
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(
|
_ => Err(
|
||||||
ShErr::simple(
|
ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
@@ -482,6 +492,7 @@ impl Display for ShOptPrompt {
|
|||||||
"linebreak_on_incomplete = {}",
|
"linebreak_on_incomplete = {}",
|
||||||
self.linebreak_on_incomplete
|
self.linebreak_on_incomplete
|
||||||
));
|
));
|
||||||
|
output.push(format!("leader = {}", self.leader));
|
||||||
|
|
||||||
let final_output = output.join("\n");
|
let final_output = output.join("\n");
|
||||||
|
|
||||||
@@ -498,6 +509,7 @@ impl Default for ShOptPrompt {
|
|||||||
highlight: true,
|
highlight: true,
|
||||||
auto_indent: true,
|
auto_indent: true,
|
||||||
linebreak_on_incomplete: true,
|
linebreak_on_incomplete: true,
|
||||||
|
leader: "\\".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
src/state.rs
29
src/state.rs
@@ -11,7 +11,7 @@ use std::{
|
|||||||
use nix::unistd::{User, gethostname, getppid};
|
use nix::unistd::{User, gethostname, getppid};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::{BUILTINS, map::MapNode, trap::TrapTarget},
|
builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget},
|
||||||
exec_input,
|
exec_input,
|
||||||
jobs::JobTab,
|
jobs::JobTab,
|
||||||
libsh::{
|
libsh::{
|
||||||
@@ -24,8 +24,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
readline::{
|
readline::{
|
||||||
complete::{BashCompSpec, CompSpec},
|
complete::{BashCompSpec, CompSpec}, keys::KeyEvent, markers
|
||||||
markers,
|
|
||||||
},
|
},
|
||||||
shopt::ShOpts,
|
shopt::ShOpts,
|
||||||
};
|
};
|
||||||
@@ -533,12 +532,36 @@ pub struct LogTab {
|
|||||||
functions: HashMap<String, ShFunc>,
|
functions: HashMap<String, ShFunc>,
|
||||||
aliases: HashMap<String, ShAlias>,
|
aliases: HashMap<String, ShAlias>,
|
||||||
traps: HashMap<TrapTarget, String>,
|
traps: HashMap<TrapTarget, String>,
|
||||||
|
keymaps: Vec<KeyMap>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogTab {
|
impl LogTab {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
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) {
|
pub fn insert_func(&mut self, name: &str, src: ShFunc) {
|
||||||
self.functions.insert(name.into(), src);
|
self.functions.insert(name.into(), src);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user