forked from extern/nushell
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> This PR adds a new evaluator path with callbacks to a mutable trait object implementing a Debugger trait. The trait object can do anything, e.g., profiling, code coverage, step debugging. Currently, entering/leaving a block and a pipeline element is marked with callbacks, but more callbacks can be added as necessary. Not all callbacks need to be used by all debuggers; unused ones are simply empty calls. A simple profiler is implemented as a proof of concept. The debugging support is implementing by making `eval_xxx()` functions generic depending on whether we're debugging or not. This has zero computational overhead, but makes the binary slightly larger (see benchmarks below). `eval_xxx()` variants called from commands (like `eval_block_with_early_return()` in `each`) are chosen with a dynamic dispatch for two reasons: to not grow the binary size due to duplicating the code of many commands, and for the fact that it isn't possible because it would make Command trait objects object-unsafe. In the future, I hope it will be possible to allow plugin callbacks such that users would be able to implement their profiler plugins instead of having to recompile Nushell. [DAP](https://microsoft.github.io/debug-adapter-protocol/) would also be interesting to explore. Try `help debug profile`. ## Screenshots Basic output:  To profile with more granularity, increase the profiler depth (you'll see that repeated `is-windows` calls take a large chunk of total time, making it a good candidate for optimizing):  ## Benchmarks ### Binary size Binary size increase vs. main: **+40360 bytes**. _(Both built with `--release --features=extra,dataframe`.)_ ### Time ```nushell # bench_debug.nu use std bench let test = { 1..100 | each { ls | each {|row| $row.name | str length } } | flatten | math avg } print 'debug:' let res2 = bench { debug profile $test } --pretty print $res2 ``` ```nushell # bench_nodebug.nu use std bench let test = { 1..100 | each { ls | each {|row| $row.name | str length } } | flatten | math avg } print 'no debug:' let res1 = bench { do $test } --pretty print $res1 ``` `cargo run --release -- bench_debug.nu` is consistently 1--2 ms slower than `cargo run --release -- bench_nodebug.nu` due to the collection overhead + gathering the report. This is expected. When gathering more stuff, the overhead is obviously higher. `cargo run --release -- bench_nodebug.nu` vs. `nu bench_nodebug.nu` I didn't measure any difference. Both benchmarks report times between 97 and 103 ms randomly, without one being consistently higher than the other. This suggests that at least in this particular case, when not running any debugger, there is no runtime overhead. ## API changes This PR adds a generic parameter to all `eval_xxx` functions that forces you to specify whether you use the debugger. You can resolve it in two ways: * Use a provided helper that will figure it out for you. If you wanted to use `eval_block(&engine_state, ...)`, call `let eval_block = get_eval_block(&engine_state); eval_block(&engine_state, ...)` * If you know you're in an evaluation path that doesn't need debugger support, call `eval_block::<WithoutDebug>(&engine_state, ...)` (this is the case of hooks, for example). I tried to add more explanation in the docstring of `debugger_trait.rs`. ## TODO - [x] Better profiler output to reduce spam of iterative commands like `each` - [x] Resolve `TODO: DEBUG` comments - [x] Resolve unwraps - [x] Add doc comments - [x] Add usage and extra usage for `debug profile`, explaining all columns # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> Hopefully none. # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
1496 lines
50 KiB
Rust
1496 lines
50 KiB
Rust
use crate::{menus::NuMenuCompleter, NuHelpCompleter};
|
|
use crossterm::event::{KeyCode, KeyModifiers};
|
|
use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
|
|
use nu_engine::eval_block;
|
|
use nu_parser::parse;
|
|
use nu_protocol::debugger::WithoutDebug;
|
|
use nu_protocol::{
|
|
create_menus,
|
|
engine::{EngineState, Stack, StateWorkingSet},
|
|
extract_value, Config, EditBindings, ParsedKeybinding, ParsedMenu, PipelineData, Record,
|
|
ShellError, Span, Value,
|
|
};
|
|
use reedline::{
|
|
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
|
|
ColumnarMenu, DescriptionMenu, DescriptionMode, EditCommand, IdeMenu, Keybindings, ListMenu,
|
|
MenuBuilder, Reedline, ReedlineEvent, ReedlineMenu,
|
|
};
|
|
use std::sync::Arc;
|
|
|
|
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
|
|
}
|
|
}"#;
|
|
|
|
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
|
|
}
|
|
}"#;
|
|
|
|
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
|
|
}
|
|
}"#;
|
|
|
|
// Adds all menus to line editor
|
|
pub(crate) fn add_menus(
|
|
mut line_editor: Reedline,
|
|
engine_state: Arc<EngineState>,
|
|
stack: &Stack,
|
|
config: &Config,
|
|
) -> Result<Reedline, ShellError> {
|
|
line_editor = line_editor.clear_menus();
|
|
|
|
for menu in &config.menus {
|
|
line_editor = add_menu(line_editor, menu, engine_state.clone(), stack, config)?
|
|
}
|
|
|
|
// Checking if the default menus have been added from the config file
|
|
let default_menus = [
|
|
("completion_menu", DEFAULT_COMPLETION_MENU),
|
|
("history_menu", DEFAULT_HISTORY_MENU),
|
|
("help_menu", DEFAULT_HELP_MENU),
|
|
];
|
|
|
|
for (name, definition) in default_menus {
|
|
if !config
|
|
.menus
|
|
.iter()
|
|
.any(|menu| menu.name.to_expanded_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,
|
|
);
|
|
|
|
(output, working_set.render())
|
|
};
|
|
|
|
let mut temp_stack = Stack::new();
|
|
let input = PipelineData::Empty;
|
|
|
|
let res = eval_block::<WithoutDebug>(
|
|
&engine_state,
|
|
&mut temp_stack,
|
|
&block,
|
|
input,
|
|
false,
|
|
false,
|
|
)?;
|
|
|
|
if let PipelineData::Value(value, None) = res {
|
|
for menu in create_menus(&value)? {
|
|
line_editor =
|
|
add_menu(line_editor, &menu, engine_state.clone(), stack, config)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(line_editor)
|
|
}
|
|
|
|
fn add_menu(
|
|
line_editor: Reedline,
|
|
menu: &ParsedMenu,
|
|
engine_state: Arc<EngineState>,
|
|
stack: &Stack,
|
|
config: &Config,
|
|
) -> Result<Reedline, ShellError> {
|
|
let span = menu.menu_type.span();
|
|
if let Value::Record { val, .. } = &menu.menu_type {
|
|
let layout = extract_value("layout", val, span)?.to_expanded_string("", config);
|
|
|
|
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),
|
|
"ide" => add_ide_menu(line_editor, menu, engine_state, stack, config),
|
|
"description" => add_description_menu(line_editor, menu, engine_state, stack, config),
|
|
_ => Err(ShellError::UnsupportedConfigValue {
|
|
expected: "columnar, list, ide or description".to_string(),
|
|
value: menu.menu_type.to_abbreviated_string(config),
|
|
span: menu.menu_type.span(),
|
|
}),
|
|
}
|
|
} else {
|
|
Err(ShellError::UnsupportedConfigValue {
|
|
expected: "only record type".to_string(),
|
|
value: menu.menu_type.to_abbreviated_string(config),
|
|
span: menu.menu_type.span(),
|
|
})
|
|
}
|
|
}
|
|
|
|
macro_rules! add_style {
|
|
// first arm match add!(1,2), add!(2,3) etc
|
|
($name:expr, $record: expr, $span:expr, $config: expr, $menu:expr, $f:expr) => {
|
|
$menu = match extract_value($name, $record, $span) {
|
|
Ok(text) => {
|
|
let style = match text {
|
|
Value::String { val, .. } => lookup_ansi_color_style(&val),
|
|
Value::Record { .. } => color_record_to_nustyle(&text),
|
|
_ => lookup_ansi_color_style("green"),
|
|
};
|
|
$f($menu, style)
|
|
}
|
|
Err(_) => $menu,
|
|
};
|
|
};
|
|
}
|
|
|
|
// Adds a columnar menu to the editor engine
|
|
pub(crate) fn add_columnar_menu(
|
|
line_editor: Reedline,
|
|
menu: &ParsedMenu,
|
|
engine_state: Arc<EngineState>,
|
|
stack: &Stack,
|
|
config: &Config,
|
|
) -> Result<Reedline, ShellError> {
|
|
let span = menu.menu_type.span();
|
|
let name = menu.name.to_expanded_string("", config);
|
|
let mut columnar_menu = ColumnarMenu::default().with_name(&name);
|
|
|
|
if let Value::Record { val, .. } = &menu.menu_type {
|
|
columnar_menu = match extract_value("columns", val, span) {
|
|
Ok(columns) => {
|
|
let columns = columns.as_int()?;
|
|
columnar_menu.with_columns(columns as u16)
|
|
}
|
|
Err(_) => columnar_menu,
|
|
};
|
|
|
|
columnar_menu = match extract_value("col_width", val, span) {
|
|
Ok(col_width) => {
|
|
let col_width = col_width.as_int()?;
|
|
columnar_menu.with_column_width(Some(col_width as usize))
|
|
}
|
|
Err(_) => columnar_menu.with_column_width(None),
|
|
};
|
|
|
|
columnar_menu = match extract_value("col_padding", val, span) {
|
|
Ok(col_padding) => {
|
|
let col_padding = col_padding.as_int()?;
|
|
columnar_menu.with_column_padding(col_padding as usize)
|
|
}
|
|
Err(_) => columnar_menu,
|
|
};
|
|
}
|
|
|
|
let span = menu.style.span();
|
|
if let Value::Record { val, .. } = &menu.style {
|
|
add_style!(
|
|
"text",
|
|
val,
|
|
span,
|
|
config,
|
|
columnar_menu,
|
|
ColumnarMenu::with_text_style
|
|
);
|
|
add_style!(
|
|
"selected_text",
|
|
val,
|
|
span,
|
|
config,
|
|
columnar_menu,
|
|
ColumnarMenu::with_selected_text_style
|
|
);
|
|
add_style!(
|
|
"description_text",
|
|
val,
|
|
span,
|
|
config,
|
|
columnar_menu,
|
|
ColumnarMenu::with_description_text_style
|
|
);
|
|
add_style!(
|
|
"match_text",
|
|
val,
|
|
span,
|
|
config,
|
|
columnar_menu,
|
|
ColumnarMenu::with_match_text_style
|
|
);
|
|
add_style!(
|
|
"selected_match_text",
|
|
val,
|
|
span,
|
|
config,
|
|
columnar_menu,
|
|
ColumnarMenu::with_selected_match_text_style
|
|
);
|
|
}
|
|
|
|
let marker = menu.marker.to_expanded_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);
|
|
|
|
let span = menu.source.span();
|
|
match &menu.source {
|
|
Value::Nothing { .. } => {
|
|
Ok(line_editor.with_menu(ReedlineMenu::EngineCompleter(Box::new(columnar_menu))))
|
|
}
|
|
Value::Closure { val, .. } => {
|
|
let menu_completer = NuMenuCompleter::new(
|
|
val.block_id,
|
|
span,
|
|
stack.captures_to_stack(val.captures.clone()),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
);
|
|
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
menu: Box::new(columnar_menu),
|
|
completer: Box::new(menu_completer),
|
|
}))
|
|
}
|
|
_ => Err(ShellError::UnsupportedConfigValue {
|
|
expected: "block or omitted value".to_string(),
|
|
value: menu.source.to_abbreviated_string(config),
|
|
span,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// Adds a search menu to the line editor
|
|
pub(crate) fn add_list_menu(
|
|
line_editor: Reedline,
|
|
menu: &ParsedMenu,
|
|
engine_state: Arc<EngineState>,
|
|
stack: &Stack,
|
|
config: &Config,
|
|
) -> Result<Reedline, ShellError> {
|
|
let name = menu.name.to_expanded_string("", config);
|
|
let mut list_menu = ListMenu::default().with_name(&name);
|
|
|
|
let span = menu.menu_type.span();
|
|
if let Value::Record { val, .. } = &menu.menu_type {
|
|
list_menu = match extract_value("page_size", val, span) {
|
|
Ok(page_size) => {
|
|
let page_size = page_size.as_int()?;
|
|
list_menu.with_page_size(page_size as usize)
|
|
}
|
|
Err(_) => list_menu,
|
|
};
|
|
}
|
|
|
|
let span = menu.style.span();
|
|
if let Value::Record { val, .. } = &menu.style {
|
|
add_style!(
|
|
"text",
|
|
val,
|
|
span,
|
|
config,
|
|
list_menu,
|
|
ListMenu::with_text_style
|
|
);
|
|
add_style!(
|
|
"selected_text",
|
|
val,
|
|
span,
|
|
config,
|
|
list_menu,
|
|
ListMenu::with_selected_text_style
|
|
);
|
|
add_style!(
|
|
"description_text",
|
|
val,
|
|
span,
|
|
config,
|
|
list_menu,
|
|
ListMenu::with_description_text_style
|
|
);
|
|
}
|
|
|
|
let marker = menu.marker.to_expanded_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);
|
|
|
|
let span = menu.source.span();
|
|
match &menu.source {
|
|
Value::Nothing { .. } => {
|
|
Ok(line_editor.with_menu(ReedlineMenu::HistoryMenu(Box::new(list_menu))))
|
|
}
|
|
Value::Closure { val, .. } => {
|
|
let menu_completer = NuMenuCompleter::new(
|
|
val.block_id,
|
|
span,
|
|
stack.captures_to_stack(val.captures.clone()),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
);
|
|
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
menu: Box::new(list_menu),
|
|
completer: Box::new(menu_completer),
|
|
}))
|
|
}
|
|
_ => Err(ShellError::UnsupportedConfigValue {
|
|
expected: "block or omitted value".to_string(),
|
|
value: menu.source.to_abbreviated_string(config),
|
|
span: menu.source.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
// Adds an IDE menu to the line editor
|
|
pub(crate) fn add_ide_menu(
|
|
line_editor: Reedline,
|
|
menu: &ParsedMenu,
|
|
engine_state: Arc<EngineState>,
|
|
stack: &Stack,
|
|
config: &Config,
|
|
) -> Result<Reedline, ShellError> {
|
|
let span = menu.menu_type.span();
|
|
let name = menu.name.to_expanded_string("", config);
|
|
let mut ide_menu = IdeMenu::default().with_name(&name);
|
|
|
|
if let Value::Record { val, .. } = &menu.menu_type {
|
|
ide_menu = match extract_value("min_completion_width", val, span) {
|
|
Ok(min_completion_width) => {
|
|
let min_completion_width = min_completion_width.as_int()?;
|
|
ide_menu.with_min_completion_width(min_completion_width as u16)
|
|
}
|
|
Err(_) => ide_menu,
|
|
};
|
|
|
|
ide_menu = match extract_value("max_completion_width", val, span) {
|
|
Ok(max_completion_width) => {
|
|
let max_completion_width = max_completion_width.as_int()?;
|
|
ide_menu.with_max_completion_width(max_completion_width as u16)
|
|
}
|
|
Err(_) => ide_menu,
|
|
};
|
|
|
|
ide_menu = match extract_value("max_completion_height", val, span) {
|
|
Ok(max_completion_height) => {
|
|
let max_completion_height = max_completion_height.as_int()?;
|
|
ide_menu.with_max_completion_height(max_completion_height as u16)
|
|
}
|
|
Err(_) => ide_menu.with_max_completion_height(10u16),
|
|
};
|
|
|
|
ide_menu = match extract_value("padding", val, span) {
|
|
Ok(padding) => {
|
|
let padding = padding.as_int()?;
|
|
ide_menu.with_padding(padding as u16)
|
|
}
|
|
Err(_) => ide_menu,
|
|
};
|
|
|
|
ide_menu = match extract_value("border", val, span) {
|
|
Ok(border) => {
|
|
if let Ok(border) = border.as_bool() {
|
|
if border {
|
|
ide_menu.with_default_border()
|
|
} else {
|
|
ide_menu
|
|
}
|
|
} else if let Ok(border_chars) = border.as_record() {
|
|
let top_right = extract_value("top_right", border_chars, span)?.as_char()?;
|
|
let top_left = extract_value("top_left", border_chars, span)?.as_char()?;
|
|
let bottom_right =
|
|
extract_value("bottom_right", border_chars, span)?.as_char()?;
|
|
let bottom_left =
|
|
extract_value("bottom_left", border_chars, span)?.as_char()?;
|
|
let horizontal = extract_value("horizontal", border_chars, span)?.as_char()?;
|
|
let vertical = extract_value("vertical", border_chars, span)?.as_char()?;
|
|
|
|
ide_menu.with_border(
|
|
top_right,
|
|
top_left,
|
|
bottom_right,
|
|
bottom_left,
|
|
horizontal,
|
|
vertical,
|
|
)
|
|
} else {
|
|
return Err(ShellError::UnsupportedConfigValue {
|
|
expected: "bool or record".to_string(),
|
|
value: border.to_abbreviated_string(config),
|
|
span: border.span(),
|
|
});
|
|
}
|
|
}
|
|
Err(_) => ide_menu.with_default_border(),
|
|
};
|
|
|
|
ide_menu = match extract_value("cursor_offset", val, span) {
|
|
Ok(cursor_offset) => {
|
|
let cursor_offset = cursor_offset.as_int()?;
|
|
ide_menu.with_cursor_offset(cursor_offset as i16)
|
|
}
|
|
Err(_) => ide_menu,
|
|
};
|
|
|
|
ide_menu = match extract_value("description_mode", val, span) {
|
|
Ok(description_mode) => match description_mode.coerce_str()?.as_ref() {
|
|
"left" => ide_menu.with_description_mode(DescriptionMode::Left),
|
|
"right" => ide_menu.with_description_mode(DescriptionMode::Right),
|
|
"prefer_right" => ide_menu.with_description_mode(DescriptionMode::PreferRight),
|
|
_ => {
|
|
return Err(ShellError::UnsupportedConfigValue {
|
|
expected: "\"left\", \"right\" or \"prefer_right\"".to_string(),
|
|
value: description_mode.to_abbreviated_string(config),
|
|
span: description_mode.span(),
|
|
});
|
|
}
|
|
},
|
|
Err(_) => ide_menu,
|
|
};
|
|
|
|
ide_menu = match extract_value("min_description_width", val, span) {
|
|
Ok(min_description_width) => {
|
|
let min_description_width = min_description_width.as_int()?;
|
|
ide_menu.with_min_description_width(min_description_width as u16)
|
|
}
|
|
Err(_) => ide_menu,
|
|
};
|
|
|
|
ide_menu = match extract_value("max_description_width", val, span) {
|
|
Ok(max_description_width) => {
|
|
let max_description_width = max_description_width.as_int()?;
|
|
ide_menu.with_max_description_width(max_description_width as u16)
|
|
}
|
|
Err(_) => ide_menu,
|
|
};
|
|
|
|
ide_menu = match extract_value("max_description_height", val, span) {
|
|
Ok(max_description_height) => {
|
|
let max_description_height = max_description_height.as_int()?;
|
|
ide_menu.with_max_description_height(max_description_height as u16)
|
|
}
|
|
Err(_) => ide_menu,
|
|
};
|
|
|
|
ide_menu = match extract_value("description_offset", val, span) {
|
|
Ok(description_padding) => {
|
|
let description_padding = description_padding.as_int()?;
|
|
ide_menu.with_description_offset(description_padding as u16)
|
|
}
|
|
Err(_) => ide_menu,
|
|
};
|
|
|
|
ide_menu = match extract_value("correct_cursor_pos", val, span) {
|
|
Ok(correct_cursor_pos) => {
|
|
let correct_cursor_pos = correct_cursor_pos.as_bool()?;
|
|
ide_menu.with_correct_cursor_pos(correct_cursor_pos)
|
|
}
|
|
Err(_) => ide_menu,
|
|
};
|
|
}
|
|
|
|
let span = menu.style.span();
|
|
if let Value::Record { val, .. } = &menu.style {
|
|
add_style!(
|
|
"text",
|
|
val,
|
|
span,
|
|
config,
|
|
ide_menu,
|
|
IdeMenu::with_text_style
|
|
);
|
|
add_style!(
|
|
"selected_text",
|
|
val,
|
|
span,
|
|
config,
|
|
ide_menu,
|
|
IdeMenu::with_selected_text_style
|
|
);
|
|
add_style!(
|
|
"description_text",
|
|
val,
|
|
span,
|
|
config,
|
|
ide_menu,
|
|
IdeMenu::with_description_text_style
|
|
);
|
|
add_style!(
|
|
"match_text",
|
|
val,
|
|
span,
|
|
config,
|
|
ide_menu,
|
|
IdeMenu::with_match_text_style
|
|
);
|
|
add_style!(
|
|
"selected_match_text",
|
|
val,
|
|
span,
|
|
config,
|
|
ide_menu,
|
|
IdeMenu::with_selected_match_text_style
|
|
);
|
|
}
|
|
|
|
let marker = menu.marker.to_expanded_string("", config);
|
|
ide_menu = ide_menu.with_marker(&marker);
|
|
|
|
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
|
ide_menu = ide_menu.with_only_buffer_difference(only_buffer_difference);
|
|
|
|
let span = menu.source.span();
|
|
match &menu.source {
|
|
Value::Nothing { .. } => {
|
|
Ok(line_editor.with_menu(ReedlineMenu::EngineCompleter(Box::new(ide_menu))))
|
|
}
|
|
Value::Closure { val, .. } => {
|
|
let menu_completer = NuMenuCompleter::new(
|
|
val.block_id,
|
|
span,
|
|
stack.captures_to_stack(val.captures.clone()),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
);
|
|
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
menu: Box::new(ide_menu),
|
|
completer: Box::new(menu_completer),
|
|
}))
|
|
}
|
|
_ => Err(ShellError::UnsupportedConfigValue {
|
|
expected: "block or omitted value".to_string(),
|
|
value: menu.source.to_abbreviated_string(config),
|
|
span,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// Adds a description menu to the line editor
|
|
pub(crate) fn add_description_menu(
|
|
line_editor: Reedline,
|
|
menu: &ParsedMenu,
|
|
engine_state: Arc<EngineState>,
|
|
stack: &Stack,
|
|
config: &Config,
|
|
) -> Result<Reedline, ShellError> {
|
|
let name = menu.name.to_expanded_string("", config);
|
|
let mut description_menu = DescriptionMenu::default().with_name(&name);
|
|
|
|
let span = menu.menu_type.span();
|
|
if let Value::Record { val, .. } = &menu.menu_type {
|
|
description_menu = match extract_value("columns", val, span) {
|
|
Ok(columns) => {
|
|
let columns = columns.as_int()?;
|
|
description_menu.with_columns(columns as u16)
|
|
}
|
|
Err(_) => description_menu,
|
|
};
|
|
|
|
description_menu = match extract_value("col_width", val, span) {
|
|
Ok(col_width) => {
|
|
let col_width = col_width.as_int()?;
|
|
description_menu.with_column_width(Some(col_width as usize))
|
|
}
|
|
Err(_) => description_menu.with_column_width(None),
|
|
};
|
|
|
|
description_menu = match extract_value("col_padding", val, span) {
|
|
Ok(col_padding) => {
|
|
let col_padding = col_padding.as_int()?;
|
|
description_menu.with_column_padding(col_padding as usize)
|
|
}
|
|
Err(_) => description_menu,
|
|
};
|
|
|
|
description_menu = match extract_value("selection_rows", val, span) {
|
|
Ok(selection_rows) => {
|
|
let selection_rows = selection_rows.as_int()?;
|
|
description_menu.with_selection_rows(selection_rows as u16)
|
|
}
|
|
Err(_) => description_menu,
|
|
};
|
|
|
|
description_menu = match extract_value("description_rows", val, span) {
|
|
Ok(description_rows) => {
|
|
let description_rows = description_rows.as_int()?;
|
|
description_menu.with_description_rows(description_rows as usize)
|
|
}
|
|
Err(_) => description_menu,
|
|
};
|
|
}
|
|
|
|
let span = menu.style.span();
|
|
if let Value::Record { val, .. } = &menu.style {
|
|
add_style!(
|
|
"text",
|
|
val,
|
|
span,
|
|
config,
|
|
description_menu,
|
|
DescriptionMenu::with_text_style
|
|
);
|
|
add_style!(
|
|
"selected_text",
|
|
val,
|
|
span,
|
|
config,
|
|
description_menu,
|
|
DescriptionMenu::with_selected_text_style
|
|
);
|
|
add_style!(
|
|
"description_text",
|
|
val,
|
|
span,
|
|
config,
|
|
description_menu,
|
|
DescriptionMenu::with_description_text_style
|
|
);
|
|
}
|
|
|
|
let marker = menu.marker.to_expanded_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);
|
|
|
|
let span = menu.source.span();
|
|
match &menu.source {
|
|
Value::Nothing { .. } => {
|
|
let completer = Box::new(NuHelpCompleter::new(engine_state));
|
|
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
menu: Box::new(description_menu),
|
|
completer,
|
|
}))
|
|
}
|
|
Value::Closure { val, .. } => {
|
|
let menu_completer = NuMenuCompleter::new(
|
|
val.block_id,
|
|
span,
|
|
stack.captures_to_stack(val.captures.clone()),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
);
|
|
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
menu: Box::new(description_menu),
|
|
completer: Box::new(menu_completer),
|
|
}))
|
|
}
|
|
_ => Err(ShellError::UnsupportedConfigValue {
|
|
expected: "closure or omitted value".to_string(),
|
|
value: menu.source.to_abbreviated_string(config),
|
|
span: menu.source.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn add_menu_keybindings(keybindings: &mut Keybindings) {
|
|
// Completer menu keybindings
|
|
keybindings.add_binding(
|
|
KeyModifiers::NONE,
|
|
KeyCode::Tab,
|
|
ReedlineEvent::UntilFound(vec![
|
|
ReedlineEvent::Menu("completion_menu".to_string()),
|
|
ReedlineEvent::MenuNext,
|
|
ReedlineEvent::Edit(vec![EditCommand::Complete]),
|
|
]),
|
|
);
|
|
|
|
keybindings.add_binding(
|
|
KeyModifiers::SHIFT,
|
|
KeyCode::BackTab,
|
|
ReedlineEvent::MenuPrevious,
|
|
);
|
|
|
|
keybindings.add_binding(
|
|
KeyModifiers::CONTROL,
|
|
KeyCode::Char('r'),
|
|
ReedlineEvent::Menu("history_menu".to_string()),
|
|
);
|
|
|
|
keybindings.add_binding(
|
|
KeyModifiers::CONTROL,
|
|
KeyCode::Char('x'),
|
|
ReedlineEvent::MenuPageNext,
|
|
);
|
|
|
|
keybindings.add_binding(
|
|
KeyModifiers::CONTROL,
|
|
KeyCode::Char('z'),
|
|
ReedlineEvent::UntilFound(vec![
|
|
ReedlineEvent::MenuPagePrevious,
|
|
ReedlineEvent::Edit(vec![EditCommand::Undo]),
|
|
]),
|
|
);
|
|
|
|
// Help menu keybinding
|
|
keybindings.add_binding(
|
|
KeyModifiers::NONE,
|
|
KeyCode::F(1),
|
|
ReedlineEvent::Menu("help_menu".to_string()),
|
|
);
|
|
|
|
keybindings.add_binding(
|
|
KeyModifiers::CONTROL,
|
|
KeyCode::Char('q'),
|
|
ReedlineEvent::SearchHistory,
|
|
);
|
|
}
|
|
|
|
pub enum KeybindingsMode {
|
|
Emacs(Keybindings),
|
|
Vi {
|
|
insert_keybindings: Keybindings,
|
|
normal_keybindings: Keybindings,
|
|
},
|
|
}
|
|
|
|
pub(crate) fn create_keybindings(config: &Config) -> Result<KeybindingsMode, ShellError> {
|
|
let parsed_keybindings = &config.keybindings;
|
|
|
|
let mut emacs_keybindings = default_emacs_keybindings();
|
|
let mut insert_keybindings = default_vi_insert_keybindings();
|
|
let mut normal_keybindings = default_vi_normal_keybindings();
|
|
|
|
match config.edit_mode {
|
|
EditBindings::Emacs => {
|
|
add_menu_keybindings(&mut emacs_keybindings);
|
|
}
|
|
EditBindings::Vi => {
|
|
add_menu_keybindings(&mut insert_keybindings);
|
|
add_menu_keybindings(&mut normal_keybindings);
|
|
}
|
|
}
|
|
for keybinding in parsed_keybindings {
|
|
add_keybinding(
|
|
&keybinding.mode,
|
|
keybinding,
|
|
config,
|
|
&mut emacs_keybindings,
|
|
&mut insert_keybindings,
|
|
&mut normal_keybindings,
|
|
)?
|
|
}
|
|
|
|
match config.edit_mode {
|
|
EditBindings::Emacs => Ok(KeybindingsMode::Emacs(emacs_keybindings)),
|
|
EditBindings::Vi => Ok(KeybindingsMode::Vi {
|
|
insert_keybindings,
|
|
normal_keybindings,
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn add_keybinding(
|
|
mode: &Value,
|
|
keybinding: &ParsedKeybinding,
|
|
config: &Config,
|
|
emacs_keybindings: &mut Keybindings,
|
|
insert_keybindings: &mut Keybindings,
|
|
normal_keybindings: &mut Keybindings,
|
|
) -> Result<(), ShellError> {
|
|
let span = mode.span();
|
|
match &mode {
|
|
Value::String { val, .. } => match val.as_str() {
|
|
"emacs" => add_parsed_keybinding(emacs_keybindings, keybinding, config),
|
|
"vi_insert" => add_parsed_keybinding(insert_keybindings, keybinding, config),
|
|
"vi_normal" => add_parsed_keybinding(normal_keybindings, keybinding, config),
|
|
m => Err(ShellError::UnsupportedConfigValue {
|
|
expected: "emacs, vi_insert or vi_normal".to_string(),
|
|
value: m.to_string(),
|
|
span,
|
|
}),
|
|
},
|
|
Value::List { vals, .. } => {
|
|
for inner_mode in vals {
|
|
add_keybinding(
|
|
inner_mode,
|
|
keybinding,
|
|
config,
|
|
emacs_keybindings,
|
|
insert_keybindings,
|
|
normal_keybindings,
|
|
)?
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
v => Err(ShellError::UnsupportedConfigValue {
|
|
expected: "string or list of strings".to_string(),
|
|
value: v.to_abbreviated_string(config),
|
|
span: v.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn add_parsed_keybinding(
|
|
keybindings: &mut Keybindings,
|
|
keybinding: &ParsedKeybinding,
|
|
config: &Config,
|
|
) -> Result<(), ShellError> {
|
|
let modifier = match keybinding
|
|
.modifier
|
|
.to_expanded_string("", config)
|
|
.to_ascii_lowercase()
|
|
.as_str()
|
|
{
|
|
"control" => KeyModifiers::CONTROL,
|
|
"shift" => KeyModifiers::SHIFT,
|
|
"alt" => KeyModifiers::ALT,
|
|
"none" => KeyModifiers::NONE,
|
|
"shift_alt" | "alt_shift" => KeyModifiers::SHIFT | KeyModifiers::ALT,
|
|
"control_shift" | "shift_control" => KeyModifiers::CONTROL | KeyModifiers::SHIFT,
|
|
"control_alt" | "alt_control" => KeyModifiers::CONTROL | KeyModifiers::ALT,
|
|
"control_alt_shift" | "control_shift_alt" => {
|
|
KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT
|
|
}
|
|
_ => {
|
|
return Err(ShellError::UnsupportedConfigValue {
|
|
expected: "CONTROL, SHIFT, ALT or NONE".to_string(),
|
|
value: keybinding.modifier.to_abbreviated_string(config),
|
|
span: keybinding.modifier.span(),
|
|
})
|
|
}
|
|
};
|
|
|
|
let keycode = match keybinding
|
|
.keycode
|
|
.to_expanded_string("", config)
|
|
.to_ascii_lowercase()
|
|
.as_str()
|
|
{
|
|
"backspace" => KeyCode::Backspace,
|
|
"enter" => KeyCode::Enter,
|
|
c if c.starts_with("char_") => {
|
|
let mut char_iter = c.chars().skip(5);
|
|
let pos1 = char_iter.next();
|
|
let pos2 = char_iter.next();
|
|
|
|
let char = if let (Some(char), None) = (pos1, pos2) {
|
|
char
|
|
} else {
|
|
return Err(ShellError::UnsupportedConfigValue {
|
|
expected: "char_<CHAR: unicode codepoint>".to_string(),
|
|
value: c.to_string(),
|
|
span: keybinding.keycode.span(),
|
|
});
|
|
};
|
|
|
|
KeyCode::Char(char)
|
|
}
|
|
"space" => KeyCode::Char(' '),
|
|
"down" => KeyCode::Down,
|
|
"up" => KeyCode::Up,
|
|
"left" => KeyCode::Left,
|
|
"right" => KeyCode::Right,
|
|
"home" => KeyCode::Home,
|
|
"end" => KeyCode::End,
|
|
"pageup" => KeyCode::PageUp,
|
|
"pagedown" => KeyCode::PageDown,
|
|
"tab" => KeyCode::Tab,
|
|
"backtab" => KeyCode::BackTab,
|
|
"delete" => KeyCode::Delete,
|
|
"insert" => KeyCode::Insert,
|
|
c if c.starts_with('f') => {
|
|
let fn_num: u8 = c[1..]
|
|
.parse()
|
|
.ok()
|
|
.filter(|num| matches!(num, 1..=20))
|
|
.ok_or(ShellError::UnsupportedConfigValue {
|
|
expected: "(f1|f2|...|f20)".to_string(),
|
|
value: format!("unknown function key: {c}"),
|
|
span: keybinding.keycode.span(),
|
|
})?;
|
|
KeyCode::F(fn_num)
|
|
}
|
|
"null" => KeyCode::Null,
|
|
"esc" | "escape" => KeyCode::Esc,
|
|
_ => {
|
|
return Err(ShellError::UnsupportedConfigValue {
|
|
expected: "crossterm KeyCode".to_string(),
|
|
value: keybinding.keycode.to_abbreviated_string(config),
|
|
span: keybinding.keycode.span(),
|
|
})
|
|
}
|
|
};
|
|
if let Some(event) = parse_event(&keybinding.event, config)? {
|
|
keybindings.add_binding(modifier, keycode, event);
|
|
} else {
|
|
keybindings.remove_binding(modifier, keycode);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
enum EventType<'config> {
|
|
Send(&'config Value),
|
|
Edit(&'config Value),
|
|
Until(&'config Value),
|
|
}
|
|
|
|
impl<'config> EventType<'config> {
|
|
fn try_from_record(record: &'config Record, span: Span) -> Result<Self, ShellError> {
|
|
extract_value("send", record, span)
|
|
.map(Self::Send)
|
|
.or_else(|_| extract_value("edit", record, span).map(Self::Edit))
|
|
.or_else(|_| extract_value("until", record, span).map(Self::Until))
|
|
.map_err(|_| ShellError::MissingConfigValue {
|
|
missing_value: "send, edit or until".to_string(),
|
|
span,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>, ShellError> {
|
|
let span = value.span();
|
|
match value {
|
|
Value::Record { val: record, .. } => match EventType::try_from_record(record, span)? {
|
|
EventType::Send(value) => event_from_record(
|
|
value
|
|
.to_expanded_string("", config)
|
|
.to_ascii_lowercase()
|
|
.as_str(),
|
|
record,
|
|
config,
|
|
span,
|
|
)
|
|
.map(Some),
|
|
EventType::Edit(value) => {
|
|
let edit = edit_from_record(
|
|
value
|
|
.to_expanded_string("", config)
|
|
.to_ascii_lowercase()
|
|
.as_str(),
|
|
record,
|
|
config,
|
|
span,
|
|
)?;
|
|
Ok(Some(ReedlineEvent::Edit(vec![edit])))
|
|
}
|
|
EventType::Until(value) => match value {
|
|
Value::List { vals, .. } => {
|
|
let events = vals
|
|
.iter()
|
|
.map(|value| match parse_event(value, config) {
|
|
Ok(inner) => match inner {
|
|
None => Err(ShellError::UnsupportedConfigValue {
|
|
expected: "List containing valid events".to_string(),
|
|
value: "Nothing value (null)".to_string(),
|
|
span: value.span(),
|
|
}),
|
|
Some(event) => Ok(event),
|
|
},
|
|
Err(e) => Err(e),
|
|
})
|
|
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
|
|
|
|
Ok(Some(ReedlineEvent::UntilFound(events)))
|
|
}
|
|
v => Err(ShellError::UnsupportedConfigValue {
|
|
expected: "list of events".to_string(),
|
|
value: v.to_abbreviated_string(config),
|
|
span: v.span(),
|
|
}),
|
|
},
|
|
},
|
|
Value::List { vals, .. } => {
|
|
let events = vals
|
|
.iter()
|
|
.map(|value| match parse_event(value, config) {
|
|
Ok(inner) => match inner {
|
|
None => Err(ShellError::UnsupportedConfigValue {
|
|
expected: "List containing valid events".to_string(),
|
|
value: "Nothing value (null)".to_string(),
|
|
span: value.span(),
|
|
}),
|
|
Some(event) => Ok(event),
|
|
},
|
|
Err(e) => Err(e),
|
|
})
|
|
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
|
|
|
|
Ok(Some(ReedlineEvent::Multiple(events)))
|
|
}
|
|
Value::Nothing { .. } => Ok(None),
|
|
v => Err(ShellError::UnsupportedConfigValue {
|
|
expected: "record or list of records, null to unbind key".to_string(),
|
|
value: v.to_abbreviated_string(config),
|
|
span: v.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn event_from_record(
|
|
name: &str,
|
|
record: &Record,
|
|
config: &Config,
|
|
span: Span,
|
|
) -> Result<ReedlineEvent, ShellError> {
|
|
let event = match name {
|
|
"none" => ReedlineEvent::None,
|
|
"clearscreen" => ReedlineEvent::ClearScreen,
|
|
"clearscrollback" => ReedlineEvent::ClearScrollback,
|
|
"historyhintcomplete" => ReedlineEvent::HistoryHintComplete,
|
|
"historyhintwordcomplete" => ReedlineEvent::HistoryHintWordComplete,
|
|
"ctrld" => ReedlineEvent::CtrlD,
|
|
"ctrlc" => ReedlineEvent::CtrlC,
|
|
"enter" => ReedlineEvent::Enter,
|
|
"submit" => ReedlineEvent::Submit,
|
|
"submitornewline" => ReedlineEvent::SubmitOrNewline,
|
|
"esc" | "escape" => ReedlineEvent::Esc,
|
|
"up" => ReedlineEvent::Up,
|
|
"down" => ReedlineEvent::Down,
|
|
"right" => ReedlineEvent::Right,
|
|
"left" => ReedlineEvent::Left,
|
|
"searchhistory" => ReedlineEvent::SearchHistory,
|
|
"nexthistory" => ReedlineEvent::NextHistory,
|
|
"previoushistory" => ReedlineEvent::PreviousHistory,
|
|
"repaint" => ReedlineEvent::Repaint,
|
|
"menudown" => ReedlineEvent::MenuDown,
|
|
"menuup" => ReedlineEvent::MenuUp,
|
|
"menuleft" => ReedlineEvent::MenuLeft,
|
|
"menuright" => ReedlineEvent::MenuRight,
|
|
"menunext" => ReedlineEvent::MenuNext,
|
|
"menuprevious" => ReedlineEvent::MenuPrevious,
|
|
"menupagenext" => ReedlineEvent::MenuPageNext,
|
|
"menupageprevious" => ReedlineEvent::MenuPagePrevious,
|
|
"openeditor" => ReedlineEvent::OpenEditor,
|
|
"menu" => {
|
|
let menu = extract_value("name", record, span)?;
|
|
ReedlineEvent::Menu(menu.to_expanded_string("", config))
|
|
}
|
|
"executehostcommand" => {
|
|
let cmd = extract_value("cmd", record, span)?;
|
|
ReedlineEvent::ExecuteHostCommand(cmd.to_expanded_string("", config))
|
|
}
|
|
v => {
|
|
return Err(ShellError::UnsupportedConfigValue {
|
|
expected: "Reedline event".to_string(),
|
|
value: v.to_string(),
|
|
span,
|
|
})
|
|
}
|
|
};
|
|
|
|
Ok(event)
|
|
}
|
|
|
|
fn edit_from_record(
|
|
name: &str,
|
|
record: &Record,
|
|
config: &Config,
|
|
span: Span,
|
|
) -> Result<EditCommand, ShellError> {
|
|
let edit = match name {
|
|
"movetostart" => EditCommand::MoveToStart {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"movetolinestart" => EditCommand::MoveToLineStart {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
|
|
"movetoend" => EditCommand::MoveToEnd {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"movetolineend" => EditCommand::MoveToLineEnd {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"moveleft" => EditCommand::MoveLeft {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"moveright" => EditCommand::MoveRight {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"movewordleft" => EditCommand::MoveWordLeft {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"movebigwordleft" => EditCommand::MoveBigWordLeft {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"movewordright" => EditCommand::MoveWordRight {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"movewordrightend" => EditCommand::MoveWordRightEnd {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"movebigwordrightend" => EditCommand::MoveBigWordRightEnd {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"movewordrightstart" => EditCommand::MoveWordRightStart {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"movebigwordrightstart" => EditCommand::MoveBigWordRightStart {
|
|
select: extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false),
|
|
},
|
|
"movetoposition" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let select = extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false);
|
|
|
|
EditCommand::MoveToPosition {
|
|
position: value.as_int()? as usize,
|
|
select,
|
|
}
|
|
}
|
|
"insertchar" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::InsertChar(char)
|
|
}
|
|
"insertstring" => {
|
|
let value = extract_value("value", record, span)?;
|
|
EditCommand::InsertString(value.to_expanded_string("", config))
|
|
}
|
|
"insertnewline" => EditCommand::InsertNewline,
|
|
"backspace" => EditCommand::Backspace,
|
|
"delete" => EditCommand::Delete,
|
|
"cutchar" => EditCommand::CutChar,
|
|
"backspaceword" => EditCommand::BackspaceWord,
|
|
"deleteword" => EditCommand::DeleteWord,
|
|
"clear" => EditCommand::Clear,
|
|
"cleartolineend" => EditCommand::ClearToLineEnd,
|
|
"cutcurrentline" => EditCommand::CutCurrentLine,
|
|
"cutfromstart" => EditCommand::CutFromStart,
|
|
"cutfromlinestart" => EditCommand::CutFromLineStart,
|
|
"cuttoend" => EditCommand::CutToEnd,
|
|
"cuttolineend" => EditCommand::CutToLineEnd,
|
|
"cutwordleft" => EditCommand::CutWordLeft,
|
|
"cutbigwordleft" => EditCommand::CutBigWordLeft,
|
|
"cutwordright" => EditCommand::CutWordRight,
|
|
"cutbigwordright" => EditCommand::CutBigWordRight,
|
|
"cutwordrighttonext" => EditCommand::CutWordRightToNext,
|
|
"cutbigwordrighttonext" => EditCommand::CutBigWordRightToNext,
|
|
"pastecutbufferbefore" => EditCommand::PasteCutBufferBefore,
|
|
"pastecutbufferafter" => EditCommand::PasteCutBufferAfter,
|
|
"uppercaseword" => EditCommand::UppercaseWord,
|
|
"lowercaseword" => EditCommand::LowercaseWord,
|
|
"capitalizechar" => EditCommand::CapitalizeChar,
|
|
"swapwords" => EditCommand::SwapWords,
|
|
"swapgraphemes" => EditCommand::SwapGraphemes,
|
|
"undo" => EditCommand::Undo,
|
|
"redo" => EditCommand::Redo,
|
|
"cutrightuntil" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::CutRightUntil(char)
|
|
}
|
|
"cutrightbefore" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::CutRightBefore(char)
|
|
}
|
|
"moverightuntil" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value, config)?;
|
|
let select = extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false);
|
|
EditCommand::MoveRightUntil { c: char, select }
|
|
}
|
|
"moverightbefore" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value, config)?;
|
|
let select = extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false);
|
|
EditCommand::MoveRightBefore { c: char, select }
|
|
}
|
|
"cutleftuntil" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::CutLeftUntil(char)
|
|
}
|
|
"cutleftbefore" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::CutLeftBefore(char)
|
|
}
|
|
"moveleftuntil" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value, config)?;
|
|
let select = extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false);
|
|
EditCommand::MoveLeftUntil { c: char, select }
|
|
}
|
|
"moveleftbefore" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value, config)?;
|
|
let select = extract_value("select", record, span)
|
|
.and_then(|value| value.as_bool())
|
|
.unwrap_or(false);
|
|
EditCommand::MoveLeftBefore { c: char, select }
|
|
}
|
|
"complete" => EditCommand::Complete,
|
|
"cutselection" => EditCommand::CutSelection,
|
|
"copyselection" => EditCommand::CopySelection,
|
|
"selectall" => EditCommand::SelectAll,
|
|
e => {
|
|
return Err(ShellError::UnsupportedConfigValue {
|
|
expected: "reedline EditCommand".to_string(),
|
|
value: e.to_string(),
|
|
span,
|
|
})
|
|
}
|
|
};
|
|
|
|
Ok(edit)
|
|
}
|
|
|
|
fn extract_char(value: &Value, config: &Config) -> Result<char, ShellError> {
|
|
let span = value.span();
|
|
value
|
|
.to_expanded_string("", config)
|
|
.chars()
|
|
.next()
|
|
.ok_or_else(|| ShellError::MissingConfigValue {
|
|
missing_value: "char to insert".to_string(),
|
|
span,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use nu_protocol::record;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_send_event() {
|
|
let event = record! {
|
|
"send" => Value::test_string("Enter"),
|
|
};
|
|
|
|
let span = Span::test_data();
|
|
let b = EventType::try_from_record(&event, span).unwrap();
|
|
assert!(matches!(b, EventType::Send(_)));
|
|
|
|
let event = Value::test_record(event);
|
|
let config = Config::default();
|
|
|
|
let parsed_event = parse_event(&event, &config).unwrap();
|
|
assert_eq!(parsed_event, Some(ReedlineEvent::Enter));
|
|
}
|
|
|
|
#[test]
|
|
fn test_edit_event() {
|
|
let event = record! {
|
|
"edit" => Value::test_string("Clear"),
|
|
};
|
|
|
|
let span = Span::test_data();
|
|
let b = EventType::try_from_record(&event, span).unwrap();
|
|
assert!(matches!(b, EventType::Edit(_)));
|
|
|
|
let event = Value::test_record(event);
|
|
let config = Config::default();
|
|
|
|
let parsed_event = parse_event(&event, &config).unwrap();
|
|
assert_eq!(
|
|
parsed_event,
|
|
Some(ReedlineEvent::Edit(vec![EditCommand::Clear]))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_send_menu() {
|
|
let event = record! {
|
|
"send" => Value::test_string("Menu"),
|
|
"name" => Value::test_string("history_menu"),
|
|
};
|
|
|
|
let span = Span::test_data();
|
|
let b = EventType::try_from_record(&event, span).unwrap();
|
|
assert!(matches!(b, EventType::Send(_)));
|
|
|
|
let event = Value::test_record(event);
|
|
let config = Config::default();
|
|
|
|
let parsed_event = parse_event(&event, &config).unwrap();
|
|
assert_eq!(
|
|
parsed_event,
|
|
Some(ReedlineEvent::Menu("history_menu".to_string()))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_until_event() {
|
|
let menu_event = Value::test_record(record! {
|
|
"send" => Value::test_string("Menu"),
|
|
"name" => Value::test_string("history_menu"),
|
|
});
|
|
let enter_event = Value::test_record(record! {
|
|
"send" => Value::test_string("Enter"),
|
|
});
|
|
let event = record! {
|
|
"until" => Value::list(
|
|
vec![menu_event, enter_event],
|
|
Span::test_data(),
|
|
),
|
|
};
|
|
|
|
let span = Span::test_data();
|
|
let b = EventType::try_from_record(&event, span).unwrap();
|
|
assert!(matches!(b, EventType::Until(_)));
|
|
|
|
let event = Value::test_record(event);
|
|
let config = Config::default();
|
|
|
|
let parsed_event = parse_event(&event, &config).unwrap();
|
|
assert_eq!(
|
|
parsed_event,
|
|
Some(ReedlineEvent::UntilFound(vec![
|
|
ReedlineEvent::Menu("history_menu".to_string()),
|
|
ReedlineEvent::Enter,
|
|
]))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_event() {
|
|
let menu_event = Value::test_record(record! {
|
|
"send" => Value::test_string("Menu"),
|
|
"name" => Value::test_string("history_menu"),
|
|
});
|
|
let enter_event = Value::test_record(record! {
|
|
"send" => Value::test_string("Enter"),
|
|
});
|
|
let event = Value::list(vec![menu_event, enter_event], Span::test_data());
|
|
|
|
let config = Config::default();
|
|
let parsed_event = parse_event(&event, &config).unwrap();
|
|
assert_eq!(
|
|
parsed_event,
|
|
Some(ReedlineEvent::Multiple(vec![
|
|
ReedlineEvent::Menu("history_menu".to_string()),
|
|
ReedlineEvent::Enter,
|
|
]))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_error() {
|
|
let event = record! {
|
|
"not_exist" => Value::test_string("Enter"),
|
|
};
|
|
|
|
let span = Span::test_data();
|
|
let b = EventType::try_from_record(&event, span);
|
|
assert!(matches!(b, Err(ShellError::MissingConfigValue { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_without_optional_select() {
|
|
let event = record! {
|
|
"edit" => Value::test_string("moveleft")
|
|
};
|
|
let event = Value::test_record(event);
|
|
let config = Config::default();
|
|
|
|
let parsed_event = parse_event(&event, &config).unwrap();
|
|
assert_eq!(
|
|
parsed_event,
|
|
Some(ReedlineEvent::Edit(vec![EditCommand::MoveLeft {
|
|
select: false
|
|
}]))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_with_select_false() {
|
|
let event = record! {
|
|
"edit" => Value::test_string("moveleft"),
|
|
"select" => Value::test_bool(false)
|
|
};
|
|
let event = Value::test_record(event);
|
|
let config = Config::default();
|
|
|
|
let parsed_event = parse_event(&event, &config).unwrap();
|
|
assert_eq!(
|
|
parsed_event,
|
|
Some(ReedlineEvent::Edit(vec![EditCommand::MoveLeft {
|
|
select: false
|
|
}]))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_move_with_select_true() {
|
|
let event = record! {
|
|
"edit" => Value::test_string("moveleft"),
|
|
"select" => Value::test_bool(true)
|
|
};
|
|
let event = Value::test_record(event);
|
|
let config = Config::default();
|
|
|
|
let parsed_event = parse_event(&event, &config).unwrap();
|
|
assert_eq!(
|
|
parsed_event,
|
|
Some(ReedlineEvent::Edit(vec![EditCommand::MoveLeft {
|
|
select: true
|
|
}]))
|
|
);
|
|
}
|
|
}
|