diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index 0ef28cb45..376075131 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -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 { 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); diff --git a/crates/nu-cli/src/completions/completion_options.rs b/crates/nu-cli/src/completions/completion_options.rs index e5e506c91..e686da27a 100644 --- a/crates/nu-cli/src/completions/completion_options.rs +++ b/crates/nu-cli/src/completions/completion_options.rs @@ -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 for MatchAlgorithm { + fn from(value: CompletionAlgorithm) -> Self { + match value { + CompletionAlgorithm::Prefix => MatchAlgorithm::Prefix, + CompletionAlgorithm::Fuzzy => MatchAlgorithm::Fuzzy, + } + } +} + impl TryFrom for MatchAlgorithm { type Error = InvalidMatchAlgorithm; diff --git a/crates/nu-cli/src/eval_cmds.rs b/crates/nu-cli/src/eval_cmds.rs index 6eae2163c..b5f85a994 100644 --- a/crates/nu-cli/src/eval_cmds.rs +++ b/crates/nu-cli/src/eval_cmds.rs @@ -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) } diff --git a/crates/nu-cli/src/eval_file.rs b/crates/nu-cli/src/eval_file.rs index b4e6aa7ae..94c8080a3 100644 --- a/crates/nu-cli/src/eval_file.rs +++ b/crates/nu-cli/src/eval_file.rs @@ -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); diff --git a/crates/nu-cli/src/reedline_config.rs b/crates/nu-cli/src/reedline_config.rs index e741f3e49..09e924862 100644 --- a/crates/nu-cli/src/reedline_config.rs +++ b/crates/nu-cli/src/reedline_config.rs @@ -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 { + 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 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, }), diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index c05ad95ad..90138a5db 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -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 { 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, } } diff --git a/crates/nu-cli/tests/completions.rs b/crates/nu-cli/tests/completions.rs index 714709937..da9be1812 100644 --- a/crates/nu-cli/tests/completions.rs +++ b/crates/nu-cli/tests/completions.rs @@ -795,7 +795,7 @@ fn run_external_completion(block: &str, input: &str) -> Vec { // 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); diff --git a/crates/nu-protocol/src/cli_error.rs b/crates/nu-protocol/src/cli_error.rs index a02bfd7bc..60712ac6b 100644 --- a/crates/nu-protocol/src/cli_error.rs +++ b/crates/nu-protocol/src/cli_error.rs @@ -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 = match errors_style { - "plain" => Box::new(NarratableReportHandler::new()), - _ => Box::new( + let miette_handler: Box = 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) diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs deleted file mode 100644 index 54438171f..000000000 --- a/crates/nu-protocol/src/config.rs +++ /dev/null @@ -1,1619 +0,0 @@ -use crate::{record, Record, ShellError, Span, Value}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -const TRIM_STRATEGY_DEFAULT: TrimStrategy = TrimStrategy::Wrap { - try_to_keep_words: true, -}; - -/// 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 parsed hook from the config object -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Hooks { - pub pre_prompt: Option, - pub pre_execution: Option, - pub env_change: Option, - pub display_output: Option, - pub command_not_found: Option, -} - -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() - } -} - -/// Definition of a Nushell CursorShape (to be mapped to crossterm::cursor::CursorShape) -#[derive(Serialize, Deserialize, Clone, Debug, Copy)] -pub enum NuCursorShape { - UnderScore, - Line, - Block, - BlinkUnderScore, - BlinkLine, - BlinkBlock, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Config { - pub external_completer: Option, - pub filesize_metric: bool, - pub table_mode: String, - pub table_move_header: bool, - pub table_show_empty: bool, - pub table_indent: TableIndent, - pub table_abbreviation_threshold: Option, - pub use_ls_colors: bool, - pub color_config: HashMap, - 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: String, - pub edit_mode: String, - pub max_history_size: i64, - pub sync_history_on_enter: bool, - pub history_file_format: HistoryFileFormat, - pub history_isolation: bool, - pub keybindings: Vec, - pub menus: Vec, - 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, - pub cursor_shape_vi_insert: Option, - pub cursor_shape_vi_normal: Option, - pub cursor_shape_emacs: Option, - pub datetime_normal_format: Option, - pub datetime_table_format: Option, - pub error_style: String, - 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: "rounded".into(), - table_index_mode: TableIndexMode::Always, - table_show_empty: true, - trim_strategy: TRIM_STRATEGY_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: "prefix".into(), - enable_external_completion: true, - max_external_completion_results: 100, - external_completer: None, - - filesize_metric: false, - filesize_format: "auto".into(), - - cursor_shape_emacs: None, - cursor_shape_vi_insert: None, - cursor_shape_vi_normal: None, - - 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: "emacs".into(), - shell_integration: false, - render_right_prompt_on_last_line: false, - - hooks: Hooks::new(), - - menus: Vec::new(), - - keybindings: Vec::new(), - - error_style: "fancy".into(), - - use_kitty_protocol: false, - } - } -} - -#[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, -} - -#[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, -} - -#[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, -} - -/// 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, - }, -} - -impl TrimStrategy { - pub fn wrap(dont_split_words: bool) -> Self { - Self::Wrap { - try_to_keep_words: dont_split_words, - } - } - - pub fn truncate(suffix: Option) -> Self { - Self::Truncate { suffix } - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct TableIndent { - pub left: usize, - pub right: usize, -} - -impl Value { - pub fn into_config(&mut self, config: &Config) -> (Config, Option) { - // 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 - // set-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![]; - - // When an unsupported config value is found, ignore it. - macro_rules! invalid { - ($span:expr, $msg:literal) => { - errors.push(ShellError::GenericError( - "Error while applying config changes".into(), - format!($msg), - $span, - Some("This value will be ignored.".into()), - vec![], - )); - }; - } - // Some extra helpers - macro_rules! try_bool { - ($cols:ident, $vals:ident, $index:ident, $span:expr, $setting:ident) => { - if let Ok(b) = &$vals[$index].as_bool() { - config.$setting = *b; - } else { - invalid!(Some($span), "should be a bool"); - // Reconstruct - $vals[$index] = Value::bool(config.$setting, $span); - } - }; - } - macro_rules! try_int { - ($cols:ident, $vals:ident, $index:ident, $span:expr, $setting:ident) => { - if let Ok(b) = &$vals[$index].as_int() { - config.$setting = *b; - } else { - invalid!(Some($span), "should be an int"); - // Reconstruct - $vals[$index] = Value::int(config.$setting, $span); - } - }; - } - // When an unsupported config value is found, remove it from this record. - macro_rules! invalid_key { - // 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. - ($cols:ident, $vals:ident, $index:ident, $span:expr, $msg:literal) => { - errors.push(ShellError::GenericError( - "Error while applying config changes".into(), - format!($msg), - $span, - Some("This value will not appear in your $env.config record.".into()), - vec![], - )); - $cols.remove($index); - $vals.remove($index); - }; - } - - // 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 { - let Record { cols, vals } = val; - // Because this whole algorithm removes while iterating, this must iterate in reverse. - for index in (0..cols.len()).rev() { - let value = &vals[index]; - let key = cols[index].as_str(); - let span = vals[index].span(); - match key { - // Grouped options - "ls" => { - if let Value::Record { val, .. } = &mut vals[index] { - let Record { cols, vals } = val; - for index in (0..cols.len()).rev() { - let value = &vals[index]; - let key2 = cols[index].as_str(); - match key2 { - "use_ls_colors" => { - try_bool!(cols, vals, index, span, use_ls_colors) - } - "clickable_links" => try_bool!( - cols, - vals, - index, - span, - show_clickable_links_in_ls - ), - x => { - invalid_key!( - cols, - vals, - index, - Some(value.span()), - "$env.config.{key}.{x} is an unknown config setting" - ); - } - } - } - } else { - invalid!(Some(vals[index].span()), "should be a record"); - // Reconstruct - vals[index] = 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, - ); - } - } - "cd" => { - if let Value::Record { val, .. } = &mut vals[index] { - let Record { cols, vals } = val; - for index in (0..cols.len()).rev() { - let value = &vals[index]; - let key2 = cols[index].as_str(); - { - invalid_key!( - cols, - vals, - index, - Some(value.span()), - "$env.config.{key}.{key2} is an unknown config setting" - ); - } - } - } else { - invalid!(Some(vals[index].span()), "should be a record"); - // Reconstruct - vals[index] = 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, .. } = &mut vals[index] { - let Record { cols, vals } = val; - for index in (0..cols.len()).rev() { - let value = &vals[index]; - let key2 = cols[index].as_str(); - match key2 { - "always_trash" => { - try_bool!(cols, vals, index, span, rm_always_trash) - } - x => { - invalid_key!( - cols, - vals, - index, - Some(value.span()), - "$env.config.{key}.{x} is an unknown config setting" - ); - } - } - } - } else { - invalid!(Some(vals[index].span()), "should be a record"); - // Reconstruct - vals[index] = Value::record( - record! { - "always_trash" => Value::bool(config.rm_always_trash, span), - }, - span, - ); - } - } - "history" => { - fn reconstruct_history_file_format(config: &Config, span: Span) -> Value { - Value::string( - match config.history_file_format { - HistoryFileFormat::Sqlite => "sqlite", - HistoryFileFormat::PlainText => "plaintext", - }, - span, - ) - } - - if let Value::Record { val, .. } = &mut vals[index] { - let Record { cols, vals } = val; - for index in (0..cols.len()).rev() { - let value = &vals[index]; - let key2 = cols[index].as_str(); - match key2 { - "isolation" => { - try_bool!(cols, vals, index, span, history_isolation) - } - "sync_on_enter" => { - try_bool!(cols, vals, index, span, sync_history_on_enter) - } - "max_size" => { - try_int!(cols, vals, index, span, max_history_size) - } - "file_format" => { - if let Ok(v) = value.as_string() { - let val_str = v.to_lowercase(); - match val_str.as_ref() { - "sqlite" => { - config.history_file_format = - HistoryFileFormat::Sqlite - } - "plaintext" => { - config.history_file_format = - HistoryFileFormat::PlainText - } - _ => { - invalid!(Some(span), - "unrecognized $env.config.{key}.{key2} '{val_str}'; expected either 'sqlite' or 'plaintext'" - ); - // Reconstruct - vals[index] = reconstruct_history_file_format( - &config, span, - ); - } - }; - } else { - invalid!(Some(span), "should be a string"); - // Reconstruct - vals[index] = - reconstruct_history_file_format(&config, span); - } - } - x => { - invalid_key!( - cols, - vals, - index, - Some(value.span()), - "$env.config.{key}.{x} is an unknown config setting" - ); - } - } - } - } else { - invalid!(Some(vals[index].span()), "should be a record"); - // Reconstruct - vals[index] = 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" => reconstruct_history_file_format(&config, span), - "isolation" => Value::bool(config.history_isolation, span), - }, - span, - ); - } - } - "completions" => { - fn reconstruct_external_completer(config: &Config, span: Span) -> Value { - if let Some(block) = config.external_completer { - Value::block(block, span) - } else { - Value::nothing(span) - } - } - - 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, - ) - } - - if let Value::Record { val, .. } = &mut vals[index] { - let Record { cols, vals } = val; - for index in (0..cols.len()).rev() { - let value = &vals[index]; - let key2 = cols[index].as_str(); - match key2 { - "quick" => { - try_bool!(cols, vals, index, span, quick_completions) - } - "partial" => { - try_bool!(cols, vals, index, span, partial_completions) - } - "algorithm" => { - if let Ok(v) = value.as_string() { - let val_str = v.to_lowercase(); - match val_str.as_ref() { - // This should match the MatchAlgorithm enum in completions::completion_options - "prefix" | "fuzzy" => { - config.completion_algorithm = val_str - } - _ => { - invalid!( Some(span), - "unrecognized $env.config.{key}.{key2} '{val_str}'; expected either 'prefix' or 'fuzzy'" - ); - // Reconstruct - vals[index] = Value::string( - config.completion_algorithm.clone(), - span, - ); - } - }; - } else { - invalid!(Some(span), "should be a string"); - // Reconstruct - vals[index] = Value::string( - config.completion_algorithm.clone(), - span, - ); - } - } - "case_sensitive" => { - try_bool!( - cols, - vals, - index, - span, - case_sensitive_completions - ) - } - "external" => { - if let Value::Record { val, .. } = &mut vals[index] { - let Record { cols, vals } = val; - for index in (0..cols.len()).rev() { - let value = &vals[index]; - let key3 = cols[index].as_str(); - match key3 { - "max_results" => { - try_int!( - cols, - vals, - index, - span, - max_external_completion_results - ) - } - "completer" => { - if let Ok(v) = value.as_block() { - config.external_completer = Some(v) - } else { - match value { - Value::Nothing { .. } => {} - _ => { - invalid!( - Some(span), - "should be a block or null" - ); - // Reconstruct - vals[index] = reconstruct_external_completer(&config, - span - ); - } - } - } - } - "enable" => { - try_bool!( - cols, - vals, - index, - span, - enable_external_completion - ) - } - x => { - invalid_key!( - cols, - vals, - index, - Some(value.span()), - "$env.config.{key}.{key2}.{x} is an unknown config setting" - ); - } - } - } - } else { - invalid!(Some(span), "should be a record"); - // Reconstruct - vals[index] = reconstruct_external(&config, span); - } - } - x => { - invalid_key!( - cols, - vals, - index, - Some(value.span()), - "$env.config.{key}.{x} is an unknown config setting" - ); - } - } - } - } else { - invalid!(Some(vals[index].span()), "should be a record"); - // Reconstruct record - vals[index] = Value::record( - record! { - "quick" => Value::bool(config.quick_completions, span), - "partial" => Value::bool(config.partial_completions, span), - "algorithm" => Value::string(config.completion_algorithm.clone(), span), - "case_sensitive" => Value::bool(config.case_sensitive_completions, span), - "external" => reconstruct_external(&config, span), - }, - span, - ); - } - } - "cursor_shape" => { - fn reconstruct_cursor_shape( - name: Option, - span: Span, - ) -> Value { - Value::string( - match name { - Some(NuCursorShape::Line) => "line", - Some(NuCursorShape::Block) => "block", - Some(NuCursorShape::UnderScore) => "underscore", - Some(NuCursorShape::BlinkLine) => "blink_line", - Some(NuCursorShape::BlinkBlock) => "blink_block", - Some(NuCursorShape::BlinkUnderScore) => "blink_underscore", - None => "inherit", - }, - span, - ) - } - if let Value::Record { val, .. } = &mut vals[index] { - let Record { cols, vals } = val; - for index in (0..cols.len()).rev() { - let value = &vals[index]; - let key2 = cols[index].as_str(); - match key2 { - "vi_insert" => { - if let Ok(v) = value.as_string() { - let val_str = v.to_lowercase(); - match val_str.as_ref() { - "line" => { - config.cursor_shape_vi_insert = - Some(NuCursorShape::Line); - } - "block" => { - config.cursor_shape_vi_insert = - Some(NuCursorShape::Block); - } - "underscore" => { - config.cursor_shape_vi_insert = - Some(NuCursorShape::UnderScore); - } - "blink_line" => { - config.cursor_shape_vi_insert = - Some(NuCursorShape::BlinkLine); - } - "blink_block" => { - config.cursor_shape_vi_insert = - Some(NuCursorShape::BlinkBlock); - } - "blink_underscore" => { - config.cursor_shape_vi_insert = - Some(NuCursorShape::BlinkUnderScore); - } - "inherit" => { - config.cursor_shape_vi_insert = None; - } - _ => { - invalid!(Some(span), - "unrecognized $env.config.{key}.{key2} '{val_str}'; expected either 'line', 'block', 'underscore', 'blink_line', 'blink_block', 'blink_underscore' or 'inherit'" - ); - // Reconstruct - vals[index] = reconstruct_cursor_shape( - config.cursor_shape_vi_insert, - span, - ); - } - }; - } else { - invalid!(Some(span), "should be a string"); - // Reconstruct - vals[index] = reconstruct_cursor_shape( - config.cursor_shape_vi_insert, - span, - ); - } - } - "vi_normal" => { - if let Ok(v) = value.as_string() { - let val_str = v.to_lowercase(); - match val_str.as_ref() { - "line" => { - config.cursor_shape_vi_normal = - Some(NuCursorShape::Line); - } - "block" => { - config.cursor_shape_vi_normal = - Some(NuCursorShape::Block); - } - "underscore" => { - config.cursor_shape_vi_normal = - Some(NuCursorShape::UnderScore); - } - "blink_line" => { - config.cursor_shape_vi_normal = - Some(NuCursorShape::BlinkLine); - } - "blink_block" => { - config.cursor_shape_vi_normal = - Some(NuCursorShape::BlinkBlock); - } - "blink_underscore" => { - config.cursor_shape_vi_normal = - Some(NuCursorShape::BlinkUnderScore); - } - "inherit" => { - config.cursor_shape_vi_normal = None; - } - _ => { - invalid!(Some(span), - "unrecognized $env.config.{key}.{key2} '{val_str}'; expected either 'line', 'block', 'underscore', 'blink_line', 'blink_block', 'blink_underscore' or 'inherit'" - ); - // Reconstruct - vals[index] = reconstruct_cursor_shape( - config.cursor_shape_vi_normal, - span, - ); - } - }; - } else { - invalid!(Some(span), "should be a string"); - // Reconstruct - vals[index] = reconstruct_cursor_shape( - config.cursor_shape_vi_normal, - span, - ); - } - } - "emacs" => { - if let Ok(v) = value.as_string() { - let val_str = v.to_lowercase(); - match val_str.as_ref() { - "line" => { - config.cursor_shape_emacs = - Some(NuCursorShape::Line); - } - "block" => { - config.cursor_shape_emacs = - Some(NuCursorShape::Block); - } - "underscore" => { - config.cursor_shape_emacs = - Some(NuCursorShape::UnderScore); - } - "blink_line" => { - config.cursor_shape_emacs = - Some(NuCursorShape::BlinkLine); - } - "blink_block" => { - config.cursor_shape_emacs = - Some(NuCursorShape::BlinkBlock); - } - "blink_underscore" => { - config.cursor_shape_emacs = - Some(NuCursorShape::BlinkUnderScore); - } - "inherit" => { - config.cursor_shape_emacs = None; - } - _ => { - invalid!(Some(span), - "unrecognized $env.config.{key}.{key2} '{val_str}'; expected either 'line', 'block', 'underscore', 'blink_line', 'blink_block', 'blink_underscore' or 'inherit'" - ); - // Reconstruct - vals[index] = reconstruct_cursor_shape( - config.cursor_shape_emacs, - span, - ); - } - }; - } else { - invalid!(Some(span), "should be a string"); - // Reconstruct - vals[index] = reconstruct_cursor_shape( - config.cursor_shape_emacs, - span, - ); - } - } - x => { - invalid_key!( - cols, - vals, - index, - Some(value.span()), - "$env.config.{key}.{x} is an unknown config setting" - ); - } - } - } - } else { - invalid!(Some(vals[index].span()), "should be a record"); - // Reconstruct - vals[index] = Value::record( - record! { - "vi_insert" => reconstruct_cursor_shape(config.cursor_shape_vi_insert, span), - "vi_normal" => reconstruct_cursor_shape(config.cursor_shape_vi_normal, span), - "emacs" => reconstruct_cursor_shape(config.cursor_shape_emacs, span), - }, - span, - ); - } - } - "table" => { - fn reconstruct_index_mode(config: &Config, span: Span) -> Value { - Value::string( - match config.table_index_mode { - TableIndexMode::Always => "always", - TableIndexMode::Never => "never", - TableIndexMode::Auto => "auto", - }, - span, - ) - } - 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, - ), - } - } - - if let Value::Record { val, .. } = &mut vals[index] { - let Record { cols, vals } = val; - for index in (0..cols.len()).rev() { - let value = &vals[index]; - let key2 = cols[index].as_str(); - match key2 { - "mode" => { - if let Ok(v) = value.as_string() { - config.table_mode = v; - } else { - invalid!(Some(span), "should be a string"); - vals[index] = - Value::string(config.table_mode.clone(), span); - } - } - "header_on_separator" => { - try_bool!(cols, vals, index, span, table_move_header) - } - "padding" => match value { - Value::Int { val, .. } => { - if *val < 0 { - invalid!(Some(span), "unexpected $env.config.{key}.{key2} '{val}'; expected a unsigned integer"); - } - - config.table_indent.left = *val as usize; - config.table_indent.right = *val as usize; - } - Value::Record { val, .. } => { - let Record { cols, vals } = val; - let left = cols.iter().position(|e| e == "left"); - let right = cols.iter().position(|e| e == "right"); - - if let Some(i) = left { - let value = vals[i].as_int(); - match value { - Ok(val) => { - if val < 0 { - invalid!(Some(span), "unexpected $env.config.{key}.{key2} '{val}'; expected a unsigned integer"); - } - - config.table_indent.left = val as usize; - } - Err(_) => { - invalid!(Some(span), "unexpected $env.config.{key}.{key2} value; expected a unsigned integer or a record"); - } - } - } - - if let Some(i) = right { - let value = vals[i].as_int(); - match value { - Ok(val) => { - if val < 0 { - invalid!(Some(span), "unexpected $env.config.{key}.{key2} '{val}'; expected a unsigned integer"); - } - - config.table_indent.right = val as usize; - } - Err(_) => { - invalid!(Some(span), "unexpected $env.config.{key}.{key2} value; expected a unsigned integer or a record"); - } - } - } - } - _ => { - invalid!(Some(span), "unexpected $env.config.{key}.{key2} value; expected a unsigned integer or a record"); - } - }, - "index_mode" => { - if let Ok(b) = value.as_string() { - let val_str = b.to_lowercase(); - match val_str.as_ref() { - "always" => { - config.table_index_mode = TableIndexMode::Always - } - "never" => { - config.table_index_mode = TableIndexMode::Never - } - "auto" => { - config.table_index_mode = TableIndexMode::Auto - } - _ => { - invalid!( Some(span), - "unrecognized $env.config.{key}.{key2} '{val_str}'; expected either 'never', 'always' or 'auto'" - ); - vals[index] = - reconstruct_index_mode(&config, span); - } - } - } else { - invalid!(Some(span), "should be a string"); - vals[index] = reconstruct_index_mode(&config, span); - } - } - "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); - vals[index] = - reconstruct_trim_strategy(&config, span); - } - } - } - "show_empty" => { - try_bool!(cols, vals, index, span, table_show_empty) - } - "abbreviated_row_count" => { - if let Ok(b) = value.as_int() { - if b < 0 { - invalid!(Some(span), "should be an int unsigned"); - } - - config.table_abbreviation_threshold = Some(b as usize); - } else { - invalid!(Some(span), "should be an int"); - } - } - x => { - invalid_key!( - cols, - vals, - index, - Some(value.span()), - "$env.config.{key}.{x} is an unknown config setting" - ); - } - } - } - } else { - invalid!(Some(vals[index].span()), "should be a record"); - // Reconstruct - vals[index] = Value::record( - record! { - "mode" => Value::string(config.table_mode.clone(), span), - "index_mode" => reconstruct_index_mode(&config, span), - "trim" => reconstruct_trim_strategy(&config, span), - "show_empty" => Value::bool(config.table_show_empty, span), - }, - span, - ) - } - } - "filesize" => { - if let Value::Record { val, .. } = &mut vals[index] { - let Record { cols, vals } = val; - for index in (0..cols.len()).rev() { - let value = &vals[index]; - let key2 = cols[index].as_str(); - match key2 { - "metric" => { - try_bool!(cols, vals, index, span, filesize_metric) - } - "format" => { - if let Ok(v) = value.as_string() { - config.filesize_format = v.to_lowercase(); - } else { - invalid!(Some(span), "should be a string"); - // Reconstruct - vals[index] = - Value::string(config.filesize_format.clone(), span); - } - } - x => { - invalid_key!( - cols, - vals, - index, - Some(value.span()), - "$env.config.{key}.{x} is an unknown config setting" - ); - } - } - } - } else { - invalid!(Some(vals[index].span()), "should be a record"); - // Reconstruct - vals[index] = 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 { - invalid!(Some(vals[index].span()), "should be a record"); - // Reconstruct - vals[index] = 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 { - invalid!(Some(vals[index].span()), "should be a record"); - // Reconstruct - vals[index] = Value::record( - config - .color_config - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - span, - ); - } - } - "use_grid_icons" => { - try_bool!(cols, vals, index, span, use_grid_icons); - } - "footer_mode" => { - if let Ok(b) = value.as_string() { - let val_str = b.to_lowercase(); - config.footer_mode = match val_str.as_ref() { - "auto" => FooterMode::Auto, - "never" => FooterMode::Never, - "always" => FooterMode::Always, - _ => match &val_str.parse::() { - Ok(number) => FooterMode::RowCount(*number), - _ => FooterMode::Never, - }, - }; - } else { - invalid!(Some(span), "should be a string"); - // Reconstruct - vals[index] = Value::string( - match config.footer_mode { - FooterMode::Auto => "auto".into(), - FooterMode::Never => "never".into(), - FooterMode::Always => "always".into(), - FooterMode::RowCount(number) => number.to_string(), - }, - span, - ); - } - } - "float_precision" => { - try_int!(cols, vals, index, span, float_precision); - } - "use_ansi_coloring" => { - try_bool!(cols, vals, index, span, use_ansi_coloring); - } - "edit_mode" => { - if let Ok(v) = value.as_string() { - config.edit_mode = v.to_lowercase(); - } else { - invalid!(Some(span), "should be a string"); - // Reconstruct - vals[index] = Value::string(config.edit_mode.clone(), span); - } - } - "shell_integration" => { - try_bool!(cols, vals, index, span, 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(); - } - _ => { - dbg!(value); - invalid!(Some(span), "should be a string, list, or null"); - } - }, - "show_banner" => { - try_bool!(cols, vals, index, span, show_banner); - } - "render_right_prompt_on_last_line" => { - try_bool!(cols, vals, index, span, render_right_prompt_on_last_line); - } - "bracketed_paste" => { - try_bool!(cols, vals, index, span, bracketed_paste); - } - "use_kitty_protocol" => { - try_bool!(cols, vals, index, span, use_kitty_protocol); - } - // Menus - "menus" => match create_menus(value) { - Ok(map) => config.menus = map, - Err(e) => { - invalid!(Some(span), "should be a valid list of menus"); - errors.push(e); - // Reconstruct - vals[index] = 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, - ) - } - }, - // Keybindings - "keybindings" => match create_keybindings(value) { - Ok(keybindings) => config.keybindings = keybindings, - Err(e) => { - invalid!(Some(span), "should be a valid keybindings list"); - errors.push(e); - // Reconstruct - vals[index] = 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, - ) - } - }, - // Hooks - "hooks" => match create_hooks(value) { - Ok(hooks) => config.hooks = hooks, - Err(e) => { - invalid!(Some(span), "should be a valid hooks list"); - errors.push(e); - // Reconstruct - 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()); - } - vals.push(Value::record(hook, span)); - } - }, - "datetime_format" => { - if let Value::Record { val, .. } = &mut vals[index] { - let Record { cols, vals } = val; - for index in (0..cols.len()).rev() { - let value = &vals[index]; - let key2 = cols[index].as_str(); - match key2 { - "normal" => { - if let Ok(v) = value.as_string() { - config.datetime_normal_format = Some(v); - } else { - invalid!(Some(span), "should be a string"); - } - } - "table" => { - if let Ok(v) = value.as_string() { - config.datetime_table_format = Some(v); - } else { - invalid!(Some(span), "should be a string"); - } - } - x => { - invalid_key!( - cols, - vals, - index, - Some(value.span()), - "$env.config.{key}.{x} is an unknown config setting" - ); - } - } - } - } else { - invalid!(Some(vals[index].span()), "should be a record"); - // Reconstruct - vals[index] = Value::record( - record! { - "metric" => Value::bool(config.filesize_metric, span), - "format" => Value::string(config.filesize_format.clone(), span), - }, - span, - ); - } - } - "error_style" => { - if let Ok(style) = value.as_string() { - config.error_style = style; - } else { - invalid!(Some(span), "should be a string"); - vals[index] = Value::string(config.error_style.clone(), span); - } - } - // Catch all - x => { - invalid_key!( - cols, - vals, - index, - Some(value.span()), - "$env.config.{x} is an unknown config setting" - ); - } - } - } - } 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() { - // Because the config was iterated in reverse, these errors - // need to be reversed, too. - errors.reverse(); - 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 - }, - ) - } -} - -fn try_parse_trim_strategy( - value: &Value, - errors: &mut Vec, -) -> Result { - let map = create_map(value).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(TRIM_STRATEGY_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(TRIM_STRATEGY_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 { - 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 -} - -fn create_map(value: &Value) -> Result, ShellError> { - Ok(value - .as_record()? - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect()) -} - -// Parse the hooks to find the blocks to run when the hooks fire -fn create_hooks(value: &Value) -> Result { - 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, - )), - } -} - -// Parses the config object to extract the strings that will compose a keybinding for reedline -fn create_keybindings(value: &Value) -> Result, 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::>, ShellError>>(); - - let res = res? - .into_iter() - .flatten() - .collect::>(); - - 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, 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::>, ShellError>>(); - - let res = res?.into_iter().flatten().collect::>(); - - Ok(res) - } - _ => Ok(Vec::new()), - } -} - -pub fn extract_value<'record>( - name: &str, - record: &'record Record, - span: Span, -) -> Result<&'record Value, ShellError> { - record - .iter() - .find_map(|(col, val)| if col == name { Some(val) } else { None }) - .ok_or_else(|| ShellError::MissingConfigValue(name.to_string(), span)) -} diff --git a/crates/nu-protocol/src/config/completer.rs b/crates/nu-protocol/src/config/completer.rs new file mode 100644 index 000000000..e54b03d17 --- /dev/null +++ b/crates/nu-protocol/src/config/completer.rs @@ -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 { + 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, + ) +} diff --git a/crates/nu-protocol/src/config/helper.rs b/crates/nu-protocol/src/config/helper.rs new file mode 100644 index 000000000..51c11a21d --- /dev/null +++ b/crates/nu-protocol/src/config/helper.rs @@ -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( + config_point: &mut T, + config_path: &[&str], + value: &mut Value, + errors: &mut Vec, +) where + T: FromStr + 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, + 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, + 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) { + // 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) { + 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, 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)) +} diff --git a/crates/nu-protocol/src/config/hooks.rs b/crates/nu-protocol/src/config/hooks.rs new file mode 100644 index 000000000..0a576d7cd --- /dev/null +++ b/crates/nu-protocol/src/config/hooks.rs @@ -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, + pub pre_execution: Option, + pub env_change: Option, + pub display_output: Option, + pub command_not_found: Option, +} + +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 { + 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) +} diff --git a/crates/nu-protocol/src/config/mod.rs b/crates/nu-protocol/src/config/mod.rs new file mode 100644 index 000000000..e414773e6 --- /dev/null +++ b/crates/nu-protocol/src/config/mod.rs @@ -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, + 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, + pub use_ls_colors: bool, + pub color_config: HashMap, + 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, + pub menus: Vec, + 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, + pub cursor_shape_vi_insert: NuCursorShape, + pub cursor_shape_vi_normal: NuCursorShape, + pub cursor_shape_emacs: NuCursorShape, + pub datetime_normal_format: Option, + pub datetime_table_format: Option, + 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) { + // 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, 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 + }, + ) + } +} diff --git a/crates/nu-protocol/src/config/output.rs b/crates/nu-protocol/src/config/output.rs new file mode 100644 index 000000000..473f9cfdf --- /dev/null +++ b/crates/nu-protocol/src/config/output.rs @@ -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 { + 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) +} diff --git a/crates/nu-protocol/src/config/reedline.rs b/crates/nu-protocol/src/config/reedline.rs new file mode 100644 index 000000000..7032e7408 --- /dev/null +++ b/crates/nu-protocol/src/config/reedline.rs @@ -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 { + 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 { + 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 { + 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, 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::>, ShellError>>(); + + let res = res? + .into_iter() + .flatten() + .collect::>(); + + 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, 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::>, ShellError>>(); + + let res = res?.into_iter().flatten().collect::>(); + + 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, + ) +} diff --git a/crates/nu-protocol/src/config/table.rs b/crates/nu-protocol/src/config/table.rs new file mode 100644 index 000000000..81f30bad9 --- /dev/null +++ b/crates/nu-protocol/src/config/table.rs @@ -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 { + 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 { + 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 { + 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, + }, +} + +impl TrimStrategy { + pub fn wrap(dont_split_words: bool) -> Self { + Self::Wrap { + try_to_keep_words: dont_split_words, + } + } + + pub fn truncate(suffix: Option) -> 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, +) -> Result { + 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 { + 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, + ) +} diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 952aec11c..646b842f9 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -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 { diff --git a/crates/nu-protocol/tests/into_config.rs b/crates/nu-protocol/tests/into_config.rs index 0d33825e3..7995ea63c 100644 --- a/crates/nu-protocol/tests/into_config.rs +++ b/crates/nu-protocol/tests/into_config.rs @@ -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] diff --git a/crates/nu-table/src/common.rs b/crates/nu-table/src/common.rs index f2a3b48ec..f8e9a4d1b 100644 --- a/crates/nu-table/src/common.rs +++ b/crates/nu-table/src/common.rs @@ -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(), } }