From f415413bfb1bb8c907faa87d92e37e99c5add4d4 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Fri, 28 Feb 2025 03:58:51 +0100 Subject: [PATCH] Strip "$ " prefixes on paste Code blocks are often written like $ echo hello world hello world The "$ " is widely understood to introduce a shell command. It's often easier to copy the whole line than copying everything after "$ ". This gets more pronounced when there are multiple commands without interleaved output (either due to omission or the rule of silence). Copying the whole code block is the most natural first step. You could argue that this is a presentation issue - the dollar prefix should be rendered but not copied to clipboard. But in my experience there are many cases where there is no HTML or Javascript that would allow the copy-to-clipboard functionality to strip the prefixes. The "$ " prefix is almost never useful when pasting; strip it automatically. Privileged commands use "# " as prefix which overlaps with comments, so do not strip that until we can disambiguate (another potential reason not to do that would be safety but it's unclear if that really matters). Add the new logic to the commandline builtin, because we don't know about the AST in fish script. (Technically, the tokenizer already knows whether a "$ " is in command position and at the beginning of a line, but we don't have that either (yet).) Maybe we should move the rest of __fish_paste over as well. I'm not sure what difference that would make; for one, pasting could no longer be cancelled by ctrl-c (in theory), which seems like a good direction? --- CHANGELOG.rst | 1 + doc_src/cmds/commandline.rst | 5 +- share/completions/commandline.fish | 1 + share/functions/__fish_paste.fish | 2 +- src/builtins/commandline.rs | 77 +++++++++++++++++++++++++++++- src/parse_constants.rs | 2 +- tests/checks/commandline.fish | 7 +++ 7 files changed, 89 insertions(+), 6 deletions(-) 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)