diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fcefc5f80..5242eb42b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -77,6 +77,7 @@ Interactive improvements New or improved bindings ^^^^^^^^^^^^^^^^^^^^^^^^ +- Bindings can now mix special input functions and shell commands, so ``bind \cg expand-abbr "commandline -i \n"`` works as expected (:issue:`8186`). - When the cursor is on a command that resolves to an executable script, :kbd:`Alt-O` will now open that script in your editor (:issue:`10266`). - Two improvements to the :kbd:`Alt-E` binding which edits the commandline in an external editor: - The editor's cursor position is copied back to fish. This is currently supported for Vim and Kakoune. diff --git a/doc_src/cmds/bind.rst b/doc_src/cmds/bind.rst index 3dde2e8e9..2f08909fe 100644 --- a/doc_src/cmds/bind.rst +++ b/doc_src/cmds/bind.rst @@ -32,7 +32,7 @@ To find out what sequence a key combination sends, you can use :doc:`fish_key_re ``COMMAND`` can be any fish command, but it can also be one of a set of special input functions. These include functions for moving the cursor, operating on the kill-ring, performing tab completion, etc. Use ``bind --function-names`` or :ref:`see below ` for a list of these input functions. .. note:: - The commands must be entirely a sequence of special input functions (from ``bind -f``) or all shell script commands (i.e., valid fish script). To run special input functions from regular fish script, use ``commandline -f`` (see also :doc:`commandline `). If a script produces output, it should finish by calling ``commandline -f repaint`` so that fish knows to redraw the prompt. + If a script changes the commandline, it should finish by calling the ``repaint`` special input function. If no ``SEQUENCE`` is provided, all bindings (or just the bindings in the given ``MODE``) are printed. If ``SEQUENCE`` is provided but no ``COMMAND``, just the binding matching that sequence is printed. @@ -353,7 +353,7 @@ Turn on :ref:`vi key bindings ` and rebind :kbd:`Control`\ +\ :kbd:`C` Launch ``git diff`` and repaint the commandline afterwards when :kbd:`Control`\ +\ :kbd:`G` is pressed:: - bind \cg 'git diff; commandline -f repaint' + bind \cg 'git diff' repaint .. _cmd-bind-termlimits: diff --git a/src/builtins/commandline.rs b/src/builtins/commandline.rs index 4afcdf9d0..3d7ee5657 100644 --- a/src/builtins/commandline.rs +++ b/src/builtins/commandline.rs @@ -11,9 +11,7 @@ parse_util_token_extent, }; use crate::proc::is_interactive_session; -use crate::reader::{ - commandline_get_state, commandline_set_buffer, reader_handle_command, reader_queue_ch, -}; +use crate::reader::{commandline_get_state, commandline_set_buffer, reader_queue_ch}; use crate::tokenizer::TOK_ACCEPT_UNFINISHED; use crate::tokenizer::{TokenType, Tokenizer}; use crate::wchar::prelude::*; @@ -332,18 +330,8 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) } } - // HACK: Execute these right here and now so they can affect any insertions/changes - // made via bindings. The correct solution is to change all `commandline` - // insert/replace operations into readline functions with associated data, so that - // all queued `commandline` operations - including buffer modifications - are - // executed in order - match cmd { - rl::BeginUndoGroup | rl::EndUndoGroup => reader_handle_command(cmd), - _ => { - // Inserts the readline function at the back of the queue. - reader_queue_ch(CharEvent::from_readline(cmd)); - } - } + // Inserts the readline function at the back of the queue. + reader_queue_ch(CharEvent::from_readline(cmd)); } return STATUS_CMD_OK; diff --git a/src/input.rs b/src/input.rs index 5106cbe4c..7f286a9c8 100644 --- a/src/input.rs +++ b/src/input.rs @@ -248,7 +248,7 @@ fn input_get_bind_mode(vars: &dyn Environment) -> WString { } /// Set the current bind mode. -fn input_set_bind_mode(parser: &Parser, bm: &wstr) { +pub fn input_set_bind_mode(parser: &Parser, bm: &wstr) { // Only set this if it differs to not execute variable handlers all the time. // modes may not be empty - empty is a sentinel value meaning to not change the mode assert!(!bm.is_empty()); @@ -488,64 +488,28 @@ fn function_push_args(&mut self, code: ReadlineCmd) { self.event_storage.clear(); } - /// Perform the action of the specified binding. allow_commands controls whether fish commands - /// should be executed, or should be deferred until later. - fn mapping_execute( - &mut self, - m: &InputMapping, - command_handler: &mut Option<&mut CommandHandler>, - ) { - // has_functions: there are functions that need to be put on the input queue - // has_commands: there are shell commands that need to be evaluated - let mut has_commands = false; - let mut has_functions = false; - for cmd in &m.commands { - if input_function_get_code(cmd).is_some() { - has_functions = true; - } else { - has_commands = true; - } - if has_functions && has_commands { - break; - } + /// Perform the action of the specified binding. + fn mapping_execute(&mut self, m: &InputMapping) { + let has_command = m + .commands + .iter() + .any(|cmd| input_function_get_code(cmd).is_none()); + if has_command { + self.push_front(CharEvent::from_check_exit()); } - - // !has_functions && !has_commands: only set bind mode - if !has_commands && !has_functions { - if let Some(sets_mode) = m.sets_mode.as_ref() { - input_set_bind_mode(&self.parser, sets_mode); - } - return; - } - - if has_commands && command_handler.is_none() { - // We don't want to run commands yet. Put the characters back and return check_exit. - self.insert_front(m.seq.chars().map(CharEvent::from_char)); - self.push_front(CharEvent::from_check_exit()); - return; // skip the input_set_bind_mode - } else if has_functions && !has_commands { - // Functions are added at the head of the input queue. - for cmd in m.commands.iter().rev() { - let code = input_function_get_code(cmd).unwrap(); - self.function_push_args(code); - self.push_front(CharEvent::from_readline_seq(code, m.seq.clone())); - } - } else if has_commands && !has_functions { - // Execute all commands. - // - // FIXME(snnw): if commands add stuff to input queue (e.g. commandline -f execute), we won't - // see that until all other commands have also been run. - let command_handler = command_handler.as_mut().unwrap(); - command_handler(&m.commands); - self.push_front(CharEvent::from_check_exit()); - } else { - // Invalid binding, mixed commands and functions. We would need to execute these one by - // one. - self.push_front(CharEvent::from_check_exit()); + for cmd in m.commands.iter().rev() { + let evt = match input_function_get_code(cmd) { + Some(code) => { + self.function_push_args(code); + CharEvent::from_readline_seq(code, m.seq.clone()) + } + None => CharEvent::from_command(cmd.clone()), + }; + self.push_front(evt); } // Missing bind mode indicates to not reset the mode (#2871) if let Some(sets_mode) = m.sets_mode.as_ref() { - input_set_bind_mode(&self.parser, sets_mode); + self.push_front(CharEvent::from_set_mode(sets_mode.clone())); } } @@ -667,7 +631,7 @@ fn consume(mut self) { fn char_sequence_interrupted(&self) -> bool { self.peeked .iter() - .any(|evt| evt.is_readline() || evt.is_check_exit()) + .any(|evt| evt.is_readline_or_command() || evt.is_check_exit()) } /// Reset our index back to 0. @@ -809,10 +773,7 @@ fn find_mapping(vars: &dyn Environment, peeker: &mut EventQueuePeeker) -> Option } } - fn mapping_execute_matching_or_generic( - &mut self, - command_handler: &mut Option<&mut CommandHandler>, - ) { + fn mapping_execute_matching_or_generic(&mut self) { let vars = self.parser.vars_ref(); let mut peeker = EventQueuePeeker::new(self); // Check for mouse-tracking CSI before mappings to prevent the generic mapping handler from @@ -837,7 +798,7 @@ fn mapping_execute_matching_or_generic( // Check for ordinary mappings. if let Some(mapping) = Self::find_mapping(&*vars, &mut peeker) { peeker.consume(); - self.mapping_execute(&mapping, command_handler); + self.mapping_execute(&mapping); return; } peeker.restart(); @@ -868,7 +829,7 @@ fn read_characters_no_readline(&mut self) -> CharEvent { let evt_to_return: CharEvent; loop { let evt = self.readch(); - if evt.is_readline() { + if evt.is_readline_or_command() { saved_events.push(evt); } else { evt_to_return = evt; @@ -891,11 +852,7 @@ fn read_characters_no_readline(&mut self) -> CharEvent { /// to be an escape sequence for a special character (such as an arrow key), and readch attempts /// to parse it. If no more input follows after the escape key, it is assumed to be an actual /// escape key press, and is returned as such. - /// - /// \p command_handler is used to run commands. If empty (in the std::function sense), when a - /// character is encountered that would invoke a fish command, it is unread and - /// char_event_type_t::check_exit is returned. Note the handler is not stored. - pub fn read_char(&mut self, mut command_handler: Option<&mut CommandHandler>) -> CharEvent { + pub fn read_char(&mut self) -> CharEvent { // Clear the interrupted flag. reader_reset_interrupted(); @@ -933,6 +890,9 @@ pub fn read_char(&mut self, mut command_handler: Option<&mut CommandHandler>) -> return evt; } }, + CharEventType::Command(_) | CharEventType::SetMode(_) => { + return evt; + } CharEventType::Eof => { // If we have EOF, we need to immediately quit. // There's no need to go through the input functions. @@ -944,10 +904,7 @@ pub fn read_char(&mut self, mut command_handler: Option<&mut CommandHandler>) -> } CharEventType::Char(_) => { self.push_front(evt); - self.mapping_execute_matching_or_generic(&mut command_handler); - // Regarding allow_commands, we're in a loop, but if a fish command is executed, - // check_exit is unread, so the next pass through the loop we'll break out and return - // it. + self.mapping_execute_matching_or_generic(); } } } diff --git a/src/input_common.rs b/src/input_common.rs index 4686c635a..ce7b73471 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -116,7 +116,7 @@ pub enum ReadlineCmd { } /// Represents an event on the character input stream. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub enum CharEventType { /// A character was entered. Char(char), @@ -124,6 +124,12 @@ pub enum CharEventType { /// A readline event. Readline(ReadlineCmd), + /// A shell command. + Command(WString), + + /// A request to change the input mapping mode. + SetMode(WString), + /// end-of-file was reached. Eof, @@ -162,6 +168,13 @@ pub fn is_readline(&self) -> bool { matches!(self.evt, CharEventType::Readline(_)) } + pub fn is_readline_or_command(&self) -> bool { + matches!( + self.evt, + CharEventType::Readline(_) | CharEventType::Command(_) | CharEventType::SetMode(_) + ) + } + pub fn get_char(&self) -> char { let CharEventType::Char(c) = self.evt else { panic!("Not a char type"); @@ -184,6 +197,20 @@ pub fn get_readline(&self) -> ReadlineCmd { c } + pub fn get_command(&self) -> Option<&wstr> { + match &self.evt { + CharEventType::Command(c) => Some(c), + _ => None, + } + } + + pub fn get_mode(&self) -> Option<&wstr> { + match &self.evt { + CharEventType::SetMode(m) => Some(m), + _ => None, + } + } + pub fn from_char(c: char) -> CharEvent { CharEvent { evt: CharEventType::Char(c), @@ -204,6 +231,22 @@ pub fn from_readline_seq(cmd: ReadlineCmd, seq: WString) -> CharEvent { } } + pub fn from_command(cmd: WString) -> CharEvent { + CharEvent { + evt: CharEventType::Command(cmd), + input_style: CharInputStyle::Normal, + seq: WString::new(), + } + } + + pub fn from_set_mode(mode: WString) -> CharEvent { + CharEvent { + evt: CharEventType::SetMode(mode), + input_style: CharInputStyle::Normal, + seq: WString::new(), + } + } + pub fn from_check_exit() -> CharEvent { CharEvent { evt: CharEventType::CheckExit, @@ -587,7 +630,7 @@ fn insert_front(&mut self, evts: I) fn drop_leading_readline_events(&mut self) { let queue = self.get_queue_mut(); while let Some(evt) = queue.front() { - if evt.is_readline() { + if evt.is_readline_or_command() { queue.pop_front(); } else { break; diff --git a/src/reader.rs b/src/reader.rs index 2df6bb090..49dc05e03 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -67,8 +67,8 @@ history_session_id, in_private_mode, History, HistorySearch, PersistenceMode, SearchDirection, SearchType, }; -use crate::input::init_input; use crate::input::Inputter; +use crate::input::{init_input, input_set_bind_mode}; use crate::input_common::{CharEvent, CharInputStyle, ReadlineCmd}; use crate::io::IoChain; use crate::kill::{kill_add, kill_replace, kill_yank, kill_yank_rotate}; @@ -1800,8 +1800,8 @@ fn readline(&mut self, nchars: Option) -> Option { continue; } assert!( - event_needing_handling.is_char() || event_needing_handling.is_readline(), - "Should have a char or readline" + event_needing_handling.is_char() || event_needing_handling.is_readline_or_command(), + "Should have a char, readline or command" ); if !matches!(rls.last_cmd, Some(rl::Yank | rl::YankPop)) { @@ -1837,6 +1837,10 @@ fn readline(&mut self, nchars: Option) -> Option { } rls.last_cmd = Some(readline_cmd); + } else if let Some(command) = event_needing_handling.get_command() { + zelf.run_input_command_scripts(command); + } else if let Some(mode) = event_needing_handling.get_mode() { + input_set_bind_mode(zelf.parser(), mode); } else { // Ordinary char. let c = event_needing_handling.get_char(); @@ -1913,13 +1917,11 @@ fn readline(&mut self, nchars: Option) -> Option { } /// Run a sequence of commands from an input binding. - fn run_input_command_scripts(&mut self, cmds: &[WString]) { + fn run_input_command_scripts(&mut self, cmd: &wstr) { let last_statuses = self.parser().vars().get_last_statuses(); - for cmd in cmds { - self.update_commandline_state(); - self.parser().eval(cmd, &IoChain::new()); - self.apply_commandline_state_changes(); - } + self.update_commandline_state(); + self.parser().eval(cmd, &IoChain::new()); + self.apply_commandline_state_changes(); self.parser().set_last_statuses(last_statuses); // Restore tty to shell modes. @@ -1957,22 +1959,8 @@ fn read_normal_chars(&mut self, rls: &mut ReadlineLoopState) -> Option