Refactor config updates (#13802)

# Description
This PR standardizes updates to the config through a new
`UpdateFromValue` trait. For now, this trait is private in case we need
to make changes to it.

Note that this PR adds some additional `ShellError` cases to create
standard error messages for config errors. A follow-up PR will move
usages of the old error cases to these new ones. This PR also uses
`Type::custom` in lots of places (e.g., for string enums). Not sure if
this is something we want to encourage.

# User-Facing Changes
Should be none.
This commit is contained in:
Ian Manske 2024-10-11 09:40:32 -07:00 committed by GitHub
parent 02313e6819
commit fce6146576
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1343 additions and 1487 deletions

View File

@ -5,11 +5,10 @@ use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style};
use nu_engine::eval_block; use nu_engine::eval_block;
use nu_parser::parse; use nu_parser::parse;
use nu_protocol::{ use nu_protocol::{
create_menus,
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
extract_value, Config, EditBindings, ParsedKeybinding, ParsedMenu, PipelineData, Record, extract_value, Config, EditBindings, FromValue, ParsedKeybinding, ParsedMenu, PipelineData,
ShellError, Span, Value, Record, ShellError, Span, Type, 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,
@ -173,17 +172,15 @@ pub(crate) fn add_menus(
for res in menu_eval_results.into_iter() { for res in menu_eval_results.into_iter() {
if let PipelineData::Value(value, None) = res { if let PipelineData::Value(value, None) = res {
for menu in create_menus(&value)? {
line_editor = add_menu( line_editor = add_menu(
line_editor, line_editor,
&menu, &ParsedMenu::from_value(value)?,
new_engine_state_ref.clone(), new_engine_state_ref.clone(),
stack, stack,
config.clone(), config.clone(),
)?; )?;
} }
} }
}
Ok(line_editor) Ok(line_editor)
} }
@ -204,22 +201,22 @@ fn add_menu(
"list" => add_list_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), "ide" => add_ide_menu(line_editor, menu, engine_state, stack, config),
"description" => add_description_menu(line_editor, menu, engine_state, stack, config), "description" => add_description_menu(line_editor, menu, engine_state, stack, config),
_ => Err(ShellError::UnsupportedConfigValue { str => Err(ShellError::InvalidValue {
expected: "columnar, list, ide or description".to_string(), valid: "'columnar', 'list', 'ide', or 'description'".into(),
value: menu.r#type.to_abbreviated_string(&config), actual: format!("'{str}'"),
span: menu.r#type.span(), span,
}), }),
} }
} else { } else {
Err(ShellError::UnsupportedConfigValue { Err(ShellError::RuntimeTypeMismatch {
expected: "only record type".to_string(), expected: Type::record(),
value: menu.r#type.to_abbreviated_string(&config), actual: menu.r#type.get_type(),
span: menu.r#type.span(), span,
}) })
} }
} }
fn get_style(record: &Record, name: &str, span: Span) -> Option<Style> { fn get_style(record: &Record, name: &'static str, span: Span) -> Option<Style> {
extract_value(name, record, span) extract_value(name, record, span)
.ok() .ok()
.map(|text| match text { .map(|text| match text {
@ -298,30 +295,23 @@ pub(crate) fn add_columnar_menu(
let only_buffer_difference = menu.only_buffer_difference.as_bool()?; let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
columnar_menu = columnar_menu.with_only_buffer_difference(only_buffer_difference); columnar_menu = columnar_menu.with_only_buffer_difference(only_buffer_difference);
let span = menu.source.span(); let completer = if let Some(closure) = &menu.source {
match &menu.source {
Value::Nothing { .. } => {
Ok(line_editor.with_menu(ReedlineMenu::EngineCompleter(Box::new(columnar_menu))))
}
Value::Closure { val, .. } => {
let menu_completer = NuMenuCompleter::new( let menu_completer = NuMenuCompleter::new(
val.block_id, closure.block_id,
span, span,
stack.captures_to_stack(val.captures.clone()), stack.captures_to_stack(closure.captures.clone()),
engine_state, engine_state,
only_buffer_difference, only_buffer_difference,
); );
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter { ReedlineMenu::WithCompleter {
menu: Box::new(columnar_menu), menu: Box::new(columnar_menu),
completer: Box::new(menu_completer), completer: Box::new(menu_completer),
}))
}
_ => Err(ShellError::UnsupportedConfigValue {
expected: "block or omitted value".to_string(),
value: menu.source.to_abbreviated_string(config),
span,
}),
} }
} else {
ReedlineMenu::EngineCompleter(Box::new(columnar_menu))
};
Ok(line_editor.with_menu(completer))
} }
// Adds a search menu to the line editor // Adds a search menu to the line editor
@ -354,30 +344,23 @@ pub(crate) fn add_list_menu(
let only_buffer_difference = menu.only_buffer_difference.as_bool()?; let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
list_menu = list_menu.with_only_buffer_difference(only_buffer_difference); list_menu = list_menu.with_only_buffer_difference(only_buffer_difference);
let span = menu.source.span(); let completer = if let Some(closure) = &menu.source {
match &menu.source {
Value::Nothing { .. } => {
Ok(line_editor.with_menu(ReedlineMenu::HistoryMenu(Box::new(list_menu))))
}
Value::Closure { val, .. } => {
let menu_completer = NuMenuCompleter::new( let menu_completer = NuMenuCompleter::new(
val.block_id, closure.block_id,
span, span,
stack.captures_to_stack(val.captures.clone()), stack.captures_to_stack(closure.captures.clone()),
engine_state, engine_state,
only_buffer_difference, only_buffer_difference,
); );
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter { ReedlineMenu::WithCompleter {
menu: Box::new(list_menu), menu: Box::new(list_menu),
completer: Box::new(menu_completer), completer: Box::new(menu_completer),
}))
}
_ => Err(ShellError::UnsupportedConfigValue {
expected: "block or omitted value".to_string(),
value: menu.source.to_abbreviated_string(&config),
span: menu.source.span(),
}),
} }
} else {
ReedlineMenu::HistoryMenu(Box::new(list_menu))
};
Ok(line_editor.with_menu(completer))
} }
// Adds an IDE menu to the line editor // Adds an IDE menu to the line editor
@ -452,9 +435,9 @@ pub(crate) fn add_ide_menu(
vertical, vertical,
) )
} else { } else {
return Err(ShellError::UnsupportedConfigValue { return Err(ShellError::RuntimeTypeMismatch {
expected: "bool or record".to_string(), expected: Type::custom("bool or record"),
value: border.to_abbreviated_string(&config), actual: border.get_type(),
span: border.span(), span: border.span(),
}); });
} }
@ -475,10 +458,10 @@ pub(crate) fn add_ide_menu(
"left" => ide_menu.with_description_mode(DescriptionMode::Left), "left" => ide_menu.with_description_mode(DescriptionMode::Left),
"right" => ide_menu.with_description_mode(DescriptionMode::Right), "right" => ide_menu.with_description_mode(DescriptionMode::Right),
"prefer_right" => ide_menu.with_description_mode(DescriptionMode::PreferRight), "prefer_right" => ide_menu.with_description_mode(DescriptionMode::PreferRight),
_ => { str => {
return Err(ShellError::UnsupportedConfigValue { return Err(ShellError::InvalidValue {
expected: "\"left\", \"right\" or \"prefer_right\"".to_string(), valid: "'left', 'right', or 'prefer_right'".into(),
value: description_mode.to_abbreviated_string(&config), actual: format!("'{str}'"),
span: description_mode.span(), span: description_mode.span(),
}); });
} }
@ -535,30 +518,23 @@ pub(crate) fn add_ide_menu(
let only_buffer_difference = menu.only_buffer_difference.as_bool()?; let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
ide_menu = ide_menu.with_only_buffer_difference(only_buffer_difference); ide_menu = ide_menu.with_only_buffer_difference(only_buffer_difference);
let span = menu.source.span(); let completer = if let Some(closure) = &menu.source {
match &menu.source {
Value::Nothing { .. } => {
Ok(line_editor.with_menu(ReedlineMenu::EngineCompleter(Box::new(ide_menu))))
}
Value::Closure { val, .. } => {
let menu_completer = NuMenuCompleter::new( let menu_completer = NuMenuCompleter::new(
val.block_id, closure.block_id,
span, span,
stack.captures_to_stack(val.captures.clone()), stack.captures_to_stack(closure.captures.clone()),
engine_state, engine_state,
only_buffer_difference, only_buffer_difference,
); );
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter { ReedlineMenu::WithCompleter {
menu: Box::new(ide_menu), menu: Box::new(ide_menu),
completer: Box::new(menu_completer), completer: Box::new(menu_completer),
}))
}
_ => Err(ShellError::UnsupportedConfigValue {
expected: "block or omitted value".to_string(),
value: menu.source.to_abbreviated_string(&config),
span,
}),
} }
} else {
ReedlineMenu::EngineCompleter(Box::new(ide_menu))
};
Ok(line_editor.with_menu(completer))
} }
// Adds a description menu to the line editor // Adds a description menu to the line editor
@ -623,34 +599,27 @@ pub(crate) fn add_description_menu(
let only_buffer_difference = menu.only_buffer_difference.as_bool()?; let only_buffer_difference = menu.only_buffer_difference.as_bool()?;
description_menu = description_menu.with_only_buffer_difference(only_buffer_difference); description_menu = description_menu.with_only_buffer_difference(only_buffer_difference);
let span = menu.source.span(); let completer = if let Some(closure) = &menu.source {
match &menu.source {
Value::Nothing { .. } => {
let completer = Box::new(NuHelpCompleter::new(engine_state, config));
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter {
menu: Box::new(description_menu),
completer,
}))
}
Value::Closure { val, .. } => {
let menu_completer = NuMenuCompleter::new( let menu_completer = NuMenuCompleter::new(
val.block_id, closure.block_id,
span, span,
stack.captures_to_stack(val.captures.clone()), stack.captures_to_stack(closure.captures.clone()),
engine_state, engine_state,
only_buffer_difference, only_buffer_difference,
); );
Ok(line_editor.with_menu(ReedlineMenu::WithCompleter { ReedlineMenu::WithCompleter {
menu: Box::new(description_menu), menu: Box::new(description_menu),
completer: Box::new(menu_completer), completer: Box::new(menu_completer),
}))
} }
_ => Err(ShellError::UnsupportedConfigValue { } else {
expected: "closure or omitted value".to_string(), let menu_completer = NuHelpCompleter::new(engine_state, config);
value: menu.source.to_abbreviated_string(&config), ReedlineMenu::WithCompleter {
span: menu.source.span(), menu: Box::new(description_menu),
}), completer: Box::new(menu_completer),
} }
};
Ok(line_editor.with_menu(completer))
} }
fn add_menu_keybindings(keybindings: &mut Keybindings) { fn add_menu_keybindings(keybindings: &mut Keybindings) {
@ -774,9 +743,9 @@ fn add_keybinding(
"emacs" => add_parsed_keybinding(emacs_keybindings, keybinding, config), "emacs" => add_parsed_keybinding(emacs_keybindings, keybinding, config),
"vi_insert" => add_parsed_keybinding(insert_keybindings, keybinding, config), "vi_insert" => add_parsed_keybinding(insert_keybindings, keybinding, config),
"vi_normal" => add_parsed_keybinding(normal_keybindings, keybinding, config), "vi_normal" => add_parsed_keybinding(normal_keybindings, keybinding, config),
m => Err(ShellError::UnsupportedConfigValue { str => Err(ShellError::InvalidValue {
expected: "emacs, vi_insert or vi_normal".to_string(), valid: "'emacs', 'vi_insert', or 'vi_normal'".into(),
value: m.to_string(), actual: format!("'{str}'"),
span, span,
}), }),
}, },
@ -794,9 +763,9 @@ fn add_keybinding(
Ok(()) Ok(())
} }
v => Err(ShellError::UnsupportedConfigValue { v => Err(ShellError::RuntimeTypeMismatch {
expected: "string or list of strings".to_string(), expected: Type::custom("string or list<string>"),
value: v.to_abbreviated_string(config), actual: v.get_type(),
span: v.span(), span: v.span(),
}), }),
} }
@ -807,14 +776,17 @@ fn add_parsed_keybinding(
keybinding: &ParsedKeybinding, keybinding: &ParsedKeybinding,
config: &Config, config: &Config,
) -> Result<(), ShellError> { ) -> Result<(), ShellError> {
let modifier_string = keybinding let Ok(modifier_str) = keybinding.modifier.as_str().map(str::to_ascii_lowercase) else {
.modifier return Err(ShellError::RuntimeTypeMismatch {
.to_expanded_string("", config) expected: Type::String,
.to_ascii_lowercase(); actual: keybinding.modifier.get_type(),
span: keybinding.modifier.span(),
});
};
let mut modifier = KeyModifiers::NONE; let mut modifier = KeyModifiers::NONE;
if modifier_string != "none" { if modifier_str != "none" {
for part in modifier_string.split('_') { for part in modifier_str.split('_') {
match part { match part {
"control" => modifier |= KeyModifiers::CONTROL, "control" => modifier |= KeyModifiers::CONTROL,
"shift" => modifier |= KeyModifiers::SHIFT, "shift" => modifier |= KeyModifiers::SHIFT,
@ -822,26 +794,30 @@ fn add_parsed_keybinding(
"super" => modifier |= KeyModifiers::SUPER, "super" => modifier |= KeyModifiers::SUPER,
"hyper" => modifier |= KeyModifiers::HYPER, "hyper" => modifier |= KeyModifiers::HYPER,
"meta" => modifier |= KeyModifiers::META, "meta" => modifier |= KeyModifiers::META,
_ => { str => {
return Err(ShellError::UnsupportedConfigValue { return Err(ShellError::InvalidValue {
expected: "CONTROL, SHIFT, ALT, SUPER, HYPER, META or NONE".to_string(), valid: "'control', 'shift', 'alt', 'super', 'hyper', 'meta', or 'none'"
value: keybinding.modifier.to_abbreviated_string(config), .into(),
actual: format!("'{str}'"),
span: keybinding.modifier.span(), 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_str = keybinding let keycode = if let Some(rest) = keycode.strip_prefix("char_") {
.keycode let error = |valid: &str, actual: &str| ShellError::InvalidValue {
.to_expanded_string("", config) valid: valid.into(),
.to_ascii_lowercase(); actual: actual.into(),
let keycode = if let Some(rest) = keycode_str.strip_prefix("char_") {
let error = |exp: &str, value| ShellError::UnsupportedConfigValue {
expected: exp.to_string(),
value,
span: keybinding.keycode.span(), span: keybinding.keycode.span(),
}; };
@ -851,22 +827,17 @@ fn add_parsed_keybinding(
(Some('u'), Some(_)) => { (Some('u'), Some(_)) => {
// This will never panic as we know there are at least two symbols // 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 { let Ok(code_point) = u32::from_str_radix(&rest[1..], 16) else {
return Err(error("valid hex code in keycode", keycode_str)); return Err(error("a valid hex code", keycode));
}; };
char::from_u32(code_point).ok_or(error("valid Unicode code point", keycode_str))? char::from_u32(code_point).ok_or(error("a valid Unicode code point", keycode))?
}
_ => {
return Err(error(
"format 'char_<char>' or 'char_u<hex code>'",
keycode_str,
))
} }
_ => return Err(error("'char_<char>' or 'char_u<hex code>'", keycode)),
}; };
KeyCode::Char(char) KeyCode::Char(char)
} else { } else {
match keycode_str.as_str() { match keycode {
"backspace" => KeyCode::Backspace, "backspace" => KeyCode::Backspace,
"enter" => KeyCode::Enter, "enter" => KeyCode::Enter,
"space" => KeyCode::Char(' '), "space" => KeyCode::Char(' '),
@ -882,29 +853,28 @@ fn add_parsed_keybinding(
"backtab" => KeyCode::BackTab, "backtab" => KeyCode::BackTab,
"delete" => KeyCode::Delete, "delete" => KeyCode::Delete,
"insert" => KeyCode::Insert, "insert" => KeyCode::Insert,
c if c.starts_with('f') => { c if c.starts_with('f') => c[1..]
let fn_num: u8 = c[1..]
.parse() .parse()
.ok() .ok()
.filter(|num| matches!(num, 1..=20)) .filter(|num| (1..=20).contains(num))
.ok_or(ShellError::UnsupportedConfigValue { .map(KeyCode::F)
expected: "(f1|f2|...|f20)".to_string(), .ok_or(ShellError::InvalidValue {
value: format!("unknown function key: {c}"), valid: "'f1', 'f2', ..., or 'f20'".into(),
actual: format!("'{c}'"),
span: keybinding.keycode.span(), span: keybinding.keycode.span(),
})?; })?,
KeyCode::F(fn_num)
}
"null" => KeyCode::Null, "null" => KeyCode::Null,
"esc" | "escape" => KeyCode::Esc, "esc" | "escape" => KeyCode::Esc,
_ => { str => {
return Err(ShellError::UnsupportedConfigValue { return Err(ShellError::InvalidValue {
expected: "crossterm KeyCode".to_string(), valid: "a crossterm KeyCode".into(),
value: keybinding.keycode.to_abbreviated_string(config), actual: format!("'{str}'"),
span: keybinding.keycode.span(), span: keybinding.keycode.span(),
}) });
} }
} }
}; };
if let Some(event) = parse_event(&keybinding.event, config)? { if let Some(event) = parse_event(&keybinding.event, config)? {
keybindings.add_binding(modifier, keycode, event); keybindings.add_binding(modifier, keycode, event);
} else { } else {
@ -926,8 +896,8 @@ impl<'config> EventType<'config> {
.map(Self::Send) .map(Self::Send)
.or_else(|_| extract_value("edit", record, span).map(Self::Edit)) .or_else(|_| extract_value("edit", record, span).map(Self::Edit))
.or_else(|_| extract_value("until", record, span).map(Self::Until)) .or_else(|_| extract_value("until", record, span).map(Self::Until))
.map_err(|_| ShellError::MissingConfigValue { .map_err(|_| ShellError::MissingRequiredColumn {
missing_value: "send, edit or until".to_string(), column: "'send', 'edit', or 'until'",
span, span,
}) })
} }
@ -965,9 +935,9 @@ fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>,
.iter() .iter()
.map(|value| match parse_event(value, config) { .map(|value| match parse_event(value, config) {
Ok(inner) => match inner { Ok(inner) => match inner {
None => Err(ShellError::UnsupportedConfigValue { None => Err(ShellError::RuntimeTypeMismatch {
expected: "List containing valid events".to_string(), expected: Type::custom("record or table"),
value: "Nothing value (null)".to_string(), actual: value.get_type(),
span: value.span(), span: value.span(),
}), }),
Some(event) => Ok(event), Some(event) => Ok(event),
@ -978,9 +948,9 @@ fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>,
Ok(Some(ReedlineEvent::UntilFound(events))) Ok(Some(ReedlineEvent::UntilFound(events)))
} }
v => Err(ShellError::UnsupportedConfigValue { v => Err(ShellError::RuntimeTypeMismatch {
expected: "list of events".to_string(), expected: Type::list(Type::Any),
value: v.to_abbreviated_string(config), actual: v.get_type(),
span: v.span(), span: v.span(),
}), }),
}, },
@ -990,9 +960,9 @@ fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>,
.iter() .iter()
.map(|value| match parse_event(value, config) { .map(|value| match parse_event(value, config) {
Ok(inner) => match inner { Ok(inner) => match inner {
None => Err(ShellError::UnsupportedConfigValue { None => Err(ShellError::RuntimeTypeMismatch {
expected: "List containing valid events".to_string(), expected: Type::custom("record or table"),
value: "Nothing value (null)".to_string(), actual: value.get_type(),
span: value.span(), span: value.span(),
}), }),
Some(event) => Ok(event), Some(event) => Ok(event),
@ -1004,9 +974,9 @@ fn parse_event(value: &Value, config: &Config) -> Result<Option<ReedlineEvent>,
Ok(Some(ReedlineEvent::Multiple(events))) Ok(Some(ReedlineEvent::Multiple(events)))
} }
Value::Nothing { .. } => Ok(None), Value::Nothing { .. } => Ok(None),
v => Err(ShellError::UnsupportedConfigValue { v => Err(ShellError::RuntimeTypeMismatch {
expected: "record or list of records, null to unbind key".to_string(), expected: Type::custom("record, table, or nothing"),
value: v.to_abbreviated_string(config), actual: v.get_type(),
span: v.span(), span: v.span(),
}), }),
} }
@ -1055,12 +1025,12 @@ fn event_from_record(
let cmd = extract_value("cmd", record, span)?; let cmd = extract_value("cmd", record, span)?;
ReedlineEvent::ExecuteHostCommand(cmd.to_expanded_string("", config)) ReedlineEvent::ExecuteHostCommand(cmd.to_expanded_string("", config))
} }
v => { str => {
return Err(ShellError::UnsupportedConfigValue { return Err(ShellError::InvalidValue {
expected: "Reedline event".to_string(), valid: "a reedline event".into(),
value: v.to_string(), actual: format!("'{str}'"),
span, span,
}) });
} }
}; };
@ -1153,7 +1123,7 @@ fn edit_from_record(
} }
"insertchar" => { "insertchar" => {
let value = extract_value("value", record, span)?; let value = extract_value("value", record, span)?;
let char = extract_char(value, config)?; let char = extract_char(value)?;
EditCommand::InsertChar(char) EditCommand::InsertChar(char)
} }
"insertstring" => { "insertstring" => {
@ -1190,17 +1160,17 @@ fn edit_from_record(
"redo" => EditCommand::Redo, "redo" => EditCommand::Redo,
"cutrightuntil" => { "cutrightuntil" => {
let value = extract_value("value", record, span)?; let value = extract_value("value", record, span)?;
let char = extract_char(value, config)?; let char = extract_char(value)?;
EditCommand::CutRightUntil(char) EditCommand::CutRightUntil(char)
} }
"cutrightbefore" => { "cutrightbefore" => {
let value = extract_value("value", record, span)?; let value = extract_value("value", record, span)?;
let char = extract_char(value, config)?; let char = extract_char(value)?;
EditCommand::CutRightBefore(char) EditCommand::CutRightBefore(char)
} }
"moverightuntil" => { "moverightuntil" => {
let value = extract_value("value", record, span)?; let value = extract_value("value", record, span)?;
let char = extract_char(value, config)?; let char = extract_char(value)?;
let select = extract_value("select", record, span) let select = extract_value("select", record, span)
.and_then(|value| value.as_bool()) .and_then(|value| value.as_bool())
.unwrap_or(false); .unwrap_or(false);
@ -1208,7 +1178,7 @@ fn edit_from_record(
} }
"moverightbefore" => { "moverightbefore" => {
let value = extract_value("value", record, span)?; let value = extract_value("value", record, span)?;
let char = extract_char(value, config)?; let char = extract_char(value)?;
let select = extract_value("select", record, span) let select = extract_value("select", record, span)
.and_then(|value| value.as_bool()) .and_then(|value| value.as_bool())
.unwrap_or(false); .unwrap_or(false);
@ -1216,17 +1186,17 @@ fn edit_from_record(
} }
"cutleftuntil" => { "cutleftuntil" => {
let value = extract_value("value", record, span)?; let value = extract_value("value", record, span)?;
let char = extract_char(value, config)?; let char = extract_char(value)?;
EditCommand::CutLeftUntil(char) EditCommand::CutLeftUntil(char)
} }
"cutleftbefore" => { "cutleftbefore" => {
let value = extract_value("value", record, span)?; let value = extract_value("value", record, span)?;
let char = extract_char(value, config)?; let char = extract_char(value)?;
EditCommand::CutLeftBefore(char) EditCommand::CutLeftBefore(char)
} }
"moveleftuntil" => { "moveleftuntil" => {
let value = extract_value("value", record, span)?; let value = extract_value("value", record, span)?;
let char = extract_char(value, config)?; let char = extract_char(value)?;
let select = extract_value("select", record, span) let select = extract_value("select", record, span)
.and_then(|value| value.as_bool()) .and_then(|value| value.as_bool())
.unwrap_or(false); .unwrap_or(false);
@ -1234,7 +1204,7 @@ fn edit_from_record(
} }
"moveleftbefore" => { "moveleftbefore" => {
let value = extract_value("value", record, span)?; let value = extract_value("value", record, span)?;
let char = extract_char(value, config)?; let char = extract_char(value)?;
let select = extract_value("select", record, span) let select = extract_value("select", record, span)
.and_then(|value| value.as_bool()) .and_then(|value| value.as_bool())
.unwrap_or(false); .unwrap_or(false);
@ -1251,28 +1221,36 @@ fn edit_from_record(
#[cfg(feature = "system-clipboard")] #[cfg(feature = "system-clipboard")]
"pastesystem" => EditCommand::PasteSystem, "pastesystem" => EditCommand::PasteSystem,
"selectall" => EditCommand::SelectAll, "selectall" => EditCommand::SelectAll,
e => { str => {
return Err(ShellError::UnsupportedConfigValue { return Err(ShellError::InvalidValue {
expected: "reedline EditCommand".to_string(), valid: "a reedline EditCommand".into(),
value: e.to_string(), actual: format!("'{str}'"),
span, span,
}) });
} }
}; };
Ok(edit) Ok(edit)
} }
fn extract_char(value: &Value, config: &Config) -> Result<char, ShellError> { fn extract_char(value: &Value) -> Result<char, ShellError> {
let span = value.span(); if let Ok(str) = value.as_str() {
value let mut chars = str.chars();
.to_expanded_string("", config) match (chars.next(), chars.next()) {
.chars() (Some(c), None) => Ok(c),
.next() _ => Err(ShellError::InvalidValue {
.ok_or_else(|| ShellError::MissingConfigValue { valid: "a single character".into(),
missing_value: "char to insert".to_string(), actual: format!("'{str}'"),
span, span: value.span(),
}),
}
} else {
Err(ShellError::RuntimeTypeMismatch {
expected: Type::String,
actual: value.get_type(),
span: value.span(),
}) })
}
} }
#[cfg(test)] #[cfg(test)]
@ -1401,7 +1379,7 @@ mod test {
let span = Span::test_data(); let span = Span::test_data();
let b = EventType::try_from_record(&event, span); let b = EventType::try_from_record(&event, span);
assert!(matches!(b, Err(ShellError::MissingConfigValue { .. }))); assert!(matches!(b, Err(ShellError::MissingRequiredColumn { .. })));
} }
#[test] #[test]

View File

@ -86,11 +86,12 @@ pub fn eval_hook(
); );
if let Some(err) = working_set.parse_errors.first() { if let Some(err) = working_set.parse_errors.first() {
report_parse_error(&working_set, err); report_parse_error(&working_set, err);
return Err(ShellError::GenericError {
return Err(ShellError::UnsupportedConfigValue { error: format!("Failed to run {hook_name} hook"),
expected: "valid source code".into(), msg: "source code has errors".into(),
value: "source code with syntax errors".into(), span: Some(span),
span, help: None,
inner: Vec::new(),
}); });
} }
@ -161,10 +162,10 @@ pub fn eval_hook(
{ {
val val
} else { } else {
return Err(ShellError::UnsupportedConfigValue { return Err(ShellError::RuntimeTypeMismatch {
expected: "boolean output".to_string(), expected: Type::Bool,
value: "other PipelineData variant".to_string(), actual: pipeline_data.get_type(),
span: other_span, span: pipeline_data.span().unwrap_or(other_span),
}); });
} }
} }
@ -173,9 +174,9 @@ pub fn eval_hook(
} }
} }
} else { } else {
return Err(ShellError::UnsupportedConfigValue { return Err(ShellError::RuntimeTypeMismatch {
expected: "block".to_string(), expected: Type::Closure,
value: format!("{}", condition.get_type()), actual: condition.get_type(),
span: other_span, span: other_span,
}); });
} }
@ -218,11 +219,12 @@ pub fn eval_hook(
); );
if let Some(err) = working_set.parse_errors.first() { if let Some(err) = working_set.parse_errors.first() {
report_parse_error(&working_set, err); report_parse_error(&working_set, err);
return Err(ShellError::GenericError {
return Err(ShellError::UnsupportedConfigValue { error: format!("Failed to run {hook_name} hook"),
expected: "valid source code".into(), msg: "source code has errors".into(),
value: "source code with syntax errors".into(), span: Some(span),
span: source_span, help: None,
inner: Vec::new(),
}); });
} }
@ -257,9 +259,9 @@ pub fn eval_hook(
run_hook(engine_state, stack, val, input, arguments, source_span)?; run_hook(engine_state, stack, val, input, arguments, source_span)?;
} }
other => { other => {
return Err(ShellError::UnsupportedConfigValue { return Err(ShellError::RuntimeTypeMismatch {
expected: "block or string".to_string(), expected: Type::custom("string or closure"),
value: format!("{}", other.get_type()), actual: other.get_type(),
span: source_span, span: source_span,
}); });
} }
@ -270,9 +272,9 @@ pub fn eval_hook(
output = run_hook(engine_state, stack, val, input, arguments, span)?; output = run_hook(engine_state, stack, val, input, arguments, span)?;
} }
other => { other => {
return Err(ShellError::UnsupportedConfigValue { return Err(ShellError::RuntimeTypeMismatch {
expected: "string, block, record, or list of commands".into(), expected: Type::custom("string, closure, record, or list"),
value: format!("{}", other.get_type()), actual: other.get_type(),
span: other.span(), span: other.span(),
}); });
} }

View File

@ -2,9 +2,8 @@ use crate::{color_record_to_nustyle, lookup_ansi_color_style, text_style::Alignm
use nu_ansi_term::{Color, Style}; use nu_ansi_term::{Color, Style};
use nu_engine::ClosureEvalOnce; use nu_engine::ClosureEvalOnce;
use nu_protocol::{ use nu_protocol::{
cli_error::CliError, engine::{Closure, EngineState, Stack},
engine::{Closure, EngineState, Stack, StateWorkingSet}, report_shell_error, Span, Value,
Span, Value,
}; };
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -70,14 +69,8 @@ impl<'a> StyleComputer<'a> {
_ => Style::default(), _ => Style::default(),
} }
} }
// This is basically a copy of nu_cli::report_error(), but that isn't usable due to Err(err) => {
// dependencies. While crudely spitting out a bunch of errors like this is not ideal, report_shell_error(self.engine_state, &err);
// currently hook closure errors behave roughly the same.
Err(e) => {
eprintln!(
"Error: {:?}",
CliError(&e, &StateWorkingSet::new(self.engine_state))
);
Style::default() Style::default()
} }
} }

View File

@ -300,14 +300,14 @@ impl UrlComponents {
return Ok(true); return Ok(true);
} }
match key { match key {
"host" => Err(ShellError::UnsupportedConfigValue { "host" => Err(ShellError::InvalidValue {
expected: "non-empty string".into(), valid: "a non-empty string".into(),
value: "empty string".into(), actual: format!("'{s}'"),
span: value_span, span: value_span,
}), }),
"scheme" => Err(ShellError::UnsupportedConfigValue { "scheme" => Err(ShellError::InvalidValue {
expected: "non-empty string".into(), valid: "a non-empty string".into(),
value: "empty string".into(), actual: format!("'{s}'"),
span: value_span, span: value_span,
}), }),
_ => Ok(false), _ => Ok(false),

View File

@ -1,83 +0,0 @@
use super::prelude::*;
use crate as nu_protocol;
use crate::engine::Closure;
#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompletionAlgorithm {
#[default]
Prefix,
Fuzzy,
}
impl FromStr for CompletionAlgorithm {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"prefix" => Ok(Self::Prefix),
"fuzzy" => Ok(Self::Fuzzy),
_ => Err("expected either 'prefix' or 'fuzzy'"),
}
}
}
#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompletionSort {
#[default]
Smart,
Alphabetical,
}
impl FromStr for CompletionSort {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"smart" => Ok(Self::Smart),
"alphabetical" => Ok(Self::Alphabetical),
_ => Err("expected either 'smart' or 'alphabetical'"),
}
}
}
#[derive(Clone, Debug, IntoValue, Serialize, Deserialize)]
pub struct ExternalCompleterConfig {
pub enable: bool,
pub max_results: i64,
pub completer: Option<Closure>,
}
impl Default for ExternalCompleterConfig {
fn default() -> Self {
Self {
enable: true,
max_results: 100,
completer: None,
}
}
}
#[derive(Clone, Debug, IntoValue, Serialize, Deserialize)]
pub struct CompleterConfig {
pub sort: CompletionSort,
pub case_sensitive: bool,
pub quick: bool,
pub partial: bool,
pub algorithm: CompletionAlgorithm,
pub external: ExternalCompleterConfig,
pub use_ls_colors: bool,
}
impl Default for CompleterConfig {
fn default() -> Self {
Self {
sort: CompletionSort::default(),
case_sensitive: false,
quick: true,
partial: true,
algorithm: CompletionAlgorithm::default(),
external: ExternalCompleterConfig::default(),
use_ls_colors: true,
}
}
}

