nushell/crates/nu-protocol/src/config.rs
JT 6c730def4b
revert: move to ahash (#9464)
This PR reverts https://github.com/nushell/nushell/pull/9391

We try not to revert PRs like this, though after discussion with the
Nushell team, we decided to revert this one.

The main reason is that Nushell, as a codebase, isn't ready for these
kinds of optimisations. It's in the part of the development cycle where
our main focus should be on improving the algorithms inside of Nushell
itself. Once we have matured our algorithms, then we can look for
opportunities to switch out technologies we're using for alternate
forms.

Much of Nushell still has lots of opportunities for tuning the codebase,
paying down technical debt, and making the codebase generally cleaner
and more robust. This should be the focus. Performance improvements
should flow out of that work.

Said another, optimisation that isn't part of tuning the codebase is
premature at this stage. We need to focus on doing the hard work of
making the engine, parser, etc better.

# User-Facing Changes

Reverts the HashMap -> ahash change.

cc @FilipAndersson245
2023-06-18 15:27:57 +12:00

1699 lines
83 KiB
Rust

use crate::{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 menu from the config object
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Hooks {
pub pre_prompt: Option<Value>,
pub pre_execution: Option<Value>,
pub env_change: Option<Value>,
pub display_output: Option<Value>,
pub command_not_found: Option<Value>,
}
impl Hooks {
pub fn new() -> Self {
Self {
pre_prompt: None,
pre_execution: None,
env_change: None,
display_output: None,
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<usize>,
pub filesize_metric: bool,
pub table_mode: String,
pub table_show_empty: bool,
pub use_ls_colors: bool,
pub color_config: HashMap<String, Value>,
pub use_grid_icons: bool,
pub footer_mode: FooterMode,
pub float_precision: i64,
pub max_external_completion_results: i64,
pub filesize_format: String,
pub use_ansi_coloring: bool,
pub quick_completions: bool,
pub partial_completions: bool,
pub completion_algorithm: 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 log_level: String,
pub keybindings: Vec<ParsedKeybinding>,
pub menus: Vec<ParsedMenu>,
pub hooks: Hooks,
pub rm_always_trash: bool,
pub shell_integration: bool,
pub buffer_editor: String,
pub table_index_mode: TableIndexMode,
pub cd_with_abbreviations: bool,
pub case_sensitive_completions: bool,
pub enable_external_completion: bool,
pub trim_strategy: TrimStrategy,
pub show_banner: bool,
pub bracketed_paste: bool,
pub show_clickable_links_in_ls: bool,
pub render_right_prompt_on_last_line: bool,
pub explore: HashMap<String, Value>,
pub cursor_shape_vi_insert: NuCursorShape,
pub cursor_shape_vi_normal: NuCursorShape,
pub cursor_shape_emacs: NuCursorShape,
}
impl Default for Config {
fn default() -> Config {
Config {
filesize_metric: false,
table_mode: "rounded".into(),
table_show_empty: true,
external_completer: None,
use_ls_colors: true,
color_config: HashMap::new(),
use_grid_icons: false,
footer_mode: FooterMode::RowCount(25),
float_precision: 4,
max_external_completion_results: 100,
filesize_format: "auto".into(),
use_ansi_coloring: true,
quick_completions: true,
partial_completions: true,
completion_algorithm: "prefix".into(),
edit_mode: "emacs".into(),
max_history_size: i64::MAX,
sync_history_on_enter: true,
history_file_format: HistoryFileFormat::PlainText,
history_isolation: false,
log_level: String::new(),
keybindings: Vec::new(),
menus: Vec::new(),
hooks: Hooks::new(),
rm_always_trash: false,
shell_integration: false,
buffer_editor: String::new(),
table_index_mode: TableIndexMode::Always,
cd_with_abbreviations: false,
case_sensitive_completions: false,
enable_external_completion: true,
trim_strategy: TRIM_STRATEGY_DEFAULT,
show_banner: true,
bracketed_paste: true,
show_clickable_links_in_ls: true,
render_right_prompt_on_last_line: false,
explore: HashMap::new(),
cursor_shape_vi_insert: NuCursorShape::Block,
cursor_shape_vi_normal: NuCursorShape::UnderScore,
cursor_shape_emacs: NuCursorShape::Line,
}
}
}
#[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<String>,
},
}
impl TrimStrategy {
pub fn wrap(dont_split_words: bool) -> Self {
Self::Wrap {
try_to_keep_words: dont_split_words,
}
}
pub fn truncate(suffix: Option<String>) -> Self {
Self::Truncate { suffix }
}
}
impl Value {
pub fn into_config(&mut self, config: &Config) -> (Config, Option<ShellError>) {
// Clone the passed-in config rather than mutating it.
let mut config = config.clone();
let mut legacy_options_used = false;
// 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::boolean(config.$setting, *$span);
}
};
}
macro_rules! try_int {
($cols:ident, $vals:ident, $index:ident, $span:expr, $setting:ident) => {
if let Ok(b) = &$vals[$index].as_integer() {
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 { cols, vals, span } = self {
// 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();
match key {
// Grouped options
"ls" => {
if let Value::Record { cols, vals, span } = &mut vals[index] {
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,
value.span().ok(),
"$env.config.{key}.{x} is an unknown config setting"
);
}
}
}
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["use_ls_colors".into(), "clickable_links".into()],
vec![
Value::boolean(config.use_ls_colors, *span),
Value::boolean(config.show_clickable_links_in_ls, *span),
],
*span,
);
}
}
"cd" => {
if let Value::Record { cols, vals, span } = &mut vals[index] {
for index in (0..cols.len()).rev() {
let value = &vals[index];
let key2 = cols[index].as_str();
match key2 {
"abbreviations" => {
try_bool!(cols, vals, index, span, cd_with_abbreviations)
}
x => {
invalid_key!(
cols,
vals,
index,
value.span().ok(),
"$env.config.{key}.{x} is an unknown config setting"
);
}
}
}
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["use_ls_colors".into(), "clickable_links".into()],
vec![
Value::boolean(config.use_ls_colors, *span),
Value::boolean(config.show_clickable_links_in_ls, *span),
],
*span,
);
}
}
"rm" => {
if let Value::Record { cols, vals, span } = &mut vals[index] {
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,
value.span().ok(),
"$env.config.{key}.{x} is an unknown config setting"
);
}
}
}
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["always_trash".into()],
vec![Value::boolean(config.rm_always_trash, *span)],
*span,
);
}
}
"history" => {
macro_rules! reconstruct_history_file_format {
($span:expr) => {
Value::string(
match config.history_file_format {
HistoryFileFormat::Sqlite => "sqlite",
HistoryFileFormat::PlainText => "plaintext",
},
*$span,
)
};
}
if let Value::Record { cols, vals, span } = &mut vals[index] {
for index in (0..cols.len()).rev() {
let value = &vals[index];
let key2 = cols[index].as_str();
match key2 {
"history_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!(span);
}
};
} else {
invalid!(Some(*span), "should be a string");
// Reconstruct
vals[index] = reconstruct_history_file_format!(span);
}
}
x => {
invalid_key!(
cols,
vals,
index,
value.span().ok(),
"$env.config.{key}.{x} is an unknown config setting"
);
}
}
}
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec![
"sync_on_enter".into(),
"max_size".into(),
"file_format".into(),
],
vec![
Value::boolean(config.sync_history_on_enter, *span),
Value::int(config.max_history_size, *span),
reconstruct_history_file_format!(span),
],
*span,
);
}
}
"completions" => {
macro_rules! reconstruct_external_completer {
($span: expr) => {
if let Some(block) = config.external_completer {
Value::Block {
val: block,
span: *$span,
}
} else {
Value::Nothing { span: *$span }
}
};
}
macro_rules! reconstruct_external {
($span: expr) => {
Value::record(
vec!["max_results".into(), "completer".into(), "enable".into()],
vec![
Value::int(config.max_external_completion_results, *$span),
reconstruct_external_completer!($span),
Value::boolean(config.enable_external_completion, *$span),
],
*$span,
)
};
}
if let Value::Record { cols, vals, span } = &mut vals[index] {
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 { cols, vals, span } = &mut vals[index]
{
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!(
span
);
}
}
}
}
"enable" => {
try_bool!(
cols,
vals,
index,
span,
enable_external_completion
)
}
x => {
invalid_key!(
cols,
vals,
index,
value.span().ok(),
"$env.config.{key}.{key2}.{x} is an unknown config setting"
);
}
}
}
} else {
invalid!(Some(*span), "should be a record");
// Reconstruct
vals[index] = reconstruct_external!(span);
}
}
x => {
invalid_key!(
cols,
vals,
index,
value.span().ok(),
"$env.config.{key}.{x} is an unknown config setting"
);
}
}
}
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct record
vals[index] = Value::record(
vec![
"quick".into(),
"partial".into(),
"algorithm".into(),
"case_sensitive".into(),
"external".into(),
],
vec![
Value::boolean(config.quick_completions, *span),
Value::boolean(config.partial_completions, *span),
Value::string(config.completion_algorithm.clone(), *span),
Value::boolean(config.case_sensitive_completions, *span),
reconstruct_external!(span),
],
*span,
);
}
}
"cursor_shape" => {
macro_rules! reconstruct_cursor_shape {
($name:expr, $span:expr) => {
Value::string(
match $name {
NuCursorShape::Line => "line",
NuCursorShape::Block => "block",
NuCursorShape::UnderScore => "underscore",
NuCursorShape::BlinkLine => "blink_line",
NuCursorShape::BlinkBlock => "blink_block",
NuCursorShape::BlinkUnderScore => "blink_underscore",
},
*$span,
)
};
}
if let Value::Record { cols, vals, span } = &mut vals[index] {
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 =
NuCursorShape::Line;
}
"block" => {
config.cursor_shape_vi_insert =
NuCursorShape::Block;
}
"underscore" => {
config.cursor_shape_vi_insert =
NuCursorShape::UnderScore;
}
"blink_line" => {
config.cursor_shape_vi_insert =
NuCursorShape::BlinkLine;
}
"blink_block" => {
config.cursor_shape_vi_insert =
NuCursorShape::BlinkBlock;
}
"blink_underscore" => {
config.cursor_shape_vi_insert =
NuCursorShape::BlinkUnderScore;
}
_ => {
invalid!(Some(*span),
"unrecognized $env.config.{key}.{key2} '{val_str}'; expected either 'line', 'block', 'underscore', 'blink_line', 'blink_block', or 'blink_underscore'"
);
// 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 =
NuCursorShape::Line;
}
"block" => {
config.cursor_shape_vi_normal =
NuCursorShape::Block;
}
"underscore" => {
config.cursor_shape_vi_normal =
NuCursorShape::UnderScore;
}
"blink_line" => {
config.cursor_shape_vi_normal =
NuCursorShape::BlinkLine;
}
"blink_block" => {
config.cursor_shape_vi_normal =
NuCursorShape::BlinkBlock;
}
"blink_underscore" => {
config.cursor_shape_vi_normal =
NuCursorShape::BlinkUnderScore;
}
_ => {
invalid!(Some(*span),
"unrecognized $env.config.{key}.{key2} '{val_str}'; expected either 'line', 'block', 'underscore', 'blink_line', 'blink_block', or 'blink_underscore'"
);
// 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 = NuCursorShape::Line;
}
"block" => {
config.cursor_shape_emacs =
NuCursorShape::Block;
}
"underscore" => {
config.cursor_shape_emacs =
NuCursorShape::UnderScore;
}
"blink_line" => {
config.cursor_shape_emacs =
NuCursorShape::BlinkLine;
}
"blink_block" => {
config.cursor_shape_emacs =
NuCursorShape::BlinkBlock;
}
"blink_underscore" => {
config.cursor_shape_emacs =
NuCursorShape::BlinkUnderScore;
}
_ => {
invalid!(Some(*span),
"unrecognized $env.config.{key}.{key2} '{val_str}'; expected either 'line', 'block', 'underscore', 'blink_line', 'blink_block', or 'blink_underscore'"
);
// 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,
value.span().ok(),
"$env.config.{key}.{x} is an unknown config setting"
);
}
}
}
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["vi_insert".into(), "vi_normal".into(), "emacs".into()],
vec![
reconstruct_cursor_shape!(config.cursor_shape_vi_insert, span),
reconstruct_cursor_shape!(config.cursor_shape_vi_normal, span),
reconstruct_cursor_shape!(config.cursor_shape_emacs, span),
],
*span,
);
}
}
"table" => {
macro_rules! reconstruct_index_mode {
($span:expr) => {
Value::string(
match config.table_index_mode {
TableIndexMode::Always => "always",
TableIndexMode::Never => "never",
TableIndexMode::Auto => "auto",
},
*$span,
)
};
}
macro_rules! reconstruct_trim_strategy {
($span:expr) => {
match &config.trim_strategy {
TrimStrategy::Wrap { try_to_keep_words } => Value::record(
vec![
"methodology".into(),
"wrapping_try_keep_words".into(),
],
vec![
Value::string("wrapping", *$span),
Value::boolean(*try_to_keep_words, *$span),
],
*$span,
),
TrimStrategy::Truncate { suffix } => Value::record(
vec!["methodology".into(), "truncating_suffix".into()],
match suffix {
Some(s) => vec![
Value::string("truncating", *$span),
Value::string(s.clone(), *$span),
],
None => vec![
Value::string("truncating", *$span),
Value::Nothing { span: *span },
],
},
*$span,
),
}
};
}
if let Value::Record { cols, vals, span } = &mut vals[index] {
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);
}
}
"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!(span);
}
}
} else {
invalid!(Some(*span), "should be a string");
vals[index] = reconstruct_index_mode!(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!(span);
}
}
}
"show_empty" => {
try_bool!(cols, vals, index, span, table_show_empty)
}
x => {
invalid_key!(
cols,
vals,
index,
value.span().ok(),
"$env.config.{key}.{x} is an unknown config setting"
);
}
}
}
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["mode".into(), "index_mode".into(), "trim".into()],
vec![
Value::string(config.table_mode.clone(), *span),
reconstruct_index_mode!(span),
reconstruct_trim_strategy!(span),
],
*span,
)
}
}
"filesize" => {
if let Value::Record { cols, vals, span } = &mut vals[index] {
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,
value.span().ok(),
"$env.config.{key}.{x} is an unknown config setting"
);
}
}
}
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record(
vec!["metric".into(), "format".into()],
vec![
Value::boolean(config.filesize_metric, *span),
Value::string(config.filesize_format.clone(), *span),
],
*span,
);
}
}
"explore" => {
if let Ok(map) = create_map(value) {
config.explore = map;
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record_from_hashmap(&config.explore, *span);
}
}
// Misc. options
"color_config" => {
if let Ok(map) = create_map(value) {
config.color_config = map;
} else {
invalid!(vals[index].span().ok(), "should be a record");
// Reconstruct
vals[index] = Value::record_from_hashmap(&config.color_config, *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::<u64>() {
Ok(number) => FooterMode::RowCount(*number),
_ => FooterMode::Never,
},
};
} else {
invalid!(Some(*span), "should be a string");
// Reconstruct
vals[index] = Value::String {
val: match config.footer_mode {
FooterMode::Auto => "auto".into(),
FooterMode::Never => "never".into(),
FooterMode::Always => "always".into(),
FooterMode::RowCount(number) => number.to_string(),
},
span: *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);
}
}
"log_level" => {
if let Ok(v) = value.as_string() {
config.log_level = v.to_lowercase();
} else {
invalid!(Some(*span), "should be a string");
// Reconstruct
vals[index] = Value::string(config.log_level.clone(), *span);
}
}
"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 {
vals: 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 {
cols: vec![
"name".into(),
"only_buffer_difference".into(),
"marker".into(),
"style".into(),
"type".into(),
"source".into(),
],
vals: vec![
name.clone(),
only_buffer_difference.clone(),
marker.clone(),
style.clone(),
menu_type.clone(),
source.clone(),
],
span: *span,
}
},
)
.collect(),
span: *span,
}
}
},
"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 {
vals: config
.keybindings
.iter()
.map(
|ParsedKeybinding {
modifier,
keycode,
mode,
event,
}| {
Value::Record {
cols: vec![
"modifier".into(),
"keycode".into(),
"mode".into(),
"event".into(),
],
vals: vec![
modifier.clone(),
keycode.clone(),
mode.clone(),
event.clone(),
],
span: *span,
}
},
)
.collect(),
span: *span,
}
}
},
"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_cols = vec![];
let mut hook_vals = vec![];
if let Some(ref value) = config.hooks.pre_prompt {
hook_cols.push("pre_prompt".into());
hook_vals.push(value.clone());
}
if let Some(ref value) = config.hooks.pre_execution {
hook_cols.push("pre_execution".into());
hook_vals.push(value.clone());
}
if let Some(ref value) = config.hooks.env_change {
hook_cols.push("env_change".into());
hook_vals.push(value.clone());
}
if let Some(ref value) = config.hooks.display_output {
hook_cols.push("display_output".into());
hook_vals.push(value.clone());
}
vals.push(Value::Record {
cols: hook_cols,
vals: hook_vals,
span: *span,
});
}
},
"shell_integration" => {
try_bool!(cols, vals, index, span, shell_integration);
}
"buffer_editor" => {
if let Ok(v) = value.as_string() {
config.buffer_editor = v.to_lowercase();
} else {
invalid!(Some(*span), "should be a string");
}
}
"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);
}
// Legacy config options (deprecated as of 2022-11-02)
// Legacy options do NOT reconstruct their values on error
"use_ls_colors" => {
legacy_options_used = true;
try_bool!(cols, vals, index, span, use_ls_colors);
}
"rm_always_trash" => {
legacy_options_used = true;
try_bool!(cols, vals, index, span, rm_always_trash);
}
"history_file_format" => {
legacy_options_used = true;
if let Ok(b) = value.as_string() {
let val_str = b.to_lowercase();
config.history_file_format = match val_str.as_ref() {
"sqlite" => HistoryFileFormat::Sqlite,
"plaintext" => HistoryFileFormat::PlainText,
_ => {
invalid!(
Some(*span),
"unrecognized $env.config.{key} '{val_str}'"
);
HistoryFileFormat::PlainText
}
};
} else {
invalid!(Some(*span), "should be a string");
}
}
"sync_history_on_enter" => {
legacy_options_used = true;
try_bool!(cols, vals, index, span, sync_history_on_enter);
}
"max_history_size" => {
legacy_options_used = true;
try_int!(cols, vals, index, span, max_history_size);
}
"quick_completions" => {
legacy_options_used = true;
try_bool!(cols, vals, index, span, quick_completions);
}
"partial_completions" => {
legacy_options_used = true;
try_bool!(cols, vals, index, span, partial_completions);
}
"max_external_completion_results" => {
legacy_options_used = true;
try_int!(cols, vals, index, span, max_external_completion_results);
}
"completion_algorithm" => {
legacy_options_used = true;
if let Ok(v) = value.as_string() {
let val_str = v.to_lowercase();
config.completion_algorithm = match val_str.as_ref() {
// This should match the MatchAlgorithm enum in completions::completion_options
"prefix" => val_str,
"fuzzy" => val_str,
_ => {
invalid!( Some(*span),
"unrecognized $env.config.{key} '{val_str}'; expected either 'prefix' or 'fuzzy'"
);
val_str
}
};
} else {
invalid!(Some(*span), "should be a string");
}
}
"case_sensitive_completions" => {
legacy_options_used = true;
try_bool!(cols, vals, index, span, case_sensitive_completions);
}
"enable_external_completion" => {
legacy_options_used = true;
try_bool!(cols, vals, index, span, enable_external_completion);
}
"external_completer" => {
legacy_options_used = true;
if let Ok(v) = value.as_block() {
config.external_completer = Some(v)
}
// No error here because external completers are optional.
// Idea: maybe error if this is a non-block, non-null?
}
"table_mode" => {
legacy_options_used = true;
if let Ok(v) = value.as_string() {
config.table_mode = v;
} else {
invalid!(Some(*span), "should be a string");
}
}
"table_index_mode" => {
legacy_options_used = true;
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.table_index_mode '{val_str}'; expected either 'never', 'always' or 'auto'"
);
}
}
} else {
invalid!(Some(*span), "should be a string");
}
}
"table_trim" => {
legacy_options_used = true;
match try_parse_trim_strategy(value, &mut errors) {
Ok(v) => config.trim_strategy = v,
Err(e) => {
// try_parse_trim_strategy() already calls eprintln!() on error
cols.remove(index);
vals.remove(index);
errors.push(e);
}
}
}
"show_clickable_links_in_ls" => {
legacy_options_used = true;
try_bool!(cols, vals, index, span, show_clickable_links_in_ls);
}
"cd_with_abbreviations" => {
legacy_options_used = true;
try_bool!(cols, vals, index, span, cd_with_abbreviations);
}
"filesize_metric" => {
legacy_options_used = true;
try_bool!(cols, vals, index, span, filesize_metric);
}
"filesize_format" => {
legacy_options_used = true;
if let Ok(v) = value.as_string() {
config.filesize_format = v.to_lowercase();
} else {
invalid!(Some(*span), "should be a string");
}
}
"cursor_shape_vi_insert" => {
legacy_options_used = true;
if let Ok(b) = value.as_string() {
let val_str = b.to_lowercase();
config.cursor_shape_vi_insert = match val_str.as_ref() {
"block" => NuCursorShape::Block,
"underline" => NuCursorShape::UnderScore,
"line" => NuCursorShape::Line,
_ => {
invalid!(
Some(*span),
"unrecognized $env.config.{key} '{val_str}'"
);
NuCursorShape::Line
}
};
} else {
invalid!(Some(*span), "should be a string");
}
}
"cursor_shape_vi_normal" => {
legacy_options_used = true;
if let Ok(b) = value.as_string() {
let val_str = b.to_lowercase();
config.cursor_shape_vi_normal = match val_str.as_ref() {
"block" => NuCursorShape::Block,
"underline" => NuCursorShape::UnderScore,
"line" => NuCursorShape::Line,
_ => {
invalid!(
Some(*span),
"unrecognized $env.config.{key} '{val_str}'"
);
NuCursorShape::Line
}
};
} else {
invalid!(Some(*span), "should be a string");
}
}
"cursor_shape_emacs" => {
legacy_options_used = true;
if let Ok(b) = value.as_string() {
let val_str = b.to_lowercase();
config.cursor_shape_emacs = match val_str.as_ref() {
"block" => NuCursorShape::Block,
"underline" => NuCursorShape::UnderScore,
"line" => NuCursorShape::Line,
_ => {
invalid!(
Some(*span),
"unrecognized $env.config.{key} '{val_str}'"
);
NuCursorShape::Line
}
};
} else {
invalid!(Some(*span), "should be a string");
}
}
// End legacy options
x => {
invalid_key!(
cols,
vals,
index,
value.span().ok(),
"$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(),
self.span().ok(),
None,
vec![],
)),
);
}
if legacy_options_used {
// This is a notification message, not an error.
eprintln!(
r#"The format of $env.config has recently changed, and several options have been grouped into sub-records. You may need to update your config.nu file.
Please consult https://www.nushell.sh/blog/2022-11-29-nushell-0.72.html for details. Support for the old format will be removed in an upcoming Nu release."#
);
}
// 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<ShellError>,
) -> Result<TrimStrategy, ShellError> {
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(),
value.span().ok(),
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(),
value.span().ok(),
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(),
value.span().ok(),
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(),
value.span().ok(),
Some("Please consult the documentation for configuring Nushell.".into()),
vec![],
));
}
}
}
}
Ok(methodology)
}
fn try_parse_trim_methodology(value: &Value) -> Option<TrimStrategy> {
if let Ok(value) = value.as_string() {
match value.to_lowercase().as_str() {
"wrapping" => {
return Some(TrimStrategy::Wrap {
try_to_keep_words: false,
});
}
"truncating" => return Some(TrimStrategy::Truncate { suffix: None }),
_ => eprintln!("unrecognized $config.table.trim.methodology value; expected either 'truncating' or 'wrapping'"),
}
} else {
eprintln!("$env.config.table.trim.methodology is not a string")
}
None
}
fn create_map(value: &Value) -> Result<HashMap<String, Value>, ShellError> {
let (cols, inner_vals) = value.as_record()?;
let mut hm: HashMap<String, Value> = HashMap::new();
for (k, v) in cols.iter().zip(inner_vals) {
hm.insert(k.to_string(), v.clone());
}
Ok(hm)
}
// Parse the hooks to find the blocks to run when the hooks fire
fn create_hooks(value: &Value) -> Result<Hooks, ShellError> {
match value {
Value::Record { cols, vals, span } => {
let mut hooks = Hooks::new();
for idx in 0..cols.len() {
match cols[idx].as_str() {
"pre_prompt" => hooks.pre_prompt = Some(vals[idx].clone()),
"pre_execution" => hooks.pre_execution = Some(vals[idx].clone()),
"env_change" => hooks.env_change = Some(vals[idx].clone()),
"display_output" => hooks.display_output = Some(vals[idx].clone()),
"command_not_found" => hooks.command_not_found = Some(vals[idx].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)
}
v => Err(ShellError::UnsupportedConfigValue(
"record for 'hooks' config".into(),
"non-record value".into(),
v.span().unwrap_or_else(|_| Span::unknown()),
)),
}
}
// Parses the config object to extract the strings that will compose a keybinding for reedline
fn create_keybindings(value: &Value) -> Result<Vec<ParsedKeybinding>, ShellError> {
match value {
Value::Record { cols, vals, span } => {
// Finding the modifier value in the record
let modifier = extract_value("modifier", cols, vals, span)?.clone();
let keycode = extract_value("keycode", cols, vals, span)?.clone();
let mode = extract_value("mode", cols, vals, span)?.clone();
let event = extract_value("event", cols, vals, span)?.clone();
let keybinding = ParsedKeybinding {
modifier,
keycode,
mode,
event,
};
// We return a menu to be able to do recursion on the same function
Ok(vec![keybinding])
}
Value::List { vals, .. } => {
let res = vals
.iter()
.map(create_keybindings)
.collect::<Result<Vec<Vec<ParsedKeybinding>>, ShellError>>();
let res = res?
.into_iter()
.flatten()
.collect::<Vec<ParsedKeybinding>>();
Ok(res)
}
_ => Ok(Vec::new()),
}
}
// Parses the config object to extract the strings that will compose a keybinding for reedline
pub fn create_menus(value: &Value) -> Result<Vec<ParsedMenu>, ShellError> {
match value {
Value::Record { cols, vals, span } => {
// Finding the modifier value in the record
let name = extract_value("name", cols, vals, span)?.clone();
let marker = extract_value("marker", cols, vals, span)?.clone();
let only_buffer_difference =
extract_value("only_buffer_difference", cols, vals, span)?.clone();
let style = extract_value("style", cols, vals, span)?.clone();
let menu_type = extract_value("type", cols, vals, span)?.clone();
// Source is an optional value
let source = match extract_value("source", cols, vals, span) {
Ok(source) => source.clone(),
Err(_) => Value::Nothing { span: *span },
};
let menu = ParsedMenu {
name,
only_buffer_difference,
marker,
style,
menu_type,
source,
};
Ok(vec![menu])
}
Value::List { vals, .. } => {
let res = vals
.iter()
.map(create_menus)
.collect::<Result<Vec<Vec<ParsedMenu>>, ShellError>>();
let res = res?.into_iter().flatten().collect::<Vec<ParsedMenu>>();
Ok(res)
}
_ => Ok(Vec::new()),
}
}
pub fn extract_value<'record>(
name: &str,
cols: &'record [String],
vals: &'record [Value],
span: &Span,
) -> Result<&'record Value, ShellError> {
cols.iter()
.position(|col| col.as_str() == name)
.and_then(|index| vals.get(index))
.ok_or_else(|| ShellError::MissingConfigValue(name.to_string(), *span))
}