mirror of
https://github.com/nushell/reedline.git
synced 2024-09-11 15:55:50 +03:00
Descriptions for completion suggestions (#358)
* add description to completion suggestions * completion menu with help * corrected doc tests * moved menu functions to module * menu type with its own completer * corrected doc tests * function for menus to quick complete
This commit is contained in:
parent
bc528de132
commit
69fad67b90
@ -30,5 +30,16 @@ impl Span {
|
||||
pub trait Completer: Send {
|
||||
/// the action that will take the line and position and convert it to a vector of completions, which include the
|
||||
/// span to replace and the contents of that replacement
|
||||
fn complete(&self, line: &str, pos: usize) -> Vec<(Span, String)>;
|
||||
fn complete(&self, line: &str, pos: usize) -> Vec<Suggestion>;
|
||||
}
|
||||
|
||||
/// Suggestion returned by the Completer
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Suggestion {
|
||||
/// String replacement that will be introduced to the the buffer
|
||||
pub value: String,
|
||||
/// Optional description for the replacement
|
||||
pub description: Option<String>,
|
||||
/// Replacement span in the buffer
|
||||
pub span: Span,
|
||||
}
|
||||
|
@ -52,13 +52,13 @@ impl CircularCompletionHandler {
|
||||
match self.index {
|
||||
index if index < completions.len() => {
|
||||
self.index += 1;
|
||||
let span = completions[index].0;
|
||||
let span = completions[index].span;
|
||||
|
||||
let mut offset = present_buffer.insertion_point();
|
||||
offset += completions[index].1.len() - (span.end - span.start);
|
||||
offset += completions[index].value.len() - (span.end - span.start);
|
||||
|
||||
// TODO improve the support for multiline replace
|
||||
present_buffer.replace(span.start..span.end, &completions[index].1);
|
||||
present_buffer.replace(span.start..span.end, &completions[index].value);
|
||||
present_buffer.set_insertion_point(offset);
|
||||
}
|
||||
_ => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::{Completer, Span};
|
||||
use crate::{Completer, Span, Suggestion};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
str::Chars,
|
||||
@ -48,27 +48,27 @@ impl Completer for DefaultCompleter {
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use reedline::{DefaultCompleter,Completer,Span};
|
||||
/// use reedline::{DefaultCompleter,Completer,Span,Suggestion};
|
||||
///
|
||||
/// let mut completions = DefaultCompleter::default();
|
||||
/// completions.insert(vec!["batman","robin","batmobile","batcave","robber"].iter().map(|s| s.to_string()).collect());
|
||||
/// assert_eq!(
|
||||
/// completions.complete("bat",3),
|
||||
/// vec![
|
||||
/// (Span { start: 0, end: 3 }, "batcave".into()),
|
||||
/// (Span { start: 0, end: 3 }, "batman".into()),
|
||||
/// (Span { start: 0, end: 3 }, "batmobile".into()),
|
||||
/// Suggestion {value: "batcave".into(), description: None, span: Span { start: 0, end: 3 }},
|
||||
/// Suggestion {value: "batman".into(), description: None, span: Span { start: 0, end: 3 }},
|
||||
/// Suggestion {value: "batmobile".into(), description: None, span: Span { start: 0, end: 3 }},
|
||||
/// ]);
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// completions.complete("to the bat",10),
|
||||
/// vec![
|
||||
/// (Span { start: 7, end: 10 }, "batcave".into()),
|
||||
/// (Span { start: 7, end: 10 }, "batman".into()),
|
||||
/// (Span { start: 7, end: 10 }, "batmobile".into()),
|
||||
/// Suggestion {value: "batcave".into(), description: None, span: Span { start: 7, end: 10 }},
|
||||
/// Suggestion {value: "batman".into(), description: None, span: Span { start: 7, end: 10 }},
|
||||
/// Suggestion {value: "batmobile".into(), description: None, span: Span { start: 7, end: 10 }},
|
||||
/// ]);
|
||||
/// ```
|
||||
fn complete(&self, line: &str, pos: usize) -> Vec<(Span, String)> {
|
||||
fn complete(&self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||
let mut span_line_whitespaces = 0;
|
||||
let mut completions = vec![];
|
||||
if !line.is_empty() {
|
||||
@ -91,16 +91,19 @@ impl Completer for DefaultCompleter {
|
||||
extensions
|
||||
.iter()
|
||||
.map(|ext| {
|
||||
(
|
||||
Span::new(
|
||||
pos - span_line.len() - span_line_whitespaces,
|
||||
pos,
|
||||
),
|
||||
format!("{}{}", span_line, ext),
|
||||
)
|
||||
let span = Span::new(
|
||||
pos - span_line.len() - span_line_whitespaces,
|
||||
pos,
|
||||
);
|
||||
|
||||
Suggestion {
|
||||
value: format!("{}{}", span_line, ext),
|
||||
description: None,
|
||||
span,
|
||||
}
|
||||
})
|
||||
.filter(|t| t.1.len() > (t.0.end - t.0.start))
|
||||
.collect::<Vec<(Span, String)>>(),
|
||||
.filter(|t| t.value.len() > (t.span.end - t.span.start))
|
||||
.collect::<Vec<Suggestion>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -162,21 +165,21 @@ impl DefaultCompleter {
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use reedline::{DefaultCompleter,Completer,Span};
|
||||
/// use reedline::{DefaultCompleter,Completer,Span,Suggestion};
|
||||
///
|
||||
/// let mut completions = DefaultCompleter::default();
|
||||
/// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
|
||||
/// assert_eq!(
|
||||
/// completions.complete("te",2),
|
||||
/// vec![(Span { start: 0, end: 2 }, "test".into())]);
|
||||
/// vec![Suggestion {value: "test".into(), description: None, span: Span { start: 0, end: 2 }}]);
|
||||
///
|
||||
/// let mut completions = DefaultCompleter::with_inclusions(&['-', '_']);
|
||||
/// completions.insert(vec!["test-hyphen","test_underscore"].iter().map(|s| s.to_string()).collect());
|
||||
/// assert_eq!(
|
||||
/// completions.complete("te",2),
|
||||
/// vec![
|
||||
/// (Span { start: 0, end: 2 }, "test-hyphen".into()),
|
||||
/// (Span { start: 0, end: 2 }, "test_underscore".into()),
|
||||
/// Suggestion {value: "test-hyphen".into(), description: None, span: Span { start: 0, end: 2 }},
|
||||
/// Suggestion {value: "test_underscore".into(), description: None, span: Span { start: 0, end: 2 }},
|
||||
/// ]);
|
||||
/// ```
|
||||
pub fn with_inclusions(incl: &[char]) -> Self {
|
||||
|
@ -2,6 +2,6 @@ mod base;
|
||||
mod circular;
|
||||
mod default;
|
||||
|
||||
pub use base::{Completer, Span};
|
||||
pub use base::{Completer, Span, Suggestion};
|
||||
pub use circular::CircularCompletionHandler;
|
||||
pub use default::DefaultCompleter;
|
||||
|
@ -1,5 +1,3 @@
|
||||
use crate::{menu::IndexDirection, utils::text_manipulation};
|
||||
|
||||
use {
|
||||
crate::{
|
||||
completion::{CircularCompletionHandler, Completer, DefaultCompleter},
|
||||
@ -9,9 +7,13 @@ use {
|
||||
highlighter::SimpleMatchHighlighter,
|
||||
hinter::{DefaultHinter, Hinter},
|
||||
history::{FileBackedHistory, History, HistoryNavigationQuery},
|
||||
menu::{parse_selection_char, Menu, MenuEvent},
|
||||
menu::{
|
||||
menu_functions::{parse_selection_char, IndexDirection},
|
||||
Menu, MenuEvent, MenuType,
|
||||
},
|
||||
painting::{Painter, PromptLines},
|
||||
prompt::{PromptEditMode, PromptHistorySearchStatus},
|
||||
utils::text_manipulation,
|
||||
DefaultValidator, EditCommand, ExampleHighlighter, Highlighter, Prompt,
|
||||
PromptHistorySearch, Signal, ValidationResult, Validator,
|
||||
},
|
||||
@ -112,7 +114,7 @@ pub struct Reedline {
|
||||
use_ansi_coloring: bool,
|
||||
|
||||
// Engine Menus
|
||||
menus: Vec<Box<dyn Menu>>,
|
||||
menus: Vec<MenuType>,
|
||||
}
|
||||
|
||||
impl Drop for Reedline {
|
||||
@ -311,8 +313,13 @@ impl Reedline {
|
||||
|
||||
/// A builder that appends a menu to the engine
|
||||
#[must_use]
|
||||
pub fn with_menu(mut self, menu: Box<dyn Menu>) -> Self {
|
||||
self.menus.push(menu);
|
||||
pub fn with_menu(mut self, menu: Box<dyn Menu>, completer: Option<Box<dyn Completer>>) -> Self {
|
||||
if let Some(completer) = completer {
|
||||
self.menus.push(MenuType::WithCompleter { menu, completer })
|
||||
} else {
|
||||
self.menus.push(MenuType::EngineCompleter(menu))
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
@ -573,7 +580,7 @@ impl Reedline {
|
||||
if let Some(menu) = self.menus.iter_mut().find(|menu| menu.name() == name) {
|
||||
menu.menu_event(MenuEvent::Activate(self.quick_completions));
|
||||
|
||||
if self.quick_completions {
|
||||
if self.quick_completions && menu.can_quick_complete() {
|
||||
menu.update_values(
|
||||
self.editor.line_buffer(),
|
||||
self.history.as_ref(),
|
||||
@ -759,7 +766,7 @@ impl Reedline {
|
||||
ReedlineEvent::Edit(commands) => {
|
||||
self.run_edit_commands(&commands);
|
||||
if let Some(menu) = self.menus.iter_mut().find(|men| men.is_active()) {
|
||||
if self.quick_completions {
|
||||
if self.quick_completions && menu.can_quick_complete() {
|
||||
menu.menu_event(MenuEvent::Edit(self.quick_completions));
|
||||
menu.update_values(
|
||||
self.editor.line_buffer(),
|
||||
@ -860,8 +867,8 @@ impl Reedline {
|
||||
}
|
||||
}
|
||||
|
||||
fn active_menu(&mut self) -> Option<&mut Box<dyn Menu>> {
|
||||
self.menus.iter_mut().find(|men| men.is_active())
|
||||
fn active_menu(&mut self) -> Option<&mut MenuType> {
|
||||
self.menus.iter_mut().find(|menu| menu.is_active())
|
||||
}
|
||||
|
||||
fn previous_history(&mut self) {
|
||||
@ -1176,11 +1183,7 @@ impl Reedline {
|
||||
}
|
||||
}
|
||||
|
||||
let menu = self
|
||||
.menus
|
||||
.iter()
|
||||
.find(|menu| menu.is_active())
|
||||
.map(|menu| menu.as_ref());
|
||||
let menu = self.menus.iter().find(|menu| menu.is_active());
|
||||
|
||||
self.painter
|
||||
.repaint_buffer(prompt, &lines, menu, self.use_ansi_coloring)
|
||||
|
@ -121,7 +121,7 @@
|
||||
//! // Use the interactive menu to select options from the completer
|
||||
//! let completion_menu = Box::new(CompletionMenu::default());
|
||||
//!
|
||||
//! let mut line_editor = Reedline::create()?.with_completer(completer).with_menu(completion_menu);
|
||||
//! let mut line_editor = Reedline::create()?.with_completer(completer).with_menu(completion_menu, None);
|
||||
//! # Ok::<(), io::Error>(())
|
||||
//! ```
|
||||
//!
|
||||
@ -222,7 +222,7 @@ mod highlighter;
|
||||
pub use highlighter::{ExampleHighlighter, Highlighter, SimpleMatchHighlighter};
|
||||
|
||||
mod completion;
|
||||
pub use completion::{Completer, DefaultCompleter, Span};
|
||||
pub use completion::{Completer, DefaultCompleter, Span, Suggestion};
|
||||
|
||||
mod hinter;
|
||||
pub use hinter::{DefaultHinter, Hinter};
|
||||
@ -231,7 +231,7 @@ mod validator;
|
||||
pub use validator::{DefaultValidator, ValidationResult, Validator};
|
||||
|
||||
mod menu;
|
||||
pub use menu::{CompletionMenu, HistoryMenu, Menu, MenuEvent};
|
||||
pub use menu::{menu_functions, CompletionMenu, HistoryMenu, Menu, MenuEvent, MenuTextStyle};
|
||||
|
||||
mod utils;
|
||||
pub use utils::{
|
||||
|
@ -80,8 +80,8 @@ fn main() -> Result<()> {
|
||||
let completion_menu = Box::new(CompletionMenu::default());
|
||||
let history_menu = Box::new(HistoryMenu::default());
|
||||
line_editor = line_editor
|
||||
.with_menu(completion_menu)
|
||||
.with_menu(history_menu);
|
||||
.with_menu(completion_menu, None)
|
||||
.with_menu(history_menu, None);
|
||||
|
||||
let edit_mode: Box<dyn EditMode> = if vi_mode {
|
||||
let mut normal_keybindings = default_vi_normal_keybindings();
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::{find_common_string, Menu, MenuEvent, MenuTextStyle};
|
||||
use crate::{painting::Painter, Completer, History, LineBuffer, Span};
|
||||
use super::{menu_functions::find_common_string, Menu, MenuEvent, MenuTextStyle};
|
||||
use crate::{painting::Painter, Completer, History, LineBuffer, Suggestion};
|
||||
use nu_ansi_term::{ansi::RESET, Style};
|
||||
|
||||
/// Default values used as reference for the menu. These values are set during
|
||||
@ -48,7 +48,7 @@ pub struct CompletionMenu {
|
||||
/// Working column details keep changing based on the collected values
|
||||
working_details: ColumnDetails,
|
||||
/// Menu cached values
|
||||
values: Vec<(Span, String)>,
|
||||
values: Vec<Suggestion>,
|
||||
/// column position of the cursor. Starts from 0
|
||||
col_pos: u16,
|
||||
/// row position in the menu. Starts from 0
|
||||
@ -91,6 +91,13 @@ impl CompletionMenu {
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new value for text style
|
||||
#[must_use]
|
||||
pub fn with_description_text_style(mut self, description_text_style: Style) -> Self {
|
||||
self.color.description_style = description_text_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new columns value
|
||||
#[must_use]
|
||||
pub fn with_columns(mut self, columns: u16) -> Self {
|
||||
@ -226,7 +233,7 @@ impl CompletionMenu {
|
||||
}
|
||||
|
||||
/// Get selected value from the menu
|
||||
fn get_value(&self) -> Option<(Span, String)> {
|
||||
fn get_value(&self) -> Option<Suggestion> {
|
||||
self.get_values().get(self.index()).cloned()
|
||||
}
|
||||
|
||||
@ -286,50 +293,73 @@ impl CompletionMenu {
|
||||
}
|
||||
}
|
||||
|
||||
/// Text style for menu
|
||||
fn text_style(&self, index: usize) -> String {
|
||||
if index == self.index() {
|
||||
self.color.selected_text_style.prefix().to_string()
|
||||
} else {
|
||||
self.color.text_style.prefix().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates default string that represents one line from a menu
|
||||
/// Creates default string that represents one suggestion from the menu
|
||||
fn create_string(
|
||||
&self,
|
||||
line: &str,
|
||||
suggestion: &Suggestion,
|
||||
index: usize,
|
||||
column: u16,
|
||||
empty_space: usize,
|
||||
use_ansi_coloring: bool,
|
||||
) -> String {
|
||||
if use_ansi_coloring {
|
||||
let description = if let Some(description) = &suggestion.description {
|
||||
format!(
|
||||
"{}{}{}{:empty$}{}",
|
||||
self.text_style(index),
|
||||
&line,
|
||||
RESET,
|
||||
"{:empty$}{}",
|
||||
"",
|
||||
self.end_of_line(column),
|
||||
empty = empty_space
|
||||
description,
|
||||
empty = self.default_details.col_padding
|
||||
)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
if use_ansi_coloring {
|
||||
if index == self.index() {
|
||||
format!(
|
||||
"{}{}{:>empty$}{}{}",
|
||||
self.color.selected_text_style.prefix(),
|
||||
&suggestion.value,
|
||||
description,
|
||||
RESET,
|
||||
self.end_of_line(column),
|
||||
empty = empty_space,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{}{}{:>empty$}{}{}",
|
||||
self.color.text_style.prefix(),
|
||||
&suggestion.value,
|
||||
RESET,
|
||||
self.color.description_style.prefix(),
|
||||
description,
|
||||
RESET,
|
||||
self.end_of_line(column),
|
||||
empty = empty_space,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// If no ansi coloring is found, then the selection word is
|
||||
// the line in uppercase
|
||||
let line_str = if index == self.index() {
|
||||
format!(">{}", line.to_uppercase())
|
||||
let (marker, empty_space) = if index == self.index() {
|
||||
(">", empty_space.saturating_sub(1))
|
||||
} else {
|
||||
line.to_string()
|
||||
("", empty_space)
|
||||
};
|
||||
|
||||
// Final string with formatting
|
||||
format!(
|
||||
"{:width$}{}",
|
||||
line_str,
|
||||
let line = format!(
|
||||
"{}{}{:>empty$}{}",
|
||||
marker,
|
||||
&suggestion.value,
|
||||
description,
|
||||
self.end_of_line(column),
|
||||
width = self.get_width()
|
||||
)
|
||||
empty = empty_space,
|
||||
);
|
||||
|
||||
if index == self.index() {
|
||||
line.to_uppercase()
|
||||
} else {
|
||||
line
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -350,6 +380,11 @@ impl Menu for CompletionMenu {
|
||||
self.active
|
||||
}
|
||||
|
||||
/// The completion menu can to quick complete if there is only one element
|
||||
fn can_quick_complete(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// The completion menu can try to find the common string and replace it
|
||||
/// in the given line buffer
|
||||
fn can_partially_complete(
|
||||
@ -366,7 +401,7 @@ impl Menu for CompletionMenu {
|
||||
}
|
||||
|
||||
let values = self.get_values();
|
||||
if let (Some((span, value)), Some(index)) = find_common_string(values) {
|
||||
if let (Some(Suggestion { value, span, .. }), Some(index)) = find_common_string(values) {
|
||||
let index = index.min(value.len());
|
||||
let matching = &value[0..index];
|
||||
|
||||
@ -432,6 +467,56 @@ impl Menu for CompletionMenu {
|
||||
painter: &Painter,
|
||||
) {
|
||||
if let Some(event) = self.event.take() {
|
||||
// The working value for the menu are updated first before executing any of the
|
||||
// menu events
|
||||
//
|
||||
// If there is at least one suggestion that contains a description, then the layout
|
||||
// is changed to one column to fit the description
|
||||
let exist_description = self
|
||||
.get_values()
|
||||
.iter()
|
||||
.any(|suggestion| suggestion.description.is_some());
|
||||
|
||||
if exist_description {
|
||||
self.working_details.columns = 1;
|
||||
self.working_details.col_width = painter.screen_width() as usize;
|
||||
} else {
|
||||
let max_width = self.get_values().iter().fold(0, |acc, suggestion| {
|
||||
let str_len = suggestion.value.len() + self.default_details.col_padding;
|
||||
if str_len > acc {
|
||||
str_len
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
});
|
||||
|
||||
// If no default width is found, then the total screen width is used to estimate
|
||||
// the column width based on the default number of columns
|
||||
let default_width = if let Some(col_width) = self.default_details.col_width {
|
||||
col_width
|
||||
} else {
|
||||
let col_width = painter.screen_width() / self.default_details.columns;
|
||||
col_width as usize
|
||||
};
|
||||
|
||||
// Adjusting the working width of the column based the max line width found
|
||||
// in the menu values
|
||||
if max_width > default_width {
|
||||
self.working_details.col_width = max_width;
|
||||
} else {
|
||||
self.working_details.col_width = default_width;
|
||||
};
|
||||
|
||||
// The working columns is adjusted based on possible number of columns
|
||||
// that could be fitted in the screen with the calculated column width
|
||||
let possible_cols = painter.screen_width() / self.working_details.col_width as u16;
|
||||
if possible_cols > self.default_details.columns {
|
||||
self.working_details.columns = self.default_details.columns.max(1);
|
||||
} else {
|
||||
self.working_details.columns = possible_cols;
|
||||
}
|
||||
}
|
||||
|
||||
match event {
|
||||
MenuEvent::Activate(updated) => {
|
||||
self.active = true;
|
||||
@ -459,47 +544,12 @@ impl Menu for CompletionMenu {
|
||||
// The completion menu doest have the concept of pages, yet
|
||||
}
|
||||
}
|
||||
|
||||
let max_width = self.get_values().iter().fold(0, |acc, (_, string)| {
|
||||
let str_len = string.len() + self.default_details.col_padding;
|
||||
if str_len > acc {
|
||||
str_len
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
});
|
||||
|
||||
// If no default width is found, then the total screen width is used to estimate
|
||||
// the column width based on the default number of columns
|
||||
let default_width = if let Some(col_width) = self.default_details.col_width {
|
||||
col_width
|
||||
} else {
|
||||
let col_width = painter.screen_width() / self.default_details.columns;
|
||||
col_width as usize
|
||||
};
|
||||
|
||||
// Adjusting the working width of the column based the max line width found
|
||||
// in the menu values
|
||||
if max_width > default_width {
|
||||
self.working_details.col_width = max_width;
|
||||
} else {
|
||||
self.working_details.col_width = default_width;
|
||||
};
|
||||
|
||||
// The working columns is adjusted based on possible number of columns
|
||||
// that could be fitted in the screen with the calculated column width
|
||||
let possible_cols = painter.screen_width() / self.working_details.col_width as u16;
|
||||
if possible_cols > self.default_details.columns {
|
||||
self.working_details.columns = self.default_details.columns.max(1);
|
||||
} else {
|
||||
self.working_details.columns = possible_cols;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The buffer gets replaced in the Span location
|
||||
fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) {
|
||||
if let Some((span, value)) = self.get_value() {
|
||||
if let Some(Suggestion { value, span, .. }) = self.get_value() {
|
||||
let mut offset = line_buffer.insertion_point();
|
||||
offset += value.len() - (span.end - span.start);
|
||||
|
||||
@ -514,7 +564,7 @@ impl Menu for CompletionMenu {
|
||||
}
|
||||
|
||||
/// Gets values from filler that will be displayed in the menu
|
||||
fn get_values(&self) -> &[(Span, String)] {
|
||||
fn get_values(&self) -> &[Suggestion] {
|
||||
&self.values
|
||||
}
|
||||
|
||||
@ -544,13 +594,13 @@ impl Menu for CompletionMenu {
|
||||
.skip(skip_values)
|
||||
.take(available_values)
|
||||
.enumerate()
|
||||
.map(|(index, (_, line))| {
|
||||
.map(|(index, suggestion)| {
|
||||
// Correcting the enumerate index based on the number of skipped values
|
||||
let index = index + skip_values;
|
||||
let column = index as u16 % self.get_cols();
|
||||
let empty_space = self.get_width().saturating_sub(line.len());
|
||||
let empty_space = self.get_width().saturating_sub(suggestion.value.len());
|
||||
|
||||
self.create_string(line, index, column, empty_space, use_ansi_coloring)
|
||||
self.create_string(suggestion, index, column, empty_space, use_ansi_coloring)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
use super::{parse_selection_char, Menu, MenuEvent, MenuTextStyle};
|
||||
use super::{
|
||||
menu_functions::{parse_selection_char, string_difference},
|
||||
Menu, MenuEvent, MenuTextStyle,
|
||||
};
|
||||
use crate::{
|
||||
painting::{estimate_single_line_wraps, Painter},
|
||||
Completer, History, LineBuffer, Span,
|
||||
Completer, History, LineBuffer, Span, Suggestion,
|
||||
};
|
||||
use nu_ansi_term::{ansi::RESET, Style};
|
||||
use std::iter::Sum;
|
||||
@ -48,7 +51,7 @@ pub struct HistoryMenu {
|
||||
/// page_size records.
|
||||
/// When performing a query to the history object, the cached values will
|
||||
/// be the result from such query
|
||||
values: Vec<(Span, String)>,
|
||||
values: Vec<Suggestion>,
|
||||
/// row position in the menu. Starts from 0
|
||||
row_position: u16,
|
||||
/// Max size of the history when querying without a search buffer
|
||||
@ -187,7 +190,7 @@ impl HistoryMenu {
|
||||
}
|
||||
|
||||
/// Get selected value from the menu
|
||||
fn get_value(&self) -> Option<(Span, String)> {
|
||||
fn get_value(&self) -> Option<Suggestion> {
|
||||
self.get_values().get(self.index()).cloned()
|
||||
}
|
||||
|
||||
@ -207,11 +210,11 @@ impl HistoryMenu {
|
||||
.iter()
|
||||
.fold(
|
||||
(0, Some(0)),
|
||||
|(lines, total_lines), (_, entry)| match total_lines {
|
||||
|(lines, total_lines), suggestion| match total_lines {
|
||||
None => (lines, None),
|
||||
Some(total_lines) => {
|
||||
let new_total_lines =
|
||||
total_lines + self.number_of_lines(entry, painter.screen_width());
|
||||
let new_total_lines = total_lines
|
||||
+ self.number_of_lines(&suggestion.value, painter.screen_width());
|
||||
|
||||
if new_total_lines < available_lines {
|
||||
(lines + 1, Some(new_total_lines))
|
||||
@ -332,6 +335,11 @@ impl Menu for HistoryMenu {
|
||||
self.active
|
||||
}
|
||||
|
||||
/// There is no use for quick complete for the history menu
|
||||
fn can_quick_complete(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The history menu should not try to auto complete to avoid comparing
|
||||
/// all registered values
|
||||
fn can_partially_complete(
|
||||
@ -389,20 +397,23 @@ impl Menu for HistoryMenu {
|
||||
|
||||
self.values = values
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
(
|
||||
Span {
|
||||
start,
|
||||
end: start + input.len(),
|
||||
},
|
||||
s,
|
||||
)
|
||||
.map(|value| {
|
||||
let span = Span {
|
||||
start,
|
||||
end: start + input.len(),
|
||||
};
|
||||
|
||||
Suggestion {
|
||||
value,
|
||||
description: None,
|
||||
span,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Gets values from cached values that will be displayed in the menu
|
||||
fn get_values(&self) -> &[(Span, String)] {
|
||||
fn get_values(&self) -> &[Suggestion] {
|
||||
if self.history_size.is_some() {
|
||||
// When there is a history size value it means that only a chunk of the
|
||||
// chronological data from the database was collected
|
||||
@ -430,7 +441,7 @@ impl Menu for HistoryMenu {
|
||||
|
||||
/// The buffer gets cleared with the actual value
|
||||
fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) {
|
||||
if let Some((span, value)) = self.get_value() {
|
||||
if let Some(Suggestion { value, span, .. }) = self.get_value() {
|
||||
line_buffer.replace(span.start..span.end, &value);
|
||||
|
||||
let mut offset = line_buffer.insertion_point();
|
||||
@ -546,8 +557,8 @@ impl Menu for HistoryMenu {
|
||||
/// Calculates the real required lines for the menu considering how many lines
|
||||
/// wrap the terminal and if an entry is larger than the remaining lines
|
||||
fn menu_required_lines(&self, terminal_columns: u16) -> u16 {
|
||||
self.get_values().iter().fold(0, |acc, (_, entry)| {
|
||||
acc + self.number_of_lines(entry, terminal_columns)
|
||||
self.get_values().iter().fold(0, |acc, suggestion| {
|
||||
acc + self.number_of_lines(&suggestion.value, terminal_columns)
|
||||
}) + 1
|
||||
}
|
||||
|
||||
@ -561,8 +572,9 @@ impl Menu for HistoryMenu {
|
||||
.iter()
|
||||
.take(page.size)
|
||||
.enumerate()
|
||||
.map(|(index, (_, line))| {
|
||||
.map(|(index, suggestion)| {
|
||||
// Final string with colors
|
||||
let line = &suggestion.value;
|
||||
let line = if line.lines().count() > self.max_lines as usize {
|
||||
let lines = line
|
||||
.lines()
|
||||
@ -620,143 +632,10 @@ fn number_of_lines(entry: &str, max_lines: usize, terminal_columns: u16) -> u16
|
||||
lines
|
||||
}
|
||||
|
||||
fn string_difference<'a>(new_string: &'a str, old_string: &str) -> (usize, &'a str) {
|
||||
if old_string.is_empty() {
|
||||
return (0, new_string);
|
||||
}
|
||||
|
||||
let old_chars = old_string.chars().collect::<Vec<char>>();
|
||||
|
||||
let (_, start, end) = new_string.chars().enumerate().fold(
|
||||
(0, None, None),
|
||||
|(old_index, start, end), (index, c)| {
|
||||
let equal = if start.is_some() {
|
||||
if (old_string.len() - old_index) == (new_string.len() - index) {
|
||||
let new_iter = new_string.chars().skip(index);
|
||||
let old_iter = old_string.chars().skip(old_index);
|
||||
|
||||
new_iter.zip(old_iter).all(|(new, old)| new == old)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
c == old_chars[old_index]
|
||||
};
|
||||
|
||||
if equal {
|
||||
let old_index = (old_index + 1).min(old_string.len() - 1);
|
||||
|
||||
let end = match (start, end) {
|
||||
(Some(_), Some(_)) => end,
|
||||
(Some(_), None) => Some(index),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
(old_index, start, end)
|
||||
} else {
|
||||
let start = match start {
|
||||
Some(_) => start,
|
||||
None => Some(index),
|
||||
};
|
||||
|
||||
(old_index, start, end)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
match (start, end) {
|
||||
(Some(start), Some(end)) => (start, &new_string[start..end]),
|
||||
(Some(start), None) => (start, &new_string[start..new_string.len()]),
|
||||
(None, None) => (new_string.len(), ""),
|
||||
(None, Some(_)) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn string_difference_test() {
|
||||
let new_string = "this is a new string";
|
||||
let old_string = "this is a string";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (10, "new "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_new_larger() {
|
||||
let new_string = "this is a new string";
|
||||
let old_string = "this is";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (7, " a new string"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_new_shorter() {
|
||||
let new_string = "this is the";
|
||||
let old_string = "this is the original";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (11, ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_longer_string() {
|
||||
let new_string = "this is a new another";
|
||||
let old_string = "this is a string";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (10, "new another"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_start_same() {
|
||||
let new_string = "this is a new something string";
|
||||
let old_string = "this is a string";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (10, "new something "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_empty_old() {
|
||||
let new_string = "this new another";
|
||||
let old_string = "";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (0, "this new another"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_very_difference() {
|
||||
let new_string = "this new another";
|
||||
let old_string = "complete different string";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (0, "this new another"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_both_equal() {
|
||||
let new_string = "this new another";
|
||||
let old_string = "this new another";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (16, ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_with_non_ansi() {
|
||||
let new_string = "let b = ñ ";
|
||||
let old_string = "let a =";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (4, "b = ñ "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn number_of_lines_test() {
|
||||
let input = "let a: another:\nsomething\nanother";
|
||||
|
406
src/menu/menu_functions.rs
Normal file
406
src/menu/menu_functions.rs
Normal file
@ -0,0 +1,406 @@
|
||||
//! Collection of common functions that can be used to create menus
|
||||
use crate::Suggestion;
|
||||
|
||||
/// Index result obtained from parsing a string with an index marker
|
||||
/// For example, the next string:
|
||||
/// "this is an example :10"
|
||||
///
|
||||
/// Contains an index marker :10. This marker indicates that the user
|
||||
/// may want to select the 10th element from a list
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ParseResult<'buffer> {
|
||||
/// Text before the marker
|
||||
pub remainder: &'buffer str,
|
||||
/// Parsed value from the marker
|
||||
pub index: Option<usize>,
|
||||
/// Marker representation as string
|
||||
pub marker: Option<&'buffer str>,
|
||||
/// Direction of the search based on the marker
|
||||
pub direction: IndexDirection,
|
||||
}
|
||||
|
||||
/// Direction of the index found in the string
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum IndexDirection {
|
||||
/// Forward index
|
||||
Forward,
|
||||
/// Backward index
|
||||
Backward,
|
||||
}
|
||||
|
||||
/// Splits a string that contains a marker character
|
||||
///
|
||||
/// ## Example usage
|
||||
/// ```
|
||||
/// use reedline::menu_functions::{parse_selection_char, IndexDirection, ParseResult};
|
||||
///
|
||||
/// let parsed = parse_selection_char("this is an example!10", '!');
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// parsed,
|
||||
/// ParseResult {
|
||||
/// remainder: "this is an example",
|
||||
/// index: Some(10),
|
||||
/// marker: Some("!10"),
|
||||
/// direction: IndexDirection::Forward
|
||||
/// }
|
||||
/// )
|
||||
///
|
||||
/// ```
|
||||
pub fn parse_selection_char(buffer: &str, marker: char) -> ParseResult {
|
||||
if buffer.is_empty() {
|
||||
return ParseResult {
|
||||
remainder: buffer,
|
||||
index: None,
|
||||
marker: None,
|
||||
direction: IndexDirection::Forward,
|
||||
};
|
||||
}
|
||||
|
||||
let mut input = buffer.chars().peekable();
|
||||
|
||||
let mut index = 0;
|
||||
let mut direction = IndexDirection::Forward;
|
||||
while let Some(char) = input.next() {
|
||||
if char == marker {
|
||||
match input.peek() {
|
||||
Some(&x) if x == marker => {
|
||||
return ParseResult {
|
||||
remainder: &buffer[0..index],
|
||||
index: Some(0),
|
||||
marker: Some(&buffer[index..index + 2]),
|
||||
direction: IndexDirection::Backward,
|
||||
}
|
||||
}
|
||||
Some(&x) if x.is_ascii_digit() || x == '-' => {
|
||||
let mut count: usize = 0;
|
||||
let mut size: usize = 1;
|
||||
while let Some(&c) = input.peek() {
|
||||
if c == '-' {
|
||||
let _ = input.next();
|
||||
size += 1;
|
||||
direction = IndexDirection::Backward;
|
||||
} else if c.is_ascii_digit() {
|
||||
let c = c.to_digit(10).expect("already checked if is a digit");
|
||||
let _ = input.next();
|
||||
count *= 10;
|
||||
count += c as usize;
|
||||
size += 1;
|
||||
} else {
|
||||
return ParseResult {
|
||||
remainder: &buffer[0..index],
|
||||
index: Some(count),
|
||||
marker: Some(&buffer[index..index + size]),
|
||||
direction,
|
||||
};
|
||||
}
|
||||
}
|
||||
return ParseResult {
|
||||
remainder: &buffer[0..index],
|
||||
index: Some(count),
|
||||
marker: Some(&buffer[index..index + size]),
|
||||
direction,
|
||||
};
|
||||
}
|
||||
None => {
|
||||
return ParseResult {
|
||||
remainder: &buffer[0..index],
|
||||
index: Some(0),
|
||||
marker: Some(&buffer[index..buffer.len()]),
|
||||
direction,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
ParseResult {
|
||||
remainder: buffer,
|
||||
index: None,
|
||||
marker: None,
|
||||
direction,
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds index for the common string in a list of suggestions
|
||||
pub fn find_common_string(values: &[Suggestion]) -> (Option<&Suggestion>, Option<usize>) {
|
||||
let first = values.iter().next();
|
||||
|
||||
let index = first.and_then(|first| {
|
||||
values.iter().skip(1).fold(None, |index, suggestion| {
|
||||
if suggestion.value.starts_with(&first.value) {
|
||||
Some(first.value.len())
|
||||
} else {
|
||||
first
|
||||
.value
|
||||
.chars()
|
||||
.zip(suggestion.value.chars())
|
||||
.position(|(mut lhs, mut rhs)| {
|
||||
lhs.make_ascii_lowercase();
|
||||
rhs.make_ascii_lowercase();
|
||||
|
||||
lhs != rhs
|
||||
})
|
||||
.map(|new_index| match index {
|
||||
Some(index) => {
|
||||
if index <= new_index {
|
||||
index
|
||||
} else {
|
||||
new_index
|
||||
}
|
||||
}
|
||||
None => new_index,
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
(first, index)
|
||||
}
|
||||
|
||||
/// Finds different string between two strings
|
||||
///
|
||||
/// ## Example usage
|
||||
/// ```
|
||||
/// use reedline::menu_functions::string_difference;
|
||||
///
|
||||
/// let new_string = "this is a new string";
|
||||
/// let old_string = "this is a string";
|
||||
///
|
||||
/// let res = string_difference(new_string, old_string);
|
||||
/// assert_eq!(res, (10, "new "));
|
||||
/// ```
|
||||
pub fn string_difference<'a>(new_string: &'a str, old_string: &str) -> (usize, &'a str) {
|
||||
if old_string.is_empty() {
|
||||
return (0, new_string);
|
||||
}
|
||||
|
||||
let old_chars = old_string.chars().collect::<Vec<char>>();
|
||||
|
||||
let (_, start, end) = new_string.chars().enumerate().fold(
|
||||
(0, None, None),
|
||||
|(old_index, start, end), (index, c)| {
|
||||
let equal = if start.is_some() {
|
||||
if (old_string.len() - old_index) == (new_string.len() - index) {
|
||||
let new_iter = new_string.chars().skip(index);
|
||||
let old_iter = old_string.chars().skip(old_index);
|
||||
|
||||
new_iter.zip(old_iter).all(|(new, old)| new == old)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
c == old_chars[old_index]
|
||||
};
|
||||
|
||||
if equal {
|
||||
let old_index = (old_index + 1).min(old_string.len() - 1);
|
||||
|
||||
let end = match (start, end) {
|
||||
(Some(_), Some(_)) => end,
|
||||
(Some(_), None) => Some(index),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
(old_index, start, end)
|
||||
} else {
|
||||
let start = match start {
|
||||
Some(_) => start,
|
||||
None => Some(index),
|
||||
};
|
||||
|
||||
(old_index, start, end)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
match (start, end) {
|
||||
(Some(start), Some(end)) => (start, &new_string[start..end]),
|
||||
(Some(start), None) => (start, &new_string[start..new_string.len()]),
|
||||
(None, None) => (new_string.len(), ""),
|
||||
(None, Some(_)) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_row_test() {
|
||||
let input = "search:6";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "search");
|
||||
assert_eq!(res.index, Some(6));
|
||||
assert_eq!(res.marker, Some(":6"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_double_char() {
|
||||
let input = "search!!";
|
||||
let res = parse_selection_char(input, '!');
|
||||
|
||||
assert_eq!(res.remainder, "search");
|
||||
assert_eq!(res.index, Some(0));
|
||||
assert_eq!(res.marker, Some("!!"));
|
||||
assert!(matches!(res.direction, IndexDirection::Backward));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_row_other_marker_test() {
|
||||
let input = "search?9";
|
||||
let res = parse_selection_char(input, '?');
|
||||
|
||||
assert_eq!(res.remainder, "search");
|
||||
assert_eq!(res.index, Some(9));
|
||||
assert_eq!(res.marker, Some("?9"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_row_double_test() {
|
||||
let input = "ls | where:16";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "ls | where");
|
||||
assert_eq!(res.index, Some(16));
|
||||
assert_eq!(res.marker, Some(":16"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_row_empty_test() {
|
||||
let input = ":10";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "");
|
||||
assert_eq!(res.index, Some(10));
|
||||
assert_eq!(res.marker, Some(":10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_row_fake_indicator_test() {
|
||||
let input = "let a: another :10";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "let a: another ");
|
||||
assert_eq!(res.index, Some(10));
|
||||
assert_eq!(res.marker, Some(":10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_row_no_number_test() {
|
||||
let input = "let a: another:";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "let a: another");
|
||||
assert_eq!(res.index, Some(0));
|
||||
assert_eq!(res.marker, Some(":"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_buffer_test() {
|
||||
let input = "";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "");
|
||||
assert_eq!(res.index, None);
|
||||
assert_eq!(res.marker, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_negative_direction() {
|
||||
let input = "!-2";
|
||||
let res = parse_selection_char(input, '!');
|
||||
|
||||
assert_eq!(res.remainder, "");
|
||||
assert_eq!(res.index, Some(2));
|
||||
assert_eq!(res.marker, Some("!-2"));
|
||||
assert!(matches!(res.direction, IndexDirection::Backward));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_test() {
|
||||
let new_string = "this is a new string";
|
||||
let old_string = "this is a string";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (10, "new "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_new_larger() {
|
||||
let new_string = "this is a new string";
|
||||
let old_string = "this is";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (7, " a new string"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_new_shorter() {
|
||||
let new_string = "this is the";
|
||||
let old_string = "this is the original";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (11, ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_longer_string() {
|
||||
let new_string = "this is a new another";
|
||||
let old_string = "this is a string";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (10, "new another"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_start_same() {
|
||||
let new_string = "this is a new something string";
|
||||
let old_string = "this is a string";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (10, "new something "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_empty_old() {
|
||||
let new_string = "this new another";
|
||||
let old_string = "";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (0, "this new another"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_very_difference() {
|
||||
let new_string = "this new another";
|
||||
let old_string = "complete different string";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (0, "this new another"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_both_equal() {
|
||||
let new_string = "this new another";
|
||||
let old_string = "this new another";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (16, ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_difference_with_non_ansi() {
|
||||
let new_string = "let b = ñ ";
|
||||
let old_string = "let a =";
|
||||
|
||||
let res = string_difference(new_string, old_string);
|
||||
assert_eq!(res, (4, "b = ñ "));
|
||||
}
|
||||
}
|
323
src/menu/mod.rs
323
src/menu/mod.rs
@ -1,15 +1,20 @@
|
||||
mod completion_menu;
|
||||
mod history_menu;
|
||||
pub mod menu_functions;
|
||||
|
||||
use crate::{painting::Painter, Completer, History, LineBuffer, Span};
|
||||
use crate::{painting::Painter, Completer, History, LineBuffer, Suggestion};
|
||||
pub use completion_menu::CompletionMenu;
|
||||
pub use history_menu::HistoryMenu;
|
||||
use nu_ansi_term::{Color, Style};
|
||||
|
||||
/// Struct to store the menu style
|
||||
struct MenuTextStyle {
|
||||
selected_text_style: Style,
|
||||
text_style: Style,
|
||||
pub struct MenuTextStyle {
|
||||
/// Text style for selected text in a menu
|
||||
pub selected_text_style: Style,
|
||||
/// Text style for not selected text in the menu
|
||||
pub text_style: Style,
|
||||
/// Text style for the item description
|
||||
pub description_style: Style,
|
||||
}
|
||||
|
||||
impl Default for MenuTextStyle {
|
||||
@ -17,6 +22,7 @@ impl Default for MenuTextStyle {
|
||||
Self {
|
||||
selected_text_style: Color::Green.bold().reverse(),
|
||||
text_style: Color::DarkGray.normal(),
|
||||
description_style: Color::Yellow.normal(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,9 +62,7 @@ pub trait Menu: Send {
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Menu indicator
|
||||
fn indicator(&self) -> &str {
|
||||
"% "
|
||||
}
|
||||
fn indicator(&self) -> &str;
|
||||
|
||||
/// Checks if the menu is active
|
||||
fn is_active(&self) -> bool;
|
||||
@ -66,6 +70,10 @@ pub trait Menu: Send {
|
||||
/// Selects what type of event happened with the menu
|
||||
fn menu_event(&mut self, event: MenuEvent);
|
||||
|
||||
/// A menu may not be allowed to quick complete because it needs to stay
|
||||
/// active even with one element
|
||||
fn can_quick_complete(&self) -> bool;
|
||||
|
||||
/// The completion menu can try to find the common string and replace it
|
||||
/// in the given line buffer
|
||||
fn can_partially_complete(
|
||||
@ -115,234 +123,131 @@ pub trait Menu: Send {
|
||||
fn min_rows(&self) -> u16;
|
||||
|
||||
/// Gets cached values from menu that will be displayed
|
||||
fn get_values(&self) -> &[(Span, String)];
|
||||
fn get_values(&self) -> &[Suggestion];
|
||||
}
|
||||
|
||||
pub(crate) enum IndexDirection {
|
||||
Forward,
|
||||
Backward,
|
||||
/// Type of menu that can be used with Reedline engine
|
||||
pub(crate) enum MenuType {
|
||||
/// Menu that uses the engine completer to update its values
|
||||
EngineCompleter(Box<dyn Menu>),
|
||||
/// Menu that has its own Completer
|
||||
WithCompleter {
|
||||
menu: Box<dyn Menu>,
|
||||
completer: Box<dyn Completer>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) struct ParseResult<'buffer> {
|
||||
pub remainder: &'buffer str,
|
||||
pub index: Option<usize>,
|
||||
pub marker: Option<&'buffer str>,
|
||||
pub direction: IndexDirection,
|
||||
}
|
||||
|
||||
/// Splits a string that contains a marker character
|
||||
/// e.g: this is an example!10
|
||||
/// returns:
|
||||
/// this is an example
|
||||
/// (10, "!10") (index and index as string)
|
||||
pub(crate) fn parse_selection_char(buffer: &str, marker: char) -> ParseResult {
|
||||
if buffer.is_empty() {
|
||||
return ParseResult {
|
||||
remainder: buffer,
|
||||
index: None,
|
||||
marker: None,
|
||||
direction: IndexDirection::Forward,
|
||||
};
|
||||
}
|
||||
|
||||
let mut input = buffer.chars().peekable();
|
||||
|
||||
let mut index = 0;
|
||||
let mut direction = IndexDirection::Forward;
|
||||
while let Some(char) = input.next() {
|
||||
if char == marker {
|
||||
match input.peek() {
|
||||
Some(&x) if x == marker => {
|
||||
return ParseResult {
|
||||
remainder: &buffer[0..index],
|
||||
index: Some(0),
|
||||
marker: Some(&buffer[index..index + 2]),
|
||||
direction: IndexDirection::Backward,
|
||||
}
|
||||
}
|
||||
Some(&x) if x.is_ascii_digit() || x == '-' => {
|
||||
let mut count: usize = 0;
|
||||
let mut size: usize = 1;
|
||||
while let Some(&c) = input.peek() {
|
||||
if c == '-' {
|
||||
let _ = input.next();
|
||||
size += 1;
|
||||
direction = IndexDirection::Backward;
|
||||
} else if c.is_ascii_digit() {
|
||||
let c = c.to_digit(10).expect("already checked if is a digit");
|
||||
let _ = input.next();
|
||||
count *= 10;
|
||||
count += c as usize;
|
||||
size += 1;
|
||||
} else {
|
||||
return ParseResult {
|
||||
remainder: &buffer[0..index],
|
||||
index: Some(count),
|
||||
marker: Some(&buffer[index..index + size]),
|
||||
direction,
|
||||
};
|
||||
}
|
||||
}
|
||||
return ParseResult {
|
||||
remainder: &buffer[0..index],
|
||||
index: Some(count),
|
||||
marker: Some(&buffer[index..index + size]),
|
||||
direction,
|
||||
};
|
||||
}
|
||||
None => {
|
||||
return ParseResult {
|
||||
remainder: &buffer[0..index],
|
||||
index: Some(0),
|
||||
marker: Some(&buffer[index..buffer.len()]),
|
||||
direction,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
impl MenuType {
|
||||
fn as_ref(&self) -> &dyn Menu {
|
||||
match self {
|
||||
Self::EngineCompleter(menu) => menu.as_ref(),
|
||||
Self::WithCompleter { menu, .. } => menu.as_ref(),
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
ParseResult {
|
||||
remainder: buffer,
|
||||
index: None,
|
||||
marker: None,
|
||||
direction,
|
||||
fn as_mut(&mut self) -> &mut dyn Menu {
|
||||
match self {
|
||||
Self::EngineCompleter(menu) => menu.as_mut(),
|
||||
Self::WithCompleter { menu, .. } => menu.as_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds common string in a list of values
|
||||
fn find_common_string(values: &[(Span, String)]) -> (Option<&(Span, String)>, Option<usize>) {
|
||||
let first = values.iter().next();
|
||||
impl Menu for MenuType {
|
||||
fn name(&self) -> &str {
|
||||
self.as_ref().name()
|
||||
}
|
||||
|
||||
let index = first.and_then(|(_, first_string)| {
|
||||
values.iter().skip(1).fold(None, |index, (_, value)| {
|
||||
if value.starts_with(first_string) {
|
||||
Some(first_string.len())
|
||||
} else {
|
||||
first_string
|
||||
.chars()
|
||||
.zip(value.chars())
|
||||
.position(|(mut lhs, mut rhs)| {
|
||||
lhs.make_ascii_lowercase();
|
||||
rhs.make_ascii_lowercase();
|
||||
fn indicator(&self) -> &str {
|
||||
self.as_ref().indicator()
|
||||
}
|
||||
|
||||
lhs != rhs
|
||||
})
|
||||
.map(|new_index| match index {
|
||||
Some(index) => {
|
||||
if index <= new_index {
|
||||
index
|
||||
} else {
|
||||
new_index
|
||||
}
|
||||
}
|
||||
None => new_index,
|
||||
})
|
||||
fn is_active(&self) -> bool {
|
||||
self.as_ref().is_active()
|
||||
}
|
||||
|
||||
fn menu_event(&mut self, event: MenuEvent) {
|
||||
self.as_mut().menu_event(event)
|
||||
}
|
||||
|
||||
fn can_quick_complete(&self) -> bool {
|
||||
self.as_ref().can_quick_complete()
|
||||
}
|
||||
|
||||
fn can_partially_complete(
|
||||
&mut self,
|
||||
values_updated: bool,
|
||||
line_buffer: &mut LineBuffer,
|
||||
history: &dyn History,
|
||||
completer: &dyn Completer,
|
||||
) -> bool {
|
||||
match self {
|
||||
Self::EngineCompleter(menu) => {
|
||||
menu.can_partially_complete(values_updated, line_buffer, history, completer)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
(first, index)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_row_test() {
|
||||
let input = "search:6";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "search");
|
||||
assert_eq!(res.index, Some(6));
|
||||
assert_eq!(res.marker, Some(":6"));
|
||||
Self::WithCompleter {
|
||||
menu,
|
||||
completer: own_completer,
|
||||
} => menu.can_partially_complete(
|
||||
values_updated,
|
||||
line_buffer,
|
||||
history,
|
||||
own_completer.as_ref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_double_char() {
|
||||
let input = "search!!";
|
||||
let res = parse_selection_char(input, '!');
|
||||
|
||||
assert_eq!(res.remainder, "search");
|
||||
assert_eq!(res.index, Some(0));
|
||||
assert_eq!(res.marker, Some("!!"));
|
||||
assert!(matches!(res.direction, IndexDirection::Backward));
|
||||
fn update_values(
|
||||
&mut self,
|
||||
line_buffer: &mut LineBuffer,
|
||||
history: &dyn History,
|
||||
completer: &dyn Completer,
|
||||
) {
|
||||
match self {
|
||||
Self::EngineCompleter(menu) => menu.update_values(line_buffer, history, completer),
|
||||
Self::WithCompleter {
|
||||
menu,
|
||||
completer: own_completer,
|
||||
} => menu.update_values(line_buffer, history, own_completer.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_row_other_marker_test() {
|
||||
let input = "search?9";
|
||||
let res = parse_selection_char(input, '?');
|
||||
|
||||
assert_eq!(res.remainder, "search");
|
||||
assert_eq!(res.index, Some(9));
|
||||
assert_eq!(res.marker, Some("?9"));
|
||||
fn update_working_details(
|
||||
&mut self,
|
||||
line_buffer: &mut LineBuffer,
|
||||
history: &dyn History,
|
||||
completer: &dyn Completer,
|
||||
painter: &Painter,
|
||||
) {
|
||||
match self {
|
||||
Self::EngineCompleter(menu) => {
|
||||
menu.update_working_details(line_buffer, history, completer, painter)
|
||||
}
|
||||
Self::WithCompleter {
|
||||
menu,
|
||||
completer: own_completer,
|
||||
} => menu.update_working_details(line_buffer, history, own_completer.as_ref(), painter),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_row_double_test() {
|
||||
let input = "ls | where:16";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "ls | where");
|
||||
assert_eq!(res.index, Some(16));
|
||||
assert_eq!(res.marker, Some(":16"));
|
||||
fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) {
|
||||
self.as_ref().replace_in_buffer(line_buffer)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_row_empty_test() {
|
||||
let input = ":10";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "");
|
||||
assert_eq!(res.index, Some(10));
|
||||
assert_eq!(res.marker, Some(":10"));
|
||||
fn menu_required_lines(&self, terminal_columns: u16) -> u16 {
|
||||
self.as_ref().menu_required_lines(terminal_columns)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_row_fake_indicator_test() {
|
||||
let input = "let a: another :10";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "let a: another ");
|
||||
assert_eq!(res.index, Some(10));
|
||||
assert_eq!(res.marker, Some(":10"));
|
||||
fn menu_string(&self, available_lines: u16, use_ansi_coloring: bool) -> String {
|
||||
self.as_ref()
|
||||
.menu_string(available_lines, use_ansi_coloring)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_row_no_number_test() {
|
||||
let input = "let a: another:";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "let a: another");
|
||||
assert_eq!(res.index, Some(0));
|
||||
assert_eq!(res.marker, Some(":"));
|
||||
fn min_rows(&self) -> u16 {
|
||||
self.as_ref().min_rows()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_buffer_test() {
|
||||
let input = "";
|
||||
let res = parse_selection_char(input, ':');
|
||||
|
||||
assert_eq!(res.remainder, "");
|
||||
assert_eq!(res.index, None);
|
||||
assert_eq!(res.marker, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_negative_direction() {
|
||||
let input = "!-2";
|
||||
let res = parse_selection_char(input, '!');
|
||||
|
||||
assert_eq!(res.remainder, "");
|
||||
assert_eq!(res.index, Some(2));
|
||||
assert_eq!(res.marker, Some("!-2"));
|
||||
assert!(matches!(res.direction, IndexDirection::Backward));
|
||||
fn get_values(&self) -> &[Suggestion] {
|
||||
self.as_ref().get_values()
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
use super::utils::{coerce_crlf, line_width};
|
||||
|
||||
use {
|
||||
crate::{menu::Menu, painting::PromptLines, Prompt},
|
||||
super::utils::{coerce_crlf, line_width},
|
||||
crate::{
|
||||
menu::{Menu, MenuType},
|
||||
painting::PromptLines,
|
||||
Prompt,
|
||||
},
|
||||
crossterm::{
|
||||
cursor::{self, MoveTo, RestorePosition, SavePosition},
|
||||
style::{Print, ResetColor, SetForegroundColor},
|
||||
@ -73,7 +76,8 @@ impl Painter {
|
||||
self.terminal_size.0
|
||||
}
|
||||
|
||||
fn remaining_lines(&self) -> u16 {
|
||||
/// Returns the available lines from the prompt down
|
||||
pub fn remaining_lines(&self) -> u16 {
|
||||
self.screen_height() - self.prompt_start_row
|
||||
}
|
||||
|
||||
@ -124,7 +128,7 @@ impl Painter {
|
||||
&mut self,
|
||||
prompt: &dyn Prompt,
|
||||
lines: &PromptLines,
|
||||
menu: Option<&dyn Menu>,
|
||||
menu: Option<&MenuType>,
|
||||
use_ansi_coloring: bool,
|
||||
) -> Result<()> {
|
||||
self.stdout.queue(cursor::Hide)?;
|
||||
@ -219,7 +223,7 @@ impl Painter {
|
||||
&mut self,
|
||||
prompt: &dyn Prompt,
|
||||
lines: &PromptLines,
|
||||
menu: Option<&dyn Menu>,
|
||||
menu: Option<&MenuType>,
|
||||
use_ansi_coloring: bool,
|
||||
) -> Result<()> {
|
||||
// print our prompt with color
|
||||
@ -261,7 +265,7 @@ impl Painter {
|
||||
&mut self,
|
||||
prompt: &dyn Prompt,
|
||||
lines: &PromptLines,
|
||||
menu: Option<&dyn Menu>,
|
||||
menu: Option<&MenuType>,
|
||||
use_ansi_coloring: bool,
|
||||
) -> Result<()> {
|
||||
let screen_width = self.screen_width();
|
||||
|
@ -1,5 +1,9 @@
|
||||
use super::utils::{coerce_crlf, estimate_required_lines, line_width};
|
||||
use crate::{menu::Menu, prompt::PromptEditMode, Prompt, PromptHistorySearch};
|
||||
use crate::{
|
||||
menu::{Menu, MenuType},
|
||||
prompt::PromptEditMode,
|
||||
Prompt, PromptHistorySearch,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Aggregate of prompt and input string used by `Painter`
|
||||
@ -49,7 +53,7 @@ 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.
|
||||
pub(crate) fn required_lines(&self, terminal_columns: u16, menu: Option<&dyn Menu>) -> u16 {
|
||||
pub(crate) fn required_lines(&self, terminal_columns: u16, menu: Option<&MenuType>) -> u16 {
|
||||
let input = if menu.is_none() {
|
||||
self.prompt_str_left.to_string()
|
||||
+ &self.prompt_indicator
|
||||
|
Loading…
Reference in New Issue
Block a user