diff --git a/src/builtin_string.cpp b/src/builtin_string.cpp index ca2b47e89..b6cf4f936 100644 --- a/src/builtin_string.cpp +++ b/src/builtin_string.cpp @@ -203,11 +203,13 @@ struct options_t { //!OCLINT(too many fields) static size_t width_without_escapes(const wcstring &ins) { ssize_t width = 0; - // TODO: this is the same as fish_wcwidth_min_0 from screen.cpp - // Make that reusable (and add a wcswidth version). for (auto c : ins) { - auto w = fish_wcwidth(c); - if (w > 0) width += w; + auto w = fish_wcwidth_visible(c); + // We assume that this string is on its own line, + // in which case a backslash can't bring us below 0. + if (w > 0 || width > 0) { + width += w; + } } // ANSI escape sequences like \e\[31m contain printable characters. Subtract their width @@ -218,8 +220,8 @@ static size_t width_without_escapes(const wcstring &ins) { if (len) { auto sub = ins.substr(pos, *len); for (auto c : sub) { - auto w = fish_wcwidth(c); - if (w > 0) width -= w; + auto w = fish_wcwidth_visible(c); + width -= w; } // Move us forward behind the escape code, // it might include a second escape! diff --git a/src/screen.cpp b/src/screen.cpp index 7d4e44ebe..49a1bef48 100644 --- a/src/screen.cpp +++ b/src/screen.cpp @@ -81,13 +81,14 @@ static size_t next_tab_stop(size_t current_line_width) { return ((current_line_width / tab_width) + 1) * tab_width; } -/// Like fish_wcwidth, but returns 0 for control characters instead of -1. -static int fish_wcwidth_min_0(wchar_t widechar) { return std::max(0, fish_wcwidth(widechar)); } - int line_t::wcswidth_min_0(size_t max) const { int result = 0; for (size_t idx = 0, end = std::min(max, text.size()); idx < end; idx++) { - result += fish_wcwidth_min_0(text[idx].character); + auto w = fish_wcwidth_visible(text[idx].character); + // A backspace at the start of the line does nothing. + if (w > 0 || result > 0) { + result += w; + } } return result; } @@ -351,7 +352,11 @@ static size_t measure_run_from(const wchar_t *input, size_t start, size_t *out_e width = next_tab_stop(width); } else { // Ordinary char. Add its width with care to ignore control chars which have width -1. - width += fish_wcwidth_min_0(input[idx]); + auto w = fish_wcwidth_visible(input[idx]); + // A backspace at the start of the line does nothing. + if (w != -1 || width > 0) { + width += w; + } } } if (out_end) *out_end = idx; @@ -389,7 +394,7 @@ static void truncate_run(wcstring *run, size_t desired_width, size_t *width, curr_width = measure_run_from(run->c_str(), 0, nullptr, cache); idx = 0; } else { - size_t char_width = fish_wcwidth_min_0(c); + size_t char_width = fish_wcwidth_visible(c); curr_width -= std::min(curr_width, char_width); run->erase(idx, 1); } @@ -856,7 +861,7 @@ static void s_update(screen_t *scr, const wcstring &left_prompt, const wcstring // Skip over skip_remaining width worth of characters. size_t j = 0; for (; j < o_line.size(); j++) { - size_t width = fish_wcwidth_min_0(o_line.char_at(j)); + size_t width = fish_wcwidth_visible(o_line.char_at(j)); if (skip_remaining < width) break; skip_remaining -= width; current_width += width; @@ -864,7 +869,7 @@ static void s_update(screen_t *scr, const wcstring &left_prompt, const wcstring // Skip over zero-width characters (e.g. combining marks at the end of the prompt). for (; j < o_line.size(); j++) { - int width = fish_wcwidth_min_0(o_line.char_at(j)); + int width = fish_wcwidth_visible(o_line.char_at(j)); if (width > 0) break; } @@ -888,7 +893,7 @@ static void s_update(screen_t *scr, const wcstring &left_prompt, const wcstring perform_any_impending_soft_wrap(scr, current_width, static_cast(i)); s_move(scr, current_width, static_cast(i)); set_color(o_line.color_at(j)); - auto width = fish_wcwidth_min_0(o_line.char_at(j)); + auto width = fish_wcwidth_visible(o_line.char_at(j)); s_write_char(scr, o_line.char_at(j), width); current_width += width; } @@ -1035,7 +1040,7 @@ static screen_layout_t compute_layout(screen_t *s, size_t screen_width, multiline = true; break; } else { - first_line_width += fish_wcwidth_min_0(c); + first_line_width += fish_wcwidth_visible(c); } } const size_t first_command_line_width = first_line_width; @@ -1050,7 +1055,7 @@ static screen_layout_t compute_layout(screen_t *s, size_t screen_width, autosuggest_truncated_widths.reserve(1 + autosuggestion_str.size()); for (size_t i = 0; autosuggestion[i] != L'\0'; i++) { autosuggest_truncated_widths.push_back(autosuggest_total_width); - autosuggest_total_width += fish_wcwidth_min_0(autosuggestion[i]); + autosuggest_total_width += fish_wcwidth_visible(autosuggestion[i]); } } @@ -1219,7 +1224,7 @@ void s_write(screen_t *s, const wcstring &left_prompt, const wcstring &right_pro } s_desired_append_char(s, effective_commandline.at(i), colors[i], indent[i], first_line_prompt_space, - fish_wcwidth_min_0(effective_commandline.at(i))); + fish_wcwidth_visible(effective_commandline.at(i))); } // Cursor may have been at the end too. diff --git a/src/wcstringutil.cpp b/src/wcstringutil.cpp index aaea65e7f..9e33b0c12 100644 --- a/src/wcstringutil.cpp +++ b/src/wcstringutil.cpp @@ -310,3 +310,20 @@ wcstring join_strings(const wcstring_list_t &vals, wchar_t sep) { void wcs2string_bad_char(wchar_t wc) { FLOGF(char_encoding, L"Wide character U+%4X has no narrow representation", wc); } + +int fish_wcwidth_visible(wchar_t widechar) { + if (widechar == L'\b') return -1; + return std::max(0, fish_wcwidth(widechar)); +} + +int fish_wcswidth_visible(const wcstring &str) { + size_t res = 0; + for (wchar_t ch : str) { + if (ch == L'\b') { + res += -1; + } else { + res += std::max(0, fish_wcwidth(ch)); + } + } + return res; +} diff --git a/src/wcstringutil.h b/src/wcstringutil.h index 7ffb2a79c..d10304bfa 100644 --- a/src/wcstringutil.h +++ b/src/wcstringutil.h @@ -294,4 +294,11 @@ class line_iterator_t { } }; +/// Like fish_wcwidth, but returns 0 for characters with no real width instead of -1. +int fish_wcwidth_visible(wchar_t widechar); + +/// The same, but for all chars. Note that this only makes sense if the string has an arbitrary long prefix - backslashes can move the cursor *before* the string. +/// +/// In typical usage, you probably want to wrap wcwidth_visible to accumulate the width, but never go below 0. +int fish_wcswidth_visible(const wcstring &str); #endif diff --git a/tests/checks/string.fish b/tests/checks/string.fish index 8f4296b96..fdf4f0e6f 100644 --- a/tests/checks/string.fish +++ b/tests/checks/string.fish @@ -112,6 +112,23 @@ string length --visible a(set_color blue)b\ncde # CHECK: 2 # CHECK: 3 +# Backslashes and visible length: +# It can't move us before the start of the line. +string length --visible \b +# CHECK: 0 + +# It can't move us before the start of the line. +string length --visible \bf +# CHECK: 1 + +# But it does erase chars before. +string length --visible \bf\b +# CHECK: 0 + +# Never move past 0. +string length --visible \bf\b\b\b\b\b +# CHECK: 0 + string sub --length 2 abcde # CHECK: ab