Show soft-wrapped portions in autosuggestions

Co-authored-by: Johannes Altmanninger <aclopte@gmail.com>

Closes #12153
This commit is contained in:
Asuka Minato
2025-12-11 01:46:24 +09:00
committed by Johannes Altmanninger
parent 92c814841a
commit f417cbc981
4 changed files with 79 additions and 109 deletions

View File

@@ -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
-------------------------

View File

@@ -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,
"<right",
" autosugges",
" autosuggesTION",
)
);
validate!(
@@ -2522,7 +2491,7 @@ macro_rules! validate {
"left>",
5,
"<right",
" autosuggestion t",
" autosuggestion tRUNCATED",
)
);
validate!(
@@ -2532,7 +2501,7 @@ macro_rules! validate {
"left>",
5,
"<right",
" autosuggesti",
" autosuggestiON TRUNCATED",
)
);
let indent = validate!(
@@ -2542,10 +2511,10 @@ macro_rules! validate {
"left>",
5,
"<right",
" autosuggesti",
" autosuggestiON TRUNCATED",
)
);
assert_eq!(indent["if :\ncommand autosuggesti\n".len()], 1);
assert_eq!(indent["if :\ncommand autosuggestiON TRUNCATED\n".len()], 1);
validate!(
(
@@ -2554,7 +2523,7 @@ macro_rules! validate {
"left>",
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,
"<right",
"and ",
"and AUTOSUGGESTION",
)
);
validate!(
@@ -2604,7 +2573,7 @@ macro_rules! validate {
"left>",
5,
"<right",
"",
"AUTOSUGGESTION",
)
);
validate!( //
@@ -2614,7 +2583,7 @@ macro_rules! validate {
"left>",
5,
"<right",
"utosuggestion sof",
"utosuggestion sofT WRAP",
)
);
validate!(
@@ -2624,7 +2593,7 @@ macro_rules! validate {
"left>",
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",
)
);
}

View File

@@ -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: ^

View File

@@ -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