mirror of
https://github.com/fish-shell/fish-shell.git
synced 2024-11-22 08:41:13 +08:00
Add Control+R incremental history search in pager
This reimplements ridiculousfish/control_r which is a more future-proof approach than #6686. Pressing Control+R shows history in our pager and allows to search filter commands with the pager search field. On the surface, this works just like in other shells; though there are some differences. - Our pager shows multiple results at a time. - Other shells allow to use up arrow/down arrow to select adjacent entries in history. Shouldn't be hard to implement but the hidden state might confuse users and it doesn't play well with up-or-search, so this is left out. Users might expect the history pager to use subsequence matching (fuzzy matching) like the completion pager, however due to the history pager design it uses substring matching. We could change this in future, however that means we would also want to change the ordering from "reverse-chronological" to "longest common subsequence" (e.g. what fuzzy finders do), because otherwise a query "fis" might give this ordering: fsck /dev/disk/by-partlabel/Linux\x20filesystem fish which is probably not what the user wants. The pager shows only a small number of history items at a time. This is because, as explained above, the history pager does not support subsequence matching, so navigating it does not scale well. Closes #602
This commit is contained in:
parent
b0233c9aa7
commit
dcff0a2f2b
|
@ -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
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)");
|
||||
}
|
||||
|
|
110
src/reader.cpp
110
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<reader_data_t> {
|
|||
std::shared_ptr<history_t> 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<reader_data_t> {
|
|||
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<reader_data_t> {
|
|||
/// 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<reader_data_t> {
|
|||
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<source_range_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_t> &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()) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user