mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 08:55:40 +02:00
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:
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
151
crates/nu-protocol/src/config/completions.rs
Normal file
151
crates/nu-protocol/src/config/completions.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,3 +6,34 @@ pub struct DatetimeFormatConfig {
|
||||
pub normal: 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
85
crates/nu-protocol/src/config/error.rs
Normal file
85
crates/nu-protocol/src/config/error.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,132 +1,171 @@
|
||||
use crate::{IntoValue, Record, ShellError, Span, Value};
|
||||
use std::{collections::HashMap, fmt::Display, str::FromStr};
|
||||
use super::error::ConfigErrors;
|
||||
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>(
|
||||
config_point: &mut T,
|
||||
config_path: &[&str],
|
||||
value: &mut Value,
|
||||
errors: &mut Vec<ShellError>,
|
||||
) where
|
||||
T: FromStr<Err = E> + Clone + IntoValue,
|
||||
E: Display,
|
||||
pub(super) struct ConfigPath<'a> {
|
||||
components: Vec<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> ConfigPath<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
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();
|
||||
if let Ok(v) = value.coerce_str() {
|
||||
match v.parse() {
|
||||
Ok(format) => {
|
||||
*config_point = format;
|
||||
}
|
||||
Err(err) => {
|
||||
errors.push(ShellError::GenericError {
|
||||
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);
|
||||
}
|
||||
fn update<'a>(
|
||||
&mut self,
|
||||
value: &'a Value,
|
||||
path: &mut ConfigPath<'a>,
|
||||
errors: &mut ConfigErrors,
|
||||
) {
|
||||
if let Ok(record) = value.as_record() {
|
||||
*self = record
|
||||
.iter()
|
||||
.map(|(key, val)| {
|
||||
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.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);
|
||||
errors.type_mismatch(path, Type::String, value);
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn extract_value<'record>(
|
||||
name: &str,
|
||||
column: &'static str,
|
||||
record: &'record Record,
|
||||
span: Span,
|
||||
) -> Result<&'record Value, ShellError> {
|
||||
record
|
||||
.get(name)
|
||||
.ok_or_else(|| ShellError::MissingConfigValue {
|
||||
missing_value: name.to_string(),
|
||||
span,
|
||||
})
|
||||
.get(column)
|
||||
.ok_or_else(|| ShellError::MissingRequiredColumn { column, span })
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::prelude::*;
|
||||
use super::{config_update_string_enum, prelude::*};
|
||||
use crate as nu_protocol;
|
||||
|
||||
#[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -26,11 +26,17 @@ impl FromStr for HistoryFileFormat {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"sqlite" => Ok(Self::Sqlite),
|
||||
"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)]
|
||||
pub struct HistoryConfig {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use super::prelude::*;
|
||||
use crate as nu_protocol;
|
||||
use crate::ShellError;
|
||||
|
||||
/// Definition of a parsed hook from the config object
|
||||
#[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
|
||||
pub(super) fn create_hooks(value: &Value) -> Result<Hooks, ShellError> {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::Record { val, .. } => {
|
||||
let mut hooks = Hooks::new();
|
||||
|
||||
for (col, val) in &**val {
|
||||
match col.as_str() {
|
||||
"pre_prompt" => hooks.pre_prompt = Some(val.clone()),
|
||||
"pre_execution" => hooks.pre_execution = Some(val.clone()),
|
||||
"env_change" => hooks.env_change = Some(val.clone()),
|
||||
"display_output" => hooks.display_output = Some(val.clone()),
|
||||
"command_not_found" => hooks.command_not_found = Some(val.clone()),
|
||||
x => {
|
||||
return Err(ShellError::UnsupportedConfigValue {
|
||||
expected: "'pre_prompt', 'pre_execution', 'env_change', 'display_output', 'command_not_found'".into(),
|
||||
value: x.into(),
|
||||
span
|
||||
});
|
||||
}
|
||||
}
|
||||
impl UpdateFromValue for Hooks {
|
||||
fn update<'a>(
|
||||
&mut self,
|
||||
value: &'a Value,
|
||||
path: &mut ConfigPath<'a>,
|
||||
errors: &mut ConfigErrors,
|
||||
) {
|
||||
fn update_option(field: &mut Option<Value>, value: &Value) {
|
||||
if value.is_nothing() {
|
||||
*field = None;
|
||||
} else {
|
||||
*field = Some(value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
"pre_prompt" => update_option(&mut self.pre_prompt, val),
|
||||
"pre_execution" => update_option(&mut self.pre_execution, val),
|
||||
"env_change" => update_option(&mut self.env_change, val),
|
||||
"display_output" => update_option(&mut self.display_output, val),
|
||||
"command_not_found" => update_option(&mut self.command_not_found, val),
|
||||
_ => errors.unknown_option(path, val),
|
||||
}
|
||||
|
||||
Ok(hooks)
|
||||
}
|
||||
_ => Err(ShellError::UnsupportedConfigValue {
|
||||
expected: "record for 'hooks' config".into(),
|
||||
value: "non-record value".into(),
|
||||
span,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +1,32 @@
|
||||
//! Module containing the internal representation of user configuration
|
||||
use self::helper::*;
|
||||
use self::hooks::*;
|
||||
|
||||
use crate::{IntoValue, ShellError, Span, Value};
|
||||
use reedline::create_keybindings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate as nu_protocol;
|
||||
use crate::FromValue;
|
||||
use helper::*;
|
||||
use prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use table::try_parse_trim_strategy;
|
||||
|
||||
pub use self::completer::{
|
||||
CompleterConfig, CompletionAlgorithm, CompletionSort, ExternalCompleterConfig,
|
||||
pub use completions::{
|
||||
CompletionAlgorithm, CompletionConfig, CompletionSort, ExternalCompleterConfig,
|
||||
};
|
||||
pub use self::datetime_format::DatetimeFormatConfig;
|
||||
pub use self::display_errors::DisplayErrors;
|
||||
pub use self::filesize::FilesizeConfig;
|
||||
pub use self::helper::extract_value;
|
||||
pub use self::history::{HistoryConfig, HistoryFileFormat};
|
||||
pub use self::hooks::Hooks;
|
||||
pub use self::ls::LsConfig;
|
||||
pub use self::output::ErrorStyle;
|
||||
pub use self::plugin_gc::{PluginGcConfig, PluginGcConfigs};
|
||||
pub use self::reedline::{
|
||||
create_menus, CursorShapeConfig, EditBindings, NuCursorShape, ParsedKeybinding, ParsedMenu,
|
||||
};
|
||||
pub use self::rm::RmConfig;
|
||||
pub use self::shell_integration::ShellIntegrationConfig;
|
||||
pub use self::table::{FooterMode, TableConfig, TableIndexMode, TableMode, TrimStrategy};
|
||||
pub use datetime_format::DatetimeFormatConfig;
|
||||
pub use display_errors::DisplayErrors;
|
||||
pub use filesize::FilesizeConfig;
|
||||
pub use helper::extract_value;
|
||||
pub use history::{HistoryConfig, HistoryFileFormat};
|
||||
pub use hooks::Hooks;
|
||||
pub use ls::LsConfig;
|
||||
pub use output::ErrorStyle;
|
||||
pub use plugin_gc::{PluginGcConfig, PluginGcConfigs};
|
||||
pub use reedline::{CursorShapeConfig, EditBindings, NuCursorShape, ParsedKeybinding, ParsedMenu};
|
||||
pub use rm::RmConfig;
|
||||
pub use shell_integration::ShellIntegrationConfig;
|
||||
pub use table::{FooterMode, TableConfig, TableIndexMode, TableMode, TrimStrategy};
|
||||
|
||||
mod completer;
|
||||
mod completions;
|
||||
mod datetime_format;
|
||||
mod display_errors;
|
||||
mod error;
|
||||
mod filesize;
|
||||
mod helper;
|
||||
mod history;
|
||||
@ -43,7 +40,7 @@ mod rm;
|
||||
mod shell_integration;
|
||||
mod table;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(Clone, Debug, IntoValue, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
pub filesize: FilesizeConfig,
|
||||
pub table: TableConfig,
|
||||
@ -53,7 +50,7 @@ pub struct Config {
|
||||
pub float_precision: i64,
|
||||
pub recursion_limit: i64,
|
||||
pub use_ansi_coloring: bool,
|
||||
pub completions: CompleterConfig,
|
||||
pub completions: CompletionConfig,
|
||||
pub edit_mode: EditBindings,
|
||||
pub history: HistoryConfig,
|
||||
pub keybindings: Vec<ParsedKeybinding>,
|
||||
@ -97,7 +94,7 @@ impl Default for Config {
|
||||
|
||||
history: HistoryConfig::default(),
|
||||
|
||||
completions: CompleterConfig::default(),
|
||||
completions: CompletionConfig::default(),
|
||||
|
||||
recursion_limit: 50,
|
||||
|
||||
@ -135,630 +132,104 @@ impl Default for Config {
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// Parse the given [`Value`] as a configuration record, and recover encountered mistakes
|
||||
///
|
||||
/// If any given (sub)value is detected as impossible, this value will be restored to the value
|
||||
/// in `existing_config`, thus mutates `self`.
|
||||
///
|
||||
/// Returns a new [`Config`] (that is in a valid state) and if encountered the [`ShellError`]
|
||||
/// containing all observed inner errors.
|
||||
pub fn parse_as_config(&mut self, existing_config: &Config) -> (Config, Option<ShellError>) {
|
||||
// Clone the passed-in config rather than mutating it.
|
||||
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![],
|
||||
}),
|
||||
);
|
||||
impl UpdateFromValue for Config {
|
||||
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;
|
||||
};
|
||||
|
||||
val.to_mut().retain_mut(|key, value| {
|
||||
let span = value.span();
|
||||
match key {
|
||||
"ls" => {
|
||||
if let Value::Record { val, .. } = value {
|
||||
val.to_mut().retain_mut(|key2, value| {
|
||||
let span = value.span();
|
||||
match key2 {
|
||||
"use_ls_colors" => {
|
||||
process_bool_config(value, &mut errors, &mut config.ls.use_ls_colors);
|
||||
}
|
||||
"clickable_links" => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
for (col, val) in record.iter() {
|
||||
let path = &mut path.push(col);
|
||||
match col.as_str() {
|
||||
"ls" => self.ls.update(val, path, errors),
|
||||
"rm" => self.rm.update(val, path, errors),
|
||||
"history" => self.history.update(val, path, errors),
|
||||
"completions" => self.completions.update(val, path, errors),
|
||||
"cursor_shape" => self.cursor_shape.update(val, path, errors),
|
||||
"table" => self.table.update(val, path, errors),
|
||||
"filesize" => self.filesize.update(val, path, errors),
|
||||
"explore" => self.explore.update(val, path, errors),
|
||||
"color_config" => self.color_config.update(val, path, errors),
|
||||
"use_grid_icons" => {
|
||||
// TODO: delete it after 0.99
|
||||
report_invalid_value(
|
||||
"`use_grid_icons` is deleted, you should delete the key, and use `grid -i` in such case.",
|
||||
span,
|
||||
&mut errors
|
||||
);
|
||||
errors.deprecated_option(path, "use `grid -i`", val.span());
|
||||
}
|
||||
"footer_mode" => {
|
||||
process_string_enum(
|
||||
&mut config.footer_mode,
|
||||
&[key],
|
||||
value,
|
||||
&mut errors
|
||||
);
|
||||
}
|
||||
"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 {
|
||||
"footer_mode" => self.footer_mode.update(val, path, errors),
|
||||
"float_precision" => self.float_precision.update(val, path, errors),
|
||||
"use_ansi_coloring" => self.use_ansi_coloring.update(val, path, errors),
|
||||
"edit_mode" => self.edit_mode.update(val, path, errors),
|
||||
"shell_integration" => self.shell_integration.update(val, path, errors),
|
||||
"buffer_editor" => match val {
|
||||
Value::Nothing { .. } | Value::String { .. } => {
|
||||
config.buffer_editor = value.clone();
|
||||
self.buffer_editor = val.clone();
|
||||
}
|
||||
Value::List { vals, .. }
|
||||
if vals.iter().all(|val| matches!(val, Value::String { .. })) =>
|
||||
{
|
||||
config.buffer_editor = value.clone();
|
||||
}
|
||||
_ => {
|
||||
report_invalid_value("should be a string, list<string>, or null", span, &mut errors);
|
||||
*value = config.buffer_editor.clone();
|
||||
self.buffer_editor = val.clone();
|
||||
}
|
||||
_ => errors.type_mismatch(
|
||||
path,
|
||||
Type::custom("string, list<string>, or nothing"),
|
||||
val,
|
||||
),
|
||||
},
|
||||
"show_banner" => {
|
||||
process_bool_config(value, &mut errors, &mut config.show_banner);
|
||||
}
|
||||
"display_errors" => {
|
||||
if let Value::Record { val, .. } = value {
|
||||
val.to_mut().retain_mut(|key2, value| {
|
||||
let span = value.span();
|
||||
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);
|
||||
}
|
||||
"show_banner" => self.show_banner.update(val, path, errors),
|
||||
"display_errors" => self.display_errors.update(val, path, errors),
|
||||
"render_right_prompt_on_last_line" => self
|
||||
.render_right_prompt_on_last_line
|
||||
.update(val, path, errors),
|
||||
"bracketed_paste" => self.bracketed_paste.update(val, path, errors),
|
||||
"use_kitty_protocol" => self.use_kitty_protocol.update(val, path, errors),
|
||||
"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) {
|
||||
Ok(keybindings) => config.keybindings = keybindings,
|
||||
Err(e) => {
|
||||
report_invalid_value("should be a valid keybindings list", span, &mut errors);
|
||||
errors.push(e);
|
||||
*value = config.keybindings.clone().into_value(span);
|
||||
}
|
||||
"keybindings" => match Vec::from_value(val.clone()) {
|
||||
Ok(keybindings) => self.keybindings = keybindings,
|
||||
Err(err) => errors.error(err.into()),
|
||||
},
|
||||
"hooks" => match create_hooks(value) {
|
||||
Ok(hooks) => config.hooks = hooks,
|
||||
Err(e) => {
|
||||
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
|
||||
);
|
||||
}
|
||||
"hooks" => self.hooks.update(val, path, errors),
|
||||
"datetime_format" => self.datetime_format.update(val, path, errors),
|
||||
"error_style" => self.error_style.update(val, path, errors),
|
||||
"recursion_limit" => {
|
||||
if let Value::Int { val, internal_span } = value {
|
||||
if val > &mut 1 {
|
||||
config.recursion_limit = *val;
|
||||
if let Ok(limit) = val.as_int() {
|
||||
if limit > 1 {
|
||||
self.recursion_limit = limit;
|
||||
} else {
|
||||
report_invalid_value("should be a integer greater than 1", span, &mut errors);
|
||||
*value = Value::Int { val: 50, internal_span: *internal_span };
|
||||
errors.invalid_value(path, "an int greater than 1", val);
|
||||
}
|
||||
} else {
|
||||
report_invalid_value("should be a integer greater than 1", span, &mut errors);
|
||||
*value = Value::Int { val: 50, internal_span: value.span() };
|
||||
errors.type_mismatch(path, Type::Int, val);
|
||||
}
|
||||
}
|
||||
// Catch all
|
||||
_ => {
|
||||
report_invalid_key(&[key], span, &mut errors);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
true
|
||||
});
|
||||
|
||||
// Return the config and the vec of errors.
|
||||
(
|
||||
config,
|
||||
if !errors.is_empty() {
|
||||
Some(ShellError::GenericError {
|
||||
error: "Config record contains invalid values or unknown settings".into(),
|
||||
msg: "".into(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: errors,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
_ => errors.unknown_option(path, val),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
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
|
||||
// of the config from being updated (fields with errors are skipped/not updated).
|
||||
// Errors are simply collected one-by-one and wrapped into a ShellError variant at the end.
|
||||
let mut errors = ConfigErrors::new(old);
|
||||
let mut path = ConfigPath::new();
|
||||
|
||||
self.update(value, &mut path, &mut errors);
|
||||
|
||||
errors.into_shell_error()
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::prelude::*;
|
||||
use super::{config_update_string_enum, prelude::*};
|
||||
use crate as nu_protocol;
|
||||
|
||||
#[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -14,7 +14,13 @@ impl FromStr for ErrorStyle {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"fancy" => Ok(Self::Fancy),
|
||||
"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)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
use super::helper::{process_bool_config, report_invalid_key, report_invalid_value};
|
||||
use super::prelude::*;
|
||||
use crate as nu_protocol;
|
||||
use crate::ShellError;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Configures when plugins should be stopped if inactive
|
||||
@ -19,79 +17,28 @@ impl PluginGcConfigs {
|
||||
pub fn get(&self, plugin_name: &str) -> &PluginGcConfig {
|
||||
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(
|
||||
path: &[&str],
|
||||
value: &mut Value,
|
||||
errors: &mut Vec<ShellError>,
|
||||
plugins: &mut HashMap<String, PluginGcConfig>,
|
||||
) {
|
||||
if let Value::Record { val, .. } = value {
|
||||
// Remove any plugin configs that aren't in the value
|
||||
plugins.retain(|key, _| val.contains(key));
|
||||
impl UpdateFromValue for PluginGcConfigs {
|
||||
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;
|
||||
};
|
||||
|
||||
val.to_mut().retain_mut(|key, value| {
|
||||
if matches!(value, Value::Record { .. }) {
|
||||
plugins.entry(key.to_owned()).or_default().process(
|
||||
&join_path(path, &[key]),
|
||||
value,
|
||||
errors,
|
||||
);
|
||||
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
|
||||
}
|
||||
for (col, val) in record.iter() {
|
||||
let path = &mut path.push(col);
|
||||
match col.as_str() {
|
||||
"default" => self.default.update(val, path, errors),
|
||||
"plugins" => self.plugins.update(val, path, errors),
|
||||
_ => errors.unknown_option(path, val),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,57 +70,43 @@ impl IntoValue for PluginGcConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginGcConfig {
|
||||
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("enabled") {
|
||||
self.enabled = PluginGcConfig::default().enabled;
|
||||
}
|
||||
if !val.contains("stop_after") {
|
||||
self.stop_after = PluginGcConfig::default().stop_after;
|
||||
}
|
||||
impl UpdateFromValue for PluginGcConfig {
|
||||
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;
|
||||
};
|
||||
|
||||
val.to_mut().retain_mut(|key, value| {
|
||||
let span = value.span();
|
||||
match key {
|
||||
"enabled" => process_bool_config(value, errors, &mut self.enabled),
|
||||
"stop_after" => match value {
|
||||
Value::Duration { val, .. } => {
|
||||
if *val >= 0 {
|
||||
self.stop_after = *val;
|
||||
} else {
|
||||
report_invalid_value("must not be negative", span, errors);
|
||||
*val = self.stop_after;
|
||||
}
|
||||
for (col, val) in record.iter() {
|
||||
let path = &mut path.push(col);
|
||||
match col.as_str() {
|
||||
"enabled" => self.enabled.update(val, path, errors),
|
||||
"stop_after" => {
|
||||
if let Ok(duration) = val.as_duration() {
|
||||
if duration >= 0 {
|
||||
self.stop_after = duration;
|
||||
} else {
|
||||
errors.invalid_value(path, "a non-negative duration", val);
|
||||
}
|
||||
_ => {
|
||||
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;
|
||||
} else {
|
||||
errors.type_mismatch(path, Type::Duration, val);
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
} else {
|
||||
report_invalid_value("should be a record", value.span(), errors);
|
||||
*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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nu_protocol::{record, Span};
|
||||
use crate::{record, Config, Span};
|
||||
|
||||
fn test_pair() -> (PluginGcConfigs, Value) {
|
||||
(
|
||||
@ -208,11 +141,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process() {
|
||||
let (expected, mut input) = test_pair();
|
||||
let mut errors = vec![];
|
||||
fn update() {
|
||||
let (expected, input) = test_pair();
|
||||
let config = Config::default();
|
||||
let mut errors = ConfigErrors::new(&config);
|
||||
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_eq!(expected, result);
|
||||
}
|
||||
|
@ -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 std::str::FromStr;
|
||||
|
@ -1,9 +1,9 @@
|
||||
use super::{extract_value, prelude::*};
|
||||
use super::{config_update_string_enum, prelude::*};
|
||||
use crate as nu_protocol;
|
||||
use crate::ShellError;
|
||||
use crate::{engine::Closure, FromValue};
|
||||
|
||||
/// 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 modifier: Value,
|
||||
pub keycode: Value,
|
||||
@ -12,14 +12,14 @@ pub struct ParsedKeybinding {
|
||||
}
|
||||
|
||||
/// 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 name: Value,
|
||||
pub marker: Value,
|
||||
pub only_buffer_difference: Value,
|
||||
pub style: Value,
|
||||
pub r#type: Value,
|
||||
pub source: Value,
|
||||
pub source: Option<Closure>,
|
||||
}
|
||||
|
||||
/// 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_underscore" => Ok(NuCursorShape::BlinkUnderscore),
|
||||
"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)]
|
||||
pub struct CursorShapeConfig {
|
||||
pub emacs: NuCursorShape,
|
||||
@ -59,6 +65,30 @@ pub struct CursorShapeConfig {
|
||||
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)]
|
||||
pub enum EditBindings {
|
||||
Vi,
|
||||
@ -73,89 +103,13 @@ impl FromStr for EditBindings {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"vi" => Ok(Self::Vi),
|
||||
"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
|
||||
pub(super) fn create_keybindings(value: &Value) -> Result<Vec<ParsedKeybinding>, ShellError> {
|
||||
let span = value.span();
|
||||
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()),
|
||||
impl UpdateFromValue for EditBindings {
|
||||
fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
|
||||
config_update_string_enum(self, value, path, errors)
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use super::prelude::*;
|
||||
use super::{config_update_string_enum, prelude::*};
|
||||
use crate as nu_protocol;
|
||||
use crate::ShellError;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TableMode {
|
||||
@ -45,11 +44,17 @@ impl FromStr for TableMode {
|
||||
"restructured" => Ok(Self::Restructured),
|
||||
"ascii_rounded" => Ok(Self::AsciiRounded),
|
||||
"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)]
|
||||
pub enum FooterMode {
|
||||
/// Never show the footer
|
||||
@ -70,13 +75,30 @@ impl FromStr for FooterMode {
|
||||
"always" => Ok(FooterMode::Always),
|
||||
"never" => Ok(FooterMode::Never),
|
||||
"auto" => Ok(FooterMode::Auto),
|
||||
x => {
|
||||
if let Ok(count) = x.parse() {
|
||||
Ok(FooterMode::RowCount(count))
|
||||
_ => Err("'never', 'always', 'auto', or int"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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::Never => "never".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),
|
||||
"never" => Ok(TableIndexMode::Never),
|
||||
"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
|
||||
/// we need to limit cell width in order to adjust for a terminal size.
|
||||
#[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 {
|
||||
fn into_value(self, span: Span) -> Value {
|
||||
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)]
|
||||
pub struct TableIndent {
|
||||
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)]
|
||||
pub struct TableConfig {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use crate::{
|
||||
ArgumentStack, EngineState, ErrorHandlerStack, Redirection, StackCallArgGuard,
|
||||
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::{
|
||||
collections::{HashMap, HashSet},
|
||||
@ -211,12 +211,13 @@ impl Stack {
|
||||
///
|
||||
/// 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> {
|
||||
if let Some(mut config) = self.get_env_var(engine_state, "config").cloned() {
|
||||
let existing_config = self.get_config(engine_state);
|
||||
let (new_config, error) = config.parse_as_config(&existing_config);
|
||||
self.config = Some(new_config.into());
|
||||
if let Some(value) = self.get_env_var(engine_state, "config") {
|
||||
let old = self.get_config(engine_state);
|
||||
let mut config = (*old).clone();
|
||||
let error = config.update_from_value(&old, value);
|
||||
// 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 {
|
||||
None => Ok(()),
|
||||
Some(err) => Err(err),
|
||||
|
@ -15,8 +15,8 @@ use thiserror::Error;
|
||||
/// forwards most methods, except for `.source_code()`, which we provide.
|
||||
#[derive(Error)]
|
||||
#[error("{0}")]
|
||||
pub struct CliError<'src>(
|
||||
pub &'src (dyn miette::Diagnostic + Send + Sync + 'static),
|
||||
struct CliError<'src>(
|
||||
pub &'src dyn miette::Diagnostic,
|
||||
pub &'src StateWorkingSet<'src>,
|
||||
);
|
||||
|
||||
@ -48,10 +48,7 @@ pub fn report_compile_error(working_set: &StateWorkingSet, error: &CompileError)
|
||||
report_error(working_set, error);
|
||||
}
|
||||
|
||||
fn report_error(
|
||||
working_set: &StateWorkingSet,
|
||||
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
|
||||
) {
|
||||
fn report_error(working_set: &StateWorkingSet, error: &dyn miette::Diagnostic) {
|
||||
eprintln!("Error: {:?}", CliError(error, working_set));
|
||||
// reset vt processing, aka ansi because illbehaved externals can break it
|
||||
#[cfg(windows)]
|
||||
@ -60,10 +57,7 @@ fn report_error(
|
||||
}
|
||||
}
|
||||
|
||||
fn report_warning(
|
||||
working_set: &StateWorkingSet,
|
||||
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
|
||||
) {
|
||||
fn report_warning(working_set: &StateWorkingSet, error: &dyn miette::Diagnostic) {
|
||||
eprintln!("Warning: {:?}", CliError(error, working_set));
|
||||
// reset vt processing, aka ansi because illbehaved externals can break it
|
||||
#[cfg(windows)]
|
||||
|
56
crates/nu-protocol/src/errors/config_error.rs
Normal file
56
crates/nu-protocol/src/errors/config_error.rs
Normal 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),
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
pub mod cli_error;
|
||||
mod compile_error;
|
||||
mod config_error;
|
||||
mod labeled_error;
|
||||
mod parse_error;
|
||||
mod parse_warning;
|
||||
@ -10,6 +11,7 @@ pub use cli_error::{
|
||||
report_shell_warning,
|
||||
};
|
||||
pub use compile_error::CompileError;
|
||||
pub use config_error::ConfigError;
|
||||
pub use labeled_error::{ErrorLabel, LabeledError};
|
||||
pub use parse_error::{DidYouMean, ParseError};
|
||||
pub use parse_warning::ParseWarning;
|
||||
|
@ -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 serde::{Deserialize, Serialize};
|
||||
use std::{io, num::NonZeroI32};
|
||||
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 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.
|
||||
@ -108,6 +107,34 @@ pub enum ShellError {
|
||||
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.
|
||||
///
|
||||
/// ## Resolution
|
||||
@ -1085,30 +1112,28 @@ pub enum ShellError {
|
||||
span: Span,
|
||||
},
|
||||
|
||||
/// The value given for this configuration is not supported.
|
||||
/// Failed to update the config due to one or more errors.
|
||||
///
|
||||
/// ## Resolution
|
||||
///
|
||||
/// Refer to the specific error message for details and convert values as needed.
|
||||
#[error("Unsupported config value")]
|
||||
#[diagnostic(code(nu::shell::unsupported_config_value))]
|
||||
UnsupportedConfigValue {
|
||||
expected: String,
|
||||
value: String,
|
||||
#[label("expected {expected}, got {value}")]
|
||||
span: Span,
|
||||
/// Refer to the error messages for specific details.
|
||||
#[error("Encountered {} error(s) when updating config", errors.len())]
|
||||
#[diagnostic(code(nu::shell::invalid_config))]
|
||||
InvalidConfig {
|
||||
#[related]
|
||||
errors: Vec<ConfigError>,
|
||||
},
|
||||
|
||||
/// An expected configuration value is not present.
|
||||
/// A value was missing a required column.
|
||||
///
|
||||
/// ## Resolution
|
||||
///
|
||||
/// Refer to the specific error message and add the configuration value to your config file as needed.
|
||||
#[error("Missing config value")]
|
||||
#[diagnostic(code(nu::shell::missing_config_value))]
|
||||
MissingConfigValue {
|
||||
missing_value: String,
|
||||
#[label("missing {missing_value}")]
|
||||
/// Make sure the value has the required column.
|
||||
#[error("Value is missing a required '{column}' column")]
|
||||
#[diagnostic(code(nu::shell::missing_required_column))]
|
||||
MissingRequiredColumn {
|
||||
column: &'static str,
|
||||
#[label("has no '{column}' column")]
|
||||
span: Span,
|
||||
},
|
||||
|
||||
|
@ -48,6 +48,10 @@ impl Type {
|
||||
Self::Table([].into())
|
||||
}
|
||||
|
||||
pub fn custom(name: impl Into<Box<str>>) -> Self {
|
||||
Self::Custom(name.into())
|
||||
}
|
||||
|
||||
pub fn is_subtype(&self, other: &Type) -> bool {
|
||||
// Structural subtyping
|
||||
let is_subtype_collection = |this: &[(String, Type)], that: &[(String, Type)]| {
|
||||
|
Reference in New Issue
Block a user