diff --git a/doc_src/cmds/commandline.rst b/doc_src/cmds/commandline.rst index 3473f42c3..10f929415 100644 --- a/doc_src/cmds/commandline.rst +++ b/doc_src/cmds/commandline.rst @@ -70,6 +70,9 @@ The following options change what part of the commandline is printed or updated: **-t** or **--current-token** Selects the current token +**--search-field** + Use the pager search field instead of the command line. Returns false is the search field is not shown. + The following options change the way ``commandline`` prints the current commandline buffer: **-c** or **--cut-at-cursor** diff --git a/share/completions/commandline.fish b/share/completions/commandline.fish index 5f95e70ec..2f43d3bf2 100644 --- a/share/completions/commandline.fish +++ b/share/completions/commandline.fish @@ -22,6 +22,7 @@ complete -c commandline -s L -l line -d "Print the line that the cursor is on" complete -c commandline -s S -l search-mode -d "Return true if performing a history search" complete -c commandline -s P -l paging-mode -d "Return true if showing pager content" complete -c commandline -l paging-full-mode -d "Return true if pager is showing all content" +complete -c commandline -l search-field -d "Operate on the pager search field" complete -c commandline -l is-valid -d "Return true if the command line is syntactically valid and complete" complete -c commandline -n '__fish_contains_opt -s f function' -a '(bind --function-names)' -d 'Function name' -x diff --git a/share/functions/fish_clipboard_paste.fish b/share/functions/fish_clipboard_paste.fish index 8ce4991c1..0b46b7bdb 100644 --- a/share/functions/fish_clipboard_paste.fish +++ b/share/functions/fish_clipboard_paste.fish @@ -26,6 +26,11 @@ function fish_clipboard_paste # Also split on \r, otherwise it looks confusing set data (string split \r -- $data | string split \n) + if commandline --search-field >/dev/null + commandline --search-field -i -- $data + return + end + # If the current token has an unmatched single-quote, # escape all single-quotes (and backslashes) in the paste, # in order to turn it into a single literal token. diff --git a/src/builtins/commandline.rs b/src/builtins/commandline.rs index 3d7ee5657..ccdd5af81 100644 --- a/src/builtins/commandline.rs +++ b/src/builtins/commandline.rs @@ -11,7 +11,9 @@ parse_util_token_extent, }; use crate::proc::is_interactive_session; -use crate::reader::{commandline_get_state, commandline_set_buffer, reader_queue_ch}; +use crate::reader::{ + commandline_get_state, commandline_set_buffer, commandline_set_search_field, reader_queue_ch, +}; use crate::tokenizer::TOK_ACCEPT_UNFINISHED; use crate::tokenizer::{TokenType, Tokenizer}; use crate::wchar::prelude::*; @@ -57,6 +59,7 @@ fn replace_part( insert_mode: AppendMode, buff: &wstr, cursor_pos: usize, + search_field_mode: bool, ) { let mut out_pos = cursor_pos; let mut out = buff[..range.start].to_owned(); @@ -81,7 +84,11 @@ fn replace_part( } out.push_utfstr(&buff[range.end..]); - commandline_set_buffer(out, Some(out_pos)); + if search_field_mode { + commandline_set_search_field(out, Some(out_pos)); + } else { + commandline_set_buffer(out, Some(out_pos)); + } } /// Output the specified selection. @@ -191,6 +198,7 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) let mut search_mode = false; let mut paging_mode = false; let mut paging_full_mode = false; + let mut search_field_mode = false; let mut is_valid = false; let mut range = 0..0; @@ -224,6 +232,7 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) wopt(L!("search-mode"), woption_argument_t::no_argument, 'S'), wopt(L!("paging-mode"), woption_argument_t::no_argument, 'P'), wopt(L!("paging-full-mode"), woption_argument_t::no_argument, 'F'), + wopt(L!("search-field"), woption_argument_t::no_argument, '\x03'), wopt(L!("is-valid"), woption_argument_t::no_argument, '\x01'), ]; @@ -269,6 +278,7 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) 's' => selection_mode = true, 'P' => paging_mode = true, 'F' => paging_full_mode = true, + '\x03' => search_field_mode = true, '\x01' => is_valid = true, 'h' => { builtin_print_help(parser, streams, cmd); @@ -361,10 +371,10 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) return STATUS_INVALID_ARGS; } - if (buffer_part.is_some() || token_mode.is_some() || cut_at_cursor) + if (buffer_part.is_some() || token_mode.is_some() || cut_at_cursor || search_field_mode) && (cursor_mode || line_mode || search_mode || paging_mode || paging_full_mode) // Special case - we allow to get/set cursor position relative to the process/job/token. - && (buffer_part.is_none() || !cursor_mode) + && ((buffer_part.is_none() && !search_field_mode) || !cursor_mode) { streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd)); builtin_print_error_trailer(parser, streams.err, cmd); @@ -381,6 +391,12 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) return STATUS_INVALID_ARGS; } + if search_field_mode && buffer_part.is_some() { + streams.err.append(wgettext_fmt!(BUILTIN_ERR_COMBO, cmd,)); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + if append_mode.is_some() && positional_args == 0 { // No tokens in insert mode just means we do nothing. return STATUS_CMD_ERROR; @@ -447,7 +463,15 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) let current_buffer; let current_cursor_pos; let transient; - if let Some(override_buffer) = &override_buffer { + + if search_field_mode { + let Some((search_field_text, cursor_pos)) = commandline_get_state().search_field else { + return STATUS_CMD_ERROR; + }; + transient = search_field_text; + current_buffer = &transient; + current_cursor_pos = cursor_pos; + } else if let Some(override_buffer) = &override_buffer { current_buffer = override_buffer; current_cursor_pos = current_buffer.len(); } else if !ld.transient_commandlines.is_empty() && !cursor_mode { @@ -487,18 +511,22 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) }; } - match buffer_part { - TextScope::String => { - range = 0..current_buffer.len(); - } - TextScope::Job => { - range = parse_util_job_extent(current_buffer, current_cursor_pos, None); - } - TextScope::Process => { - range = parse_util_process_extent(current_buffer, current_cursor_pos, None); - } - TextScope::Token => { - parse_util_token_extent(current_buffer, current_cursor_pos, &mut range, None); + if search_field_mode { + range = 0..current_buffer.len(); + } else { + match buffer_part { + TextScope::String => { + range = 0..current_buffer.len(); + } + TextScope::Job => { + range = parse_util_job_extent(current_buffer, current_cursor_pos, None); + } + TextScope::Process => { + range = parse_util_process_extent(current_buffer, current_cursor_pos, None); + } + TextScope::Token => { + parse_util_token_extent(current_buffer, current_cursor_pos, &mut range, None); + } } } @@ -546,10 +574,18 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) append_mode, current_buffer, current_cursor_pos, + search_field_mode, ); } else { let sb = join_strings(&w.argv[w.woptind..], '\n'); - replace_part(range, &sb, append_mode, current_buffer, current_cursor_pos); + replace_part( + range, + &sb, + append_mode, + current_buffer, + current_cursor_pos, + search_field_mode, + ); } STATUS_CMD_OK diff --git a/src/reader.rs b/src/reader.rs index 951bd1663..bde049217 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -21,6 +21,7 @@ use nix::sys::stat::Mode; use once_cell::sync::Lazy; use std::cell::UnsafeCell; +use std::cmp; use std::io::BufReader; use std::num::NonZeroUsize; use std::ops::Range; @@ -305,6 +306,8 @@ pub struct CommandlineState { pub pager_mode: bool, /// pager already shows everything if possible pub pager_fully_disclosed: bool, + /// The search field, if shown. + pub search_field: Option<(WString, usize)>, /// pager is visible and search is active pub search_mode: bool, /// if false, the reader has not yet been entered @@ -320,6 +323,7 @@ const fn new() -> Self { history: None, pager_mode: false, pager_fully_disclosed: false, + search_field: None, search_mode: false, initialized: false, } @@ -937,10 +941,17 @@ pub fn commandline_get_state() -> CommandlineState { /// will pick it up when it is done executing. pub fn commandline_set_buffer(text: WString, cursor_pos: Option) { let mut state = commandline_state_snapshot(); - state.cursor_pos = std::cmp::min(cursor_pos.unwrap_or(usize::MAX), text.len()); + state.cursor_pos = cmp::min(cursor_pos.unwrap_or(usize::MAX), text.len()); state.text = text; } +pub fn commandline_set_search_field(text: WString, cursor_pos: Option) { + let mut state = commandline_state_snapshot(); + assert!(state.search_field.is_some()); + let new_pos = cmp::min(cursor_pos.unwrap_or(usize::MAX), text.len()); + state.search_field = Some((text, new_pos)); +} + /// Return the current interactive reads loop count. Useful for determining how many commands have /// been executed between invocations of code. pub fn reader_run_count() -> u64 { @@ -1163,6 +1174,12 @@ fn update_commandline_state(&self) { snapshot.selection = self.get_selection(); snapshot.pager_mode = !self.pager.is_empty(); snapshot.pager_fully_disclosed = self.current_page_rendering.remaining_to_disclose == 0; + snapshot.search_field = self.pager.search_field_shown.then(|| { + ( + self.pager.search_field_line.text().to_owned(), + self.pager.search_field_line.position(), + ) + }); snapshot.search_mode = self.history_search.active(); snapshot.initialized = true; } @@ -1179,6 +1196,20 @@ fn apply_commandline_state_changes(&mut self) { self.clear_pager(); self.set_buffer_maintaining_pager(&state.text, state.cursor_pos, false); self.reset_loop_state = true; + } else if let Some((new_search_field, new_cursor_pos)) = state.search_field { + if !self.pager.search_field_shown { + return; // Not yet supported. + } + if new_search_field == self.pager.search_field_line.text() + && new_cursor_pos == self.pager.search_field_line.position() + { + return; + } + self.push_edit( + EditableLineTag::SearchField, + Edit::new(0..self.pager.search_field_line.len(), new_search_field), + ); + self.pager.search_field_line.set_position(new_cursor_pos); } }