mirror of
https://github.com/nushell/reedline.git
synced 2024-09-19 03:57:45 +03:00
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:
parent
a2682b50f9
commit
dcf8ff3f7a
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
259
src/painter.rs
259
src/painter.rs
@ -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",);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user