From 04c9134275717bb25b9a44ac5e0ea21ce954392c Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Fri, 25 Oct 2024 08:20:20 +0200 Subject: [PATCH] Limit command line rendering to $LINES lines Render the command line buffer only until the last line we can fit on the screen. If the cursor pushes the viewport such that neither the prompt nor the first line of the command line buffer are visible, then we are "scrolled". In this case we need to make sure to erase any leftover prompt, so add a hack to disable the "shared_prefix" optimization that tries to minimize redraws. Down-arrow scrolls down only when on the last line, and up-arrow always scrolls up as much as possible. This is somewhat unconventional; probably we should change the up-arrow behavior but I guess it's a good idea to show the prompt whenever possible. In future we could solve that in a different way: we could keep the prompt visible even if we're scrolled. This would work well because at least the left prompt lives in a different column from the command line buffer. However this assumption breaks when the first line in the command line buffer is soft-wrapped, so keep this approach for now. Note that we're still broken when complete-and-search or history-pager try to draw a pager on top of an overfull screen. Will try to fix this later. Closes #7296 --- src/screen.rs | 121 +++++++++++++++++++++++++---- tests/checks/tmux-commandline.fish | 14 ++++ 2 files changed, 120 insertions(+), 15 deletions(-) diff --git a/src/screen.rs b/src/screen.rs index 4bc0684b0..3fec27ae5 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -253,13 +253,19 @@ pub fn write( ) { let curr_termsize = termsize_last(); let screen_width = curr_termsize.width; + let screen_height = curr_termsize.height; static REPAINTS: AtomicU32 = AtomicU32::new(0); FLOGF!( screen, "Repaint %u", 1 + REPAINTS.fetch_add(1, std::sync::atomic::Ordering::Relaxed) ); - let mut cursor_arr = Cursor::default(); + #[derive(Clone, Copy)] + struct ScrolledCursor { + cursor: Cursor, + scroll_amount: usize, + } + let mut cursor_arr: Option = None; // Turn the command line into the explicit portion and the autosuggestion. let (explicit_command_line, autosuggestion) = commandline.split_at(explicit_len); @@ -284,6 +290,10 @@ pub fn write( return; } let screen_width = usize::try_from(screen_width).unwrap(); + if screen_height == 0 { + return; + } + let screen_height = usize::try_from(curr_termsize.height).unwrap(); // Compute a layout. let layout = compute_layout( @@ -306,7 +316,14 @@ pub fn write( // Append spaces for the left prompt. for _ in 0..layout.left_prompt_space { - self.desired_append_char(' ', HighlightSpec::new(), 0, layout.left_prompt_space, 1); + let _ = self.desired_append_char( + usize::MAX, + ' ', + HighlightSpec::new(), + 0, + layout.left_prompt_space, + 1, + ); } // If overflowing, give the prompt its own line to improve the situation. @@ -320,26 +337,64 @@ pub fn write( loop { // Grab the current cursor's x,y position if this character matches the cursor's offset. if !cursor_is_within_pager && i == cursor_pos { - cursor_arr = self.desired.cursor; + cursor_arr = Some(ScrolledCursor { + cursor: self.desired.cursor, + scroll_amount: (self.desired.line_count() + + if self + .desired + .line_datas + .last() + .as_ref() + .map(|ld| ld.is_soft_wrapped) + .unwrap_or_default() + { + 1 + } else { + 0 + }) + .saturating_sub(screen_height), + }); } if i == effective_commandline.len() { break; } - self.desired_append_char( + if !self.desired_append_char( + cursor_arr + .map(|sc| { + if sc.scroll_amount != 0 { + sc.cursor.y + } else { + screen_height - 1 + } + }) + .unwrap_or(usize::MAX), effective_commandline.as_char_slice()[i], colors[i], usize::try_from(indent[i]).unwrap(), first_line_prompt_space, wcwidth_rendered_min_0(effective_commandline.as_char_slice()[i]), - ); + ) { + break; + } i += 1; } + cursor_arr.as_mut().map( + |ScrolledCursor { + ref mut cursor, + scroll_amount, + }| { + if *scroll_amount != 0 { + self.desired.line_datas = self.desired.line_datas.split_off(*scroll_amount); + cursor.y -= *scroll_amount; + } + }, + ); let full_line_count = self.desired.cursor.y + 1; // Now that we've output everything, set the cursor to the position that we saved in the loop // above. - self.desired.cursor = cursor_arr; + self.desired.cursor = cursor_arr.as_ref().map(|sc| sc.cursor).unwrap_or_default(); if cursor_is_within_pager { self.desired.cursor.x = cursor_pos; @@ -362,7 +417,12 @@ pub fn write( // Append pager_data (none if empty). self.desired.append_lines(&page_rendering.screen_data); - self.update(&layout.left_prompt, &layout.right_prompt, vars); + self.update( + vars, + &layout.left_prompt, + &layout.right_prompt, + cursor_arr.is_some_and(|sc| sc.scroll_amount != 0), + ); self.save_status(); } @@ -516,17 +576,21 @@ pub fn cursor_is_wrapped_to_own_line(&self) -> bool { /// automatically handles linebreaks and lines longer than the screen width. fn desired_append_char( &mut self, + max_y: usize, b: char, c: HighlightSpec, indent: usize, prompt_width: usize, bwidth: usize, - ) { + ) -> bool { let mut line_no = self.desired.cursor.y; if b == '\n' { // Current line is definitely hard wrapped. // Create the next line. + if self.desired.cursor.y + 1 > max_y { + return false; + } self.desired.create_line(self.desired.cursor.y + 1); self.desired.line_mut(self.desired.cursor.y).is_soft_wrapped = false; self.desired.cursor.y += 1; @@ -536,7 +600,16 @@ fn desired_append_char( let line = self.desired.line_mut(line_no); line.indentation = indentation; for _ in 0..indentation { - self.desired_append_char(' ', HighlightSpec::default(), indent, prompt_width, 1); + if !self.desired_append_char( + max_y, + ' ', + HighlightSpec::default(), + indent, + prompt_width, + 1, + ) { + return false; + } } } else if b == '\r' { let current = self.desired.line_mut(line_no); @@ -546,10 +619,16 @@ fn desired_append_char( let screen_width = self.desired.screen_width; let cw = bwidth; + if line_no > max_y { + return false; + } self.desired.create_line(line_no); // Check if we are at the end of the line. If so, continue on the next line. if screen_width.is_none_or(|sw| (self.desired.cursor.x + cw) > sw) { + if self.desired.cursor.y + 1 > max_y { + return false; + } // Current line is soft wrapped (assuming we support it). self.desired.line_mut(self.desired.cursor.y).is_soft_wrapped = true; @@ -570,6 +649,7 @@ fn desired_append_char( self.desired.cursor.y += 1; } } + true } /// Stat stdout and stderr and compare result to previous result in reader_save_status. Repaint @@ -761,7 +841,13 @@ fn scoped_buffer(&mut self) -> impl ScopeGuarding { } /// Update the screen to match the desired output. - fn update(&mut self, left_prompt: &wstr, right_prompt: &wstr, vars: &dyn Environment) { + fn update( + &mut self, + vars: &dyn Environment, + left_prompt: &wstr, + right_prompt: &wstr, + scrolled: bool, + ) { // 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| { @@ -817,7 +903,8 @@ fn update(&mut self, left_prompt: &wstr, right_prompt: &wstr, vars: &dyn Environ let term = term.as_ref(); // Output the left prompt if it has changed. - if left_prompt != zelf.actual_left_prompt { + let visible_left_prompt = if scrolled { L!("") } else { left_prompt }; + if visible_left_prompt != zelf.actual_left_prompt { zelf.r#move(0, 0); let mut start = 0; let osc_133_prompt_start = @@ -832,11 +919,11 @@ fn update(&mut self, left_prompt: &wstr, right_prompt: &wstr, vars: &dyn Environ if i == 0 { osc_133_prompt_start(&mut zelf); } - zelf.write_str(&left_prompt[start..=line_break]); + zelf.write_str(&visible_left_prompt[start..=line_break]); start = line_break + 1; } - zelf.write_str(&left_prompt[start..]); - zelf.actual_left_prompt = left_prompt.to_owned(); + zelf.write_str(&visible_left_prompt[start..]); + zelf.actual_left_prompt = visible_left_prompt.to_owned(); zelf.actual.cursor.x = left_prompt_width; } @@ -867,7 +954,11 @@ 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 = line_shared_prefix(o_line(&zelf, i), s_line(&zelf, i)); + let shared_prefix = if scrolled { + 0 + } else { + line_shared_prefix(o_line(&zelf, i), s_line(&zelf, i)) + }; let mut skip_prefix = shared_prefix; if shared_prefix < o_line(&zelf, i).indentation { if o_line(&zelf, i).indentation > s_line(&zelf, i).indentation diff --git a/tests/checks/tmux-commandline.fish b/tests/checks/tmux-commandline.fish index 0ae7a03ac..910de3fd2 100644 --- a/tests/checks/tmux-commandline.fish +++ b/tests/checks/tmux-commandline.fish @@ -8,3 +8,17 @@ isolated-tmux send-keys 'echo bar|cat' \eg foo tmux-sleep isolated-tmux capture-pane -p # CHECK: prompt 1> echo foobar|cat + +isolated-tmux send-keys C-k C-u C-l 'commandline -i (seq $LINES) scroll_here' Enter +tmux-sleep +isolated-tmux capture-pane -p +# CHECK: 2 +# CHECK: 3 +# CHECK: 4 +# CHECK: 5 +# CHECK: 6 +# CHECK: 7 +# CHECK: 8 +# CHECK: 9 +# CHECK: 10 +# CHECK: scroll_here