nushell/src/reedline_config.rs

754 lines
25 KiB
Rust
Raw Normal View History

2022-01-18 09:48:28 +01:00
use crossterm::event::{KeyCode, KeyModifiers};
use nu_color_config::lookup_ansi_color_style;
2022-02-19 02:00:23 +01:00
use nu_protocol::{extract_value, Config, ParsedKeybinding, ShellError, Span, Value};
2022-01-18 09:48:28 +01:00
use reedline::{
2022-01-18 20:32:45 +01:00
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
CompletionMenu, EditCommand, HistoryMenu, Keybindings, Reedline, ReedlineEvent,
2022-01-18 09:48:28 +01:00
};
// Creates an input object for the completion menu based on the dictionary
2022-01-18 09:48:28 +01:00
// stored in the config variable
pub(crate) fn add_completion_menu(line_editor: Reedline, config: &Config) -> Reedline {
let mut completion_menu = CompletionMenu::default();
completion_menu = match config
2022-01-18 09:48:28 +01:00
.menu_config
.get("columns")
.and_then(|value| value.as_integer().ok())
{
Some(value) => completion_menu.with_columns(value as u16),
None => completion_menu,
2022-01-18 09:48:28 +01:00
};
completion_menu = completion_menu.with_column_width(
2022-01-18 09:48:28 +01:00
config
.menu_config
.get("col_width")
.and_then(|value| value.as_integer().ok())
.map(|value| value as usize),
);
completion_menu = match config
2022-01-18 09:48:28 +01:00
.menu_config
.get("col_padding")
.and_then(|value| value.as_integer().ok())
{
Some(value) => completion_menu.with_column_padding(value as usize),
None => completion_menu,
2022-01-18 09:48:28 +01:00
};
completion_menu = match config
2022-01-18 09:48:28 +01:00
.menu_config
.get("text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => completion_menu.with_text_style(lookup_ansi_color_style(&value)),
None => completion_menu,
2022-01-18 09:48:28 +01:00
};
completion_menu = match config
2022-01-18 09:48:28 +01:00
.menu_config
.get("selected_text_style")
.and_then(|value| value.as_string().ok())
{
Some(value) => completion_menu.with_selected_text_style(lookup_ansi_color_style(&value)),
None => completion_menu,
2022-01-27 08:53:23 +01:00
};
completion_menu = match config
2022-01-27 08:53:23 +01:00
.menu_config
.get("marker")
.and_then(|value| value.as_string().ok())
{
Some(value) => completion_menu.with_marker(value),
None => completion_menu,
2022-01-18 09:48:28 +01:00
};
line_editor.with_menu(Box::new(completion_menu))
2022-01-18 09:48:28 +01:00
}
2022-01-25 10:39:22 +01:00
// Creates an input object for the history menu based on the dictionary
// stored in the config variable
2022-01-27 08:53:23 +01:00
pub(crate) fn add_history_menu(line_editor: Reedline, config: &Config) -> Reedline {
let mut history_menu = HistoryMenu::default();
2022-01-25 10:39:22 +01:00
2022-01-27 08:53:23 +01:00
history_menu = match config
2022-01-25 10:39:22 +01:00
.history_config
.get("page_size")
.and_then(|value| value.as_integer().ok())
{
2022-01-27 08:53:23 +01:00
Some(value) => history_menu.with_page_size(value as usize),
None => history_menu,
2022-01-25 10:39:22 +01:00
};
2022-01-27 08:53:23 +01:00
history_menu = match config
2022-01-25 10:39:22 +01:00
.history_config
.get("selector")
.and_then(|value| value.as_string().ok())
{
Some(value) => {
let char = value.chars().next().unwrap_or('!');
history_menu.with_selection_char(char)
2022-01-25 10:39:22 +01:00
}
2022-01-27 08:53:23 +01:00
None => history_menu,
2022-01-25 10:39:22 +01:00
};
2022-01-27 08:53:23 +01:00
history_menu = match config
2022-01-25 10:39:22 +01:00
.history_config
.get("text_style")
.and_then(|value| value.as_string().ok())
{
2022-01-27 08:53:23 +01:00
Some(value) => history_menu.with_text_style(lookup_ansi_color_style(&value)),
None => history_menu,
2022-01-25 10:39:22 +01:00
};
2022-01-27 08:53:23 +01:00
history_menu = match config
2022-01-25 10:39:22 +01:00
.history_config
.get("selected_text_style")
.and_then(|value| value.as_string().ok())
{
2022-01-27 08:53:23 +01:00
Some(value) => history_menu.with_selected_text_style(lookup_ansi_color_style(&value)),
None => history_menu,
};
history_menu = match config
.history_config
.get("marker")
.and_then(|value| value.as_string().ok())
{
Some(value) => history_menu.with_marker(value),
None => history_menu,
2022-01-25 10:39:22 +01:00
};
line_editor.with_menu(Box::new(history_menu))
2022-01-27 08:53:23 +01:00
}
fn add_menu_keybindings(keybindings: &mut Keybindings) {
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]),
]),
);
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,
);
}
2022-01-18 20:32:45 +01:00
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;
match config.edit_mode.as_str() {
"emacs" => {
let mut keybindings = default_emacs_keybindings();
add_menu_keybindings(&mut keybindings);
2022-01-18 20:32:45 +01:00
for parsed_keybinding in parsed_keybindings {
if parsed_keybinding.mode.into_string("", config).as_str() == "emacs" {
add_keybinding(&mut keybindings, parsed_keybinding, config)?
2022-01-18 20:32:45 +01:00
}
}
Ok(KeybindingsMode::Emacs(keybindings))
}
_ => {
let mut insert_keybindings = default_vi_insert_keybindings();
let mut normal_keybindings = default_vi_normal_keybindings();
2022-01-18 09:48:28 +01:00
add_menu_keybindings(&mut insert_keybindings);
add_menu_keybindings(&mut normal_keybindings);
2022-01-18 20:32:45 +01:00
for parsed_keybinding in parsed_keybindings {
if parsed_keybinding.mode.into_string("", config).as_str() == "vi_insert" {
add_keybinding(&mut insert_keybindings, parsed_keybinding, config)?
} else if parsed_keybinding.mode.into_string("", config).as_str() == "vi_normal" {
add_keybinding(&mut normal_keybindings, parsed_keybinding, config)?
2022-01-18 20:32:45 +01:00
}
2022-01-18 09:48:28 +01:00
}
2022-01-18 20:32:45 +01:00
Ok(KeybindingsMode::Vi {
insert_keybindings,
normal_keybindings,
})
}
2022-01-18 09:48:28 +01:00
}
2022-01-18 20:32:45 +01:00
}
fn add_keybinding(
keybindings: &mut Keybindings,
keybinding: &ParsedKeybinding,
config: &Config,
2022-01-18 20:32:45 +01:00
) -> 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,
2022-01-18 20:32:45 +01:00
_ => {
return Err(ShellError::UnsupportedConfigValue(
"CONTROL, SHIFT, ALT or NONE".to_string(),
keybinding.modifier.into_abbreviated_string(config),
keybinding.modifier.span()?,
2022-01-18 20:32:45 +01:00
))
}
};
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()?,
)),
}?;
2022-01-18 20:32:45 +01:00
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,
2022-01-18 20:32:45 +01:00
_ => {
return Err(ShellError::UnsupportedConfigValue(
"crossterm KeyCode".to_string(),
keybinding.keycode.into_abbreviated_string(config),
keybinding.keycode.span()?,
2022-01-18 20:32:45 +01:00
))
}
};
2022-02-19 02:00:23 +01:00
let event = parse_event(&keybinding.event, config)?;
2022-01-18 20:32:45 +01:00
keybindings.add_binding(modifier, keycode, event);
2022-01-18 09:48:28 +01:00
2022-01-18 20:32:45 +01:00
Ok(())
2022-01-18 09:48:28 +01:00
}
2022-02-19 02:00:23 +01:00
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<ReedlineEvent, ShellError> {
match value {
Value::Record { cols, vals, span } => {
2022-02-19 02:00:23 +01:00
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,
),
EventType::Edit(value) => {
let edit = parse_edit(value, config)?;
Ok(ReedlineEvent::Edit(vec![edit]))
}
EventType::Until(value) => match value {
Value::List { vals, .. } => {
let events = vals
.iter()
.map(|value| parse_event(value, config))
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
Ok(ReedlineEvent::UntilFound(events))
}
2022-02-19 02:00:23 +01:00
v => Err(ShellError::UnsupportedConfigValue(
"list of events".to_string(),
v.into_abbreviated_string(config),
v.span()?,
)),
},
2022-02-19 02:00:23 +01:00
}
}
Value::List { vals, .. } => {
let events = vals
2022-02-19 02:00:23 +01:00
.iter()
.map(|value| parse_event(value, config))
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
2022-02-19 02:00:23 +01:00
Ok(ReedlineEvent::Multiple(events))
}
v => Err(ShellError::UnsupportedConfigValue(
"record or list of records".to_string(),
v.into_abbreviated_string(config),
v.span()?,
)),
}
}
2022-02-19 02:00:23 +01:00
fn event_from_record(
name: &str,
cols: &[String],
vals: &[Value],
config: &Config,
span: &Span,
) -> Result<ReedlineEvent, ShellError> {
match name {
"none" => Ok(ReedlineEvent::None),
"actionhandler" => Ok(ReedlineEvent::ActionHandler),
"clearscreen" => Ok(ReedlineEvent::ClearScreen),
"historyhintcomplete" => Ok(ReedlineEvent::HistoryHintComplete),
"historyhintwordcomplete" => Ok(ReedlineEvent::HistoryHintWordComplete),
"ctrld" => Ok(ReedlineEvent::CtrlD),
"ctrlc" => Ok(ReedlineEvent::CtrlC),
"enter" => Ok(ReedlineEvent::Enter),
"esc" | "escape" => Ok(ReedlineEvent::Esc),
"up" => Ok(ReedlineEvent::Up),
"down" => Ok(ReedlineEvent::Down),
"right" => Ok(ReedlineEvent::Right),
"left" => Ok(ReedlineEvent::Left),
"searchhistory" => Ok(ReedlineEvent::SearchHistory),
"nexthistory" => Ok(ReedlineEvent::NextHistory),
"previoushistory" => Ok(ReedlineEvent::PreviousHistory),
"repaint" => Ok(ReedlineEvent::Repaint),
"menudown" => Ok(ReedlineEvent::MenuDown),
"menuup" => Ok(ReedlineEvent::MenuUp),
"menuleft" => Ok(ReedlineEvent::MenuLeft),
"menuright" => Ok(ReedlineEvent::MenuRight),
"menunext" => Ok(ReedlineEvent::MenuNext),
"menuprevious" => Ok(ReedlineEvent::MenuPrevious),
"menupagenext" => Ok(ReedlineEvent::MenuPageNext),
"menupageprevious" => Ok(ReedlineEvent::MenuPagePrevious),
"menu" => {
let menu = extract_value("name", cols, vals, span)?;
Ok(ReedlineEvent::Menu(menu.into_string("", config)))
}
"edit" => {
let edit_command = parse_edit(
&Value::Record {
cols: cols.to_vec(),
vals: vals.to_vec(),
span: *span,
},
config,
)?;
Ok(ReedlineEvent::Edit(vec![edit_command]))
}
2022-02-19 02:00:23 +01:00
v => Err(ShellError::UnsupportedConfigValue(
"Reedline event".to_string(),
v.to_string(),
*span,
)),
}
}
fn parse_edit(edit: &Value, config: &Config) -> Result<EditCommand, ShellError> {
let edit = match edit {
Value::Record {
cols: edit_cols,
vals: edit_vals,
span: edit_span,
} => {
let cmd = extract_value("cmd", edit_cols, edit_vals, edit_span)?;
match cmd.into_string("", config).to_lowercase().as_str() {
"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 char = extract_char("value", edit_cols, edit_vals, config, edit_span)?;
EditCommand::InsertChar(char)
}
"insertstring" => {
let value = extract_value("value", edit_cols, edit_vals, edit_span)?;
EditCommand::InsertString(value.into_string("", config))
}
"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 char = extract_char("value", edit_cols, edit_vals, config, edit_span)?;
EditCommand::CutRightUntil(char)
}
"cutrightbefore" => {
let char = extract_char("value", edit_cols, edit_vals, config, edit_span)?;
EditCommand::CutRightBefore(char)
}
"moverightuntil" => {
let char = extract_char("value", edit_cols, edit_vals, config, edit_span)?;
EditCommand::MoveRightUntil(char)
}
"moverightbefore" => {
let char = extract_char("value", edit_cols, edit_vals, config, edit_span)?;
EditCommand::MoveRightBefore(char)
}
"cutleftuntil" => {
let char = extract_char("value", edit_cols, edit_vals, config, edit_span)?;
EditCommand::CutLeftUntil(char)
}
"cutleftbefore" => {
let char = extract_char("value", edit_cols, edit_vals, config, edit_span)?;
EditCommand::CutLeftBefore(char)
}
"moveleftuntil" => {
let char = extract_char("value", edit_cols, edit_vals, config, edit_span)?;
EditCommand::MoveLeftUntil(char)
}
"moveleftbefore" => {
let char = extract_char("value", edit_cols, edit_vals, config, edit_span)?;
EditCommand::MoveLeftBefore(char)
}
e => {
return Err(ShellError::UnsupportedConfigValue(
"reedline EditCommand".to_string(),
e.to_string(),
edit.span()?,
))
}
}
}
e => {
return Err(ShellError::UnsupportedConfigValue(
"record with EditCommand".to_string(),
e.into_abbreviated_string(config),
edit.span()?,
))
}
};
Ok(edit)
}
fn extract_char<'record>(
name: &str,
cols: &'record [String],
vals: &'record [Value],
config: &Config,
span: &Span,
) -> Result<char, ShellError> {
let value = extract_value(name, cols, vals, span)?;
value
.into_string("", config)
.chars()
.next()
.ok_or_else(|| ShellError::MissingConfigValue("char to insert".to_string(), *span))
}
2022-02-19 02:00:23 +01:00
#[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, ReedlineEvent::Enter);
}
#[test]
fn test_edit_event() {
let cols = vec!["edit".to_string()];
let vals = vec![Value::Record {
cols: vec!["cmd".to_string()],
vals: vec![Value::String {
val: "Clear".to_string(),
span: Span::test_data(),
}],
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, 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,
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,
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,
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(_, _))));
}
}