View File

@ -0,0 +1,151 @@
use super::{config_update_string_enum, prelude::*};
use crate as nu_protocol;
use crate::engine::Closure;
#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompletionAlgorithm {
#[default]
Prefix,
Fuzzy,
}
impl FromStr for CompletionAlgorithm {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"prefix" => Ok(Self::Prefix),
"fuzzy" => Ok(Self::Fuzzy),
_ => Err("'prefix' or 'fuzzy'"),
}
}
}
impl UpdateFromValue for CompletionAlgorithm {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
config_update_string_enum(self, value, path, errors)
}
}
#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompletionSort {
#[default]
Smart,
Alphabetical,
}
impl FromStr for CompletionSort {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"smart" => Ok(Self::Smart),
"alphabetical" => Ok(Self::Alphabetical),
_ => Err("'smart' or 'alphabetical'"),
}
}
}
impl UpdateFromValue for CompletionSort {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
config_update_string_enum(self, value, path, errors)
}
}
#[derive(Clone, Debug, IntoValue, Serialize, Deserialize)]
pub struct ExternalCompleterConfig {
pub enable: bool,
pub max_results: i64,
pub completer: Option<Closure>,
}
impl Default for ExternalCompleterConfig {
fn default() -> Self {
Self {
enable: true,
max_results: 100,
completer: None,
}
}
}
impl UpdateFromValue for ExternalCompleterConfig {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"completer" => match val {
Value::Nothing { .. } => self.completer = None,
Value::Closure { val, .. } => self.completer = Some(val.as_ref().clone()),
_ => errors.type_mismatch(path, Type::custom("closure or nothing"), val),
},
"max_results" => self.max_results.update(val, path, errors),
"enable" => self.enable.update(val, path, errors),
_ => errors.unknown_option(path, val),
}
}
}
}
#[derive(Clone, Debug, IntoValue, Serialize, Deserialize)]
pub struct CompletionConfig {
pub sort: CompletionSort,
pub case_sensitive: bool,
pub quick: bool,
pub partial: bool,
pub algorithm: CompletionAlgorithm,
pub external: ExternalCompleterConfig,
pub use_ls_colors: bool,
}
impl Default for CompletionConfig {
fn default() -> Self {
Self {
sort: CompletionSort::default(),
case_sensitive: false,
quick: true,
partial: true,
algorithm: CompletionAlgorithm::default(),
external: ExternalCompleterConfig::default(),
use_ls_colors: true,
}
}
}
impl UpdateFromValue for CompletionConfig {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"sort" => self.sort.update(val, path, errors),
"quick" => self.quick.update(val, path, errors),
"partial" => self.partial.update(val, path, errors),
"algorithm" => self.algorithm.update(val, path, errors),
"case_sensitive" => self.case_sensitive.update(val, path, errors),
"external" => self.external.update(val, path, errors),
"use_ls_colors" => self.use_ls_colors.update(val, path, errors),
_ => errors.unknown_option(path, val),
}
}
}
}

