diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 849c6988d..eaf3fff07 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ fish ?.?.? (released ???) Notable improvements and fixes ------------------------------ - New Taiwanese Chinese translation. +- Fixed not properly clearing lines when a :ref:`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} ` have been taught to reset :doc:`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 ---------------------- diff --git a/src/reader.rs b/src/reader.rs index fcbbd2c1f..409906b82 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -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 diff --git a/src/screen.rs b/src/screen.rs index 73136c639..0bbdf0a04 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -154,7 +154,12 @@ pub struct ScreenData { /// The width of the screen once we have rendered. screen_width: Option, + /// 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 { diff --git a/src/tests/screen.rs b/src/tests/screen.rs index 0caed14c3..79a734d05 100644 --- a/src/tests/screen.rs +++ b/src/tests/screen.rs @@ -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(), } diff --git a/tests/checks/tmux-commandline.fish b/tests/checks/tmux-commandline.fish index b766a098b..b2ec0128b 100644 --- a/tests/checks/tmux-commandline.fish +++ b/tests/checks/tmux-commandline.fish @@ -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 diff --git a/tests/checks/tmux-multiline-prompt.fish b/tests/checks/tmux-multiline-prompt.fish index c47f5e4a2..991e2fb7e 100644 --- a/tests/checks/tmux-multiline-prompt.fish +++ b/tests/checks/tmux-multiline-prompt.fish @@ -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> diff --git a/tests/checks/tmux-prompt.fish b/tests/checks/tmux-prompt.fish index 3e002c67e..9f457f686 100644 --- a/tests/checks/tmux-prompt.fish +++ b/tests/checks/tmux-prompt.fish @@ -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