diff --git a/yazi-config/preset/keymap.toml b/yazi-config/preset/keymap.toml index 6ace5ff2..2c9e5b8e 100644 --- a/yazi-config/preset/keymap.toml +++ b/yazi-config/preset/keymap.toml @@ -171,40 +171,56 @@ keymap = [ [input] keymap = [ - { on = [ "" ], exec = "close", desc = "Cancel input" }, - { on = [ "" ], exec = "close --submit", desc = "Submit the input" }, - { on = [ "" ], exec = "escape", desc = "Go back the normal mode, or cancel input" }, + { on = [ "" ], exec = "close", desc = "Cancel input" }, + { on = [ "" ], exec = "close --submit", desc = "Submit the input" }, + { on = [ "" ], exec = "escape", desc = "Go back the normal mode, or cancel input" }, # Mode { on = [ "i" ], exec = "insert", desc = "Enter insert mode" }, { on = [ "a" ], exec = "insert --append", desc = "Enter append mode" }, + { on = [ "I" ], exec = [ "move -999", "insert" ], desc = "Move to the BOL, and enter insert mode" }, + { on = [ "A" ], exec = [ "move 999", "insert --append" ], desc = "Move to the EOL, and enter append mode" }, { on = [ "v" ], exec = "visual", desc = "Enter visual mode" }, { on = [ "V" ], exec = [ "move -999", "visual", "move 999" ], desc = "Enter visual mode and select all" }, - # Navigation - { on = [ "h" ], exec = "move -1", desc = "Move cursor left" }, - { on = [ "l" ], exec = "move 1", desc = "Move cursor right" }, + # Character-wise movement + { on = [ "h" ], exec = "move -1", desc = "Move back a character" }, + { on = [ "l" ], exec = "move 1", desc = "Move forward a character" }, + { on = [ "" ], exec = "move -1", desc = "Move back a character" }, + { on = [ "" ], exec = "move 1", desc = "Move forward a character" }, + { on = [ "" ], exec = "move -1", desc = "Move back a character" }, + { on = [ "" ], exec = "move 1", desc = "Move forward a character" }, - { on = [ "0" ], exec = "move -999", desc = "Move to the BOL" }, - { on = [ "$" ], exec = "move 999", desc = "Move to the EOL" }, - { on = [ "I" ], exec = [ "move -999", "insert" ], desc = "Move to the BOL, and enter insert mode" }, - { on = [ "A" ], exec = [ "move 999", "insert --append" ], desc = "Move to the EOL, and enter append mode" }, + # Word-wise movement + { on = [ "b" ], exec = "backward", desc = "Move back to the start of the current or previous word" }, + { on = [ "w" ], exec = "forward", desc = "Move forward to the start of the next word" }, + { on = [ "e" ], exec = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, + { on = [ "" ], exec = "backward", desc = "Move back to the start of the current or previous word" }, + { on = [ "" ], exec = "forward --end-of-word", desc = "Move forward to the end of the current or next word" }, - { on = [ "" ], exec = "move -1", desc = "Move cursor left" }, - { on = [ "" ], exec = "move 1", desc = "Move cursor right" }, + # Line-wise movement + { on = [ "0" ], exec = "move -999", desc = "Move to the BOL" }, + { on = [ "$" ], exec = "move 999", desc = "Move to the EOL" }, + { on = [ "" ], exec = "move -999", desc = "Move to the BOL" }, + { on = [ "" ], exec = "move 999", desc = "Move to the EOL" }, - { on = [ "b" ], exec = "backward", desc = "Move to the beginning of the previous word" }, - { on = [ "w" ], exec = "forward", desc = "Move to the beginning of the next word" }, - { on = [ "e" ], exec = "forward --end-of-word", desc = "Move to the end of the next word" }, + # Delete + { on = [ "" ], exec = "backspace", desc = "Delete the character before the cursor" }, + { on = [ "" ], exec = "backspace", desc = "Delete the character before the cursor" }, + { on = [ "" ], exec = "backspace --under", desc = "Delete the character under the cursor" }, - # Deletion + # Kill + { on = [ "" ], exec = "kill bol", desc = "Kill backwards to the BOL" }, + { on = [ "" ], exec = "kill eol", desc = "Kill forwards to the EOL" }, + { on = [ "" ], exec = "kill backward", desc = "Kill backwards to the start of the current word" }, + { on = [ "" ], exec = "kill forward", desc = "Kill forwards to the end of the current word" }, + + # Cut/Yank/Paste { on = [ "d" ], exec = "delete --cut", desc = "Cut the selected characters" }, { on = [ "D" ], exec = [ "delete --cut", "move 999" ], desc = "Cut until the EOL" }, { on = [ "c" ], exec = "delete --cut --insert", desc = "Cut the selected characters, and enter insert mode" }, { on = [ "C" ], exec = [ "delete --cut --insert", "move 999" ], desc = "Cut until the EOL, and enter insert mode" }, { on = [ "x" ], exec = [ "delete --cut", "move 1 --in-operating" ], desc = "Cut the current character" }, - - # Yank/Paste { on = [ "y" ], exec = "yank", desc = "Copy the selected characters" }, { on = [ "p" ], exec = "paste", desc = "Paste the copied characters after the cursor" }, { on = [ "P" ], exec = "paste --before", desc = "Paste the copied characters before the cursor" }, diff --git a/yazi-config/src/popup/offset.rs b/yazi-config/src/popup/offset.rs index d8524321..1a36860b 100644 --- a/yazi-config/src/popup/offset.rs +++ b/yazi-config/src/popup/offset.rs @@ -32,3 +32,8 @@ impl TryFrom> for Offset { }) } } + +impl Offset { + #[inline] + pub fn line() -> Self { Self { x: 0, y: 0, width: u16::MAX, height: 1 } } +} diff --git a/yazi-core/src/help/commands/filter.rs b/yazi-core/src/help/commands/filter.rs index 54865299..0721bd43 100644 --- a/yazi-core/src/help/commands/filter.rs +++ b/yazi-core/src/help/commands/filter.rs @@ -1,6 +1,6 @@ -use yazi_config::keymap::Exec; +use yazi_config::{keymap::Exec, popup::{Offset, Origin, Position}}; -use crate::help::Help; +use crate::{help::Help, input::Input}; pub struct Opt; @@ -10,7 +10,10 @@ impl From<&Exec> for Opt { impl Help { pub fn filter(&mut self, _: impl Into) -> bool { - self.in_filter = Some(Default::default()); + let mut input = Input::default(); + input.position = Position::new(Origin::BottomLeft, Offset::line()); + + self.in_filter = Some(input); self.filter_apply(); true } diff --git a/yazi-core/src/help/help.rs b/yazi-core/src/help/help.rs index 97696135..a50f8641 100644 --- a/yazi-core/src/help/help.rs +++ b/yazi-core/src/help/help.rs @@ -1,3 +1,4 @@ +use crossterm::event::KeyCode; use unicode_width::UnicodeWidthStr; use yazi_config::{keymap::{Control, Key, KeymapLayer}, KEYMAP}; use yazi_shared::Term; @@ -65,11 +66,14 @@ impl Help { return true; } - if input.type_(key) { - return self.filter_apply(); - } + let b = match &key { + Key { code: KeyCode::Backspace, shift: false, ctrl: false, alt: false } => { + input.backspace(false) + } + _ => input.type_(key), + }; - false + if b { self.filter_apply() } else { false } } } diff --git a/yazi-core/src/input/commands/backspace.rs b/yazi-core/src/input/commands/backspace.rs new file mode 100644 index 00000000..8828796a --- /dev/null +++ b/yazi-core/src/input/commands/backspace.rs @@ -0,0 +1,38 @@ +use yazi_config::keymap::Exec; + +use crate::input::Input; + +pub struct Opt { + under: bool, +} + +impl From<&Exec> for Opt { + fn from(e: &Exec) -> Self { Self { under: e.named.contains_key("under") } } +} +impl From for Opt { + fn from(under: bool) -> Self { Self { under } } +} + +impl Input { + pub fn backspace(&mut self, opt: impl Into) -> bool { + let opt = opt.into() as Opt; + let snap = self.snaps.current_mut(); + + if !opt.under && snap.cursor < 1 { + return false; + } else if opt.under && snap.cursor >= snap.value.len() { + return false; + } + + if opt.under { + snap.value.remove(snap.idx(snap.cursor).unwrap()); + self.move_(0); + } else { + snap.value.remove(snap.idx(snap.cursor - 1).unwrap()); + self.move_(-1); + } + + self.flush_value(); + true + } +} diff --git a/yazi-core/src/input/commands/forward.rs b/yazi-core/src/input/commands/forward.rs index 55cd685e..523f1f63 100644 --- a/yazi-core/src/input/commands/forward.rs +++ b/yazi-core/src/input/commands/forward.rs @@ -14,14 +14,13 @@ impl From<&Exec> for Opt { impl Input { pub fn forward(&mut self, opt: impl Into) -> bool { let opt = opt.into() as Opt; - let snap = self.snap(); - if snap.value.is_empty() { - return self.move_(0); - } let mut it = snap.value.chars().skip(snap.cursor).enumerate(); - let mut prev = CharKind::new(it.next().unwrap().1); + let Some(mut prev) = it.next().map(|(_, c)| CharKind::new(c)) else { + return self.move_(0); + }; + for (i, c) in it { let c = CharKind::new(c); let b = if opt.end_of_word { diff --git a/yazi-core/src/input/commands/kill.rs b/yazi-core/src/input/commands/kill.rs new file mode 100644 index 00000000..918943a9 --- /dev/null +++ b/yazi-core/src/input/commands/kill.rs @@ -0,0 +1,96 @@ +use std::ops::RangeBounds; + +use yazi_config::keymap::Exec; +use yazi_shared::CharKind; + +use crate::input::Input; + +pub struct Opt<'a> { + kind: &'a str, +} + +impl<'a> From<&'a Exec> for Opt<'a> { + fn from(e: &'a Exec) -> Self { + Self { kind: e.args.first().map(|s| s.as_str()).unwrap_or_default() } + } +} + +impl Input { + fn kill_range(&mut self, range: impl RangeBounds) -> bool { + let snap = self.snap_mut(); + snap.cursor = match range.start_bound() { + std::ops::Bound::Included(i) => *i, + std::ops::Bound::Excluded(_) => unreachable!(), + std::ops::Bound::Unbounded => 0, + }; + if snap.value.drain(range).next().is_none() { + return false; + } + + self.move_(0); + self.flush_value(); + true + } + + /// Searches for a word boundary and returns the movement in the cursor + /// position. + /// + /// A word boundary is where the [`CharKind`] changes. + /// + /// If `skip_whitespace_first` is true, we skip initial whitespace. + /// Otherwise, we skip whitespace after reaching a word boundary. + /// + /// If `stop_before_boundary` is true, returns how many characters the cursor + /// needs to move to be at the character *BEFORE* the word boundary, or until + /// the end of the iterator. + /// + /// Otherwise, returns how many characters to move to reach right *AFTER* the + /// word boundary, or the end of the iterator. + fn find_word_boundary(input: impl Iterator + Clone) -> usize { + fn count_spaces(input: impl Iterator) -> usize { + // Move until we don't see any more whitespace. + input.take_while(|&c| CharKind::new(c) == CharKind::Space).count() + } + + fn count_characters(mut input: impl Iterator) -> usize { + // Determine the current character class. + let first = match input.next() { + Some(c) => CharKind::new(c), + None => return 0, + }; + + // Move until we see a different character class or the end of the iterator. + input.take_while(|&c| CharKind::new(c) == first).count() + 1 + } + + let spaces = count_spaces(input.clone()); + spaces + count_characters(input.skip(spaces)) + } + + pub fn kill<'a>(&mut self, opt: impl Into>) -> bool { + let opt = opt.into() as Opt; + let snap = self.snap_mut(); + + match opt.kind.as_bytes() { + b"bol" => { + let end = snap.idx(snap.cursor).unwrap_or(snap.len()); + self.kill_range(..end) + } + b"eol" => { + let start = snap.idx(snap.cursor).unwrap_or(snap.len()); + self.kill_range(start..) + } + b"backward" => { + let end = snap.idx(snap.cursor).unwrap_or(snap.len()); + let start = end - Self::find_word_boundary(snap.value[..end].chars().rev()); + self.kill_range(start..end) + } + b"forward" => { + let start = snap.idx(snap.cursor).unwrap_or(snap.len()); + let end = start + Self::find_word_boundary(snap.value[start..].chars()); + self.kill_range(start..end) + } + _ => false, + } + } +} diff --git a/yazi-core/src/input/commands/mod.rs b/yazi-core/src/input/commands/mod.rs index feca8424..9ad96381 100644 --- a/yazi-core/src/input/commands/mod.rs +++ b/yazi-core/src/input/commands/mod.rs @@ -1,3 +1,4 @@ +mod backspace; mod backward; mod close; mod complete; @@ -5,6 +6,7 @@ mod delete; mod escape; mod forward; mod insert; +mod kill; mod move_; mod paste; mod redo; diff --git a/yazi-core/src/input/commands/type_.rs b/yazi-core/src/input/commands/type_.rs index 44df4036..db9296df 100644 --- a/yazi-core/src/input/commands/type_.rs +++ b/yazi-core/src/input/commands/type_.rs @@ -1,8 +1,4 @@ -use std::ops::RangeBounds; - -use crossterm::event::KeyCode; use yazi_config::keymap::{Exec, Key}; -use yazi_shared::CharKind; use crate::input::{Input, InputMode}; @@ -13,77 +9,6 @@ impl From<&Exec> for Opt { } impl Input { - /// Searches for a word boundary and returns the movement in the cursor - /// position. - /// - /// A word boundary is where the [`CharKind`] changes. - /// - /// If `skip_whitespace_first` is true, we skip initial whitespace. - /// Otherwise, we skip whitespace after reaching a word boundary. - /// - /// If `stop_before_boundary` is true, returns how many characters the cursor - /// needs to move to be at the character *BEFORE* the word boundary, or until - /// the end of the iterator. - /// - /// Otherwise, returns how many characters to move to reach right *AFTER* the - /// word boundary, or the end of the iterator. - fn find_word_boundary(input: impl Iterator + Clone) -> usize { - fn count_spaces(input: impl Iterator) -> usize { - // Move until we don't see any more whitespace. - input.take_while(|&c| CharKind::new(c) == CharKind::Space).count() - } - - fn count_characters(mut input: impl Iterator) -> usize { - // Determine the current character class. - let first = match input.next() { - Some(c) => CharKind::new(c), - None => return 0, - }; - - // Move until we see a different character class or the end of the iterator. - input.take_while(|&c| CharKind::new(c) == first).count() + 1 - } - - let spaces = count_spaces(input.clone()); - spaces + count_characters(input.skip(spaces)) - } - - fn delete_range(&mut self, range: impl RangeBounds) -> bool { - let snap = self.snap_mut(); - snap.cursor = match range.start_bound() { - std::ops::Bound::Included(i) => *i, - std::ops::Bound::Excluded(_) => unreachable!(), - std::ops::Bound::Unbounded => 0, - }; - if snap.value.drain(range).next().is_none() { - return false; - } - - self.move_(0); - self.flush_value(); - true - } - - fn backspace(&mut self, under: bool) -> bool { - let snap = self.snaps.current_mut(); - if !under && snap.cursor < 1 { - return false; - } else if under && snap.cursor >= snap.value.len() { - return false; - } - - if under { - snap.value.remove(snap.idx(snap.cursor).unwrap()); - self.move_(0); - } else { - snap.value.remove(snap.idx(snap.cursor - 1).unwrap()); - self.move_(-1); - } - - self.flush_value(); - true - } - pub fn type_(&mut self, key: &Key) -> bool { if self.mode() != InputMode::Insert { return false; @@ -93,71 +18,6 @@ impl Input { let mut bits = [0; 4]; return self.type_str(c.encode_utf8(&mut bits)); } - - use KeyCode::{Backspace, Char as C}; - - match key { - // Move to the start of the line - Key { code: C('a'), shift: false, ctrl: true, alt: false } => self.move_(isize::MIN), - // Move to the end of the line - Key { code: C('e'), shift: false, ctrl: true, alt: false } => self.move_(isize::MAX), - - // Move back a character - Key { code: C('b'), shift: false, ctrl: true, alt: false } => self.move_(-1), - // Move forward a character - Key { code: C('f'), shift: false, ctrl: true, alt: false } => self.move_(1), - - // Delete the character before the cursor - Key { code: Backspace, shift: false, ctrl: false, alt: false } => self.backspace(false), - Key { code: C('h'), shift: false, ctrl: true, alt: false } => self.backspace(false), - // Delete the character under the cursor - Key { code: C('d'), shift: false, ctrl: true, alt: false } => self.backspace(true), - - // Move back to the start of the current or previous word - Key { code: C('b'), shift: false, ctrl: false, alt: true } => { - let snap = self.snap(); - let idx = snap.idx(snap.cursor).unwrap_or(snap.len()); - - let step = Self::find_word_boundary(snap.value[..idx].chars().rev()); - self.move_(-(step as isize)) - } - // Move forward to the end of the next word - Key { code: C('f'), shift: false, ctrl: false, alt: true } => { - let snap = self.snap(); - let step = Self::find_word_boundary(snap.value.chars().skip(snap.cursor)); - self.move_(step as isize) - } - - // Kill backwards to the start of the line - Key { code: C('u'), shift: false, ctrl: true, alt: false } => { - let snap = self.snap_mut(); - let end = snap.idx(snap.cursor).unwrap_or(snap.len()); - self.delete_range(..end) - } - // Kill forwards to the end of the line - Key { code: C('k'), shift: false, ctrl: true, alt: false } => { - let snap = self.snap_mut(); - let start = snap.idx(snap.cursor).unwrap_or(snap.len()); - self.delete_range(start..) - } - - // Kill backwards to the start of the current word - Key { code: C('w'), shift: false, ctrl: true, alt: false } - | Key { code: Backspace, shift: false, ctrl: false, alt: true } => { - let snap = self.snap_mut(); - let end = snap.idx(snap.cursor).unwrap_or(snap.len()); - let start = end - Self::find_word_boundary(snap.value[..end].chars().rev()); - self.delete_range(start..end) - } - // Kill forwards to the end of the current word - Key { code: C('d'), shift: false, ctrl: false, alt: true } => { - let snap = self.snap_mut(); - let start = snap.idx(snap.cursor).unwrap_or(snap.len()); - let end = start + Self::find_word_boundary(snap.value[start..].chars()); - self.delete_range(start..end) - } - - _ => false, - } + false } } diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index d39e5962..c2d3fad2 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -13,10 +13,10 @@ impl<'a> Executor<'a> { if self.cx.which.visible { return self.cx.which.press(key); } - if self.cx.input.visible && self.cx.input.type_(&key) { + if self.cx.help.visible && self.cx.help.type_(&key) { return true; } - if self.cx.help.visible && self.cx.help.type_(&key) { + if self.cx.input.visible && self.cx.input.type_(&key) { return true; } @@ -212,6 +212,8 @@ impl<'a> Executor<'a> { on!(close); on!(escape); on!(move_, "move"); + on!(backward); + on!(forward); if exec.cmd.as_str() == "complete" { return if exec.named.contains_key("trigger") { @@ -226,10 +228,7 @@ impl<'a> Executor<'a> { on!(insert); on!(visual); - on!(backward); - on!(forward); on!(delete); - on!(yank); on!(paste); @@ -241,7 +240,12 @@ impl<'a> Executor<'a> { _ => false, } } - InputMode::Insert => false, + InputMode::Insert => { + on!(backspace); + on!(kill); + + false + } } }