diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 106541e18..4a2a086f7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,6 +31,7 @@ Scripting improvements Interactive improvements ------------------------ - Command-specific tab completions may now offer results whose first character is a period. For example, it is now possible to tab-complete ``git add`` for files with leading periods. The default file completions hide these files, unless the token itself has a leading period (:issue:`3707`). +- The :kbd:`Control-R` history search now uses glob syntax (:issue:`10131`). New or improved bindings ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/parse_util.cpp b/src/parse_util.cpp index fc4e03722..c0bb989a7 100644 --- a/src/parse_util.cpp +++ b/src/parse_util.cpp @@ -466,6 +466,48 @@ wcstring parse_util_unescape_wildcards(const wcstring &str) { return result; } +bool parse_util_contains_wildcards(const wcstring &str) { + bool unesc_qmark = !feature_test(feature_flag_t::qmark_noglob); + + const wchar_t *const cs = str.c_str(); + for (size_t i = 0; cs[i] != L'\0'; i++) { + if (cs[i] == L'*') { + return true; + } else if (cs[i] == L'?' && unesc_qmark) { + return true; + } else if (cs[i] == L'\\' && cs[i + 1] == L'*') { + i += 1; + } else if (cs[i] == L'\\' && cs[i + 1] == L'?' && unesc_qmark) { + i += 1; + } else if (cs[i] == L'\\' && cs[i + 1] == L'\\') { + // Not a wildcard, but ensure the next iteration doesn't see this escaped backslash. + i += 1; + } + } + return false; +} + +wcstring parse_util_escape_wildcards(const wcstring &str) { + wcstring result; + result.reserve(str.size()); + bool unesc_qmark = !feature_test(feature_flag_t::qmark_noglob); + + const wchar_t *const cs = str.c_str(); + for (size_t i = 0; cs[i] != L'\0'; i++) { + if (cs[i] == L'*') { + result.append(L"\\*"); + } else if (cs[i] == L'?' && unesc_qmark) { + result.append(L"\\?"); + } else if (cs[i] == L'\\') { + result.append(L"\\\\"); + } else { + result.push_back(cs[i]); + } + } + return result; +} + + /// Find the outermost quoting style of current token. Returns 0 if token is not quoted. static wchar_t get_quote(const wcstring &cmd_str, size_t len) { size_t i = 0; diff --git a/src/parse_util.h b/src/parse_util.h index f4a075035..7f6d786ed 100644 --- a/src/parse_util.h +++ b/src/parse_util.h @@ -96,6 +96,14 @@ size_t parse_util_get_offset(const wcstring &str, int line, long line_offset); /// transformation. wcstring parse_util_unescape_wildcards(const wcstring &str); +/// Return if the given string contains wildcard characters. +bool parse_util_contains_wildcards(const wcstring &str); + +/// Escape any wildcard characters in the given string. e.g. convert +/// "a*b" to "a\*b". +wcstring parse_util_escape_wildcards(const wcstring &str); + + /// Calculates information on the parameter at the specified index. /// /// \param cmd The command to be analyzed diff --git a/src/reader.cpp b/src/reader.cpp index aa60466fd..afe45270e 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -1135,17 +1135,21 @@ static history_pager_result_t history_pager_search(const HistorySharedPtr &histo size_t page_size = std::max(termsize_last().height / 2 - 2, (rust::isize)12); rust::Box completions = new_completion_list(); + rust::Box search = - rust_history_search_new(history, search_string.c_str(), history_search_type_t::Contains, + rust_history_search_new(history, search_string.c_str(), history_search_type_t::ContainsGlob, smartcase_flags(search_string), history_index); bool next_match_found = search->go_to_next_match(direction); - if (!next_match_found) { - // If there were no matches, try again with subsequence search + + if (!next_match_found && !parse_util_contains_wildcards(search_string)) { + // If there were no matches, and the user is not intending for + // wildcard search, try again with subsequence search. search = rust_history_search_new(history, search_string.c_str(), history_search_type_t::ContainsSubsequence, smartcase_flags(search_string), history_index); next_match_found = search->go_to_next_match(direction); } + while (completions->size() < page_size && next_match_found) { const history_item_t &item = search->current_item(); completions->push_back(*new_completion_with( @@ -3671,7 +3675,9 @@ void reader_data_t::handle_readline_command(readline_cmd_t c, readline_loop_stat pager.set_prefix(MB_CUR_MAX > 1 ? L"► " : L"> ", false /* highlight */); // Update the search field, which triggers the actual history search. if (!history_search.active() || history_search.search_string().empty()) { - insert_string(pager.search_field_line(), *command_line.text()); + // Escape any wildcards the user may have in their input. + auto escaped_command_line = parse_util_escape_wildcards(*command_line.text()); + insert_string(pager.search_field_line(), escaped_command_line); } else { // If we have an actual history search already going, reuse that term // - this is if the user looks around a bit and decides to switch to the pager.