Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/nushell/reedline
Browse files Browse the repository at this point in the history
  • Loading branch information
cactusdualcore committed Apr 30, 2024
2 parents 029a991 + 4cf8c75 commit 0d8da0e
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 49 deletions.
2 changes: 1 addition & 1 deletion examples/ide_completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn main() -> io::Result<()> {
// Max width of the completion box, including the border
let max_completion_width: u16 = 50;
// Max height of the completion box, including the border
let max_completion_height = u16::MAX;
let max_completion_height: u16 = u16::MAX;
// Padding inside of the completion box (on the left and right side)
let padding: u16 = 0;
// Whether to draw the default border around the completion box
Expand Down
33 changes: 18 additions & 15 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::{
FileBackedHistory, History, HistoryCursor, HistoryItem, HistoryItemId,
HistoryNavigationQuery, HistorySessionId, SearchDirection, SearchQuery,
},
painting::{Painter, PromptLines},
painting::{Painter, PainterSuspendedState, PromptLines},
prompt::PromptHistorySearchStatus,
result::{ReedlineError, ReedlineErrorVariants},
terminal_extensions::{bracketed_paste::BracketedPasteGuard, kitty::KittyProtocolGuard},
Expand Down Expand Up @@ -115,8 +115,9 @@ pub struct Reedline {
history_cursor_on_excluded: bool,
input_mode: InputMode,

// Yielded to the host program after a `ReedlineEvent::ExecuteHostCommand`, thus redraw in-place
executing_host_command: bool,
// State of the painter after a `ReedlineEvent::ExecuteHostCommand` was requested, used after
// execution to decide if we can re-use the previous prompt or paint a new one.
suspended_state: Option<PainterSuspendedState>,

// Validator
validator: Option<Box<dyn Validator>>,
Expand Down Expand Up @@ -368,12 +369,14 @@ impl Reedline {
/// Helper implementing the logic for [`Reedline::read_line()`] to be wrapped
/// in a `raw_mode` context.
fn read_line_helper(&mut self, prompt: &dyn Prompt) -> io::Result<Signal> {
if self.executing_host_command {
self.executing_host_command = false;
} else {
self.painter.initialize_prompt_position()?;
self.hide_hints = false;
self.painter
.initialize_prompt_position(self.suspended_state.as_ref())?;
if self.suspended_state.is_some() {
// Last editor was suspended to run a ExecuteHostCommand event,
// we are resuming operation now.
self.suspended_state = None;
}
self.hide_hints = false;

self.repaint(prompt)?;

Expand Down Expand Up @@ -470,8 +473,11 @@ impl Reedline {
for event in reedline_events.drain(..) {
match self.handle_event(prompt, event)? {
EventStatus::Exits(signal) => {
if !self.executing_host_command {
// Move the cursor below the input area, for external commands or new read_line call
// Check if we are merely suspended (to process an ExecuteHostCommand event)
// or if we're about to quit the editor.
if self.suspended_state.is_none() {
// We are about to quit the editor, move the cursor below the input
// area, for external commands or new read_line call
self.painter.move_cursor_to_end()?;
}
return Ok(signal);
Expand Down Expand Up @@ -552,8 +558,7 @@ impl Reedline {
Ok(EventStatus::Handled)
}
ReedlineEvent::ExecuteHostCommand(host_command) => {
// TODO: Decide if we need to do something special to have a nicer painter state on the next go
self.executing_host_command = true;
self.suspended_state = Some(self.painter.state_before_suspension());
Ok(EventStatus::Exits(Signal::Success(host_command)))
}
ReedlineEvent::Edit(commands) => {
Expand Down Expand Up @@ -823,8 +828,7 @@ impl Reedline {
}
}
ReedlineEvent::ExecuteHostCommand(host_command) => {
// TODO: Decide if we need to do something special to have a nicer painter state on the next go
self.executing_host_command = true;
self.suspended_state = Some(self.painter.state_before_suspension());
Ok(EventStatus::Exits(Signal::Success(host_command)))
}
ReedlineEvent::Edit(commands) => {
Expand Down Expand Up @@ -1268,7 +1272,6 @@ impl Reedline {
// If we don't find any history searching by session id, then let's
// search everything, otherwise use the result from the session search
if history_search_by_session.is_none() {
eprintln!("Using global search");
self.history
.search(SearchQuery::last_with_prefix(
parsed_prefix.clone(),
Expand Down
2 changes: 1 addition & 1 deletion src/engine/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl ReedlineBuilder {
self.history_session_id,
),
input_mode: InputMode::Regular,
executing_host_command: false,
suspended_state: None,
}
}

Expand Down
1 change: 1 addition & 0 deletions src/menu/ide_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,7 @@ impl Menu for IdeMenu {

fn menu_required_lines(&self, _terminal_columns: u16) -> u16 {
self.get_rows()
.min(self.default_details.max_completion_height)
}

fn menu_string(&self, available_lines: u16, use_ansi_coloring: bool) -> String {
Expand Down
2 changes: 1 addition & 1 deletion src/painting/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod prompt_lines;
mod styled_text;
mod utils;

pub use painter::Painter;
pub use painter::{Painter, PainterSuspendedState};
pub(crate) use prompt_lines::PromptLines;
pub use styled_text::StyledText;
pub(crate) use utils::estimate_single_line_wraps;
146 changes: 115 additions & 31 deletions src/painting/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use {
QueueableCommand,
},
std::io::{Result, Write},
std::ops::RangeInclusive,
};
#[cfg(feature = "external_printer")]
use {crate::LineBuffer, crossterm::cursor::MoveUp};
Expand Down Expand Up @@ -49,6 +50,42 @@ fn skip_buffer_lines(string: &str, skip: usize, offset: Option<usize>) -> &str {
/// the type used by crossterm operations
pub type W = std::io::BufWriter<std::io::Stderr>;

#[derive(Debug, PartialEq, Eq)]
pub struct PainterSuspendedState {
previous_prompt_rows_range: RangeInclusive<u16>,
}

#[derive(Debug, PartialEq, Eq)]
enum PromptRowSelector {
UseExistingPrompt { start_row: u16 },
MakeNewPrompt { new_row: u16 },
}

// Selects the row where the next prompt should start on, taking into account and whether it should re-use a previous
// prompt.
fn select_prompt_row(
suspended_state: Option<&PainterSuspendedState>,
(column, row): (u16, u16), // NOTE: Positions are 0 based here
) -> PromptRowSelector {
if let Some(painter_state) = suspended_state {
// The painter was suspended, try to re-use the last prompt position to avoid
// unnecessarily making new prompts.
if painter_state.previous_prompt_rows_range.contains(&row) {
// Cursor is still in the range of the previous prompt, re-use it.
let start_row = *painter_state.previous_prompt_rows_range.start();
return PromptRowSelector::UseExistingPrompt { start_row };
} else {
// There was some output or cursor is outside of the range of previous prompt make a
// fresh new prompt.
}
}

// Assumption: if the cursor is not on the zeroth column,
// there is content we want to leave intact, thus advance to the next row.
let new_row = if column > 0 { row + 1 } else { row };
PromptRowSelector::MakeNewPrompt { new_row }
}

/// Implementation of the output to the terminal
pub struct Painter {
// Stdout
Expand All @@ -57,6 +94,7 @@ pub struct Painter {
terminal_size: (u16, u16),
last_required_lines: u16,
large_buffer: bool,
after_cursor_lines: Option<String>,
}

impl Painter {
Expand All @@ -67,6 +105,7 @@ impl Painter {
terminal_size: (0, 0),
last_required_lines: 0,
large_buffer: false,
after_cursor_lines: None,
}
}

Expand All @@ -85,12 +124,26 @@ impl Painter {
self.screen_height().saturating_sub(self.prompt_start_row)
}

/// Returns the state necessary before suspending the painter (to run a host command event).
///
/// This state will be used to re-initialize the painter to re-use last prompt if possible.
pub fn state_before_suspension(&self) -> PainterSuspendedState {
let start_row = self.prompt_start_row;
let final_row = start_row + self.last_required_lines;
PainterSuspendedState {
previous_prompt_rows_range: start_row..=final_row,
}
}

/// Sets the prompt origin position and screen size for a new line editor
/// invocation
///
/// Not to be used for resizes during a running line editor, use
/// [`Painter::handle_resize()`] instead
pub(crate) fn initialize_prompt_position(&mut self) -> Result<()> {
pub(crate) fn initialize_prompt_position(
&mut self,
suspended_state: Option<&PainterSuspendedState>,
) -> Result<()> {
// Update the terminal size
self.terminal_size = {
let size = terminal::size()?;
Expand All @@ -102,26 +155,26 @@ impl Painter {
size
}
};
// Cursor positions are 0 based here.
let (column, row) = cursor::position()?;
// Assumption: if the cursor is not on the zeroth column,
// there is content we want to leave intact, thus advance to the next row
let new_row = if column > 0 { row + 1 } else { row };
// If we are on the last line and would move beyond the last line due to
// the condition above, we need to make room for the prompt.
// Otherwise printing the prompt would scroll of the stored prompt
// origin, causing issues after repaints.
let new_row = if new_row == self.screen_height() {
self.print_crlf()?;
new_row.saturating_sub(1)
} else {
new_row
let prompt_selector = select_prompt_row(suspended_state, cursor::position()?);
self.prompt_start_row = match prompt_selector {
PromptRowSelector::UseExistingPrompt { start_row } => start_row,
PromptRowSelector::MakeNewPrompt { new_row } => {
// If we are on the last line and would move beyond the last line, we need to make
// room for the prompt.
// Otherwise printing the prompt would scroll off the stored prompt
// origin, causing issues after repaints.
if new_row == self.screen_height() {
self.print_crlf()?;
new_row.saturating_sub(1)
} else {
new_row
}
}
};
self.prompt_start_row = new_row;
Ok(())
}

/// Main pain painter for the prompt and buffer
/// Main painter for the prompt and buffer
/// It queues all the actions required to print the prompt together with
/// lines that make the buffer.
/// Using the prompt lines object in this function it is estimated how the
Expand Down Expand Up @@ -181,10 +234,15 @@ impl Painter {
self.print_small_buffer(prompt, lines, menu, 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 painting
// The last_required_lines is used to calculate safe range of the current prompt.
self.last_required_lines = required_lines;

self.after_cursor_lines = if !lines.after_cursor.is_empty() {
Some(lines.after_cursor.to_string())
} else {
None
};

self.stdout.queue(RestorePosition)?;

if let Some(shapes) = cursor_config {
Expand Down Expand Up @@ -461,7 +519,7 @@ impl Painter {
self.stdout.queue(cursor::Show)?;

self.stdout.flush()?;
self.initialize_prompt_position()
self.initialize_prompt_position(None)
}

pub(crate) fn clear_scrollback(&mut self) -> Result<()> {
Expand All @@ -470,22 +528,17 @@ impl Painter {
.queue(crossterm::terminal::Clear(ClearType::Purge))?
.queue(cursor::MoveTo(0, 0))?
.flush()?;
self.initialize_prompt_position()
self.initialize_prompt_position(None)
}

// The prompt is moved to the end of the buffer after the event was handled
// If the prompt is in the middle of a multiline buffer, then the output to stdout
// could overwrite the buffer writing
pub(crate) fn move_cursor_to_end(&mut self) -> Result<()> {
let final_row = self.prompt_start_row + self.last_required_lines;
let scroll = final_row.saturating_sub(self.screen_height() - 1);
if scroll != 0 {
self.queue_universal_scroll(scroll)?;
if let Some(after_cursor) = &self.after_cursor_lines {
self.stdout
.queue(Clear(ClearType::FromCursorDown))?
.queue(Print(after_cursor))?;
}
self.stdout
.queue(MoveTo(0, final_row.min(self.screen_height() - 1)))?;

self.stdout.flush()
self.print_crlf()
}

/// Prints an external message
Expand Down Expand Up @@ -619,4 +672,35 @@ mod tests {
assert_eq!(skip_buffer_lines(string, 0, Some(0)), "sentence1",);
assert_eq!(skip_buffer_lines(string, 1, Some(0)), "sentence2",);
}

#[test]
fn test_select_new_prompt_with_no_state_no_output() {
assert_eq!(
select_prompt_row(None, (0, 12)),
PromptRowSelector::MakeNewPrompt { new_row: 12 }
);
}

#[test]
fn test_select_new_prompt_with_no_state_but_output() {
assert_eq!(
select_prompt_row(None, (3, 12)),
PromptRowSelector::MakeNewPrompt { new_row: 13 }
);
}

#[test]
fn test_select_existing_prompt() {
let state = PainterSuspendedState {
previous_prompt_rows_range: 11..=13,
};
assert_eq!(
select_prompt_row(Some(&state), (0, 12)),
PromptRowSelector::UseExistingPrompt { start_row: 11 }
);
assert_eq!(
select_prompt_row(Some(&state), (3, 12)),
PromptRowSelector::UseExistingPrompt { start_row: 11 }
);
}
}

0 comments on commit 0d8da0e

Please sign in to comment.