feat: replace mode and Vim motions (W, E, B, ^, _) for inputs (#2143)

Co-authored-by: 三咲雅 · Misaki Masa <sxyazi@gmail.com>
This commit is contained in:
darcy 2025-01-08 21:38:21 +11:00 committed by GitHub
parent 2eed881f12
commit ba62b407bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 184 additions and 65 deletions

View file

@ -232,12 +232,13 @@ keymap = [
{ on = "<C-[>", 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 = "<C-f>", 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 = "<A-b>", run = "backward", desc = "Move back to the start of the current or previous word" },
{ on = "<A-f>", 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 = "<A-b>", run = "backward", desc = "Move back to the start of the current or previous word" },
{ on = "<A-f>", 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 = "<C-a>", run = "move -999", desc = "Move to the BOL" },
{ on = "<C-e>", run = "move 999", desc = "Move to the EOL" },
{ on = "<Home>", run = "move -999", desc = "Move to the BOL" },
{ on = "<End>", 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 = "<C-a>", run = "move bol", desc = "Move to the BOL" },
{ on = "<C-e>", run = "move eol", desc = "Move to the EOL" },
{ on = "<Home>", run = "move bol", desc = "Move to the BOL" },
{ on = "<End>", run = "move eol", desc = "Move to the EOL" },
# Delete
{ on = "<Backspace>", 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" },

View file

@ -17,7 +17,7 @@ impl From<bool> 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() {

View file

@ -2,8 +2,17 @@ use yazi_shared::{CharKind, event::CmdCow};
use crate::input::Input;
struct Opt {
far: bool,
}
impl From<CmdCow> 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 {

View file

@ -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;
}

View file

@ -32,6 +32,9 @@ impl Input {
CompletionProxy::close();
}
}
InputMode::Replace => {
snap.mode = InputMode::Normal;
}
}
self.snaps.tag(self.limit());

View file

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

View file

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

View file

@ -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<CmdCow> 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<isize> 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<Self, Self::Err> {
Ok(match s {
"bol" => Self::Bol,
"eol" => Self::Eol,
"first-char" => Self::FirstChar,
s => Self::Offset(s.parse().map_err(|_| ())?),
})
}
}
impl From<isize> for OptStep {
fn from(value: isize) -> Self { Self::Offset(value) }
}
impl TryFrom<&Data> for OptStep {
type Error = ();
fn try_from(value: &Data) -> Result<Self, Self::Error> {
match value {
Data::String(s) => s.parse().map_err(|_| ()),
Data::Integer(i) => Ok(Self::from(*i as isize)),
_ => Err(()),
}
}
}

View file

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

View file

@ -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);
}

View file

@ -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<CmdCow> 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!();
}
}

View file

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

View file

@ -3,6 +3,7 @@ pub enum InputMode {
Normal,
#[default]
Insert,
Replace,
}
impl InputMode {

View file

@ -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);
}
}
}

View file

@ -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 => {}
}
}

View file

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

View file

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

View file

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