diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index 5cb376778..638f3e8f8 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -2289,7 +2289,7 @@ static void test_pager_navigation() { } pager_t pager; - pager.set_completions(completions); + pager.set_completions(completions, wcstring{}); pager.set_term_size(termsize_t::defaults()); page_rendering_t render = pager.render(); @@ -2410,7 +2410,7 @@ static void test_pager_layout() { // These test cases have equal completions and descriptions const completion_t c1(L"abcdefghij", L"1234567890"); - pager.set_completions(completion_list_t(1, c1)); + pager.set_completions(completion_list_t{c1}, wcstring{}); const pager_layout_testcase_t testcases1[] = { {26, L"abcdefghij (1234567890)"}, {25, L"abcdefghij (1234567890)"}, {24, L"abcdefghij (1234567890)"}, {23, L"abcdefghij (12345678…)"}, @@ -2425,7 +2425,7 @@ static void test_pager_layout() { // These test cases have heavyweight completions const completion_t c2(L"abcdefghijklmnopqrs", L"1"); - pager.set_completions(completion_list_t(1, c2)); + pager.set_completions(completion_list_t{c2}, wcstring{}); const pager_layout_testcase_t testcases2[] = { {26, L"abcdefghijklmnopqrs (1)"}, {25, L"abcdefghijklmnopqrs (1)"}, {24, L"abcdefghijklmnopqrs (1)"}, {23, L"abcdefghijklmnopq… (1)"}, @@ -2440,7 +2440,7 @@ static void test_pager_layout() { // These test cases have no descriptions const completion_t c3(L"abcdefghijklmnopqrst", L""); - pager.set_completions(completion_list_t(1, c3)); + pager.set_completions(completion_list_t{c3}, wcstring{}); const pager_layout_testcase_t testcases3[] = { {26, L"abcdefghijklmnopqrst"}, {25, L"abcdefghijklmnopqrst"}, {24, L"abcdefghijklmnopqrst"}, {23, L"abcdefghijklmnopqrst"}, diff --git a/src/pager.cpp b/src/pager.cpp index 0437d793f..a1ca59a91 100644 --- a/src/pager.cpp +++ b/src/pager.cpp @@ -39,6 +39,10 @@ using comp_info_list_t = std::vector; /// Text we use for the search field. #define SEARCH_FIELD_PROMPT _(L"search: ") +/// Maximum length of prefix string when printing completion list. Longer prefixes will be +/// ellipsized. +#define PREFIX_MAX_LEN 9 + inline bool selection_direction_is_cardinal(selection_motion_t dir) { switch (dir) { case selection_motion_t::north: @@ -75,8 +79,10 @@ static size_t divide_round_up(size_t numer, size_t denom) { /// \param max the maximum space that may be used for printing /// \param has_more if this flag is true, this is not the entire string, and the string should be /// ellipsized even if the string fits but takes up the whole space. +/// \param prefix_len Hack: if nonzero, then color the first prefix_len chars with the prefix color. +/// \param prefix_color the color to use for the first prefix_len characters. static size_t print_max(const wcstring &str, highlight_spec_t color, size_t max, bool has_more, - line_t *line) { + line_t *line, size_t prefix_len = 0, highlight_spec_t prefix_color = {}) { size_t remaining = max; for (size_t i = 0; i < str.size(); i++) { wchar_t c = str.at(i); @@ -97,7 +103,7 @@ static size_t print_max(const wcstring &str, highlight_spec_t color, size_t max, break; } - line->append(c, color); + line->append(c, i < prefix_len ? prefix_color : color); assert(remaining >= width_c); remaining -= width_c; } @@ -108,8 +114,8 @@ static size_t print_max(const wcstring &str, highlight_spec_t color, size_t max, } /// Print the specified item using at the specified amount of space. -line_t pager_t::completion_print_item(const wcstring &prefix, const comp_t *c, size_t row, - size_t column, size_t width, bool secondary, bool selected, +line_t pager_t::completion_print_item(const comp_t *c, size_t row, size_t column, size_t width, + bool secondary, bool selected, page_rendering_t *rendering) const { UNUSED(column); UNUSED(row); @@ -157,19 +163,16 @@ line_t pager_t::completion_print_item(const wcstring &prefix, const comp_t *c, s highlight_spec_t comp_col = {modify_role(highlight_role_t::pager_completion), bg_role}; highlight_spec_t desc_col = {modify_role(highlight_role_t::pager_description), bg_role}; - // Print the completion part + // Print the completion part. size_t comp_remaining = comp_width; for (size_t i = 0; i < c->comp.size(); i++) { const wcstring &comp = c->comp.at(i); - if (i > 0) { comp_remaining -= print_max(PAGER_SPACER_STRING, bg, comp_remaining, true /* has_more */, &line_data); } - - comp_remaining -= print_max(prefix, prefix_col, comp_remaining, !comp.empty(), &line_data); - comp_remaining -= - print_max(comp, comp_col, comp_remaining, i + 1 < c->comp.size(), &line_data); + comp_remaining -= print_max(comp, comp_col, comp_remaining, i + 1 < c->comp.size(), + &line_data, c->prefix_len, prefix_col); } size_t desc_remaining = width - comp_width + comp_remaining; @@ -203,10 +206,9 @@ line_t pager_t::completion_print_item(const wcstring &prefix, const comp_t *c, s /// \param width_by_column An array specifying the width of each column /// \param row_start The first row to print /// \param row_stop the row after the last row to print -/// \param prefix The string to print before each completion /// \param lst The list of completions to print void pager_t::completion_print(size_t cols, const size_t *width_by_column, size_t row_start, - size_t row_stop, const wcstring &prefix, const comp_info_list_t &lst, + size_t row_stop, const comp_info_list_t &lst, page_rendering_t *rendering) const { // Teach the rendering about the rows it printed. assert(row_stop >= row_start); @@ -226,7 +228,7 @@ void pager_t::completion_print(size_t cols, const size_t *width_by_column, size_ bool is_selected = (idx == effective_selected_idx); // Print this completion on its own "line". - line_t line = completion_print_item(prefix, el, row, col, width_by_column[col], row % 2, + line_t line = completion_print_item(el, row, col, width_by_column[col], row % 2, is_selected, rendering); // If there's more to come, append two spaces. @@ -300,7 +302,8 @@ static void join_completions(comp_info_list_t *comps) { } /// Generate a list of comp_t structures from a list of completions. -static comp_info_list_t process_completions_into_infos(const completion_list_t &lst) { +static comp_info_list_t process_completions_into_infos(const completion_list_t &lst, + size_t prefix_len) { const size_t lst_size = lst.size(); // Make the list of the correct size up-front. @@ -308,9 +311,22 @@ static comp_info_list_t process_completions_into_infos(const completion_list_t & for (size_t i = 0; i < lst_size; i++) { const completion_t &comp = lst.at(i); comp_t *comp_info = &result.at(i); + comp_info->prefix_len = prefix_len; + + // Perhaps ellipsize the prefix. + // FIXME: The escaping mucks with the length here; we may color the wrong number of + // characters. Prefix should be based on width not length anyways. + wcstring comp_str = escape_string(comp.completion, ESCAPE_NO_QUOTED); + if (prefix_len > PREFIX_MAX_LEN && comp_str.size() > PREFIX_MAX_LEN) { + // Discard the prefix, except for the last PREFIX_MAX_LEN. + // Then ellipsize the first char. + comp_str.erase(0, prefix_len - PREFIX_MAX_LEN); + comp_str.at(0) = get_ellipsis_char(); + comp_info->prefix_len = PREFIX_MAX_LEN; + } // Append the single completion string. We may later merge these into multiple. - comp_info->comp.push_back(escape_string(comp.completion, ESCAPE_NO_QUOTED)); + comp_info->comp.push_back(std::move(comp_str)); // Append the mangled description. comp_info->desc = comp.description; @@ -322,8 +338,7 @@ static comp_info_list_t process_completions_into_infos(const completion_list_t & return result; } -void pager_t::measure_completion_infos(comp_info_list_t *infos, const wcstring &prefix) const { - size_t prefix_len = fish_wcswidth(prefix); +void pager_t::measure_completion_infos(comp_info_list_t *infos) const { for (auto &info : *infos) { comp_t *comp = &info; const wcstring_list_t &comp_strings = comp->comp; @@ -334,7 +349,7 @@ void pager_t::measure_completion_infos(comp_info_list_t *infos, const wcstring & // fish_wcswidth() can return -1 if it can't calculate the width. So be cautious. int comp_width = fish_wcswidth(comp_strings.at(j)); - if (comp_width >= 0) comp->comp_width += prefix_len + comp_width; + if (comp_width >= 0) comp->comp_width += comp_width; } // fish_wcswidth() can return -1 if it can't calculate the width. So be cautious. @@ -357,7 +372,7 @@ bool pager_t::completion_info_passes_filter(const comp_t &info) const { // Match against the completion strings. for (const auto &i : info.comp) { - if (string_fuzzy_match_string(needle, prefix + i)) { + if (string_fuzzy_match_string(needle, i)) { return true; } } @@ -375,31 +390,31 @@ void pager_t::refilter_completions() { } } -void pager_t::set_completions(const completion_list_t &raw_completions) { +void pager_t::set_completions(const completion_list_t &raw_completions, + const wcstring &shared_prefix) { // Get completion infos out of it. - unfiltered_completion_infos = process_completions_into_infos(raw_completions); + unfiltered_completion_infos = + process_completions_into_infos(raw_completions, shared_prefix.size()); // Maybe join them. - if (prefix == L"-") join_completions(&unfiltered_completion_infos); + if (shared_prefix == L"-") join_completions(&unfiltered_completion_infos); // Compute their various widths. - measure_completion_infos(&unfiltered_completion_infos, prefix); + measure_completion_infos(&unfiltered_completion_infos); // Refilter them. this->refilter_completions(); } -void pager_t::set_prefix(const wcstring &pref) { prefix = pref; } - void pager_t::set_term_size(termsize_t ts) { available_term_width = ts.width > 0 ? ts.width : 0; available_term_height = ts.height > 0 ? ts.height : 0; } -/// Try to print the list of completions lst with the prefix prefix using cols as the number of +/// Try to print the list of completions \p lst using \p cols as the number of /// columns. Return true if the completion list was printed, false if the terminal is too narrow for /// the specified number of columns. Always succeeds if cols is 1. -bool pager_t::completion_try_print(size_t cols, const wcstring &prefix, const comp_info_list_t &lst, +bool pager_t::completion_try_print(size_t cols, const comp_info_list_t &lst, page_rendering_t *rendering, size_t suggested_start_row) const { assert(cols > 0); // The calculated preferred width of each column. @@ -480,7 +495,7 @@ bool pager_t::completion_try_print(size_t cols, const wcstring &prefix, const co assert(stop_row >= start_row); assert(stop_row <= row_count); assert(stop_row - start_row <= term_height); - completion_print(cols, width_by_column, start_row, stop_row, prefix, lst, rendering); + completion_print(cols, width_by_column, start_row, stop_row, lst, rendering); // Add the progress line. It's a "more to disclose" line if necessary, or a row listing if // it's scrollable; otherwise ignore it. @@ -566,7 +581,7 @@ page_rendering_t pager_t::render() const { rendering.selected_completion_idx = this->visual_selected_completion_index(rendering.rows, rendering.cols); - if (completion_try_print(cols, prefix, completion_infos, &rendering, suggested_row_start)) { + if (completion_try_print(cols, completion_infos, &rendering, suggested_row_start)) { break; } } @@ -836,7 +851,6 @@ size_t pager_t::get_selected_column(const page_rendering_t &rendering) const { void pager_t::clear() { unfiltered_completion_infos.clear(); completion_infos.clear(); - prefix.clear(); selected_completion_idx = PAGER_SELECTION_NONE; fully_disclosed = false; search_field_shown = false; diff --git a/src/pager.h b/src/pager.h index 7ea45aafa..d9fc4e63e 100644 --- a/src/pager.h +++ b/src/pager.h @@ -91,6 +91,9 @@ class pager_t { size_t comp_width{0}; /// On-screen width of the description information. size_t desc_width{0}; + /// Length of the shared prefix for each completion. + /// These characters are colored using pager_prefix highlight role. + size_t prefix_len{0}; // Our text looks like this: // completion (description) @@ -116,32 +119,26 @@ class pager_t { // The unfiltered list. Note there's a lot of duplication here. comp_info_list_t unfiltered_completion_infos; - wcstring prefix; - - bool completion_try_print(size_t cols, const wcstring &prefix, const comp_info_list_t &lst, - page_rendering_t *rendering, size_t suggested_start_row) const; + bool completion_try_print(size_t cols, const comp_info_list_t &lst, page_rendering_t *rendering, + size_t suggested_start_row) const; void recalc_min_widths(comp_info_list_t *lst) const; - void measure_completion_infos(std::vector *infos, const wcstring &prefix) const; + void measure_completion_infos(std::vector *infos) const; bool completion_info_passes_filter(const comp_t &info) const; void completion_print(size_t cols, const size_t *width_by_column, size_t row_start, - size_t row_stop, const wcstring &prefix, const comp_info_list_t &lst, + size_t row_stop, const comp_info_list_t &lst, page_rendering_t *rendering) const; - line_t completion_print_item(const wcstring &prefix, const comp_t *c, size_t row, size_t column, - size_t width, bool secondary, bool selected, - page_rendering_t *rendering) const; + line_t completion_print_item(const comp_t *c, size_t row, size_t column, size_t width, + bool secondary, bool selected, page_rendering_t *rendering) const; public: // The text of the search field. editable_line_t search_field_line; - // Sets the set of completions. - void set_completions(const completion_list_t &raw_completions); - - // Sets the prefix. - void set_prefix(const wcstring &pref); + // Sets the set of completions, and their shared prefix. + void set_completions(const completion_list_t &raw_completions, const wcstring &prefix); // Sets the terminal size. void set_term_size(termsize_t ts); diff --git a/src/reader.cpp b/src/reader.cpp index f594d877c..c14651a7a 100644 --- a/src/reader.cpp +++ b/src/reader.cpp @@ -82,10 +82,6 @@ // interactive command to complete. #define ENV_CMD_DURATION L"CMD_DURATION" -/// Maximum length of prefix string when printing completion list. Longer prefixes will be -/// ellipsized. -#define PREFIX_MAX_LEN 9 - /// A simple prompt for reading shell commands that does not rely on fish specific commands, meaning /// it will work even if fish is not installed. This is used by read_i. #define DEFAULT_PROMPT L"echo -n \"$USER@$hostname $PWD \"'> '" @@ -1814,6 +1810,45 @@ static uint32_t get_best_rank(const completion_list_t &comp) { return best_rank; } +/// \return the common string prefix of a list of completions. +static wcstring extract_common_prefix(const completion_list_t &completions) { + bool has_seed = false; + wcstring result; + // Seed it with the first samecase completion (if any), so that the prefix has the same case as + // the command line. + for (const completion_t &c : completions) { + if (c.match.is_samecase()) { + result = c.completion; + has_seed = true; + break; + } + } + + for (const completion_t &c : completions) { + if (!has_seed) { + result = c.completion; + has_seed = true; + continue; + } + + // Allow case insensitive common prefix if our completion was not samecase. + bool icase = !c.match.is_samecase(); + size_t i = 0; + size_t max = std::min(c.completion.size(), result.size()); + for (; i < max; i++) { + wchar_t c1 = c.completion[i]; + wchar_t c2 = result[i]; + bool chars_match = (c1 == c2 || (icase && towlower(c1) == towlower(c2))); + if (!chars_match) { + break; + } + } + assert(i <= result.size() && "Shared prefix should not make string longer"); + result.resize(i); + } + return result; +} + /// Handle the list of completions. This means the following: /// /// - If the list is empty, flash the terminal. @@ -1835,7 +1870,7 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok bool success = false; const editable_line_t *el = &command_line; - const wcstring tok(el->text().c_str() + token_begin, token_end - token_begin); + const wcstring tok(el->text(), token_begin, token_end - token_begin); // Check trivial cases. size_t size = comp.size(); @@ -1860,118 +1895,51 @@ bool reader_data_t::handle_completions(const completion_list_t &comp, size_t tok return success; } - auto best_rank = get_best_rank(comp); - - // Determine whether we are going to replace the token or not. If any commands of the best - // rank do not require replacement, then ignore all those that want to use replacement. - bool will_replace_token = true; - for (const completion_t &el : comp) { - if (el.rank() <= best_rank && !(el.flags & COMPLETE_REPLACES_TOKEN)) { - will_replace_token = false; - break; - } - } - // Decide which completions survived. There may be a lot of them; it would be nice if we could // figure out how to avoid copying them here. + auto best_rank = get_best_rank(comp); completion_list_t surviving_completions; bool all_matches_exact_or_prefix = true; - for (const completion_t &el : comp) { + for (const completion_t &c : comp) { // Ignore completions with a less suitable match rank than the best. - if (el.rank() > best_rank) continue; - - // Only use completions that match replace_token. - bool completion_replace_token = static_cast(el.flags & COMPLETE_REPLACES_TOKEN); - if (completion_replace_token != will_replace_token) continue; + if (c.rank() > best_rank) continue; // Don't use completions that want to replace, if we cannot replace them. - if (completion_replace_token && !reader_can_replace(tok, el.flags)) continue; + bool completion_replace_token = (c.flags & COMPLETE_REPLACES_TOKEN); + if (completion_replace_token && !reader_can_replace(tok, c.flags)) continue; // This completion survived. - surviving_completions.push_back(el); - all_matches_exact_or_prefix = all_matches_exact_or_prefix && el.match.is_exact_or_prefix(); + surviving_completions.push_back(c); + all_matches_exact_or_prefix = all_matches_exact_or_prefix && c.match.is_exact_or_prefix(); } - bool use_prefix = false; - wcstring common_prefix; - if (all_matches_exact_or_prefix) { - // Try to find a common prefix to insert among the surviving completions. - complete_flags_t flags = 0; - bool prefix_is_partial_completion = false; - bool first = true; - for (const completion_t &el : surviving_completions) { - if (first) { - // First entry, use the whole string. - common_prefix = el.completion; - flags = el.flags; - first = false; - } else { - // Determine the shared prefix length. - size_t idx, max = std::min(common_prefix.size(), el.completion.size()); - - for (idx = 0; idx < max; idx++) { - wchar_t ac = common_prefix.at(idx), bc = el.completion.at(idx); - bool matches = (ac == bc); - // If we are replacing the token, allow case to vary. - if (will_replace_token && !matches) { - // Hackish way to compare two strings in a case insensitive way, - // hopefully better than towlower(). - matches = (wcsncasecmp(&ac, &bc, 1) == 0); - } - if (!matches) break; - } - - // idx is now the length of the new common prefix. - common_prefix.resize(idx); - prefix_is_partial_completion = true; - - // Early out if we decide there's no common prefix. - if (idx == 0) break; - } - } - - // Determine if we use the prefix. We use it if it's non-empty and it will actually make - // the command line longer. It may make the command line longer by virtue of not using - // REPLACE_TOKEN (so it always appends to the command line), or by virtue of replacing - // the token but being longer than it. - use_prefix = common_prefix.size() > (will_replace_token ? tok.size() : 0); - assert(!use_prefix || !common_prefix.empty()); - - if (use_prefix) { - // We got something. If more than one completion contributed, then it means we have - // a prefix; don't insert a space after it. - if (prefix_is_partial_completion) flags |= COMPLETE_NO_SPACE; - completion_insert(common_prefix, token_end, flags); - cycle_command_line = command_line.text(); - cycle_cursor_pos = command_line.position(); + // Ensure that all surviving completions replace their token, so we can handle them uniformly. + for (completion_t &c : surviving_completions) { + if (!(c.flags & COMPLETE_REPLACES_TOKEN)) { + c.flags |= COMPLETE_REPLACES_TOKEN; + c.completion.insert(0, tok); } } - if (use_prefix) { - for (completion_t &c : surviving_completions) { - c.flags &= ~COMPLETE_REPLACES_TOKEN; - c.completion.erase(0, common_prefix.size()); - } - } + // Compute the common prefix (perhaps empty) of all surviving completions, and replace our token + // with it if it would make the token longer. + wcstring common_prefix = extract_common_prefix(surviving_completions); + if (common_prefix.size() > tok.size()) { + complete_flags_t flags = COMPLETE_REPLACES_TOKEN; - // Print the completion list. - wcstring prefix; - if (will_replace_token || !all_matches_exact_or_prefix) { - if (use_prefix) prefix = std::move(common_prefix); - } else if (tok.size() + common_prefix.size() <= PREFIX_MAX_LEN) { - prefix = tok + common_prefix; - } else { - // Append just the end of the string. - prefix = wcstring{get_ellipsis_char()}; - prefix.append(tok + common_prefix, tok.size() + common_prefix.size() - PREFIX_MAX_LEN, - PREFIX_MAX_LEN); + // Replace the token! Note this invalidates token_begin and token_end. + // Do not insert a space if more than one completion contributed. + if (surviving_completions.size() > 1) flags |= COMPLETE_NO_SPACE; + completion_insert(common_prefix, token_end, flags); + + cycle_command_line = command_line.text(); + cycle_cursor_pos = command_line.position(); } // Update the pager data. - pager.set_prefix(prefix); - pager.set_completions(surviving_completions); + pager.set_completions(surviving_completions, common_prefix); // Invalidate our rendering. - current_page_rendering = page_rendering_t(); + current_page_rendering = page_rendering_t{}; // Modify the command line to reflect the new pager. pager_selection_changed(); return false; diff --git a/src/wcstringutil.h b/src/wcstringutil.h index 826d62594..489ce804f 100644 --- a/src/wcstringutil.h +++ b/src/wcstringutil.h @@ -67,6 +67,9 @@ struct string_fuzzy_match_t { return type == contain_type_t::exact && case_fold == case_fold_t::samecase; } + /// \return if this is a samecase completion. + bool is_samecase() const { return case_fold == case_fold_t::samecase; } + /// \return if we are exact or prefix match. bool is_exact_or_prefix() const { switch (type) {