From ba62b407bdf0cb0b21641981a5729c4a6de0fa6c Mon Sep 17 00:00:00 2001 From: darcy Date: Wed, 8 Jan 2025 21:38:21 +1100 Subject: [PATCH] feat: replace mode and Vim motions (`W`, `E`, `B`, `^`, `_`) for inputs (#2143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 三咲雅 · Misaki Masa --- yazi-config/preset/keymap-default.toml | 44 ++++++++------ yazi-core/src/input/commands/backspace.rs | 2 +- yazi-core/src/input/commands/backward.rs | 17 ++++-- yazi-core/src/input/commands/complete.rs | 2 +- yazi-core/src/input/commands/escape.rs | 3 + yazi-core/src/input/commands/forward.rs | 11 ++-- yazi-core/src/input/commands/mod.rs | 5 +- yazi-core/src/input/commands/move_.rs | 72 +++++++++++++++++++---- yazi-core/src/input/commands/replace.rs | 32 ++++++++++ yazi-core/src/input/commands/show.rs | 2 +- yazi-core/src/input/commands/type_.rs | 23 ++++++-- yazi-core/src/input/input.rs | 16 +---- yazi-core/src/input/mode.rs | 1 + yazi-fm/src/app/app.rs | 2 + yazi-fm/src/executor.rs | 2 + yazi-fm/src/input/input.rs | 1 + yazi-fm/src/term.rs | 10 ++++ yazi-shared/src/chars.rs | 4 ++ 18 files changed, 184 insertions(+), 65 deletions(-) create mode 100644 yazi-core/src/input/commands/replace.rs diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index bb4b6a47..be9916ea 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -232,12 +232,13 @@ keymap = [ { on = "", run = "escape", desc = "Go back the normal mode, or cancel input" }, # Mode - { on = "i", run = "insert", desc = "Enter insert mode" }, - { on = "a", run = "insert --append", desc = "Enter append mode" }, - { on = "I", run = [ "move -999", "insert" ], desc = "Move to the BOL, and enter insert mode" }, - { on = "A", run = [ "move 999", "insert --append" ], desc = "Move to the EOL, and enter append mode" }, - { on = "v", run = "visual", desc = "Enter visual mode" }, - { on = "V", run = [ "move -999", "visual", "move 999" ], desc = "Enter visual mode and select all" }, + { on = "i", run = "insert", desc = "Enter insert mode" }, + { on = "I", run = [ "move first-char", "insert" ], desc = "Move to the BOL, and enter insert mode" }, + { on = "a", run = "insert --append", desc = "Enter append mode" }, + { on = "A", run = [ "move eol", "insert --append" ], desc = "Move to the EOL, and enter append mode" }, + { on = "v", run = "visual", desc = "Enter visual mode" }, + { on = "V", run = [ "move bol", "visual", "move eol" ], desc = "Enter visual mode and select all" }, + { on = "r", run = "replace", desc = "Replace a single character" }, # Character-wise movement { on = "h", run = "move -1", desc = "Move back a character" }, @@ -248,19 +249,24 @@ keymap = [ { on = "", run = "move 1", desc = "Move forward a character" }, # Word-wise movement - { on = "b", run = "backward", desc = "Move back to the start of the current or previous word" }, - { on = "w", run = "forward", desc = "Move forward to the start of the next word" }, - { on = "e", run = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, - { on = "", run = "backward", desc = "Move back to the start of the current or previous word" }, - { on = "", run = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, + { on = "b", run = "backward", desc = "Move back to the start of the current or previous word" }, + { on = "B", run = "backward --far", desc = "Move back to the start of the current or previous WORD" }, + { on = "w", run = "forward", desc = "Move forward to the start of the next word" }, + { on = "W", run = "forward --far", desc = "Move forward to the start of the next WORD" }, + { on = "e", run = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, + { on = "E", run = "forward --far --end-of-word", desc = "Move forward to the end of the current or next WORD" }, + { on = "", run = "backward", desc = "Move back to the start of the current or previous word" }, + { on = "", run = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, # Line-wise movement - { on = "0", run = "move -999", desc = "Move to the BOL" }, - { on = "$", run = "move 999", desc = "Move to the EOL" }, - { on = "", run = "move -999", desc = "Move to the BOL" }, - { on = "", run = "move 999", desc = "Move to the EOL" }, - { on = "", run = "move -999", desc = "Move to the BOL" }, - { on = "", run = "move 999", desc = "Move to the EOL" }, + { on = "0", run = "move bol", desc = "Move to the BOL" }, + { on = "$", run = "move eol", desc = "Move to the EOL" }, + { on = "_", run = "move first-char", desc = "Move to the first non-whitespace character" }, + { on = "^", run = "move first-char", desc = "Move to the first non-whitespace character" }, + { on = "", run = "move bol", desc = "Move to the BOL" }, + { on = "", run = "move eol", desc = "Move to the EOL" }, + { on = "", run = "move bol", desc = "Move to the BOL" }, + { on = "", run = "move eol", desc = "Move to the EOL" }, # Delete { on = "", run = "backspace", desc = "Delete the character before the cursor" }, @@ -276,9 +282,9 @@ keymap = [ # Cut/Yank/Paste { on = "d", run = "delete --cut", desc = "Cut the selected characters" }, - { on = "D", run = [ "delete --cut", "move 999" ], desc = "Cut until the EOL" }, + { on = "D", run = [ "delete --cut", "move eol" ], desc = "Cut until the EOL" }, { on = "c", run = "delete --cut --insert", desc = "Cut the selected characters, and enter insert mode" }, - { on = "C", run = [ "delete --cut --insert", "move 999" ], desc = "Cut until the EOL, and enter insert mode" }, + { on = "C", run = [ "delete --cut --insert", "move eol" ], desc = "Cut until the EOL, and enter insert mode" }, { on = "x", run = [ "delete --cut", "move 1 --in-operating" ], desc = "Cut the current character" }, { on = "y", run = "yank", desc = "Copy the selected characters" }, { on = "p", run = "paste", desc = "Paste the copied characters after the cursor" }, diff --git a/yazi-core/src/input/commands/backspace.rs b/yazi-core/src/input/commands/backspace.rs index cc7d9162..c25b557a 100644 --- a/yazi-core/src/input/commands/backspace.rs +++ b/yazi-core/src/input/commands/backspace.rs @@ -17,7 +17,7 @@ impl From for Opt { impl Input { #[yazi_codegen::command] pub fn backspace(&mut self, opt: Opt) { - let snap = self.snaps.current_mut(); + let snap = self.snap_mut(); if !opt.under && snap.cursor < 1 { return; } else if opt.under && snap.cursor >= snap.count() { diff --git a/yazi-core/src/input/commands/backward.rs b/yazi-core/src/input/commands/backward.rs index 3a69c88b..35f3d65b 100644 --- a/yazi-core/src/input/commands/backward.rs +++ b/yazi-core/src/input/commands/backward.rs @@ -2,8 +2,17 @@ use yazi_shared::{CharKind, event::CmdCow}; use crate::input::Input; +struct Opt { + far: bool, +} + +impl From for Opt { + fn from(c: CmdCow) -> Self { Self { far: c.bool("far") } } +} + impl Input { - pub fn backward(&mut self, _: CmdCow) { + #[yazi_codegen::command] + pub fn backward(&mut self, opt: Opt) { let snap = self.snap(); if snap.cursor == 0 { return self.move_(0); @@ -13,11 +22,11 @@ impl Input { let mut it = snap.value[..idx].chars().rev().enumerate(); let mut prev = CharKind::new(it.next().unwrap().1); for (i, c) in it { - let c = CharKind::new(c); - if prev != CharKind::Space && prev != c { + let k = CharKind::new(c); + if prev != CharKind::Space && prev.vary(k, opt.far) { return self.move_(-(i as isize)); } - prev = c; + prev = k; } if prev != CharKind::Space { diff --git a/yazi-core/src/input/commands/complete.rs b/yazi-core/src/input/commands/complete.rs index 872867af..065557ab 100644 --- a/yazi-core/src/input/commands/complete.rs +++ b/yazi-core/src/input/commands/complete.rs @@ -39,7 +39,7 @@ impl Input { format!("{}{after}", opt.word).replace(SEPARATOR, MAIN_SEPARATOR_STR) }; - let snap = self.snaps.current_mut(); + let snap = self.snap_mut(); if new == snap.value { return; } diff --git a/yazi-core/src/input/commands/escape.rs b/yazi-core/src/input/commands/escape.rs index 6c8cbadb..72bb062d 100644 --- a/yazi-core/src/input/commands/escape.rs +++ b/yazi-core/src/input/commands/escape.rs @@ -32,6 +32,9 @@ impl Input { CompletionProxy::close(); } } + InputMode::Replace => { + snap.mode = InputMode::Normal; + } } self.snaps.tag(self.limit()); diff --git a/yazi-core/src/input/commands/forward.rs b/yazi-core/src/input/commands/forward.rs index f27b2520..97b08e43 100644 --- a/yazi-core/src/input/commands/forward.rs +++ b/yazi-core/src/input/commands/forward.rs @@ -3,11 +3,12 @@ use yazi_shared::{CharKind, event::CmdCow}; use crate::input::{Input, op::InputOp}; struct Opt { + far: bool, end_of_word: bool, } impl From for Opt { - fn from(c: CmdCow) -> Self { Self { end_of_word: c.bool("end-of-word") } } + fn from(c: CmdCow) -> Self { Self { far: c.bool("far"), end_of_word: c.bool("end-of-word") } } } impl Input { @@ -21,18 +22,18 @@ impl Input { }; for (i, c) in it { - let c = CharKind::new(c); + let k = CharKind::new(c); let b = if opt.end_of_word { - prev != CharKind::Space && prev != c && i != 1 + prev != CharKind::Space && prev.vary(k, opt.far) && i != 1 } else { - c != CharKind::Space && c != prev + k != CharKind::Space && k.vary(prev, opt.far) }; if b && !matches!(snap.op, InputOp::None | InputOp::Select(_)) { return self.move_(i as isize); } else if b { return self.move_(if opt.end_of_word { i - 1 } else { i } as isize); } - prev = c; + prev = k; } self.move_(snap.len() as isize) diff --git a/yazi-core/src/input/commands/mod.rs b/yazi-core/src/input/commands/mod.rs index 6648217c..87017d3f 100644 --- a/yazi-core/src/input/commands/mod.rs +++ b/yazi-core/src/input/commands/mod.rs @@ -1,4 +1 @@ -yazi_macro::mod_flat!( - backspace backward close complete delete escape forward insert kill move_ paste redo - show type_ undo visual yank -); +yazi_macro::mod_flat!(backspace backward close complete delete escape forward insert kill move_ paste redo replace show type_ undo visual yank); diff --git a/yazi-core/src/input/commands/move_.rs b/yazi-core/src/input/commands/move_.rs index 66331936..10cc1e78 100644 --- a/yazi-core/src/input/commands/move_.rs +++ b/yazi-core/src/input/commands/move_.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use unicode_width::UnicodeWidthStr; use yazi_macro::render; use yazi_shared::event::{CmdCow, Data}; @@ -5,20 +7,20 @@ use yazi_shared::event::{CmdCow, Data}; use crate::input::{Input, op::InputOp, snap::InputSnap}; struct Opt { - step: isize, + step: OptStep, in_operating: bool, } impl From for Opt { fn from(c: CmdCow) -> Self { Self { - step: c.first().and_then(Data::as_isize).unwrap_or(0), + step: c.first().and_then(|d| d.try_into().ok()).unwrap_or_default(), in_operating: c.bool("in-operating"), } } } impl From for Opt { - fn from(step: isize) -> Self { Self { step, in_operating: false } } + fn from(step: isize) -> Self { Self { step: step.into(), in_operating: false } } } impl Input { @@ -29,14 +31,7 @@ impl Input { return; } - render!(self.handle_op( - if opt.step <= 0 { - snap.cursor.saturating_sub(opt.step.unsigned_abs()) - } else { - snap.count().min(snap.cursor + opt.step as usize) - }, - false, - )); + render!(self.handle_op(opt.step.cursor(snap), false)); let (limit, snap) = (self.limit(), self.snap_mut()); if snap.offset > snap.cursor { @@ -53,3 +48,58 @@ impl Input { } } } + +// --- Step +enum OptStep { + Offset(isize), + Bol, + Eol, + FirstChar, +} + +impl OptStep { + fn cursor(self, snap: &InputSnap) -> usize { + match self { + Self::Offset(n) if n <= 0 => snap.cursor.saturating_add_signed(n), + Self::Offset(n) => snap.count().min(snap.cursor + n as usize), + Self::Bol => 0, + Self::Eol => snap.count(), + Self::FirstChar => { + snap.value.chars().enumerate().find(|(_, c)| !c.is_whitespace()).map_or(0, |(i, _)| i) + } + } + } +} + +impl Default for OptStep { + fn default() -> Self { 0.into() } +} + +impl FromStr for OptStep { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "bol" => Self::Bol, + "eol" => Self::Eol, + "first-char" => Self::FirstChar, + s => Self::Offset(s.parse().map_err(|_| ())?), + }) + } +} + +impl From for OptStep { + fn from(value: isize) -> Self { Self::Offset(value) } +} + +impl TryFrom<&Data> for OptStep { + type Error = (); + + fn try_from(value: &Data) -> Result { + match value { + Data::String(s) => s.parse().map_err(|_| ()), + Data::Integer(i) => Ok(Self::from(*i as isize)), + _ => Err(()), + } + } +} diff --git a/yazi-core/src/input/commands/replace.rs b/yazi-core/src/input/commands/replace.rs new file mode 100644 index 00000000..f028ebb2 --- /dev/null +++ b/yazi-core/src/input/commands/replace.rs @@ -0,0 +1,32 @@ +use yazi_macro::render; +use yazi_shared::event::CmdCow; + +use crate::input::{Input, InputMode, op::InputOp}; + +impl Input { + #[yazi_codegen::command] + pub fn replace(&mut self, _: CmdCow) { + let snap = self.snap_mut(); + if snap.mode == InputMode::Normal { + snap.op = InputOp::None; + snap.mode = InputMode::Replace; + render!(); + } + } + + pub fn replace_str(&mut self, s: &str) { + let snap = self.snap_mut(); + snap.mode = InputMode::Normal; + + let start = snap.idx(snap.cursor).unwrap(); + let mut it = snap.value[start..].char_indices(); + match (it.next(), it.next()) { + (None, _) => {} + (Some(_), None) => snap.value.replace_range(start..snap.len(), s), + (Some(_), Some((len, _))) => snap.value.replace_range(start..start + len, s), + } + + render!(); + self.snaps.tag(self.limit()).then(|| self.flush_value()); + } +} diff --git a/yazi-core/src/input/commands/show.rs b/yazi-core/src/input/commands/show.rs index b3128fe7..ac4e3879 100644 --- a/yazi-core/src/input/commands/show.rs +++ b/yazi-core/src/input/commands/show.rs @@ -40,7 +40,7 @@ impl Input { // Set cursor after reset if let Some(cursor) = opt.cfg.cursor { - self.snaps.current_mut().cursor = cursor; + self.snap_mut().cursor = cursor; self.move_(0); } diff --git a/yazi-core/src/input/commands/type_.rs b/yazi-core/src/input/commands/type_.rs index 72488998..09d59622 100644 --- a/yazi-core/src/input/commands/type_.rs +++ b/yazi-core/src/input/commands/type_.rs @@ -1,4 +1,5 @@ use yazi_config::keymap::Key; +use yazi_macro::render; use yazi_shared::event::CmdCow; use crate::input::{Input, InputMode}; @@ -11,15 +12,29 @@ impl From for Opt { impl Input { pub fn type_(&mut self, key: &Key) -> bool { - if self.mode() != InputMode::Insert { - return false; - } + let Some(c) = key.plain() else { return false }; - if let Some(c) = key.plain() { + if self.mode() == InputMode::Insert { self.type_str(c.encode_utf8(&mut [0; 4])); return true; + } else if self.mode() == InputMode::Replace { + self.replace_str(c.encode_utf8(&mut [0; 4])); + return true; } false } + + pub fn type_str(&mut self, s: &str) { + let snap = self.snap_mut(); + if snap.cursor < 1 { + snap.value.insert_str(0, s); + } else { + snap.value.insert_str(snap.idx(snap.cursor).unwrap(), s); + } + + self.move_(s.chars().count() as isize); + self.flush_value(); + render!(); + } } diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 59486442..27dff313 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -3,7 +3,6 @@ use std::ops::Range; use tokio::sync::mpsc::UnboundedSender; use unicode_width::UnicodeWidthStr; use yazi_config::{INPUT, popup::Position}; -use yazi_macro::render; use yazi_plugin::CLIPBOARD; use yazi_shared::errors::InputError; @@ -33,22 +32,9 @@ impl Input { self.position.offset.width.saturating_sub(INPUT.border()) as usize } - pub fn type_str(&mut self, s: &str) { - let snap = self.snaps.current_mut(); - if snap.cursor < 1 { - snap.value.insert_str(0, s); - } else { - snap.value.insert_str(snap.idx(snap.cursor).unwrap(), s); - } - - self.move_(s.chars().count() as isize); - self.flush_value(); - render!(); - } - pub(super) fn handle_op(&mut self, cursor: usize, include: bool) -> bool { let old = self.snap().clone(); - let snap = self.snaps.current_mut(); + let snap = self.snap_mut(); match snap.op { InputOp::None | InputOp::Select(_) => { diff --git a/yazi-core/src/input/mode.rs b/yazi-core/src/input/mode.rs index 4668143b..67fa7458 100644 --- a/yazi-core/src/input/mode.rs +++ b/yazi-core/src/input/mode.rs @@ -3,6 +3,7 @@ pub enum InputMode { Normal, #[default] Insert, + Replace, } impl InputMode { diff --git a/yazi-fm/src/app/app.rs b/yazi-fm/src/app/app.rs index 1d08e5aa..2a8fb106 100644 --- a/yazi-fm/src/app/app.rs +++ b/yazi-fm/src/app/app.rs @@ -92,6 +92,8 @@ impl App { let input = &mut self.cx.input; if input.mode() == InputMode::Insert { input.type_str(&str); + } else if input.mode() == InputMode::Replace { + input.replace_str(&str); } } } diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 41a34f6b..769a4409 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -259,6 +259,7 @@ impl<'a> Executor<'a> { InputMode::Normal => { on!(insert); on!(visual); + on!(replace); on!(delete); on!(yank); @@ -279,6 +280,7 @@ impl<'a> Executor<'a> { on!(backspace); on!(kill); } + InputMode::Replace => {} } } diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index eb17a3b1..1174e7ab 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -59,6 +59,7 @@ impl Widget for Input<'_> { _ = match input.mode() { InputMode::Insert => Term::set_cursor_bar(), + InputMode::Replace => Term::set_cursor_underscore(), _ => Term::set_cursor_block(), }; } diff --git a/yazi-fm/src/term.rs b/yazi-fm/src/term.rs index 8598ad61..b5050855 100644 --- a/yazi-fm/src/term.rs +++ b/yazi-fm/src/term.rs @@ -159,6 +159,16 @@ impl Term { queue!(stderr(), SetCursorStyle::SteadyBar)? }) } + + #[inline] + pub(super) fn set_cursor_underscore() -> Result<()> { + use crossterm::cursor::SetCursorStyle; + Ok(if INPUT.cursor_blink { + queue!(stderr(), SetCursorStyle::BlinkingUnderScore)? + } else { + queue!(stderr(), SetCursorStyle::SteadyUnderScore)? + }) + } } impl Drop for Term { diff --git a/yazi-shared/src/chars.rs b/yazi-shared/src/chars.rs index 491389c2..e234b03c 100644 --- a/yazi-shared/src/chars.rs +++ b/yazi-shared/src/chars.rs @@ -20,6 +20,10 @@ impl CharKind { Self::Other } } + + pub fn vary(self, other: Self, far: bool) -> bool { + if far { (self == Self::Space) != (other == Self::Space) } else { self != other } + } } pub fn strip_trailing_newline(mut s: String) -> String {