mirror of
https://github.com/nushell/nushell
synced 2024-12-27 05:23:11 +00:00
better keybinding parsing (#4543)
This commit is contained in:
parent
d53eaac7a1
commit
0f4f660759
2 changed files with 310 additions and 75 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -3312,7 +3312,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reedline"
|
name = "reedline"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
source = "git+https://github.com/nushell/reedline?branch=main#8c565e4f1de2c6dd4f67b18876d011756bfbe16d"
|
source = "git+https://github.com/nushell/reedline?branch=main#42ec23f08399519e79077921a08901420e27b05c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crossterm::event::{KeyCode, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyModifiers};
|
||||||
use nu_color_config::lookup_ansi_color_style;
|
use nu_color_config::lookup_ansi_color_style;
|
||||||
use nu_protocol::{extract_value, Config, ParsedKeybinding, ShellError, Span, Type, Value};
|
use nu_protocol::{extract_value, Config, ParsedKeybinding, ShellError, Span, Value};
|
||||||
use reedline::{
|
use reedline::{
|
||||||
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
|
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
|
||||||
CompletionMenu, EditCommand, HistoryMenu, Keybindings, Reedline, ReedlineEvent,
|
CompletionMenu, EditCommand, HistoryMenu, Keybindings, Reedline, ReedlineEvent,
|
||||||
|
@ -297,95 +297,72 @@ fn add_keybinding(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let event = parse_event(keybinding.event.clone(), config)?;
|
let event = parse_event(&keybinding.event, config)?;
|
||||||
|
|
||||||
keybindings.add_binding(modifier, keycode, event);
|
keybindings.add_binding(modifier, keycode, event);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_event(value: Value, config: &Config) -> Result<ReedlineEvent, ShellError> {
|
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 {
|
match value {
|
||||||
Value::Record { cols, vals, span } => {
|
Value::Record { cols, vals, span } => {
|
||||||
let event = match extract_value("send", &cols, &vals, &span) {
|
match EventType::try_from_columns(cols, vals, span)? {
|
||||||
Ok(event) => match event.into_string("", config).to_lowercase().as_str() {
|
EventType::Send(value) => event_from_record(
|
||||||
"none" => ReedlineEvent::None,
|
value.into_string("", config).to_lowercase().as_str(),
|
||||||
"actionhandler" => ReedlineEvent::ActionHandler,
|
cols,
|
||||||
"clearscreen" => ReedlineEvent::ClearScreen,
|
vals,
|
||||||
"historyhintcomplete" => ReedlineEvent::HistoryHintComplete,
|
config,
|
||||||
"historyhintwordcomplete" => ReedlineEvent::HistoryHintWordComplete,
|
span,
|
||||||
"ctrld" => ReedlineEvent::CtrlD,
|
),
|
||||||
"ctrlc" => ReedlineEvent::CtrlC,
|
EventType::Edit(value) => {
|
||||||
"enter" => ReedlineEvent::Enter,
|
let edit = parse_edit(value, config)?;
|
||||||
"esc" | "escape" => ReedlineEvent::Esc,
|
Ok(ReedlineEvent::Edit(vec![edit]))
|
||||||
"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))
|
|
||||||
}
|
|
||||||
"edit" => {
|
|
||||||
let edit = extract_value("edit", &cols, &vals, &span)?;
|
|
||||||
let edit = parse_edit(edit, config)?;
|
|
||||||
|
|
||||||
ReedlineEvent::Edit(vec![edit])
|
|
||||||
}
|
|
||||||
v => {
|
|
||||||
return Err(ShellError::UnsupportedConfigValue(
|
|
||||||
"Reedline event".to_string(),
|
|
||||||
v.to_string(),
|
|
||||||
span,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
let edit = extract_value("edit", &cols, &vals, &span);
|
|
||||||
let edit = match edit {
|
|
||||||
Ok(edit_value) => parse_edit(edit_value, config)?,
|
|
||||||
Err(_) => {
|
|
||||||
return Err(ShellError::MissingConfigValue(
|
|
||||||
"send or edit".to_string(),
|
|
||||||
span,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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(event)
|
Ok(ReedlineEvent::UntilFound(events))
|
||||||
|
}
|
||||||
|
v => Err(ShellError::UnsupportedConfigValue(
|
||||||
|
"list of events".to_string(),
|
||||||
|
v.into_abbreviated_string(config),
|
||||||
|
v.span()?,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
// If all the elements in the list are lists, then they represent an UntilFound event.
|
|
||||||
// This means that only one of the parsed events from the list will be executed.
|
|
||||||
// Otherwise, the expect shape should be lists of records which indicates a sequence
|
|
||||||
// of events that will happen one after the other
|
|
||||||
let until_found = vals.iter().all(|v| matches!(v.get_type(), Type::List(..)));
|
|
||||||
let events = vals
|
let events = vals
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|value| parse_event(value, config))
|
.map(|value| parse_event(value, config))
|
||||||
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
|
.collect::<Result<Vec<ReedlineEvent>, ShellError>>()?;
|
||||||
|
|
||||||
if until_found {
|
Ok(ReedlineEvent::Multiple(events))
|
||||||
Ok(ReedlineEvent::UntilFound(events))
|
|
||||||
} else {
|
|
||||||
Ok(ReedlineEvent::Multiple(events))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
v => Err(ShellError::UnsupportedConfigValue(
|
v => Err(ShellError::UnsupportedConfigValue(
|
||||||
"record or list of records".to_string(),
|
"record or list of records".to_string(),
|
||||||
|
@ -395,6 +372,51 @@ fn parse_event(value: Value, config: &Config) -> Result<ReedlineEvent, ShellErro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
v => Err(ShellError::UnsupportedConfigValue(
|
||||||
|
"Reedline event".to_string(),
|
||||||
|
v.to_string(),
|
||||||
|
*span,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_edit(edit: &Value, config: &Config) -> Result<EditCommand, ShellError> {
|
fn parse_edit(edit: &Value, config: &Config) -> Result<EditCommand, ShellError> {
|
||||||
let edit = match edit {
|
let edit = match edit {
|
||||||
Value::Record {
|
Value::Record {
|
||||||
|
@ -511,3 +533,216 @@ fn extract_char<'record>(
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| ShellError::MissingConfigValue("char to insert".to_string(), *span))
|
.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, 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(_, _))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue