Limit command line rendering to $LINES lines
Some checks are pending
make test / ubuntu (push) Waiting to run
make test / ubuntu-32bit-static-pcre2 (push) Waiting to run
make test / ubuntu-asan (push) Waiting to run
make test / macos (push) Waiting to run
Rust checks / rustfmt (push) Waiting to run
Rust checks / clippy (push) Waiting to run

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 @@ impl Screen {
) {
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 @@ impl Screen {
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 @@ impl Screen {
// 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 @@ impl Screen {
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 @@ impl Screen {
// 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 @@ impl Screen {
/// 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 @@ impl Screen {
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 @@ impl Screen {
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 @@ impl Screen {
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 @@ impl 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 @@ impl Screen {
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 @@ impl Screen {
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 @@ impl Screen {
// 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