mirror of
https://github.com/nushell/nushell
synced 2024-11-10 07:04:13 +00:00
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:
parent
a86e6ce89b
commit
608b6f3634
11 changed files with 916 additions and 344 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use {
|
||||
crate::help_completions::{EXAMPLE_MARKER, EXAMPLE_NEW_LINE},
|
||||
nu_ansi_term::{ansi::RESET, Style},
|
||||
reedline::{
|
||||
menu_functions::string_difference, Completer, History, LineBuffer, Menu, MenuEvent,
|
||||
MenuTextStyle, Painter, Suggestion,
|
||||
menu_functions::string_difference, Completer, LineBuffer, Menu, MenuEvent, MenuTextStyle,
|
||||
Painter, Suggestion,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -48,7 +47,10 @@ struct WorkingDetails {
|
|||
}
|
||||
|
||||
/// Completion menu definition
|
||||
pub struct NuHelpMenu {
|
||||
pub struct DescriptionMenu {
|
||||
/// Menu name
|
||||
name: String,
|
||||
/// Menu status
|
||||
active: bool,
|
||||
/// Menu coloring
|
||||
color: MenuTextStyle,
|
||||
|
@ -80,11 +82,15 @@ pub struct NuHelpMenu {
|
|||
show_examples: bool,
|
||||
/// Skipped description rows
|
||||
skipped_rows: usize,
|
||||
/// Calls the completer using only the line buffer difference difference
|
||||
/// after the menu was activated
|
||||
only_buffer_difference: bool,
|
||||
}
|
||||
|
||||
impl Default for NuHelpMenu {
|
||||
impl Default for DescriptionMenu {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "description_menu".to_string(),
|
||||
active: false,
|
||||
color: MenuTextStyle::default(),
|
||||
default_details: DefaultMenuDetails::default(),
|
||||
|
@ -100,11 +106,19 @@ impl Default for NuHelpMenu {
|
|||
example_index: None,
|
||||
show_examples: true,
|
||||
skipped_rows: 0,
|
||||
only_buffer_difference: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NuHelpMenu {
|
||||
// Menu configuration
|
||||
impl DescriptionMenu {
|
||||
/// Menu builder with new name
|
||||
pub fn with_name(mut self, name: &str) -> Self {
|
||||
self.name = name.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new value for text style
|
||||
pub fn with_text_style(mut self, text_style: Style) -> Self {
|
||||
self.color.text_style = text_style;
|
||||
|
@ -159,6 +173,15 @@ impl NuHelpMenu {
|
|||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new only buffer difference
|
||||
pub fn with_only_buffer_difference(mut self, only_buffer_difference: bool) -> Self {
|
||||
self.only_buffer_difference = only_buffer_difference;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Menu functionality
|
||||
impl DescriptionMenu {
|
||||
/// Move menu cursor to the next element
|
||||
fn move_next(&mut self) {
|
||||
let mut new_col = self.col_pos + 1;
|
||||
|
@ -279,19 +302,11 @@ impl NuHelpMenu {
|
|||
|
||||
/// Update list of examples from the actual value
|
||||
fn update_examples(&mut self) {
|
||||
let examples = self
|
||||
self.examples = self
|
||||
.get_value()
|
||||
.and_then(|suggestion| suggestion.description)
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
.lines()
|
||||
.filter(|line| line.starts_with(EXAMPLE_MARKER))
|
||||
.map(|line| {
|
||||
line.replace(EXAMPLE_MARKER, "")
|
||||
.replace(EXAMPLE_NEW_LINE, "\r\n")
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
.and_then(|suggestion| suggestion.extra)
|
||||
.unwrap_or_default();
|
||||
|
||||
self.examples = examples;
|
||||
self.example_index = None;
|
||||
}
|
||||
|
||||
|
@ -359,7 +374,6 @@ impl NuHelpMenu {
|
|||
.and_then(|suggestion| suggestion.description)
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
.lines()
|
||||
.filter(|line| !line.starts_with(EXAMPLE_MARKER))
|
||||
.skip(self.skipped_rows)
|
||||
.take(self.working_details.description_rows)
|
||||
.collect::<Vec<&str>>()
|
||||
|
@ -420,10 +434,10 @@ impl NuHelpMenu {
|
|||
}
|
||||
}
|
||||
|
||||
impl Menu for NuHelpMenu {
|
||||
impl Menu for DescriptionMenu {
|
||||
/// Menu name
|
||||
fn name(&self) -> &str {
|
||||
"help_menu"
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
/// Menu indicator
|
||||
|
@ -436,17 +450,16 @@ impl Menu for NuHelpMenu {
|
|||
self.active
|
||||
}
|
||||
|
||||
/// The help menu stays active even with one record
|
||||
/// The menu stays active even with one record
|
||||
fn can_quick_complete(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The help menu does not need to partially complete
|
||||
/// The menu does not need to partially complete
|
||||
fn can_partially_complete(
|
||||
&mut self,
|
||||
_values_updated: bool,
|
||||
_line_buffer: &mut LineBuffer,
|
||||
_history: &dyn History,
|
||||
_completer: &dyn Completer,
|
||||
) -> bool {
|
||||
false
|
||||
|
@ -468,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);
|
||||
}
|
||||
}
|
|
@ -2,20 +2,15 @@ use nu_engine::documentation::get_flags_section;
|
|||
use nu_protocol::{engine::EngineState, levenshtein_distance};
|
||||
use reedline::{Completer, Suggestion};
|
||||
|
||||
pub const EXAMPLE_MARKER: &str = ">>>>>>";
|
||||
pub const EXAMPLE_NEW_LINE: &str = "%%%%%%";
|
||||
|
||||
pub struct NuHelpCompleter {
|
||||
engine_state: EngineState,
|
||||
}
|
||||
pub struct NuHelpCompleter(EngineState);
|
||||
|
||||
impl NuHelpCompleter {
|
||||
pub fn new(engine_state: EngineState) -> Self {
|
||||
Self { engine_state }
|
||||
Self(engine_state)
|
||||
}
|
||||
|
||||
fn completion_helper(&self, line: &str, _pos: usize) -> Vec<Suggestion> {
|
||||
let full_commands = self.engine_state.get_signatures_with_examples(false);
|
||||
fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||
let full_commands = self.0.get_signatures_with_examples(false);
|
||||
|
||||
//Vec<(Signature, Vec<Example>, bool, bool)> {
|
||||
let mut commands = full_commands
|
||||
|
@ -83,20 +78,18 @@ impl NuHelpCompleter {
|
|||
}
|
||||
}
|
||||
|
||||
for example in examples {
|
||||
long_desc.push_str(&format!(
|
||||
"{}{}\r\n",
|
||||
EXAMPLE_MARKER,
|
||||
example.example.replace('\n', EXAMPLE_NEW_LINE)
|
||||
))
|
||||
}
|
||||
let extra: Vec<String> = examples
|
||||
.iter()
|
||||
.map(|example| example.example.to_string())
|
||||
.collect();
|
||||
|
||||
Suggestion {
|
||||
value: sig.name.clone(),
|
||||
description: Some(long_desc),
|
||||
extra: Some(extra),
|
||||
span: reedline::Span {
|
||||
start: 0,
|
||||
end: sig.name.len(),
|
||||
start: pos,
|
||||
end: pos + line.len(),
|
||||
},
|
||||
}
|
||||
})
|
167
crates/nu-cli/src/menus/menu_completions.rs
Normal file
167
crates/nu-cli/src/menus/menu_completions.rs
Normal 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(),
|
||||
},
|
||||
}],
|
||||
}
|
||||
}
|
7
crates/nu-cli/src/menus/mod.rs
Normal file
7
crates/nu-cli/src/menus/mod.rs
Normal 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;
|
|
@ -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<dyn Completer>,
|
||||
// Adds all menus to line editor
|
||||
pub(crate) fn add_menus(
|
||||
mut line_editor: Reedline,
|
||||
engine_state: &EngineState,
|
||||
stack: &Stack,
|
||||
config: &Config,
|
||||
) -> Reedline {
|
||||
let mut help_menu = NuHelpMenu::default();
|
||||
) -> Result<Reedline, ShellError> {
|
||||
line_editor = line_editor.clear_menus();
|
||||
|
||||
help_menu = match config
|
||||
.help_config
|
||||
.get("columns")
|
||||
.and_then(|value| value.as_integer().ok())
|
||||
{
|
||||
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<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
|
||||
.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<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) {
|
||||
|
|
|
@ -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!());
|
||||
|
|
|
@ -13,6 +13,17 @@ pub struct ParsedKeybinding {
|
|||
pub mode: Value,
|
||||
}
|
||||
|
||||
/// Definition of a parsed menu from the config object
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct ParsedMenu {
|
||||
pub name: Value,
|
||||
pub marker: Value,
|
||||
pub only_buffer_difference: Value,
|
||||
pub style: Value,
|
||||
pub menu_type: Value,
|
||||
pub source: Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub filesize_metric: bool,
|
||||
|
@ -31,10 +42,8 @@ pub struct Config {
|
|||
pub max_history_size: i64,
|
||||
pub sync_history_on_enter: bool,
|
||||
pub log_level: String,
|
||||
pub menu_config: HashMap<String, Value>,
|
||||
pub keybindings: Vec<ParsedKeybinding>,
|
||||
pub history_config: HashMap<String, Value>,
|
||||
pub help_config: HashMap<String, Value>,
|
||||
pub menus: Vec<ParsedMenu>,
|
||||
pub rm_always_trash: bool,
|
||||
}
|
||||
|
||||
|
@ -57,10 +66,8 @@ impl Default for Config {
|
|||
max_history_size: 1000,
|
||||
sync_history_on_enter: true,
|
||||
log_level: String::new(),
|
||||
menu_config: HashMap::new(),
|
||||
history_config: HashMap::new(),
|
||||
help_config: HashMap::new(),
|
||||
keybindings: Vec::new(),
|
||||
menus: Vec::new(),
|
||||
rm_always_trash: false,
|
||||
}
|
||||
}
|
||||
|
@ -215,34 +222,20 @@ impl Value {
|
|||
eprintln!("$config.log_level is not a string")
|
||||
}
|
||||
}
|
||||
"menu_config" => {
|
||||
if let Ok(map) = create_map(value, &config) {
|
||||
config.menu_config = map;
|
||||
} else {
|
||||
eprintln!("$config.menu_config is not a record")
|
||||
"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<Vec<ParsedKeybin
|
|||
match value {
|
||||
Value::Record { cols, vals, span } => {
|
||||
// Finding the modifier value in the record
|
||||
let modifier = extract_value("modifier", cols, vals, span)?;
|
||||
let keycode = extract_value("keycode", cols, vals, span)?;
|
||||
let mode = extract_value("mode", cols, vals, span)?;
|
||||
let event = extract_value("event", cols, vals, span)?;
|
||||
let modifier = extract_value("modifier", cols, vals, span)?.clone();
|
||||
let keycode = extract_value("keycode", cols, vals, span)?.clone();
|
||||
let mode = extract_value("mode", cols, vals, span)?.clone();
|
||||
let event = extract_value("event", cols, vals, span)?.clone();
|
||||
|
||||
let keybinding = ParsedKeybinding {
|
||||
modifier: modifier.clone(),
|
||||
keycode: keycode.clone(),
|
||||
mode: mode.clone(),
|
||||
event: event.clone(),
|
||||
modifier,
|
||||
keycode,
|
||||
mode,
|
||||
event,
|
||||
};
|
||||
|
||||
// We return a menu to be able to do recursion on the same function
|
||||
Ok(vec![keybinding])
|
||||
}
|
||||
Value::List { vals, .. } => {
|
||||
|
@ -341,6 +335,49 @@ fn create_keybindings(value: &Value, config: &Config) -> Result<Vec<ParsedKeybin
|
|||
}
|
||||
}
|
||||
|
||||
// Parses the config object to extract the strings that will compose a keybinding for reedline
|
||||
pub fn create_menus(value: &Value, config: &Config) -> Result<Vec<ParsedMenu>, ShellError> {
|
||||
match value {
|
||||
Value::Record { cols, vals, span } => {
|
||||
// Finding the modifier value in the record
|
||||
let name = extract_value("name", cols, vals, span)?.clone();
|
||||
let marker = extract_value("marker", cols, vals, span)?.clone();
|
||||
let only_buffer_difference =
|
||||
extract_value("only_buffer_difference", cols, vals, span)?.clone();
|
||||
let style = extract_value("style", cols, vals, span)?.clone();
|
||||
let menu_type = extract_value("type", cols, vals, span)?.clone();
|
||||
|
||||
// Source is an optional value
|
||||
let source = match extract_value("source", cols, vals, span) {
|
||||
Ok(source) => source.clone(),
|
||||
Err(_) => Value::Nothing { span: *span },
|
||||
};
|
||||
|
||||
let menu = ParsedMenu {
|
||||
name,
|
||||
only_buffer_difference,
|
||||
marker,
|
||||
style,
|
||||
menu_type,
|
||||
source,
|
||||
};
|
||||
|
||||
Ok(vec![menu])
|
||||
}
|
||||
Value::List { vals, .. } => {
|
||||
let res = vals
|
||||
.iter()
|
||||
.map(|inner_value| create_menus(inner_value, config))
|
||||
.collect::<Result<Vec<Vec<ParsedMenu>>, ShellError>>();
|
||||
|
||||
let res = res?.into_iter().flatten().collect::<Vec<ParsedMenu>>();
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
_ => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_value<'record>(
|
||||
name: &str,
|
||||
cols: &'record [String],
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue