diff --git a/doc_src/cmds/bind.rst b/doc_src/cmds/bind.rst index 8d945e44f..9632a789d 100644 --- a/doc_src/cmds/bind.rst +++ b/doc_src/cmds/bind.rst @@ -401,6 +401,13 @@ The following special input functions are available: ``self-insert-notfirst`` inserts the matching sequence into the command line, unless the cursor is at the beginning +``get-key`` + sets :envvar:`fish_key` to the key that was pressed to trigger this binding. Example use:: + + for i in (seq 0 9) + bind $i get-key 'commandline -i "#$fish_key"' 'set -eg fish_key' + end + ``suppress-autosuggestion`` remove the current autosuggestion. Returns true if there was a suggestion to remove. diff --git a/doc_src/cmds/fish_mode_prompt.rst b/doc_src/cmds/fish_mode_prompt.rst index 37dce5f00..89dc74a58 100644 --- a/doc_src/cmds/fish_mode_prompt.rst +++ b/doc_src/cmds/fish_mode_prompt.rst @@ -55,7 +55,7 @@ Example case visual set_color --bold brmagenta echo 'V' - case operator + case operator f F t T set_color --bold cyan echo 'N' case '*' diff --git a/share/functions/fish_default_mode_prompt.fish b/share/functions/fish_default_mode_prompt.fish index e607dc0c1..17656729a 100644 --- a/share/functions/fish_default_mode_prompt.fish +++ b/share/functions/fish_default_mode_prompt.fish @@ -18,7 +18,7 @@ function fish_default_mode_prompt --description "Display vi prompt mode" case visual set_color --bold magenta echo '[V]' - case operator + case operator f F t T set_color --bold cyan echo '[N]' end diff --git a/share/functions/fish_vi_key_bindings.fish b/share/functions/fish_vi_key_bindings.fish index 3181638b4..a56436424 100644 --- a/share/functions/fish_vi_key_bindings.fish +++ b/share/functions/fish_vi_key_bindings.fish @@ -53,7 +53,7 @@ function fish_vi_yank_selection end function fish_vi_exec_motion - argparse linewise -- $argv + argparse --stop-nonopt linewise -- $argv or return set -l motion $argv @@ -83,7 +83,7 @@ function fish_vi_exec_motion else set -l use_selection true set -l swap_case_hack - switch $motion + switch $motion[1] case forward-word-vi forward-bigword-vi if test $__fish_vi_operator = swap-case set swap_case_hack (string replace -r -- '^forward-((?:big)?word)-vi$' '$1' $motion) @@ -93,50 +93,62 @@ function fish_vi_exec_motion set motion (string replace -- forward kill $motion) end end + switch $motion[1] + case commandline + case '*' + set motion commandline -f $motion + end if $use_selection commandline -f begin-selection else commandline -f begin-undo-group end + set -l ok true switch $__fish_vi_operator case delete for i in (seq $total) - commandline -f $motion + $motion || { set ok false; break } end - if $use_selection + if $ok && $use_selection commandline -f kill-selection end case change for i in (seq $total) - commandline -f $motion + $motion || { set ok false; break } end - if $use_selection - commandline -f kill-selection + if $ok + if $use_selection + commandline -f kill-selection + end + set fish_bind_mode insert end - set fish_bind_mode insert case yank for i in (seq $total) - commandline -f $motion + $motion || { set ok false; break } end - if $use_selection - fish_vi_yank_selection - else - commandline -f yank + if $ok + if $use_selection + fish_vi_yank_selection + else + commandline -f yank + end end case swap-case for i in (seq $total) - commandline -f $motion + $motion || { set ok false; break } end - if set -q swap_case_hack[1] - set -l word $swap_case_hack - commandline -f \ - backward-$word \ - forward-$word-end \ - togglecase-selection \ - backward-$word \ - forward-$word-vi - else - commandline -f togglecase-selection + if $ok + if set -q swap_case_hack[1] + set -l word $swap_case_hack + commandline -f \ + backward-$word \ + forward-$word-end \ + togglecase-selection \ + backward-$word \ + forward-$word-vi + else + commandline -f togglecase-selection + end end end if $use_selection @@ -400,10 +412,16 @@ function fish_vi_key_bindings --description 'vi-like key bindings for fish' bind --preset -M operator \^ 'fish_vi_exec_motion beginning-of-line' bind --preset -M operator \$ 'fish_vi_exec_motion end-of-line' - bind --preset -M operator f 'fish_vi_exec_motion forward-jump' - bind --preset -M operator F 'fish_vi_exec_motion backward-jump' - bind --preset -M operator t 'fish_vi_exec_motion forward-jump-till' - bind --preset -M operator T 'fish_vi_exec_motion backward-jump-till' + bind --preset -M operator f --sets-mode f '' + bind --preset -M operator F --sets-mode F '' + bind --preset -M operator t --sets-mode t '' + bind --preset -M operator T --sets-mode T '' + + bind --preset -M f '' get-key 'fish_vi_exec_motion commandline --forward-jump=$fish_key' 'set -eg fish_key' + bind --preset -M F '' get-key 'fish_vi_exec_motion commandline --backward-jump=$fish_key' 'set -eg fish_key' + bind --preset -M t '' get-key 'fish_vi_exec_motion commandline --forward-jump-till=$fish_key' 'set -eg fish_key' + bind --preset -M T '' get-key 'fish_vi_exec_motion commandline --backward-jump-till=$fish_key' 'set -eg fish_key' + bind --preset -M operator ';' 'fish_vi_exec_motion repeat-jump' bind --preset -M operator , 'fish_vi_exec_motion repeat-jump-reverse' diff --git a/share/prompts/nim.fish b/share/prompts/nim.fish index b4d1ffec0..c193fa96f 100644 --- a/share/prompts/nim.fish +++ b/share/prompts/nim.fish @@ -86,7 +86,7 @@ function fish_prompt switch $fish_bind_mode case default set mode (set_color --bold red)N - case operator + case operator f F t T set mode (set_color --bold cyan)N case insert set mode (set_color --bold green)I diff --git a/src/builtins/commandline.rs b/src/builtins/commandline.rs index 19c00f6d3..bb41f0a43 100644 --- a/src/builtins/commandline.rs +++ b/src/builtins/commandline.rs @@ -15,8 +15,9 @@ use crate::prelude::*; use crate::proc::is_interactive_session; use crate::reader::{ - commandline_get_state, commandline_set_buffer, commandline_set_search_field, - reader_execute_readline_cmd, reader_showing_suggestion, + JumpDirection, JumpPrecision, commandline_get_state, commandline_set_buffer, + commandline_set_search_field, reader_execute_readline_cmd, reader_jump, + reader_showing_suggestion, }; use crate::tokenizer::{TOK_ACCEPT_UNFINISHED, TokenType, Tokenizer}; use fish_wcstringutil::join_strings; @@ -262,6 +263,11 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) let mut showing_suggestion = false; let mut override_buffer = None; + let mut forward_jump = false; + let mut backward_jump = false; + let mut forward_jump_till = false; + let mut backward_jump_till = false; + let mut jump_target = None; let short_options = L!("abijpctfxorhI:CBELSsP"); let long_options: &[WOption] = &[ @@ -292,6 +298,10 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) wopt(L!("search-field"), ArgType::NoArgument, '\x03'), wopt(L!("is-valid"), ArgType::NoArgument, '\x01'), wopt(L!("showing-suggestion"), ArgType::NoArgument, '\x04'), + wopt(L!("forward-jump"), ArgType::RequiredArgument, '\x07'), + wopt(L!("backward-jump"), ArgType::RequiredArgument, '\x08'), + wopt(L!("forward-jump-till"), ArgType::RequiredArgument, '\x09'), + wopt(L!("backward-jump-till"), ArgType::RequiredArgument, '\x0a'), ]; let mut w = WGetopter::new(short_options, long_options, args); @@ -341,6 +351,22 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) '\x03' => search_field_mode = true, '\x01' => is_valid = true, '\x04' => showing_suggestion = true, + '\x07' => { + forward_jump = true; + jump_target = Some(w.woptarg.unwrap().to_owned()); + } + '\x08' => { + backward_jump = true; + jump_target = Some(w.woptarg.unwrap().to_owned()); + } + '\x09' => { + forward_jump_till = true; + jump_target = Some(w.woptarg.unwrap().to_owned()); + } + '\x0a' => { + backward_jump_till = true; + jump_target = Some(w.woptarg.unwrap().to_owned()); + } 'h' => { builtin_print_help(parser, streams, cmd); return Ok(SUCCESS); @@ -360,6 +386,27 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) _ => panic!(), } } + if forward_jump || forward_jump_till || backward_jump || backward_jump_till { + let direction = if forward_jump || forward_jump_till { + JumpDirection::Forward + } else { + JumpDirection::Backward + }; + let precision = if forward_jump || backward_jump { + JumpPrecision::To + } else { + JumpPrecision::Till + }; + let target = jump_target.unwrap(); + let Some(target) = target.chars().next() else { + return Err(STATUS_INVALID_ARGS); + }; + return if reader_jump(direction, precision, target) { + Ok(SUCCESS) + } else { + Err(STATUS_CMD_ERROR) + }; + } let positional_args = w.argv.len() - w.wopt_index; diff --git a/src/input.rs b/src/input.rs index 961f512d6..0d0cc6357 100644 --- a/src/input.rs +++ b/src/input.rs @@ -162,6 +162,7 @@ const fn make_md(name: &'static wstr, code: ReadlineCmd) -> InputFunctionMetadat make_md(L!("forward-word"), ReadlineCmd::ForwardWordEmacs), make_md(L!("forward-word-end"), ReadlineCmd::ForwardWordEnd), make_md(L!("forward-word-vi"), ReadlineCmd::ForwardWordVi), + make_md(L!("get-key"), ReadlineCmd::GetKey), make_md(L!("history-delete"), ReadlineCmd::HistoryDelete), make_md(L!("history-last-token-search-backward"), ReadlineCmd::HistoryLastTokenSearchBackward), make_md(L!("history-last-token-search-forward"), ReadlineCmd::HistoryLastTokenSearchForward), @@ -698,7 +699,9 @@ pub fn read_char(&mut self) -> CharEvent { let evt = self.readch(); match evt { CharEvent::Readline(ref readline_event) => match readline_event.cmd { - ReadlineCmd::SelfInsert | ReadlineCmd::SelfInsertNotFirst => { + ReadlineCmd::SelfInsert + | ReadlineCmd::SelfInsertNotFirst + | ReadlineCmd::GetKey => { // Typically self-insert is generated by the generic (empty) binding. // However if it is generated by a real sequence, then insert that sequence. let seq = readline_event.seq.chars().map(CharEvent::from_char); @@ -721,6 +724,20 @@ pub fn read_char(&mut self) -> CharEvent { kevt.input_style = CharInputStyle::NotFirst; } } + if readline_event.cmd == ReadlineCmd::GetKey { + if let CharEvent::Key(kevt) = res { + return CharEvent::Command(sprintf!( + "set -g fish_key %s", + escape( + &kevt + .key + .codepoint_text() + .map(|c| WString::from_chars(vec![c])) + .unwrap_or_default() + ) + )); + } + } return res; } ReadlineCmd::FuncAnd | ReadlineCmd::FuncOr => { diff --git a/src/input_common.rs b/src/input_common.rs index 28b8f0466..043ba3daf 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -107,6 +107,7 @@ pub enum ReadlineCmd { HistoryLastTokenSearchForward, SelfInsert, SelfInsertNotFirst, + GetKey, TransposeChars, TransposeWords, UpcaseWord, diff --git a/src/reader/reader.rs b/src/reader/reader.rs index d26e6ae5d..b25c79ed1 100644 --- a/src/reader/reader.rs +++ b/src/reader/reader.rs @@ -546,13 +546,13 @@ enum Kill { } #[derive(Clone, Copy, Eq, PartialEq)] -enum JumpDirection { +pub enum JumpDirection { Forward, Backward, } #[derive(Clone, Copy, Eq, PartialEq)] -enum JumpPrecision { +pub enum JumpPrecision { Till, To, } @@ -1175,6 +1175,15 @@ pub fn reader_execute_readline_cmd(parser: &Parser, ch: CharEvent) { let _ = data.handle_char_event(Some(ch)); } +pub fn reader_jump(direction: JumpDirection, precision: JumpPrecision, target: char) -> bool { + let Some(data) = current_data() else { + return false; + }; + data.save_screen_state(); + let elt = data.active_edit_line_tag(); + data.jump_and_remember_last_jump(direction, precision, elt, target, false) +} + pub fn reader_showing_suggestion(parser: &Parser) -> bool { if !is_interactive_session() { return false; @@ -4346,7 +4355,7 @@ fn handle_readline_command(&mut self, c: ReadlineCmd) { rl::ScrollbackPush => { self.screen.push_to_scrollback(); } - rl::SelfInsert | rl::SelfInsertNotFirst | rl::FuncAnd | rl::FuncOr => { + rl::SelfInsert | rl::SelfInsertNotFirst | rl::GetKey | rl::FuncAnd | rl::FuncOr => { // This can be reached via `commandline -f and` etc // panic!("should have been handled by inputter_t::readch"); } @@ -6220,6 +6229,7 @@ fn command_ends_paging(c: ReadlineCmd, focused_on_search_field: bool) -> bool { | rl::BackwardKillBigword | rl::BackwardKillToken | rl::SelfInsert + | rl::GetKey | rl::SelfInsertNotFirst | rl::TransposeChars | rl::TransposeWords diff --git a/tests/pexpects/bind.py b/tests/pexpects/bind.py index 1e5838e87..f986a58a0 100644 --- a/tests/pexpects/bind.py +++ b/tests/pexpects/bind.py @@ -321,7 +321,7 @@ expect_prompt("echo two three") # Now test that exactly the expected bind modes are defined sendline("bind --list-modes") expect_prompt( - "default\r\ninsert\r\noperator\r\nreplace\r\nreplace_one\r\nvisual\r\n", + "F\r\nT\r\ndefault\r\nf\r\ninsert\r\noperator\r\nreplace\r\nreplace_one\r\nt\r\nvisual\r\n", unmatched="Unexpected vi bind modes", )