mirror of
https://github.com/nushell/nushell.git
synced 2025-07-01 15:11:52 +02:00
Help menu (#4992)
* nu-completer with suggestions * help menu with scrolling * updates description rows based on space * configuration for help menu * update nu-ansi-term * corrected test for update cells * changed keybinding
This commit is contained in:
@ -12,13 +12,16 @@ nu-path = { path = "../nu-path", version = "0.60.1" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.60.1" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.60.1" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.60.1" }
|
||||
nu-ansi-term = "0.45.0"
|
||||
nu-ansi-term = "0.45.1"
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.60.1" }
|
||||
|
||||
crossterm = "0.23.0"
|
||||
crossterm_winapi = "0.9.0"
|
||||
miette = { version = "4.1.0", features = ["fancy"] }
|
||||
thiserror = "1.0.29"
|
||||
reedline = { git = "https://github.com/nushell/reedline" }
|
||||
#reedline = "0.3.0"
|
||||
reedline = { git = "https://github.com/nushell/reedline", branch = "main" }
|
||||
|
||||
log = "0.4"
|
||||
is_executable = "1.0.1"
|
||||
|
||||
|
@ -5,7 +5,7 @@ use nu_protocol::{
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
PipelineData, Span, Value, CONFIG_VARIABLE_ID,
|
||||
};
|
||||
use reedline::Completer;
|
||||
use reedline::{Completer, Suggestion};
|
||||
|
||||
const SEP: char = std::path::MAIN_SEPARATOR;
|
||||
|
||||
@ -83,46 +83,49 @@ impl NuCompleter {
|
||||
prefix: &[u8],
|
||||
span: Span,
|
||||
offset: usize,
|
||||
) -> Vec<(reedline::Span, String)> {
|
||||
) -> Vec<Suggestion> {
|
||||
let mut output = vec![];
|
||||
|
||||
let builtins = ["$nu", "$in", "$config", "$env", "$nothing"];
|
||||
|
||||
for builtin in builtins {
|
||||
if builtin.as_bytes().starts_with(prefix) {
|
||||
output.push((
|
||||
reedline::Span {
|
||||
output.push(Suggestion {
|
||||
value: builtin.to_string(),
|
||||
description: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
builtin.to_string(),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for scope in &working_set.delta.scope {
|
||||
for v in &scope.vars {
|
||||
if v.0.starts_with(prefix) {
|
||||
output.push((
|
||||
reedline::Span {
|
||||
output.push(Suggestion {
|
||||
value: String::from_utf8_lossy(v.0).to_string(),
|
||||
description: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(v.0).to_string(),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
for scope in &self.engine_state.scope {
|
||||
for v in &scope.vars {
|
||||
if v.0.starts_with(prefix) {
|
||||
output.push((
|
||||
reedline::Span {
|
||||
output.push(Suggestion {
|
||||
value: String::from_utf8_lossy(v.0).to_string(),
|
||||
description: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(v.0).to_string(),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -138,34 +141,32 @@ impl NuCompleter {
|
||||
span: Span,
|
||||
offset: usize,
|
||||
find_externals: bool,
|
||||
) -> Vec<(reedline::Span, String)> {
|
||||
) -> Vec<Suggestion> {
|
||||
let prefix = working_set.get_span_contents(span);
|
||||
|
||||
let results = working_set
|
||||
.find_commands_by_prefix(prefix)
|
||||
.into_iter()
|
||||
.map(move |x| {
|
||||
(
|
||||
reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(&x).to_string(),
|
||||
)
|
||||
.map(move |x| Suggestion {
|
||||
value: String::from_utf8_lossy(&x).to_string(),
|
||||
description: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
});
|
||||
|
||||
let results_aliases =
|
||||
working_set
|
||||
.find_aliases_by_prefix(prefix)
|
||||
.into_iter()
|
||||
.map(move |x| {
|
||||
(
|
||||
reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(&x).to_string(),
|
||||
)
|
||||
.map(move |x| Suggestion {
|
||||
value: String::from_utf8_lossy(&x).to_string(),
|
||||
description: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
});
|
||||
|
||||
let mut results = results.chain(results_aliases).collect::<Vec<_>>();
|
||||
@ -176,19 +177,22 @@ impl NuCompleter {
|
||||
let results_external =
|
||||
self.external_command_completion(&prefix)
|
||||
.into_iter()
|
||||
.map(move |x| {
|
||||
(
|
||||
reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
x,
|
||||
)
|
||||
.map(move |x| Suggestion {
|
||||
value: x,
|
||||
description: None,
|
||||
span: reedline::Span {
|
||||
start: span.start - offset,
|
||||
end: span.end - offset,
|
||||
},
|
||||
});
|
||||
|
||||
for external in results_external {
|
||||
if results.contains(&external) {
|
||||
results.push((external.0, format!("^{}", external.1)))
|
||||
results.push(Suggestion {
|
||||
value: format!("^{}", external.value),
|
||||
description: None,
|
||||
span: external.span,
|
||||
})
|
||||
} else {
|
||||
results.push(external)
|
||||
}
|
||||
@ -200,7 +204,7 @@ impl NuCompleter {
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_helper(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> {
|
||||
fn completion_helper(&self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||
let mut working_set = StateWorkingSet::new(&self.engine_state);
|
||||
let offset = working_set.next_span_start();
|
||||
let mut line = line.to_string();
|
||||
@ -231,7 +235,7 @@ impl NuCompleter {
|
||||
if prefix.starts_with(b"$") {
|
||||
let mut output =
|
||||
self.complete_variables(&working_set, &prefix, new_span, offset);
|
||||
output.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
output.sort_by(|a, b| a.value.cmp(&b.value));
|
||||
return output;
|
||||
}
|
||||
if prefix.starts_with(b"-") {
|
||||
@ -248,13 +252,14 @@ impl NuCompleter {
|
||||
short.encode_utf8(&mut named);
|
||||
named.insert(0, b'-');
|
||||
if named.starts_with(&prefix) {
|
||||
output.push((
|
||||
reedline::Span {
|
||||
output.push(Suggestion {
|
||||
value: String::from_utf8_lossy(&named).to_string(),
|
||||
description: None,
|
||||
span: reedline::Span {
|
||||
start: new_span.start - offset,
|
||||
end: new_span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(&named).to_string(),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,16 +271,17 @@ impl NuCompleter {
|
||||
named.insert(0, b'-');
|
||||
named.insert(0, b'-');
|
||||
if named.starts_with(&prefix) {
|
||||
output.push((
|
||||
reedline::Span {
|
||||
output.push(Suggestion {
|
||||
value: String::from_utf8_lossy(&named).to_string(),
|
||||
description: None,
|
||||
span: reedline::Span {
|
||||
start: new_span.start - offset,
|
||||
end: new_span.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(&named).to_string(),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
output.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
output.sort_by(|a, b| a.value.cmp(&b.value));
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@ -317,18 +323,19 @@ impl NuCompleter {
|
||||
list: impl Iterator<Item = &'a Value>,
|
||||
new_span: Span,
|
||||
offset: usize,
|
||||
) -> Vec<(reedline::Span, String)> {
|
||||
) -> Vec<Suggestion> {
|
||||
list.filter_map(move |x| {
|
||||
let s = x.as_string();
|
||||
|
||||
match s {
|
||||
Ok(s) => Some((
|
||||
reedline::Span {
|
||||
Ok(s) => Some(Suggestion {
|
||||
value: s,
|
||||
description: None,
|
||||
span: reedline::Span {
|
||||
start: new_span.start - offset,
|
||||
end: new_span.end - offset,
|
||||
},
|
||||
s,
|
||||
)),
|
||||
}),
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
@ -388,17 +395,19 @@ impl NuCompleter {
|
||||
_ => (vec![], CompletionOptions::default()),
|
||||
};
|
||||
|
||||
let mut completions: Vec<(reedline::Span, String)> = completions
|
||||
let mut completions: Vec<Suggestion> = completions
|
||||
.into_iter()
|
||||
.filter(|it| {
|
||||
// Minimise clones for new functionality
|
||||
match (options.case_sensitive, options.positional) {
|
||||
(true, true) => it.1.as_bytes().starts_with(&prefix),
|
||||
(true, false) => it.1.contains(
|
||||
(true, true) => {
|
||||
it.value.as_bytes().starts_with(&prefix)
|
||||
}
|
||||
(true, false) => it.value.contains(
|
||||
std::str::from_utf8(&prefix).unwrap_or(""),
|
||||
),
|
||||
(false, positional) => {
|
||||
let value = it.1.to_lowercase();
|
||||
let value = it.value.to_lowercase();
|
||||
let prefix = std::str::from_utf8(&prefix)
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
@ -413,7 +422,7 @@ impl NuCompleter {
|
||||
.collect();
|
||||
|
||||
if options.sort {
|
||||
completions.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
completions.sort_by(|a, b| a.value.cmp(&b.value));
|
||||
}
|
||||
|
||||
return completions;
|
||||
@ -431,17 +440,16 @@ impl NuCompleter {
|
||||
let mut output: Vec<_> =
|
||||
file_path_completion(new_span, &prefix, &cwd)
|
||||
.into_iter()
|
||||
.map(move |x| {
|
||||
(
|
||||
reedline::Span {
|
||||
start: x.0.start - offset,
|
||||
end: x.0.end - offset,
|
||||
},
|
||||
x.1,
|
||||
)
|
||||
.map(move |x| Suggestion {
|
||||
value: x.1,
|
||||
description: None,
|
||||
span: reedline::Span {
|
||||
start: x.0.start - offset,
|
||||
end: x.0.end - offset,
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
output.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
output.sort_by(|a, b| a.value.cmp(&b.value));
|
||||
return output;
|
||||
}
|
||||
flat_shape => {
|
||||
@ -542,20 +550,19 @@ impl NuCompleter {
|
||||
(x.0, x.1)
|
||||
}
|
||||
})
|
||||
.map(move |x| {
|
||||
(
|
||||
reedline::Span {
|
||||
start: x.0.start - offset,
|
||||
end: x.0.end - offset,
|
||||
},
|
||||
x.1,
|
||||
)
|
||||
.map(move |x| Suggestion {
|
||||
value: x.1,
|
||||
description: None,
|
||||
span: reedline::Span {
|
||||
start: x.0.start - offset,
|
||||
end: x.0.end - offset,
|
||||
},
|
||||
})
|
||||
.chain(subcommands.into_iter())
|
||||
.chain(commands.into_iter())
|
||||
.collect::<Vec<_>>();
|
||||
//output.dedup_by(|a, b| a.1 == b.1);
|
||||
output.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
output.sort_by(|a, b| a.value.cmp(&b.value));
|
||||
|
||||
return output;
|
||||
}
|
||||
@ -570,7 +577,7 @@ impl NuCompleter {
|
||||
}
|
||||
|
||||
impl Completer for NuCompleter {
|
||||
fn complete(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> {
|
||||
fn complete(&self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||
self.completion_helper(line, pos)
|
||||
}
|
||||
}
|
||||
|
101
crates/nu-cli/src/help_completions.rs
Normal file
101
crates/nu-cli/src/help_completions.rs
Normal file
@ -0,0 +1,101 @@
|
||||
use nu_engine::documentation::get_flags_section;
|
||||
use nu_protocol::engine::EngineState;
|
||||
use reedline::{Completer, Suggestion};
|
||||
|
||||
pub const EXAMPLE_MARKER: &str = ">>>>>>";
|
||||
pub const EXAMPLE_NEW_LINE: &str = "%%%%%%";
|
||||
|
||||
pub struct NuHelpCompleter {
|
||||
engine_state: EngineState,
|
||||
}
|
||||
|
||||
impl NuHelpCompleter {
|
||||
pub fn new(engine_state: EngineState) -> Self {
|
||||
Self { engine_state }
|
||||
}
|
||||
|
||||
fn completion_helper(&self, line: &str, _pos: usize) -> Vec<Suggestion> {
|
||||
let full_commands = self.engine_state.get_signatures_with_examples(false);
|
||||
|
||||
//Vec<(Signature, Vec<Example>, bool, bool)> {
|
||||
full_commands
|
||||
.iter()
|
||||
.filter(|(sig, _, _, _)| {
|
||||
sig.name.to_lowercase().contains(&line.to_lowercase())
|
||||
|| sig.usage.to_lowercase().contains(&line.to_lowercase())
|
||||
|| sig
|
||||
.extra_usage
|
||||
.to_lowercase()
|
||||
.contains(&line.to_lowercase())
|
||||
})
|
||||
.map(|(sig, examples, _, _)| {
|
||||
let mut long_desc = String::new();
|
||||
|
||||
let usage = &sig.usage;
|
||||
if !usage.is_empty() {
|
||||
long_desc.push_str(usage);
|
||||
long_desc.push_str("\r\n\r\n");
|
||||
}
|
||||
|
||||
let extra_usage = &sig.extra_usage;
|
||||
if !extra_usage.is_empty() {
|
||||
long_desc.push_str(extra_usage);
|
||||
long_desc.push_str("\r\n\r\n");
|
||||
}
|
||||
|
||||
long_desc.push_str(&format!("Usage:\r\n > {}\r\n", sig.call_signature()));
|
||||
|
||||
if !sig.named.is_empty() {
|
||||
long_desc.push_str(&get_flags_section(sig))
|
||||
}
|
||||
|
||||
if !sig.required_positional.is_empty()
|
||||
|| !sig.optional_positional.is_empty()
|
||||
|| sig.rest_positional.is_some()
|
||||
{
|
||||
long_desc.push_str("\r\nParameters:\r\n");
|
||||
for positional in &sig.required_positional {
|
||||
long_desc
|
||||
.push_str(&format!(" {}: {}\r\n", positional.name, positional.desc));
|
||||
}
|
||||
for positional in &sig.optional_positional {
|
||||
long_desc.push_str(&format!(
|
||||
" (optional) {}: {}\r\n",
|
||||
positional.name, positional.desc
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(rest_positional) = &sig.rest_positional {
|
||||
long_desc.push_str(&format!(
|
||||
" ...{}: {}\r\n",
|
||||
rest_positional.name, rest_positional.desc
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for example in examples {
|
||||
long_desc.push_str(&format!(
|
||||
"{}{}\r\n",
|
||||
EXAMPLE_MARKER,
|
||||
example.example.replace('\n', EXAMPLE_NEW_LINE)
|
||||
))
|
||||
}
|
||||
|
||||
Suggestion {
|
||||
value: sig.name.clone(),
|
||||
description: Some(long_desc),
|
||||
span: reedline::Span {
|
||||
start: 0,
|
||||
end: sig.name.len(),
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for NuHelpCompleter {
|
||||
fn complete(&self, line: &str, pos: usize) -> Vec<Suggestion> {
|
||||
self.completion_helper(line, pos)
|
||||
}
|
||||
}
|
718
crates/nu-cli/src/help_menu.rs
Normal file
718
crates/nu-cli/src/help_menu.rs
Normal file
@ -0,0 +1,718 @@
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
/// Default values used as reference for the menu. These values are set during
|
||||
/// the initial declaration of the menu and are always kept as reference for the
|
||||
/// changeable [`WorkingDetails`]
|
||||
struct DefaultMenuDetails {
|
||||
/// Number of columns that the menu will have
|
||||
pub columns: u16,
|
||||
/// Column width
|
||||
pub col_width: Option<usize>,
|
||||
/// Column padding
|
||||
pub col_padding: usize,
|
||||
/// Number of rows for commands
|
||||
pub selection_rows: u16,
|
||||
/// Number of rows allowed to display the description
|
||||
pub description_rows: usize,
|
||||
}
|
||||
|
||||
impl Default for DefaultMenuDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
columns: 4,
|
||||
col_width: None,
|
||||
col_padding: 2,
|
||||
selection_rows: 4,
|
||||
description_rows: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the actual column conditions of the menu. These conditions change
|
||||
/// since they need to accommodate possible different line sizes for the column values
|
||||
#[derive(Default)]
|
||||
struct WorkingDetails {
|
||||
/// Number of columns that the menu will have
|
||||
pub columns: u16,
|
||||
/// Column width
|
||||
pub col_width: usize,
|
||||
/// Number of rows for description
|
||||
pub description_rows: usize,
|
||||
}
|
||||
|
||||
/// Completion menu definition
|
||||
pub struct NuHelpMenu {
|
||||
active: bool,
|
||||
/// Menu coloring
|
||||
color: MenuTextStyle,
|
||||
/// Default column details that are set when creating the menu
|
||||
/// These values are the reference for the working details
|
||||
default_details: DefaultMenuDetails,
|
||||
/// Number of minimum rows that are displayed when
|
||||
/// the required lines is larger than the available lines
|
||||
min_rows: u16,
|
||||
/// Working column details keep changing based on the collected values
|
||||
working_details: WorkingDetails,
|
||||
/// Menu cached values
|
||||
values: Vec<Suggestion>,
|
||||
/// column position of the cursor. Starts from 0
|
||||
col_pos: u16,
|
||||
/// row position in the menu. Starts from 0
|
||||
row_pos: u16,
|
||||
/// Menu marker when active
|
||||
marker: String,
|
||||
/// Event sent to the menu
|
||||
event: Option<MenuEvent>,
|
||||
/// String collected after the menu is activated
|
||||
input: Option<String>,
|
||||
/// Examples to select
|
||||
examples: Vec<String>,
|
||||
/// Example index
|
||||
example_index: Option<usize>,
|
||||
/// Examples may not be shown if there is not enough space in the screen
|
||||
show_examples: bool,
|
||||
/// Skipped description rows
|
||||
skipped_rows: usize,
|
||||
}
|
||||
|
||||
impl Default for NuHelpMenu {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active: false,
|
||||
color: MenuTextStyle::default(),
|
||||
default_details: DefaultMenuDetails::default(),
|
||||
min_rows: 3,
|
||||
working_details: WorkingDetails::default(),
|
||||
values: Vec::new(),
|
||||
col_pos: 0,
|
||||
row_pos: 0,
|
||||
marker: "| ".to_string(),
|
||||
event: None,
|
||||
input: None,
|
||||
examples: Vec::new(),
|
||||
example_index: None,
|
||||
show_examples: true,
|
||||
skipped_rows: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NuHelpMenu {
|
||||
/// 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;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new value for text style
|
||||
pub fn with_selected_text_style(mut self, selected_text_style: Style) -> Self {
|
||||
self.color.selected_text_style = selected_text_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new value for text style
|
||||
pub fn with_description_text_style(mut self, description_text_style: Style) -> Self {
|
||||
self.color.description_style = description_text_style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new columns value
|
||||
pub fn with_columns(mut self, columns: u16) -> Self {
|
||||
self.default_details.columns = columns;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new column width value
|
||||
pub fn with_column_width(mut self, col_width: Option<usize>) -> Self {
|
||||
self.default_details.col_width = col_width;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new column width value
|
||||
pub fn with_column_padding(mut self, col_padding: usize) -> Self {
|
||||
self.default_details.col_padding = col_padding;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new selection rows value
|
||||
pub fn with_selection_rows(mut self, selection_rows: u16) -> Self {
|
||||
self.default_details.selection_rows = selection_rows;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with new description rows value
|
||||
pub fn with_description_rows(mut self, description_rows: usize) -> Self {
|
||||
self.default_details.description_rows = description_rows;
|
||||
self
|
||||
}
|
||||
|
||||
/// Menu builder with marker
|
||||
pub fn with_marker(mut self, marker: String) -> Self {
|
||||
self.marker = marker;
|
||||
self
|
||||
}
|
||||
|
||||
/// Move menu cursor to the next element
|
||||
fn move_next(&mut self) {
|
||||
let mut new_col = self.col_pos + 1;
|
||||
let mut new_row = self.row_pos;
|
||||
|
||||
if new_col >= self.get_cols() {
|
||||
new_row += 1;
|
||||
new_col = 0;
|
||||
}
|
||||
|
||||
if new_row >= self.get_rows() {
|
||||
new_row = 0;
|
||||
new_col = 0;
|
||||
}
|
||||
|
||||
let position = new_row * self.get_cols() + new_col;
|
||||
if position >= self.get_values().len() as u16 {
|
||||
self.reset_position();
|
||||
} else {
|
||||
self.col_pos = new_col;
|
||||
self.row_pos = new_row;
|
||||
}
|
||||
}
|
||||
|
||||
/// Move menu cursor to the previous element
|
||||
fn move_previous(&mut self) {
|
||||
let new_col = self.col_pos.checked_sub(1);
|
||||
|
||||
let (new_col, new_row) = match new_col {
|
||||
Some(col) => (col, self.row_pos),
|
||||
None => match self.row_pos.checked_sub(1) {
|
||||
Some(row) => (self.get_cols().saturating_sub(1), row),
|
||||
None => (
|
||||
self.get_cols().saturating_sub(1),
|
||||
self.get_rows().saturating_sub(1),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
let position = new_row * self.get_cols() + new_col;
|
||||
if position >= self.get_values().len() as u16 {
|
||||
self.col_pos = (self.get_values().len() as u16 % self.get_cols()).saturating_sub(1);
|
||||
self.row_pos = self.get_rows().saturating_sub(1);
|
||||
} else {
|
||||
self.col_pos = new_col;
|
||||
self.row_pos = new_row;
|
||||
}
|
||||
}
|
||||
|
||||
/// Menu index based on column and row position
|
||||
fn index(&self) -> usize {
|
||||
let index = self.row_pos * self.get_cols() + self.col_pos;
|
||||
index as usize
|
||||
}
|
||||
|
||||
/// Get selected value from the menu
|
||||
fn get_value(&self) -> Option<Suggestion> {
|
||||
self.get_values().get(self.index()).cloned()
|
||||
}
|
||||
|
||||
/// Calculates how many rows the Menu will use
|
||||
fn get_rows(&self) -> u16 {
|
||||
let values = self.get_values().len() as u16;
|
||||
|
||||
if values == 0 {
|
||||
// When the values are empty the no_records_msg is shown, taking 1 line
|
||||
return 1;
|
||||
}
|
||||
|
||||
let rows = values / self.get_cols();
|
||||
if values % self.get_cols() != 0 {
|
||||
rows + 1
|
||||
} else {
|
||||
rows
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns working details col width
|
||||
fn get_width(&self) -> usize {
|
||||
self.working_details.col_width
|
||||
}
|
||||
|
||||
/// Reset menu position
|
||||
fn reset_position(&mut self) {
|
||||
self.col_pos = 0;
|
||||
self.row_pos = 0;
|
||||
self.skipped_rows = 0;
|
||||
}
|
||||
|
||||
fn no_records_msg(&self, use_ansi_coloring: bool) -> String {
|
||||
let msg = "TYPE TO START SEACH";
|
||||
if use_ansi_coloring {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
self.color.selected_text_style.prefix(),
|
||||
msg,
|
||||
RESET
|
||||
)
|
||||
} else {
|
||||
msg.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns working details columns
|
||||
fn get_cols(&self) -> u16 {
|
||||
self.working_details.columns.max(1)
|
||||
}
|
||||
|
||||
/// End of line for menu
|
||||
fn end_of_line(&self, column: u16, index: usize) -> &str {
|
||||
let is_last = index == self.values.len().saturating_sub(1);
|
||||
if column == self.get_cols().saturating_sub(1) || is_last {
|
||||
"\r\n"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
/// Update list of examples from the actual value
|
||||
fn update_examples(&mut self) {
|
||||
let 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>>();
|
||||
|
||||
self.examples = examples;
|
||||
self.example_index = None;
|
||||
}
|
||||
|
||||
/// Creates default string that represents one suggestion from the menu
|
||||
fn create_entry_string(
|
||||
&self,
|
||||
suggestion: &Suggestion,
|
||||
index: usize,
|
||||
column: u16,
|
||||
empty_space: usize,
|
||||
use_ansi_coloring: bool,
|
||||
) -> String {
|
||||
if use_ansi_coloring {
|
||||
if index == self.index() {
|
||||
format!(
|
||||
"{}{}{:>empty$}{}{}",
|
||||
self.color.selected_text_style.prefix(),
|
||||
&suggestion.value,
|
||||
"",
|
||||
RESET,
|
||||
self.end_of_line(column, index),
|
||||
empty = empty_space,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{}{}{:>empty$}{}{}",
|
||||
self.color.text_style.prefix(),
|
||||
&suggestion.value,
|
||||
"",
|
||||
RESET,
|
||||
self.end_of_line(column, index),
|
||||
empty = empty_space,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// If no ansi coloring is found, then the selection word is
|
||||
// the line in uppercase
|
||||
let (marker, empty_space) = if index == self.index() {
|
||||
(">", empty_space.saturating_sub(1))
|
||||
} else {
|
||||
("", empty_space)
|
||||
};
|
||||
|
||||
let line = format!(
|
||||
"{}{}{:>empty$}{}",
|
||||
marker,
|
||||
&suggestion.value,
|
||||
"",
|
||||
self.end_of_line(column, index),
|
||||
empty = empty_space,
|
||||
);
|
||||
|
||||
if index == self.index() {
|
||||
line.to_uppercase()
|
||||
} else {
|
||||
line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Description string with color
|
||||
fn create_description_string(&self, use_ansi_coloring: bool) -> String {
|
||||
let description = self
|
||||
.get_value()
|
||||
.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>>()
|
||||
.join("\r\n");
|
||||
|
||||
if use_ansi_coloring && !description.is_empty() {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
self.color.description_style.prefix(),
|
||||
description,
|
||||
RESET,
|
||||
)
|
||||
} else {
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
/// Selectable list of examples from the actual value
|
||||
fn create_example_string(&self, use_ansi_coloring: bool) -> String {
|
||||
if !self.show_examples {
|
||||
return "".into();
|
||||
}
|
||||
|
||||
let examples: String = self
|
||||
.examples
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, example)| {
|
||||
if let Some(example_index) = self.example_index {
|
||||
if index == example_index {
|
||||
format!(
|
||||
" {}{}{}\r\n",
|
||||
self.color.selected_text_style.prefix(),
|
||||
example,
|
||||
RESET
|
||||
)
|
||||
} else {
|
||||
format!(" {}\r\n", example)
|
||||
}
|
||||
} else {
|
||||
format!(" {}\r\n", example)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if examples.is_empty() {
|
||||
"".into()
|
||||
} else if use_ansi_coloring {
|
||||
format!(
|
||||
"{}\r\n\r\nExamples:\r\n{}{}",
|
||||
self.color.description_style.prefix(),
|
||||
RESET,
|
||||
examples,
|
||||
)
|
||||
} else {
|
||||
format!("\r\n\r\nExamples:\r\n{}", examples,)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Menu for NuHelpMenu {
|
||||
/// Menu name
|
||||
fn name(&self) -> &str {
|
||||
"help_menu"
|
||||
}
|
||||
|
||||
/// Menu indicator
|
||||
fn indicator(&self) -> &str {
|
||||
self.marker.as_str()
|
||||
}
|
||||
|
||||
/// Deactivates context menu
|
||||
fn is_active(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
/// The help menu stays active even with one record
|
||||
fn can_quick_complete(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The help 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
|
||||
}
|
||||
|
||||
/// Selects what type of event happened with the menu
|
||||
fn menu_event(&mut self, event: MenuEvent) {
|
||||
match &event {
|
||||
MenuEvent::Activate(_) => self.active = true,
|
||||
MenuEvent::Deactivate => {
|
||||
self.active = false;
|
||||
self.input = None;
|
||||
self.values = Vec::new();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
self.event = Some(event);
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The working details for the menu changes based on the size of the lines
|
||||
/// collected from the completer
|
||||
fn update_working_details(
|
||||
&mut self,
|
||||
line_buffer: &mut LineBuffer,
|
||||
history: &dyn History,
|
||||
completer: &dyn Completer,
|
||||
painter: &Painter,
|
||||
) {
|
||||
if let Some(event) = self.event.take() {
|
||||
// Updating all working parameters from the menu before executing any of the
|
||||
// possible event
|
||||
let max_width = self.get_values().iter().fold(0, |acc, suggestion| {
|
||||
let str_len = suggestion.value.len() + self.default_details.col_padding;
|
||||
if str_len > acc {
|
||||
str_len
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
});
|
||||
|
||||
// If no default width is found, then the total screen width is used to estimate
|
||||
// the column width based on the default number of columns
|
||||
let default_width = if let Some(col_width) = self.default_details.col_width {
|
||||
col_width
|
||||
} else {
|
||||
let col_width = painter.screen_width() / self.default_details.columns;
|
||||
col_width as usize
|
||||
};
|
||||
|
||||
// Adjusting the working width of the column based the max line width found
|
||||
// in the menu values
|
||||
if max_width > default_width {
|
||||
self.working_details.col_width = max_width;
|
||||
} else {
|
||||
self.working_details.col_width = default_width;
|
||||
};
|
||||
|
||||
// The working columns is adjusted based on possible number of columns
|
||||
// that could be fitted in the screen with the calculated column width
|
||||
let possible_cols = painter.screen_width() / self.working_details.col_width as u16;
|
||||
if possible_cols > self.default_details.columns {
|
||||
self.working_details.columns = self.default_details.columns.max(1);
|
||||
} else {
|
||||
self.working_details.columns = possible_cols;
|
||||
}
|
||||
|
||||
// Updating the working rows to display the description
|
||||
if self.menu_required_lines(painter.screen_width()) <= painter.remaining_lines() {
|
||||
self.working_details.description_rows = self.default_details.description_rows;
|
||||
self.show_examples = true;
|
||||
} else {
|
||||
self.working_details.description_rows = painter
|
||||
.remaining_lines()
|
||||
.saturating_sub(self.default_details.selection_rows + 1)
|
||||
as usize;
|
||||
|
||||
self.show_examples = false;
|
||||
}
|
||||
|
||||
match event {
|
||||
MenuEvent::Activate(_) => {
|
||||
self.reset_position();
|
||||
self.input = Some(line_buffer.get_buffer().to_string());
|
||||
self.update_values(line_buffer, history, completer);
|
||||
}
|
||||
MenuEvent::Deactivate => self.active = false,
|
||||
MenuEvent::Edit(_) => {
|
||||
self.reset_position();
|
||||
self.update_values(line_buffer, history, completer);
|
||||
self.update_examples()
|
||||
}
|
||||
MenuEvent::NextElement => {
|
||||
self.skipped_rows = 0;
|
||||
self.move_next();
|
||||
self.update_examples();
|
||||
}
|
||||
MenuEvent::PreviousElement => {
|
||||
self.skipped_rows = 0;
|
||||
self.move_previous();
|
||||
self.update_examples();
|
||||
}
|
||||
MenuEvent::MoveUp => {
|
||||
if let Some(example_index) = self.example_index {
|
||||
if let Some(index) = example_index.checked_sub(1) {
|
||||
self.example_index = Some(index);
|
||||
} else {
|
||||
self.example_index = Some(self.examples.len().saturating_sub(1));
|
||||
}
|
||||
} else {
|
||||
self.example_index = Some(0);
|
||||
}
|
||||
}
|
||||
MenuEvent::MoveDown => {
|
||||
if let Some(example_index) = self.example_index {
|
||||
let index = example_index + 1;
|
||||
if index < self.examples.len() {
|
||||
self.example_index = Some(index);
|
||||
} else {
|
||||
self.example_index = Some(0);
|
||||
}
|
||||
} else {
|
||||
self.example_index = Some(0);
|
||||
}
|
||||
}
|
||||
MenuEvent::MoveLeft => self.skipped_rows = self.skipped_rows.saturating_sub(1),
|
||||
MenuEvent::MoveRight => {
|
||||
let skipped = self.skipped_rows + 1;
|
||||
let description_rows = self
|
||||
.get_value()
|
||||
.and_then(|suggestion| suggestion.description)
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
.lines()
|
||||
.filter(|line| !line.starts_with(EXAMPLE_MARKER))
|
||||
.count();
|
||||
|
||||
let allowed_skips =
|
||||
description_rows.saturating_sub(self.working_details.description_rows);
|
||||
|
||||
if skipped < allowed_skips {
|
||||
self.skipped_rows = skipped;
|
||||
} else {
|
||||
self.skipped_rows = allowed_skips;
|
||||
}
|
||||
}
|
||||
MenuEvent::PreviousPage | MenuEvent::NextPage => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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);
|
||||
example.len()
|
||||
} else {
|
||||
line_buffer.replace(span.start..span.end, &value);
|
||||
value.len()
|
||||
};
|
||||
|
||||
let mut offset = line_buffer.insertion_point();
|
||||
offset += string_len.saturating_sub(span.end - span.start);
|
||||
line_buffer.set_insertion_point(offset);
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimum rows that should be displayed by the menu
|
||||
fn min_rows(&self) -> u16 {
|
||||
self.get_rows().min(self.min_rows)
|
||||
}
|
||||
|
||||
/// Gets values from filler that will be displayed in the menu
|
||||
fn get_values(&self) -> &[Suggestion] {
|
||||
&self.values
|
||||
}
|
||||
|
||||
fn menu_required_lines(&self, _terminal_columns: u16) -> u16 {
|
||||
let example_lines = self
|
||||
.examples
|
||||
.iter()
|
||||
.fold(0, |acc, example| example.lines().count() + acc);
|
||||
|
||||
self.default_details.selection_rows
|
||||
+ self.default_details.description_rows as u16
|
||||
+ example_lines as u16
|
||||
+ 3
|
||||
}
|
||||
|
||||
fn menu_string(&self, _available_lines: u16, use_ansi_coloring: bool) -> String {
|
||||
if self.get_values().is_empty() {
|
||||
self.no_records_msg(use_ansi_coloring)
|
||||
} else {
|
||||
// The skip values represent the number of lines that should be skipped
|
||||
// while printing the menu
|
||||
let available_lines = self.default_details.selection_rows;
|
||||
let skip_values = if self.row_pos >= available_lines {
|
||||
let skip_lines = self.row_pos.saturating_sub(available_lines) + 1;
|
||||
(skip_lines * self.get_cols()) as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// It seems that crossterm prefers to have a complete string ready to be printed
|
||||
// rather than looping through the values and printing multiple things
|
||||
// This reduces the flickering when printing the menu
|
||||
let available_values = (available_lines * self.get_cols()) as usize;
|
||||
let selection_values: String = self
|
||||
.get_values()
|
||||
.iter()
|
||||
.skip(skip_values)
|
||||
.take(available_values)
|
||||
.enumerate()
|
||||
.map(|(index, suggestion)| {
|
||||
// Correcting the enumerate index based on the number of skipped values
|
||||
let index = index + skip_values;
|
||||
let column = index as u16 % self.get_cols();
|
||||
let empty_space = self.get_width().saturating_sub(suggestion.value.len());
|
||||
|
||||
self.create_entry_string(
|
||||
suggestion,
|
||||
index,
|
||||
column,
|
||||
empty_space,
|
||||
use_ansi_coloring,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
format!(
|
||||
"{}{}{}",
|
||||
selection_values,
|
||||
self.create_description_string(use_ansi_coloring),
|
||||
self.create_example_string(use_ansi_coloring)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ mod completions;
|
||||
mod config_files;
|
||||
mod errors;
|
||||
mod eval_file;
|
||||
mod help_completions;
|
||||
mod help_menu;
|
||||
mod nu_highlight;
|
||||
mod print;
|
||||
mod prompt;
|
||||
@ -18,6 +20,8 @@ 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 nu_highlight::NuHighlight;
|
||||
pub use print::Print;
|
||||
pub use prompt::NushellPrompt;
|
||||
|
@ -1,9 +1,10 @@
|
||||
use super::NuHelpMenu;
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use nu_color_config::lookup_ansi_color_style;
|
||||
use nu_protocol::{extract_value, Config, ParsedKeybinding, ShellError, Span, Value};
|
||||
use reedline::{
|
||||
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
|
||||
CompletionMenu, EditCommand, HistoryMenu, Keybindings, Reedline, ReedlineEvent,
|
||||
Completer, CompletionMenu, EditCommand, HistoryMenu, Keybindings, Reedline, ReedlineEvent,
|
||||
};
|
||||
|
||||
// Creates an input object for the completion menu based on the dictionary
|
||||
@ -64,7 +65,7 @@ pub(crate) fn add_completion_menu(line_editor: Reedline, config: &Config) -> Ree
|
||||
None => completion_menu,
|
||||
};
|
||||
|
||||
line_editor.with_menu(Box::new(completion_menu))
|
||||
line_editor.with_menu(Box::new(completion_menu), None)
|
||||
}
|
||||
|
||||
// Creates an input object for the history menu based on the dictionary
|
||||
@ -120,10 +121,119 @@ pub(crate) fn add_history_menu(line_editor: Reedline, config: &Config) -> Reedli
|
||||
None => history_menu,
|
||||
};
|
||||
|
||||
line_editor.with_menu(Box::new(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,
|
||||
) -> Reedline {
|
||||
let mut help_menu = NuHelpMenu::default();
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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),
|
||||
);
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
line_editor.with_menu(Box::new(help_menu), Some(help_completer))
|
||||
}
|
||||
|
||||
fn add_menu_keybindings(keybindings: &mut Keybindings) {
|
||||
// Completer menu keybindings
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::NONE,
|
||||
KeyCode::Tab,
|
||||
ReedlineEvent::UntilFound(vec![
|
||||
ReedlineEvent::Menu("completer_menu".to_string()),
|
||||
ReedlineEvent::MenuNext,
|
||||
]),
|
||||
);
|
||||
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::SHIFT,
|
||||
KeyCode::BackTab,
|
||||
ReedlineEvent::MenuPrevious,
|
||||
);
|
||||
|
||||
// History menu keybinding
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::CONTROL,
|
||||
KeyCode::Char('x'),
|
||||
@ -142,19 +252,11 @@ fn add_menu_keybindings(keybindings: &mut Keybindings) {
|
||||
]),
|
||||
);
|
||||
|
||||
// Help menu keybinding
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::NONE,
|
||||
KeyCode::Tab,
|
||||
ReedlineEvent::UntilFound(vec![
|
||||
ReedlineEvent::Menu("completion_menu".to_string()),
|
||||
ReedlineEvent::MenuNext,
|
||||
]),
|
||||
);
|
||||
|
||||
keybindings.add_binding(
|
||||
KeyModifiers::SHIFT,
|
||||
KeyCode::BackTab,
|
||||
ReedlineEvent::MenuPrevious,
|
||||
KeyModifiers::CONTROL,
|
||||
KeyCode::Char('i'),
|
||||
ReedlineEvent::Menu("help_menu".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::reedline_config::{add_completion_menu, add_history_menu};
|
||||
use crate::{prompt_update, reedline_config};
|
||||
use crate::reedline_config::{add_completion_menu, add_help_menu, add_history_menu};
|
||||
use crate::{prompt_update, reedline_config, NuHelpCompleter};
|
||||
use crate::{
|
||||
reedline_config::KeybindingsMode,
|
||||
util::{eval_source, report_error},
|
||||
@ -160,6 +160,9 @@ pub fn evaluate_repl(
|
||||
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);
|
||||
|
||||
if is_perf_true {
|
||||
info!("setup colors {}:{}:{}", file!(), line!(), column!());
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ version = "0.60.1"
|
||||
|
||||
[dependencies]
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.60.1" }
|
||||
nu-ansi-term = "0.45.0"
|
||||
nu-ansi-term = "0.45.1"
|
||||
nu-json = { path = "../nu-json", version = "0.60.1" }
|
||||
nu-table = { path = "../nu-table", version = "0.60.1" }
|
||||
serde = { version="1.0.123", features=["derive"] }
|
||||
|
@ -23,7 +23,7 @@ nu-table = { path = "../nu-table", version = "0.60.1" }
|
||||
nu-term-grid = { path = "../nu-term-grid", version = "0.60.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.60.1" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.60.1" }
|
||||
nu-ansi-term = "0.45.0"
|
||||
nu-ansi-term = "0.45.1"
|
||||
|
||||
# Potential dependencies for extras
|
||||
base64 = "0.13.0"
|
||||
@ -77,7 +77,8 @@ unicode-segmentation = "1.8.0"
|
||||
url = "2.2.1"
|
||||
uuid = { version = "0.8.2", features = ["v4"] }
|
||||
which = { version = "4.2.2", optional = true }
|
||||
reedline = { git = "https://github.com/nushell/reedline" }
|
||||
#reedline = "0.3.0"
|
||||
reedline = { git = "https://github.com/nushell/reedline", branch = "main" }
|
||||
zip = { version="0.5.9", optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
|
@ -43,7 +43,7 @@ impl Command for UpdateCells {
|
||||
example: r#"[
|
||||
["2021-04-16", "2021-06-10", "2021-09-18", "2021-10-15", "2021-11-16", "2021-11-17", "2021-11-18"];
|
||||
[ 37, 0, 0, 0, 37, 0, 0]
|
||||
] | update cells {|value|
|
||||
] | update cells { |value|
|
||||
if $value == 0 {
|
||||
""
|
||||
} else {
|
||||
@ -80,7 +80,7 @@ impl Command for UpdateCells {
|
||||
example: r#"[
|
||||
["2021-04-16", "2021-06-10", "2021-09-18", "2021-10-15", "2021-11-16", "2021-11-17", "2021-11-18"];
|
||||
[ 37, 0, 0, 0, 37, 0, 0]
|
||||
] | update cells -c ["2021-11-18", "2021-11-17"] {|value|
|
||||
] | update cells -c ["2021-11-18", "2021-11-17"] { |value|
|
||||
if $value == 0 {
|
||||
""
|
||||
} else {
|
||||
|
@ -33,6 +33,7 @@ pub struct Config {
|
||||
pub menu_config: HashMap<String, Value>,
|
||||
pub keybindings: Vec<ParsedKeybinding>,
|
||||
pub history_config: HashMap<String, Value>,
|
||||
pub help_config: HashMap<String, Value>,
|
||||
pub rm_always_trash: bool,
|
||||
}
|
||||
|
||||
@ -55,8 +56,9 @@ impl Default for Config {
|
||||
max_history_size: 1000,
|
||||
log_level: String::new(),
|
||||
menu_config: HashMap::new(),
|
||||
keybindings: Vec::new(),
|
||||
history_config: HashMap::new(),
|
||||
help_config: HashMap::new(),
|
||||
keybindings: Vec::new(),
|
||||
rm_always_trash: false,
|
||||
}
|
||||
}
|
||||
@ -211,13 +213,6 @@ impl Value {
|
||||
eprintln!("$config.menu_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")
|
||||
}
|
||||
}
|
||||
"history_config" => {
|
||||
if let Ok(map) = create_map(value, &config) {
|
||||
config.history_config = map;
|
||||
@ -225,6 +220,20 @@ impl Value {
|
||||
eprintln!("$config.history_config is not a record")
|
||||
}
|
||||
}
|
||||
"help_config" => {
|
||||
if let Ok(map) = create_map(value, &config) {
|
||||
config.help_config = map;
|
||||
} else {
|
||||
eprintln!("$config.help_config is not a record")
|
||||
}
|
||||
}
|
||||
"keybindings" => {
|
||||
if let Ok(keybindings) = create_keybindings(value, &config) {
|
||||
config.keybindings = keybindings;
|
||||
} else {
|
||||
eprintln!("$config.keybindings is not a valid keybindings list")
|
||||
}
|
||||
}
|
||||
x => {
|
||||
eprintln!("$config.{} is an unknown config setting", x)
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ name = "table"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
nu-ansi-term = "0.45.0"
|
||||
nu-ansi-term = "0.45.1"
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.60.1" }
|
||||
regex = "1.4"
|
||||
unicode-width = "0.1.8"
|
||||
|
Reference in New Issue
Block a user