View File

@ -6,3 +6,34 @@ pub struct DatetimeFormatConfig {
pub normal: Option<String>, pub normal: Option<String>,
pub table: Option<String>, pub table: Option<String>,
} }
impl UpdateFromValue for DatetimeFormatConfig {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"normal" => match val {
Value::Nothing { .. } => self.normal = None,
Value::String { val, .. } => self.normal = Some(val.clone()),
_ => errors.type_mismatch(path, Type::custom("string or nothing"), val),
},
"table" => match val {
Value::Nothing { .. } => self.table = None,
Value::String { val, .. } => self.table = Some(val.clone()),
_ => errors.type_mismatch(path, Type::custom("string or nothing"), val),
},
_ => errors.unknown_option(path, val),
}
}
}
}

View File

@ -27,3 +27,26 @@ impl Default for DisplayErrors {
} }
} }
} }
impl UpdateFromValue for DisplayErrors {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"exit_code" => self.exit_code.update(val, path, errors),
"termination_signal" => self.termination_signal.update(val, path, errors),
_ => errors.unknown_option(path, val),
}
}
}
}

View File

@ -0,0 +1,85 @@
use super::ConfigPath;
use crate::{Config, ConfigError, ShellError, Span, Type, Value};
#[derive(Debug)]
pub(super) struct ConfigErrors<'a> {
config: &'a Config,
errors: Vec<ConfigError>,
}
impl<'a> ConfigErrors<'a> {
pub fn new(config: &'a Config) -> Self {
Self {
config,
errors: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn error(&mut self, error: ConfigError) {
self.errors.push(error);
}
pub fn type_mismatch(&mut self, path: &ConfigPath, expected: Type, actual: &Value) {
self.error(ConfigError::TypeMismatch {
path: path.to_string(),
expected,
actual: actual.get_type(),
span: actual.span(),
});
}
pub fn invalid_value(
&mut self,
path: &ConfigPath,
expected: impl Into<String>,
actual: &Value,
) {
self.error(ConfigError::InvalidValue {
path: path.to_string(),
valid: expected.into(),
actual: if let Ok(str) = actual.as_str() {
format!("'{str}'")
} else {
actual.to_abbreviated_string(self.config)
},
span: actual.span(),
});
}
pub fn missing_column(&mut self, path: &ConfigPath, column: &'static str, span: Span) {
self.error(ConfigError::MissingRequiredColumn {
path: path.to_string(),
column,
span,
})
}
pub fn unknown_option(&mut self, path: &ConfigPath, value: &Value) {
self.error(ConfigError::UnknownOption {
path: path.to_string(),
span: value.span(),
});
}
pub fn deprecated_option(&mut self, path: &ConfigPath, suggestion: &'static str, span: Span) {
self.error(ConfigError::Deprecated {
path: path.to_string(),
suggestion,
span,
});
}
pub fn into_shell_error(self) -> Option<ShellError> {
if self.is_empty() {
None
} else {
Some(ShellError::InvalidConfig {
errors: self.errors,
})
}
}
}

View File

@ -15,3 +15,26 @@ impl Default for FilesizeConfig {
} }
} }
} }
impl UpdateFromValue for FilesizeConfig {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"metric" => self.metric.update(val, path, errors),
"format" => self.format.update(val, path, errors),
_ => errors.unknown_option(path, val),
}
}
}
}

View File

