From f417cbc9818200b267f295144a42cd13b81fe074 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Thu, 11 Dec 2025 01:46:24 +0900 Subject: [PATCH] Show soft-wrapped portions in autosuggestions Co-authored-by: Johannes Altmanninger Closes #12153 --- CHANGELOG.rst | 1 + src/screen.rs | 147 +++++++----------- .../checks/tmux-autosuggestion-multiline.fish | 32 ++-- tests/checks/tmux-pager.fish | 8 +- 4 files changed, 79 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f28392f91..aa1ddfcd8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,6 +37,7 @@ Interactive improvements - Prefix-matching completions are now shown even if they don't have the case typed by the user (:issue:`7944`). - On Cygwin/MSYS, command name completion will favor the non-exe name (``foo``) unless the user started typing the extension. - When using the exe name (``foo.exe``), fish will use to the description and completions for ``foo`` if there are none for ``foo.exe``. +- Autosuggestions now also show soft-wrapped portions (:issue:`12045`). Improved terminal support ------------------------- diff --git a/src/screen.rs b/src/screen.rs index 6edae2ec4..412c04553 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -10,7 +10,6 @@ use crate::editable_line::line_at_cursor; use crate::key::ViewportPosition; use crate::pager::{PAGER_MIN_HEIGHT, PageRendering, Pager}; -use std::borrow::Cow; use std::cell::RefCell; use std::collections::LinkedList; use std::io::Write; @@ -1953,21 +1952,6 @@ struct ScreenLayout { pub(crate) autosuggestion: WString, } -// Given a vector whose indexes are offsets and whose values are the widths of the string if -// truncated at that offset, return the offset that fits in the given width. Returns -// width_by_offset.size() - 1 if they all fit. The first value in width_by_offset is assumed to be -// 0. -fn truncation_offset_for_width(str: &wstr, max_width: usize) -> usize { - let mut i = 0; - let mut width = 0; - while i < str.len() && width <= max_width { - width += wcwidth_rendered_min_0(str.char_at(i)); - i += 1; - } - // i is the first index that did not fit; i - 1 is therefore the last that did. - i - 1 -} - #[allow(clippy::too_many_arguments)] fn compute_layout( ellipsis_char: char, @@ -2043,8 +2027,9 @@ fn compute_layout( // Now we should definitely fit. assert!(left_prompt_width + right_prompt_width <= screen_width); - // Truncate each logical line from the autosuggestion to fit on a single screen line. - // In future, we should truncate only once, at the end (#12004). + // Track each logical line from the autosuggestion so we can determine how much of it fits + // on screen. We allow the lines to soft wrap naturally and we only truncate vertically if + // we would exceed the screen height. let cursor_y = left_prompt_layout.line_starts.len() - 1 + commandline_before_suggestion .chars() @@ -2052,9 +2037,7 @@ fn compute_layout( .count(); struct SuggestionLine<'a> { - available_autosuggest_space: usize, autosuggestion_line: &'a wstr, - autosuggest_total_width: usize, } let mut suggestion_lines = vec![]; @@ -2070,10 +2053,6 @@ struct SuggestionLine<'a> { .enumerate() { let autosuggestion_line = wstr::from_char_slice(autosuggestion_line); - let autosuggest_total_width = autosuggestion_line - .chars() - .map(wcwidth_rendered_min_0) - .sum(); // Calculate space available for autosuggestion. let indent_width = |pos| usize::try_from(indent[pos]).unwrap() * INDENT_STEP; @@ -2087,17 +2066,27 @@ struct SuggestionLine<'a> { } else { indent_width(suggestion_start - "\n".len()) }; - let available_horizontal_space = screen_width - (width % screen_width); - - let suggestion_line_height = - if width >= screen_width || autosuggest_total_width >= available_horizontal_space { - // As per the comment above, we truncate and autosuggestion lines that would wrap. - // We truncate them at the very end of the screen, so they (barely) soft wrap, - // and take up two screen lines. - 2 - } else { - 1 - }; + let column = width % screen_width; + let suggestion_line_height = { + let mut column = column; + let mut lines = 1; + for ch in autosuggestion_line.chars() { + let ch_width = wcwidth_rendered_min_0(ch); + let new_column = column + ch_width; + if new_column > screen_width { + column = 0; + } + if column == 0 && ch_width != 0 { + lines += 1; + } + column = if new_column == screen_width { + 0 + } else { + column + ch_width + }; + } + lines + }; match available_vertical_space.checked_sub(suggestion_line_height) { Some(lines) => available_vertical_space = lines, None => { @@ -2107,60 +2096,40 @@ struct SuggestionLine<'a> { }; suggestion_lines.push(SuggestionLine { - available_autosuggest_space: available_horizontal_space, autosuggestion_line, - autosuggest_total_width, }); suggestion_start += autosuggestion_line.len() + "\n".len(); } let mut autosuggestion = WString::new(); - let mut erased = 0; - let mut suggestion_start = commandline_before_suggestion.len(); + let mut displayed_len = 0; for ( - line, + line_idx, &SuggestionLine { - available_autosuggest_space, autosuggestion_line, - autosuggest_total_width, }, ) in suggestion_lines.iter().enumerate() { - let truncated_suggestion_line; - let mut vertical_truncation_marker = None; - if autosuggest_total_width > 0 && autosuggest_total_width >= available_autosuggest_space { - // horizontal truncation - let truncation_offset = - truncation_offset_for_width(autosuggestion_line, available_autosuggest_space - 1); - truncated_suggestion_line = Cow::Owned( - autosuggestion_line[..truncation_offset].to_owned() - + wstr::from_char_slice(&[ellipsis_char]), - ); - } else if truncated_vertically && line == suggestion_lines.len() - 1 { - // vertical truncation - truncated_suggestion_line = Cow::Borrowed(autosuggestion_line); - vertical_truncation_marker = Some(ellipsis_char); - } else { - // no truncation - assert!(available_autosuggest_space >= autosuggest_total_width); - truncated_suggestion_line = Cow::Borrowed(autosuggestion_line); - } - - let truncation_range = suggestion_start - erased + truncated_suggestion_line.len() - ..suggestion_start - erased + autosuggestion_line.len(); - colors.drain(truncation_range.clone()); - indent.drain(truncation_range.clone()); - erased += truncation_range.len(); - suggestion_start += autosuggestion_line.len() + "\n".len(); - if line != 0 { + if line_idx != 0 { autosuggestion.push('\n'); + displayed_len += "\n".len(); } - autosuggestion.push_utfstr(&truncated_suggestion_line); - if let Some(extra) = vertical_truncation_marker { - autosuggestion.push(extra); - colors.insert(truncation_range.end, colors[truncation_range.end - 1]); - indent.insert(truncation_range.end, indent[truncation_range.end - 1]); + autosuggestion.push_utfstr(autosuggestion_line); + displayed_len += autosuggestion_line.len(); + } + + let total_autosuggestion_len = autosuggestion_str.len(); + let truncated_chars = total_autosuggestion_len.saturating_sub(displayed_len); + if truncated_chars > 0 { + let suggestion_end = commandline_before_suggestion.len() + displayed_len; + colors.drain(suggestion_end..suggestion_end + truncated_chars); + indent.drain(suggestion_end..suggestion_end + truncated_chars); + if truncated_vertically && displayed_len > 0 { + let suggestion_last = suggestion_end - 1; + autosuggestion.push(ellipsis_char); + colors.insert(suggestion_end, colors[suggestion_last]); + indent.insert(suggestion_end, indent[suggestion_last]); } } result.autosuggestion = autosuggestion; @@ -2502,7 +2471,7 @@ macro_rules! validate { "left>", 5, "", 5, "", 5, "", 5, "", 5, "", - " auto…", + " autoSUGGESTION", ) ); validate!( @@ -2564,7 +2533,7 @@ macro_rules! validate { "left>", 5, "", - "…", + "s", ) ); validate!( @@ -2574,7 +2543,7 @@ macro_rules! validate { "left>", 5, "", - "…", + "SUGGESTION", ) ); validate!( @@ -2584,7 +2553,7 @@ macro_rules! validate { "left>", 5, "", - "uggestion long so…", + "uggestion long soFT WRAP", ) ); validate!( @@ -2594,7 +2563,7 @@ macro_rules! validate { "left>", 5, "", 5, "", 5, "", 5, "", - "and …", + "and AUTOSUGGESTION", ) ); validate!( @@ -2634,7 +2603,7 @@ macro_rules! validate { "left>", 5, "", - "…", + "AUTOSUGGESTION", ) ); validate!( @@ -2644,7 +2613,7 @@ macro_rules! validate { "left>", 5, "", - "utosuggestion sof…", + "utosuggestion sofT WRAP", ) ); } diff --git a/tests/checks/tmux-autosuggestion-multiline.fish b/tests/checks/tmux-autosuggestion-multiline.fish index 44b718a0a..d37bd5d13 100644 --- a/tests/checks/tmux-autosuggestion-multiline.fish +++ b/tests/checks/tmux-autosuggestion-multiline.fish @@ -16,8 +16,8 @@ tmux-sleep isolated-tmux capture-pane -p | sed /if/,/end/s/^/^/ # CHECK: ^prompt> if true # CHECK: ^ echo 00000000000000000000000000000000000000000000000000 -# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000… -# CHECK: ^ +# CHECK: ^ echo 000000000000000000000000000000000000000000000000000000000000000 +# CHECK: ^0000000000000000000000000000000000000 # CHECK: ^ end # Enter does not invalidate autosuggestion. @@ -26,8 +26,8 @@ tmux-sleep isolated-tmux capture-pane -p | sed /if/,/end/s/^/^/ # CHECK: ^prompt> if true # CHECK: ^ echo 00000000000000000000000000000000000000000000000000 -# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000… -# CHECK: ^ +# CHECK: ^ echo 000000000000000000000000000000000000000000000000000000000000000 +# CHECK: ^0000000000000000000000000000000000000 # CHECK: ^ end # Autosuggestion is also computed after Enter. @@ -36,8 +36,8 @@ tmux-sleep isolated-tmux capture-pane -p \; send-keys C-u C-u C-u C-l | sed /if/,/end/s/^/^/ # CHECK: ^prompt> if true # CHECK: ^ echo 00000000000000000000000000000000000000000000000000 -# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000… -# CHECK: ^ +# CHECK: ^ echo 000000000000000000000000000000000000000000000000000000000000000 +# CHECK: ^0000000000000000000000000000000000000 # CHECK: ^ end # Test smaller windows; only the lines that fit will be shown. @@ -46,8 +46,8 @@ tmux-sleep isolated-tmux capture-pane -p | sed s/^/^/ # CHECK: ^prompt> if true # CHECK: ^ echo 00000000000000000000000000000000000000000000000000 -# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000… -# CHECK: ^ +# CHECK: ^ echo 000000000000000000000000000000000000000000000000000000000000000 +# CHECK: ^0000000000000000000000000000000000000… # Currently, we take either all or nothing from soft-wrapped suggestion-lines. # The ellipsis means that we'll get more lines. @@ -73,8 +73,8 @@ isolated-tmux capture-pane -p | sed s/^/^/ # CHECK: ^prompt> # CHECK: ^prompt> if true # CHECK: ^ echo 00000000000000000000000000000000000000000000000000 -# CHECK: ^ echo 00000000000000000000000000000000000000000000000000000000000000… -# CHECK: ^ +# CHECK: ^ echo 000000000000000000000000000000000000000000000000000000000000000 +# CHECK: ^0000000000000000000000000000000000000… # Again, we take all or nothing from a soft-wrapped line. isolated-tmux send-keys C-u Enter if @@ -114,8 +114,8 @@ isolated-tmux capture-pane -p | sed s/^/^/ # CHECK: ^prompt-line1/2> # CHECK: ^prompt-line2/2> if true # CHECK: ^ echo 00000000000000000000000000000000000000000000000000 -# CHECK: ^ echo 000000000000000000000000000000000000000000000000000000… -# CHECK: ^ +# CHECK: ^ echo 0000000000000000000000000000000000000000000000000000000 +# CHECK: ^000000000000000000000000000000000000000000000… # Autosuggestion with a line that barely wraps. isolated-tmux resize-window -x 80 -y 4 \; send-keys C-u \ @@ -131,7 +131,7 @@ tmux-sleep isolated-tmux capture-pane -p | sed s/^/^/ # CHECK: ^prompt-line1 # CHECK: ^> begin -# CHECK: ^ : 00000000000000000000000000000000000000000000000000000000000000000000000… +# CHECK: ^ : 000000000000000000000000000000000000000000000000000000000000000000000000 # CHECK: ^ # Autosuggestions on a soft-wrapped commandline don't push the prompt. @@ -142,8 +142,8 @@ isolated-tmux resize-window -x 6 -y 4 \; send-keys C-u \ tmux-sleep isolated-tmux capture-pane -p | sed s/^/^/ # CHECK: ^> -# CHECK: ^> ech… -# CHECK: ^ +# CHECK: ^> echo +# CHECK: ^ l1 \… # CHECK: ^ isolated-tmux resize-window -x 6 -y 4 \; send-keys C-u \ @@ -159,4 +159,4 @@ isolated-tmux capture-pane -p | sed s/^/^/ # CHECK: ^> # CHECK: ^> # CHECK: ^> echo -# CHECK: ^ wrap… +# CHECK: ^ diff --git a/tests/checks/tmux-pager.fish b/tests/checks/tmux-pager.fish index 9f3ada353..e3571aea5 100644 --- a/tests/checks/tmux-pager.fish +++ b/tests/checks/tmux-pager.fish @@ -11,7 +11,8 @@ isolated-tmux-start -C ' isolated-tmux send-keys : Space Tab Tab tmux-sleep isolated-tmux capture-pane -p -# CHECK: prompt 0> : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA… +# CHECK: prompt 0> : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +# CHECK: AAAAAAAAAAAA # CHECK: 1 135 269 403 537 671 # CHECK: 2 136 270 404 538 672 # CHECK: 3 137 271 405 539 673 @@ -19,10 +20,9 @@ isolated-tmux capture-pane -p # CHECK: 5 139 273 407 541 675 # CHECK: 6 140 274 408 542 676 # CHECK: 7 141 275 409 543 677 -# CHECK: 8 142 276 410 544 678 -# CHECK: rows 1 to 8 of 134 +# CHECK: rows 1 to 7 of 134 -# Check that completions don't break than called on empty line +# Check that completions don't break when called on empty line isolated-tmux send-keys C-u 'set fish_autosuggestion_enabled 0; function fish_prompt; string repeat (math $COLUMNS - 2) 0; end' Enter isolated-tmux send-keys C-l : Space Tab Tab tmux-sleep