mirror of
https://github.com/nushell/nushell
synced 2024-12-31 23:39:00 +00:00
995d8db1fe
Changes to reedline since `v0.4.0`: - vi normal mode `I` for inserting at line beginning - `InsertNewline` edit command that can be bound to `Alt-Enter` if desired to have line breaks without relying on the `Validator` - `ClearScreen` will directly clear the visible screen. `Signal::CtrlL` has been removed. - `ClearScrollback` will clear the screen and scrollback. Can be used to mimic macOS `Cmd-K` screen clearing. Helps with #5089
1179 lines
37 KiB
Rust
1179 lines
37 KiB
Rust
use super::DescriptionMenu;
|
|
use crate::{menus::NuMenuCompleter, NuHelpCompleter};
|
|
use crossterm::event::{KeyCode, KeyModifiers};
|
|
use nu_color_config::lookup_ansi_color_style;
|
|
use nu_engine::eval_block;
|
|
use nu_parser::parse;
|
|
use nu_protocol::{
|
|
color_value_string, create_menus,
|
|
engine::{EngineState, Stack, StateWorkingSet},
|
|
extract_value, Config, IntoPipelineData, ParsedKeybinding, ParsedMenu, PipelineData,
|
|
ShellError, Span, Value,
|
|
};
|
|
use reedline::{
|
|
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
|
|
ColumnarMenu, EditCommand, Keybindings, ListMenu, 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 = vec![
|
|
("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.into_string("", config) == name)
|
|
{
|
|
let (block, _) = {
|
|
let mut working_set = StateWorkingSet::new(&engine_state);
|
|
let (output, _) = parse(
|
|
&mut working_set,
|
|
Some(name), // format!("entry #{}", entry_num)
|
|
definition.as_bytes(),
|
|
true,
|
|
&[],
|
|
);
|
|
|
|
(output, working_set.render())
|
|
};
|
|
|
|
let mut temp_stack = Stack::new();
|
|
let input = Value::nothing(Span::test_data()).into_pipeline_data();
|
|
let res = eval_block(&engine_state, &mut temp_stack, &block, input, false, false)?;
|
|
|
|
if let PipelineData::Value(value, None) = res {
|
|
for menu in create_menus(&value, config)? {
|
|
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> {
|
|
if let Value::Record { cols, vals, span } = &menu.menu_type {
|
|
let layout = extract_value("layout", cols, vals, span)?.into_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),
|
|
"description" => add_description_menu(line_editor, menu, engine_state, stack, config),
|
|
_ => Err(ShellError::UnsupportedConfigValue(
|
|
"columnar, list or description".to_string(),
|
|
menu.menu_type.into_abbreviated_string(config),
|
|
menu.menu_type.span()?,
|
|
)),
|
|
}
|
|
} else {
|
|
Err(ShellError::UnsupportedConfigValue(
|
|
"only record type".to_string(),
|
|
menu.menu_type.into_abbreviated_string(config),
|
|
menu.menu_type.span()?,
|
|
))
|
|
}
|
|
}
|
|
|
|
macro_rules! add_style {
|
|
// first arm match add!(1,2), add!(2,3) etc
|
|
($name:expr, $cols: expr, $vals:expr, $span:expr, $config: expr, $menu:expr, $f:expr) => {
|
|
$menu = match extract_value($name, $cols, $vals, $span) {
|
|
Ok(text) => {
|
|
let text = match text {
|
|
Value::String { val, .. } => val.clone(),
|
|
Value::Record { cols, vals, span } => {
|
|
color_value_string(span, cols, vals, $config).into_string("", $config)
|
|
}
|
|
_ => "green".to_string(),
|
|
};
|
|
let style = lookup_ansi_color_style(&text);
|
|
$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 name = menu.name.into_string("", config);
|
|
let mut columnar_menu = ColumnarMenu::default().with_name(&name);
|
|
|
|
if let Value::Record { cols, vals, span } = &menu.menu_type {
|
|
columnar_menu = match extract_value("columns", cols, vals, span) {
|
|
Ok(columns) => {
|
|
let columns = columns.as_integer()?;
|
|
columnar_menu.with_columns(columns as u16)
|
|
}
|
|
Err(_) => columnar_menu,
|
|
};
|
|
|
|
columnar_menu = match extract_value("col_width", cols, vals, span) {
|
|
Ok(col_width) => {
|
|
let col_width = col_width.as_integer()?;
|
|
columnar_menu.with_column_width(Some(col_width as usize))
|
|
}
|
|
Err(_) => columnar_menu.with_column_width(None),
|
|
};
|
|
|
|
columnar_menu = match extract_value("col_padding", cols, vals, span) {
|
|
Ok(col_padding) => {
|
|
let col_padding = col_padding.as_integer()?;
|
|
columnar_menu.with_column_padding(col_padding as usize)
|
|
}
|
|
Err(_) => columnar_menu,
|
|
};
|
|
}
|
|
|
|
if let Value::Record { cols, vals, span } = &menu.style {
|
|
add_style!(
|
|
"text",
|
|
cols,
|
|
vals,
|
|
span,
|
|
config,
|
|
columnar_menu,
|
|
ColumnarMenu::with_text_style
|
|
);
|
|
add_style!(
|
|
"selected_text",
|
|
cols,
|
|
vals,
|
|
span,
|
|
config,
|
|
columnar_menu,
|
|
ColumnarMenu::with_selected_text_style
|
|
);
|
|
add_style!(
|
|
"description_text",
|
|
cols,
|
|
vals,
|
|
span,
|
|
config,
|
|
columnar_menu,
|
|
ColumnarMenu::with_description_text_style
|
|
);
|
|
}
|
|
|
|
let marker = menu.marker.into_string("", config);
|
|
columnar_menu = columnar_menu.with_marker(marker);
|
|
|
|
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
|
columnar_menu = columnar_menu.with_only_buffer_difference(only_buffer_difference);
|
|
|
|
match &menu.source {
|
|
Value::Nothing { .. } => {
|
|
Ok(line_editor.with_menu(ReedlineMenu::EngineCompleter(Box::new(columnar_menu))))
|
|
}
|
|
Value::Block {
|
|
val,
|
|
captures,
|
|
span,
|
|
} => {
|
|
let menu_completer = NuMenuCompleter::new(
|
|
*val,
|
|
*span,
|
|
stack.captures_to_stack(captures),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
);
|
|
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
menu: Box::new(columnar_menu),
|
|
completer: Box::new(menu_completer),
|
|
}))
|
|
}
|
|
_ => Err(ShellError::UnsupportedConfigValue(
|
|
"block or omitted value".to_string(),
|
|
menu.source.into_abbreviated_string(config),
|
|
menu.source.span()?,
|
|
)),
|
|
}
|
|
}
|
|
|
|
// Adds a search menu to the line editor
|
|
pub(crate) fn add_list_menu(
|
|
line_editor: Reedline,
|
|
menu: &ParsedMenu,
|
|
engine_state: Arc<EngineState>,
|
|
stack: &Stack,
|
|
config: &Config,
|
|
) -> Result<Reedline, ShellError> {
|
|
let name = menu.name.into_string("", config);
|
|
let mut list_menu = ListMenu::default().with_name(&name);
|
|
|
|
if let Value::Record { cols, vals, span } = &menu.menu_type {
|
|
list_menu = match extract_value("page_size", cols, vals, span) {
|
|
Ok(page_size) => {
|
|
let page_size = page_size.as_integer()?;
|
|
list_menu.with_page_size(page_size as usize)
|
|
}
|
|
Err(_) => list_menu,
|
|
};
|
|
}
|
|
|
|
if let Value::Record { cols, vals, span } = &menu.style {
|
|
add_style!(
|
|
"text",
|
|
cols,
|
|
vals,
|
|
span,
|
|
config,
|
|
list_menu,
|
|
ListMenu::with_text_style
|
|
);
|
|
add_style!(
|
|
"selected_text",
|
|
cols,
|
|
vals,
|
|
span,
|
|
config,
|
|
list_menu,
|
|
ListMenu::with_selected_text_style
|
|
);
|
|
add_style!(
|
|
"description_text",
|
|
cols,
|
|
vals,
|
|
span,
|
|
config,
|
|
list_menu,
|
|
ListMenu::with_description_text_style
|
|
);
|
|
}
|
|
|
|
let marker = menu.marker.into_string("", config);
|
|
list_menu = list_menu.with_marker(marker);
|
|
|
|
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
|
list_menu = list_menu.with_only_buffer_difference(only_buffer_difference);
|
|
|
|
match &menu.source {
|
|
Value::Nothing { .. } => {
|
|
Ok(line_editor.with_menu(ReedlineMenu::HistoryMenu(Box::new(list_menu))))
|
|
}
|
|
Value::Block {
|
|
val,
|
|
captures,
|
|
span,
|
|
} => {
|
|
let menu_completer = NuMenuCompleter::new(
|
|
*val,
|
|
*span,
|
|
stack.captures_to_stack(captures),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
);
|
|
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
menu: Box::new(list_menu),
|
|
completer: Box::new(menu_completer),
|
|
}))
|
|
}
|
|
_ => Err(ShellError::UnsupportedConfigValue(
|
|
"block or omitted value".to_string(),
|
|
menu.source.into_abbreviated_string(config),
|
|
menu.source.span()?,
|
|
)),
|
|
}
|
|
}
|
|
|
|
// Adds a description menu to the line editor
|
|
pub(crate) fn add_description_menu(
|
|
line_editor: Reedline,
|
|
menu: &ParsedMenu,
|
|
engine_state: Arc<EngineState>,
|
|
stack: &Stack,
|
|
config: &Config,
|
|
) -> Result<Reedline, ShellError> {
|
|
let name = menu.name.into_string("", config);
|
|
let mut description_menu = DescriptionMenu::default().with_name(&name);
|
|
|
|
if let Value::Record { cols, vals, span } = &menu.menu_type {
|
|
description_menu = match extract_value("columns", cols, vals, span) {
|
|
Ok(columns) => {
|
|
let columns = columns.as_integer()?;
|
|
description_menu.with_columns(columns as u16)
|
|
}
|
|
Err(_) => description_menu,
|
|
};
|
|
|
|
description_menu = match extract_value("col_width", cols, vals, span) {
|
|
Ok(col_width) => {
|
|
let col_width = col_width.as_integer()?;
|
|
description_menu.with_column_width(Some(col_width as usize))
|
|
}
|
|
Err(_) => description_menu.with_column_width(None),
|
|
};
|
|
|
|
description_menu = match extract_value("col_padding", cols, vals, span) {
|
|
Ok(col_padding) => {
|
|
let col_padding = col_padding.as_integer()?;
|
|
description_menu.with_column_padding(col_padding as usize)
|
|
}
|
|
Err(_) => description_menu,
|
|
};
|
|
|
|
description_menu = match extract_value("selection_rows", cols, vals, span) {
|
|
Ok(selection_rows) => {
|
|
let selection_rows = selection_rows.as_integer()?;
|
|
description_menu.with_selection_rows(selection_rows as u16)
|
|
}
|
|
Err(_) => description_menu,
|
|
};
|
|
|
|
description_menu = match extract_value("description_rows", cols, vals, span) {
|
|
Ok(description_rows) => {
|
|
let description_rows = description_rows.as_integer()?;
|
|
description_menu.with_description_rows(description_rows as usize)
|
|
}
|
|
Err(_) => description_menu,
|
|
};
|
|
}
|
|
|
|
if let Value::Record { cols, vals, span } = &menu.style {
|
|
add_style!(
|
|
"text",
|
|
cols,
|
|
vals,
|
|
span,
|
|
config,
|
|
description_menu,
|
|
DescriptionMenu::with_text_style
|
|
);
|
|
add_style!(
|
|
"selected_text",
|
|
cols,
|
|
vals,
|
|
span,
|
|
config,
|
|
description_menu,
|
|
DescriptionMenu::with_selected_text_style
|
|
);
|
|
add_style!(
|
|
"description_text",
|
|
cols,
|
|
vals,
|
|
span,
|
|
config,
|
|
description_menu,
|
|
DescriptionMenu::with_description_text_style
|
|
);
|
|
}
|
|
|
|
let marker = menu.marker.into_string("", config);
|
|
description_menu = description_menu.with_marker(marker);
|
|
|
|
let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
|
|
description_menu = description_menu.with_only_buffer_difference(only_buffer_difference);
|
|
|
|
match &menu.source {
|
|
Value::Nothing { .. } => {
|
|
let completer = Box::new(NuHelpCompleter::new(engine_state));
|
|
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
menu: Box::new(description_menu),
|
|
completer,
|
|
}))
|
|
}
|
|
Value::Block {
|
|
val,
|
|
captures,
|
|
span,
|
|
} => {
|
|
let menu_completer = NuMenuCompleter::new(
|
|
*val,
|
|
*span,
|
|
stack.captures_to_stack(captures),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
);
|
|
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
|
|
menu: Box::new(description_menu),
|
|
completer: Box::new(menu_completer),
|
|
}))
|
|
}
|
|
_ => Err(ShellError::UnsupportedConfigValue(
|
|
"block or omitted value".to_string(),
|
|
menu.source.into_abbreviated_string(config),
|
|
menu.source.span()?,
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn add_menu_keybindings(keybindings: &mut Keybindings) {
|
|
// Completer menu keybindings
|
|
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,
|
|
);
|
|
|
|
// History menu keybinding
|
|
keybindings.add_binding(
|
|
KeyModifiers::CONTROL,
|
|
KeyCode::Char('x'),
|
|
ReedlineEvent::UntilFound(vec![
|
|
ReedlineEvent::Menu("history_menu".to_string()),
|
|
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::CONTROL,
|
|
KeyCode::Char('q'),
|
|
ReedlineEvent::Menu("help_menu".to_string()),
|
|
);
|
|
}
|
|
|
|
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.as_str() {
|
|
"emacs" => {
|
|
add_menu_keybindings(&mut emacs_keybindings);
|
|
}
|
|
_ => {
|
|
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.as_str() {
|
|
"emacs" => Ok(KeybindingsMode::Emacs(emacs_keybindings)),
|
|
_ => 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> {
|
|
match &mode {
|
|
Value::String { val, span } => 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(
|
|
"emacs, vi_insert or vi_normal".to_string(),
|
|
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(
|
|
"string or list of strings".to_string(),
|
|
v.into_abbreviated_string(config),
|
|
v.span()?,
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn add_parsed_keybinding(
|
|
keybindings: &mut Keybindings,
|
|
keybinding: &ParsedKeybinding,
|
|
config: &Config,
|
|
) -> Result<(), ShellError> {
|
|
let modifier = match keybinding
|
|
.modifier
|
|
.into_string("", config)
|
|
.to_lowercase()
|
|
.as_str()
|
|
{
|
|
"control" => KeyModifiers::CONTROL,
|
|
"shift" => KeyModifiers::SHIFT,
|
|
"alt" => KeyModifiers::ALT,
|
|
"none" => KeyModifiers::NONE,
|
|
"control | shift" => KeyModifiers::CONTROL | KeyModifiers::SHIFT,
|
|
"control | alt" => KeyModifiers::CONTROL | KeyModifiers::ALT,
|
|
"control | alt | shift" => KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT,
|
|
_ => {
|
|
return Err(ShellError::UnsupportedConfigValue(
|
|
"CONTROL, SHIFT, ALT or NONE".to_string(),
|
|
keybinding.modifier.into_abbreviated_string(config),
|
|
keybinding.modifier.span()?,
|
|
))
|
|
}
|
|
};
|
|
|
|
let keycode = match keybinding
|
|
.keycode
|
|
.into_string("", config)
|
|
.to_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 = match (pos1, pos2) {
|
|
(Some(char), None) => Ok(char),
|
|
_ => Err(ShellError::UnsupportedConfigValue(
|
|
"char_<CHAR: unicode codepoint>".to_string(),
|
|
c.to_string(),
|
|
keybinding.keycode.span()?,
|
|
)),
|
|
}?;
|
|
|
|
KeyCode::Char(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..=12))
|
|
.ok_or(ShellError::UnsupportedConfigValue(
|
|
"(f1|f2|...|f12)".to_string(),
|
|
format!("unknown function key: {}", c),
|
|
keybinding.keycode.span()?,
|
|
))?;
|
|
KeyCode::F(fn_num)
|
|
}
|
|
"null" => KeyCode::Null,
|
|
"esc" | "escape" => KeyCode::Esc,
|
|
_ => {
|
|
return Err(ShellError::UnsupportedConfigValue(
|
|
"crossterm KeyCode".to_string(),
|
|
keybinding.keycode.into_abbreviated_string(config),
|
|
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_columns(
|
|
cols: &'config [String],
|
|
vals: &'config [Value],
|
|
span: &'config Span,
|
|
) -> Result<Self, ShellError> {
|
|
extract_value("send", cols, vals, span)
|
|
.map(Self::Send)
|
|
.or_else(|_| extract_value("edit", cols, vals, span).map(Self::Edit))
|
|
.or_else(|_| extract_value("until", cols, vals, span).map(Self::Until))
|
|
.map_err(|_| ShellError::MissingConfigValue("send, edit or until".to_string(), *span))
|
|
}
|
|
}
|
|
|
|
fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>, ShellError> {
|
|
match value {
|
|
Value::Record { cols, vals, span } => {
|
|
match EventType::try_from_columns(cols, vals, span)? {
|
|
EventType::Send(value) => event_from_record(
|
|
value.into_string("", config).to_lowercase().as_str(),
|
|
cols,
|
|
vals,
|
|
config,
|
|
span,
|
|
)
|
|
.map(Some),
|
|
EventType::Edit(value) => {
|
|
let edit = edit_from_record(
|
|
value.into_string("", config).to_lowercase().as_str(),
|
|
cols,
|
|
vals,
|
|
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(
|
|
"List containing valid events".to_string(),
|
|
"Nothing value (null)".to_string(),
|
|
value.span()?,
|
|
)),
|
|
Some(event) => Ok(event),
|
|
},
|
|
Err(e) => Err(e),
|
|
})
|
|
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
|
|
|
|
Ok(Some(ReedlineEvent::UntilFound(events)))
|
|
}
|
|
v => Err(ShellError::UnsupportedConfigValue(
|
|
"list of events".to_string(),
|
|
v.into_abbreviated_string(config),
|
|
v.span()?,
|
|
)),
|
|
},
|
|
}
|
|
}
|
|
Value::List { vals, .. } => {
|
|
let events = vals
|
|
.iter()
|
|
.map(|value| match parse_event(value, config) {
|
|
Ok(inner) => match inner {
|
|
None => Err(ShellError::UnsupportedConfigValue(
|
|
"List containing valid events".to_string(),
|
|
"Nothing value (null)".to_string(),
|
|
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(
|
|
"record or list of records, null to unbind key".to_string(),
|
|
v.into_abbreviated_string(config),
|
|
v.span()?,
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn event_from_record(
|
|
name: &str,
|
|
cols: &[String],
|
|
vals: &[Value],
|
|
config: &Config,
|
|
span: &Span,
|
|
) -> Result<ReedlineEvent, ShellError> {
|
|
let event = match name {
|
|
"none" => ReedlineEvent::None,
|
|
"actionhandler" => ReedlineEvent::ActionHandler,
|
|
"clearscreen" => ReedlineEvent::ClearScreen,
|
|
"clearscrollback" => ReedlineEvent::ClearScrollback,
|
|
"historyhintcomplete" => ReedlineEvent::HistoryHintComplete,
|
|
"historyhintwordcomplete" => ReedlineEvent::HistoryHintWordComplete,
|
|
"ctrld" => ReedlineEvent::CtrlD,
|
|
"ctrlc" => ReedlineEvent::CtrlC,
|
|
"enter" => ReedlineEvent::Enter,
|
|
"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,
|
|
"menu" => {
|
|
let menu = extract_value("name", cols, vals, span)?;
|
|
ReedlineEvent::Menu(menu.into_string("", config))
|
|
}
|
|
"executehostcommand" => {
|
|
let cmd = extract_value("cmd", cols, vals, span)?;
|
|
ReedlineEvent::ExecuteHostCommand(cmd.into_string("", config))
|
|
}
|
|
v => {
|
|
return Err(ShellError::UnsupportedConfigValue(
|
|
"Reedline event".to_string(),
|
|
v.to_string(),
|
|
*span,
|
|
))
|
|
}
|
|
};
|
|
|
|
Ok(event)
|
|
}
|
|
|
|
fn edit_from_record(
|
|
name: &str,
|
|
cols: &[String],
|
|
vals: &[Value],
|
|
config: &Config,
|
|
span: &Span,
|
|
) -> Result<EditCommand, ShellError> {
|
|
let edit = match name {
|
|
"movetostart" => EditCommand::MoveToStart,
|
|
"movetolinestart" => EditCommand::MoveToLineStart,
|
|
"movetoend" => EditCommand::MoveToEnd,
|
|
"movetolineend" => EditCommand::MoveToLineEnd,
|
|
"moveleft" => EditCommand::MoveLeft,
|
|
"moveright" => EditCommand::MoveRight,
|
|
"movewordleft" => EditCommand::MoveWordLeft,
|
|
"movewordright" => EditCommand::MoveWordRight,
|
|
"insertchar" => {
|
|
let value = extract_value("value", cols, vals, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::InsertChar(char)
|
|
}
|
|
"insertstring" => {
|
|
let value = extract_value("value", cols, vals, span)?;
|
|
EditCommand::InsertString(value.into_string("", config))
|
|
}
|
|
"insertnewline" => EditCommand::InsertNewline,
|
|
"backspace" => EditCommand::Backspace,
|
|
"delete" => EditCommand::Delete,
|
|
"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,
|
|
"cutwordright" => EditCommand::CutWordRight,
|
|
"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", cols, vals, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::CutRightUntil(char)
|
|
}
|
|
"cutrightbefore" => {
|
|
let value = extract_value("value", cols, vals, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::CutRightBefore(char)
|
|
}
|
|
"moverightuntil" => {
|
|
let value = extract_value("value", cols, vals, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::MoveRightUntil(char)
|
|
}
|
|
"moverightbefore" => {
|
|
let value = extract_value("value", cols, vals, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::MoveRightBefore(char)
|
|
}
|
|
"cutleftuntil" => {
|
|
let value = extract_value("value", cols, vals, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::CutLeftUntil(char)
|
|
}
|
|
"cutleftbefore" => {
|
|
let value = extract_value("value", cols, vals, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::CutLeftBefore(char)
|
|
}
|
|
"moveleftuntil" => {
|
|
let value = extract_value("value", cols, vals, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::MoveLeftUntil(char)
|
|
}
|
|
"moveleftbefore" => {
|
|
let value = extract_value("value", cols, vals, span)?;
|
|
let char = extract_char(value, config)?;
|
|
EditCommand::MoveLeftBefore(char)
|
|
}
|
|
e => {
|
|
return Err(ShellError::UnsupportedConfigValue(
|
|
"reedline EditCommand".to_string(),
|
|
e.to_string(),
|
|
*span,
|
|
))
|
|
}
|
|
};
|
|
|
|
Ok(edit)
|
|
}
|
|
|
|
fn extract_char(value: &Value, config: &Config) -> Result<char, ShellError> {
|
|
let span = value.span()?;
|
|
value
|
|
.into_string("", config)
|
|
.chars()
|
|
.next()
|
|
.ok_or_else(|| ShellError::MissingConfigValue("char to insert".to_string(), span))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_send_event() {
|
|
let cols = vec!["send".to_string()];
|
|
let vals = vec![Value::String {
|
|
val: "Enter".to_string(),
|
|
span: Span::test_data(),
|
|
}];
|
|
|
|
let span = Span::test_data();
|
|
let b = EventType::try_from_columns(&cols, &vals, &span).unwrap();
|
|
assert!(matches!(b, EventType::Send(_)));
|
|
|
|
let event = Value::Record {
|
|
vals,
|
|
cols,
|
|
span: Span::test_data(),
|
|
};
|
|
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 cols = vec!["edit".to_string()];
|
|
let vals = vec![Value::String {
|
|
val: "Clear".to_string(),
|
|
span: Span::test_data(),
|
|
}];
|
|
|
|
let span = Span::test_data();
|
|
let b = EventType::try_from_columns(&cols, &vals, &span).unwrap();
|
|
assert!(matches!(b, EventType::Edit(_)));
|
|
|
|
let event = Value::Record {
|
|
vals,
|
|
cols,
|
|
span: Span::test_data(),
|
|
};
|
|
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 cols = vec!["send".to_string(), "name".to_string()];
|
|
let vals = vec![
|
|
Value::String {
|
|
val: "Menu".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
Value::String {
|
|
val: "history_menu".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
];
|
|
|
|
let span = Span::test_data();
|
|
let b = EventType::try_from_columns(&cols, &vals, &span).unwrap();
|
|
assert!(matches!(b, EventType::Send(_)));
|
|
|
|
let event = Value::Record {
|
|
vals,
|
|
cols,
|
|
span: Span::test_data(),
|
|
};
|
|
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() {
|
|
// Menu event
|
|
let cols = vec!["send".to_string(), "name".to_string()];
|
|
let vals = vec![
|
|
Value::String {
|
|
val: "Menu".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
Value::String {
|
|
val: "history_menu".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
];
|
|
|
|
let menu_event = Value::Record {
|
|
cols,
|
|
vals,
|
|
span: Span::test_data(),
|
|
};
|
|
|
|
// Enter event
|
|
let cols = vec!["send".to_string()];
|
|
let vals = vec![Value::String {
|
|
val: "Enter".to_string(),
|
|
span: Span::test_data(),
|
|
}];
|
|
|
|
let enter_event = Value::Record {
|
|
cols,
|
|
vals,
|
|
span: Span::test_data(),
|
|
};
|
|
|
|
// Until event
|
|
let cols = vec!["until".to_string()];
|
|
let vals = vec![Value::List {
|
|
vals: vec![menu_event, enter_event],
|
|
span: Span::test_data(),
|
|
}];
|
|
|
|
let span = Span::test_data();
|
|
let b = EventType::try_from_columns(&cols, &vals, &span).unwrap();
|
|
assert!(matches!(b, EventType::Until(_)));
|
|
|
|
let event = Value::Record {
|
|
cols,
|
|
vals,
|
|
span: Span::test_data(),
|
|
};
|
|
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() {
|
|
// Menu event
|
|
let cols = vec!["send".to_string(), "name".to_string()];
|
|
let vals = vec![
|
|
Value::String {
|
|
val: "Menu".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
Value::String {
|
|
val: "history_menu".to_string(),
|
|
span: Span::test_data(),
|
|
},
|
|
];
|
|
|
|
let menu_event = Value::Record {
|
|
cols,
|
|
vals,
|
|
span: Span::test_data(),
|
|
};
|
|
|
|
// Enter event
|
|
let cols = vec!["send".to_string()];
|
|
let vals = vec![Value::String {
|
|
val: "Enter".to_string(),
|
|
span: Span::test_data(),
|
|
}];
|
|
|
|
let enter_event = Value::Record {
|
|
cols,
|
|
vals,
|
|
span: Span::test_data(),
|
|
};
|
|
|
|
// Multiple event
|
|
let event = Value::List {
|
|
vals: vec![menu_event, enter_event],
|
|
span: 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 cols = vec!["not_exist".to_string()];
|
|
let vals = vec![Value::String {
|
|
val: "Enter".to_string(),
|
|
span: Span::test_data(),
|
|
}];
|
|
|
|
let span = Span::test_data();
|
|
let b = EventType::try_from_columns(&cols, &vals, &span);
|
|
assert!(matches!(b, Err(ShellError::MissingConfigValue(_, _))));
|
|
}
|
|
}
|