@ -1,132 +1,171 @@
use crate::{IntoValue, Record, ShellError, Span, Value}; use super::error::ConfigErrors;
use std::{collections::HashMap, fmt::Display, str::FromStr}; use crate::{Record, ShellError, Span, Type, Value};
use std::{
borrow::Borrow,
collections::HashMap,
fmt::{self, Display},
hash::Hash,
ops::{Deref, DerefMut},
str::FromStr,
};
pub(super) fn process_string_enum<T, E>( pub(super) struct ConfigPath<'a> {
config_point: &mut T, components: Vec<&'a str>,
config_path: &[&str], }
value: &mut Value,
errors: &mut Vec<ShellError>, impl<'a> ConfigPath<'a> {
) where pub fn new() -> Self {
T: FromStr<Err = E> + Clone + IntoValue, Self {
E: Display, components: vec!["$env.config"],
}
}
pub fn push(&mut self, key: &'a str) -> ConfigPathScope<'_, 'a> {
self.components.push(key);
ConfigPathScope { inner: self }
}
}
impl Display for ConfigPath<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.components.join("."))
}
}
pub(super) struct ConfigPathScope<'whole, 'part> {
inner: &'whole mut ConfigPath<'part>,
}
impl Drop for ConfigPathScope<'_, '_> {
fn drop(&mut self) {
self.inner.components.pop();
}
}
impl<'a> Deref for ConfigPathScope<'_, 'a> {
type Target = ConfigPath<'a>;
fn deref(&self) -> &Self::Target {
self.inner
}
}
impl DerefMut for ConfigPathScope<'_, '_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.inner
}
}
pub(super) trait UpdateFromValue: Sized {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
);
}
impl UpdateFromValue for Value {
fn update(&mut self, value: &Value, _path: &mut ConfigPath, _errors: &mut ConfigErrors) {
*self = value.clone();
}
}
impl UpdateFromValue for bool {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
if let Ok(val) = value.as_bool() {
*self = val;
} else {
errors.type_mismatch(path, Type::Bool, value);
}
}
}
impl UpdateFromValue for i64 {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
if let Ok(val) = value.as_int() {
*self = val;
} else {
errors.type_mismatch(path, Type::Int, value);
}
}
}
impl UpdateFromValue for usize {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
if let Ok(val) = value.as_int() {
if let Ok(val) = val.try_into() {
*self = val;
} else {
errors.invalid_value(path, "a non-negative integer", value);
}
} else {
errors.type_mismatch(path, Type::Int, value);
}
}
}
impl UpdateFromValue for String {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
if let Ok(val) = value.as_str() {
*self = val.into();
} else {
errors.type_mismatch(path, Type::String, value);
}
}
}
impl<K, V> UpdateFromValue for HashMap<K, V>
where
K: Borrow<str> + for<'a> From<&'a str> + Eq + Hash,
V: Default + UpdateFromValue,
{ {
let span = value.span(); fn update<'a>(
if let Ok(v) = value.coerce_str() { &mut self,
match v.parse() { value: &'a Value,
Ok(format) => { path: &mut ConfigPath<'a>,
*config_point = format; errors: &mut ConfigErrors,
} ) {
Err(err) => { if let Ok(record) = value.as_record() {
errors.push(ShellError::GenericError { *self = record
error: "Error while applying config changes".into(),
msg: format!(
"unrecognized $env.config.{} option '{v}'",
config_path.join(".")
),
span: Some(span),
help: Some(err.to_string()),
inner: vec![],
});
// Reconstruct
*value = config_point.clone().into_value(span);
}
}
} else {
errors.push(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: format!("$env.config.{} should be a string", config_path.join(".")),
span: Some(span),
help: Some("This value will be ignored.".into()),
inner: vec![],
});
// Reconstruct
*value = config_point.clone().into_value(span);
}
}
pub(super) fn process_bool_config(
value: &mut Value,
errors: &mut Vec<ShellError>,
config_point: &mut bool,
) {
if let Ok(b) = value.as_bool() {
*config_point = b;
} else {
errors.push(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: "should be a bool".to_string(),
span: Some(value.span()),
help: Some("This value will be ignored.".into()),
inner: vec![],
});
// Reconstruct
*value = Value::bool(*config_point, value.span());
}
}
pub(super) fn process_int_config(
value: &mut Value,
errors: &mut Vec<ShellError>,
config_point: &mut i64,
) {
if let Ok(b) = value.as_int() {
*config_point = b;
} else {
errors.push(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: "should be an int".into(),
span: Some(value.span()),
help: Some("This value will be ignored.".into()),
inner: vec![],
});
// Reconstruct
*value = Value::int(*config_point, value.span());
}
}
pub(super) fn report_invalid_key(keys: &[&str], span: Span, errors: &mut Vec<ShellError>) {
// Because Value::Record discards all of the spans of its
// column names (by storing them as Strings), the key name cannot be provided
// as a value, even in key errors.
errors.push(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: format!(
"$env.config.{} is an unknown config setting",
keys.join(".")
),
span: Some(span),
help: Some("This value will not appear in your $env.config record.".into()),
inner: vec![],
});
}
pub(super) fn report_invalid_value(msg: &str, span: Span, errors: &mut Vec<ShellError>) {
errors.push(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: msg.into(),
span: Some(span),
help: Some("This value will be ignored.".into()),
inner: vec![],
});
}
pub(super) fn create_map(value: &Value) -> Result<HashMap<String, Value>, ShellError> {
Ok(value
.as_record()?
.iter() .iter()
.map(|(k, v)| (k.clone(), v.clone())) .map(|(key, val)| {
.collect()) let mut old = self.remove(key).unwrap_or_default();
old.update(val, &mut path.push(key), errors);
(key.as_str().into(), old)
})
.collect();
} else {
errors.type_mismatch(path, Type::record(), value);
}
}
}
pub(super) fn config_update_string_enum<T>(
choice: &mut T,
value: &Value,
path: &mut ConfigPath,
errors: &mut ConfigErrors,
) where
T: FromStr,
T::Err: Display,
{
if let Ok(str) = value.as_str() {
match str.parse() {
Ok(val) => *choice = val,
Err(err) => errors.invalid_value(path, err.to_string(), value),
}
} else {
errors.type_mismatch(path, Type::String, value);
}
} }
pub fn extract_value<'record>( pub fn extract_value<'record>(
name: &str, column: &'static str,
record: &'record Record, record: &'record Record,
span: Span, span: Span,
) -> Result<&'record Value, ShellError> { ) -> Result<&'record Value, ShellError> {
record record
.get(name) .get(column)
.ok_or_else(|| ShellError::MissingConfigValue { .ok_or_else(|| ShellError::MissingRequiredColumn { column, span })
missing_value: name.to_string(),
span,
})
} }

View File

@ -1,4 +1,4 @@
use super::prelude::*; use super::{config_update_string_enum, prelude::*};
use crate as nu_protocol; use crate as nu_protocol;
#[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
@ -26,11 +26,17 @@ impl FromStr for HistoryFileFormat {
match s.to_ascii_lowercase().as_str() { match s.to_ascii_lowercase().as_str() {
"sqlite" => Ok(Self::Sqlite), "sqlite" => Ok(Self::Sqlite),
"plaintext" => Ok(Self::Plaintext), "plaintext" => Ok(Self::Plaintext),
_ => Err("expected either 'sqlite' or 'plaintext'"), _ => Err("'sqlite' or 'plaintext'"),
} }
} }
} }
impl UpdateFromValue for HistoryFileFormat {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
config_update_string_enum(self, value, path, errors)
}
}
#[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
pub struct HistoryConfig { pub struct HistoryConfig {
pub max_size: i64, pub max_size: i64,
@ -58,3 +64,28 @@ impl Default for HistoryConfig {
} }
} }
} }
impl UpdateFromValue for HistoryConfig {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"isolation" => self.isolation.update(val, path, errors),
"sync_on_enter" => self.sync_on_enter.update(val, path, errors),
"max_size" => self.max_size.update(val, path, errors),
"file_format" => self.file_format.update(val, path, errors),
_ => errors.unknown_option(path, val),
}
}
}
}

View File

@ -1,6 +1,5 @@
use super::prelude::*; use super::prelude::*;
use crate as nu_protocol; use crate as nu_protocol;
use crate::ShellError;
/// Definition of a parsed hook from the config object /// Definition of a parsed hook from the config object
#[derive(Clone, Debug, IntoValue, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, IntoValue, PartialEq, Serialize, Deserialize)]
@ -33,36 +32,36 @@ impl Default for Hooks {
} }
} }
/// Parse the hooks to find the blocks to run when the hooks fire impl UpdateFromValue for Hooks {
pub(super) fn create_hooks(value: &Value) -> Result<Hooks, ShellError> { fn update<'a>(
let span = value.span(); &mut self,
match value { value: &'a Value,
Value::Record { val, .. } => { path: &mut ConfigPath<'a>,
let mut hooks = Hooks::new(); errors: &mut ConfigErrors,
) {
fn update_option(field: &mut Option<Value>, value: &Value) {
if value.is_nothing() {
*field = None;
} else {
*field = Some(value.clone());
}
}
for (col, val) in &**val { let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() { match col.as_str() {
"pre_prompt" => hooks.pre_prompt = Some(val.clone()), "pre_prompt" => update_option(&mut self.pre_prompt, val),
"pre_execution" => hooks.pre_execution = Some(val.clone()), "pre_execution" => update_option(&mut self.pre_execution, val),
"env_change" => hooks.env_change = Some(val.clone()), "env_change" => update_option(&mut self.env_change, val),
"display_output" => hooks.display_output = Some(val.clone()), "display_output" => update_option(&mut self.display_output, val),
"command_not_found" => hooks.command_not_found = Some(val.clone()), "command_not_found" => update_option(&mut self.command_not_found, val),
x => { _ => errors.unknown_option(path, val),
return Err(ShellError::UnsupportedConfigValue {
expected: "'pre_prompt', 'pre_execution', 'env_change', 'display_output', 'command_not_found'".into(),
value: x.into(),
span
});
} }
} }
} }
Ok(hooks)
}
_ => Err(ShellError::UnsupportedConfigValue {
expected: "record for 'hooks' config".into(),
value: "non-record value".into(),
span,
}),
}
} }

View File

@ -15,3 +15,26 @@ impl Default for LsConfig {
} }
} }
} }
impl UpdateFromValue for LsConfig {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"use_ls_colors" => self.use_ls_colors.update(val, path, errors),
"clickable_links" => self.clickable_links.update(val, path, errors),
_ => errors.unknown_option(path, val),
}
}
}
}

View File

