diff --git a/doc_src/cmds/bind.rst b/doc_src/cmds/bind.rst index 1d22b4541..998e888b2 100644 --- a/doc_src/cmds/bind.rst +++ b/doc_src/cmds/bind.rst @@ -120,6 +120,8 @@ The following special input functions are available: - ``execute`` run the current commandline +- ``force-repaint`` reexecute the prompt functions without coalescing + - ``forward-bigword``, move one whitespace-delimited word to the right - ``forward-char``, move one character to the right @@ -160,7 +162,9 @@ The following special input functions are available: - ``repaint-mode`` reexecutes the fish_mode_prompt function and redraws the prompt. This is useful for vi-mode. If no fish_mode_prompt exists, it acts like a normal repaint. -- ``force-repaint`` reexecute the prompt functions without coalescing. +- ``self-insert``, inserts the matching sequence into the command line + +- ``self-insert-notfirst``, inserts the matching sequence into the command line, unless the cursor is at the beginning - ``suppress-autosuggestion``, remove the current autosuggestion diff --git a/share/functions/__fish_shared_key_bindings.fish b/share/functions/__fish_shared_key_bindings.fish index 668f59228..3a030ee64 100644 --- a/share/functions/__fish_shared_key_bindings.fish +++ b/share/functions/__fish_shared_key_bindings.fish @@ -161,7 +161,7 @@ function __fish_shared_key_bindings -d "Bindings shared between emacs and vi mod bind --preset -M paste \\ "__fish_commandline_insert_escaped \\\ \$__fish_paste_quoted" # Only insert spaces if we're either quoted or not at the beginning of the commandline # - this strips leading spaces if they would trigger histignore. - bind --preset -M paste \ 'if set -q __fish_paste_quoted[1]; or string length -q -- (commandline -c); commandline -i " "; end' + bind --preset -M paste " " self-insert-notfirst end function __fish_commandline_insert_escaped --description 'Insert the first arg escaped if a second arg is given' diff --git a/src/input.cpp b/src/input.cpp index 1dc5b979d..aac96e983 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -117,6 +117,7 @@ static const input_function_metadata_t input_function_metadata[] = { {readline_cmd_t::history_token_search_backward, L"history-token-search-backward"}, {readline_cmd_t::history_token_search_forward, L"history-token-search-forward"}, {readline_cmd_t::self_insert, L"self-insert"}, + {readline_cmd_t::self_insert_notfirst, L"self-insert-notfirst"}, {readline_cmd_t::transpose_chars, L"transpose-chars"}, {readline_cmd_t::transpose_words, L"transpose-words"}, {readline_cmd_t::upcase_word, L"upcase-word"}, @@ -497,7 +498,8 @@ char_event_t inputter_t::readch(bool allow_commands) { if (evt.is_readline()) { switch (evt.get_readline()) { - case readline_cmd_t::self_insert: { + case readline_cmd_t::self_insert: + case readline_cmd_t::self_insert_notfirst: { // Typically self-insert is generated by the generic (empty) binding. // However if it is generated by a real sequence, then insert that sequence. for (auto iter = evt.seq.crbegin(); iter != evt.seq.crend(); ++iter) { @@ -505,7 +507,13 @@ char_event_t inputter_t::readch(bool allow_commands) { } // Issue #1595: ensure we only insert characters, not readline functions. The // common case is that this will be empty. - return read_characters_no_readline(); + char_event_t res = read_characters_no_readline(); + + // Hackish: mark the input style. + res.input_style = evt.get_readline() == readline_cmd_t::self_insert_notfirst + ? char_event_t::style_notfirst + : char_event_t::style_normal; + return res; } case readline_cmd_t::func_and: { if (function_status_) { diff --git a/src/input_common.h b/src/input_common.h index f7351f3c4..646674fa9 100644 --- a/src/input_common.h +++ b/src/input_common.h @@ -42,6 +42,7 @@ enum class readline_cmd_t { history_token_search_backward, history_token_search_forward, self_insert, + self_insert_notfirst, transpose_chars, transpose_words, upcase_word, @@ -111,6 +112,17 @@ class char_event_t { /// The type of event. char_event_type_t type; + /// Hackish: the input style, which describes how char events (only) are applied to the command + /// line. Note this is set only after applying bindings; it is not set from readb(). + enum input_style_t : uint8_t { + // Insert characters normally. + style_normal, + + // Insert characters only if the cursor is not at the beginning. Otherwise, discard them. + style_notfirst, + }; + input_style_t input_style{style_normal}; + /// The sequence of characters in the input mapping which generated this event. /// Note that the generic self-insert case does not have any characters, so this would be empty. wcstring seq{}; diff --git a/src/reader.cpp b/src/reader.cpp index cfb45b61f..839300430 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -1180,6 +1180,7 @@ static bool command_ends_paging(readline_cmd_t c, bool focused_on_search_field) case rl::backward_kill_path_component: case rl::backward_kill_bigword: case rl::self_insert: + case rl::self_insert_notfirst: case rl::transpose_chars: case rl::transpose_words: case rl::upcase_word: @@ -2558,47 +2559,36 @@ struct readline_loop_state_t { /// Read normal characters, inserting them into the command line. /// \return the next unhandled event. maybe_t reader_data_t::read_normal_chars(readline_loop_state_t &rls) { - maybe_t event_needing_handling = inputter.readch(); - - if (!event_is_normal_char(*event_needing_handling) || !can_read(STDIN_FILENO)) - return event_needing_handling; - - // This is a normal character input. - // We are going to handle it directly, accumulating more. - char_event_t evt = event_needing_handling.acquire(); + maybe_t event_needing_handling{}; + wcstring accumulated_chars; size_t limit = std::min(rls.nchars - command_line.size(), READAHEAD_MAX); - - wchar_t arr[READAHEAD_MAX + 1] = {}; - arr[0] = evt.get_char(); - - for (size_t i = 1; i < limit; ++i) { - if (!can_read(0)) { + while (accumulated_chars.size() < limit) { + bool allow_commands = (accumulated_chars.empty()); + auto evt = inputter.readch(allow_commands); + if (!event_is_normal_char(evt) || !can_read(STDIN_FILENO)) { + event_needing_handling = std::move(evt); break; - } - // Only allow commands on the first key; otherwise, we might have data we - // need to insert on the commandline that the command might need to be able - // to see. - auto next_event = inputter.readch(false); - if (event_is_normal_char(next_event)) { - arr[i] = next_event.get_char(); + } else if (evt.input_style == char_event_t::style_notfirst && accumulated_chars.empty() && + active_edit_line()->position() == 0) { + // The cursor is at the beginning and nothing is accumulated, so skip this character. + continue; } else { - // We need to process this in the outer loop. - assert(!event_needing_handling && "Should not have an unhandled event"); - event_needing_handling = next_event; - break; + accumulated_chars.push_back(evt.get_char()); } } - editable_line_t *el = active_edit_line(); - insert_string(el, arr); + if (!accumulated_chars.empty()) { + editable_line_t *el = active_edit_line(); + insert_string(el, accumulated_chars); - // End paging upon inserting into the normal command line. - if (el == &command_line) { - clear_pager(); + // End paging upon inserting into the normal command line. + if (el == &command_line) { + clear_pager(); + } + + // Since we handled a normal character, we don't have a last command. + rls.last_cmd.reset(); } - - // Since we handled a normal character, we don't have a last command. - rls.last_cmd.reset(); return event_needing_handling; } @@ -3373,12 +3363,11 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } break; } - // Some commands should have been handled internally by input_readch(). - case rl::self_insert: { - DIE("self-insert should have been handled by inputter_t::readch"); - } + // Some commands should have been handled internally by inputter_t::readch(). + case rl::self_insert: + case rl::self_insert_notfirst: case rl::func_and: { - DIE("self-insert should have been handled by inputter_t::readch"); + DIE("should have been handled by inputter_t::readch"); } } } @@ -3492,8 +3481,11 @@ maybe_t reader_data_t::readline(int nchars_or_0) { } else { // Ordinary char. wchar_t c = event_needing_handling->get_char(); - if (!fish_reserved_codepoint(c) && (c >= L' ' || c == L'\n' || c == L'\r') && - c != 0x7F) { + if (event_needing_handling->input_style == char_event_t::style_notfirst && + active_edit_line()->position() == 0) { + // This character is skipped. + } else if (!fish_reserved_codepoint(c) && (c >= L' ' || c == L'\n' || c == L'\r') && + c != 0x7F) { // Regular character. editable_line_t *el = active_edit_line(); insert_char(active_edit_line(), c); diff --git a/tests/bind.expect b/tests/bind.expect index bb9984dbf..3b9a3290c 100644 --- a/tests/bind.expect +++ b/tests/bind.expect @@ -299,3 +299,15 @@ expect_prompt -re {nul seen\r\nnul seen\r\nnul seen} { } unmatched { puts stderr "nul not seen" } + +# Test self-insert-notfirst. (#6603) +# Here the leading 'q's should be stripped, but the trailing ones not. +send "bind q self-insert-notfirst\r" +expect_prompt +send "qqqecho qqq" +send "\r" +expect_prompt -re {qqq} { + puts "Leading q properly stripped" +} unmatched { + puts stderr "Leading qs not stripped" +} diff --git a/tests/bind.expect.out b/tests/bind.expect.out index e8c467f53..a10e5fb0c 100644 --- a/tests/bind.expect.out +++ b/tests/bind.expect.out @@ -23,3 +23,4 @@ ctrl-o seen ctrl-w stops at : ctrl-w stops at @ nul seen +Leading q properly stripped