* 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:
Fernando Herrera
2022-03-27 14:01:04 +01:00
committed by GitHub
parent 0011f4df56
commit a4410fef40
15 changed files with 1152 additions and 192 deletions

View File

@ -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"

View File

@ -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)
}
}

View 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)
}
}

View 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)
)
}
}
}

View File

@ -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;

View File

@ -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()),
);
}

View File

@ -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!());
}

View File

@ -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"] }

View File

@ -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]

View File

@ -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 {

View File

@ -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)
}

View File

@ -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"