@ -1,35 +1,32 @@
//! Module containing the internal representation of user configuration //! Module containing the internal representation of user configuration
use self::helper::*;
use self::hooks::*;
use crate::{IntoValue, ShellError, Span, Value}; use crate as nu_protocol;
use reedline::create_keybindings; use crate::FromValue;
use serde::{Deserialize, Serialize}; use helper::*;
use prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
use table::try_parse_trim_strategy;
pub use self::completer::{ pub use completions::{
CompleterConfig, CompletionAlgorithm, CompletionSort, ExternalCompleterConfig, CompletionAlgorithm, CompletionConfig, CompletionSort, ExternalCompleterConfig,
}; };
pub use self::datetime_format::DatetimeFormatConfig; pub use datetime_format::DatetimeFormatConfig;
pub use self::display_errors::DisplayErrors; pub use display_errors::DisplayErrors;
pub use self::filesize::FilesizeConfig; pub use filesize::FilesizeConfig;
pub use self::helper::extract_value; pub use helper::extract_value;
pub use self::history::{HistoryConfig, HistoryFileFormat}; pub use history::{HistoryConfig, HistoryFileFormat};
pub use self::hooks::Hooks; pub use hooks::Hooks;
pub use self::ls::LsConfig; pub use ls::LsConfig;
pub use self::output::ErrorStyle; pub use output::ErrorStyle;
pub use self::plugin_gc::{PluginGcConfig, PluginGcConfigs}; pub use plugin_gc::{PluginGcConfig, PluginGcConfigs};
pub use self::reedline::{ pub use reedline::{CursorShapeConfig, EditBindings, NuCursorShape, ParsedKeybinding, ParsedMenu};
create_menus, CursorShapeConfig, EditBindings, NuCursorShape, ParsedKeybinding, ParsedMenu, pub use rm::RmConfig;
}; pub use shell_integration::ShellIntegrationConfig;
pub use self::rm::RmConfig; pub use table::{FooterMode, TableConfig, TableIndexMode, TableMode, TrimStrategy};
pub use self::shell_integration::ShellIntegrationConfig;
pub use self::table::{FooterMode, TableConfig, TableIndexMode, TableMode, TrimStrategy};
mod completer; mod completions;
mod datetime_format; mod datetime_format;
mod display_errors; mod display_errors;
mod error;
mod filesize; mod filesize;
mod helper; mod helper;
mod history; mod history;
@ -43,7 +40,7 @@ mod rm;
mod shell_integration; mod shell_integration;
mod table; mod table;
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Clone, Debug, IntoValue, Serialize, Deserialize)]
pub struct Config { pub struct Config {
pub filesize: FilesizeConfig, pub filesize: FilesizeConfig,
pub table: TableConfig, pub table: TableConfig,
@ -53,7 +50,7 @@ pub struct Config {
pub float_precision: i64, pub float_precision: i64,
pub recursion_limit: i64, pub recursion_limit: i64,
pub use_ansi_coloring: bool, pub use_ansi_coloring: bool,
pub completions: CompleterConfig, pub completions: CompletionConfig,
pub edit_mode: EditBindings, pub edit_mode: EditBindings,
pub history: HistoryConfig, pub history: HistoryConfig,
pub keybindings: Vec<ParsedKeybinding>, pub keybindings: Vec<ParsedKeybinding>,
@ -97,7 +94,7 @@ impl Default for Config {
history: HistoryConfig::default(), history: HistoryConfig::default(),
completions: CompleterConfig::default(), completions: CompletionConfig::default(),
recursion_limit: 50, recursion_limit: 50,
@ -135,630 +132,104 @@ impl Default for Config {
} }
} }
impl Value { impl UpdateFromValue for Config {
/// Parse the given [`Value`] as a configuration record, and recover encountered mistakes fn update<'a>(
/// &mut self,
/// If any given (sub)value is detected as impossible, this value will be restored to the value value: &'a Value,
/// in `existing_config`, thus mutates `self`. path: &mut ConfigPath<'a>,
/// errors: &mut ConfigErrors,
/// Returns a new [`Config`] (that is in a valid state) and if encountered the [`ShellError`] ) {
/// containing all observed inner errors. let Value::Record { val: record, .. } = value else {
pub fn parse_as_config(&mut self, existing_config: &Config) -> (Config, Option<ShellError>) { errors.type_mismatch(path, Type::record(), value);
// Clone the passed-in config rather than mutating it. return;
let mut config = existing_config.clone();
// Vec for storing errors. Current Nushell behaviour (Dec 2022) is that having some typo
// like `"always_trash": tru` in your config.nu's `$env.config` record shouldn't abort all
// config parsing there and then. Thus, errors are simply collected one-by-one and wrapped
// in a GenericError at the end.
let mut errors = vec![];
// Config record (self) mutation rules:
// * When parsing a config Record, if a config key error occurs, remove the key.
// * When parsing a config Record, if a config value error occurs, replace the value
// with a reconstructed Nu value for the current (unaltered) configuration for that setting.
// For instance:
// `$env.config.ls.use_ls_colors = 2` results in an error, so the current `use_ls_colors`
// config setting is converted to a `Value::Boolean` and inserted in the record in place of
// the `2`.
let Value::Record { val, .. } = self else {
return (
config,
Some(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: "$env.config is not a record".into(),
span: Some(self.span()),
help: None,
inner: vec![],
}),
);
}; };
val.to_mut().retain_mut(|key, value| { for (col, val) in record.iter() {
let span = value.span(); let path = &mut path.push(col);
match key { match col.as_str() {
"ls" => { "ls" => self.ls.update(val, path, errors),
if let Value::Record { val, .. } = value { "rm" => self.rm.update(val, path, errors),
val.to_mut().retain_mut(|key2, value| { "history" => self.history.update(val, path, errors),
let span = value.span(); "completions" => self.completions.update(val, path, errors),
match key2 { "cursor_shape" => self.cursor_shape.update(val, path, errors),
"use_ls_colors" => { "table" => self.table.update(val, path, errors),
process_bool_config(value, &mut errors, &mut config.ls.use_ls_colors); "filesize" => self.filesize.update(val, path, errors),
} "explore" => self.explore.update(val, path, errors),
"clickable_links" => { "color_config" => self.color_config.update(val, path, errors),
process_bool_config(value, &mut errors, &mut config.ls.clickable_links);
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
}
true
});
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.ls.into_value(span);
}
}
"rm" => {
if let Value::Record { val, .. } = value {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"always_trash" => {
process_bool_config(value, &mut errors, &mut config.rm.always_trash);
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
};
true
});
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.rm.into_value(span);
}
}
"history" => {
let history = &mut config.history;
if let Value::Record { val, .. } = value {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"isolation" => {
process_bool_config(value, &mut errors, &mut history.isolation);
}
"sync_on_enter" => {
process_bool_config(value, &mut errors, &mut history.sync_on_enter);
}
"max_size" => {
process_int_config(value, &mut errors, &mut history.max_size);
}
"file_format" => {
process_string_enum(
&mut history.file_format,
&[key, key2],
value,
&mut errors
);
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
};
true
});
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.history.into_value(span);
}
}
"completions" => {
if let Value::Record { val, .. } = value {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"quick" => {
process_bool_config(value, &mut errors, &mut config.completions.quick);
}
"partial" => {
process_bool_config(value, &mut errors, &mut config.completions.partial);
}
"algorithm" => {
process_string_enum(
&mut config.completions.algorithm,
&[key, key2],
value,
&mut errors
);
}
"case_sensitive" => {
process_bool_config(value, &mut errors, &mut config.completions.case_sensitive);
}
"sort" => {
process_string_enum(
&mut config.completions.sort,
&[key, key2],
value,
&mut errors
);
}
"external" => {
if let Value::Record { val, .. } = value {
val.to_mut().retain_mut(|key3, value| {
let span = value.span();
match key3 {
"max_results" => {
process_int_config(value, &mut errors, &mut config.completions.external.max_results);
}
"completer" => {
if let Ok(v) = value.as_closure() {
config.completions.external.completer = Some(v.clone())
} else {
match value {
Value::Nothing { .. } => {}
_ => {
report_invalid_value("should be a closure or null", span, &mut errors);
*value = config.completions.external.completer.clone().into_value(span);
}
}
}
}
"enable" => {
process_bool_config(value, &mut errors, &mut config.completions.external.enable);
}
_ => {
report_invalid_key(&[key, key2, key3], span, &mut errors);
return false;
}
};
true
});
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.completions.external.clone().into_value(span);
}
}
"use_ls_colors" => {
process_bool_config(value, &mut errors, &mut config.completions.use_ls_colors);
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
};
true
});
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.completions.clone().into_value(span);
}
}
"cursor_shape" => {
if let Value::Record { val, .. } = value {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
let config_point = match key2 {
"vi_insert" => &mut config.cursor_shape.vi_insert,
"vi_normal" => &mut config.cursor_shape.vi_normal,
"emacs" => &mut config.cursor_shape.emacs,
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
};
process_string_enum(
config_point,
&[key, key2],
value,
&mut errors
);
true
});
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.cursor_shape.into_value(span);
}
}
"table" => {
if let Value::Record { val, .. } = value {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"mode" => {
process_string_enum(
&mut config.table.mode,
&[key, key2],
value,
&mut errors
);
}
"header_on_separator" => {
process_bool_config(value, &mut errors, &mut config.table.header_on_separator);
}
"padding" => match value {
Value::Int { val, .. } => {
if *val < 0 {
report_invalid_value("expected a unsigned integer", span, &mut errors);
*value = config.table.padding.into_value(span);
} else {
config.table.padding.left = *val as usize;
config.table.padding.right = *val as usize;
}
}
Value::Record { val, .. } => {
let mut invalid = false;
val.to_mut().retain(|key3, value| {
match key3 {
"left" => {
match value.as_int() {
Ok(val) if val >= 0 => {
config.table.padding.left = val as usize;
}
_ => {
report_invalid_value("expected a unsigned integer >= 0", span, &mut errors);
invalid = true;
}
}
}
"right" => {
match value.as_int() {
Ok(val) if val >= 0 => {
config.table.padding.right = val as usize;
}
_ => {
report_invalid_value("expected a unsigned integer >= 0", span, &mut errors);
invalid = true;
}
}
}
_ => {
report_invalid_key(&[key, key2, key3], span, &mut errors);
return false;
}
};
true
});
if invalid {
*value = config.table.padding.into_value(span);
}
}
_ => {
report_invalid_value("expected a unsigned integer or a record", span, &mut errors);
*value = config.table.padding.into_value(span);
}
},
"index_mode" => {
process_string_enum(
&mut config.table.index_mode,
&[key, key2],
value,
&mut errors
);
}
"trim" => {
match try_parse_trim_strategy(value, &mut errors) {
Ok(v) => config.table.trim = v,
Err(e) => {
// try_parse_trim_strategy() already adds its own errors
errors.push(e);
*value = config.table.trim.clone().into_value(span);
}
}
}
"show_empty" => {
process_bool_config(value, &mut errors, &mut config.table.show_empty);
}
"abbreviated_row_count" => {
match *value {
Value::Int { val, .. } => {
if val >= 0 {
config.table.abbreviated_row_count = Some(val as usize);
} else {
report_invalid_value("should be an int unsigned", span, &mut errors);
*value = config.table.abbreviated_row_count.map(|count| Value::int(count as i64, span)).unwrap_or(Value::nothing(span));
}
}
Value::Nothing { .. } => {
config.table.abbreviated_row_count = None;
}
_ => {
report_invalid_value("should be an int", span, &mut errors);
*value = config.table.abbreviated_row_count.map(|count| Value::int(count as i64, span)).unwrap_or(Value::nothing(span))
}
}
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
};
true
});
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.table.clone().into_value(span);
}
}
"filesize" => {
if let Value::Record { val, .. } = value {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"metric" => {
process_bool_config(value, &mut errors, &mut config.filesize.metric);
}
"format" => {
if let Ok(v) = value.coerce_str() {
config.filesize.format = v.to_lowercase();
} else {
report_invalid_value("should be a string", span, &mut errors);
*value = Value::string(config.filesize.format.clone(), span);
}
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
};
true
})
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.filesize.clone().into_value(span);
}
}
"explore" => {
if let Ok(map) = create_map(value) {
config.explore = map;
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.explore.clone().into_value(span);
}
}
// Misc. options
"color_config" => {
if let Ok(map) = create_map(value) {
config.color_config = map;
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.color_config.clone().into_value(span);
}
}
"use_grid_icons" => { "use_grid_icons" => {
// TODO: delete it after 0.99 // TODO: delete it after 0.99
report_invalid_value( errors.deprecated_option(path, "use `grid -i`", val.span());
"`use_grid_icons` is deleted, you should delete the key, and use `grid -i` in such case.",
span,
&mut errors
);
} }
"footer_mode" => { "footer_mode" => self.footer_mode.update(val, path, errors),
process_string_enum( "float_precision" => self.float_precision.update(val, path, errors),
&mut config.footer_mode, "use_ansi_coloring" => self.use_ansi_coloring.update(val, path, errors),
&[key], "edit_mode" => self.edit_mode.update(val, path, errors),
value, "shell_integration" => self.shell_integration.update(val, path, errors),
&mut errors "buffer_editor" => match val {
);
}
"float_precision" => {
process_int_config(value, &mut errors, &mut config.float_precision);
}
"use_ansi_coloring" => {
process_bool_config(value, &mut errors, &mut config.use_ansi_coloring);
}
"edit_mode" => {
process_string_enum(
&mut config.edit_mode,
&[key],
value,
&mut errors
);
}
"shell_integration" => {
if let Value::Record { val, .. } = value {
val.to_mut().retain_mut(|key2, value| {
let span = value.span();
match key2 {
"osc2" => {
process_bool_config(value, &mut errors, &mut config.shell_integration.osc2);
}
"osc7" => {
process_bool_config(value, &mut errors, &mut config.shell_integration.osc7);
}
"osc8" => {
process_bool_config(value, &mut errors, &mut config.shell_integration.osc8);
}
"osc9_9" => {
process_bool_config(value, &mut errors, &mut config.shell_integration.osc9_9);
}
"osc133" => {
process_bool_config(value, &mut errors, &mut config.shell_integration.osc133);
}
"osc633" => {
process_bool_config(value, &mut errors, &mut config.shell_integration.osc633);
}
"reset_application_mode" => {
process_bool_config(value, &mut errors, &mut config.shell_integration.reset_application_mode);
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
};
true
})
} else {
report_invalid_value("boolean value is deprecated, should be a record. see `config nu --default`.", span, &mut errors);
*value = config.shell_integration.into_value(span);
}
}
"buffer_editor" => match value {
Value::Nothing { .. } | Value::String { .. } => { Value::Nothing { .. } | Value::String { .. } => {
config.buffer_editor = value.clone(); self.buffer_editor = val.clone();
} }
Value::List { vals, .. } Value::List { vals, .. }
if vals.iter().all(|val| matches!(val, Value::String { .. })) => if vals.iter().all(|val| matches!(val, Value::String { .. })) =>
{ {
config.buffer_editor = value.clone(); self.buffer_editor = val.clone();
}
_ => {
report_invalid_value("should be a string, list<string>, or null", span, &mut errors);
*value = config.buffer_editor.clone();
} }
_ => errors.type_mismatch(
path,
Type::custom("string, list<string>, or nothing"),
val,
),
}, },
"show_banner" => { "show_banner" => self.show_banner.update(val, path, errors),
process_bool_config(value, &mut errors, &mut config.show_banner); "display_errors" => self.display_errors.update(val, path, errors),
} "render_right_prompt_on_last_line" => self
"display_errors" => { .render_right_prompt_on_last_line
if let Value::Record { val, .. } = value { .update(val, path, errors),
val.to_mut().retain_mut(|key2, value| { "bracketed_paste" => self.bracketed_paste.update(val, path, errors),
let span = value.span(); "use_kitty_protocol" => self.use_kitty_protocol.update(val, path, errors),
match key2 {
"exit_code" => {
process_bool_config(value, &mut errors, &mut config.display_errors.exit_code);
}
"termination_signal" => {
process_bool_config(value, &mut errors, &mut config.display_errors.termination_signal);
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
};
true
});
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.display_errors.into_value(span);
}
}
"render_right_prompt_on_last_line" => {
process_bool_config(value, &mut errors, &mut config.render_right_prompt_on_last_line);
}
"bracketed_paste" => {
process_bool_config(value, &mut errors, &mut config.bracketed_paste);
}
"use_kitty_protocol" => {
process_bool_config(value, &mut errors, &mut config.use_kitty_protocol);
}
"highlight_resolved_externals" => { "highlight_resolved_externals" => {
process_bool_config(value, &mut errors, &mut config.highlight_resolved_externals); self.highlight_resolved_externals.update(val, path, errors)
}
"plugins" => {
if let Ok(map) = create_map(value) {
config.plugins = map;
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.plugins.clone().into_value(span);
}
}
"plugin_gc" => {
config.plugin_gc.process(&[key], value, &mut errors);
}
"menus" => match create_menus(value) {
Ok(map) => config.menus = map,
Err(e) => {
report_invalid_value("should be a valid list of menus", span, &mut errors);
errors.push(e);
*value = config.menus.clone().into_value(span);
} }
"plugins" => self.plugins.update(val, path, errors),
"plugin_gc" => self.plugin_gc.update(val, path, errors),
"menus" => match Vec::from_value(val.clone()) {
Ok(menus) => self.menus = menus,
Err(err) => errors.error(err.into()),
}, },
"keybindings" => match create_keybindings(value) { "keybindings" => match Vec::from_value(val.clone()) {
Ok(keybindings) => config.keybindings = keybindings, Ok(keybindings) => self.keybindings = keybindings,
Err(e) => { Err(err) => errors.error(err.into()),
report_invalid_value("should be a valid keybindings list", span, &mut errors);
errors.push(e);
*value = config.keybindings.clone().into_value(span);
}
}, },
"hooks" => match create_hooks(value) { "hooks" => self.hooks.update(val, path, errors),
Ok(hooks) => config.hooks = hooks, "datetime_format" => self.datetime_format.update(val, path, errors),
Err(e) => { "error_style" => self.error_style.update(val, path, errors),
report_invalid_value("should be a valid hooks list", span, &mut errors);
errors.push(e);
*value = config.hooks.clone().into_value(span);
}
},
"datetime_format" => {
if let Value::Record { val, .. } = value {
val.to_mut().retain_mut(|key2, value|
{
let span = value.span();
match key2 {
"normal" => {
if let Ok(v) = value.coerce_string() {
config.datetime_format.normal = Some(v);
} else {
report_invalid_value("should be a string", span, &mut errors);
}
}
"table" => {
if let Ok(v) = value.coerce_string() {
config.datetime_format.table = Some(v);
} else {
report_invalid_value("should be a string", span, &mut errors);
}
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
};
true
})
} else {
report_invalid_value("should be a record", span, &mut errors);
*value = config.datetime_format.clone().into_value(span);
}
}
"error_style" => {
process_string_enum(
&mut config.error_style,
&[key],
value,
&mut errors
);
}
"recursion_limit" => { "recursion_limit" => {
if let Value::Int { val, internal_span } = value { if let Ok(limit) = val.as_int() {
if val > &mut 1 { if limit > 1 {
config.recursion_limit = *val; self.recursion_limit = limit;
} else { } else {
report_invalid_value("should be a integer greater than 1", span, &mut errors); errors.invalid_value(path, "an int greater than 1", val);
*value = Value::Int { val: 50, internal_span: *internal_span };
} }
} else { } else {
report_invalid_value("should be a integer greater than 1", span, &mut errors); errors.type_mismatch(path, Type::Int, val);
*value = Value::Int { val: 50, internal_span: value.span() };
} }
} }
// Catch all _ => errors.unknown_option(path, val),
_ => { }
report_invalid_key(&[key], span, &mut errors);
return false;
} }
}; }
true }
});
impl Config {
// Return the config and the vec of errors. pub fn update_from_value(&mut self, old: &Config, value: &Value) -> Option<ShellError> {
( // Current behaviour is that config errors are displayed, but do not prevent the rest
config, // of the config from being updated (fields with errors are skipped/not updated).
if !errors.is_empty() { // Errors are simply collected one-by-one and wrapped into a ShellError variant at the end.
Some(ShellError::GenericError { let mut errors = ConfigErrors::new(old);
error: "Config record contains invalid values or unknown settings".into(), let mut path = ConfigPath::new();
msg: "".into(),
span: None, self.update(value, &mut path, &mut errors);
help: None,
inner: errors, errors.into_shell_error()
})
} else {
None
},
)
} }
} }

