Refactor and fix Config<->Value mechanism (#10896)

# Description
Our config exists both as a `Config` struct for internal consumption and
as a `Value`. The latter is exposed through `$env.config` and can be
both set and read.
Thus we have a complex bug-prone mechanism, that reads a `Value` and
then tries to plug anything where the value is unrepresentable in
`Config` with the correct state from `Config`.

The parsing involves therefore mutation of the `Value` in a nested
`Record` structure. Previously this was wholy done manually, with
indices.
To enable deletion for example, things had to be iterated over from the
back. Also things were indexed in a bunch of places. This was hard to
read and an invitation for bugs.

With #10876 we can now use `Record::retain_mut` to traverse the records,
modify anything that needs fixing, and drop invalid fields.

# Parts:

- Error messages now consistently use the correct spans pointing to the
problematic value and the paths displayed in some messages are also
aligned with the keys used for lookup.
- Reconstruction of values has been fixed for:
	- `table.padding`
	- `buffer_editor`
	- `hooks.command_not_found`
	- `datetime_format` (partial solution)
- Fix validation of `table.padding` input so value is not set (and
underflows `usize` causing `table` to run forever with negative values)
- New proper types for settings. Fully validated enums instead of
strings:
  - `config.edit_mode` -> `EditMode` 
  	- Don't fall back to vi-mode on invalid string
  - `config.table.mode` -> `TableMode`
- there is still a fall back to `rounded` if given an invalid
`TableMode` as argument to the `nu` binary
  - `config.completions.algorithm` -> `CompletionAlgorithm`
  - `config.error_style` -> `ErrorStyle`
    - don't implicitly fall back to `fancy` when given an invalid value.
- This should also shrink the size of `Config` as instead of 4x24 bytes
those fields now need only 4x1 bytes in `Config`
- Completely removed macros relying on the scope of `Value::into_config`
so we can break it up into smaller parts in the future.
- Factored everything into smaller files with the types and helpers for
particular topics.
- `NuCursorShape` now explicitly expresses the `Inherit` setting.
conversion to option only happens at the interface to `reedline`
This commit is contained in:
Stefan Holderbach 2023-11-08 20:31:30 +01:00 committed by GitHub
parent edbf3aaccb
commit 86cd387439
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1729 additions and 1684 deletions

View File

@ -1,6 +1,6 @@
use crate::completions::{
CommandCompletion, Completer, CompletionOptions, CustomCompletion, DirectoryCompletion,
DotNuCompletion, FileCompletion, FlagCompletion, MatchAlgorithm, VariableCompletion,
DotNuCompletion, FileCompletion, FlagCompletion, VariableCompletion,
};
use nu_engine::eval_block;
use nu_parser::{flatten_expression, parse, FlatShape};
@ -39,15 +39,12 @@ impl NuCompleter {
) -> Vec<Suggestion> {
let config = self.engine_state.get_config();
let mut options = CompletionOptions {
let options = CompletionOptions {
case_sensitive: config.case_sensitive_completions,
match_algorithm: config.completion_algorithm.into(),
..Default::default()
};
if config.completion_algorithm == "fuzzy" {
options.match_algorithm = MatchAlgorithm::Fuzzy;
}
// Fetch
let mut suggestions =
completer.fetch(working_set, prefix.clone(), new_span, offset, pos, &options);

View File

@ -2,6 +2,7 @@ use std::fmt::Display;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use nu_parser::trim_quotes_str;
use nu_protocol::CompletionAlgorithm;
#[derive(Copy, Clone)]
pub enum SortBy {
@ -55,6 +56,15 @@ impl MatchAlgorithm {
}
}
impl From<CompletionAlgorithm> for MatchAlgorithm {
fn from(value: CompletionAlgorithm) -> Self {
match value {
CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix,
CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy,
}
}
}
impl TryFrom<String> for MatchAlgorithm {
type Error = InvalidMatchAlgorithm;

View File

@ -28,8 +28,8 @@ pub fn evaluate_commands(
let (block, delta) = {
if let Some(ref t_mode) = table_mode {
let mut config = engine_state.get_config().clone();
config.table_mode = t_mode.as_string()?;
engine_state.set_config(&config);
config.table_mode = t_mode.as_string()?.parse().unwrap_or_default();
engine_state.set_config(config);
}
let mut working_set = StateWorkingSet::new(engine_state);
@ -55,7 +55,7 @@ pub fn evaluate_commands(
Ok(pipeline_data) => {
let mut config = engine_state.get_config().clone();
if let Some(t_mode) = table_mode {
config.table_mode = t_mode.as_string()?;
config.table_mode = t_mode.as_string()?.parse().unwrap_or_default();
}
crate::eval_file::print_table_or_error(engine_state, stack, pipeline_data, &mut config)
}

View File

@ -195,7 +195,7 @@ pub(crate) fn print_table_or_error(
};
// Change the engine_state config to use the passed in configuration
engine_state.set_config(config);
engine_state.set_config(config.clone());
if let PipelineData::Value(Value::Error { error, .. }, ..) = &pipeline_data {
let working_set = StateWorkingSet::new(engine_state);

View File

@ -7,8 +7,8 @@ use nu_parser::parse;
use nu_protocol::{
create_menus,
engine::{EngineState, Stack, StateWorkingSet},
extract_value, Config, ParsedKeybinding, ParsedMenu, PipelineData, Record, ShellError, Span,
Value,
extract_value, Config, EditBindings, ParsedKeybinding, ParsedMenu, PipelineData, Record,
ShellError, Span, Value,
};
use reedline::{
default_emacs_keybindings, default_vi_insert_keybindings, default_vi_normal_keybindings,
@ -537,11 +537,11 @@ pub(crate) fn create_keybindings(config: &Config) -> Result<KeybindingsMode, She
let mut insert_keybindings = default_vi_insert_keybindings();
let mut normal_keybindings = default_vi_normal_keybindings();
match config.edit_mode.as_str() {
"emacs" => {
match config.edit_mode {
EditBindings::Emacs => {
add_menu_keybindings(&mut emacs_keybindings);
}
_ => {
EditBindings::Vi => {
add_menu_keybindings(&mut insert_keybindings);
add_menu_keybindings(&mut normal_keybindings);
}
@ -557,9 +557,9 @@ pub(crate) fn create_keybindings(config: &Config) -> Result<KeybindingsMode, She
)?
}
match config.edit_mode.as_str() {
"emacs" => Ok(KeybindingsMode::Emacs(emacs_keybindings)),
_ => Ok(KeybindingsMode::Vi {
match config.edit_mode {
EditBindings::Emacs => Ok(KeybindingsMode::Emacs(emacs_keybindings)),
EditBindings::Vi => Ok(KeybindingsMode::Vi {
insert_keybindings,
normal_keybindings,
}),

View File

@ -245,15 +245,9 @@ pub fn evaluate_repl(
// Find the configured cursor shapes for each mode
let cursor_config = CursorConfig {
vi_insert: config
.cursor_shape_vi_insert
.map(map_nucursorshape_to_cursorshape),
vi_normal: config
.cursor_shape_vi_normal
.map(map_nucursorshape_to_cursorshape),
emacs: config
.cursor_shape_emacs
.map(map_nucursorshape_to_cursorshape),
vi_insert: map_nucursorshape_to_cursorshape(config.cursor_shape_vi_insert),
vi_normal: map_nucursorshape_to_cursorshape(config.cursor_shape_vi_normal),
emacs: map_nucursorshape_to_cursorshape(config.cursor_shape_emacs),
};
perf(
"get config/cursor config",
@ -770,14 +764,15 @@ fn update_line_editor_history(
Ok(line_editor)
}
fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> SetCursorStyle {
fn map_nucursorshape_to_cursorshape(shape: NuCursorShape) -> Option<SetCursorStyle> {
match shape {
NuCursorShape::Block => SetCursorStyle::SteadyBlock,
NuCursorShape::UnderScore => SetCursorStyle::SteadyUnderScore,
NuCursorShape::Line => SetCursorStyle::SteadyBar,
NuCursorShape::BlinkBlock => SetCursorStyle::BlinkingBlock,
NuCursorShape::BlinkUnderScore => SetCursorStyle::BlinkingUnderScore,
NuCursorShape::BlinkLine => SetCursorStyle::BlinkingBar,
NuCursorShape::Block => Some(SetCursorStyle::SteadyBlock),
NuCursorShape::UnderScore => Some(SetCursorStyle::SteadyUnderScore),
NuCursorShape::Line => Some(SetCursorStyle::SteadyBar),
NuCursorShape::BlinkBlock => Some(SetCursorStyle::BlinkingBlock),
NuCursorShape::BlinkUnderScore => Some(SetCursorStyle::BlinkingUnderScore),
NuCursorShape::BlinkLine => Some(SetCursorStyle::BlinkingBar),
NuCursorShape::Inherit => None,
}
}

View File

@ -795,7 +795,7 @@ fn run_external_completion(block: &str, input: &str) -> Vec<Suggestion> {
// Change config adding the external completer
let mut config = engine_state.get_config().clone();
config.external_completer = Some(latest_block_id);
engine_state.set_config(&config);
engine_state.set_config(config);
// Instantiate a new completer
let mut completer = NuCompleter::new(std::sync::Arc::new(engine_state), stack);

View File

@ -1,4 +1,7 @@
use crate::engine::{EngineState, StateWorkingSet};
use crate::{
engine::{EngineState, StateWorkingSet},
ErrorStyle,
};
use miette::{
LabeledSpan, MietteHandlerOpts, NarratableReportHandler, ReportHandler, RgbColors, Severity,
SourceCode,
@ -49,12 +52,11 @@ impl std::fmt::Debug for CliError<'_> {
let ansi_support = &config.use_ansi_coloring;
let ansi_support = *ansi_support;
let error_style = &config.error_style.as_str();
let errors_style = *error_style;
let error_style = &config.error_style;
let miette_handler: Box<dyn ReportHandler> = match errors_style {
"plain" => Box::new(NarratableReportHandler::new()),
_ => Box::new(
let miette_handler: Box<dyn ReportHandler> = match error_style {
ErrorStyle::Plain => Box::new(NarratableReportHandler::new()),
ErrorStyle::Fancy => Box::new(
MietteHandlerOpts::new()
// For better support of terminal themes use the ANSI coloring
.rgb_colors(RgbColors::Never)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::{record, Config, Span, Value};
use super::helper::ReconstructVal;
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default)]
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'"),
}
}
}
impl ReconstructVal for CompletionAlgorithm {
fn reconstruct_value(&self, span: Span) -> Value {
let str = match self {
CompletionAlgorithm::Prefix => "prefix",
CompletionAlgorithm::Fuzzy => "fuzzy",
};
Value::string(str, span)
}
}
pub(super) fn reconstruct_external_completer(config: &Config, span: Span) -> Value {
if let Some(block) = config.external_completer {
Value::block(block, span)
} else {
Value::nothing(span)
}
}
pub(super) fn reconstruct_external(config: &Config, span: Span) -> Value {
Value::record(
record! {
"max_results" => Value::int(config.max_external_completion_results, span),
"completer" => reconstruct_external_completer(config, span),
"enable" => Value::bool(config.enable_external_completion, span),
},
span,
)
}

View File

@ -0,0 +1,133 @@
use crate::{Record, ShellError, Span, Value};
use std::{collections::HashMap, fmt::Display, str::FromStr};
pub(super) trait ReconstructVal {
fn reconstruct_value(&self, span: Span) -> Value;
}
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> + ReconstructVal,
E: Display,
{
let span = value.span();
if let Ok(v) = value.as_string() {
match v.parse() {
Ok(format) => {
*config_point = format;
}
Err(err) => {
errors.push(ShellError::GenericError(
"Error while applying config changes".into(),
format!(
"unrecognized $env.config.{} option '{v}'",
config_path.join(".")
),
Some(span),
Some(err.to_string()),
vec![],
));
// Reconstruct
*value = config_point.reconstruct_value(span);
}
}
} else {
errors.push(ShellError::GenericError(
"Error while applying config changes".into(),
format!("$env.config.{} should be a string", config_path.join(".")),
Some(span),
Some("This value will be ignored.".into()),
vec![],
));
// Reconstruct
*value = config_point.reconstruct_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 while applying config changes".into(),
"should be a bool".to_string(),
Some(value.span()),
Some("This value will be ignored.".into()),
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 while applying config changes".into(),
"should be an int".to_string(),
Some(value.span()),
Some("This value will be ignored.".into()),
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 while applying config changes".into(),
format!(
"$env.config.{} is an unknown config setting",
keys.join(".")
),
Some(span),
Some("This value will not appear in your $env.config record.".into()),
vec![],
));
}
pub(super) fn report_invalid_value(msg: &str, span: Span, errors: &mut Vec<ShellError>) {
errors.push(ShellError::GenericError(
"Error while applying config changes".into(),
msg.into(),
Some(span),
Some("This value will be ignored.".into()),
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,
record: &'record Record,
span: Span,
) -> Result<&'record Value, ShellError> {
record
.get(name)
.ok_or_else(|| ShellError::MissingConfigValue(name.to_string(), span))
}

View File

@ -0,0 +1,88 @@
use crate::{Config, Record, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
/// Definition of a parsed hook from the config object
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Hooks {
pub pre_prompt: Option<Value>,
pub pre_execution: Option<Value>,
pub env_change: Option<Value>,
pub display_output: Option<Value>,
pub command_not_found: Option<Value>,
}
impl Hooks {
pub fn new() -> Self {
Self {
pre_prompt: None,
pre_execution: None,
env_change: None,
display_output: Some(Value::string(
"if (term size).columns >= 100 { table -e } else { table }",
Span::unknown(),
)),
command_not_found: None,
}
}
}
impl Default for Hooks {
fn default() -> Self {
Self::new()
}
}
/// 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(
"'pre_prompt', 'pre_execution', 'env_change', 'display_output', 'command_not_found'"
.to_string(),
x.to_string(),
span,
));
}
}
}
Ok(hooks)
}
_ => Err(ShellError::UnsupportedConfigValue(
"record for 'hooks' config".into(),
"non-record value".into(),
span,
)),
}
}
pub(super) fn reconstruct_hooks(config: &Config, span: Span) -> Value {
let mut hook = Record::new();
if let Some(ref value) = config.hooks.pre_prompt {
hook.push("pre_prompt", value.clone());
}
if let Some(ref value) = config.hooks.pre_execution {
hook.push("pre_execution", value.clone());
}
if let Some(ref value) = config.hooks.env_change {
hook.push("env_change", value.clone());
}
if let Some(ref value) = config.hooks.display_output {
hook.push("display_output", value.clone());
}
if let Some(ref value) = config.hooks.command_not_found {
hook.push("command_not_found", value.clone());
}
Value::record(hook, span)
}

View File

@ -0,0 +1,730 @@
use self::completer::*;
use self::helper::*;
use self::hooks::*;
use self::output::*;
use self::reedline::*;
use self::table::*;
use crate::{record, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub use self::completer::CompletionAlgorithm;
pub use self::helper::extract_value;
pub use self::hooks::Hooks;
pub use self::output::ErrorStyle;
pub use self::reedline::{
create_menus, EditBindings, HistoryFileFormat, NuCursorShape, ParsedKeybinding, ParsedMenu,
};
pub use self::table::{FooterMode, TableIndexMode, TableMode, TrimStrategy};
mod completer;
mod helper;
mod hooks;
mod output;
mod reedline;
mod table;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Config {
pub external_completer: Option<usize>,
pub filesize_metric: bool,
pub table_mode: TableMode,
pub table_move_header: bool,
pub table_show_empty: bool,
pub table_indent: TableIndent,
pub table_abbreviation_threshold: Option<usize>,
pub use_ls_colors: bool,
pub color_config: HashMap<String, Value>,
pub use_grid_icons: bool,
pub footer_mode: FooterMode,
pub float_precision: i64,
pub max_external_completion_results: i64,
pub filesize_format: String,
pub use_ansi_coloring: bool,
pub quick_completions: bool,
pub partial_completions: bool,
pub completion_algorithm: CompletionAlgorithm,
pub edit_mode: EditBindings,
pub max_history_size: i64,
pub sync_history_on_enter: bool,
pub history_file_format: HistoryFileFormat,
pub history_isolation: bool,
pub keybindings: Vec<ParsedKeybinding>,
pub menus: Vec<ParsedMenu>,
pub hooks: Hooks,
pub rm_always_trash: bool,
pub shell_integration: bool,
pub buffer_editor: Value,
pub table_index_mode: TableIndexMode,
pub case_sensitive_completions: bool,
pub enable_external_completion: bool,
pub trim_strategy: TrimStrategy,
pub show_banner: bool,
pub bracketed_paste: bool,
pub show_clickable_links_in_ls: bool,
pub render_right_prompt_on_last_line: bool,
pub explore: HashMap<String, Value>,
pub cursor_shape_vi_insert: NuCursorShape,
pub cursor_shape_vi_normal: NuCursorShape,
pub cursor_shape_emacs: NuCursorShape,
pub datetime_normal_format: Option<String>,
pub datetime_table_format: Option<String>,
pub error_style: ErrorStyle,
pub use_kitty_protocol: bool,
}
impl Default for Config {
fn default() -> Config {
Config {
show_banner: true,
use_ls_colors: true,
show_clickable_links_in_ls: true,
rm_always_trash: false,
table_mode: TableMode::Rounded,
table_index_mode: TableIndexMode::Always,
table_show_empty: true,
trim_strategy: TrimStrategy::default(),
table_move_header: false,
table_indent: TableIndent { left: 1, right: 1 },
table_abbreviation_threshold: None,
datetime_normal_format: None,
datetime_table_format: None,
explore: HashMap::new(),
max_history_size: 100_000,
sync_history_on_enter: true,
history_file_format: HistoryFileFormat::PlainText,
history_isolation: false,
case_sensitive_completions: false,
quick_completions: true,
partial_completions: true,
completion_algorithm: CompletionAlgorithm::default(),
enable_external_completion: true,
max_external_completion_results: 100,
external_completer: None,
filesize_metric: false,
filesize_format: "auto".into(),
cursor_shape_emacs: NuCursorShape::default(),
cursor_shape_vi_insert: NuCursorShape::default(),
cursor_shape_vi_normal: NuCursorShape::default(),
color_config: HashMap::new(),
use_grid_icons: true,
footer_mode: FooterMode::RowCount(25),
float_precision: 2,
buffer_editor: Value::nothing(Span::unknown()),
use_ansi_coloring: true,
bracketed_paste: true,
edit_mode: EditBindings::default(),
shell_integration: false,
render_right_prompt_on_last_line: false,
hooks: Hooks::new(),
menus: Vec::new(),
keybindings: Vec::new(),
error_style: ErrorStyle::Fancy,
use_kitty_protocol: false,
}
}
}
impl Value {
pub fn into_config(&mut self, config: &Config) -> (Config, Option<ShellError>) {
// Clone the passed-in config rather than mutating it.
let mut config = 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`.
if let Value::Record { val, .. } = self {
val.retain_mut( |key, value| {
let span = value.span();
match key {
// Grouped options
"ls" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| {
let span = value.span();
match key2 {
"use_ls_colors" => {
process_bool_config(value, &mut errors, &mut config.use_ls_colors);
}
"clickable_links" => {
process_bool_config(value, &mut errors, &mut config.show_clickable_links_in_ls);
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
}; true
});
} else {
report_invalid_value("should be a record", span, &mut errors);
// Reconstruct
*value = Value::record(
record! {
"use_ls_colors" => Value::bool(config.use_ls_colors, span),
"clickable_links" => Value::bool(config.show_clickable_links_in_ls, span),
},
span,
);
}
}
"rm" => {
if let Value::Record { val, .. } = value {
val.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);
// Reconstruct
*value = Value::record(
record! {
"always_trash" => Value::bool(config.rm_always_trash, span),
},
span,
);
}
}
"history" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| {
let span = value.span();
match key2 {
"isolation" => {
process_bool_config(value, &mut errors, &mut config.history_isolation);
}
"sync_on_enter" => {
process_bool_config(value, &mut errors, &mut config.sync_history_on_enter);
}
"max_size" => {
process_int_config(value, &mut errors, &mut config.max_history_size);
}
"file_format" => {
process_string_enum(
&mut config.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);
// Reconstruct
*value = Value::record(
record! {
"sync_on_enter" => Value::bool(config.sync_history_on_enter, span),
"max_size" => Value::int(config.max_history_size, span),
"file_format" => config.history_file_format.reconstruct_value(span),
"isolation" => Value::bool(config.history_isolation, span),
},
span,
);
}
}
"completions" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value| {
let span = value.span();
match key2 {
"quick" => {
process_bool_config(value, &mut errors, &mut config.quick_completions);
}
"partial" => {
process_bool_config(value, &mut errors, &mut config.partial_completions);
}
"algorithm" => {
process_string_enum(
&mut config.completion_algorithm,
&[key, key2],
value,
&mut errors);
}
"case_sensitive" => {
process_bool_config(value, &mut errors, &mut config.case_sensitive_completions);
}
"external" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key3, value|
{
let span = value.span();
match key3 {
"max_results" => {
process_int_config(value, &mut errors, &mut config.max_external_completion_results);
}
"completer" => {
if let Ok(v) = value.as_block() {
config.external_completer = Some(v)
} else {
match value {
Value::Nothing { .. } => {}
_ => {
report_invalid_value("should be a block or null", span, &mut errors);
// Reconstruct
*value = reconstruct_external_completer(&config,
span
);
}
}
}
}
"enable" => {
process_bool_config(value, &mut errors, &mut config.enable_external_completion);
}
_ => {
report_invalid_key(&[key, key2, key3], span, &mut errors);
return false;
}
};
true
});
} else {
report_invalid_value("should be a record", span, &mut errors);
// Reconstruct
*value = reconstruct_external(&config, span);
}
}
_ => {
report_invalid_key(&[key, key2], span, &mut errors);
return false;
}
};
true
});
} else {
report_invalid_value("should be a record", span, &mut errors);
// Reconstruct record
*value = Value::record(
record! {
"quick" => Value::bool(config.quick_completions, span),
"partial" => Value::bool(config.partial_completions, span),
"algorithm" => config.completion_algorithm.reconstruct_value(span),
"case_sensitive" => Value::bool(config.case_sensitive_completions, span),
"external" => reconstruct_external(&config, span),
},
span,
);
}
}
"cursor_shape" => {
if let Value::Record { val, .. } = value {
val.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);
// Reconstruct
*value = Value::record(
record! {
"vi_insert" => config.cursor_shape_vi_insert.reconstruct_value(span),
"vi_normal" => config.cursor_shape_vi_normal.reconstruct_value(span),
"emacs" => config.cursor_shape_emacs.reconstruct_value(span),
},
span,
);
}
}
"table" => {
if let Value::Record { val, .. } = value {
val.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_move_header);
}
"padding" => match value {
Value::Int { val, .. } => {
if *val < 0 {
report_invalid_value("expected a unsigned integer", span, &mut errors);
*value = reconstruct_padding(&config, span);
} else {
config.table_indent.left = *val as usize;
config.table_indent.right = *val as usize;
}
}
Value::Record { val, .. } => {
let mut invalid = false;
val.retain(|key3, value| {
match key3 {
"left" => {
match value.as_int() {
Ok(val) if val >= 0 => {
config.table_indent.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_indent.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 = reconstruct_padding(&config, span);
}
}
_ => {
report_invalid_value("expected a unsigned integer or a record", span, &mut errors);
*value = reconstruct_padding(&config, 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.trim_strategy = v,
Err(e) => {
// try_parse_trim_strategy() already adds its own errors
errors.push(e);
*value =
reconstruct_trim_strategy(&config, span);
}
}
}
"show_empty" => {
process_bool_config(value, &mut errors, &mut config.table_show_empty);
}
"abbreviated_row_count" => {
if let Ok(b) = value.as_int() {
if b < 0 {
report_invalid_value("should be an int unsigned", span, &mut errors);
}
config.table_abbreviation_threshold = Some(b as usize);
} else {
report_invalid_value("should be an int", 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);
// Reconstruct
*value = Value::record(
record! {
"mode" => config.table_mode.reconstruct_value(span),
"index_mode" => config.table_index_mode.reconstruct_value(span),
"trim" => reconstruct_trim_strategy(&config, span),
"show_empty" => Value::bool(config.table_show_empty, span),
},
span,
);
}
}
"filesize" => {
if let Value::Record { val, .. } = value {
val.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.as_string() {
config.filesize_format = v.to_lowercase();
} else {
report_invalid_value("should be a string", span, &mut errors);
// Reconstruct
*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);
// Reconstruct
*value = Value::record(
record! {
"metric" => Value::bool(config.filesize_metric, span),
"format" => Value::string(config.filesize_format.clone(), span),
},
span,
);
}
}
"explore" => {
if let Ok(map) = create_map(value) {
config.explore = map;
} else {
report_invalid_value("should be a record", span, &mut errors);
// Reconstruct
*value = Value::record(
config
.explore
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
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);
// Reconstruct
*value = Value::record(
config
.color_config
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
span,
);
}
}
"use_grid_icons" => {
process_bool_config(value, &mut errors, &mut config.use_grid_icons);
}
"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" => {
process_bool_config(value, &mut errors, &mut config.shell_integration);
}
"buffer_editor" => match value {
Value::Nothing { .. } | Value::String { .. } => {
config.buffer_editor = value.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();
}
},
"show_banner" => {
process_bool_config(value, &mut errors, &mut config.show_banner);
}
"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);
}
// Menus
"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);
// Reconstruct
*value = reconstruct_menus(&config, span);
}
},
// Keybindings
"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);
// Reconstruct
*value = reconstruct_keybindings(&config, span);
}
},
// Hooks
"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 = reconstruct_hooks(&config, span);
}
},
"datetime_format" => {
if let Value::Record { val, .. } = value {
val.retain_mut(|key2, value|
{
let span = value.span();
match key2 {
"normal" => {
if let Ok(v) = value.as_string() {
config.datetime_normal_format = Some(v);
} else {
report_invalid_value("should be a string", span, &mut errors);
}
}
"table" => {
if let Ok(v) = value.as_string() {
config.datetime_table_format = 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);
// Reconstruct
*value = reconstruct_datetime_format(&config, span);
}
}
"error_style" => {
process_string_enum(
&mut config.error_style,
&[key],
value,
&mut errors);
}
// Catch all
_ => {
report_invalid_key(&[key], span, &mut errors);
return false;
}
};
true
});
} else {
return (
config,
Some(ShellError::GenericError(
"Error while applying config changes".into(),
"$env.config is not a record".into(),
Some(self.span()),
None,
vec![],
)),
);
}
// Return the config and the vec of errors.
(
config,
if !errors.is_empty() {
Some(ShellError::GenericError(
"Config record contains invalid values or unknown settings".into(),
// Without a span, this second string is ignored.
"".into(),
None,
None,
errors,
))
} else {
None
},
)
}
}

View File

@ -0,0 +1,45 @@
use super::helper::ReconstructVal;
use crate::{Config, Record, Span, Value};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
pub enum ErrorStyle {
Plain,
Fancy,
}
impl FromStr for ErrorStyle {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"fancy" => Ok(Self::Fancy),
"plain" => Ok(Self::Plain),
_ => Err("expected either 'fancy' or 'plain'"),
}
}
}
impl ReconstructVal for ErrorStyle {
fn reconstruct_value(&self, span: Span) -> Value {
Value::string(
match self {
ErrorStyle::Fancy => "fancy",
ErrorStyle::Plain => "plain",
},
span,
)
}
}
pub(super) fn reconstruct_datetime_format(config: &Config, span: Span) -> Value {
let mut record = Record::new();
if let Some(normal) = &config.datetime_normal_format {
record.push("normal", Value::string(normal, span));
}
if let Some(table) = &config.datetime_table_format {
record.push("table", Value::string(table, span));
}
Value::record(record, span)
}

View File

@ -0,0 +1,277 @@
use std::str::FromStr;
use super::{extract_value, helper::ReconstructVal};
use crate::{record, Config, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
/// Definition of a parsed keybinding from the config object
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ParsedKeybinding {
pub modifier: Value,
pub keycode: Value,
pub event: Value,
pub mode: Value,
}
/// Definition of a parsed menu from the config object
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ParsedMenu {
pub name: Value,
pub marker: Value,
pub only_buffer_difference: Value,
pub style: Value,
pub menu_type: Value,
pub source: Value,
}
/// Definition of a Nushell CursorShape (to be mapped to crossterm::cursor::CursorShape)
#[derive(Serialize, Deserialize, Clone, Debug, Copy, Default)]
pub enum NuCursorShape {
UnderScore,
Line,
Block,
BlinkUnderScore,
BlinkLine,
BlinkBlock,
#[default]
Inherit,
}
impl FromStr for NuCursorShape {
type Err = &'static str;
fn from_str(s: &str) -> Result<NuCursorShape, &'static str> {
match s.to_ascii_lowercase().as_str() {
"line" => Ok(NuCursorShape::Line),
"block" => Ok(NuCursorShape::Block),
"underscore" => Ok(NuCursorShape::UnderScore),
"blink_line" => Ok(NuCursorShape::BlinkLine),
"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'"),
}
}
}
impl ReconstructVal for NuCursorShape {
fn reconstruct_value(&self, span: Span) -> Value {
Value::string(
match self {
NuCursorShape::Line => "line",
NuCursorShape::Block => "block",
NuCursorShape::UnderScore => "underscore",
NuCursorShape::BlinkLine => "blink_line",
NuCursorShape::BlinkBlock => "blink_block",
NuCursorShape::BlinkUnderScore => "blink_underscore",
NuCursorShape::Inherit => "inherit",
},
span,
)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
pub enum HistoryFileFormat {
/// Store history as an SQLite database with additional context
Sqlite,
/// store history as a plain text file where every line is one command (without any context such as timestamps)
PlainText,
}
impl FromStr for HistoryFileFormat {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"sqlite" => Ok(Self::Sqlite),
"plaintext" => Ok(Self::PlainText),
_ => Err("expected either 'sqlite' or 'plaintext'"),
}
}
}
impl ReconstructVal for HistoryFileFormat {
fn reconstruct_value(&self, span: Span) -> Value {
Value::string(
match self {
HistoryFileFormat::Sqlite => "sqlite",
HistoryFileFormat::PlainText => "plaintext",
},
span,
)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Default, Copy)]
pub enum EditBindings {
Vi,
#[default]
Emacs,
}
impl FromStr for EditBindings {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"vi" => Ok(Self::Vi),
"emacs" => Ok(Self::Emacs),
_ => Err("expected either 'emacs' or 'vi'"),
}
}
}
impl ReconstructVal for EditBindings {
fn reconstruct_value(&self, span: Span) -> Value {
Value::string(
match self {
EditBindings::Vi => "vi",
EditBindings::Emacs => "emacs",
},
span,
)
}
}
/// 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()),
}
}
pub(super) fn reconstruct_keybindings(config: &Config, span: Span) -> Value {
Value::list(
config
.keybindings
.iter()
.map(
|ParsedKeybinding {
modifier,
keycode,
mode,
event,
}| {
Value::record(
record! {
"modifier" => modifier.clone(),
"keycode" => keycode.clone(),
"mode" => mode.clone(),
"event" => event.clone(),
},
span,
)
},
)
.collect(),
span,
)
}
/// 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 menu_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,
menu_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()),
}
}
pub(super) fn reconstruct_menus(config: &Config, span: Span) -> Value {
Value::list(
config
.menus
.iter()
.map(
|ParsedMenu {
name,
only_buffer_difference,
marker,
style,
menu_type, // WARNING: this is not the same name as what is used in Config.nu! ("type")
source,
}| {
Value::record(
record! {
"name" => name.clone(),
"only_buffer_difference" => only_buffer_difference.clone(),
"marker" => marker.clone(),
"style" => style.clone(),
"type" => menu_type.clone(),
"source" => source.clone(),
},
span,
)
},
)
.collect(),
span,
)
}

View File

@ -0,0 +1,330 @@
use super::helper::ReconstructVal;
use crate::{record, Config, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub enum TableMode {
Basic,
Thin,
Light,
Compact,
WithLove,
CompactDouble,
#[default]
Rounded,
Reinforced,
Heavy,
None,
Psql,
Markdown,
Dots,
Restructured,
AsciiRounded,
BasicCompact,
}
impl FromStr for TableMode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"basic" => Ok(Self::Basic),
"thin" => Ok(Self::Thin),
"light" => Ok(Self::Light),
"compact" => Ok(Self::Compact),
"with_love" => Ok(Self::WithLove),
"compact_double" => Ok(Self::CompactDouble),
"rounded" => Ok(Self::Rounded),
"reinforced" => Ok(Self::Reinforced),
"heavy" => Ok(Self::Heavy),
"none" => Ok(Self::None),
"psql" => Ok(Self::Psql),
"markdown" => Ok(Self::Markdown),
"dots" => Ok(Self::Dots),
"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'"),
}
}
}
impl ReconstructVal for TableMode {
fn reconstruct_value(&self, span: Span) -> Value {
Value::string(
match self {
TableMode::Basic => "basic",
TableMode::Thin => "thin",
TableMode::Light => "light",
TableMode::Compact => "compact",
TableMode::WithLove => "with_love",
TableMode::CompactDouble => "compact_double",
TableMode::Rounded => "rounded",
TableMode::Reinforced => "reinforced",
TableMode::Heavy => "heavy",
TableMode::None => "none",
TableMode::Psql => "psql",
TableMode::Markdown => "markdown",
TableMode::Dots => "dots",
TableMode::Restructured => "restructured",
TableMode::AsciiRounded => "ascii_rounded",
TableMode::BasicCompact => "basic_compact",
},
span,
)
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum FooterMode {
/// Never show the footer
Never,
/// Always show the footer
Always,
/// Only show the footer if there are more than RowCount rows
RowCount(u64),
/// Calculate the screen height, calculate row count, if display will be bigger than screen, add the footer
Auto,
}
impl FromStr for FooterMode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"always" => Ok(FooterMode::Always),
"never" => Ok(FooterMode::Never),
"auto" => Ok(FooterMode::Auto),
x => {
if let Ok(count) = x.parse() {
Ok(FooterMode::RowCount(count))
} else {
Err("expected either 'never', 'always', 'auto' or a row count")
}
}
}
}
}
impl ReconstructVal for FooterMode {
fn reconstruct_value(&self, span: Span) -> Value {
Value::string(
match self {
FooterMode::Always => "always".to_string(),
FooterMode::Never => "never".to_string(),
FooterMode::Auto => "auto".to_string(),
FooterMode::RowCount(c) => c.to_string(),
},
span,
)
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum TableIndexMode {
/// Always show indexes
Always,
/// Never show indexes
Never,
/// Show indexes when a table has "index" column
Auto,
}
impl FromStr for TableIndexMode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"always" => Ok(TableIndexMode::Always),
"never" => Ok(TableIndexMode::Never),
"auto" => Ok(TableIndexMode::Auto),
_ => Err("expected either 'never', 'always' or 'auto'"),
}
}
}
impl ReconstructVal for TableIndexMode {
fn reconstruct_value(&self, span: Span) -> Value {
Value::string(
match self {
TableIndexMode::Always => "always",
TableIndexMode::Never => "never",
TableIndexMode::Auto => "auto",
},
span,
)
}
}
/// A Table view configuration, for a situation where
/// we need to limit cell width in order to adjust for a terminal size.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum TrimStrategy {
/// Wrapping strategy.
///
/// It it's similar to original nu_table, strategy.
Wrap {
/// A flag which indicates whether is it necessary to try
/// to keep word boundaries.
try_to_keep_words: bool,
},
/// Truncating strategy, where we just cut the string.
/// And append the suffix if applicable.
Truncate {
/// Suffix which can be appended to a truncated string after being cut.
///
/// It will be applied only when there's enough room for it.
/// For example in case where a cell width must be 12 chars, but
/// the suffix takes 13 chars it won't be used.
suffix: Option<String>,
},
}
impl TrimStrategy {
pub fn wrap(dont_split_words: bool) -> Self {
Self::Wrap {
try_to_keep_words: dont_split_words,
}
}
pub fn truncate(suffix: Option<String>) -> Self {
Self::Truncate { suffix }
}
}
impl Default for TrimStrategy {
fn default() -> Self {
TrimStrategy::Wrap {
try_to_keep_words: true,
}
}
}
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 while applying config changes".into(),
"$env.config.table.trim is not a record".into(),
Some(value.span()),
Some("Please consult the documentation for configuring Nushell.".into()),
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 while applying config changes".into(),
"$env.config.table.trim.methodology was not provided".into(),
Some(value.span()),
Some("Please consult the documentation for configuring Nushell.".into()),
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 while applying config changes".into(),
"$env.config.table.trim.wrapping_try_keep_words is not a bool".into(),
Some(value.span()),
Some("Please consult the documentation for configuring Nushell.".into()),
vec![],
));
}
}
}
TrimStrategy::Truncate { suffix } => {
if let Some(value) = map.get("truncating_suffix") {
if let Ok(v) = value.as_string() {
*suffix = Some(v);
} else {
errors.push(ShellError::GenericError(
"Error while applying config changes".into(),
"$env.config.table.trim.truncating_suffix is not a string".into(),
Some(value.span()),
Some("Please consult the documentation for configuring Nushell.".into()),
vec![],
));
}
}
}
}
Ok(methodology)
}
fn try_parse_trim_methodology(value: &Value) -> Option<TrimStrategy> {
if let Ok(value) = value.as_string() {
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
}
pub(super) fn reconstruct_trim_strategy(config: &Config, span: Span) -> Value {
match &config.trim_strategy {
TrimStrategy::Wrap { try_to_keep_words } => Value::record(
record! {
"methodology" => Value::string("wrapping", span),
"wrapping_try_keep_words" => Value::bool(*try_to_keep_words, span),
},
span,
),
TrimStrategy::Truncate { suffix } => Value::record(
match suffix {
Some(s) => record! {
"methodology" => Value::string("truncating", span),
"truncating_suffix" => Value::string(s.clone(), span),
},
None => record! {
"methodology" => Value::string("truncating", span),
"truncating_suffix" => Value::nothing(span),
},
},
span,
),
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TableIndent {
pub left: usize,
pub right: usize,
}
pub(super) fn reconstruct_padding(config: &Config, span: Span) -> Value {
// For better completions always reconstruct the record version even though unsigned int would
// be supported, `as` conversion is sane as it came from an i64 original
Value::record(
record!(
"left" => Value::int(config.table_indent.left as i64, span),
"right" => Value::int(config.table_indent.right as i64, span),
),
span,
)
}

View File

@ -705,8 +705,8 @@ impl EngineState {
&self.config
}
pub fn set_config(&mut self, conf: &Config) {
self.config = conf.clone();
pub fn set_config(&mut self, conf: Config) {
self.config = conf;
}
pub fn get_var(&self, var_id: VarId) -> &Variable {

View File

@ -70,9 +70,12 @@ fn config_add_unsupported_value() {
r#"$env.config.history.file_format = ''"#,
r#";"#]));
assert!(actual.err.contains(
"unrecognized $env.config.history.file_format ''; expected either 'sqlite' or 'plaintext'"
));
assert!(actual
.err
.contains("unrecognized $env.config.history.file_format option ''"));
assert!(actual
.err
.contains("expected either 'sqlite' or 'plaintext'"));
}
#[test]

View File

@ -1,6 +1,6 @@
use nu_color_config::{Alignment, StyleComputer, TextStyle};
use nu_protocol::TrimStrategy;
use nu_protocol::{Config, FooterMode, ShellError, Span, Value};
use nu_protocol::{TableMode, TrimStrategy};
use crate::{
clean_charset, colorize_space_str, string_wrap, NuTableConfig, TableOutput, TableTheme,
@ -174,24 +174,23 @@ fn is_cfg_trim_keep_words(config: &Config) -> bool {
}
pub fn load_theme_from_config(config: &Config) -> TableTheme {
match config.table_mode.as_str() {
"basic" => TableTheme::basic(),
"thin" => TableTheme::thin(),
"light" => TableTheme::light(),
"compact" => TableTheme::compact(),
"with_love" => TableTheme::with_love(),
"compact_double" => TableTheme::compact_double(),
"rounded" => TableTheme::rounded(),
"reinforced" => TableTheme::reinforced(),
"heavy" => TableTheme::heavy(),
"none" => TableTheme::none(),
"psql" => TableTheme::psql(),
"markdown" => TableTheme::markdown(),
"dots" => TableTheme::dots(),
"restructured" => TableTheme::restructured(),
"ascii_rounded" => TableTheme::ascii_rounded(),
"basic_compact" => TableTheme::basic_compact(),
_ => TableTheme::rounded(),
match config.table_mode {
TableMode::Basic => TableTheme::basic(),
TableMode::Thin => TableTheme::thin(),
TableMode::Light => TableTheme::light(),
TableMode::Compact => TableTheme::compact(),
TableMode::WithLove => TableTheme::with_love(),
TableMode::CompactDouble => TableTheme::compact_double(),
TableMode::Rounded => TableTheme::rounded(),
TableMode::Reinforced => TableTheme::reinforced(),
TableMode::Heavy => TableTheme::heavy(),
TableMode::None => TableTheme::none(),
TableMode::Psql => TableTheme::psql(),
TableMode::Markdown => TableTheme::markdown(),
TableMode::Dots => TableTheme::dots(),
TableMode::Restructured => TableTheme::restructured(),
TableMode::AsciiRounded => TableTheme::ascii_rounded(),
TableMode::BasicCompact => TableTheme::basic_compact(),
}
}