mirror of
https://github.com/fish-shell/fish-shell.git
synced 2026-05-23 04:51:16 -03:00
Make ScreenData track multiline prompt
Instead of pretending that prompt is always 1 line, track multiline prompt in ScreenData.visible_prompt_lines and ScreenData.line_datas as empty lines. This enables: - Trimming part of the prompt that leaves the viewport. - Removing of the old hack needed for locating first prompt line. - Fixing #11875. Part of #11911
This commit is contained in:
committed by
Johannes Altmanninger
parent
71b619bab0
commit
606802daaf
@@ -4,6 +4,7 @@ fish ?.?.? (released ???)
|
||||
Notable improvements and fixes
|
||||
------------------------------
|
||||
- New Taiwanese Chinese translation.
|
||||
- Fixed not properly clearing lines when a :ref:`transient prompt <transient-prompt>` contains more lines than the final prompt (:issue:`11875`).
|
||||
|
||||
Deprecations and removed features
|
||||
---------------------------------
|
||||
@@ -11,6 +12,7 @@ Deprecations and removed features
|
||||
Interactive improvements
|
||||
------------------------
|
||||
- :doc:`fish_config prompt {choose,save} <cmds/fish_config>` have been taught to reset :doc:`fish_mode_prompt <cmds/fish_mode_prompt>` in addition to the other prompt functions (:issue:`11937`).
|
||||
- Fish now hides the portion of a multiline prompt that is scrolled out of view due to a huge command line. This prevents duplicate lines after repainting with partially visible prompt (:issue:`11911`).
|
||||
|
||||
Scripting improvements
|
||||
----------------------
|
||||
|
||||
@@ -4741,10 +4741,6 @@ fn exec_prompt(&mut self, full_prompt: bool, final_prompt: bool) {
|
||||
|
||||
self.left_prompt_buff =
|
||||
join_strings(&self.exec_prompt_cmd(prompt_cmd, final_prompt), '\n');
|
||||
|
||||
if final_prompt {
|
||||
self.screen.multiline_prompt_hack();
|
||||
}
|
||||
}
|
||||
|
||||
// Don't execute the right prompt if it is undefined fish_right_prompt
|
||||
|
||||
185
src/screen.rs
185
src/screen.rs
@@ -154,7 +154,12 @@ pub struct ScreenData {
|
||||
/// The width of the screen once we have rendered.
|
||||
screen_width: Option<usize>,
|
||||
|
||||
/// Virtual cursor position used for writing to `line_datas`,
|
||||
/// and also the viewport final cursor position.
|
||||
cursor: Cursor,
|
||||
|
||||
/// Number of prompt lines rendered on the screen.
|
||||
visible_prompt_lines: usize,
|
||||
}
|
||||
|
||||
impl ScreenData {
|
||||
@@ -349,9 +354,11 @@ struct ScrolledCursor {
|
||||
|
||||
// Clear the desired screen and set its width.
|
||||
self.desired.screen_width = Some(screen_width);
|
||||
self.desired.clear_lines();
|
||||
self.desired.cursor.x = 0;
|
||||
self.desired.cursor.y = 0;
|
||||
self.desired.cursor.y = layout.left_prompt_lines - 1;
|
||||
|
||||
self.desired.clear_lines();
|
||||
self.desired.resize(self.desired.cursor.y);
|
||||
|
||||
// Append spaces for the left prompt.
|
||||
let prompt_offset = if pager.search_field_shown {
|
||||
@@ -454,7 +461,7 @@ struct ScrolledCursor {
|
||||
self.desired.add_line();
|
||||
}
|
||||
|
||||
let full_line_count = self.desired.cursor.y
|
||||
let full_line_count = self.desired.cursor.y + 1
|
||||
- if self.desired.cursor.x == 0
|
||||
&& self.desired.cursor.y.checked_sub(1).is_some_and(|y| {
|
||||
self.desired.line_datas[y].is_soft_wrapped
|
||||
@@ -467,8 +474,7 @@ struct ScrolledCursor {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
+ calc_prompt_lines(&layout.left_prompt);
|
||||
};
|
||||
let pager_available_height = std::cmp::max(
|
||||
1,
|
||||
curr_termsize
|
||||
@@ -514,30 +520,21 @@ struct ScrolledCursor {
|
||||
self.desired.append_lines(&page_rendering.screen_data);
|
||||
|
||||
self.scrolled = scrolled_cursor.scroll_amount != 0;
|
||||
self.desired.visible_prompt_lines =
|
||||
layout
|
||||
.left_prompt_lines
|
||||
.saturating_sub(if is_final_rendering {
|
||||
0
|
||||
} else {
|
||||
scrolled_cursor.scroll_amount
|
||||
});
|
||||
|
||||
self.with_buffered_output(|zelf| {
|
||||
zelf.update(
|
||||
vars,
|
||||
&layout.left_prompt,
|
||||
&layout.right_prompt,
|
||||
is_final_rendering,
|
||||
)
|
||||
zelf.update(vars, &layout.left_prompt, &layout.right_prompt)
|
||||
});
|
||||
self.save_status();
|
||||
}
|
||||
|
||||
pub fn multiline_prompt_hack(&mut self) {
|
||||
// If the prompt is multi-line, we need to move up to the prompt's initial line. We do this
|
||||
// by lying to ourselves and claiming that we're really below what we consider "line 0"
|
||||
// (which is the last line of the prompt). This will cause us to move up to try to get back
|
||||
// to line 0, but really we're getting back to the initial line of the prompt.
|
||||
let prompt_line_count = self
|
||||
.actual_left_prompt
|
||||
.as_ref()
|
||||
.map_or(1, |p| calc_prompt_lines(p));
|
||||
self.actual.cursor.y += prompt_line_count.checked_sub(1).unwrap();
|
||||
}
|
||||
|
||||
/// Resets the screen buffer's internal knowledge about the contents of the screen,
|
||||
/// optionally repainting the prompt as well.
|
||||
/// This function assumes that the current line is still valid.
|
||||
@@ -549,7 +546,6 @@ pub fn reset_line(&mut self, repaint_prompt: bool /* = false */) {
|
||||
std::cmp::max(self.actual_lines_before_reset, self.actual.line_count());
|
||||
|
||||
if repaint_prompt {
|
||||
self.multiline_prompt_hack();
|
||||
self.actual_left_prompt = None;
|
||||
self.need_clear_screen = true;
|
||||
}
|
||||
@@ -564,24 +560,7 @@ pub fn reset_line(&mut self, repaint_prompt: bool /* = false */) {
|
||||
}
|
||||
|
||||
pub fn push_to_scrollback(&mut self, cursor_y: usize) {
|
||||
let prompt_y = self.command_line_y_given_cursor_y(cursor_y);
|
||||
let trailing_prompt_lines = self
|
||||
.actual_left_prompt
|
||||
.as_ref()
|
||||
.map_or(0, |p| calc_prompt_lines(p) - 1);
|
||||
let lines_to_scroll = prompt_y
|
||||
.checked_sub(trailing_prompt_lines)
|
||||
.unwrap_or_else(|| {
|
||||
FLOG!(
|
||||
reader,
|
||||
"Number of trailing prompt lines prompt lines",
|
||||
trailing_prompt_lines,
|
||||
"exceeds prompt's y",
|
||||
prompt_y,
|
||||
"inferred from reported cursor position",
|
||||
);
|
||||
0
|
||||
});
|
||||
let lines_to_scroll = self.command_line_y_given_cursor_y(cursor_y);
|
||||
if lines_to_scroll == 0 {
|
||||
return;
|
||||
}
|
||||
@@ -625,8 +604,9 @@ pub fn offset_in_cmdline_given_cursor(
|
||||
"inferred from reported cursor position",
|
||||
);
|
||||
0
|
||||
});
|
||||
let y = y.min(self.actual.line_count() - 1);
|
||||
})
|
||||
.max(self.actual.visible_prompt_lines - 1)
|
||||
.min(self.actual.line_count() - 1);
|
||||
let Some(viewport_prompt_x) = viewport_cursor.x.checked_sub(self.actual.cursor.x) else {
|
||||
FLOGF!(
|
||||
reader,
|
||||
@@ -1044,13 +1024,7 @@ fn with_buffered_output(&mut self, f: impl FnOnce(&mut Self)) {
|
||||
}
|
||||
|
||||
/// Update the screen to match the desired output.
|
||||
fn update(
|
||||
&mut self,
|
||||
vars: &dyn Environment,
|
||||
left_prompt: &wstr,
|
||||
right_prompt: &wstr,
|
||||
is_final_rendering: bool,
|
||||
) {
|
||||
fn update(&mut self, vars: &dyn Environment, left_prompt: &wstr, right_prompt: &wstr) {
|
||||
// Helper function to set a resolved color, using the caching resolver.
|
||||
let mut color_resolver = HighlightColorResolver::new();
|
||||
let mut set_color = |zelf: &mut Self, c| {
|
||||
@@ -1101,46 +1075,57 @@ fn update(
|
||||
need_clear_screen = true;
|
||||
}
|
||||
|
||||
// Output the left prompt if it has changed.
|
||||
if self.scrolled && !is_final_rendering {
|
||||
self.r#move(0, 0);
|
||||
self.write_command(ClearToEndOfLine);
|
||||
self.actual_left_prompt = None;
|
||||
self.actual.cursor.x = 0;
|
||||
} else {
|
||||
let prompt_changed = self
|
||||
let is_prompt_visible = self.desired.visible_prompt_lines > 0;
|
||||
let prompt_last_line = self.desired.visible_prompt_lines.saturating_sub(1);
|
||||
|
||||
let prompt_changed = is_prompt_visible
|
||||
&& (self
|
||||
.actual_left_prompt
|
||||
.as_ref()
|
||||
.is_none_or(|p| p != left_prompt)
|
||||
|| (self.scrolled && is_final_rendering);
|
||||
|| self.actual.visible_prompt_lines != self.desired.visible_prompt_lines);
|
||||
|
||||
let prompt_last_line_should_wrap =
|
||||
Some(prompt_last_line_width) == screen_width && self.should_wrap(0);
|
||||
let prompt_last_line_screen_wide =
|
||||
is_prompt_visible && Some(prompt_last_line_width) == screen_width;
|
||||
let prompt_last_line_should_wrap =
|
||||
prompt_last_line_screen_wide && self.should_wrap(prompt_last_line);
|
||||
|
||||
if prompt_changed || prompt_last_line_should_wrap {
|
||||
// Output the left prompt if it has changed or we need to refresh softwrap.
|
||||
if prompt_changed || prompt_last_line_should_wrap {
|
||||
let mut start;
|
||||
if prompt_changed {
|
||||
self.r#move(0, 0);
|
||||
let mut start = 0;
|
||||
if left_prompt_layout.line_starts.len() <= 1 {
|
||||
let prompt_first_visible_line =
|
||||
left_prompt_layout.line_starts.len() - self.desired.visible_prompt_lines;
|
||||
start = left_prompt_layout.line_starts[prompt_first_visible_line];
|
||||
if self.desired.visible_prompt_lines == 1 {
|
||||
self.write_command(Osc133PromptStart);
|
||||
}
|
||||
if prompt_changed {
|
||||
for (i, &next_line) in left_prompt_layout.line_starts[1..].iter().enumerate() {
|
||||
self.write_command(ClearToEndOfLine);
|
||||
if i == 0 {
|
||||
self.write_command(Osc133PromptStart);
|
||||
}
|
||||
self.write_str(&left_prompt[start..next_line]);
|
||||
start = next_line;
|
||||
for (i, &next_line) in left_prompt_layout.line_starts
|
||||
[prompt_first_visible_line + 1..]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
self.write_command(ClearToEndOfLine);
|
||||
if i == 0 {
|
||||
self.write_command(Osc133PromptStart);
|
||||
}
|
||||
} else {
|
||||
start = *left_prompt_layout.line_starts.last().unwrap();
|
||||
}
|
||||
self.write_str(&left_prompt[start..]);
|
||||
self.actual_left_prompt = Some(left_prompt.to_owned());
|
||||
self.actual.cursor.x = prompt_last_line_width;
|
||||
if prompt_last_line_should_wrap {
|
||||
self.soft_wrap_location = Some(Cursor { x: 0, y: 1 });
|
||||
self.write_str(&left_prompt[start..next_line]);
|
||||
start = next_line;
|
||||
}
|
||||
} else {
|
||||
self.r#move(0, prompt_last_line);
|
||||
start = *left_prompt_layout.line_starts.last().unwrap();
|
||||
}
|
||||
self.write_str(&left_prompt[start..]);
|
||||
self.actual_left_prompt = Some(left_prompt.to_owned());
|
||||
self.actual.cursor.x = prompt_last_line_width;
|
||||
self.actual.cursor.y = prompt_last_line;
|
||||
if prompt_last_line_should_wrap {
|
||||
self.soft_wrap_location = Some(Cursor {
|
||||
x: 0,
|
||||
y: prompt_last_line + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1152,9 +1137,10 @@ fn s_line(zelf: &Screen, i: usize) -> &Line {
|
||||
}
|
||||
|
||||
// Output all lines.
|
||||
for i in 0..self.desired.line_count() {
|
||||
let commandline_start = prompt_last_line + if prompt_last_line_screen_wide { 1 } else { 0 };
|
||||
for i in commandline_start..self.desired.line_count() {
|
||||
self.actual.create_line(i);
|
||||
let is_prompt_line = i == 0 && !self.scrolled;
|
||||
let is_prompt_line = is_prompt_visible && i == prompt_last_line;
|
||||
let start_pos = if is_prompt_line {
|
||||
prompt_last_line_width
|
||||
} else {
|
||||
@@ -1174,17 +1160,21 @@ fn s_line(zelf: &Screen, i: usize) -> &Line {
|
||||
// Note that skip_remaining is a width, not a character count.
|
||||
let mut skip_remaining = start_pos;
|
||||
|
||||
let shared_prefix = if self.scrolled {
|
||||
let previously_prompt_line = self.actual.visible_prompt_lines > i + 1;
|
||||
|
||||
let shared_prefix = if self.scrolled || previously_prompt_line {
|
||||
0
|
||||
} else {
|
||||
line_shared_prefix(o_line(self, i), s_line(self, i))
|
||||
};
|
||||
let mut skip_prefix = shared_prefix;
|
||||
if shared_prefix < o_line(self, i).indentation {
|
||||
if o_line(self, i).indentation > s_line(self, i).indentation && !has_cleared_screen
|
||||
if shared_prefix < o_line(self, i).indentation || previously_prompt_line {
|
||||
if !has_cleared_screen
|
||||
&& (o_line(self, i).indentation > s_line(self, i).indentation
|
||||
|| previously_prompt_line)
|
||||
{
|
||||
set_color(self, HighlightSpec::new());
|
||||
self.r#move(0, i);
|
||||
self.r#move(start_pos, i);
|
||||
self.write_command(if should_clear_screen_this_line {
|
||||
ClearToEndOfScreen
|
||||
} else {
|
||||
@@ -1296,7 +1286,7 @@ fn s_line(zelf: &Screen, i: usize) -> &Line {
|
||||
if is_prompt_line && right_prompt_width > 0 {
|
||||
// Move the cursor to the beginning of the line first to be independent of the width.
|
||||
// This helps prevent staircase effects if fish and the terminal disagree.
|
||||
self.r#move(0, 0);
|
||||
self.r#move(0, i);
|
||||
self.r#move(screen_width.unwrap() - right_prompt_width, i);
|
||||
set_color(self, HighlightSpec::new());
|
||||
self.write_str(right_prompt);
|
||||
@@ -1851,22 +1841,6 @@ fn truncate_run(
|
||||
*width = curr_width;
|
||||
}
|
||||
|
||||
fn calc_prompt_lines(prompt: &wstr) -> usize {
|
||||
// Hack for the common case where there's no newline at all. I don't know if a newline can
|
||||
// appear in an escape sequence, so if we detect a newline we have to defer to
|
||||
// calc_prompt_width_and_lines.
|
||||
let mut result = 1;
|
||||
if prompt.chars().any(|c| matches!(c, '\n' | '\x0C')) {
|
||||
result = LAYOUT_CACHE_SHARED
|
||||
.lock()
|
||||
.unwrap()
|
||||
.calc_prompt_layout(prompt, None, usize::MAX)
|
||||
.line_starts
|
||||
.len();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns the length of the "shared prefix" of the two lines, which is the run of matching text
|
||||
/// and colors. If the prefix ends on a combining character, do not include the previous character
|
||||
/// in the prefix.
|
||||
@@ -1932,6 +1906,8 @@ pub(crate) fn only_grayscale() -> bool {
|
||||
pub(crate) struct ScreenLayout {
|
||||
// The left prompt that we're going to use.
|
||||
pub(crate) left_prompt: WString,
|
||||
// How many lines in the left prompt.
|
||||
pub(crate) left_prompt_lines: usize,
|
||||
// How much space to leave for it.
|
||||
pub(crate) left_prompt_space: usize,
|
||||
// The right prompt.
|
||||
@@ -2016,6 +1992,7 @@ pub(crate) fn compute_layout(
|
||||
// Always visible.
|
||||
result.left_prompt = left_prompt;
|
||||
result.left_prompt_space = left_prompt_width;
|
||||
result.left_prompt_lines = left_prompt_layout.line_starts.len();
|
||||
|
||||
// Hide the right prompt if it doesn't fit on the first line.
|
||||
if left_prompt_width + first_command_line_width + right_prompt_width < screen_width {
|
||||
|
||||
@@ -277,6 +277,7 @@ macro_rules! validate {
|
||||
ScreenLayout {
|
||||
left_prompt: L!($left_prompt).to_owned(),
|
||||
left_prompt_space: $left_prompt_space,
|
||||
left_prompt_lines: 1,
|
||||
right_prompt: L!($right_prompt).to_owned(),
|
||||
autosuggestion: L!($autosuggestion).to_owned(),
|
||||
}
|
||||
|
||||
@@ -42,29 +42,40 @@ isolated-tmux capture-pane -p
|
||||
|
||||
isolated-tmux send-keys C-c
|
||||
tmux-sleep
|
||||
isolated-tmux send-keys C-l 'commandline -i ": \'$(seq $LINES)" A B "C\'"' Enter Enter
|
||||
isolated-tmux send-keys C-l '
|
||||
function fish_right_prompt
|
||||
echo right-prompt
|
||||
end
|
||||
' 'commandline -i ": \'$(seq (math $LINES \* 2))\'"' Enter Enter
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p
|
||||
isolated-tmux capture-pane -p -S -12
|
||||
# CHECK: prompt 4> commandline -i ": '$(seq (math $LINES \* 2))'" right-prompt
|
||||
# CHECK: prompt 5> : '1 right-prompt
|
||||
# CHECK: 2
|
||||
# CHECK: 3
|
||||
# CHECK: 4
|
||||
# CHECK: 5
|
||||
# CHECK: 6
|
||||
# CHECK: 7
|
||||
# CHECK: 8
|
||||
# CHECK: 9
|
||||
# CHECK: 10
|
||||
# CHECK: A
|
||||
# CHECK: B
|
||||
# CHECK: C'
|
||||
# CHECK: prompt 5>
|
||||
# CHECK: 11
|
||||
# CHECK: 12
|
||||
# CHECK: 13
|
||||
# CHECK: 14
|
||||
# CHECK: 15
|
||||
# CHECK: 16
|
||||
# CHECK: 17
|
||||
# CHECK: 18
|
||||
# CHECK: 19
|
||||
# CHECK: 20'
|
||||
# CHECK: prompt 6> right-prompt
|
||||
|
||||
# Soft-wrapped commandline with omitted right prompt.
|
||||
isolated-tmux send-keys C-q '
|
||||
function fish_right_prompt
|
||||
echo right-prompt
|
||||
end
|
||||
commandline -i "echo $(printf %0"$COLUMNS"d)"
|
||||
' Enter
|
||||
isolated-tmux send-keys 'commandline -i "echo $(printf %0"$COLUMNS"d)"' Enter C-l Enter
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p | sed 1,5d
|
||||
isolated-tmux capture-pane -p
|
||||
# CHECK: prompt {{\d+}}> echo 00000000000000000000000000000000000000000000000000000000000000000
|
||||
# CHECK: 000000000000000
|
||||
# CHECK: 00000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
@@ -61,3 +61,16 @@ isolated-tmux capture-pane -p
|
||||
# CHECK: Hello World
|
||||
# CHECK: prompt-line-1
|
||||
# CHECK: prompt-line-2>
|
||||
|
||||
# Test that transient prompt does not break the prompt.
|
||||
isolated-tmux send-keys C-l "set fish_transient_prompt 1" Enter : Enter Enter
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p
|
||||
# CHECK: prompt-line-1
|
||||
# CHECK: prompt-line-2> set fish_transient_prompt 1
|
||||
# CHECK: prompt-line-1
|
||||
# CHECK: prompt-line-2> :
|
||||
# CHECK: prompt-line-1
|
||||
# CHECK: prompt-line-2>
|
||||
# CHECK: prompt-line-1
|
||||
# CHECK: prompt-line-2>
|
||||
|
||||
@@ -46,3 +46,45 @@ isolated-tmux capture-pane -p
|
||||
# CHECK: …<----------------------------------------------two-last-characters-rendered->!!
|
||||
# CHECK: test "
|
||||
# CHECK: indent"
|
||||
|
||||
isolated-tmux send-keys C-c '
|
||||
function fish_prompt
|
||||
string repeat (math $COLUMNS) x
|
||||
end
|
||||
' C-l 'echo hello'
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p
|
||||
# CHECK: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# CHECK: echo hello
|
||||
|
||||
isolated-tmux send-keys C-c '
|
||||
function fish_prompt
|
||||
seq (math $LINES + 1)
|
||||
end
|
||||
function fish_right_prompt
|
||||
echo test
|
||||
end
|
||||
' Enter
|
||||
tmux-sleep
|
||||
isolated-tmux capture-pane -p -S -11
|
||||
# CHECK: 1
|
||||
# CHECK: 2
|
||||
# CHECK: 3
|
||||
# CHECK: 4
|
||||
# CHECK: 5
|
||||
# CHECK: 6
|
||||
# CHECK: 7
|
||||
# CHECK: 8
|
||||
# CHECK: 9
|
||||
# CHECK: 10
|
||||
# CHECK: 11 test
|
||||
# CHECK: 2
|
||||
# CHECK: 3
|
||||
# CHECK: 4
|
||||
# CHECK: 5
|
||||
# CHECK: 6
|
||||
# CHECK: 7
|
||||
# CHECK: 8
|
||||
# CHECK: 9
|
||||
# CHECK: 10
|
||||
# CHECK: 11 test
|
||||
|
||||
Reference in New Issue
Block a user