From 8e34dc4cdb812ce7786364726923b9451468f5a0 Mon Sep 17 00:00:00 2001 From: iksuddle Date: Mon, 5 Jan 2026 16:22:08 -0500 Subject: [PATCH] bind: show all modes by default Ensure `bind` builtin lists binds for all modes if `--mode` is not given. - The `get` function in `src/input.rs` now takes an optional bind mode and returns a list of input mappings (binds). - The `list_one` function in `src/builtins/bind.rs` lists binds in the results returned by `get`. - Creating the output string for a bind has been extracted to its own function: `BuiltinBind::generate_output_string`. - The `bind_mode_given` option has been removed. Fixes #12214 Closes #12285 --- CHANGELOG.rst | 1 + doc_src/cmds/bind.rst | 4 +- src/builtins/bind.rs | 129 +++++++++++++++++++++-------------------- src/input.rs | 36 ++++++------ tests/checks/bind.fish | 13 +++++ 5 files changed, 100 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 30df7dae4..6c7fa50ad 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,7 @@ Interactive improvements - The behavior of ``{,d}{w,W}``, ``{,d}{,g}{e,E}`` bindings in vi-mode is now more compatible with vim, except that the underscore is not a keyword (which can be archived by setting ``set iskeyword-=_`` in vim). - Add commands ``{forward,kill}-{word,bigword}-vi``, ``{forward,backward,kill,backward-kill}-{word,bigword}-end`` and ``kill-{a,inner}-{word,bigword}`` corresponding to above-mentioned bindings. - Vi mode key bindings now support counts for movement and deletion commands (e.g. `d3w` or `3l`), via a new operator mode (:issue:`2192`). +- The ``bind`` builtin lists mappings from all modes if ``--mode`` is not provided (:issue:`12214`). New or improved bindings ------------------------ diff --git a/doc_src/cmds/bind.rst b/doc_src/cmds/bind.rst index 1e8d36938..5a5fb43b4 100644 --- a/doc_src/cmds/bind.rst +++ b/doc_src/cmds/bind.rst @@ -19,8 +19,8 @@ Description ``bind`` manages key bindings. If both ``KEYS`` and ``COMMAND`` are given, ``bind`` adds (or replaces) a binding in ``MODE``. -If only ``KEYS`` is given, any existing binding for those keys in the given ``MODE`` will be printed. -If no ``KEYS`` argument is provided, all bindings (in the given ``MODE``) are printed. +If only ``KEYS`` is given, ``bind`` lists any existing bindings for those keys in ``MODE`` or in all modes. +If no ``KEYS`` argument is provided, ``bind`` lists all bindings in ``MODE`` or in all modes. ``KEYS`` is a comma-separated list of key names. Modifier keys can be specified by prefixing a key name with a combination of ``ctrl-``, ``alt-``, ``shift-`` and ``super-`` (i.e. the "windows" or "command" key). diff --git a/src/builtins/bind.rs b/src/builtins/bind.rs index 03799dfa2..458ec9feb 100644 --- a/src/builtins/bind.rs +++ b/src/builtins/bind.rs @@ -5,7 +5,9 @@ EscapeFlags, EscapeStringStyle, bytes2wcstring, escape, escape_string, valid_var_name, }; use crate::highlight::highlight_and_colorize; -use crate::input::{InputMappingSet, KeyNameStyle, input_function_get_names, input_mappings}; +use crate::input::{ + InputMapping, InputMappingSet, KeyNameStyle, input_function_get_names, input_mappings, +}; use crate::key::{ self, KEY_NAMES, Key, MAX_FUNCTION_KEY, Modifiers, char_to_symbol, function_key, parse_keys, }; @@ -20,7 +22,6 @@ struct Options { all: bool, - bind_mode_given: bool, list_modes: bool, print_help: bool, silent: bool, @@ -29,7 +30,7 @@ struct Options { have_preset: bool, preset: bool, mode: c_int, - bind_mode: WString, + bind_mode: Option, sets_bind_mode: Option, color: ColorEnabled, } @@ -38,7 +39,6 @@ impl Options { fn new() -> Options { Options { all: false, - bind_mode_given: false, list_modes: false, print_help: false, silent: false, @@ -47,7 +47,7 @@ fn new() -> Options { have_preset: false, preset: false, mode: BIND_INSERT, - bind_mode: DEFAULT_BIND_MODE.to_owned(), + bind_mode: None, sets_bind_mode: None, color: ColorEnabled::default(), } @@ -70,30 +70,9 @@ fn new() -> BuiltinBind { } } - /// List a single key binding. - /// Returns false if no binding with that sequence and mode exists. - fn list_one( - &self, - seq: &[Key], - bind_mode: &wstr, - user: bool, - parser: &Parser, - streams: &mut IoStreams, - ) -> bool { - let mut ecmds: &[_] = &[]; - let mut sets_mode = None; - let mut key_name_style = KeyNameStyle::Plain; + /// Returns a WString for the output line of a bind + fn generate_output_string(seq: &[Key], user: bool, bind: &InputMapping) -> WString { let mut out = WString::new(); - if !self.input_mappings.get( - seq, - bind_mode, - &mut ecmds, - user, - &mut sets_mode, - &mut key_name_style, - ) { - return false; - } out.push_str("bind"); @@ -101,20 +80,20 @@ fn list_one( if !user { out.push_str(" --preset"); } - if bind_mode != DEFAULT_BIND_MODE { + if bind.mode != DEFAULT_BIND_MODE { out.push_str(" -M "); - out.push_utfstr(&escape(bind_mode)); + out.push_utfstr(&escape(&bind.mode)); } - if let Some(sets_mode) = sets_mode { - if sets_mode != bind_mode { + if let Some(sets_mode) = &bind.sets_mode { + if *sets_mode != bind.mode { out.push_str(" -m "); out.push_utfstr(&escape(sets_mode)); } } out.push(' '); - match key_name_style { + match bind.key_name_style { KeyNameStyle::Plain => { // Append the name. for (i, key) in seq.iter().enumerate() { @@ -148,31 +127,59 @@ fn list_one( } // Now show the list of commands. - for ecmd in ecmds { + for ecmd in &bind.commands { out.push(' '); out.push_utfstr(&escape(ecmd)); } out.push('\n'); - if self.opts.color.enabled(streams) { - streams.out.append(&bytes2wcstring(&highlight_and_colorize( - &out, - &parser.context(), - parser.vars(), - ))); - } else { - streams.out.append(&out); + out + } + + /// List a single binding of a specific sequence from the specified mode. + /// Returns false if no binding with that sequence and mode exists. + /// + /// If bind_mode is None, then binds from all modes are listed. + fn list_one( + &self, + seq: &[Key], + bind_mode: Option<&wstr>, + user: bool, + parser: &Parser, + streams: &mut IoStreams, + ) -> bool { + let results = self.input_mappings.get(seq, bind_mode, user); + + // no binds found + if results.is_empty() { + return false; + } + + for bind in results { + let out = Self::generate_output_string(seq, user, bind); + + if self.opts.color.enabled(streams) { + streams.out.append(&bytes2wcstring(&highlight_and_colorize( + &out, + &parser.context(), + parser.vars(), + ))); + } else { + streams.out.append(&out); + } } true } - // Overload with both kinds of bindings. - // Returns false only if neither exists. + /// Overload with both preset and user bindings. + /// Returns false only if neither exists. + /// + /// If bind_mode is None, then binds from all modes are listed. fn list_one_user_andor_preset( &self, seq: &[Key], - bind_mode: &wstr, + bind_mode: Option<&wstr>, user: bool, preset: bool, parser: &Parser, @@ -196,7 +203,7 @@ fn list(&self, bind_mode: Option<&wstr>, user: bool, parser: &Parser, streams: & continue; } - self.list_one(&binding.seq, &binding.mode, user, parser, streams); + self.list_one(&binding.seq, Some(&binding.mode), user, parser, streams); } } @@ -267,24 +274,20 @@ fn compute_seq(&self, streams: &mut IoStreams, seq: &wstr) -> Option> { /// if specified, _all_ key bindings will be erased /// fn erase(&mut self, seq: &[&wstr], all: bool, user: bool, streams: &mut IoStreams) -> bool { - let mode = if self.opts.bind_mode_given { - Some(self.opts.bind_mode.as_utfstr()) - } else { - None - }; + let bind_mode = self.opts.bind_mode.as_deref(); if all { - self.input_mappings.clear(mode, user); + self.input_mappings.clear(bind_mode, user); return false; } - let mode = mode.unwrap_or(DEFAULT_BIND_MODE); + let bind_mode = bind_mode.unwrap_or(DEFAULT_BIND_MODE); for s in seq { let Some(s) = self.compute_seq(streams, s) else { return true; }; - self.input_mappings.erase(&s, mode, user); + self.input_mappings.erase(&s, bind_mode, user); } false } @@ -321,11 +324,7 @@ fn insert( if arg_count == 0 { // We don't overload this with user and def because we want them to be grouped. // First the presets, then the users (because of scrolling). - let bind_mode = if self.opts.bind_mode_given { - Some(self.opts.bind_mode.as_utfstr()) - } else { - None - }; + let bind_mode = self.opts.bind_mode.as_deref(); if self.opts.preset { self.list(bind_mode, false, parser, streams); } @@ -337,9 +336,11 @@ fn insert( return true; }; + let bind_mode = self.opts.bind_mode.as_deref(); + if !self.list_one_user_andor_preset( &seq, - &self.opts.bind_mode, + bind_mode, self.opts.user, self.opts.preset, parser, @@ -372,7 +373,10 @@ fn insert( if self.add( seq, &argv[optind + 1..], - self.opts.bind_mode.clone(), + self.opts + .bind_mode + .clone() + .unwrap_or(DEFAULT_BIND_MODE.to_owned()), self.opts.sets_bind_mode.clone(), self.opts.user, streams, @@ -461,8 +465,7 @@ fn parse_cmd_opts( 'M' => { let applicable_mode = w.woptarg.unwrap(); check_mode_name(streams, applicable_mode)?; - opts.bind_mode = applicable_mode.to_owned(); - opts.bind_mode_given = true; + opts.bind_mode = Some(applicable_mode.to_owned()); } 'm' => { let new_mode = w.woptarg.unwrap(); diff --git a/src/input.rs b/src/input.rs index 51d194daf..c663c03be 100644 --- a/src/input.rs +++ b/src/input.rs @@ -44,11 +44,11 @@ pub struct InputMapping { /// We wish to preserve the user-specified order. This is just an incrementing value. specification_order: u32, /// Mode in which this command should be evaluated. - mode: WString, + pub mode: WString, /// New mode that should be switched to after command evaluation, or None to leave the mode unchanged. - sets_mode: Option, + pub sets_mode: Option, /// Perhaps this binding was created using a raw escape sequence. - key_name_style: KeyNameStyle, + pub key_name_style: KeyNameStyle, } impl InputMapping { @@ -938,31 +938,31 @@ pub fn erase(&mut self, sequence: &[Key], mode: &wstr, user: bool) -> bool { result } - /// Gets the command bound to the specified key sequence in the specified mode. Returns true if - /// it exists, false if not. + /// Returns the command bound to the specified bind mode. + /// + /// If bind_mode is None, then binds from all modes are returned. pub fn get<'a>( &'a self, sequence: &[Key], - mode: &wstr, - out_cmds: &mut &'a [WString], + bind_mode: Option<&wstr>, user: bool, - out_sets_mode: &mut Option<&'a wstr>, - out_key_name_style: &mut KeyNameStyle, - ) -> bool { + ) -> Vec<&'a InputMapping> { let ml = if user { &self.mapping_list } else { &self.preset_mapping_list }; - for m in ml { - if m.seq == sequence && m.mode == mode { - *out_cmds = &m.commands; - *out_sets_mode = m.sets_mode.as_deref(); - *out_key_name_style = m.key_name_style.clone(); - return true; - } + + let ml = ml.iter().filter(|mapping| mapping.seq == sequence); + let mut mappings: Vec<_>; + if let Some(mode) = bind_mode { + mappings = ml.filter(|mapping| mapping.mode == mode).collect(); + assert!(mappings.len() <= 1); + } else { + mappings = ml.collect(); + mappings.sort_unstable_by_key(|mapping| mapping.specification_order); } - false + mappings } } diff --git a/tests/checks/bind.fish b/tests/checks/bind.fish index b889abcfc..33ee3febc 100644 --- a/tests/checks/bind.fish +++ b/tests/checks/bind.fish @@ -178,4 +178,17 @@ bind ctrl-shift-a bind ctrl-shift-ä # CHECKERR: bind: No binding found for key 'ctrl-shift-ä' +# Verify binds from all modes are returned when querying a sequence +fish_vi_key_bindings +bind --preset ctrl-q 'echo preset' +bind ctrl-q 'echo default' +bind ctrl-q --mode insert 'echo insert' +bind ctrl-q --mode replace 'echo replace' +bind ctrl-q +# CHECK: bind --preset ctrl-q 'echo preset' +# CHECK: bind ctrl-q 'echo default' +# CHECK: bind -M insert ctrl-q 'echo insert' +# CHECK: bind -M replace ctrl-q 'echo replace' +fish_default_key_bindings + exit 0