View File

@ -1,4 +1,4 @@
use super::prelude::*; use super::{config_update_string_enum, prelude::*};
use crate as nu_protocol; use crate as nu_protocol;
#[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
@ -14,7 +14,13 @@ impl FromStr for ErrorStyle {
match s.to_ascii_lowercase().as_str() { match s.to_ascii_lowercase().as_str() {
"fancy" => Ok(Self::Fancy), "fancy" => Ok(Self::Fancy),
"plain" => Ok(Self::Plain), "plain" => Ok(Self::Plain),
_ => Err("expected either 'fancy' or 'plain'"), _ => Err("'fancy' or 'plain'"),
} }
} }
} }
impl UpdateFromValue for ErrorStyle {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
config_update_string_enum(self, value, path, errors)
}
}

View File

@ -1,7 +1,5 @@
use super::helper::{process_bool_config, report_invalid_key, report_invalid_value};
use super::prelude::*; use super::prelude::*;
use crate as nu_protocol; use crate as nu_protocol;
use crate::ShellError;
use std::collections::HashMap; use std::collections::HashMap;
/// Configures when plugins should be stopped if inactive /// Configures when plugins should be stopped if inactive
@ -19,79 +17,28 @@ impl PluginGcConfigs {
pub fn get(&self, plugin_name: &str) -> &PluginGcConfig { pub fn get(&self, plugin_name: &str) -> &PluginGcConfig {
self.plugins.get(plugin_name).unwrap_or(&self.default) self.plugins.get(plugin_name).unwrap_or(&self.default)
} }
pub(super) fn process(
&mut self,
path: &[&str],
value: &mut Value,
errors: &mut Vec<ShellError>,
) {
if let Value::Record { val, .. } = value {
// Handle resets to default if keys are missing
if !val.contains("default") {
self.default = PluginGcConfig::default();
}
if !val.contains("plugins") {
self.plugins = HashMap::new();
}
val.to_mut().retain_mut(|key, value| {
let span = value.span();
match key {
"default" => {
self.default
.process(&join_path(path, &["default"]), value, errors)
}
"plugins" => process_plugins(
&join_path(path, &["plugins"]),
value,
errors,
&mut self.plugins,
),
_ => {
report_invalid_key(&join_path(path, &[key]), span, errors);
return false;
}
}
true
});
} else {
report_invalid_value("should be a record", value.span(), errors);
*value = self.clone().into_value(value.span());
}
}
} }
fn process_plugins( impl UpdateFromValue for PluginGcConfigs {
path: &[&str], fn update<'a>(
value: &mut Value, &mut self,
errors: &mut Vec<ShellError>, value: &'a Value,
plugins: &mut HashMap<String, PluginGcConfig>, path: &mut ConfigPath<'a>,
) { errors: &mut ConfigErrors,
if let Value::Record { val, .. } = value { ) {
// Remove any plugin configs that aren't in the value let Value::Record { val: record, .. } = value else {
plugins.retain(|key, _| val.contains(key)); errors.type_mismatch(path, Type::record(), value);
return;
};
val.to_mut().retain_mut(|key, value| { for (col, val) in record.iter() {
if matches!(value, Value::Record { .. }) { let path = &mut path.push(col);
plugins.entry(key.to_owned()).or_default().process( match col.as_str() {
&join_path(path, &[key]), "default" => self.default.update(val, path, errors),
value, "plugins" => self.plugins.update(val, path, errors),
errors, _ => errors.unknown_option(path, val),
);
true
} else {
report_invalid_value("should be a record", value.span(), errors);
if let Some(conf) = plugins.get(key) {
// Reconstruct the value if it existed before
*value = conf.clone().into_value(value.span());
true
} else {
// Remove it if it didn't
false
} }
} }
});
} }
} }
@ -123,57 +70,43 @@ impl IntoValue for PluginGcConfig {
} }
} }
impl PluginGcConfig { impl UpdateFromValue for PluginGcConfig {
fn process(&mut self, path: &[&str], value: &mut Value, errors: &mut Vec<ShellError>) { fn update<'a>(
if let Value::Record { val, .. } = value { &mut self,
// Handle resets to default if keys are missing value: &'a Value,
if !val.contains("enabled") { path: &mut ConfigPath<'a>,
self.enabled = PluginGcConfig::default().enabled; errors: &mut ConfigErrors,
} ) {
if !val.contains("stop_after") { let Value::Record { val: record, .. } = value else {
self.stop_after = PluginGcConfig::default().stop_after; errors.type_mismatch(path, Type::record(), value);
} return;
};
val.to_mut().retain_mut(|key, value| { for (col, val) in record.iter() {
let span = value.span(); let path = &mut path.push(col);
match key { match col.as_str() {
"enabled" => process_bool_config(value, errors, &mut self.enabled), "enabled" => self.enabled.update(val, path, errors),
"stop_after" => match value { "stop_after" => {
Value::Duration { val, .. } => { if let Ok(duration) = val.as_duration() {
if *val >= 0 { if duration >= 0 {
self.stop_after = *val; self.stop_after = duration;
} else { } else {
report_invalid_value("must not be negative", span, errors); errors.invalid_value(path, "a non-negative duration", val);
*val = self.stop_after;
} }
}
_ => {
report_invalid_value("should be a duration", span, errors);
*value = Value::duration(self.stop_after, span);
}
},
_ => {
report_invalid_key(&join_path(path, &[key]), span, errors);
return false;
}
}
true
})
} else { } else {
report_invalid_value("should be a record", value.span(), errors); errors.type_mismatch(path, Type::Duration, val);
*value = self.clone().into_value(value.span()); }
}
_ => errors.unknown_option(path, val),
}
} }
} }
}
fn join_path<'a>(a: &[&'a str], b: &[&'a str]) -> Vec<&'a str> {
a.iter().copied().chain(b.iter().copied()).collect()
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use nu_protocol::{record, Span}; use crate::{record, Config, Span};
fn test_pair() -> (PluginGcConfigs, Value) { fn test_pair() -> (PluginGcConfigs, Value) {
( (
@ -208,11 +141,12 @@ mod tests {
} }
#[test] #[test]
fn process() { fn update() {
let (expected, mut input) = test_pair(); let (expected, input) = test_pair();
let mut errors = vec![]; let config = Config::default();
let mut errors = ConfigErrors::new(&config);
let mut result = PluginGcConfigs::default(); let mut result = PluginGcConfigs::default();
result.process(&[], &mut input, &mut errors); result.update(&input, &mut ConfigPath::new(), &mut errors);
assert!(errors.is_empty(), "errors: {errors:#?}"); assert!(errors.is_empty(), "errors: {errors:#?}");
assert_eq!(expected, result); assert_eq!(expected, result);
} }

View File

@ -1,3 +1,4 @@
pub use crate::{record, IntoValue, Span, Value}; pub(super) use super::{error::ConfigErrors, ConfigPath, UpdateFromValue};
pub use crate::{record, IntoValue, ShellError, Span, Type, Value};
pub use serde::{Deserialize, Serialize}; pub use serde::{Deserialize, Serialize};
pub use std::str::FromStr; pub use std::str::FromStr;

View File

@ -1,9 +1,9 @@
use super::{extract_value, prelude::*}; use super::{config_update_string_enum, prelude::*};
use crate as nu_protocol; use crate as nu_protocol;
use crate::ShellError; use crate::{engine::Closure, FromValue};
/// Definition of a parsed keybinding from the config object /// Definition of a parsed keybinding from the config object
#[derive(Clone, Debug, IntoValue, Serialize, Deserialize)] #[derive(Clone, Debug, FromValue, IntoValue, Serialize, Deserialize)]
pub struct ParsedKeybinding { pub struct ParsedKeybinding {
pub modifier: Value, pub modifier: Value,
pub keycode: Value, pub keycode: Value,
@ -12,14 +12,14 @@ pub struct ParsedKeybinding {
} }
/// Definition of a parsed menu from the config object /// Definition of a parsed menu from the config object
#[derive(Clone, Debug, IntoValue, Serialize, Deserialize)] #[derive(Clone, Debug, FromValue, IntoValue, Serialize, Deserialize)]
pub struct ParsedMenu { pub struct ParsedMenu {
pub name: Value, pub name: Value,
pub marker: Value, pub marker: Value,
pub only_buffer_difference: Value, pub only_buffer_difference: Value,
pub style: Value, pub style: Value,
pub r#type: Value, pub r#type: Value,
pub source: Value, pub source: Option<Closure>,
} }
/// Definition of a Nushell CursorShape (to be mapped to crossterm::cursor::CursorShape) /// Definition of a Nushell CursorShape (to be mapped to crossterm::cursor::CursorShape)
@ -47,11 +47,17 @@ impl FromStr for NuCursorShape {
"blink_block" => Ok(NuCursorShape::BlinkBlock), "blink_block" => Ok(NuCursorShape::BlinkBlock),
"blink_underscore" => Ok(NuCursorShape::BlinkUnderscore), "blink_underscore" => Ok(NuCursorShape::BlinkUnderscore),
"inherit" => Ok(NuCursorShape::Inherit), "inherit" => Ok(NuCursorShape::Inherit),
_ => Err("expected either 'line', 'block', 'underscore', 'blink_line', 'blink_block', 'blink_underscore' or 'inherit'"), _ => Err("'line', 'block', 'underscore', 'blink_line', 'blink_block', 'blink_underscore' or 'inherit'"),
} }
} }
} }
impl UpdateFromValue for NuCursorShape {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
config_update_string_enum(self, value, path, errors)
}
}
#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
pub struct CursorShapeConfig { pub struct CursorShapeConfig {
pub emacs: NuCursorShape, pub emacs: NuCursorShape,
@ -59,6 +65,30 @@ pub struct CursorShapeConfig {
pub vi_normal: NuCursorShape, pub vi_normal: NuCursorShape,
} }
impl UpdateFromValue for CursorShapeConfig {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"vi_insert" => self.vi_insert.update(val, path, errors),
"vi_normal" => self.vi_normal.update(val, path, errors),
"emacs" => self.emacs.update(val, path, errors),
_ => errors.unknown_option(path, val),
}
}
}
}
#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
pub enum EditBindings { pub enum EditBindings {
Vi, Vi,
@ -73,89 +103,13 @@ impl FromStr for EditBindings {
match s.to_ascii_lowercase().as_str() { match s.to_ascii_lowercase().as_str() {
"vi" => Ok(Self::Vi), "vi" => Ok(Self::Vi),
"emacs" => Ok(Self::Emacs), "emacs" => Ok(Self::Emacs),
_ => Err("expected either 'emacs' or 'vi'"), _ => Err("'emacs' or 'vi'"),
} }
} }
} }
/// Parses the config object to extract the strings that will compose a keybinding for reedline impl UpdateFromValue for EditBindings {
pub(super) fn create_keybindings(value: &Value) -> Result<Vec<ParsedKeybinding>, ShellError> { fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
let span = value.span(); config_update_string_enum(self, value, path, errors)
match value {
Value::Record { val, .. } => {
// Finding the modifier value in the record
let modifier = extract_value("modifier", val, span)?.clone();
let keycode = extract_value("keycode", val, span)?.clone();
let mode = extract_value("mode", val, span)?.clone();
let event = extract_value("event", val, span)?.clone();
let keybinding = ParsedKeybinding {
modifier,
keycode,
mode,
event,
};
// We return a menu to be able to do recursion on the same function
Ok(vec![keybinding])
}
Value::List { vals, .. } => {
let res = vals
.iter()
.map(create_keybindings)
.collect::<Result<Vec<Vec<ParsedKeybinding>>, ShellError>>();
let res = res?
.into_iter()
.flatten()
.collect::<Vec<ParsedKeybinding>>();
Ok(res)
}
_ => Ok(Vec::new()),
}
}
/// Parses the config object to extract the strings that will compose a keybinding for reedline
pub fn create_menus(value: &Value) -> Result<Vec<ParsedMenu>, ShellError> {
let span = value.span();
match value {
Value::Record { val, .. } => {
// Finding the modifier value in the record
let name = extract_value("name", val, span)?.clone();
let marker = extract_value("marker", val, span)?.clone();
let only_buffer_difference =
extract_value("only_buffer_difference", val, span)?.clone();
let style = extract_value("style", val, span)?.clone();
let r#type = extract_value("type", val, span)?.clone();
// Source is an optional value
let source = match extract_value("source", val, span) {
Ok(source) => source.clone(),
Err(_) => Value::nothing(span),
};
let menu = ParsedMenu {
name,
only_buffer_difference,
marker,
style,
r#type,
source,
};
Ok(vec![menu])
}
Value::List { vals, .. } => {
let res = vals
.iter()
.map(create_menus)
.collect::<Result<Vec<Vec<ParsedMenu>>, ShellError>>();
let res = res?.into_iter().flatten().collect::<Vec<ParsedMenu>>();
Ok(res)
}
_ => Ok(Vec::new()),
} }
} }

