Big buffer (#230)

* test big buffer

* clippy error

* more clippy corrections

* extra else for position

* debug cursor

* moving to debug section

* hints in big buffer

* case for prompt one line

* corrected extra line
This commit is contained in:
Fernando Herrera 2022-01-03 10:13:08 +00:00 committed by GitHub
parent a2682b50f9
commit dcf8ff3f7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 256 additions and 17 deletions

View File

@ -281,6 +281,13 @@ impl Reedline {
self
}
/// A builder which configures the painter for debug mode
pub fn with_debug_mode(mut self) -> Reedline {
self.painter = Painter::new_with_debug(io::stdout());
self
}
/// Returns the corresponding expected prompt style for the given edit mode
pub fn prompt_edit_mode(&self) -> PromptEditMode {
self.edit_mode.edit_mode()
@ -394,7 +401,7 @@ impl Reedline {
if let Some(ec) = last_edit_commands {
reedline_events.push(ReedlineEvent::Edit(ec));
}
} else if self.animate {
} else if self.animate && !self.painter.large_buffer {
reedline_events.push(ReedlineEvent::Repaint);
};

View File

@ -19,6 +19,7 @@ use {
fn main() -> Result<()> {
// quick command like parameter handling
let vi_mode = matches!(std::env::args().nth(1), Some(x) if x == "--vi");
let debug_mode = matches!(std::env::args().nth(2), Some(x) if x == "--debug");
let args: Vec<String> = std::env::args().collect();
// if -k is passed, show the events
if args.len() > 1 && args[1] == "-k" {
@ -70,6 +71,10 @@ fn main() -> Result<()> {
))
.with_ansi_colors(true);
if debug_mode {
line_editor = line_editor.with_debug_mode();
}
let prompt = DefaultPrompt::new(1);
loop {

View File

@ -64,7 +64,7 @@ impl<'prompt> PromptLines<'prompt> {
+ self.after_cursor.matches('\n').count()
+ 1;
// adjust lines by the numnber of wrapped additional lines we have
// adjust lines by the number of wrapped additional lines we have
let input =
prompt_indicator.to_string() + self.before_cursor + self.after_cursor + self.hint;
for line in input.split('\n') {
@ -94,12 +94,44 @@ fn estimated_wrapped_line_count(line: &str, terminal_columns: u16) -> usize {
}
}
// Returns a string that skips N number of lines with the next offset of lines
// An offset of 0 would return only one line after skipping the required lines
fn skip_buffer_lines(string: &str, skip: usize, offset: Option<usize>) -> &str {
let mut matches = string.match_indices('\n');
let index = if skip == 0 {
0
} else {
matches
.clone()
.nth(skip - 1)
.map(|(index, _)| index + 1)
.unwrap_or(string.len())
};
let limit = match offset {
Some(offset) => {
let offset = skip + offset;
matches
.nth(offset)
.map(|(index, _)| index)
.unwrap_or(string.len())
}
None => string.len(),
};
let line = &string[index..limit];
line.trim_end_matches('\n')
}
pub struct Painter {
// Stdout
stdout: Stdout,
prompt_coords: PromptCoordinates,
terminal_size: (u16, u16),
last_required_lines: u16,
pub large_buffer: bool,
debug_mode: bool,
}
impl Painter {
@ -109,6 +141,19 @@ impl Painter {
prompt_coords: PromptCoordinates::default(),
terminal_size: (0, 0),
last_required_lines: 0,
large_buffer: false,
debug_mode: false,
}
}
pub fn new_with_debug(stdout: Stdout) -> Self {
Painter {
stdout,
prompt_coords: PromptCoordinates::default(),
terminal_size: (0, 0),
last_required_lines: 0,
large_buffer: false,
debug_mode: true,
}
}
@ -168,6 +213,9 @@ impl Painter {
/// Using the prompt lines object in this function it is estimated how the
/// prompt should scroll up and how much space is required to print all the
/// lines for the buffer
///
/// Note. The ScrollUp operation in crossterm deletes lines from the top of
/// the screen.
pub fn repaint_buffer(
&mut self,
prompt: &dyn Prompt,
@ -179,7 +227,7 @@ impl Painter {
self.stdout.queue(cursor::Hide)?;
// String representation of the prompt
let (screen_width, _) = self.terminal_size;
let (screen_width, screen_height) = self.terminal_size;
let prompt_str = prompt.render_prompt(screen_width as usize);
// The prompt indicator could be normal one or the history indicator
@ -192,16 +240,21 @@ impl Painter {
let required_lines = lines.required_lines(&prompt_str, &prompt_indicator, screen_width);
let remaining_lines = self.remaining_lines();
// Marking the painter state as larger buffer to avoid animations
self.large_buffer = required_lines >= screen_height;
// Cursor distance from prompt
let cursor_distance = self.distance_from_prompt()?;
// Delta indicates how many row are required based on the distance
// from the prompt. The closer the cursor to the prompt (smaller distance)
// the larger the delta and the real required extra lines
//
// TODO. Case when delta is larger than terminal size
let delta = required_lines.saturating_sub(cursor_distance);
if delta > remaining_lines {
// Moving the start position of the cursor based on the size of the required lines
if self.large_buffer {
self.prompt_coords.prompt_start.1 = 0;
} else if delta > remaining_lines {
// Checked sub in case there is overflow
let sub = self
.prompt_coords
@ -226,7 +279,7 @@ impl Painter {
// in the middle of a multi line buffer)
self.stdout.queue(ScrollUp(1))?;
self.prompt_coords.prompt_start.1 = self.prompt_coords.prompt_start.1.saturating_sub(1);
};
}
// Moving the cursor to the start of the prompt
// from this position everything will be printed
@ -235,6 +288,64 @@ impl Painter {
self.prompt_coords.prompt_start.1,
))?;
self.stdout
.queue(MoveToColumn(0))?
.queue(Clear(ClearType::FromCursorDown))?;
if self.large_buffer {
self.print_large_buffer(
prompt,
(&prompt_str, &prompt_indicator),
lines,
cursor_distance,
use_ansi_coloring,
)?
} else {
self.print_small_buffer(
prompt,
&prompt_str,
&prompt_indicator,
lines,
use_ansi_coloring,
)?
}
// The last_required_lines is used to move the cursor at the end where stdout
// can print without overwriting the things written during the paining
self.last_required_lines = required_lines + prompt_str.lines().count() as u16;
// In debug mode a string with position information is printed at the end of the buffer
if self.debug_mode {
let prompt_length = prompt_str.len() + prompt_indicator.len();
let estimated_prompt = estimated_wrapped_line_count(&prompt_str, screen_width);
self.stdout
.queue(Print(format!(" [h{}:", screen_height)))?
.queue(Print(format!("w{}] ", screen_width)))?
.queue(Print(format!("[x{}:", self.prompt_coords.prompt_start.0)))?
.queue(Print(format!("y{}] ", self.prompt_coords.prompt_start.1)))?
.queue(Print(format!("re:{} ", required_lines)))?
.queue(Print(format!("de:{} ", delta)))?
.queue(Print(format!("di:{} ", cursor_distance)))?
.queue(Print(format!("pr:{} ", prompt_length)))?
.queue(Print(format!("wr:{} ", estimated_prompt)))?
.queue(Print(format!("rm:{} ", remaining_lines)))?
.queue(Print(format!("ls:{} ", self.last_required_lines)))?;
}
self.stdout.queue(RestorePosition)?.queue(cursor::Show)?;
self.flush()
}
fn print_small_buffer(
&mut self,
prompt: &dyn Prompt,
prompt_str: &str,
prompt_indicator: &str,
lines: PromptLines,
use_ansi_coloring: bool,
) -> Result<()> {
// print our prompt with color
if use_ansi_coloring {
self.stdout
@ -242,27 +353,87 @@ impl Painter {
}
self.stdout
.queue(MoveToColumn(0))?
.queue(Clear(ClearType::FromCursorDown))?
.queue(Print(&prompt_str))?
.queue(Print(&prompt_indicator))?;
if use_ansi_coloring {
self.stdout.queue(ResetColor)?;
}
self.stdout
.queue(Print(&lines.before_cursor))?
.queue(SavePosition)?
.queue(Print(&lines.hint))?
.queue(Print(&lines.after_cursor))?
.queue(RestorePosition)?
.queue(cursor::Show)?;
.queue(Print(&lines.after_cursor))?;
// The last_required_lines is used to move the cursor at the end where stdout
// can print without overwriting the things written during the paining
// The number 3 is to give enough space after the buffer lines
self.last_required_lines = required_lines + 3;
Ok(())
}
self.flush()
fn print_large_buffer(
&mut self,
prompt: &dyn Prompt,
prompt_str: (&str, &str),
lines: PromptLines,
cursor_distance: u16,
use_ansi_coloring: bool,
) -> Result<()> {
let (prompt_str, prompt_indicator) = prompt_str;
let (screen_width, screen_height) = self.terminal_size;
let remaining_lines = screen_height.saturating_sub(cursor_distance);
// Calculating the total lines before the cursor
// The -1 in the total_lines_before is there because the at least one line of the prompt
// indicator is printed in the same line as the first line of the buffer
let complete_prompt = prompt_str.to_string() + prompt_indicator;
let prompt_wrap = estimated_wrapped_line_count(&complete_prompt, screen_width);
let prompt_lines = prompt_str.matches('\n').count() + prompt_wrap;
let prompt_indicator_lines = prompt_indicator.lines().count();
let before_cursor_lines = lines.before_cursor.lines().count();
let total_lines_before = prompt_lines + prompt_indicator_lines + before_cursor_lines - 1;
// Extra rows represent how many rows are "above" the visible area in the terminal
let extra_rows = (total_lines_before).saturating_sub(screen_height as usize);
// print our prompt with color
if use_ansi_coloring {
self.stdout
.queue(SetForegroundColor(prompt.get_prompt_color()))?;
}
// In case the prompt is made out of multiple lines, the prompt is split by
// lines and only the required ones are printed
let prompt_skipped = skip_buffer_lines(prompt_str, extra_rows, None);
self.stdout.queue(Print(prompt_skipped))?;
// Adjusting extra_rows base on the calculated prompt line size
let extra_rows = extra_rows.saturating_sub(prompt_lines);
let indicator_skipped = skip_buffer_lines(prompt_indicator, extra_rows, None);
self.stdout.queue(Print(indicator_skipped))?;
if use_ansi_coloring {
self.stdout.queue(ResetColor)?;
}
// Selecting the lines before the cursor that will be printed
let before_cursor_skipped = skip_buffer_lines(lines.before_cursor, extra_rows, None);
self.stdout.queue(Print(before_cursor_skipped))?;
self.stdout.queue(SavePosition)?;
// Selecting lines for the hint
// The -1 subtraction is done because the remaining lines consider the line where the
// cursor is located as a remaining line. That has to be removed to get the correct offset
// for the hint and after cursor lines
let offset = remaining_lines.saturating_sub(1) as usize;
let hint_skipped = skip_buffer_lines(lines.hint, 0, Some(offset));
self.stdout.queue(Print(hint_skipped))?;
// Selecting lines after the cursor
let after_cursor_skipped = skip_buffer_lines(lines.after_cursor, 0, Some(offset));
self.stdout.queue(Print(after_cursor_skipped))?;
Ok(())
}
/// Updates prompt origin and offset to handle a screen resize event
@ -339,3 +510,59 @@ impl Painter {
self.stdout.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_skip_lines() {
let string = "sentence1\nsentence2\nsentence3\n";
assert_eq!(skip_buffer_lines(string, 1, None), "sentence2\nsentence3");
assert_eq!(skip_buffer_lines(string, 2, None), "sentence3");
assert_eq!(skip_buffer_lines(string, 3, None), "");
assert_eq!(skip_buffer_lines(string, 4, None), "");
}
#[test]
fn test_skip_lines_no_newline() {
let string = "sentence1";
assert_eq!(skip_buffer_lines(string, 0, None), "sentence1");
assert_eq!(skip_buffer_lines(string, 1, None), "");
}
#[test]
fn test_skip_lines_with_limit() {
let string = "sentence1\nsentence2\nsentence3\nsentence4\nsentence5";
assert_eq!(
skip_buffer_lines(string, 1, Some(1)),
"sentence2\nsentence3",
);
assert_eq!(
skip_buffer_lines(string, 1, Some(2)),
"sentence2\nsentence3\nsentence4",
);
assert_eq!(
skip_buffer_lines(string, 2, Some(1)),
"sentence3\nsentence4",
);
assert_eq!(
skip_buffer_lines(string, 1, Some(10)),
"sentence2\nsentence3\nsentence4\nsentence5",
);
assert_eq!(
skip_buffer_lines(string, 0, Some(1)),
"sentence1\nsentence2",
);
assert_eq!(skip_buffer_lines(string, 0, Some(0)), "sentence1",);
assert_eq!(skip_buffer_lines(string, 1, Some(0)), "sentence2",);
}
}