From 608b6f3634dc47ed37455414758147c65e7feb41 Mon Sep 17 00:00:00 2001 From: Fernando Herrera Date: Mon, 4 Apr 2022 15:54:48 +0100 Subject: [PATCH] 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 --- Cargo.lock | 2 +- crates/nu-cli/src/completions.rs | 12 + crates/nu-cli/src/lib.rs | 6 +- .../description_menu.rs} | 110 +-- .../src/{ => menus}/help_completions.rs | 29 +- crates/nu-cli/src/menus/menu_completions.rs | 167 +++++ crates/nu-cli/src/menus/mod.rs | 7 + crates/nu-cli/src/reedline_config.rs | 626 ++++++++++++------ crates/nu-cli/src/repl.rs | 17 +- crates/nu-protocol/src/config.rs | 117 ++-- docs/sample_config/default_config.nu | 167 ++++- 11 files changed, 916 insertions(+), 344 deletions(-) rename crates/nu-cli/src/{help_menu.rs => menus/description_menu.rs} (90%) rename crates/nu-cli/src/{ => menus}/help_completions.rs (82%) create mode 100644 crates/nu-cli/src/menus/menu_completions.rs create mode 100644 crates/nu-cli/src/menus/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 6f38a3ed9..2377465da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/nu-cli/src/completions.rs b/crates/nu-cli/src/completions.rs index 30c60a5fb..3baceecea 100644 --- a/crates/nu-cli/src/completions.rs +++ b/crates/nu-cli/src/completions.rs @@ -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, diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index af9b0867c..4ff922b7d 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -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; diff --git a/crates/nu-cli/src/help_menu.rs b/crates/nu-cli/src/menus/description_menu.rs similarity index 90% rename from crates/nu-cli/src/help_menu.rs rename to crates/nu-cli/src/menus/description_menu.rs index a36e54397..69da0fac8 100644 --- a/crates/nu-cli/src/help_menu.rs +++ b/crates/nu-cli/src/menus/description_menu.rs @@ -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::>(); + .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::>() @@ -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,29 +481,20 @@ impl Menu for NuHelpMenu { } /// Updates menu values - fn update_values( - &mut self, - line_buffer: &mut LineBuffer, - _history: &dyn History, - completer: &dyn Completer, - ) { - 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(); + 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, 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(); } } @@ -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); } } diff --git a/crates/nu-cli/src/help_completions.rs b/crates/nu-cli/src/menus/help_completions.rs similarity index 82% rename from crates/nu-cli/src/help_completions.rs rename to crates/nu-cli/src/menus/help_completions.rs index f385cad24..bd7029f0d 100644 --- a/crates/nu-cli/src/help_completions.rs +++ b/crates/nu-cli/src/menus/help_completions.rs @@ -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 { - let full_commands = self.engine_state.get_signatures_with_examples(false); + fn completion_helper(&self, line: &str, pos: usize) -> Vec { + let full_commands = self.0.get_signatures_with_examples(false); //Vec<(Signature, Vec, 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 = 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(), }, } }) diff --git a/crates/nu-cli/src/menus/menu_completions.rs b/crates/nu-cli/src/menus/menu_completions.rs new file mode 100644 index 000000000..394590a1e --- /dev/null +++ b/crates/nu-cli/src/menus/menu_completions.rs @@ -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 { + 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 { + 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 = 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(), + }, + }], + } +} diff --git a/crates/nu-cli/src/menus/mod.rs b/crates/nu-cli/src/menus/mod.rs new file mode 100644 index 000000000..234134895 --- /dev/null +++ b/crates/nu-cli/src/menus/mod.rs @@ -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; diff --git a/crates/nu-cli/src/reedline_config.rs b/crates/nu-cli/src/reedline_config.rs index 20cc6e88e..04f98ec4b 100644 --- a/crates/nu-cli/src/reedline_config.rs +++ b/crates/nu-cli/src/reedline_config.rs @@ -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(); +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 + } + style: { + text: green, + selected_text: green_reverse + description_text: yellow + } +}"#; - 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, - }; +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 + } +}"#; - 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), - ); +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 + } +}"#; - 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) - } - None => history_menu, - }; - - 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, - }; - - 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, - }; - - 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, +// 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 { + line_editor = line_editor.clear_menus(); - help_menu = match config - .help_config - .get("columns") - .and_then(|value| value.as_integer().ok()) - { - Some(value) => help_menu.with_columns(value as u16), - None => help_menu, - }; + for menu in &config.menus { + line_editor = add_menu(line_editor, menu, engine_state, stack, config)? + } - 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), - ); + // 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), + ]; - 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, - }; + for (name, definition) in default_menus { + if !config + .menus + .iter() + .any(|menu| menu.name.into_string("", config) == name) + { + 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("selection_rows") - .and_then(|value| value.as_integer().ok()) - { - Some(value) => help_menu.with_selection_rows(value as u16), - None => help_menu, - }; + (output, working_set.render()) + }; - 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, - }; + 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)?; - 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, - }; + 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)?; + } + } + } + } - 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, - }; + Ok(line_editor) +} - 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, - }; +fn add_menu( + line_editor: Reedline, + menu: &ParsedMenu, + engine_state: &EngineState, + stack: &Stack, + config: &Config, +) -> Result { + if let Value::Record { cols, vals, span } = &menu.menu_type { + let layout = extract_value("layout", cols, vals, span)?.into_string("", config); - 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, - }; + 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()?, + )) + } +} - line_editor.with_menu(Box::new(help_menu), Some(help_completer)) +// 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 { + 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, + }; + + 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), + }; + + 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, + }; + + 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, + }; + + 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 { + 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, + }; + + 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, + }; + + 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 { + 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) { diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index d1df2ce2b..cc11fb09f 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -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!()); diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index d237cbc02..d0b137d0e 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -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, pub keybindings: Vec, - pub history_config: HashMap, - pub help_config: HashMap, + pub menus: Vec, 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") + "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); } - } - "history_config" => { - if let Ok(map) = create_map(value, &config) { - config.history_config = map; - } else { - eprintln!("$config.history_config is not a record") + }, + "keybindings" => match create_keybindings(value, &config) { + Ok(keybindings) => config.keybindings = keybindings, + Err(e) => { + eprintln!("$config.keybindings is not a valid keybindings list"); + eprintln!("{:?}", e); } - } - "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") - } - } + }, x => { eprintln!("$config.{} is an unknown config setting", x) } @@ -310,18 +303,19 @@ fn create_keybindings(value: &Value, config: &Config) -> Result { // 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 Result, 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::>, ShellError>>(); + + let res = res?.into_iter().flatten().collect::>(); + + Ok(res) + } + _ => Ok(Vec::new()), + } +} + pub fn extract_value<'record>( name: &str, cols: &'record [String], diff --git a/docs/sample_config/default_config.nu b/docs/sample_config/default_config.nu index 1f117251f..1a87c81a3 100644 --- a/docs/sample_config/default_config.nu +++ b/docs/sample_config/default_config.nu @@ -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: { - 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 - marker: "? " - } - help_config: { - 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: "? " - } + 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 + } + style: { + text: green + selected_text: green_reverse + description_text: yellow + } + } + { + name: history_menu + only_buffer_difference: true + marker: "? " + type: { + layout: list + page_size: 10 + } + 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 + } + 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 } + } ] }