View File

@ -14,3 +14,25 @@ impl Default for RmConfig {
} }
} }
} }
impl UpdateFromValue for RmConfig {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"always_trash" => self.always_trash.update(val, path, errors),
_ => errors.unknown_option(path, val),
}
}
}
}

View File

@ -26,3 +26,31 @@ impl Default for ShellIntegrationConfig {
} }
} }
} }
impl UpdateFromValue for ShellIntegrationConfig {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"osc2" => self.osc2.update(val, path, errors),
"osc7" => self.osc7.update(val, path, errors),
"osc8" => self.osc8.update(val, path, errors),
"osc9_9" => self.osc9_9.update(val, path, errors),
"osc133" => self.osc133.update(val, path, errors),
"osc633" => self.osc633.update(val, path, errors),
"reset_application_mode" => self.reset_application_mode.update(val, path, errors),
_ => errors.unknown_option(path, val),
}
}
}
}

View File

@ -1,6 +1,5 @@
use super::prelude::*; use super::{config_update_string_enum, prelude::*};
use crate as nu_protocol; use crate as nu_protocol;
use crate::ShellError;
#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
pub enum TableMode { pub enum TableMode {
@ -45,11 +44,17 @@ impl FromStr for TableMode {
"restructured" => Ok(Self::Restructured), "restructured" => Ok(Self::Restructured),
"ascii_rounded" => Ok(Self::AsciiRounded), "ascii_rounded" => Ok(Self::AsciiRounded),
"basic_compact" => Ok(Self::BasicCompact), "basic_compact" => Ok(Self::BasicCompact),
_ => Err("expected either 'basic', 'thin', 'light', 'compact', 'with_love', 'compact_double', 'rounded', 'reinforced', 'heavy', 'none', 'psql', 'markdown', 'dots', 'restructured', 'ascii_rounded', or 'basic_compact'"), _ => Err("'basic', 'thin', 'light', 'compact', 'with_love', 'compact_double', 'rounded', 'reinforced', 'heavy', 'none', 'psql', 'markdown', 'dots', 'restructured', 'ascii_rounded', or 'basic_compact'"),
} }
} }
} }
impl UpdateFromValue for TableMode {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
config_update_string_enum(self, value, path, errors)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum FooterMode { pub enum FooterMode {
/// Never show the footer /// Never show the footer
@ -70,13 +75,30 @@ impl FromStr for FooterMode {
"always" => Ok(FooterMode::Always), "always" => Ok(FooterMode::Always),
"never" => Ok(FooterMode::Never), "never" => Ok(FooterMode::Never),
"auto" => Ok(FooterMode::Auto), "auto" => Ok(FooterMode::Auto),
x => { _ => Err("'never', 'always', 'auto', or int"),
if let Ok(count) = x.parse() { }
Ok(FooterMode::RowCount(count)) }
}
impl UpdateFromValue for FooterMode {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
match value {
Value::String { val, .. } => match val.parse() {
Ok(val) => *self = val,
Err(err) => errors.invalid_value(path, err.to_string(), value),
},
&Value::Int { val, .. } => {
if val >= 0 {
*self = Self::RowCount(val as u64);
} else { } else {
Err("expected either 'never', 'always', 'auto' or a row count") errors.invalid_value(path, "a non-negative integer", value);
} }
} }
_ => errors.type_mismatch(
path,
Type::custom("'never', 'always', 'auto', or int"),
value,
),
} }
} }
} }
@ -87,7 +109,7 @@ impl IntoValue for FooterMode {
FooterMode::Always => "always".into_value(span), FooterMode::Always => "always".into_value(span),
FooterMode::Never => "never".into_value(span), FooterMode::Never => "never".into_value(span),
FooterMode::Auto => "auto".into_value(span), FooterMode::Auto => "auto".into_value(span),
FooterMode::RowCount(c) => c.to_string().into_value(span), FooterMode::RowCount(c) => (c as i64).into_value(span),
} }
} }
} }
@ -110,11 +132,17 @@ impl FromStr for TableIndexMode {
"always" => Ok(TableIndexMode::Always), "always" => Ok(TableIndexMode::Always),
"never" => Ok(TableIndexMode::Never), "never" => Ok(TableIndexMode::Never),
"auto" => Ok(TableIndexMode::Auto), "auto" => Ok(TableIndexMode::Auto),
_ => Err("expected either 'never', 'always' or 'auto'"), _ => Err("'never', 'always' or 'auto'"),
} }
} }
} }
impl UpdateFromValue for TableIndexMode {
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
config_update_string_enum(self, value, path, errors)
}
}
/// A Table view configuration, for a situation where /// A Table view configuration, for a situation where
/// we need to limit cell width in order to adjust for a terminal size. /// we need to limit cell width in order to adjust for a terminal size.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -159,93 +187,6 @@ impl Default for TrimStrategy {
} }
} }
pub(super) fn try_parse_trim_strategy(
value: &Value,
errors: &mut Vec<ShellError>,
) -> Result<TrimStrategy, ShellError> {
let map = value.as_record().map_err(|e| ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: "$env.config.table.trim is not a record".into(),
span: Some(value.span()),
help: Some("Please consult the documentation for configuring Nushell.".into()),
inner: vec![e],
})?;
let mut methodology = match map.get("methodology") {
Some(value) => match try_parse_trim_methodology(value) {
Some(methodology) => methodology,
None => return Ok(TrimStrategy::default()),
},
None => {
errors.push(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: "$env.config.table.trim.methodology was not provided".into(),
span: Some(value.span()),
help: Some("Please consult the documentation for configuring Nushell.".into()),
inner: vec![],
});
return Ok(TrimStrategy::default());
}
};
match &mut methodology {
TrimStrategy::Wrap { try_to_keep_words } => {
if let Some(value) = map.get("wrapping_try_keep_words") {
if let Ok(b) = value.as_bool() {
*try_to_keep_words = b;
} else {
errors.push(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: "$env.config.table.trim.wrapping_try_keep_words is not a bool".into(),
span: Some(value.span()),
help: Some(
"Please consult the documentation for configuring Nushell.".into(),
),
inner: vec![],
});
}
}
}
TrimStrategy::Truncate { suffix } => {
if let Some(value) = map.get("truncating_suffix") {
if let Ok(v) = value.coerce_string() {
*suffix = Some(v);
} else {
errors.push(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: "$env.config.table.trim.truncating_suffix is not a string".into(),
span: Some(value.span()),
help: Some(
"Please consult the documentation for configuring Nushell.".into(),
),
inner: vec![],
});
}
}
}
}
Ok(methodology)
}
fn try_parse_trim_methodology(value: &Value) -> Option<TrimStrategy> {
if let Ok(value) = value.coerce_str() {
match value.to_lowercase().as_str() {
"wrapping" => {
return Some(TrimStrategy::Wrap {
try_to_keep_words: false,
});
}
"truncating" => return Some(TrimStrategy::Truncate { suffix: None }),
_ => eprintln!("unrecognized $config.table.trim.methodology value; expected either 'truncating' or 'wrapping'"),
}
} else {
eprintln!("$env.config.table.trim.methodology is not a string")
}
None
}
impl IntoValue for TrimStrategy { impl IntoValue for TrimStrategy {
fn into_value(self, span: Span) -> Value { fn into_value(self, span: Span) -> Value {
match self { match self {
@ -266,6 +207,70 @@ impl IntoValue for TrimStrategy {
} }
} }
impl UpdateFromValue for TrimStrategy {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
let Some(methodology) = record.get("methodology") else {
errors.missing_column(path, "methodology", value.span());
return;
};
match methodology.as_str() {
Ok("wrapping") => {
let mut try_to_keep_words = if let &mut Self::Wrap { try_to_keep_words } = self {
try_to_keep_words
} else {
false
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"wrapping_try_keep_words" => try_to_keep_words.update(val, path, errors),
"methodology" | "truncating_suffix" => (),
_ => errors.unknown_option(path, val),
}
}
*self = Self::Wrap { try_to_keep_words };
}
Ok("truncating") => {
let mut suffix = if let Self::Truncate { suffix } = self {
suffix.take()
} else {
None
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"truncating_suffix" => match val {
Value::Nothing { .. } => suffix = None,
Value::String { val, .. } => suffix = Some(val.clone()),
_ => errors.type_mismatch(path, Type::String, val),
},
"methodology" | "wrapping_try_keep_words" => (),
_ => errors.unknown_option(path, val),
}
}
*self = Self::Truncate { suffix };
}
Ok(_) => errors.invalid_value(
&path.push("methodology"),
"'wrapping' or 'truncating'",
methodology,
),
Err(_) => errors.type_mismatch(&path.push("methodology"), Type::String, methodology),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableIndent { pub struct TableIndent {
pub left: usize, pub left: usize,
@ -288,6 +293,37 @@ impl Default for TableIndent {
} }
} }
impl UpdateFromValue for TableIndent {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
match value {
&Value::Int { val, .. } => {
if let Ok(val) = val.try_into() {
self.left = val;
self.right = val;
} else {
errors.invalid_value(path, "a non-negative integer", value);
}
}
Value::Record { val: record, .. } => {
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"left" => self.left.update(val, path, errors),
"right" => self.right.update(val, path, errors),
_ => errors.unknown_option(path, val),
}
}
}
_ => errors.type_mismatch(path, Type::custom("int or record"), value),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableConfig { pub struct TableConfig {
pub mode: TableMode, pub mode: TableMode,
@ -332,3 +368,41 @@ impl Default for TableConfig {
} }
} }
} }
impl UpdateFromValue for TableConfig {
fn update<'a>(
&mut self,
value: &'a Value,
path: &mut ConfigPath<'a>,
errors: &mut ConfigErrors,
) {
let Value::Record { val: record, .. } = value else {
errors.type_mismatch(path, Type::record(), value);
return;
};
for (col, val) in record.iter() {
let path = &mut path.push(col);
match col.as_str() {
"mode" => self.mode.update(val, path, errors),
"index_mode" => self.index_mode.update(val, path, errors),
"show_empty" => self.show_empty.update(val, path, errors),
"trim" => self.trim.update(val, path, errors),
"header_on_separator" => self.header_on_separator.update(val, path, errors),
"padding" => self.padding.update(val, path, errors),
"abbreviated_row_count" => match val {
Value::Nothing { .. } => self.abbreviated_row_count = None,
&Value::Int { val: count, .. } => {
if let Ok(count) = count.try_into() {
self.abbreviated_row_count = Some(count);
} else {
errors.invalid_value(path, "a non-negative integer", val);
}
}
_ => errors.type_mismatch(path, Type::custom("int or nothing"), val),
},
_ => errors.unknown_option(path, val),
}
}
}
}

View File

