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]] [[package]]
name = "reedline" name = "reedline"
version = "0.3.1" 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 = [ dependencies = [
"chrono", "chrono",
"crossterm", "crossterm",

View File

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

View File

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

View File

@ -1,9 +1,8 @@
use { use {
crate::help_completions::{EXAMPLE_MARKER, EXAMPLE_NEW_LINE},
nu_ansi_term::{ansi::RESET, Style}, nu_ansi_term::{ansi::RESET, Style},
reedline::{ reedline::{
menu_functions::string_difference, Completer, History, LineBuffer, Menu, MenuEvent, menu_functions::string_difference, Completer, LineBuffer, Menu, MenuEvent, MenuTextStyle,
MenuTextStyle, Painter, Suggestion, Painter, Suggestion,
}, },
}; };
@ -48,7 +47,10 @@ struct WorkingDetails {
} }
/// Completion menu definition /// Completion menu definition
pub struct NuHelpMenu { pub struct DescriptionMenu {
/// Menu name
name: String,
/// Menu status
active: bool, active: bool,
/// Menu coloring /// Menu coloring
color: MenuTextStyle, color: MenuTextStyle,
@ -80,11 +82,15 @@ pub struct NuHelpMenu {
show_examples: bool, show_examples: bool,
/// Skipped description rows /// Skipped description rows
skipped_rows: usize, 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 { fn default() -> Self {
Self { Self {
name: "description_menu".to_string(),
active: false, active: false,
color: MenuTextStyle::default(), color: MenuTextStyle::default(),
default_details: DefaultMenuDetails::default(), default_details: DefaultMenuDetails::default(),
@ -100,11 +106,19 @@ impl Default for NuHelpMenu {
example_index: None, example_index: None,
show_examples: true, show_examples: true,
skipped_rows: 0, 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 /// Menu builder with new value for text style
pub fn with_text_style(mut self, text_style: Style) -> Self { pub fn with_text_style(mut self, text_style: Style) -> Self {
self.color.text_style = text_style; self.color.text_style = text_style;
@ -159,6 +173,15 @@ impl NuHelpMenu {
self 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 /// Move menu cursor to the next element
fn move_next(&mut self) { fn move_next(&mut self) {
let mut new_col = self.col_pos + 1; let mut new_col = self.col_pos + 1;
@ -279,19 +302,11 @@ impl NuHelpMenu {
/// Update list of examples from the actual value /// Update list of examples from the actual value
fn update_examples(&mut self) { fn update_examples(&mut self) {
let examples = self self.examples = self
.get_value() .get_value()
.and_then(|suggestion| suggestion.description) .and_then(|suggestion| suggestion.extra)
.unwrap_or_else(|| "".to_string()) .unwrap_or_default();
.lines()
.filter(|line| line.starts_with(EXAMPLE_MARKER))
.map(|line| {
line.replace(EXAMPLE_MARKER, "")
.replace(EXAMPLE_NEW_LINE, "\r\n")
})
.collect::<Vec<String>>();
self.examples = examples;
self.example_index = None; self.example_index = None;
} }
@ -359,7 +374,6 @@ impl NuHelpMenu {
.and_then(|suggestion| suggestion.description) .and_then(|suggestion| suggestion.description)
.unwrap_or_else(|| "".to_string()) .unwrap_or_else(|| "".to_string())
.lines() .lines()
.filter(|line| !line.starts_with(EXAMPLE_MARKER))
.skip(self.skipped_rows) .skip(self.skipped_rows)
.take(self.working_details.description_rows) .take(self.working_details.description_rows)
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
@ -420,10 +434,10 @@ impl NuHelpMenu {
} }
} }
impl Menu for NuHelpMenu { impl Menu for DescriptionMenu {
/// Menu name /// Menu name
fn name(&self) -> &str { fn name(&self) -> &str {
"help_menu" self.name.as_str()
} }
/// Menu indicator /// Menu indicator
@ -436,17 +450,16 @@ impl Menu for NuHelpMenu {
self.active 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 { fn can_quick_complete(&self) -> bool {
false false
} }
/// The help menu does not need to partially complete /// The menu does not need to partially complete
fn can_partially_complete( fn can_partially_complete(
&mut self, &mut self,
_values_updated: bool, _values_updated: bool,
_line_buffer: &mut LineBuffer, _line_buffer: &mut LineBuffer,
_history: &dyn History,
_completer: &dyn Completer, _completer: &dyn Completer,
) -> bool { ) -> bool {
false false
@ -468,29 +481,20 @@ impl Menu for NuHelpMenu {
} }
/// Updates menu values /// Updates menu values
fn update_values( fn update_values(&mut self, line_buffer: &mut LineBuffer, completer: &dyn Completer) {
&mut self, if self.only_buffer_difference {
line_buffer: &mut LineBuffer, if let Some(old_string) = &self.input {
_history: &dyn History, let (start, input) = string_difference(line_buffer.get_buffer(), old_string);
completer: &dyn Completer, if !input.is_empty() {
) { self.reset_position();
if let Some(old_string) = &self.input { self.values = completer.complete(input, start);
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();
} }
} 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( fn update_working_details(
&mut self, &mut self,
line_buffer: &mut LineBuffer, line_buffer: &mut LineBuffer,
history: &dyn History,
completer: &dyn Completer, completer: &dyn Completer,
painter: &Painter, painter: &Painter,
) { ) {
@ -558,12 +561,12 @@ impl Menu for NuHelpMenu {
MenuEvent::Activate(_) => { MenuEvent::Activate(_) => {
self.reset_position(); self.reset_position();
self.input = Some(line_buffer.get_buffer().to_string()); 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::Deactivate => self.active = false,
MenuEvent::Edit(_) => { MenuEvent::Edit(_) => {
self.reset_position(); self.reset_position();
self.update_values(line_buffer, history, completer); self.update_values(line_buffer, completer);
self.update_examples() self.update_examples()
} }
MenuEvent::NextElement => { MenuEvent::NextElement => {
@ -607,7 +610,6 @@ impl Menu for NuHelpMenu {
.and_then(|suggestion| suggestion.description) .and_then(|suggestion| suggestion.description)
.unwrap_or_else(|| "".to_string()) .unwrap_or_else(|| "".to_string())
.lines() .lines()
.filter(|line| !line.starts_with(EXAMPLE_MARKER))
.count(); .count();
let allowed_skips = let allowed_skips =
@ -627,20 +629,24 @@ impl Menu for NuHelpMenu {
/// The buffer gets replaced in the Span location /// The buffer gets replaced in the Span location
fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) { fn replace_in_buffer(&self, line_buffer: &mut LineBuffer) {
if let Some(Suggestion { value, span, .. }) = self.get_value() { 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 string_len = if let Some(example_index) = self.example_index {
let example = self let example = self
.examples .examples
.get(example_index) .get(example_index)
.expect("the example index is always checked"); .expect("the example index is always checked");
line_buffer.replace(span.start..span.end, example);
line_buffer.replace(start..end, example);
example.len() example.len()
} else { } else {
line_buffer.replace(span.start..span.end, &value); line_buffer.replace(start..end, &value);
value.len() value.len()
}; };
let mut offset = line_buffer.insertion_point(); 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); 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 nu_protocol::{engine::EngineState, levenshtein_distance};
use reedline::{Completer, Suggestion}; use reedline::{Completer, Suggestion};
pub const EXAMPLE_MARKER: &str = ">>>>>>"; pub struct NuHelpCompleter(EngineState);
pub const EXAMPLE_NEW_LINE: &str = "%%%%%%";
pub struct NuHelpCompleter {
engine_state: EngineState,
}
impl NuHelpCompleter { impl NuHelpCompleter {
pub fn new(engine_state: EngineState) -> Self { pub fn new(engine_state: EngineState) -> Self {
Self { engine_state } Self(engine_state)
} }
fn completion_helper(&self, line: &str, _pos: usize) -> Vec<Suggestion> { fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
let full_commands = self.engine_state.get_signatures_with_examples(false); let full_commands = self.0.get_signatures_with_examples(false);
//Vec<(Signature, Vec<Example>, bool, bool)> { //Vec<(Signature, Vec<Example>, bool, bool)> {
let mut commands = full_commands let mut commands = full_commands
@ -83,20 +78,18 @@ impl NuHelpCompleter {
} }
} }
for example in examples { let extra: Vec<String> = examples
long_desc.push_str(&format!( .iter()
"{}{}\r\n", .map(|example| example.example.to_string())
EXAMPLE_MARKER, .collect();
example.example.replace('\n', EXAMPLE_NEW_LINE)
))
}
Suggestion { Suggestion {
value: sig.name.clone(), value: sig.name.clone(),
description: Some(long_desc), description: Some(long_desc),
extra: Some(extra),
span: reedline::Span { span: reedline::Span {
start: 0, start: pos,
end: sig.name.len(), 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 crossterm::event::{KeyCode, KeyModifiers};
use nu_color_config::lookup_ansi_color_style; 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::{ use reedline::{
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings, 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 const DEFAULT_COMPLETION_MENU: &str = r#"
// stored in the config variable {
pub(crate) fn add_completion_menu(line_editor: Reedline, config: &Config) -> Reedline { name: completion_menu
let mut completion_menu = CompletionMenu::default(); 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 const DEFAULT_HISTORY_MENU: &str = r#"
.menu_config {
.get("columns") name: history_menu
.and_then(|value| value.as_integer().ok()) only_buffer_difference: true
{ marker: "? "
Some(value) => completion_menu.with_columns(value as u16), type: {
None => completion_menu, layout: list
}; page_size: 10
}
style: {
text: green,
selected_text: green_reverse
description_text: yellow
}
}"#;
completion_menu = completion_menu.with_column_width( const DEFAULT_HELP_MENU: &str = r#"
config {
.menu_config name: help_menu
.get("col_width") only_buffer_difference: true
.and_then(|value| value.as_integer().ok()) marker: "? "
.map(|value| value as usize), 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 // Adds all menus to line editor
.menu_config pub(crate) fn add_menus(
.get("col_padding") mut line_editor: Reedline,
.and_then(|value| value.as_integer().ok()) engine_state: &EngineState,
{ stack: &Stack,
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<dyn Completer>,
config: &Config, config: &Config,
) -> Reedline { ) -> Result<Reedline, ShellError> {
let mut help_menu = NuHelpMenu::default(); line_editor = line_editor.clear_menus();
help_menu = match config for menu in &config.menus {
.help_config line_editor = add_menu(line_editor, menu, engine_state, stack, config)?
.get("columns") }
.and_then(|value| value.as_integer().ok())
{
Some(value) => help_menu.with_columns(value as u16),
None => help_menu,
};
help_menu = help_menu.with_column_width( // Checking if the default menus have been added from the config file
config let default_menus = vec![
.help_config ("completion_menu", DEFAULT_COMPLETION_MENU),
.get("col_width") ("history_menu", DEFAULT_HISTORY_MENU),
.and_then(|value| value.as_integer().ok()) ("help_menu", DEFAULT_HELP_MENU),
.map(|value| value as usize), ];
);
help_menu = match config for (name, definition) in default_menus {
.help_config if !config
.get("col_padding") .menus
.and_then(|value| value.as_integer().ok()) .iter()
{ .any(|menu| menu.name.into_string("", config) == name)
Some(value) => help_menu.with_column_padding(value as usize), {
None => help_menu, 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 (output, working_set.render())
.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,
};
help_menu = match config let mut temp_stack = Stack::new();
.help_config let input = Value::nothing(Span::test_data()).into_pipeline_data();
.get("description_rows") let res = eval_block(engine_state, &mut temp_stack, &block, input, false, false)?;
.and_then(|value| value.as_integer().ok())
{
Some(value) => help_menu.with_description_rows(value as usize),
None => help_menu,
};
help_menu = match config if let PipelineData::Value(value, None) = res {
.help_config for menu in create_menus(&value, config)? {
.get("text_style") line_editor = add_menu(line_editor, &menu, engine_state, stack, config)?;
.and_then(|value| value.as_string().ok()) }
{ }
Some(value) => help_menu.with_text_style(lookup_ansi_color_style(&value)), }
None => help_menu, }
};
help_menu = match config Ok(line_editor)
.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,
};
help_menu = match config fn add_menu(
.help_config line_editor: Reedline,
.get("description_text_style") menu: &ParsedMenu,
.and_then(|value| value.as_string().ok()) engine_state: &EngineState,
{ stack: &Stack,
Some(value) => help_menu.with_description_text_style(lookup_ansi_color_style(&value)), config: &Config,
None => help_menu, ) -> Result<Reedline, ShellError> {
}; 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 match layout.as_str() {
.help_config "columnar" => add_columnar_menu(line_editor, menu, engine_state, stack, config),
.get("marker") "list" => add_list_menu(line_editor, menu, engine_state, stack, config),
.and_then(|value| value.as_string().ok()) "description" => add_description_menu(line_editor, menu, engine_state, stack, config),
{ _ => Err(ShellError::UnsupportedConfigValue(
Some(value) => help_menu.with_marker(value), "columnar, list or description".to_string(),
None => help_menu, 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<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,
};
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<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,
};
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<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) { 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::reedline_config::add_menus;
use crate::{prompt_update, reedline_config, NuHelpCompleter}; use crate::{prompt_update, reedline_config};
use crate::{ use crate::{
reedline_config::KeybindingsMode, reedline_config::KeybindingsMode,
util::{eval_source, report_error}, util::{eval_source, report_error},
@ -194,11 +194,14 @@ pub fn evaluate_repl(
info!("update reedline {}:{}:{}", file!(), line!(), column!()); info!("update reedline {}:{}:{}", file!(), line!(), column!());
} }
line_editor = add_completion_menu(line_editor, &config); line_editor = match add_menus(line_editor, engine_state, stack, &config) {
line_editor = add_history_menu(line_editor, &config); Ok(line_editor) => line_editor,
Err(e) => {
let help_completer = Box::new(NuHelpCompleter::new(engine_state.clone())); let working_set = StateWorkingSet::new(engine_state);
line_editor = add_help_menu(line_editor, help_completer, &config); report_error(&working_set, &e);
Reedline::create()
}
};
if is_perf_true { if is_perf_true {
info!("setup colors {}:{}:{}", file!(), line!(), column!()); info!("setup colors {}:{}:{}", file!(), line!(), column!());

View File

@ -13,6 +13,17 @@ pub struct ParsedKeybinding {
pub mode: Value, 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)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Config { pub struct Config {
pub filesize_metric: bool, pub filesize_metric: bool,
@ -31,10 +42,8 @@ pub struct Config {
pub max_history_size: i64, pub max_history_size: i64,
pub sync_history_on_enter: bool, pub sync_history_on_enter: bool,
pub log_level: String, pub log_level: String,
pub menu_config: HashMap<String, Value>,
pub keybindings: Vec<ParsedKeybinding>, pub keybindings: Vec<ParsedKeybinding>,
pub history_config: HashMap<String, Value>, pub menus: Vec<ParsedMenu>,
pub help_config: HashMap<String, Value>,
pub rm_always_trash: bool, pub rm_always_trash: bool,
} }
@ -57,10 +66,8 @@ impl Default for Config {
max_history_size: 1000, max_history_size: 1000,
sync_history_on_enter: true, sync_history_on_enter: true,
log_level: String::new(), log_level: String::new(),
menu_config: HashMap::new(),
history_config: HashMap::new(),
help_config: HashMap::new(),
keybindings: Vec::new(), keybindings: Vec::new(),
menus: Vec::new(),
rm_always_trash: false, rm_always_trash: false,
} }
} }
@ -215,34 +222,20 @@ impl Value {
eprintln!("$config.log_level is not a string") eprintln!("$config.log_level is not a string")
} }
} }
"menu_config" => { "menus" => match create_menus(value, &config) {
if let Ok(map) = create_map(value, &config) { Ok(map) => config.menus = map,
config.menu_config = map; Err(e) => {
} else { eprintln!("$config.menus is not a valid list of menus");
eprintln!("$config.menu_config is not a record") eprintln!("{:?}", e);
} }
} },
"history_config" => { "keybindings" => match create_keybindings(value, &config) {
if let Ok(map) = create_map(value, &config) { Ok(keybindings) => config.keybindings = keybindings,
config.history_config = map; Err(e) => {
} else { eprintln!("$config.keybindings is not a valid keybindings list");
eprintln!("$config.history_config is not a record") 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 => { x => {
eprintln!("$config.{} is an unknown config setting", 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 { match value {
Value::Record { cols, vals, span } => { Value::Record { cols, vals, span } => {
// Finding the modifier value in the record // Finding the modifier value in the record
let modifier = extract_value("modifier", cols, vals, span)?; let modifier = extract_value("modifier", cols, vals, span)?.clone();
let keycode = extract_value("keycode", cols, vals, span)?; let keycode = extract_value("keycode", cols, vals, span)?.clone();
let mode = extract_value("mode", cols, vals, span)?; let mode = extract_value("mode", cols, vals, span)?.clone();
let event = extract_value("event", cols, vals, span)?; let event = extract_value("event", cols, vals, span)?.clone();
let keybinding = ParsedKeybinding { let keybinding = ParsedKeybinding {
modifier: modifier.clone(), modifier,
keycode: keycode.clone(), keycode,
mode: mode.clone(), mode,
event: event.clone(), event,
}; };
// We return a menu to be able to do recursion on the same function
Ok(vec![keybinding]) Ok(vec![keybinding])
} }
Value::List { vals, .. } => { 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>( pub fn extract_value<'record>(
name: &str, name: &str,
cols: &'record [String], cols: &'record [String],

View File

@ -198,32 +198,125 @@ let $config = {
edit_mode: emacs # emacs, vi edit_mode: emacs # emacs, vi
max_history_size: 10000 # Session has to be reloaded for this to take effect 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 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: [
columns: 4 # Configuration for default nushell menus
col_width: 20 # Optional value. If missing all the screen width is used to calculate column width # Note the lack of souce parameter
col_padding: 2 {
text_style: green name: completion_menu
selected_text_style: green_reverse only_buffer_difference: false
marker: "| " marker: "| "
} type: {
history_config: { layout: columnar
page_size: 10 columns: 4
selector: "!" col_width: 20 # Optional value. If missing all the screen width is used to calculate column width
text_style: green col_padding: 2
selected_text_style: green_reverse }
marker: "? " style: {
} text: green
help_config: { selected_text: green_reverse
columns: 4 description_text: yellow
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 name: history_menu
text_style: green only_buffer_difference: true
selected_text_style: green_reverse marker: "? "
description_text_style: yellow type: {
marker: "? " 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: [ keybindings: [
{ {
name: completion_menu 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 }
}
] ]
} }