Generic menus (#5085)

* updated to reedline generic menus

* help menu with examples

* generic menus in the engine

* description menu template

* list of menus in config

* default value for menu

* menu from block

* generic menus examples

* change to reedline git path

* cargo fmt

* menu name typo

* remove commas from default file

* added error message
This commit is contained in:
Fernando Herrera 2022-04-04 15:54:48 +01:00 committed by GitHub
parent a86e6ce89b
commit 608b6f3634
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 916 additions and 344 deletions

2
Cargo.lock generated
View File

@ -3428,7 +3428,7 @@ dependencies = [
[[package]]
name = "reedline"
version = "0.3.1"
source = "git+https://github.com/nushell/reedline?branch=main#accce4af7f50ea143ed818dd5fe58484e107e922"
source = "git+https://github.com/nushell/reedline?branch=main#698190c534e8632f76561cbe8b45a5de74a6e96f"
dependencies = [
"chrono",
"crossterm",

View File

@ -95,6 +95,7 @@ impl NuCompleter {
output.push(Suggestion {
value: builtin.to_string(),
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
@ -109,6 +110,7 @@ impl NuCompleter {
output.push(Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
@ -123,6 +125,7 @@ impl NuCompleter {
output.push(Suggestion {
value: String::from_utf8_lossy(v.0).to_string(),
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
@ -152,6 +155,7 @@ impl NuCompleter {
.map(move |x| Suggestion {
value: String::from_utf8_lossy(&x.0).to_string(),
description: x.1,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
@ -165,6 +169,7 @@ impl NuCompleter {
.map(move |x| Suggestion {
value: String::from_utf8_lossy(&x).to_string(),
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
@ -182,6 +187,7 @@ impl NuCompleter {
.map(move |x| Suggestion {
value: x,
description: None,
extra: None,
span: reedline::Span {
start: span.start - offset,
end: span.end - offset,
@ -193,6 +199,7 @@ impl NuCompleter {
results.push(Suggestion {
value: format!("^{}", external.value),
description: None,
extra: None,
span: external.span,
})
} else {
@ -266,6 +273,7 @@ impl NuCompleter {
output.push(Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
extra: None,
span: reedline::Span {
start: new_span.start - offset,
end: new_span.end - offset,
@ -285,6 +293,7 @@ impl NuCompleter {
output.push(Suggestion {
value: String::from_utf8_lossy(&named).to_string(),
description: Some(flag_desc.to_string()),
extra: None,
span: reedline::Span {
start: new_span.start - offset,
end: new_span.end - offset,
@ -341,6 +350,7 @@ impl NuCompleter {
Ok(s) => Some(Suggestion {
value: s,
description: None,
extra: None,
span: reedline::Span {
start: new_span.start - offset,
end: new_span.end - offset,
@ -453,6 +463,7 @@ impl NuCompleter {
.map(move |x| Suggestion {
value: x.1,
description: None,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,
@ -569,6 +580,7 @@ impl NuCompleter {
.map(move |x| Suggestion {
value: x.1,
description: None,
extra: None,
span: reedline::Span {
start: x.0.start - offset,
end: x.0.end - offset,

View File

@ -3,8 +3,7 @@ mod completions;
mod config_files;
mod errors;
mod eval_file;
mod help_completions;
mod help_menu;
mod menus;
mod nu_highlight;
mod print;
mod prompt;
@ -20,8 +19,7 @@ pub use completions::NuCompleter;
pub use config_files::eval_config_contents;
pub use errors::CliError;
pub use eval_file::evaluate_file;
pub use help_completions::NuHelpCompleter;
pub use help_menu::NuHelpMenu;
pub use menus::{DescriptionMenu, NuHelpCompleter};
pub use nu_highlight::NuHighlight;
pub use print::Print;
pub use prompt::NushellPrompt;

View File

@ -1,9 +1,8 @@
use {
crate::help_completions::{EXAMPLE_MARKER, EXAMPLE_NEW_LINE},
nu_ansi_term::{ansi::RESET, Style},
reedline::{
menu_functions::string_difference, Completer, History, LineBuffer, Menu, MenuEvent,
MenuTextStyle, Painter, Suggestion,
menu_functions::string_difference, Completer, LineBuffer, Menu, MenuEvent, MenuTextStyle,
Painter, Suggestion,
},
};
@ -48,7 +47,10 @@ struct WorkingDetails {
}
/// Completion menu definition
pub struct NuHelpMenu {
pub struct DescriptionMenu {
/// Menu name
name: String,
/// Menu status
active: bool,
/// Menu coloring
color: MenuTextStyle,
@ -80,11 +82,15 @@ pub struct NuHelpMenu {
show_examples: bool,
/// Skipped description rows
skipped_rows: usize,
/// Calls the completer using only the line buffer difference difference
/// after the menu was activated
only_buffer_difference: bool,
}
impl Default for NuHelpMenu {
impl Default for DescriptionMenu {
fn default() -> Self {
Self {
name: "description_menu".to_string(),
active: false,
color: MenuTextStyle::default(),
default_details: DefaultMenuDetails::default(),
@ -100,11 +106,19 @@ impl Default for NuHelpMenu {
example_index: None,
show_examples: true,
skipped_rows: 0,
only_buffer_difference: true,
}
}
}
impl NuHelpMenu {
// Menu configuration
impl DescriptionMenu {
/// Menu builder with new name
pub fn with_name(mut self, name: &str) -> Self {
self.name = name.into();
self
}
/// Menu builder with new value for text style
pub fn with_text_style(mut self, text_style: Style) -> Self {
self.color.text_style = text_style;
@ -159,6 +173,15 @@ impl NuHelpMenu {
self
}
/// Menu builder with new only buffer difference
pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self {
self.only_buffer_difference = only_buffer_difference;
self
}
}
// Menu functionality
impl DescriptionMenu {
/// Move menu cursor to the next element
fn move_next(&mut self) {
let mut new_col = self.col_pos + 1;
@ -279,19 +302,11 @@ impl NuHelpMenu {
/// Update list of examples from the actual value
fn update_examples(&mut self) {
let examples = self
self.examples = self
.get_value()
.and_then(|suggestion| suggestion.description)
.unwrap_or_else(|| "".to_string())
.lines()
.filter(|line| line.starts_with(EXAMPLE_MARKER))
.map(|line| {
line.replace(EXAMPLE_MARKER, "")
.replace(EXAMPLE_NEW_LINE, "\r\n")
})
.collect::<Vec<String>>();
.and_then(|suggestion| suggestion.extra)
.unwrap_or_default();
self.examples = examples;
self.example_index = None;
}
@ -359,7 +374,6 @@ impl NuHelpMenu {
.and_then(|suggestion| suggestion.description)
.unwrap_or_else(|| "".to_string())
.lines()
.filter(|line| !line.starts_with(EXAMPLE_MARKER))
.skip(self.skipped_rows)
.take(self.working_details.description_rows)
.collect::<Vec<&str>>()
@ -420,10 +434,10 @@ impl NuHelpMenu {
}
}
impl Menu for NuHelpMenu {
impl Menu for DescriptionMenu {
/// Menu name
fn name(&self) -> &str {
"help_menu"
self.name.as_str()
}
/// Menu indicator
@ -436,17 +450,16 @@ impl Menu for NuHelpMenu {
self.active
}
/// The help menu stays active even with one record
/// The menu stays active even with one record
fn can_quick_complete(&self) -> bool {
false
}
/// The help menu does not need to partially complete
/// The menu does not need to partially complete
fn can_partially_complete(
&mut self,
_values_updated: bool,
_line_buffer: &mut LineBuffer,
_history: &dyn History,
_completer: &dyn Completer,
) -> bool {
false
@ -468,30 +481,21 @@ impl Menu for NuHelpMenu {
}
/// Updates menu values
fn update_values(
&mut self,
line_buffer: &mut LineBuffer,
_history: &dyn History,
completer: &dyn Completer,
) {
fn update_values(&mut self, line_buffer: &mut LineBuffer, completer: &dyn Completer) {
if self.only_buffer_difference {
if let Some(old_string) = &self.input {
let (start, input) = string_difference(line_buffer.get_buffer(), old_string);
if !input.is_empty() {
self.reset_position();
self.values = completer
.complete(input, line_buffer.insertion_point())
.into_iter()
.map(|suggestion| Suggestion {
value: suggestion.value,
description: suggestion.description,
span: reedline::Span {
start,
end: start + input.len(),
},
})
.collect();
self.values = completer.complete(input, start);
}
}
} else {
let trimmed_buffer = line_buffer.get_buffer().replace('\n', " ");
self.values =
completer.complete(trimmed_buffer.as_str(), line_buffer.insertion_point());
self.reset_position();
}
}
/// The working details for the menu changes based on the size of the lines
@ -499,7 +503,6 @@ impl Menu for NuHelpMenu {
fn update_working_details(
&mut self,
line_buffer: &mut LineBuffer,
history: &dyn History,
completer: &dyn Completer,
painter: &Painter,
) {
@ -558,12 +561,12 @@ impl Menu for NuHelpMenu {
MenuEvent::Activate(_) => {
self.reset_position();
self.input = Some(line_buffer.get_buffer().to_string());
self.update_values(line_buffer, history, completer);
self.update_values(line_buffer, completer);
}
MenuEvent::Deactivate => self.active = false,
MenuEvent::Edit(_) => {
self.reset_position();
self.update_values(line_buffer, history, completer);
self.update_values(line_buffer, completer);
self.update_examples()
}
MenuEvent::NextElement => {
@ -607,7 +610,6 @@ impl Menu for NuHelpMenu {
.and_then(|suggestion| suggestion.description)
.unwrap_or_else(|| "".to_string())
.lines()
.filter(|line| !line.starts_with(EXAMPLE_MARKER))
.count();
let allowed_skips =
@ -627,20 +629,24 @@ impl Menu for NuHelpMenu {
/// The buffer gets replaced in the Span location
fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) {
if let Some(Suggestion { value, span, .. }) = self.get_value() {
let start = span.start.min(line_buffer.len());
let end = span.end.min(line_buffer.len());
let string_len = if let Some(example_index) = self.example_index {
let example = self
.examples
.get(example_index)
.expect("the example index is always checked");
line_buffer.replace(span.start..span.end, example);
line_buffer.replace(start..end, example);
example.len()
} else {
line_buffer.replace(span.start..span.end, &value);
line_buffer.replace(start..end, &value);
value.len()
};
let mut offset = line_buffer.insertion_point();
offset += string_len.saturating_sub(span.end - span.start);
offset += string_len.saturating_sub(end.saturating_sub(start));
line_buffer.set_insertion_point(offset);
}
}

View File

@ -2,20 +2,15 @@ use nu_engine::documentation::get_flags_section;
use nu_protocol::{engine::EngineState, levenshtein_distance};
use reedline::{Completer, Suggestion};
pub const EXAMPLE_MARKER: &str = ">>>>>>";
pub const EXAMPLE_NEW_LINE: &str = "%%%%%%";
pub struct NuHelpCompleter {
engine_state: EngineState,
}
pub struct NuHelpCompleter(EngineState);
impl NuHelpCompleter {
pub fn new(engine_state: EngineState) -> Self {
Self { engine_state }
Self(engine_state)
}
fn completion_helper(&self, line: &str, _pos: usize) -> Vec<Suggestion> {
let full_commands = self.engine_state.get_signatures_with_examples(false);
fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
let full_commands = self.0.get_signatures_with_examples(false);
//Vec<(Signature, Vec<Example>, bool, bool)> {
let mut commands = full_commands
@ -83,20 +78,18 @@ impl NuHelpCompleter {
}
}
for example in examples {
long_desc.push_str(&format!(
"{}{}\r\n",
EXAMPLE_MARKER,
example.example.replace('\n', EXAMPLE_NEW_LINE)
))
}
let extra: Vec<String> = examples
.iter()
.map(|example| example.example.to_string())
.collect();
Suggestion {
value: sig.name.clone(),
description: Some(long_desc),
extra: Some(extra),
span: reedline::Span {
start: 0,
end: sig.name.len(),
start: pos,
end: pos + line.len(),
},
}
})

View File

@ -0,0 +1,167 @@
use nu_engine::eval_block;
use nu_protocol::{
engine::{EngineState, Stack},
IntoPipelineData, Span, Value,
};
use reedline::{menu_functions::parse_selection_char, Completer, Suggestion};
const SELECTION_CHAR: char = '!';
pub struct NuMenuCompleter {
block_id: usize,
span: Span,
stack: Stack,
engine_state: EngineState,
only_buffer_difference: bool,
}
impl NuMenuCompleter {
pub fn new(
block_id: usize,
span: Span,
stack: Stack,
engine_state: EngineState,
only_buffer_difference: bool,
) -> Self {
Self {
block_id,
span,
stack,
engine_state,
only_buffer_difference,
}
}
}
impl Completer for NuMenuCompleter {
fn complete(&self, line: &str, pos: usize) -> Vec<Suggestion> {
let parsed = parse_selection_char(line, SELECTION_CHAR);
let block = self.engine_state.get_block(self.block_id);
let mut stack = self.stack.clone();
if let Some(buffer) = block.signature.get_positional(0) {
if let Some(buffer_id) = &buffer.var_id {
let line_buffer = Value::String {
val: parsed.remainder.to_string(),
span: self.span,
};
stack.add_var(*buffer_id, line_buffer);
}
}
if let Some(position) = block.signature.get_positional(1) {
if let Some(position_id) = &position.var_id {
let line_buffer = Value::Int {
val: pos as i64,
span: self.span,
};
stack.add_var(*position_id, line_buffer);
}
}
let input = Value::nothing(self.span).into_pipeline_data();
let res = eval_block(&self.engine_state, &mut stack, block, input, false, false);
if let Ok(values) = res {
let values = values.into_value(self.span);
convert_to_suggestions(values, line, pos, self.only_buffer_difference)
} else {
Vec::new()
}
}
}
fn convert_to_suggestions(
value: Value,
line: &str,
pos: usize,
only_buffer_difference: bool,
) -> Vec<Suggestion> {
match value {
Value::Record { .. } => {
let text = match value
.get_data_by_key("value")
.and_then(|val| val.as_string().ok())
{
Some(val) => val,
None => "No value key".to_string(),
};
let description = value
.get_data_by_key("description")
.and_then(|val| val.as_string().ok());
let span = match value.get_data_by_key("span") {
Some(span @ Value::Record { .. }) => {
let start = span
.get_data_by_key("start")
.and_then(|val| val.as_integer().ok());
let end = span
.get_data_by_key("end")
.and_then(|val| val.as_integer().ok());
match (start, end) {
(Some(start), Some(end)) => {
let start = start.min(end);
reedline::Span {
start: start as usize,
end: end as usize,
}
}
_ => reedline::Span {
start: if only_buffer_difference { pos } else { 0 },
end: if only_buffer_difference {
pos + line.len()
} else {
line.len()
},
},
}
}
_ => reedline::Span {
start: if only_buffer_difference { pos } else { 0 },
end: if only_buffer_difference {
pos + line.len()
} else {
line.len()
},
},
};
let extra = match value.get_data_by_key("extra") {
Some(Value::List { vals, .. }) => {
let extra: Vec<String> = vals
.into_iter()
.filter_map(|extra| match extra {
Value::String { val, .. } => Some(val),
_ => None,
})
.collect();
Some(extra)
}
_ => None,
};
vec![Suggestion {
value: text,
description,
extra,
span,
}]
}
Value::List { vals, .. } => vals
.into_iter()
.flat_map(|val| convert_to_suggestions(val, line, pos, only_buffer_difference))
.collect(),
_ => vec![Suggestion {
value: format!("Not a record: {:?}", value),
description: None,
extra: None,
span: reedline::Span {
start: 0,
end: line.len(),
},
}],
}
}

View File

@ -0,0 +1,7 @@
mod description_menu;
mod help_completions;
mod menu_completions;
pub use description_menu::DescriptionMenu;
pub use help_completions::NuHelpCompleter;
pub use menu_completions::NuMenuCompleter;

View File

@ -1,219 +1,453 @@
use super::NuHelpMenu;
use super::DescriptionMenu;
use crate::{menus::NuMenuCompleter, NuHelpCompleter};
use crossterm::event::{KeyCode, KeyModifiers};
use nu_color_config::lookup_ansi_color_style;
use nu_protocol::{extract_value, Config, ParsedKeybinding, ShellError, Span, Value};
use nu_engine::eval_block;
use nu_parser::parse;
use nu_protocol::{
create_menus,
engine::{EngineState, Stack, StateWorkingSet},
extract_value, Config, IntoPipelineData, ParsedKeybinding, ParsedMenu, PipelineData,
ShellError, Span, Value,
};
use reedline::{
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
Completer, CompletionMenu, EditCommand, HistoryMenu, Keybindings, Reedline, ReedlineEvent,
ColumnarMenu, EditCommand, Keybindings, ListMenu, Reedline, ReedlineEvent, ReedlineMenu,
};
// Creates an input object for the completion menu based on the dictionary
// stored in the config variable
pub(crate) fn add_completion_menu(line_editor: Reedline, config: &Config) -> Reedline {
let mut completion_menu = CompletionMenu::default();
completion_menu = match config
.menu_config
.get("columns")
.and_then(|value| value.as_integer().ok())
{
Some(value) => completion_menu.with_columns(value as u16),
None => completion_menu,
};
completion_menu = completion_menu.with_column_width(
config
.menu_config
.get("col_width")
.and_then(|value| value.as_integer().ok())
.map(|value| value as usize),
);
completion_menu = match config
.menu_config
.get("col_padding")
.and_then(|value| value.as_integer().ok())
{
Some(value) => completion_menu.with_column_padding(value as usize),
None => completion_menu,
};
completion_menu = match config
.menu_config
.get("text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => completion_menu.with_text_style(lookup_ansi_color_style(&value)),
None => completion_menu,
};
completion_menu = match config
.menu_config
.get("selected_text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => completion_menu.with_selected_text_style(lookup_ansi_color_style(&value)),
None => completion_menu,
};
completion_menu = match config
.menu_config
.get("marker")
.and_then(|value| value.as_string().ok())
{
Some(value) => completion_menu.with_marker(value),
None => completion_menu,
};
line_editor.with_menu(Box::new(completion_menu), None)
}
// Creates an input object for the history menu based on the dictionary
// stored in the config variable
pub(crate) fn add_history_menu(line_editor: Reedline, config: &Config) -> Reedline {
let mut history_menu = HistoryMenu::default();
history_menu = match config
.history_config
.get("page_size")
.and_then(|value| value.as_integer().ok())
{
Some(value) => history_menu.with_page_size(value as usize),
None => history_menu,
};
history_menu = match config
.history_config
.get("selector")
.and_then(|value| value.as_string().ok())
{
Some(value) => {
let char = value.chars().next().unwrap_or('!');
history_menu.with_selection_char(char)
const DEFAULT_COMPLETION_MENU: &str = r#"
{
name: completion_menu
only_buffer_difference: false
marker: "| "
type: {
layout: columnar
columns: 4
col_width: 20
col_padding: 2
}
None => history_menu,
};
style: {
text: green,
selected_text: green_reverse
description_text: yellow
}
}"#;
history_menu = match config
.history_config
.get("text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => history_menu.with_text_style(lookup_ansi_color_style(&value)),
None => history_menu,
};
const DEFAULT_HISTORY_MENU: &str = r#"
{
name: history_menu
only_buffer_difference: true
marker: "? "
type: {
layout: list
page_size: 10
}
style: {
text: green,
selected_text: green_reverse
description_text: yellow
}
}"#;
history_menu = match config
.history_config
.get("selected_text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => history_menu.with_selected_text_style(lookup_ansi_color_style(&value)),
None => history_menu,
};
const DEFAULT_HELP_MENU: &str = r#"
{
name: help_menu
only_buffer_difference: true
marker: "? "
type: {
layout: description
columns: 4
col_width: 20
col_padding: 2
selection_rows: 4
description_rows: 10
}
style: {
text: green,
selected_text: green_reverse
description_text: yellow
}
}"#;
history_menu = match config
.history_config
.get("marker")
.and_then(|value| value.as_string().ok())
{
Some(value) => history_menu.with_marker(value),
None => history_menu,
};
line_editor.with_menu(Box::new(history_menu), None)
}
// Creates an input object for the help menu based on the dictionary
// stored in the config variable
pub(crate) fn add_help_menu(
line_editor: Reedline,
help_completer: Box<dyn Completer>,
// Adds all menus to line editor
pub(crate) fn add_menus(
mut line_editor: Reedline,
engine_state: &EngineState,
stack: &Stack,
config: &Config,
) -> Reedline {
let mut help_menu = NuHelpMenu::default();
) -> Result<Reedline, ShellError> {
line_editor = line_editor.clear_menus();
help_menu = match config
.help_config
.get("columns")
.and_then(|value| value.as_integer().ok())
for menu in &config.menus {
line_editor = add_menu(line_editor, menu, engine_state, stack, config)?
}
// Checking if the default menus have been added from the config file
let default_menus = vec![
("completion_menu", DEFAULT_COMPLETION_MENU),
("history_menu", DEFAULT_HISTORY_MENU),
("help_menu", DEFAULT_HELP_MENU),
];
for (name, definition) in default_menus {
if !config
.menus
.iter()
.any(|menu| menu.name.into_string("", config) == name)
{
Some(value) => help_menu.with_columns(value as u16),
None => help_menu,
};
help_menu = help_menu.with_column_width(
config
.help_config
.get("col_width")
.and_then(|value| value.as_integer().ok())
.map(|value| value as usize),
let (block, _) = {
let mut working_set = StateWorkingSet::new(engine_state);
let (output, _) = parse(
&mut working_set,
Some(name), // format!("entry #{}", entry_num)
definition.as_bytes(),
true,
&[],
);
help_menu = match config
.help_config
.get("col_padding")
.and_then(|value| value.as_integer().ok())
{
Some(value) => help_menu.with_column_padding(value as usize),
None => help_menu,
(output, working_set.render())
};
help_menu = match config
.help_config
.get("selection_rows")
.and_then(|value| value.as_integer().ok())
{
Some(value) => help_menu.with_selection_rows(value as u16),
None => help_menu,
let mut temp_stack = Stack::new();
let input = Value::nothing(Span::test_data()).into_pipeline_data();
let res = eval_block(engine_state, &mut temp_stack, &block, input, false, false)?;
if let PipelineData::Value(value, None) = res {
for menu in create_menus(&value, config)? {
line_editor = add_menu(line_editor, &menu, engine_state, stack, config)?;
}
}
}
}
Ok(line_editor)
}
fn add_menu(
line_editor: Reedline,
menu: &ParsedMenu,
engine_state: &EngineState,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
if let Value::Record { cols, vals, span } = &menu.menu_type {
let layout = extract_value("layout", cols, vals, span)?.into_string("", config);
match layout.as_str() {
"columnar" => add_columnar_menu(line_editor, menu, engine_state, stack, config),
"list" => add_list_menu(line_editor, menu, engine_state, stack, config),
"description" => add_description_menu(line_editor, menu, engine_state, stack, config),
_ => Err(ShellError::UnsupportedConfigValue(
"columnar, list or description".to_string(),
menu.menu_type.into_abbreviated_string(config),
menu.menu_type.span()?,
)),
}
} else {
Err(ShellError::UnsupportedConfigValue(
"only record type".to_string(),
menu.menu_type.into_abbreviated_string(config),
menu.menu_type.span()?,
))
}
}
// Adds a columnar menu to the editor engine
pub(crate) fn add_columnar_menu(
line_editor: Reedline,
menu: &ParsedMenu,
engine_state: &EngineState,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
let name = menu.name.into_string("", config);
let mut columnar_menu = ColumnarMenu::default().with_name(&name);
if let Value::Record { cols, vals, span } = &menu.menu_type {
columnar_menu = match extract_value("columns", cols, vals, span) {
Ok(columns) => {
let columns = columns.as_integer()?;
columnar_menu.with_columns(columns as u16)
}
Err(_) => columnar_menu,
};
help_menu = match config
.help_config
.get("description_rows")
.and_then(|value| value.as_integer().ok())
{
Some(value) => help_menu.with_description_rows(value as usize),
None => help_menu,
columnar_menu = match extract_value("col_width", cols, vals, span) {
Ok(col_width) => {
let col_width = col_width.as_integer()?;
columnar_menu.with_column_width(Some(col_width as usize))
}
Err(_) => columnar_menu.with_column_width(None),
};
help_menu = match config
.help_config
.get("text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => help_menu.with_text_style(lookup_ansi_color_style(&value)),
None => help_menu,
columnar_menu = match extract_value("col_padding", cols, vals, span) {
Ok(col_padding) => {
let col_padding = col_padding.as_integer()?;
columnar_menu.with_column_padding(col_padding as usize)
}
Err(_) => columnar_menu,
};
}
if let Value::Record { cols, vals, span } = &menu.style {
columnar_menu = match extract_value("text", cols, vals, span) {
Ok(text) => {
let text = text.into_string("", config);
columnar_menu.with_text_style(lookup_ansi_color_style(&text))
}
Err(_) => columnar_menu,
};
help_menu = match config
.help_config
.get("selected_text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => help_menu.with_selected_text_style(lookup_ansi_color_style(&value)),
None => help_menu,
columnar_menu = match extract_value("selected_text", cols, vals, span) {
Ok(selected) => {
let selected = selected.into_string("", config);
columnar_menu.with_selected_text_style(lookup_ansi_color_style(&selected))
}
Err(_) => columnar_menu,
};
help_menu = match config
.help_config
.get("description_text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => help_menu.with_description_text_style(lookup_ansi_color_style(&value)),
None => help_menu,
columnar_menu = match extract_value("description_text", cols, vals, span) {
Ok(description) => {
let description = description.into_string("", config);
columnar_menu.with_description_text_style(lookup_ansi_color_style(&description))
}
Err(_) => columnar_menu,
};
}
let marker = menu.marker.into_string("", config);
columnar_menu = columnar_menu.with_marker(marker);
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
columnar_menu = columnar_menu.with_only_buffer_difference(only_buffer_difference);
match &menu.source {
Value::Nothing { .. } => {
Ok(line_editor.with_menu(ReedlineMenu::EngineCompleter(Box::new(columnar_menu))))
}
Value::Block {
val,
captures,
span,
} => {
let menu_completer = NuMenuCompleter::new(
*val,
*span,
stack.captures_to_stack(captures),
engine_state.clone(),
only_buffer_difference,
);
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
menu: Box::new(columnar_menu),
completer: Box::new(menu_completer),
}))
}
_ => Err(ShellError::UnsupportedConfigValue(
"block or omitted value".to_string(),
menu.source.into_abbreviated_string(config),
menu.source.span()?,
)),
}
}
// Adds a search menu to the line editor
pub(crate) fn add_list_menu(
line_editor: Reedline,
menu: &ParsedMenu,
engine_state: &EngineState,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
let name = menu.name.into_string("", config);
let mut list_menu = ListMenu::default().with_name(&name);
if let Value::Record { cols, vals, span } = &menu.menu_type {
list_menu = match extract_value("page_size", cols, vals, span) {
Ok(page_size) => {
let page_size = page_size.as_integer()?;
list_menu.with_page_size(page_size as usize)
}
Err(_) => list_menu,
};
}
if let Value::Record { cols, vals, span } = &menu.style {
list_menu = match extract_value("text", cols, vals, span) {
Ok(text) => {
let text = text.into_string("", config);
list_menu.with_text_style(lookup_ansi_color_style(&text))
}
Err(_) => list_menu,
};
help_menu = match config
.help_config
.get("marker")
.and_then(|value| value.as_string().ok())
{
Some(value) => help_menu.with_marker(value),
None => help_menu,
list_menu = match extract_value("selected_text", cols, vals, span) {
Ok(selected) => {
let selected = selected.into_string("", config);
list_menu.with_selected_text_style(lookup_ansi_color_style(&selected))
}
Err(_) => list_menu,
};
line_editor.with_menu(Box::new(help_menu), Some(help_completer))
list_menu = match extract_value("description_text", cols, vals, span) {
Ok(description) => {
let description = description.into_string("", config);
list_menu.with_description_text_style(lookup_ansi_color_style(&description))
}
Err(_) => list_menu,
};
}
let marker = menu.marker.into_string("", config);
list_menu = list_menu.with_marker(marker);
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
list_menu = list_menu.with_only_buffer_difference(only_buffer_difference);
match &menu.source {
Value::Nothing { .. } => {
Ok(line_editor.with_menu(ReedlineMenu::HistoryMenu(Box::new(list_menu))))
}
Value::Block {
val,
captures,
span,
} => {
let menu_completer = NuMenuCompleter::new(
*val,
*span,
stack.captures_to_stack(captures),
engine_state.clone(),
only_buffer_difference,
);
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
menu: Box::new(list_menu),
completer: Box::new(menu_completer),
}))
}
_ => Err(ShellError::UnsupportedConfigValue(
"block or omitted value".to_string(),
menu.source.into_abbreviated_string(config),
menu.source.span()?,
)),
}
}
// Adds a description menu to the line editor
pub(crate) fn add_description_menu(
line_editor: Reedline,
menu: &ParsedMenu,
engine_state: &EngineState,
stack: &Stack,
config: &Config,
) -> Result<Reedline, ShellError> {
let name = menu.name.into_string("", config);
let mut description_menu = DescriptionMenu::default().with_name(&name);
if let Value::Record { cols, vals, span } = &menu.menu_type {
description_menu = match extract_value("columns", cols, vals, span) {
Ok(columns) => {
let columns = columns.as_integer()?;
description_menu.with_columns(columns as u16)
}
Err(_) => description_menu,
};
description_menu = match extract_value("col_width", cols, vals, span) {
Ok(col_width) => {
let col_width = col_width.as_integer()?;
description_menu.with_column_width(Some(col_width as usize))
}
Err(_) => description_menu.with_column_width(None),
};
description_menu = match extract_value("col_padding", cols, vals, span) {
Ok(col_padding) => {
let col_padding = col_padding.as_integer()?;
description_menu.with_column_padding(col_padding as usize)
}
Err(_) => description_menu,
};
description_menu = match extract_value("selection_rows", cols, vals, span) {
Ok(selection_rows) => {
let selection_rows = selection_rows.as_integer()?;
description_menu.with_selection_rows(selection_rows as u16)
}
Err(_) => description_menu,
};
description_menu = match extract_value("description_rows", cols, vals, span) {
Ok(description_rows) => {
let description_rows = description_rows.as_integer()?;
description_menu.with_description_rows(description_rows as usize)
}
Err(_) => description_menu,
};
}
if let Value::Record { cols, vals, span } = &menu.style {
description_menu = match extract_value("text", cols, vals, span) {
Ok(text) => {
let text = text.into_string("", config);
description_menu.with_text_style(lookup_ansi_color_style(&text))
}
Err(_) => description_menu,
};
description_menu = match extract_value("selected_text", cols, vals, span) {
Ok(selected) => {
let selected = selected.into_string("", config);
description_menu.with_selected_text_style(lookup_ansi_color_style(&selected))
}
Err(_) => description_menu,
};
description_menu = match extract_value("description_text", cols, vals, span) {
Ok(description) => {
let description = description.into_string("", config);
description_menu.with_description_text_style(lookup_ansi_color_style(&description))
}
Err(_) => description_menu,
};
}
let marker = menu.marker.into_string("", config);
description_menu = description_menu.with_marker(marker);
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
description_menu = description_menu.with_only_buffer_difference(only_buffer_difference);
match &menu.source {
Value::Nothing { .. } => {
let completer = Box::new(NuHelpCompleter::new(engine_state.clone()));
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
menu: Box::new(description_menu),
completer,
}))
}
Value::Block {
val,
captures,
span,
} => {
let menu_completer = NuMenuCompleter::new(
*val,
*span,
stack.captures_to_stack(captures),
engine_state.clone(),
only_buffer_difference,
);
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
menu: Box::new(description_menu),
completer: Box::new(menu_completer),
}))
}
_ => Err(ShellError::UnsupportedConfigValue(
"block or omitted value".to_string(),
menu.source.into_abbreviated_string(config),
menu.source.span()?,
)),
}
}
fn add_menu_keybindings(keybindings: &mut Keybindings) {

View File

@ -1,5 +1,5 @@
use crate::reedline_config::{add_completion_menu, add_help_menu, add_history_menu};
use crate::{prompt_update, reedline_config, NuHelpCompleter};
use crate::reedline_config::add_menus;
use crate::{prompt_update, reedline_config};
use crate::{
reedline_config::KeybindingsMode,
util::{eval_source, report_error},
@ -194,11 +194,14 @@ pub fn evaluate_repl(
info!("update reedline {}:{}:{}", file!(), line!(), column!());
}
line_editor = add_completion_menu(line_editor, &config);
line_editor = add_history_menu(line_editor, &config);
let help_completer = Box::new(NuHelpCompleter::new(engine_state.clone()));
line_editor = add_help_menu(line_editor, help_completer, &config);
line_editor = match add_menus(line_editor, engine_state, stack, &config) {
Ok(line_editor) => line_editor,
Err(e) => {
let working_set = StateWorkingSet::new(engine_state);
report_error(&working_set, &e);
Reedline::create()
}
};
if is_perf_true {
info!("setup colors {}:{}:{}", file!(), line!(), column!());

View File

@ -13,6 +13,17 @@ pub struct ParsedKeybinding {
pub mode: Value,
}
/// Definition of a parsed menu from the config object
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ParsedMenu {
pub name: Value,
pub marker: Value,
pub only_buffer_difference: Value,
pub style: Value,
pub menu_type: Value,
pub source: Value,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Config {
pub filesize_metric: bool,
@ -31,10 +42,8 @@ pub struct Config {
pub max_history_size: i64,
pub sync_history_on_enter: bool,
pub log_level: String,
pub menu_config: HashMap<String, Value>,
pub keybindings: Vec<ParsedKeybinding>,
pub history_config: HashMap<String, Value>,
pub help_config: HashMap<String, Value>,
pub menus: Vec<ParsedMenu>,
pub rm_always_trash: bool,
}
@ -57,10 +66,8 @@ impl Default for Config {
max_history_size: 1000,
sync_history_on_enter: true,
log_level: String::new(),
menu_config: HashMap::new(),
history_config: HashMap::new(),
help_config: HashMap::new(),
keybindings: Vec::new(),
menus: Vec::new(),
rm_always_trash: false,
}
}
@ -215,34 +222,20 @@ impl Value {
eprintln!("$config.log_level is not a string")
}
}
"menu_config" => {
if let Ok(map) = create_map(value, &config) {
config.menu_config = map;
} else {
eprintln!("$config.menu_config is not a record")
}
}
"history_config" => {
if let Ok(map) = create_map(value, &config) {
config.history_config = map;
} else {
eprintln!("$config.history_config is not a record")
}
}
"help_config" => {
if let Ok(map) = create_map(value, &config) {
config.help_config = map;
} else {
eprintln!("$config.help_config is not a record")
}
}
"keybindings" => {
if let Ok(keybindings) = create_keybindings(value, &config) {
config.keybindings = keybindings;
} else {
eprintln!("$config.keybindings is not a valid keybindings list")
"menus" => match create_menus(value, &config) {
Ok(map) => config.menus = map,
Err(e) => {
eprintln!("$config.menus is not a valid list of menus");
eprintln!("{:?}", e);
}
},
"keybindings" => match create_keybindings(value, &config) {
Ok(keybindings) => config.keybindings = keybindings,
Err(e) => {
eprintln!("$config.keybindings is not a valid keybindings list");
eprintln!("{:?}", e);
}
},
x => {
eprintln!("$config.{} is an unknown config setting", x)
}
@ -310,18 +303,19 @@ fn create_keybindings(value: &Value, config: &Config) -> Result<Vec<ParsedKeybin
match value {
Value::Record { cols, vals, span } => {
// Finding the modifier value in the record
let modifier = extract_value("modifier", cols, vals, span)?;
let keycode = extract_value("keycode", cols, vals, span)?;
let mode = extract_value("mode", cols, vals, span)?;
let event = extract_value("event", cols, vals, span)?;
let modifier = extract_value("modifier", cols, vals, span)?.clone();
let keycode = extract_value("keycode", cols, vals, span)?.clone();
let mode = extract_value("mode", cols, vals, span)?.clone();
let event = extract_value("event", cols, vals, span)?.clone();
let keybinding = ParsedKeybinding {
modifier: modifier.clone(),
keycode: keycode.clone(),
mode: mode.clone(),
event: event.clone(),
modifier,
keycode,
mode,
event,
};
// We return a menu to be able to do recursion on the same function
Ok(vec![keybinding])
}
Value::List { vals, .. } => {
@ -341,6 +335,49 @@ fn create_keybindings(value: &Value, config: &Config) -> Result<Vec<ParsedKeybin
}
}
// Parses the config object to extract the strings that will compose a keybinding for reedline
pub fn create_menus(value: &Value, config: &Config) -> Result<Vec<ParsedMenu>, ShellError> {
match value {
Value::Record { cols, vals, span } => {
// Finding the modifier value in the record
let name = extract_value("name", cols, vals, span)?.clone();
let marker = extract_value("marker", cols, vals, span)?.clone();
let only_buffer_difference =
extract_value("only_buffer_difference", cols, vals, span)?.clone();
let style = extract_value("style", cols, vals, span)?.clone();
let menu_type = extract_value("type", cols, vals, span)?.clone();
// Source is an optional value
let source = match extract_value("source", cols, vals, span) {
Ok(source) => source.clone(),
Err(_) => Value::Nothing { span: *span },
};
let menu = ParsedMenu {
name,
only_buffer_difference,
marker,
style,
menu_type,
source,
};
Ok(vec![menu])
}
Value::List { vals, .. } => {
let res = vals
.iter()
.map(|inner_value| create_menus(inner_value, config))
.collect::<Result<Vec<Vec<ParsedMenu>>, ShellError>>();
let res = res?.into_iter().flatten().collect::<Vec<ParsedMenu>>();
Ok(res)
}
_ => Ok(Vec::new()),
}
}
pub fn extract_value<'record>(
name: &str,
cols: &'record [String],

View File

@ -198,32 +198,125 @@ let $config = {
edit_mode: emacs # emacs, vi
max_history_size: 10000 # Session has to be reloaded for this to take effect
sync_history_on_enter: true # Enable to share the history between multiple sessions, else you have to close the session to persist history to file
menu_config: {
menus: [
# Configuration for default nushell menus
# Note the lack of souce parameter
{
name: completion_menu
only_buffer_difference: false
marker: "| "
type: {
layout: columnar
columns: 4
col_width: 20 # Optional value. If missing all the screen width is used to calculate column width
col_padding: 2
text_style: green
selected_text_style: green_reverse
marker: "| "
}
history_config: {
page_size: 10
selector: "!"
text_style: green
selected_text_style: green_reverse
style: {
text: green
selected_text: green_reverse
description_text: yellow
}
}
{
name: history_menu
only_buffer_difference: true
marker: "? "
type: {
layout: list
page_size: 10
}
help_config: {
style: {
text: green
selected_text: green_reverse
description_text: yellow
}
}
{
name: help_menu
only_buffer_difference: true
marker: "? "
type: {
layout: description
columns: 4
col_width: 20 # Optional value. If missing all the screen width is used to calculate column width
col_padding: 2
selection_rows: 4
description_rows: 10
text_style: green
selected_text_style: green_reverse
description_text_style: yellow
marker: "? "
}
style: {
text: green
selected_text: green_reverse
description_text: yellow
}
}
# Example of extra menus created using a nushell source
# Use the source field to create a list of records that populates
# the menu
{
name: commands_menu
only_buffer_difference: false
marker: "# "
type: {
layout: columnar
columns: 4
col_width: 20
col_padding: 2
}
style: {
text: green
selected_text: green_reverse
description_text: yellow
}
source: { |buffer, position|
$nu.scope.commands
| where command =~ $buffer
| each { |it| {value: $it.command description: $it.usage} }
}
}
{
name: vars_menu
only_buffer_difference: true
marker: "# "
type: {
layout: list
page_size: 10
}
style: {
text: green
selected_text: green_reverse
description_text: yellow
}
source: { |buffer, position|
$nu.scope.vars
| where name =~ $buffer
| sort-by name
| each { |it| {value: $it.name description: $it.type} }
}
}
{
name: commands_with_description
only_buffer_difference: true
marker: "# "
type: {
layout: description
columns: 4
col_width: 20
col_padding: 2
selection_rows: 4
description_rows: 10
}
style: {
text: green
selected_text: green_reverse
description_text: yellow
}
source: { |buffer, position|
$nu.scope.commands
| where command =~ $buffer
| each { |it| {value: $it.command description: $it.usage} }
}
}
]
keybindings: [
{
name: completion_menu
@ -268,5 +361,27 @@ let $config = {
]
}
}
# Keybindings used to trigger the user defined menus
{
name: commands_menu
modifier: control
keycode: char_t
mode: [emacs, vi_normal, vi_insert]
event: { send: menu name: commands_menu }
}
{
name: commands_menu
modifier: control
keycode: char_y
mode: [emacs, vi_normal, vi_insert]
event: { send: menu name: vars_menu }
}
{
name: commands_with_description
modifier: control
keycode: char_u
mode: [emacs, vi_normal, vi_insert]
event: { send: menu name: commands_with_description }
}
]
}