mirror of
https://github.com/nushell/nushell.git
synced 2025-02-14 17:40:12 +01:00
455d32d9e5
Trying to reduce lint allows either by checking if they are former false positives or by fixing the underlying warning. - **Remove dead `allow(dead_code)`** - **Remove recursive dead code** - **Remove dead code** - **Move test only functions to test module** The unit tests that use them, themselves are somewhat sus in that they mock the usage and not test specificly used methods of the implementation, so there is a risk for divergence - **Remove `clippy::uninit_vec` allow.** May have been a false positive, or the impl has changed somewhat. We certainly want to look at the unsafe code here to vet for correctness.
1440 lines
49 KiB
Rust
1440 lines
49 KiB
Rust
use crate::{menus::NuMenuCompleter, NuHelpCompleter};
|
|
use crossterm::event::{KeyCode, KeyModifiers};
|
|
use nu_ansi_term::Style;
|
|
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,
|
|
engine::{EngineState, Stack, StateWorkingSet},
|
|
extract_value, Config, EditBindings, FromValue, ParsedKeybinding, ParsedMenu, PipelineData,
|
|
Record, ShellError, Span, Type, 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_IDE_COMPLETION_MENU: &str = r#"
|
|
{
|
|
name: ide_completion_menu
|
|
only_buffer_difference: false
|
|
marker: "| "
|
|
type: {
|
|
layout: ide
|
|
min_completion_width: 0,
|
|
max_completion_width: 50,
|
|
max_completion_height: 10, # will be limited by the available lines in the terminal
|
|
padding: 0,
|
|
border: true,
|
|
cursor_offset: 0,
|
|
description_mode: "prefer_right"
|
|
min_description_width: 0
|
|
max_description_width: 50
|
|
max_description_height: 10
|
|
description_offset: 1
|
|
# If true, the cursor pos will be corrected, so the suggestions match up with the typed text
|
|
#
|
|
# C:\> str
|
|
# str join
|
|
# str trim
|
|
# str split
|
|
correct_cursor_pos: false
|
|
}
|
|
style: {
|
|
text: green
|
|
selected_text: { attr: r }
|
|
description_text: yellow
|
|
match_text: { attr: u }
|
|
selected_match_text: { attr: ur }
|
|
}
|
|
}"#;
|
|
|
|
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_ref: Arc<EngineState>,
|
|
stack: &Stack,
|
|
config: Arc<Config>,
|
|
) -> Result<Reedline, ShellError> {
|
|
//log::trace!("add_menus: config: {:#?}", &config);
|
|
line_editor = line_editor.clear_menus();
|
|
|
|
for menu in &config.menus {
|
|
line_editor = add_menu(
|
|
line_editor,
|
|
menu,
|
|
engine_state_ref.clone(),
|
|
stack,
|
|
config.clone(),
|
|
)?
|
|
}
|
|
|
|
// Checking if the default menus have been added from the config file
|
|
let default_menus = [
|
|
("completion_menu", DEFAULT_COMPLETION_MENU),
|
|
("ide_completion_menu", DEFAULT_IDE_COMPLETION_MENU),
|
|
("history_menu", DEFAULT_HISTORY_MENU),
|
|
("help_menu", DEFAULT_HELP_MENU),
|
|
];
|
|
|
|
let mut engine_state = (*engine_state_ref).clone();
|
|
let mut menu_eval_results = vec![];
|
|
|
|
for (name, definition) in default_menus {
|
|
if !config
|
|
.menus
|
|
.iter()
|
|
.any(|menu| menu.name.to_expanded_string("", &config) == name)
|
|
{
|
|
let (block, delta) = {
|
|
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())
|
|
};
|
|
|
|
engine_state.merge_delta(delta)?;
|
|
|
|
let mut temp_stack = Stack::new().collect_value();
|
|
let input = PipelineData::Empty;
|
|
menu_eval_results.push(eval_block::<WithoutDebug>(
|
|
&engine_state,
|
|
&mut temp_stack,
|
|
&block,
|
|
input,
|
|
)?);
|
|
}
|
|
}
|
|
|
|
let new_engine_state_ref = Arc::new(engine_state);
|
|
|
|
for res in menu_eval_results.into_iter() {
|
|
if let PipelineData::Value(value, None) = res {
|
|
line_editor = add_menu(
|
|
line_editor,
|
|
&ParsedMenu::from_value(value)?,
|
|
new_engine_state_ref.clone(),
|
|
stack,
|
|
config.clone(),
|
|
)?;
|
|
}
|
|
}
|
|
|
|
Ok(line_editor)
|
|
}
|
|
|
|
fn add_menu(
|
|
line_editor: Reedline,
|
|
menu: &ParsedMenu,
|
|
engine_state: Arc<EngineState>,
|
|
stack: &Stack,
|
|
config: Arc<Config>,
|
|
) -> Result<Reedline, ShellError> {
|
|
let span = menu.r#type.span();
|
|
if let Value::Record { val, .. } = &menu.r#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),
|
|
str => Err(ShellError::InvalidValue {
|
|
valid: "'columnar', 'list', 'ide', or 'description'".into(),
|
|
actual: format!("'{str}'"),
|
|
span,
|
|
}),
|
|
}
|
|
} else {
|
|
Err(ShellError::RuntimeTypeMismatch {
|
|
expected: Type::record(),
|
|
actual: menu.r#type.get_type(),
|
|
span,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn get_style(record: &Record, name: &'static str, span: Span) -> Option<Style> {
|
|
extract_value(name, record, span)
|
|
.ok()
|
|
.map(|text| match text {
|
|
Value::String { val, .. } => lookup_ansi_color_style(val),
|
|
Value::Record { .. } => color_record_to_nustyle(text),
|
|
_ => lookup_ansi_color_style("green"),
|
|
})
|
|
}
|
|
|
|
fn set_menu_style<M: MenuBuilder>(mut menu: M, style: &Value) -> M {
|
|
let span = style.span();
|
|
let Value::Record { val, .. } = &style else {
|
|
return menu;
|
|
};
|
|
if let Some(style) = get_style(val, "text", span) {
|
|
menu = menu.with_text_style(style);
|
|
}
|
|
if let Some(style) = get_style(val, "selected_text", span) {
|
|
menu = menu.with_selected_text_style(style);
|
|
}
|
|
if let Some(style) = get_style(val, "description_text", span) {
|
|
menu = menu.with_description_text_style(style);
|
|
}
|
|
if let Some(style) = get_style(val, "match_text", span) {
|
|
menu = menu.with_match_text_style(style);
|
|
}
|
|
if let Some(style) = get_style(val, "selected_match_text", span) {
|
|
menu = menu.with_selected_match_text_style(style);
|
|
}
|
|
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.r#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.r#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,
|
|
};
|
|
}
|
|
|
|
columnar_menu = set_menu_style(columnar_menu, &menu.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 completer = if let Some(closure) = &menu.source {
|
|
let menu_completer = NuMenuCompleter::new(
|
|
closure.block_id,
|
|
span,
|
|
stack.captures_to_stack(closure.captures.clone()),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
);
|
|
ReedlineMenu::WithCompleter {
|
|
menu: Box::new(columnar_menu),
|
|
completer: Box::new(menu_completer),
|
|
}
|
|
} else {
|
|
ReedlineMenu::EngineCompleter(Box::new(columnar_menu))
|
|
};
|
|
|
|
Ok(line_editor.with_menu(completer))
|
|
}
|
|
|
|
// 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: Arc<Config>,
|
|
) -> Result<Reedline, ShellError> {
|
|
let name = menu.name.to_expanded_string("", &config);
|
|
let mut list_menu = ListMenu::default().with_name(&name);
|
|
|
|
let span = menu.r#type.span();
|
|
if let Value::Record { val, .. } = &menu.r#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,
|
|
};
|
|
}
|
|
|
|
list_menu = set_menu_style(list_menu, &menu.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 completer = if let Some(closure) = &menu.source {
|
|
let menu_completer = NuMenuCompleter::new(
|
|
closure.block_id,
|
|
span,
|
|
stack.captures_to_stack(closure.captures.clone()),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
);
|
|
ReedlineMenu::WithCompleter {
|
|
menu: Box::new(list_menu),
|
|
completer: Box::new(menu_completer),
|
|
}
|
|
} else {
|
|
ReedlineMenu::HistoryMenu(Box::new(list_menu))
|
|
};
|
|
|
|
Ok(line_editor.with_menu(completer))
|
|
}
|
|
|
|
// 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: Arc<Config>,
|
|
) -> Result<Reedline, ShellError> {
|
|
let span = menu.r#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.r#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::RuntimeTypeMismatch {
|
|
expected: Type::custom("bool or record"),
|
|
actual: border.get_type(),
|
|
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),
|
|
str => {
|
|
return Err(ShellError::InvalidValue {
|
|
valid: "'left', 'right', or 'prefer_right'".into(),
|
|
actual: format!("'{str}'"),
|
|
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,
|
|
};
|
|
}
|
|
|
|
ide_menu = set_menu_style(ide_menu, &menu.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 completer = if let Some(closure) = &menu.source {
|
|
let menu_completer = NuMenuCompleter::new(
|
|
closure.block_id,
|
|
span,
|
|
stack.captures_to_stack(closure.captures.clone()),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
);
|
|
ReedlineMenu::WithCompleter {
|
|
menu: Box::new(ide_menu),
|
|
completer: Box::new(menu_completer),
|
|
}
|
|
} else {
|
|
ReedlineMenu::EngineCompleter(Box::new(ide_menu))
|
|
};
|
|
|
|
Ok(line_editor.with_menu(completer))
|
|
}
|
|
|
|
// 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: Arc<Config>,
|
|
) -> Result<Reedline, ShellError> {
|
|
let name = menu.name.to_expanded_string("", &config);
|
|
let mut description_menu = DescriptionMenu::default().with_name(&name);
|
|
|
|
let span = menu.r#type.span();
|
|
if let Value::Record { val, .. } = &menu.r#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,
|
|
};
|
|
}
|
|
|
|
description_menu = set_menu_style(description_menu, &menu.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 completer = if let Some(closure) = &menu.source {
|
|
let menu_completer = NuMenuCompleter::new(
|
|
closure.block_id,
|
|
span,
|
|
stack.captures_to_stack(closure.captures.clone()),
|
|
engine_state,
|
|
only_buffer_difference,
|
|
);
|
|
ReedlineMenu::WithCompleter {
|
|
menu: Box::new(description_menu),
|
|
completer: Box::new(menu_completer),
|
|
}
|
|
} else {
|
|
let menu_completer = NuHelpCompleter::new(engine_state, config);
|
|
ReedlineMenu::WithCompleter {
|
|
menu: Box::new(description_menu),
|
|
completer: Box::new(menu_completer),
|
|
}
|
|
};
|
|
|
|
Ok(line_editor.with_menu(completer))
|
|
}
|
|
|
|
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::CONTROL,
|
|
KeyCode::Char(' '),
|
|
ReedlineEvent::UntilFound(vec![
|
|
ReedlineEvent::Menu("ide_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),
|
|
str => Err(ShellError::InvalidValue {
|
|
valid: "'emacs', 'vi_insert', or 'vi_normal'".into(),
|
|
actual: format!("'{str}'"),
|
|
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::RuntimeTypeMismatch {
|
|
expected: Type::custom("string or list<string>"),
|
|
actual: v.get_type(),
|
|
span: v.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn add_parsed_keybinding(
|
|
keybindings: &mut Keybindings,
|
|
keybinding: &ParsedKeybinding,
|
|
config: &Config,
|
|
) -> Result<(), ShellError> {
|
|
let Ok(modifier_str) = keybinding.modifier.as_str() else {
|
|
return Err(ShellError::RuntimeTypeMismatch {
|
|
expected: Type::String,
|
|
actual: keybinding.modifier.get_type(),
|
|
span: keybinding.modifier.span(),
|
|
});
|
|
};
|
|
|
|
let mut modifier = KeyModifiers::NONE;
|
|
if !str::eq_ignore_ascii_case(modifier_str, "none") {
|
|
for part in modifier_str.split('_') {
|
|
match part.to_ascii_lowercase().as_str() {
|
|
"control" => modifier |= KeyModifiers::CONTROL,
|
|
"shift" => modifier |= KeyModifiers::SHIFT,
|
|
"alt" => modifier |= KeyModifiers::ALT,
|
|
"super" => modifier |= KeyModifiers::SUPER,
|
|
"hyper" => modifier |= KeyModifiers::HYPER,
|
|
"meta" => modifier |= KeyModifiers::META,
|
|
_ => {
|
|
return Err(ShellError::InvalidValue {
|
|
valid: "'control', 'shift', 'alt', 'super', 'hyper', 'meta', or 'none'"
|
|
.into(),
|
|
actual: format!("'{part}'"),
|
|
span: keybinding.modifier.span(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let Ok(keycode) = keybinding.keycode.as_str() else {
|
|
return Err(ShellError::RuntimeTypeMismatch {
|
|
expected: Type::String,
|
|
actual: keybinding.keycode.get_type(),
|
|
span: keybinding.keycode.span(),
|
|
});
|
|
};
|
|
|
|
let keycode_lower = keycode.to_ascii_lowercase();
|
|
|
|
let keycode = if let Some(rest) = keycode_lower.strip_prefix("char_") {
|
|
let error = |valid: &str, actual: &str| ShellError::InvalidValue {
|
|
valid: valid.into(),
|
|
actual: actual.into(),
|
|
span: keybinding.keycode.span(),
|
|
};
|
|
|
|
let mut char_iter = rest.chars();
|
|
let char = match (char_iter.next(), char_iter.next()) {
|
|
(Some(char), None) => char,
|
|
(Some('u'), Some(_)) => {
|
|
// This will never panic as we know there are at least two symbols
|
|
let Ok(code_point) = u32::from_str_radix(&rest[1..], 16) else {
|
|
return Err(error("a valid hex code", keycode));
|
|
};
|
|
|
|
char::from_u32(code_point).ok_or(error("a valid Unicode code point", keycode))?
|
|
}
|
|
_ => return Err(error("'char_<char>' or 'char_u<hex code>'", keycode)),
|
|
};
|
|
|
|
KeyCode::Char(char)
|
|
} else {
|
|
match keycode_lower.as_str() {
|
|
"backspace" => KeyCode::Backspace,
|
|
"enter" => KeyCode::Enter,
|
|
"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') => c[1..]
|
|
.parse()
|
|
.ok()
|
|
.filter(|num| (1..=35).contains(num))
|
|
.map(KeyCode::F)
|
|
.ok_or(ShellError::InvalidValue {
|
|
valid: "'f1', 'f2', ..., or 'f35'".into(),
|
|
actual: format!("'{keycode}'"),
|
|
span: keybinding.keycode.span(),
|
|
})?,
|
|
"null" => KeyCode::Null,
|
|
"esc" | "escape" => KeyCode::Esc,
|
|
_ => {
|
|
return Err(ShellError::InvalidValue {
|
|
valid: "a crossterm KeyCode".into(),
|
|
actual: format!("'{keycode}'"),
|
|
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::MissingRequiredColumn {
|
|
column: "'send', 'edit', or 'until'",
|
|
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::RuntimeTypeMismatch {
|
|
expected: Type::custom("record or table"),
|
|
actual: value.get_type(),
|
|
span: value.span(),
|
|
}),
|
|
Some(event) => Ok(event),
|
|
},
|
|
Err(e) => Err(e),
|
|
})
|
|
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
|
|
|
|
Ok(Some(ReedlineEvent::UntilFound(events)))
|
|
}
|
|
v => Err(ShellError::RuntimeTypeMismatch {
|
|
expected: Type::list(Type::Any),
|
|
actual: v.get_type(),
|
|
span: v.span(),
|
|
}),
|
|
},
|
|
},
|
|
Value::List { vals, .. } => {
|
|
let events = vals
|
|
.iter()
|
|
.map(|value| match parse_event(value, config) {
|
|
Ok(inner) => match inner {
|
|
None => Err(ShellError::RuntimeTypeMismatch {
|
|
expected: Type::custom("record or table"),
|
|
actual: value.get_type(),
|
|
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::RuntimeTypeMismatch {
|
|
expected: Type::custom("record, table, or nothing"),
|
|
actual: v.get_type(),
|
|
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))
|
|
}
|
|
str => {
|
|
return Err(ShellError::InvalidValue {
|
|
valid: "a reedline event".into(),
|
|
actual: format!("'{str}'"),
|
|
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)?;
|
|
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)?;
|
|
EditCommand::CutRightUntil(char)
|
|
}
|
|
"cutrightbefore" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value)?;
|
|
EditCommand::CutRightBefore(char)
|
|
}
|
|
"moverightuntil" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value)?;
|
|
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)?;
|
|
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)?;
|
|
EditCommand::CutLeftUntil(char)
|
|
}
|
|
"cutleftbefore" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value)?;
|
|
EditCommand::CutLeftBefore(char)
|
|
}
|
|
"moveleftuntil" => {
|
|
let value = extract_value("value", record, span)?;
|
|
let char = extract_char(value)?;
|
|
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)?;
|
|
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,
|
|
#[cfg(feature = "system-clipboard")]
|
|
"cutselectionsystem" => EditCommand::CutSelectionSystem,
|
|
"copyselection" => EditCommand::CopySelection,
|
|
#[cfg(feature = "system-clipboard")]
|
|
"copyselectionsystem" => EditCommand::CopySelectionSystem,
|
|
"paste" => EditCommand::Paste,
|
|
#[cfg(feature = "system-clipboard")]
|
|
"pastesystem" => EditCommand::PasteSystem,
|
|
"selectall" => EditCommand::SelectAll,
|
|
str => {
|
|
return Err(ShellError::InvalidValue {
|
|
valid: "a reedline EditCommand".into(),
|
|
actual: format!("'{str}'"),
|
|
span,
|
|
});
|
|
}
|
|
};
|
|
|
|
Ok(edit)
|
|
}
|
|
|
|
fn extract_char(value: &Value) -> Result<char, ShellError> {
|
|
if let Ok(str) = value.as_str() {
|
|
let mut chars = str.chars();
|
|
match (chars.next(), chars.next()) {
|
|
(Some(c), None) => Ok(c),
|
|
_ => Err(ShellError::InvalidValue {
|
|
valid: "a single character".into(),
|
|
actual: format!("'{str}'"),
|
|
span: value.span(),
|
|
}),
|
|
}
|
|
} else {
|
|
Err(ShellError::RuntimeTypeMismatch {
|
|
expected: Type::String,
|
|
actual: value.get_type(),
|
|
span: value.span(),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use nu_protocol::record;
|
|
|
|
#[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::MissingRequiredColumn { .. })));
|
|
}
|
|
|
|
#[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
|
|
}]))
|
|
);
|
|
}
|
|
}
|