diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3d72b987a..681886e3e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,7 @@ Interactive improvements New or improved bindings ^^^^^^^^^^^^^^^^^^^^^^^^ - The :kbd:`Alt-H` binding will now show the manpage of the command under cursor instead of the always skipping ``sudo`` and the likes (:issue:`9020`). +- New special input function ``history-pager`` (:kbd:``Control-R``) opens the command history in the searchable pager (incremental search) (:issue:`602`). Improved prompts ^^^^^^^^^^^^^^^^ diff --git a/doc_src/cmds/bind.rst b/doc_src/cmds/bind.rst index e192f788d..a44c69203 100644 --- a/doc_src/cmds/bind.rst +++ b/doc_src/cmds/bind.rst @@ -196,6 +196,9 @@ The following special input functions are available: move one word to the right; or if at the end of the commandline, accept one word from the current autosuggestion. +``history-pager`` + invoke the searchable pager on history (incremental search). + ``history-search-backward`` search the history for the previous match diff --git a/share/functions/fish_default_key_bindings.fish b/share/functions/fish_default_key_bindings.fish index a0ea7aa2a..68e676e3f 100644 --- a/share/functions/fish_default_key_bindings.fish +++ b/share/functions/fish_default_key_bindings.fish @@ -85,8 +85,7 @@ function fish_default_key_bindings -d "emacs-like key binds" bind --preset $argv \ed kill-word - # Let ctrl+r search history if there is something in the commandline. - bind --preset $argv \cr 'commandline | string length -q; and commandline -f history-search-backward' + bind --preset $argv \cr history-pager # term-specific special bindings switch "$TERM" diff --git a/src/complete.h b/src/complete.h index f2ac9a65d..72dd39e18 100644 --- a/src/complete.h +++ b/src/complete.h @@ -45,6 +45,8 @@ enum { COMPLETE_DONT_SORT = 1 << 5, /// This completion looks to have the same string as an existing argument. COMPLETE_DUPLICATES_ARGUMENT = 1 << 6, + /// This completes not just a token but replaces the entire commandline. + COMPLETE_REPLACES_COMMANDLINE = 1 << 7, }; using complete_flags_t = uint8_t; diff --git a/src/input.cpp b/src/input.cpp index 329b8e6b4..d5b6a4a24 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -130,6 +130,7 @@ static constexpr const input_function_metadata_t input_function_metadata[] = { {L"forward-jump-till", readline_cmd_t::forward_jump_till}, {L"forward-single-char", readline_cmd_t::forward_single_char}, {L"forward-word", readline_cmd_t::forward_word}, + {L"history-pager", readline_cmd_t::history_pager}, {L"history-prefix-search-backward", readline_cmd_t::history_prefix_search_backward}, {L"history-prefix-search-forward", readline_cmd_t::history_prefix_search_forward}, {L"history-search-backward", readline_cmd_t::history_search_backward}, diff --git a/src/input_common.h b/src/input_common.h index 6cec3c7f7..da7558137 100644 --- a/src/input_common.h +++ b/src/input_common.h @@ -25,6 +25,7 @@ enum class readline_cmd_t { history_search_forward, history_prefix_search_backward, history_prefix_search_forward, + history_pager, delete_char, backward_delete_char, kill_line, diff --git a/src/pager.cpp b/src/pager.cpp index a5795a1f6..99d46f274 100644 --- a/src/pager.cpp +++ b/src/pager.cpp @@ -377,6 +377,7 @@ void pager_t::refilter_completions() { } void pager_t::set_completions(const completion_list_t &raw_completions) { + selected_completion_idx = PAGER_SELECTION_NONE; // Get completion infos out of it. unfiltered_completion_infos = process_completions_into_infos(raw_completions); @@ -498,7 +499,7 @@ bool pager_t::completion_try_print(size_t cols, const wcstring &prefix, const co // these are the "past the last value". progress_text = format_string(_(L"rows %lu to %lu of %lu"), start_row + 1, stop_row, row_count); - } else if (completion_infos.empty() && !unfiltered_completion_infos.empty()) { + } else if (search_field_shown && completion_infos.empty()) { // Everything is filtered. progress_text = _(L"(no matches)"); } diff --git a/src/reader.cpp b/src/reader.cpp index 84d2fc867..f2693cafb 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -185,6 +185,12 @@ static debounce_t &debounce_highlighting() { return *res; } +static debounce_t &debounce_history_pager() { + const long kHistoryPagerTimeoutMs = 500; + static auto res = new debounce_t(kHistoryPagerTimeoutMs); + return *res; +} + bool edit_t::operator==(const edit_t &other) const { return cursor_position_before_edit == other.cursor_position_before_edit && offset == other.offset && length == other.length && old == other.old && @@ -563,6 +569,10 @@ struct highlight_result_t { wcstring text; }; +struct history_pager_result_t { + completion_list_t matched_commands; +}; + /// readline_loop_state_t encapsulates the state used in a readline loop. /// It is always stack allocated transient. This state should not be "publicly visible"; public /// state should be in reader_data_t. @@ -682,6 +692,8 @@ class reader_data_t : public std::enable_shared_from_this { std::shared_ptr history{}; /// The history search. reader_history_search_t history_search{}; + /// Whether the in-pager history search is active. + bool history_pager_active{false}; /// The cursor selection mode. cursor_selection_mode_t cursor_selection_mode{cursor_selection_mode_t::exclusive}; @@ -721,7 +733,9 @@ class reader_data_t : public std::enable_shared_from_this { wcstring in_flight_highlight_request; wcstring in_flight_autosuggest_request; - bool is_navigating_pager_contents() const { return this->pager.is_navigating_contents(); } + bool is_navigating_pager_contents() const { + return this->pager.is_navigating_contents() || history_pager_active; + } /// The line that is currently being edited. Typically the command line, but may be the search /// field. @@ -740,6 +754,7 @@ class reader_data_t : public std::enable_shared_from_this { /// Do what we need to do whenever our command line changes. void command_line_changed(const editable_line_t *el); void maybe_refilter_pager(const editable_line_t *el); + void fill_history_pager(); /// Do what we need to do whenever our pager selection changes. void pager_selection_changed(); @@ -828,7 +843,8 @@ class reader_data_t : public std::enable_shared_from_this { void handle_readline_command(readline_cmd_t cmd, readline_loop_state_t &rls); void clear_pager(); - void select_completion_in_direction(selection_motion_t dir); + void select_completion_in_direction(selection_motion_t dir, + bool force_selection_change = false); void flash(); maybe_t get_selection() const; @@ -1201,6 +1217,10 @@ void reader_data_t::command_line_changed(const editable_line_t *el) { // Update the gen count. s_generation.store(1 + read_generation_count(), std::memory_order_relaxed); } else if (el == &this->pager.search_field_line) { + if (history_pager_active) { + fill_history_pager(); + return; + } this->pager.refilter_completions(); this->pager_selection_changed(); } @@ -1214,6 +1234,38 @@ void reader_data_t::maybe_refilter_pager(const editable_line_t *el) { } } +static history_pager_result_t history_pager_search(const std::shared_ptr &history, + const wcstring &search_string) { + // Use a small page size because we don't always offer practical ways to select a item from + // a large page (since we don't support subsequence filtering here). + constexpr size_t page_size = 12; + completion_list_t completions; + history_search_t search{history, search_string, history_search_type_t::contains, + smartcase_flags(search_string)}; + while (completions.size() < page_size && search.go_backwards()) { + const history_item_t &item = search.current_item(); + completions.push_back(completion_t{ + item.str(), L"", string_fuzzy_match_t::exact_match(), + COMPLETE_REPLACES_COMMANDLINE | COMPLETE_DONT_ESCAPE | COMPLETE_DONT_SORT}); + } + return {completions}; +} + +void reader_data_t::fill_history_pager() { + auto shared_this = this->shared_from_this(); + const wcstring &search_term = pager.search_field_line.text(); + debounce_history_pager().perform( + [=]() { return history_pager_search(shared_this->history, search_term); }, + [shared_this, search_term](const history_pager_result_t &result) { + if (search_term != shared_this->pager.search_field_line.text()) + return; // Stale request. + shared_this->pager.set_completions(result.matched_commands); + shared_this->select_completion_in_direction(selection_motion_t::next, true); + shared_this->super_highlight_me_plenty(); + shared_this->layout_and_repaint(L"history-pager"); + }); +} + void reader_data_t::pager_selection_changed() { ASSERT_IS_MAIN_THREAD(); @@ -1486,6 +1538,7 @@ static bool command_ends_paging(readline_cmd_t c, bool focused_on_search_field) } case rl::complete: case rl::complete_and_search: + case rl::history_pager: case rl::backward_char: case rl::forward_char: case rl::forward_single_char: @@ -1590,14 +1643,14 @@ void reader_data_t::delete_char(bool backward) { /// using syntax highlighting, etc. /// Returns true if the string changed. void reader_data_t::insert_string(editable_line_t *el, const wcstring &str) { - if (str.empty()) return; - - if (!history_search.active() && want_to_coalesce_insertion_of(*el, str)) { - el->insert_coalesce(str); - assert(el->undo_history.may_coalesce); - } else { - el->push_edit(edit_t(el->position(), 0, str)); - el->undo_history.may_coalesce = el->undo_history.try_coalesce || (str.size() == 1); + if (!str.empty()) { + if (!history_search.active() && want_to_coalesce_insertion_of(*el, str)) { + el->insert_coalesce(str); + assert(el->undo_history.may_coalesce); + } else { + el->push_edit(edit_t(el->position(), 0, str)); + el->undo_history.may_coalesce = el->undo_history.try_coalesce || (str.size() == 1); + } } if (el == &command_line) { @@ -1640,6 +1693,7 @@ wcstring completion_apply_to_command_line(const wcstring &val, complete_flags_t bool append_only) { bool add_space = !bool(flags & COMPLETE_NO_SPACE); bool do_replace = bool(flags & COMPLETE_REPLACES_TOKEN); + bool do_replace_commandline = bool(flags & COMPLETE_REPLACES_COMMANDLINE); bool do_escape = !bool(flags & COMPLETE_DONT_ESCAPE); bool no_tilde = bool(flags & COMPLETE_DONT_ESCAPE_TILDES); @@ -1647,6 +1701,12 @@ wcstring completion_apply_to_command_line(const wcstring &val, complete_flags_t bool back_into_trailing_quote = false; bool have_space_after_token = command_line[cursor_pos] == L' '; + if (do_replace_commandline) { + assert(!do_escape && "unsupported completion flag"); + *inout_cursor_pos = val.size(); + return val; + } + if (do_replace) { size_t move_cursor; const wchar_t *begin, *end; @@ -1935,11 +1995,15 @@ void reader_data_t::accept_autosuggestion(bool full, bool single, move_word_styl } // Ensure we have no pager contents. -void reader_data_t::clear_pager() { pager.clear(); } +void reader_data_t::clear_pager() { + pager.clear(); + history_pager_active = false; +} -void reader_data_t::select_completion_in_direction(selection_motion_t dir) { +void reader_data_t::select_completion_in_direction(selection_motion_t dir, + bool force_selection_change) { bool selection_changed = pager.select_next_completion_in_direction(dir, current_page_rendering); - if (selection_changed) { + if (force_selection_change || selection_changed) { pager_selection_changed(); } } @@ -3641,6 +3705,26 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat } break; } + case rl::history_pager: { + if (history_pager_active) { + // TODO Deepen search. + break; + } + + // Record our cycle_command_line. + cycle_command_line = command_line.text(); + cycle_cursor_pos = command_line.position(); + + this->history_pager_active = true; + this->history_pager_history_index_start = 0; + this->history_pager_history_index_end = 0; + // Update the pager data. + pager.set_search_field_shown(true); + pager.set_prefix(L""); + // Update the search field, which triggers the actual history search. + insert_string(&pager.search_field_line, command_line.text()); + break; + } case rl::backward_char: { editable_line_t *el = active_edit_line(); if (is_navigating_pager_contents()) {