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
This commit is contained in:
Johannes Altmanninger
2024-10-25 08:20:20 +02:00
parent 50333d8d00
commit 04c9134275
2 changed files with 120 additions and 15 deletions

View File

@@ -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<ScrolledCursor> = 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<Target = &mut Screen> {
}
/// 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

View File

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