mirror of
https://github.com/nushell/reedline.git
synced 2024-09-19 03:57:45 +03:00
Context menu (#243)
* vi keybinding events * context menu for reedline * corrected tests * text style for menu
This commit is contained in:
parent
a5b6cc079b
commit
af19a7bfc6
191
src/context_menu.rs
Normal file
191
src/context_menu.rs
Normal file
@ -0,0 +1,191 @@
|
||||
use nu_ansi_term::Color;
|
||||
|
||||
/// Struct to store coloring for the menu
|
||||
struct MenuTextColor {
|
||||
selection_style: String,
|
||||
text_style: String,
|
||||
}
|
||||
|
||||
impl Default for MenuTextColor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
selection_style: Color::Red.bold().prefix().to_string(),
|
||||
text_style: Color::DarkGray.prefix().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Context menu definition
|
||||
pub struct ContextMenu {
|
||||
filler: Box<dyn MenuFiller>,
|
||||
active: bool,
|
||||
/// Context menu coloring
|
||||
color: MenuTextColor,
|
||||
/// Number of minimum rows that are displayed when
|
||||
/// the required lines is larger than the available lines
|
||||
min_rows: u16,
|
||||
/// column position of the cursor. Starts from 0
|
||||
pub col_pos: u16,
|
||||
/// row position in the menu. Starts from 0
|
||||
pub row_pos: u16,
|
||||
/// Number of columns that the menu will have
|
||||
pub cols: u16,
|
||||
/// Column width
|
||||
pub col_width: usize,
|
||||
}
|
||||
|
||||
impl Default for ContextMenu {
|
||||
fn default() -> Self {
|
||||
let filler = Box::new(ExampleData::new());
|
||||
Self::new_with(filler)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenu {
|
||||
/// Creates a context menu with a filler
|
||||
pub fn new_with(filler: Box<dyn MenuFiller>) -> Self {
|
||||
Self {
|
||||
filler,
|
||||
active: false,
|
||||
color: MenuTextColor::default(),
|
||||
min_rows: 3,
|
||||
col_pos: 0,
|
||||
row_pos: 0,
|
||||
cols: 4,
|
||||
col_width: 15,
|
||||
}
|
||||
}
|
||||
|
||||
/// Activates context menu
|
||||
pub fn activate(&mut self) {
|
||||
self.active = true;
|
||||
self.reset_position();
|
||||
}
|
||||
|
||||
/// Deactivates context menu
|
||||
pub fn deactivate(&mut self) {
|
||||
self.active = false
|
||||
}
|
||||
|
||||
/// Deactivates context menu
|
||||
pub fn is_active(&mut self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
/// Gets values from filler that will be displayed in the menu
|
||||
pub fn get_values(&self) -> Vec<&str> {
|
||||
self.filler.context_values()
|
||||
}
|
||||
|
||||
/// Calculates how many rows the Menu will use
|
||||
pub fn get_rows(&self) -> u16 {
|
||||
let rows = self.get_values().len() as f64 / self.cols as f64;
|
||||
rows.ceil() as u16
|
||||
}
|
||||
|
||||
/// Minimum rows that should be displayed by the menu
|
||||
pub fn min_rows(&self) -> u16 {
|
||||
self.get_rows().min(self.min_rows)
|
||||
}
|
||||
|
||||
/// Reset menu position
|
||||
pub fn reset_position(&mut self) {
|
||||
self.col_pos = 0;
|
||||
self.row_pos = 0;
|
||||
}
|
||||
|
||||
/// Menu index based on column and row position
|
||||
pub fn position(&self) -> usize {
|
||||
let position = self.row_pos * self.cols + self.col_pos;
|
||||
position as usize
|
||||
}
|
||||
|
||||
/// Move menu cursor up
|
||||
pub fn move_up(&mut self) {
|
||||
self.row_pos = if let Some(row) = self.row_pos.checked_sub(1) {
|
||||
row
|
||||
} else {
|
||||
self.get_rows().saturating_sub(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Move menu cursor left
|
||||
pub fn move_down(&mut self) {
|
||||
let new_row = self.row_pos + 1;
|
||||
self.row_pos = if new_row >= self.get_rows() {
|
||||
0
|
||||
} else {
|
||||
new_row
|
||||
}
|
||||
}
|
||||
|
||||
/// Move menu cursor left
|
||||
pub fn move_left(&mut self) {
|
||||
self.col_pos = if let Some(row) = self.col_pos.checked_sub(1) {
|
||||
row
|
||||
} else {
|
||||
self.cols.saturating_sub(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Move menu cursor right
|
||||
pub fn move_right(&mut self) {
|
||||
let new_col = self.col_pos + 1;
|
||||
self.col_pos = if new_col >= self.cols { 0 } else { new_col }
|
||||
}
|
||||
|
||||
/// Get selected value from filler
|
||||
pub fn get_value(&self) -> Option<&str> {
|
||||
self.get_values().get(self.position()).copied()
|
||||
}
|
||||
|
||||
/// Text style for menu
|
||||
pub fn text_style(&self, index: usize) -> &str {
|
||||
if index == self.position() {
|
||||
&self.color.selection_style
|
||||
} else {
|
||||
&self.color.text_style
|
||||
}
|
||||
}
|
||||
|
||||
/// End of line for menu
|
||||
pub fn end_of_line(&self, column: u16) -> &str {
|
||||
if column == self.cols.saturating_sub(1) {
|
||||
"\r\n"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/// Printable width for a line
|
||||
pub fn printable_width(&self, line: &str) -> usize {
|
||||
let printable_width = (self.col_width - 2) as usize;
|
||||
printable_width.min(line.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// The MenuFiller is a trait that defines how the data for the context menu
|
||||
/// will be collected.
|
||||
pub trait MenuFiller: Send {
|
||||
/// Collects menu values
|
||||
fn context_values(&self) -> Vec<&str>;
|
||||
}
|
||||
|
||||
/// Data example for Reedline ContextMenu
|
||||
struct ExampleData {}
|
||||
|
||||
impl MenuFiller for ExampleData {
|
||||
fn context_values(&self) -> Vec<&str> {
|
||||
vec![
|
||||
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
|
||||
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl ExampleData {
|
||||
/// Creates new instance of Example Menu
|
||||
pub fn new() -> Self {
|
||||
ExampleData {}
|
||||
}
|
||||
}
|
@ -25,26 +25,30 @@ impl EditMode for Emacs {
|
||||
fn parse_event(&mut self, event: Event) -> ReedlineEvent {
|
||||
match event {
|
||||
Event::Key(KeyEvent { code, modifiers }) => match (modifiers, code) {
|
||||
(KeyModifiers::NONE, KeyCode::Char(c)) => {
|
||||
ReedlineEvent::Edit(vec![EditCommand::InsertChar(c)])
|
||||
(modifier, KeyCode::Char(c)) => {
|
||||
// Note. The modifier can also be a combination of modifiers, for
|
||||
// example:
|
||||
// KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||
// KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT
|
||||
//
|
||||
// Mixed modifiers are used by non american keyboards that have extra
|
||||
// keys like 'alt gr'. Keep this in mind if in the future there are
|
||||
// cases where an event is not being captured
|
||||
if modifier == KeyModifiers::SHIFT {
|
||||
let char = c.to_ascii_uppercase();
|
||||
ReedlineEvent::Edit(vec![EditCommand::InsertChar(char)])
|
||||
} else if modifier == KeyModifiers::NONE
|
||||
|| modifier == KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||
|| modifier
|
||||
== KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT
|
||||
{
|
||||
ReedlineEvent::Edit(vec![EditCommand::InsertChar(c)])
|
||||
} else {
|
||||
self.keybindings
|
||||
.find_binding(modifier, code)
|
||||
.unwrap_or(ReedlineEvent::None)
|
||||
}
|
||||
}
|
||||
// This combination of modifiers (CONTROL | ALT) is needed for non american keyboards.
|
||||
// There is a special key called 'alt gr' that is captured with the combination
|
||||
// of those two modifiers
|
||||
(m, KeyCode::Char(c)) if m == KeyModifiers::CONTROL | KeyModifiers::ALT => {
|
||||
ReedlineEvent::Edit(vec![EditCommand::InsertChar(c)])
|
||||
}
|
||||
|
||||
(m, KeyCode::Char(c))
|
||||
if m == KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT =>
|
||||
{
|
||||
ReedlineEvent::Edit(vec![EditCommand::InsertChar(c)])
|
||||
}
|
||||
|
||||
(KeyModifiers::SHIFT, KeyCode::Char(c)) => {
|
||||
ReedlineEvent::Edit(vec![EditCommand::InsertChar(c.to_ascii_uppercase())])
|
||||
}
|
||||
|
||||
(KeyModifiers::NONE, KeyCode::Enter) => ReedlineEvent::Enter,
|
||||
_ => self
|
||||
.keybindings
|
||||
@ -92,7 +96,7 @@ mod test {
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::CONTROL,
|
||||
KeyCode::Char('l'),
|
||||
ReedlineEvent::HandleTab,
|
||||
ReedlineEvent::Complete,
|
||||
);
|
||||
|
||||
let mut emacs = Emacs::new(keybindings);
|
||||
@ -102,7 +106,7 @@ mod test {
|
||||
});
|
||||
let result = emacs.parse_event(ctrl_l);
|
||||
|
||||
assert_eq!(result, ReedlineEvent::HandleTab);
|
||||
assert_eq!(result, ReedlineEvent::Complete);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -52,7 +52,7 @@ impl Keybindings {
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_bind(command: EditCommand) -> ReedlineEvent {
|
||||
pub fn edit_bind(command: EditCommand) -> ReedlineEvent {
|
||||
ReedlineEvent::Edit(vec![command])
|
||||
}
|
||||
|
||||
@ -105,7 +105,7 @@ pub fn default_emacs_keybindings() -> Keybindings {
|
||||
kb.add_binding(KM::ALT, KC::Backspace, edit_bind(EC::BackspaceWord));
|
||||
kb.add_binding(KM::NONE, KC::End, edit_bind(EC::MoveToLineEnd));
|
||||
kb.add_binding(KM::NONE, KC::Home, edit_bind(EC::MoveToLineStart));
|
||||
kb.add_binding(KM::NONE, KC::Tab, ReedlineEvent::HandleTab);
|
||||
kb.add_binding(KM::NONE, KC::Tab, ReedlineEvent::Complete);
|
||||
kb.add_binding(KM::NONE, KC::Up, ReedlineEvent::Up);
|
||||
kb.add_binding(KM::NONE, KC::Down, ReedlineEvent::Down);
|
||||
kb.add_binding(KM::NONE, KC::Left, edit_bind(EC::MoveLeft));
|
||||
@ -113,28 +113,8 @@ pub fn default_emacs_keybindings() -> Keybindings {
|
||||
kb.add_binding(KM::NONE, KC::Delete, edit_bind(EC::Delete));
|
||||
kb.add_binding(KM::NONE, KC::Backspace, edit_bind(EC::Backspace));
|
||||
|
||||
kb.add_binding(KM::ALT, KC::Char('1'), ReedlineEvent::ContextMenu);
|
||||
kb.add_binding(KM::NONE, KC::Esc, ReedlineEvent::Esc);
|
||||
|
||||
kb
|
||||
}
|
||||
|
||||
pub fn default_vi_normal_keybindings() -> Keybindings {
|
||||
Keybindings::new()
|
||||
}
|
||||
|
||||
pub fn default_vi_insert_keybindings() -> Keybindings {
|
||||
use EditCommand as EC;
|
||||
use KeyCode as KC;
|
||||
use KeyModifiers as KM;
|
||||
|
||||
let mut keybindings = Keybindings::new();
|
||||
|
||||
keybindings.add_binding(KM::NONE, KC::Up, ReedlineEvent::Up);
|
||||
keybindings.add_binding(KM::NONE, KC::Down, ReedlineEvent::Down);
|
||||
keybindings.add_binding(KM::NONE, KC::Left, edit_bind(EC::MoveLeft));
|
||||
keybindings.add_binding(KM::NONE, KC::Right, edit_bind(EC::MoveRight));
|
||||
keybindings.add_binding(KM::NONE, KC::Backspace, edit_bind(EC::Backspace));
|
||||
keybindings.add_binding(KM::NONE, KC::Delete, edit_bind(EC::Delete));
|
||||
keybindings.add_binding(KM::NONE, KC::End, edit_bind(EC::MoveToLineEnd));
|
||||
keybindings.add_binding(KM::NONE, KC::Home, edit_bind(EC::MoveToLineStart));
|
||||
|
||||
keybindings
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
mod command;
|
||||
mod motion;
|
||||
mod parser;
|
||||
mod vi_keybindings;
|
||||
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use vi_keybindings::{default_vi_insert_keybindings, default_vi_normal_keybindings};
|
||||
|
||||
use super::EditMode;
|
||||
use crate::{
|
||||
edit_mode::{
|
||||
keybindings::{default_vi_insert_keybindings, default_vi_normal_keybindings, Keybindings},
|
||||
vi::parser::parse,
|
||||
},
|
||||
edit_mode::{keybindings::Keybindings, vi::parser::parse},
|
||||
enums::{EditCommand, ReedlineEvent},
|
||||
PromptEditMode, PromptViMode,
|
||||
};
|
||||
@ -54,34 +53,40 @@ impl EditMode for Vi {
|
||||
}
|
||||
}
|
||||
|
||||
let char = if let KeyModifiers::SHIFT = modifier {
|
||||
c.to_ascii_uppercase()
|
||||
} else {
|
||||
c
|
||||
};
|
||||
self.cache.push(char);
|
||||
if modifier == KeyModifiers::NONE || modifier == KeyModifiers::SHIFT {
|
||||
let char = if let KeyModifiers::SHIFT = modifier {
|
||||
c.to_ascii_uppercase()
|
||||
} else {
|
||||
c
|
||||
};
|
||||
self.cache.push(char);
|
||||
|
||||
let res = parse(&mut self.cache.iter().peekable());
|
||||
let res = parse(&mut self.cache.iter().peekable());
|
||||
|
||||
if res.enter_insert_mode() {
|
||||
self.mode = Mode::Insert;
|
||||
}
|
||||
if res.enter_insert_mode() {
|
||||
self.mode = Mode::Insert;
|
||||
}
|
||||
|
||||
let event = res.to_reedline_event();
|
||||
match event {
|
||||
ReedlineEvent::None => {
|
||||
if !res.is_valid() {
|
||||
let event = res.to_reedline_event();
|
||||
match event {
|
||||
ReedlineEvent::None => {
|
||||
if !res.is_valid() {
|
||||
self.cache.clear();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.cache.clear();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.cache.clear();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
self.previous = Some(event.clone());
|
||||
self.previous = Some(event.clone());
|
||||
|
||||
event
|
||||
event
|
||||
} else {
|
||||
self.normal_keybindings
|
||||
.find_binding(modifiers, code)
|
||||
.unwrap_or(ReedlineEvent::None)
|
||||
}
|
||||
}
|
||||
(Mode::Insert, modifier, KeyCode::Char(c)) => {
|
||||
// Note. The modifier can also be a combination of modifiers, for
|
||||
@ -92,19 +97,25 @@ impl EditMode for Vi {
|
||||
// Mixed modifiers are used by non american keyboards that have extra
|
||||
// keys like 'alt gr'. Keep this in mind if in the future there are
|
||||
// cases where an event is not being captured
|
||||
let char = if let KeyModifiers::SHIFT = modifier {
|
||||
c.to_ascii_uppercase()
|
||||
if modifier == KeyModifiers::SHIFT {
|
||||
let char = c.to_ascii_uppercase();
|
||||
ReedlineEvent::Edit(vec![EditCommand::InsertChar(char)])
|
||||
} else if modifier == KeyModifiers::NONE
|
||||
|| modifier == KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||
|| modifier
|
||||
== KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT
|
||||
{
|
||||
ReedlineEvent::Edit(vec![EditCommand::InsertChar(c)])
|
||||
} else {
|
||||
c
|
||||
};
|
||||
|
||||
ReedlineEvent::Edit(vec![EditCommand::InsertChar(char)])
|
||||
self.insert_keybindings
|
||||
.find_binding(modifier, code)
|
||||
.unwrap_or(ReedlineEvent::None)
|
||||
}
|
||||
}
|
||||
(_, KeyModifiers::NONE, KeyCode::Tab) => ReedlineEvent::HandleTab,
|
||||
(_, KeyModifiers::NONE, KeyCode::Esc) => {
|
||||
self.cache.clear();
|
||||
self.mode = Mode::Normal;
|
||||
ReedlineEvent::Repaint
|
||||
ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint])
|
||||
}
|
||||
(_, KeyModifiers::NONE, KeyCode::Enter) => {
|
||||
self.mode = Mode::Insert;
|
||||
|
35
src/edit_mode/vi/vi_keybindings.rs
Normal file
35
src/edit_mode/vi/vi_keybindings.rs
Normal file
@ -0,0 +1,35 @@
|
||||
use crate::{
|
||||
edit_mode::{keybindings::edit_bind, Keybindings},
|
||||
ReedlineEvent,
|
||||
};
|
||||
|
||||
use {
|
||||
crate::EditCommand as EC,
|
||||
crossterm::event::{KeyCode as KC, KeyModifiers as KM},
|
||||
};
|
||||
|
||||
pub fn default_vi_normal_keybindings() -> Keybindings {
|
||||
let mut kb = Keybindings::new();
|
||||
|
||||
kb.add_binding(KM::CONTROL, KC::Char('c'), ReedlineEvent::CtrlC);
|
||||
|
||||
kb
|
||||
}
|
||||
|
||||
pub fn default_vi_insert_keybindings() -> Keybindings {
|
||||
let mut kb = Keybindings::new();
|
||||
|
||||
kb.add_binding(KM::NONE, KC::Up, ReedlineEvent::Up);
|
||||
kb.add_binding(KM::NONE, KC::Down, ReedlineEvent::Down);
|
||||
kb.add_binding(KM::NONE, KC::Left, edit_bind(EC::MoveLeft));
|
||||
kb.add_binding(KM::NONE, KC::Right, edit_bind(EC::MoveRight));
|
||||
kb.add_binding(KM::NONE, KC::Backspace, edit_bind(EC::Backspace));
|
||||
kb.add_binding(KM::NONE, KC::Delete, edit_bind(EC::Delete));
|
||||
kb.add_binding(KM::NONE, KC::End, edit_bind(EC::MoveToLineEnd));
|
||||
kb.add_binding(KM::NONE, KC::Home, edit_bind(EC::MoveToLineStart));
|
||||
kb.add_binding(KM::CONTROL, KC::Char('c'), ReedlineEvent::CtrlC);
|
||||
kb.add_binding(KM::NONE, KC::Tab, ReedlineEvent::Complete);
|
||||
kb.add_binding(KM::ALT, KC::Char('1'), ReedlineEvent::ContextMenu);
|
||||
|
||||
kb
|
||||
}
|
122
src/engine.rs
122
src/engine.rs
@ -1,6 +1,7 @@
|
||||
use {
|
||||
crate::{
|
||||
completion::{CircularCompletionHandler, CompletionActionHandler},
|
||||
context_menu::ContextMenu,
|
||||
core_editor::Editor,
|
||||
edit_mode::{EditMode, Emacs},
|
||||
enums::{ReedlineEvent, UndoBehavior},
|
||||
@ -97,6 +98,9 @@ pub struct Reedline {
|
||||
|
||||
// Use ansi coloring or not
|
||||
use_ansi_coloring: bool,
|
||||
|
||||
// Context Menu
|
||||
context_menu: ContextMenu,
|
||||
}
|
||||
|
||||
impl Drop for Reedline {
|
||||
@ -115,6 +119,7 @@ impl Reedline {
|
||||
let buffer_highlighter = Box::new(ExampleHighlighter::default());
|
||||
let hinter = Box::new(DefaultHinter::default());
|
||||
let validator = Box::new(DefaultValidator);
|
||||
let context_menu = ContextMenu::default();
|
||||
|
||||
let edit_mode = Box::new(Emacs::default());
|
||||
|
||||
@ -130,6 +135,7 @@ impl Reedline {
|
||||
validator,
|
||||
animate: true,
|
||||
use_ansi_coloring: true,
|
||||
context_menu,
|
||||
};
|
||||
|
||||
Ok(reedline)
|
||||
@ -456,7 +462,7 @@ impl Reedline {
|
||||
Ok(Some(Signal::CtrlC))
|
||||
}
|
||||
ReedlineEvent::ClearScreen => Ok(Some(Signal::CtrlL)),
|
||||
ReedlineEvent::Enter | ReedlineEvent::HandleTab => {
|
||||
ReedlineEvent::Enter | ReedlineEvent::Complete => {
|
||||
if let Some(string) = self.history.string_at_cursor() {
|
||||
self.editor.set_buffer(string);
|
||||
self.editor.remember_undo_state(true);
|
||||
@ -509,6 +515,10 @@ impl Reedline {
|
||||
// Default no operation
|
||||
Ok(None)
|
||||
}
|
||||
ReedlineEvent::ContextMenu | ReedlineEvent::Esc => {
|
||||
// No context menu action when pressing Tab in history mode
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -518,7 +528,13 @@ impl Reedline {
|
||||
event: ReedlineEvent,
|
||||
) -> io::Result<Option<Signal>> {
|
||||
match event {
|
||||
ReedlineEvent::HandleTab => {
|
||||
ReedlineEvent::ContextMenu => {
|
||||
self.context_menu.activate();
|
||||
|
||||
self.buffer_paint(prompt)?;
|
||||
Ok(None)
|
||||
}
|
||||
ReedlineEvent::Complete => {
|
||||
let line_buffer = self.editor.line_buffer();
|
||||
|
||||
self.tab_handler.handle(line_buffer);
|
||||
@ -526,6 +542,11 @@ impl Reedline {
|
||||
self.buffer_paint(prompt)?;
|
||||
Ok(None)
|
||||
}
|
||||
ReedlineEvent::Esc => {
|
||||
self.context_menu.deactivate();
|
||||
self.buffer_paint(prompt)?;
|
||||
Ok(None)
|
||||
}
|
||||
ReedlineEvent::CtrlD => {
|
||||
if self.editor.is_empty() {
|
||||
self.editor.reset_undo_stack();
|
||||
@ -542,23 +563,36 @@ impl Reedline {
|
||||
}
|
||||
ReedlineEvent::ClearScreen => Ok(Some(Signal::CtrlL)),
|
||||
ReedlineEvent::Enter => {
|
||||
let buffer = self.editor.get_buffer().to_string();
|
||||
if matches!(self.validator.validate(&buffer), ValidationResult::Complete) {
|
||||
self.append_to_history();
|
||||
self.run_edit_commands(&[EditCommand::Clear])?;
|
||||
self.painter.print_crlf()?;
|
||||
self.editor.reset_undo_stack();
|
||||
|
||||
Ok(Some(Signal::Success(buffer)))
|
||||
} else {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
self.run_edit_commands(&[EditCommand::InsertChar('\r')])?;
|
||||
if self.context_menu.is_active() {
|
||||
let value = self.context_menu.get_value();
|
||||
if let Some(value) = value {
|
||||
let value = value.to_string();
|
||||
self.run_edit_commands(&[EditCommand::InsertString(value)])?;
|
||||
}
|
||||
self.run_edit_commands(&[EditCommand::InsertChar('\n')])?;
|
||||
|
||||
self.context_menu.deactivate();
|
||||
self.buffer_paint(prompt)?;
|
||||
|
||||
Ok(None)
|
||||
} else {
|
||||
let buffer = self.editor.get_buffer().to_string();
|
||||
if matches!(self.validator.validate(&buffer), ValidationResult::Complete) {
|
||||
self.append_to_history();
|
||||
self.run_edit_commands(&[EditCommand::Clear])?;
|
||||
self.painter.print_crlf()?;
|
||||
self.editor.reset_undo_stack();
|
||||
|
||||
Ok(Some(Signal::Success(buffer)))
|
||||
} else {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
self.run_edit_commands(&[EditCommand::InsertChar('\r')])?;
|
||||
}
|
||||
self.run_edit_commands(&[EditCommand::InsertChar('\n')])?;
|
||||
self.buffer_paint(prompt)?;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
ReedlineEvent::Edit(commands) => {
|
||||
@ -590,13 +624,21 @@ impl Reedline {
|
||||
Ok(None)
|
||||
}
|
||||
ReedlineEvent::Up => {
|
||||
self.up_command();
|
||||
if self.context_menu.is_active() {
|
||||
self.context_menu.move_up()
|
||||
} else {
|
||||
self.up_command();
|
||||
}
|
||||
|
||||
self.buffer_paint(prompt)?;
|
||||
Ok(None)
|
||||
}
|
||||
ReedlineEvent::Down => {
|
||||
self.down_command();
|
||||
if self.context_menu.is_active() {
|
||||
self.context_menu.move_down()
|
||||
} else {
|
||||
self.down_command();
|
||||
}
|
||||
|
||||
self.buffer_paint(prompt)?;
|
||||
Ok(None)
|
||||
@ -803,8 +845,20 @@ impl Reedline {
|
||||
EditCommand::MoveToEnd => self.editor.move_to_end(),
|
||||
EditCommand::MoveToLineStart => self.editor.move_to_line_start(),
|
||||
EditCommand::MoveToLineEnd => self.editor.move_to_line_end(),
|
||||
EditCommand::MoveLeft => self.editor.move_left(),
|
||||
EditCommand::MoveRight => self.editor.move_right(),
|
||||
EditCommand::MoveLeft => {
|
||||
if self.context_menu.is_active() {
|
||||
self.context_menu.move_left()
|
||||
} else {
|
||||
self.editor.move_left()
|
||||
}
|
||||
}
|
||||
EditCommand::MoveRight => {
|
||||
if self.context_menu.is_active() {
|
||||
self.context_menu.move_right()
|
||||
} else {
|
||||
self.editor.move_right()
|
||||
}
|
||||
}
|
||||
EditCommand::MoveWordLeft => self.editor.move_word_left(),
|
||||
EditCommand::MoveWordRight => self.editor.move_word_right(),
|
||||
|
||||
@ -927,13 +981,17 @@ impl Reedline {
|
||||
res_string
|
||||
};
|
||||
|
||||
self.painter.repaint_buffer(
|
||||
let lines = PromptLines::new(
|
||||
prompt,
|
||||
self.prompt_edit_mode(),
|
||||
PromptLines::new(&res_string, "", ""),
|
||||
Some(prompt_history_search),
|
||||
self.use_ansi_coloring,
|
||||
)?;
|
||||
&res_string,
|
||||
"",
|
||||
"",
|
||||
);
|
||||
|
||||
self.painter
|
||||
.repaint_buffer(prompt, lines, None, self.use_ansi_coloring)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -972,13 +1030,23 @@ impl Reedline {
|
||||
let after_cursor = after_cursor.replace("\n", "\r\n");
|
||||
let hint = hint.replace("\n", "\r\n");
|
||||
|
||||
self.painter.repaint_buffer(
|
||||
let context_menu = if self.context_menu.is_active() {
|
||||
Some(&self.context_menu)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let lines = PromptLines::new(
|
||||
prompt,
|
||||
self.prompt_edit_mode(),
|
||||
PromptLines::new(&before_cursor, &after_cursor, &hint),
|
||||
None,
|
||||
self.use_ansi_coloring,
|
||||
)
|
||||
&before_cursor,
|
||||
&after_cursor,
|
||||
&hint,
|
||||
);
|
||||
|
||||
self.painter
|
||||
.repaint_buffer(prompt, lines, context_menu, self.use_ansi_coloring)
|
||||
}
|
||||
}
|
||||
|
||||
|
10
src/enums.rs
10
src/enums.rs
@ -213,8 +213,11 @@ pub enum ReedlineEvent {
|
||||
/// No op event
|
||||
None,
|
||||
|
||||
/// Trigger Tab
|
||||
HandleTab,
|
||||
/// Trigger context menu
|
||||
ContextMenu,
|
||||
|
||||
/// Completes hint
|
||||
Complete,
|
||||
|
||||
/// Handle EndOfLine event
|
||||
///
|
||||
@ -240,6 +243,9 @@ pub enum ReedlineEvent {
|
||||
/// Handle enter event
|
||||
Enter,
|
||||
|
||||
/// Esc event
|
||||
Esc,
|
||||
|
||||
/// Mouse
|
||||
Mouse, // Fill in details later
|
||||
|
||||
|
@ -228,3 +228,6 @@ pub use hinter::{DefaultHinter, Hinter};
|
||||
|
||||
mod validator;
|
||||
pub use validator::{DefaultValidator, ValidationResult, Validator};
|
||||
|
||||
mod context_menu;
|
||||
pub use context_menu::{ContextMenu, MenuFiller};
|
||||
|
330
src/painter.rs
330
src/painter.rs
@ -1,5 +1,8 @@
|
||||
use crate::PromptHistorySearch;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{context_menu::ContextMenu, PromptHistorySearch};
|
||||
use crossterm::{cursor::MoveToRow, style::ResetColor, terminal::ScrollUp};
|
||||
use nu_ansi_term::ansi::RESET;
|
||||
|
||||
use {
|
||||
crate::{prompt::PromptEditMode, Prompt},
|
||||
@ -25,6 +28,9 @@ impl PromptCoordinates {
|
||||
}
|
||||
|
||||
pub struct PromptLines<'prompt> {
|
||||
prompt_str_left: Cow<'prompt, str>,
|
||||
prompt_str_right: Cow<'prompt, str>,
|
||||
prompt_indicator: Cow<'prompt, str>,
|
||||
before_cursor: &'prompt str,
|
||||
after_cursor: &'prompt str,
|
||||
hint: &'prompt str,
|
||||
@ -35,11 +41,25 @@ impl<'prompt> PromptLines<'prompt> {
|
||||
/// This vector with the str are used to calculate how many lines are
|
||||
/// required to print after the prompt
|
||||
pub fn new(
|
||||
prompt: &'prompt dyn Prompt,
|
||||
prompt_mode: PromptEditMode,
|
||||
history_indicator: Option<PromptHistorySearch>,
|
||||
before_cursor: &'prompt str,
|
||||
after_cursor: &'prompt str,
|
||||
hint: &'prompt str,
|
||||
) -> Self {
|
||||
let prompt_str_left = prompt.render_prompt_left();
|
||||
let prompt_str_right = prompt.render_prompt_right();
|
||||
|
||||
let prompt_indicator = match history_indicator {
|
||||
Some(prompt_search) => prompt.render_prompt_history_search_indicator(prompt_search),
|
||||
None => prompt.render_prompt_indicator(prompt_mode),
|
||||
};
|
||||
|
||||
Self {
|
||||
prompt_str_left,
|
||||
prompt_str_right,
|
||||
prompt_indicator,
|
||||
before_cursor,
|
||||
after_cursor,
|
||||
hint,
|
||||
@ -49,17 +69,16 @@ impl<'prompt> PromptLines<'prompt> {
|
||||
/// The required lines to paint the buffer are calculated by counting the
|
||||
/// number of newlines in all the strings that form the prompt and buffer.
|
||||
/// The plus 1 is to indicate that there should be at least one line.
|
||||
fn required_lines(
|
||||
&self,
|
||||
prompt_str: &str,
|
||||
prompt_indicator: &str,
|
||||
terminal_columns: u16,
|
||||
) -> u16 {
|
||||
let input = prompt_str.to_string()
|
||||
+ prompt_indicator
|
||||
+ self.before_cursor
|
||||
+ self.hint
|
||||
+ self.after_cursor;
|
||||
fn required_lines(&self, terminal_columns: u16, context_menu: Option<&ContextMenu>) -> u16 {
|
||||
let input = if context_menu.is_none() {
|
||||
self.prompt_str_left.to_string()
|
||||
+ &self.prompt_indicator
|
||||
+ self.before_cursor
|
||||
+ self.hint
|
||||
+ self.after_cursor
|
||||
} else {
|
||||
self.prompt_str_left.to_string() + &self.prompt_indicator + self.before_cursor
|
||||
};
|
||||
|
||||
let lines = input.lines().fold(0, |acc, line| {
|
||||
let wrap = if let Ok(line) = strip_ansi_escapes::strip(line) {
|
||||
@ -71,16 +90,17 @@ impl<'prompt> PromptLines<'prompt> {
|
||||
acc + 1 + wrap
|
||||
});
|
||||
|
||||
lines as u16
|
||||
if let Some(context_menu) = context_menu {
|
||||
lines as u16 + context_menu.get_rows()
|
||||
} else {
|
||||
lines as u16
|
||||
}
|
||||
}
|
||||
|
||||
fn distance_from_prompt(
|
||||
&self,
|
||||
prompt_str: &str,
|
||||
prompt_indicator: &str,
|
||||
terminal_columns: u16,
|
||||
) -> u16 {
|
||||
let input = prompt_str.to_string() + prompt_indicator + self.before_cursor;
|
||||
/// Estimated distance of the cursor to the prompt.
|
||||
/// This considers line wrapping
|
||||
fn distance_from_prompt(&self, terminal_columns: u16) -> u16 {
|
||||
let input = self.prompt_str_left.to_string() + &self.prompt_indicator + self.before_cursor;
|
||||
|
||||
let lines = input.lines().fold(0, |acc, line| {
|
||||
let wrap = if let Ok(line) = strip_ansi_escapes::strip(line) {
|
||||
@ -98,6 +118,40 @@ impl<'prompt> PromptLines<'prompt> {
|
||||
fn concatenate_lines(&self) -> String {
|
||||
self.before_cursor.to_string() + self.after_cursor + self.hint
|
||||
}
|
||||
|
||||
/// Total lines that the prompt uses considering that it may wrap the screen
|
||||
fn prompt_lines_with_wrap(&self, screen_width: u16) -> u16 {
|
||||
let complete_prompt = self.prompt_str_left.to_string() + &self.prompt_indicator;
|
||||
let prompt_wrap = estimated_wrapped_line_count(&complete_prompt, screen_width);
|
||||
|
||||
(self.prompt_str_left.matches('\n').count() + prompt_wrap) as u16
|
||||
}
|
||||
|
||||
/// Estimated width of the actual input
|
||||
fn estimate_first_input_line_width(&self) -> u16 {
|
||||
let last_line_left_prompt = self.prompt_str_left.lines().last();
|
||||
|
||||
let prompt_lines_total = self.concatenate_lines();
|
||||
let prompt_lines_first = prompt_lines_total.lines().next();
|
||||
|
||||
let mut estimate = 0; // space in front of the input
|
||||
|
||||
if let Some(last_line_left_prompt) = last_line_left_prompt {
|
||||
estimate += line_width(last_line_left_prompt);
|
||||
}
|
||||
|
||||
estimate += line_width(&self.prompt_indicator);
|
||||
|
||||
if let Some(prompt_lines_first) = prompt_lines_first {
|
||||
estimate += line_width(prompt_lines_first);
|
||||
}
|
||||
|
||||
if estimate > u16::MAX as usize {
|
||||
u16::MAX
|
||||
} else {
|
||||
estimate as u16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn estimated_wrapped_line_count(line: &str, terminal_columns: u16) -> usize {
|
||||
@ -148,17 +202,7 @@ fn skip_buffer_lines(string: &str, skip: usize, offset: Option<usize>) -> &str {
|
||||
None => string.len(),
|
||||
};
|
||||
|
||||
let line = &string[index..limit];
|
||||
|
||||
line.trim_end_matches('\n')
|
||||
}
|
||||
|
||||
/// Total lines that the prompt uses considering that it may wrap the screen
|
||||
fn prompt_lines_with_wrap(prompt_str: &str, prompt_indicator: &str, screen_width: u16) -> u16 {
|
||||
let complete_prompt = prompt_str.to_string() + prompt_indicator;
|
||||
let prompt_wrap = estimated_wrapped_line_count(&complete_prompt, screen_width);
|
||||
|
||||
(prompt_str.matches('\n').count() + prompt_wrap) as u16
|
||||
string[index..limit].trim_end_matches('\n')
|
||||
}
|
||||
|
||||
pub struct Painter {
|
||||
@ -248,27 +292,17 @@ impl Painter {
|
||||
pub fn repaint_buffer(
|
||||
&mut self,
|
||||
prompt: &dyn Prompt,
|
||||
prompt_mode: PromptEditMode,
|
||||
lines: PromptLines,
|
||||
history_indicator: Option<PromptHistorySearch>,
|
||||
context_menu: Option<&ContextMenu>,
|
||||
use_ansi_coloring: bool,
|
||||
) -> Result<()> {
|
||||
self.stdout.queue(cursor::Hide)?;
|
||||
|
||||
// String representation of the prompt
|
||||
let (screen_width, screen_height) = self.terminal_size;
|
||||
let prompt_str_left = prompt.render_prompt_left();
|
||||
let prompt_str_right = prompt.render_prompt_right();
|
||||
|
||||
// The prompt indicator could be normal one or the history indicator
|
||||
let prompt_indicator = match history_indicator {
|
||||
Some(prompt_search) => prompt.render_prompt_history_search_indicator(prompt_search),
|
||||
None => prompt.render_prompt_indicator(prompt_mode),
|
||||
};
|
||||
|
||||
// Lines and distance parameters
|
||||
let required_lines =
|
||||
lines.required_lines(&prompt_str_left, &prompt_indicator, screen_width);
|
||||
let required_lines = lines.required_lines(screen_width, context_menu);
|
||||
let remaining_lines = self.remaining_lines();
|
||||
|
||||
// Marking the painter state as larger buffer to avoid animations
|
||||
@ -291,19 +325,9 @@ impl Painter {
|
||||
.queue(Clear(ClearType::FromCursorDown))?;
|
||||
|
||||
if self.large_buffer {
|
||||
self.print_large_buffer(
|
||||
prompt,
|
||||
(&prompt_str_left, &prompt_str_right, &prompt_indicator),
|
||||
&lines,
|
||||
use_ansi_coloring,
|
||||
)?
|
||||
self.print_large_buffer(prompt, &lines, context_menu, use_ansi_coloring)?
|
||||
} else {
|
||||
self.print_small_buffer(
|
||||
prompt,
|
||||
(&prompt_str_left, &prompt_str_right, &prompt_indicator),
|
||||
&lines,
|
||||
use_ansi_coloring,
|
||||
)?
|
||||
self.print_small_buffer(prompt, &lines, context_menu, use_ansi_coloring)?
|
||||
}
|
||||
|
||||
// The last_required_lines is used to move the cursor at the end where stdout
|
||||
@ -312,12 +336,11 @@ impl Painter {
|
||||
|
||||
// In debug mode a string with position information is printed at the end of the buffer
|
||||
if self.debug_mode {
|
||||
let cursor_distance =
|
||||
lines.distance_from_prompt(&prompt_str_left, &prompt_indicator, screen_width);
|
||||
let prompt_lines =
|
||||
prompt_lines_with_wrap(&prompt_str_left, &prompt_indicator, screen_width);
|
||||
let prompt_length = prompt_str_left.len() + prompt_indicator.len();
|
||||
let estimated_prompt = estimated_wrapped_line_count(&prompt_str_left, screen_width);
|
||||
let cursor_distance = lines.distance_from_prompt(screen_width);
|
||||
let prompt_lines = lines.prompt_lines_with_wrap(screen_width);
|
||||
let prompt_length = lines.prompt_str_left.len() + lines.prompt_indicator.len();
|
||||
let estimated_prompt =
|
||||
estimated_wrapped_line_count(&lines.prompt_str_left, screen_width);
|
||||
|
||||
self.stdout
|
||||
.queue(Print(format!(" [h{}:", screen_height)))?
|
||||
@ -338,10 +361,11 @@ impl Painter {
|
||||
self.flush()
|
||||
}
|
||||
|
||||
fn print_right_prompt(&mut self, prompt_str_right: &str, input_width: u16) -> Result<()> {
|
||||
fn print_right_prompt(&mut self, lines: &PromptLines) -> Result<()> {
|
||||
let (screen_width, _) = self.terminal_size;
|
||||
let prompt_length_right = line_width(prompt_str_right);
|
||||
let prompt_length_right = line_width(&lines.prompt_str_right);
|
||||
let start_position = screen_width.saturating_sub(prompt_length_right as u16);
|
||||
let input_width = lines.estimate_first_input_line_width();
|
||||
|
||||
if input_width <= start_position {
|
||||
self.stdout
|
||||
@ -350,22 +374,82 @@ impl Painter {
|
||||
start_position,
|
||||
self.prompt_coords.prompt_start.1,
|
||||
))?
|
||||
.queue(Print(&prompt_str_right))?
|
||||
.queue(Print(&lines.prompt_str_right))?
|
||||
.queue(RestorePosition)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_context_menu(
|
||||
&mut self,
|
||||
context_menu: &ContextMenu,
|
||||
lines: &PromptLines,
|
||||
) -> Result<()> {
|
||||
let (screen_width, screen_height) = self.terminal_size;
|
||||
let cursor_distance = lines.distance_from_prompt(screen_width);
|
||||
|
||||
// If there is not enough space to print the menu, then the starting
|
||||
// drawing point for the menu will overwrite the last rows in the buffer
|
||||
let starting_row = if cursor_distance >= screen_height.saturating_sub(1) {
|
||||
screen_height.saturating_sub(context_menu.min_rows())
|
||||
} else {
|
||||
self.prompt_coords.prompt_start.1 + cursor_distance + 1
|
||||
};
|
||||
|
||||
// The skip values represent the number of lines that should be skipped
|
||||
// while printing the menu
|
||||
let remaining_lines = screen_height.saturating_sub(starting_row);
|
||||
let skip_values = if context_menu.row_pos >= remaining_lines {
|
||||
let skip_lines = context_menu.row_pos.saturating_sub(remaining_lines) + 1;
|
||||
(skip_lines * context_menu.cols) as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// It seems that crossterm prefers to have a complete string ready to be printed
|
||||
// rather than looping through the values and printing multiple things
|
||||
// This reduces the flickering when printing the menu
|
||||
let available_values = (remaining_lines * context_menu.cols) as usize;
|
||||
let values = context_menu
|
||||
.get_values()
|
||||
.iter()
|
||||
.skip(skip_values)
|
||||
.take(available_values)
|
||||
.enumerate()
|
||||
.map(|(index, line)| {
|
||||
// Correcting the enumerate index based on the number of skipped values
|
||||
let index = index + skip_values;
|
||||
let column = index as u16 % context_menu.cols;
|
||||
let printable_width = context_menu.printable_width(line);
|
||||
|
||||
// Final string with colors
|
||||
format!(
|
||||
"{}{:width$}{}{}",
|
||||
context_menu.text_style(index),
|
||||
&line[..printable_width],
|
||||
RESET,
|
||||
context_menu.end_of_line(column),
|
||||
width = context_menu.col_width
|
||||
)
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
self.stdout
|
||||
.queue(cursor::MoveTo(0, starting_row))?
|
||||
.queue(Clear(ClearType::FromCursorDown))?
|
||||
.queue(Print(values.trim_end_matches('\n')))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_small_buffer(
|
||||
&mut self,
|
||||
prompt: &dyn Prompt,
|
||||
prompt_str: (&str, &str, &str),
|
||||
lines: &PromptLines,
|
||||
context_menu: Option<&ContextMenu>,
|
||||
use_ansi_coloring: bool,
|
||||
) -> Result<()> {
|
||||
let (prompt_str_left, prompt_str_right, prompt_indicator) = prompt_str;
|
||||
|
||||
// print our prompt with color
|
||||
if use_ansi_coloring {
|
||||
self.stdout
|
||||
@ -373,13 +457,10 @@ impl Painter {
|
||||
}
|
||||
|
||||
self.stdout
|
||||
.queue(Print(&prompt_str_left))?
|
||||
.queue(Print(&prompt_indicator))?;
|
||||
.queue(Print(&lines.prompt_str_left))?
|
||||
.queue(Print(&lines.prompt_indicator))?;
|
||||
|
||||
let estimate_input_total_width =
|
||||
estimate_first_input_line_width(prompt_str_left, prompt_indicator, lines);
|
||||
|
||||
self.print_right_prompt(prompt_str_right, estimate_input_total_width)?;
|
||||
self.print_right_prompt(lines)?;
|
||||
|
||||
if use_ansi_coloring {
|
||||
self.stdout.queue(ResetColor)?;
|
||||
@ -387,9 +468,14 @@ impl Painter {
|
||||
|
||||
self.stdout
|
||||
.queue(Print(&lines.before_cursor))?
|
||||
.queue(SavePosition)?
|
||||
.queue(Print(&lines.hint))?
|
||||
.queue(Print(&lines.after_cursor))?;
|
||||
.queue(SavePosition)?;
|
||||
|
||||
if let Some(context_menu) = context_menu {
|
||||
self.print_context_menu(context_menu, lines)?;
|
||||
} else {
|
||||
self.stdout
|
||||
.queue(Print(format!("{}{}", &lines.hint, &lines.after_cursor)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -397,23 +483,20 @@ impl Painter {
|
||||
fn print_large_buffer(
|
||||
&mut self,
|
||||
prompt: &dyn Prompt,
|
||||
prompt_str: (&str, &str, &str),
|
||||
lines: &PromptLines,
|
||||
context_menu: Option<&ContextMenu>,
|
||||
use_ansi_coloring: bool,
|
||||
) -> Result<()> {
|
||||
let (prompt_str_left, prompt_str_right, prompt_indicator) = prompt_str;
|
||||
let (screen_width, screen_height) = self.terminal_size;
|
||||
let cursor_distance =
|
||||
lines.distance_from_prompt(prompt_str_left, prompt_indicator, screen_width);
|
||||
let cursor_distance = lines.distance_from_prompt(screen_width);
|
||||
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 prompt_lines =
|
||||
prompt_lines_with_wrap(prompt_str_left, prompt_indicator, screen_width) as usize;
|
||||
let prompt_lines = lines.prompt_lines_with_wrap(screen_width) as usize;
|
||||
|
||||
let prompt_indicator_lines = prompt_indicator.lines().count();
|
||||
let prompt_indicator_lines = 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;
|
||||
|
||||
@ -428,42 +511,60 @@ impl Painter {
|
||||
|
||||
// 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_left, extra_rows, None);
|
||||
let prompt_skipped = skip_buffer_lines(&lines.prompt_str_left, extra_rows, None);
|
||||
self.stdout.queue(Print(prompt_skipped))?;
|
||||
|
||||
let estimate_input_total_width =
|
||||
estimate_first_input_line_width(prompt_str_left, prompt_indicator, lines);
|
||||
|
||||
if extra_rows == 0 {
|
||||
self.print_right_prompt(prompt_str_right, estimate_input_total_width)?;
|
||||
self.print_right_prompt(lines)?;
|
||||
}
|
||||
|
||||
// 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);
|
||||
let indicator_skipped = skip_buffer_lines(&lines.prompt_indicator, extra_rows, None);
|
||||
self.stdout.queue(Print(indicator_skipped))?;
|
||||
|
||||
if use_ansi_coloring {
|
||||
self.stdout.queue(ResetColor)?;
|
||||
}
|
||||
|
||||
// The minimum number of lines from the menu are removed from the buffer if there is no more
|
||||
// space to print the menu. This will only happen if the cursor is at the last line and
|
||||
// it is a large buffer
|
||||
let offset = context_menu.and_then(|context_menu| {
|
||||
if cursor_distance >= screen_height.saturating_sub(1) {
|
||||
let rows = lines
|
||||
.before_cursor
|
||||
.lines()
|
||||
.count()
|
||||
.saturating_sub(extra_rows)
|
||||
.saturating_sub(context_menu.min_rows() as usize);
|
||||
Some(rows)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// Selecting the lines before the cursor that will be printed
|
||||
let before_cursor_skipped = skip_buffer_lines(lines.before_cursor, extra_rows, None);
|
||||
let before_cursor_skipped = skip_buffer_lines(lines.before_cursor, extra_rows, offset);
|
||||
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))?;
|
||||
if let Some(context_menu) = context_menu {
|
||||
self.print_context_menu(context_menu, lines)?;
|
||||
} else {
|
||||
// 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))?;
|
||||
// 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(())
|
||||
}
|
||||
@ -545,35 +646,6 @@ impl Painter {
|
||||
}
|
||||
}
|
||||
|
||||
fn estimate_first_input_line_width(
|
||||
left_prompt: &str,
|
||||
indicator: &str,
|
||||
prompt_lines: &PromptLines,
|
||||
) -> u16 {
|
||||
let last_line_left_prompt = left_prompt.lines().last();
|
||||
|
||||
let prompt_lines_total = prompt_lines.concatenate_lines();
|
||||
let prompt_lines_first = prompt_lines_total.lines().next();
|
||||
|
||||
let mut estimate = 0; // space in front of the input
|
||||
|
||||
if let Some(last_line_left_prompt) = last_line_left_prompt {
|
||||
estimate += line_width(last_line_left_prompt);
|
||||
}
|
||||
|
||||
estimate += line_width(indicator);
|
||||
|
||||
if let Some(prompt_lines_first) = prompt_lines_first {
|
||||
estimate += line_width(prompt_lines_first);
|
||||
}
|
||||
|
||||
if estimate > u16::MAX as usize {
|
||||
u16::MAX
|
||||
} else {
|
||||
estimate as u16
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
Loading…
Reference in New Issue
Block a user