diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e7900c570..7adf1622a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,7 @@ Interactive improvements - The history search now preserves ordering between :kbd:`ctrl-s` forward and :kbd:`ctrl-r` backward searches. - Left mouse click now can select pager items. - Instead of flashing all the text to the left of the cursor, fish now flashes the matched token during history token search, the completed token during completion (:issue:`11050`), the autosuggestion when deleting it, and the full command line in all other cases. +- Pasted commands are now stripped of any ``$ `` prefix. New or improved bindings ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc_src/cmds/commandline.rst b/doc_src/cmds/commandline.rst index 808a0093c..627de3041 100644 --- a/doc_src/cmds/commandline.rst +++ b/doc_src/cmds/commandline.rst @@ -45,8 +45,9 @@ The following options change the way ``commandline`` updates the command line bu **-a** or **--append** Do not remove the current commandline, append the specified string at the end of it. -**-i** or **--insert** - Do not remove the current commandline, insert the specified string at the current cursor position +**-i**, **--insert** or **--insert-smart** + Do not remove the current commandline, insert the specified string at the current cursor position. + The **--insert-smart** option turns on a Do-What-I-Mean (DWIM) mode: it strips any **$** prefix from the first command on each line. **-r** or **--replace** Remove the current commandline and replace it with the specified string (default) diff --git a/share/completions/commandline.fish b/share/completions/commandline.fish index 5855e31af..3bfa83c2c 100644 --- a/share/completions/commandline.fish +++ b/share/completions/commandline.fish @@ -1,6 +1,7 @@ complete -c commandline -s h -l help -d "Display help and exit" complete -c commandline -s a -l append -d "Add text to the end of the selected area" complete -c commandline -s i -l insert -d "Add text at cursor" +complete -c commandline -s i -l insert-smart -d 'Add text at cursor but DWIM, stripping leading $' complete -c commandline -s r -l replace -d "Replace selected part" complete -c commandline -s j -l current-job -d "Select job under cursor" diff --git a/share/functions/__fish_paste.fish b/share/functions/__fish_paste.fish index aca62434e..981c0983a 100644 --- a/share/functions/__fish_paste.fish +++ b/share/functions/__fish_paste.fish @@ -43,6 +43,6 @@ function __fish_paste end if test -n "$data" - commandline -i -- $data + commandline --insert-smart -- $data end end diff --git a/src/builtins/commandline.rs b/src/builtins/commandline.rs index 580cd1431..4944b4dfc 100644 --- a/src/builtins/commandline.rs +++ b/src/builtins/commandline.rs @@ -1,11 +1,12 @@ use super::prelude::*; +use crate::ast::{Ast, Leaf}; use crate::common::{unescape_string, UnescapeFlags, UnescapeStringStyle}; use crate::complete::Completion; use crate::expand::{expand_string, ExpandFlags, ExpandResultCode}; use crate::input::input_function_get_code; use crate::input_common::{CharEvent, ReadlineCmd}; use crate::operation_context::{no_cancel, OperationContext}; -use crate::parse_constants::ParserTestErrorBits; +use crate::parse_constants::{ParseTreeFlags, ParserTestErrorBits}; use crate::parse_util::{ parse_util_detect_errors, parse_util_get_offset_from_line, parse_util_job_extent, parse_util_lineno, parse_util_process_extent, parse_util_token_extent, @@ -31,11 +32,14 @@ enum TextScope { } /// For text insertion, how should it be done. +#[derive(Eq, PartialEq)] enum AppendMode { // replace current text Replace, // insert at cursor position Insert, + // insert at cursor position, DWIM style. + InsertSmart, // insert at end of current token/command/buffer Append, } @@ -75,9 +79,12 @@ fn replace_part( out.push_utfstr(&buff[range.clone()]); out.push_utfstr(insert); } - AppendMode::Insert => { + AppendMode::Insert | AppendMode::InsertSmart => { assert!(cursor_pos >= range.start); assert!(cursor_pos <= range.end); + let insert = strip_dollar_prefixes(insert_mode, &buff[range.start..cursor_pos], insert) + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed(insert)); out.push_utfstr(&buff[range.start..cursor_pos]); out.push_utfstr(&insert); out.push_utfstr(&buff[cursor_pos..range.end]); @@ -93,6 +100,43 @@ fn replace_part( } } +// Prefix must be at the beginning of a process. +fn strip_dollar_prefixes(insert_mode: AppendMode, prefix: &wstr, insert: &wstr) -> Option { + if insert_mode != AppendMode::InsertSmart { + return None; + } + insert.find(L!("$ "))?; // Early return. + let source = prefix.to_owned() + insert; + let ast = Ast::parse( + &source, + ParseTreeFlags::ACCEPT_INCOMPLETE_TOKENS | ParseTreeFlags::LEAVE_UNTERMINATED, + None, + ); + let mut stripped = WString::new(); + let mut have = prefix.len(); + for node in ast.walk() { + let Some(ds) = node.as_decorated_statement() else { + continue; + }; + let Some(range) = ds.command.range() else { + continue; + }; + let pos = range.start(); + if pos < prefix.len() { + continue; + } + if (pos == 0 || source.as_char_slice()[pos - 1] == '\n') + && source.as_char_slice()[pos] == '$' + && source.char_at(pos + 1) == ' ' + { + stripped.push_utfstr(&source[have..pos]); + have = pos + "$ ".len(); + } + } + stripped.push_utfstr(&source[have..]); + return Some(stripped); +} + /// Output the specified selection. /// /// \param begin start of selection @@ -222,6 +266,7 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) let long_options: &[WOption] = &[ wopt(L!("append"), ArgType::NoArgument, 'a'), wopt(L!("insert"), ArgType::NoArgument, 'i'), + wopt(L!("insert-smart"), ArgType::NoArgument, '\x06'), wopt(L!("replace"), ArgType::NoArgument, 'r'), wopt(L!("current-buffer"), ArgType::NoArgument, 'b'), wopt(L!("current-job"), ArgType::NoArgument, 'j'), @@ -255,6 +300,7 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) 'a' => append_mode = Some(AppendMode::Append), 'b' => buffer_part = Some(TextScope::String), 'i' => append_mode = Some(AppendMode::Insert), + '\x06' => append_mode = Some(AppendMode::InsertSmart), 'r' => append_mode = Some(AppendMode::Replace), 'c' => cut_at_cursor = true, 't' => buffer_part = Some(TextScope::Token), @@ -422,6 +468,33 @@ pub fn commandline(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) let buffer_part = buffer_part.unwrap_or(TextScope::String); + if append_mode == AppendMode::InsertSmart { + if search_field_mode { + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_COMBO2_EXCLUSIVE, + cmd, + "--insert-smart", + "--search-field" + )); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + match buffer_part { + TextScope::String | TextScope::Job | TextScope::Process => (), + TextScope::Token => { + // To-do: we can support it in command position. + streams.err.append(wgettext_fmt!( + BUILTIN_ERR_COMBO2_EXCLUSIVE, + cmd, + "--insert-smart", + "--current-token" + )); + builtin_print_error_trailer(parser, streams.err, cmd); + return STATUS_INVALID_ARGS; + } + } + } + if line_mode || column_mode { if positional_args != 0 { let arg = w.argv[w.wopt_index]; diff --git a/src/parse_constants.rs b/src/parse_constants.rs index 4ad84fe9c..e4e3ea4c3 100644 --- a/src/parse_constants.rs +++ b/src/parse_constants.rs @@ -17,7 +17,7 @@ pub struct ParseTreeFlags: u8 { const CONTINUE_AFTER_ERROR = 1 << 0; /// include comment tokens. const INCLUDE_COMMENTS = 1 << 1; - /// indicate that the tokenizer should accept incomplete tokens */ + /// indicate that the tokenizer should accept incomplete tokens const ACCEPT_INCOMPLETE_TOKENS = 1 << 2; /// indicate that the parser should not generate the terminate token, allowing an 'unfinished' /// tree where some nodes may have no productions. diff --git a/tests/checks/commandline.fish b/tests/checks/commandline.fish index 6562d5873..52a323be3 100644 --- a/tests/checks/commandline.fish +++ b/tests/checks/commandline.fish @@ -25,3 +25,10 @@ echo Help $status commandline -pC 0 --input "test | test" echo $status # CHECK: 0 + +commandline --insert-smart '$ echo 123' --current-token +# CHECKERR: commandline: --insert-smart --current-token: options cannot be used together +# CHECKERR: {{.*}}/commandline.fish (line {{\d+}}): +# CHECKERR: commandline --insert-smart '$ echo 123' --current-token +# CHECKERR: ^ +# CHECKERR: (Type 'help commandline' for related documentation)