@ -3,7 +3,7 @@ use crate::{
ArgumentStack, EngineState, ErrorHandlerStack, Redirection, StackCallArgGuard, ArgumentStack, EngineState, ErrorHandlerStack, Redirection, StackCallArgGuard,
StackCollectValueGuard, StackIoGuard, StackOutDest, DEFAULT_OVERLAY_NAME, StackCollectValueGuard, StackIoGuard, StackOutDest, DEFAULT_OVERLAY_NAME,
}, },
Config, OutDest, ShellError, Span, Value, VarId, ENV_VARIABLE_ID, NU_VARIABLE_ID, Config, IntoValue, OutDest, ShellError, Span, Value, VarId, ENV_VARIABLE_ID, NU_VARIABLE_ID,
}; };
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
@ -211,12 +211,13 @@ impl Stack {
/// ///
/// The config will be updated with successfully parsed values even if an error occurs. /// The config will be updated with successfully parsed values even if an error occurs.
pub fn update_config(&mut self, engine_state: &EngineState) -> Result<(), ShellError> { pub fn update_config(&mut self, engine_state: &EngineState) -> Result<(), ShellError> {
if let Some(mut config) = self.get_env_var(engine_state, "config").cloned() { if let Some(value) = self.get_env_var(engine_state, "config") {
let existing_config = self.get_config(engine_state); let old = self.get_config(engine_state);
let (new_config, error) = config.parse_as_config(&existing_config); let mut config = (*old).clone();
self.config = Some(new_config.into()); let error = config.update_from_value(&old, value);
// The config value is modified by the update, so we should add it again // The config value is modified by the update, so we should add it again
self.add_env_var("config".into(), config); self.add_env_var("config".into(), config.clone().into_value(value.span()));
self.config = Some(config.into());
match error { match error {
None => Ok(()), None => Ok(()),
Some(err) => Err(err), Some(err) => Err(err),

View File

@ -15,8 +15,8 @@ use thiserror::Error;
/// forwards most methods, except for `.source_code()`, which we provide. /// forwards most methods, except for `.source_code()`, which we provide.
#[derive(Error)] #[derive(Error)]
#[error("{0}")] #[error("{0}")]
pub struct CliError<'src>( struct CliError<'src>(
pub &'src (dyn miette::Diagnostic + Send + Sync + 'static), pub &'src dyn miette::Diagnostic,
pub &'src StateWorkingSet<'src>, pub &'src StateWorkingSet<'src>,
); );
@ -48,10 +48,7 @@ pub fn report_compile_error(working_set: &StateWorkingSet, error: &CompileError)
report_error(working_set, error); report_error(working_set, error);
} }
fn report_error( fn report_error(working_set: &StateWorkingSet, error: &dyn miette::Diagnostic) {
working_set: &StateWorkingSet,
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
) {
eprintln!("Error: {:?}", CliError(error, working_set)); eprintln!("Error: {:?}", CliError(error, working_set));
// reset vt processing, aka ansi because illbehaved externals can break it // reset vt processing, aka ansi because illbehaved externals can break it
#[cfg(windows)] #[cfg(windows)]
@ -60,10 +57,7 @@ fn report_error(
} }
} }
fn report_warning( fn report_warning(working_set: &StateWorkingSet, error: &dyn miette::Diagnostic) {
working_set: &StateWorkingSet,
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
) {
eprintln!("Warning: {:?}", CliError(error, working_set)); eprintln!("Warning: {:?}", CliError(error, working_set));
// reset vt processing, aka ansi because illbehaved externals can break it // reset vt processing, aka ansi because illbehaved externals can break it
#[cfg(windows)] #[cfg(windows)]

View File

@ -0,0 +1,56 @@
use crate::{ShellError, Span, Type};
use miette::Diagnostic;
use thiserror::Error;
/// The errors that may occur when updating the config
#[derive(Clone, Debug, PartialEq, Error, Diagnostic)]
pub enum ConfigError {
#[error("Type mismatch at {path}")]
#[diagnostic(code(nu::shell::type_mismatch))]
TypeMismatch {
path: String,
expected: Type,
actual: Type,
#[label = "expected {expected}, but got {actual}"]
span: Span,
},
#[error("Invalid value for {path}")]
#[diagnostic(code(nu::shell::invalid_value))]
InvalidValue {
path: String,
valid: String,
actual: String,
#[label = "expected {valid}, but got {actual}"]
span: Span,
},
#[error("Unknown config option: {path}")]
#[diagnostic(code(nu::shell::unknown_config_option))]
UnknownOption {
path: String,
#[label("remove this")]
span: Span,
},
#[error("{path} requires a '{column}' column")]
#[diagnostic(code(nu::shell::missing_required_column))]
MissingRequiredColumn {
path: String,
column: &'static str,
#[label("has no '{column}' column")]
span: Span,
},
#[error("{path} is deprecated")]
#[diagnostic(
code(nu::shell::deprecated_config_option),
help("please {suggestion} instead")
)]
Deprecated {
path: String,
suggestion: &'static str,
#[label("deprecated")]
span: Span,
},
// TODO: remove this
#[error(transparent)]
#[diagnostic(transparent)]
ShellError(#[from] ShellError),
}

View File

@ -1,5 +1,6 @@
pub mod cli_error; pub mod cli_error;
mod compile_error; mod compile_error;
mod config_error;
mod labeled_error; mod labeled_error;
mod parse_error; mod parse_error;
mod parse_warning; mod parse_warning;
@ -10,6 +11,7 @@ pub use cli_error::{
report_shell_warning, report_shell_warning,
}; };
pub use compile_error::CompileError; pub use compile_error::CompileError;
pub use config_error::ConfigError;
pub use labeled_error::{ErrorLabel, LabeledError}; pub use labeled_error::{ErrorLabel, LabeledError};
pub use parse_error::{DidYouMean, ParseError}; pub use parse_error::{DidYouMean, ParseError};
pub use parse_warning::ParseWarning; pub use parse_warning::ParseWarning;

View File

@ -1,13 +1,12 @@
use crate::{
ast::Operator, engine::StateWorkingSet, format_shell_error, record, ConfigError, LabeledError,
ParseError, Span, Spanned, Type, Value,
};
use miette::Diagnostic; use miette::Diagnostic;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{io, num::NonZeroI32}; use std::{io, num::NonZeroI32};
use thiserror::Error; use thiserror::Error;
use crate::{
ast::Operator, engine::StateWorkingSet, format_shell_error, record, LabeledError, ParseError,
Span, Spanned, Value,
};
/// The fundamental error type for the evaluation engine. These cases represent different kinds of errors /// The fundamental error type for the evaluation engine. These cases represent different kinds of errors
/// the evaluator might face, along with helpful spans to label. An error renderer will take this error value /// the evaluator might face, along with helpful spans to label. An error renderer will take this error value
/// and pass it into an error viewer to display to the user. /// and pass it into an error viewer to display to the user.
@ -108,6 +107,34 @@ pub enum ShellError {
span: Span, span: Span,
}, },
/// A value's type did not match the expected type.
///
/// ## Resolution
///
/// Convert the value to the correct type or provide a value of the correct type.
#[error("Type mismatch")]
#[diagnostic(code(nu::shell::type_mismatch))]
RuntimeTypeMismatch {
expected: Type,
actual: Type,
#[label = "expected {expected}, but got {actual}"]
span: Span,
},
/// A value had the correct type but is otherwise invalid.
///
/// ## Resolution
///
/// Ensure the value meets the criteria in the error message.
#[error("Invalid value")]
#[diagnostic(code(nu::shell::invalid_value))]
InvalidValue {
valid: String,
actual: String,
#[label = "expected {valid}, but got {actual}"]
span: Span,
},
/// A command received an argument with correct type but incorrect value. /// A command received an argument with correct type but incorrect value.
/// ///
/// ## Resolution /// ## Resolution
@ -1085,30 +1112,28 @@ pub enum ShellError {
span: Span, span: Span,
}, },
/// The value given for this configuration is not supported. /// Failed to update the config due to one or more errors.
/// ///
/// ## Resolution /// ## Resolution
/// ///
/// Refer to the specific error message for details and convert values as needed. /// Refer to the error messages for specific details.
#[error("Unsupported config value")] #[error("Encountered {} error(s) when updating config", errors.len())]
#[diagnostic(code(nu::shell::unsupported_config_value))] #[diagnostic(code(nu::shell::invalid_config))]
UnsupportedConfigValue { InvalidConfig {
expected: String, #[related]
value: String, errors: Vec<ConfigError>,
#[label("expected {expected}, got {value}")]
span: Span,
}, },
/// An expected configuration value is not present. /// A value was missing a required column.
/// ///
/// ## Resolution /// ## Resolution
/// ///
/// Refer to the specific error message and add the configuration value to your config file as needed. /// Make sure the value has the required column.
#[error("Missing config value")] #[error("Value is missing a required '{column}' column")]
#[diagnostic(code(nu::shell::missing_config_value))] #[diagnostic(code(nu::shell::missing_required_column))]
MissingConfigValue { MissingRequiredColumn {
missing_value: String, column: &'static str,
#[label("missing {missing_value}")] #[label("has no '{column}' column")]
span: Span, span: Span,
}, },

View File

@ -48,6 +48,10 @@ impl Type {
Self::Table([].into()) Self::Table([].into())
} }
pub fn custom(name: impl Into<Box<str>>) -> Self {
Self::Custom(name.into())
}
pub fn is_subtype(&self, other: &Type) -> bool { pub fn is_subtype(&self, other: &Type) -> bool {
// Structural subtyping // Structural subtyping
let is_subtype_collection = |this: &[(String, Type)], that: &[(String, Type)]| { let is_subtype_collection = |this: &[(String, Type)], that: &[(String, Type)]| {

View File

@ -52,7 +52,7 @@ fn config_add_unsupported_key() {
assert!(actual assert!(actual
.err .err
.contains("$env.config.foo is an unknown config setting")); .contains("Unknown config option: $env.config.foo"));
} }
#[test] #[test]
@ -61,7 +61,7 @@ fn config_add_unsupported_type() {
r#"$env.config.ls = '' "#, r#"$env.config.ls = '' "#,
r#";"#])); r#";"#]));
assert!(actual.err.contains("should be a record")); assert!(actual.err.contains("Type mismatch"));
} }
#[test] #[test]
@ -70,12 +70,8 @@ fn config_add_unsupported_value() {
r#"$env.config.history.file_format = ''"#, r#"$env.config.history.file_format = ''"#,
r#";"#])); r#";"#]));
assert!(actual assert!(actual.err.contains("Invalid value"));
.err assert!(actual.err.contains("expected 'sqlite' or 'plaintext'"));
.contains("unrecognized $env.config.history.file_format option ''"));
assert!(actual
.err
.contains("expected either 'sqlite' or 'plaintext'"));
} }
#[test] #[test]

View File

@ -2,10 +2,9 @@ use nu_cmd_base::hook::{eval_env_change_hook, eval_hook};
use nu_engine::eval_block; use nu_engine::eval_block;
use nu_parser::parse; use nu_parser::parse;
use nu_protocol::{ use nu_protocol::{
cli_error::CliError,
debugger::WithoutDebug, debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
PipelineData, Value, report_parse_error, report_shell_error, PipelineData, ShellError, Value,
}; };
use nu_std::load_standard_library; use nu_std::load_standard_library;
use std::{ use std::{
@ -209,20 +208,13 @@ pub fn chop() {
std::process::exit(0); std::process::exit(0);
} }
fn outcome_err( fn outcome_err(engine_state: &EngineState, error: &ShellError) -> ! {
engine_state: &EngineState, report_shell_error(engine_state, error);
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
) -> ! {
let working_set = StateWorkingSet::new(engine_state);
eprintln!("Error: {:?}", CliError(error, &working_set));
std::process::exit(1); std::process::exit(1);
} }
fn outcome_ok(msg: String) -> ! { fn outcome_ok(msg: String) -> ! {
println!("{msg}"); println!("{msg}");
std::process::exit(0); std::process::exit(0);
} }
@ -320,7 +312,8 @@ pub fn nu_repl() {
); );
if let Some(err) = working_set.parse_errors.first() { if let Some(err) = working_set.parse_errors.first() {
outcome_err(&engine_state, err); report_parse_error(&working_set, err);
std::process::exit(1);
} }
(block, working_set.render()) (block, working_set.render())
}; };

View File

@ -441,7 +441,7 @@ fn err_hook_wrong_env_type_1() {
let actual_repl = nu!(nu_repl_code(inp)); let actual_repl = nu!(nu_repl_code(inp));
dbg!(&actual_repl.err); dbg!(&actual_repl.err);
assert!(actual_repl.err.contains("unsupported_config_value")); assert!(actual_repl.err.contains("Type mismatch"));
assert_eq!(actual_repl.out, ""); assert_eq!(actual_repl.out, "");
} }
@ -458,7 +458,7 @@ fn err_hook_wrong_env_type_2() {
let actual_repl = nu!(nu_repl_code(inp)); let actual_repl = nu!(nu_repl_code(inp));
assert!(actual_repl.err.contains("type_mismatch")); assert!(actual_repl.err.contains("Type mismatch"));
assert_eq!(actual_repl.out, ""); assert_eq!(actual_repl.out, "");
} }
@ -480,7 +480,7 @@ fn err_hook_wrong_env_type_3() {
let actual_repl = nu!(nu_repl_code(inp)); let actual_repl = nu!(nu_repl_code(inp));
assert!(actual_repl.err.contains("unsupported_config_value")); assert!(actual_repl.err.contains("Type mismatch"));
assert_eq!(actual_repl.out, ""); assert_eq!(actual_repl.out, "");
} }
@ -503,7 +503,7 @@ fn err_hook_non_boolean_condition_output() {
let actual_repl = nu!(nu_repl_code(inp)); let actual_repl = nu!(nu_repl_code(inp));
assert!(actual_repl.err.contains("unsupported_config_value")); assert!(actual_repl.err.contains("Type mismatch"));
assert_eq!(actual_repl.out, ""); assert_eq!(actual_repl.out, "");
} }
@ -526,7 +526,7 @@ fn err_hook_non_condition_not_a_block() {
let actual_repl = nu!(nu_repl_code(inp)); let actual_repl = nu!(nu_repl_code(inp));
assert!(actual_repl.err.contains("unsupported_config_value")); assert!(actual_repl.err.contains("Type mismatch"));
assert_eq!(actual_repl.out, ""); assert_eq!(actual_repl.out, "");
} }
@ -548,7 +548,7 @@ fn err_hook_parse_error() {
let actual_repl = nu!(nu_repl_code(inp)); let actual_repl = nu!(nu_repl_code(inp));
assert!(actual_repl.err.contains("unsupported_config_value")); assert!(actual_repl.err.contains("source code has errors"));
assert_eq!(actual_repl.out, ""); assert_eq!(actual_repl.out, "");
} }

View File

@ -119,7 +119,7 @@ fn mutate_nu_config_plugin() -> TestResult {
#[test] #[test]
fn reject_nu_config_plugin_non_record() -> TestResult { fn reject_nu_config_plugin_non_record() -> TestResult {
fail_test(r#"$env.config.plugins = 5"#, "should be a record") fail_test(r#"$env.config.plugins = 5"#, "Type mismatch")
} }
#[test] #[test]
@ -151,7 +151,7 @@ fn mutate_nu_config_plugin_gc_default_stop_after_negative() -> TestResult {
$env.config.plugin_gc.default.stop_after = -1sec $env.config.plugin_gc.default.stop_after = -1sec
$env.config.plugin_gc.default.stop_after $env.config.plugin_gc.default.stop_after
"#, "#,
"must not be negative", "expected a non-negative duration",
) )
} }