diff options
author | Dan Davison <dandavison7@gmail.com> | 2020-06-28 12:10:24 -0400 |
---|---|---|
committer | Dan Davison <dandavison7@gmail.com> | 2020-06-30 08:00:30 -0400 |
commit | db25d92cba169359b8cd2bb315742a6d36a0a3dd (patch) | |
tree | 3d59ab866adb39bfc9d3c86567d20902952412d8 /src/options | |
parent | 38a9e9baf5395f34e4d6d7310929fe2941151103 (diff) |
Refactor: options module
Diffstat (limited to 'src/options')
-rw-r--r-- | src/options/get.rs | 111 | ||||
-rw-r--r-- | src/options/mod.rs | 4 | ||||
-rw-r--r-- | src/options/option_value.rs | 97 | ||||
-rw-r--r-- | src/options/rewrite.rs | 231 | ||||
-rw-r--r-- | src/options/set.rs | 284 |
5 files changed, 727 insertions, 0 deletions
diff --git a/src/options/get.rs b/src/options/get.rs new file mode 100644 index 00000000..caaccd09 --- /dev/null +++ b/src/options/get.rs @@ -0,0 +1,111 @@ +use std::collections::HashMap; + +use crate::cli; +use crate::features; +use crate::git_config::{self, GitConfigGet}; +use crate::options::option_value::{OptionValue, ProvenancedOptionValue}; +use ProvenancedOptionValue::*; + +/// Look up a value of type `T` associated with `option name`. The search rules are: +/// +/// 1. If there is a value associated with `option_name` in the main [delta] git config +/// section, then stop searching and return that value. +/// +/// 2. For each feature in the ordered list of enabled features: +/// +/// 2.1 Look-up the value, treating `feature` as a custom feature. +/// I.e., if there is a value associated with `option_name` in a git config section +/// named [delta "`feature`"] then stop searching and return that value. +/// +/// 2.2 Look-up the value, treating `feature` as a builtin feature. +/// I.e., if there is a value (not a default value) associated with `option_name` in a +/// builtin feature named `feature`, then stop searching and return that value. +/// Otherwise, record the default value and continue searching. +/// +/// 3. Return the last default value that was encountered. +pub fn get_option_value<T>( + option_name: &str, + builtin_features: &HashMap<String, features::BuiltinFeature>, + opt: &cli::Opt, + git_config: &mut Option<git_config::GitConfig>, +) -> Option<T> +where + T: GitConfigGet, + T: GetOptionValue, + T: From<OptionValue>, + T: Into<OptionValue>, +{ + T::get_option_value(option_name, builtin_features, opt, git_config) +} + +pub trait GetOptionValue { + fn get_option_value( + option_name: &str, + builtin_features: &HashMap<String, features::BuiltinFeature>, + opt: &cli::Opt, + git_config: &mut Option<git_config::GitConfig>, + ) -> Option<Self> + where + Self: Sized, + Self: GitConfigGet, + Self: From<OptionValue>, + Self: Into<OptionValue>, + { + if let Some(git_config) = git_config { + if let Some(value) = git_config.get::<Self>(&format!("delta.{}", option_name)) { + return Some(value); + } + } + for feature in opt.features.to_lowercase().split_whitespace().rev() { + match Self::get_provenanced_value_for_feature( + option_name, + &feature, + &builtin_features, + opt, + git_config, + ) { + Some(GitConfigValue(value)) | Some(DefaultValue(value)) => { + return Some(value.into()); + } + None => {} + } + } + None + } + + /// Return the value, or default value, associated with `option_name` under feature name + /// `feature`. This may refer to a custom feature, or a builtin feature, or both. Only builtin + /// features have defaults. See `GetOptionValue::get_option_value`. + fn get_provenanced_value_for_feature( + option_name: &str, + feature: &str, + builtin_features: &HashMap<String, features::BuiltinFeature>, + opt: &cli::Opt, + git_config: &mut Option<git_config::GitConfig>, + ) -> Option<ProvenancedOptionValue> + where + Self: Sized, + Self: GitConfigGet, + Self: Into<OptionValue>, + { + if let Some(git_config) = git_config { + if let Some(value) = + git_config.get::<Self>(&format!("delta.{}.{}", feature, option_name)) + { + return Some(GitConfigValue(value.into())); + } + } + if let Some(builtin_feature) = builtin_features.get(feature) { + if let Some(value_function) = builtin_feature.get(option_name) { + return Some(value_function(opt, &git_config)); + } + } + return None; + } +} + +impl GetOptionValue for Option<String> {} +impl GetOptionValue for String {} +impl GetOptionValue for bool {} +impl GetOptionValue for f64 {} +impl GetOptionValue for usize {} diff --git a/src/options/mod.rs b/src/options/mod.rs new file mode 100644 index 00000000..d0540f67 --- /dev/null +++ b/src/options/mod.rs @@ -0,0 +1,4 @@ +pub mod get; +pub mod option_value; +pub mod rewrite; +pub mod set; diff --git a/src/options/option_value.rs b/src/options/option_value.rs new file mode 100644 index 00000000..cbe5b383 --- /dev/null +++ b/src/options/option_value.rs @@ -0,0 +1,97 @@ +use crate::config::delta_unreachable; + +/// A value associated with a Delta command-line option name. +pub enum OptionValue { + Boolean(bool), + Float(f64), + OptionString(Option<String>), + String(String), + Int(usize), +} + +/// An OptionValue, tagged according to its provenance/semantics. +pub enum ProvenancedOptionValue { + GitConfigValue(OptionValue), + DefaultValue(OptionValue), +} + +impl From<bool> for OptionValue { + fn from(value: bool) -> Self { + OptionValue::Boolean(value) + } +} + +impl From<OptionValue> for bool { + fn from(value: OptionValue) -> Self { + match value { + OptionValue::Boolean(value) => value, + _ => delta_unreachable("Error converting OptionValue to bool."), + } + } +} + +impl From<f64> for OptionValue { + fn from(value: f64) -> Self { + OptionValue::Float(value) + } +} + +impl From<OptionValue> for f64 { + fn from(value: OptionValue) -> Self { + match value { + OptionValue::Float(value) => value, + _ => delta_unreachable("Error converting OptionValue to f64."), + } + } +} + +impl From<Option<String>> for OptionValue { + fn from(value: Option<String>) -> Self { + OptionValue::OptionString(value) + } +} + +impl From<OptionValue> for Option<String> { + fn from(value: OptionValue) -> Self { + match value { + OptionValue::OptionString(value) => value, + _ => delta_unreachable("Error converting OptionValue to Option<String>."), + } + } +} + +impl From<String> for OptionValue { + fn from(value: String) -> Self { + OptionValue::String(value) + } +} + +impl From<&str> for OptionValue { + fn from(value: &str) -> Self { + value.to_string().into() + } +} + +impl From<OptionValue> for String { + fn from(value: OptionValue) -> Self { + match value { + OptionValue::String(value) => value, + _ => delta_unreachable("Error converting OptionValue to String."), + } + } +} + +impl From<usize> for OptionValue { + fn from(value: usize) -> Self { + OptionValue::Int(value) + } +} + +impl From<OptionValue> for usize { + fn from(value: OptionValue) -> Self { + match value { + OptionValue::Int(value) => value, + _ => delta_unreachable("Error converting OptionValue to usize."), + } + } +} diff --git a/src/options/rewrite.rs b/src/options/rewrite.rs new file mode 100644 index 00000000..076298f7 --- /dev/null +++ b/src/options/rewrite.rs @@ -0,0 +1,231 @@ +/// This module applies rewrite rules to the command line options, in order to +/// 1. Express deprecated usages in the new non-deprecated form +/// 2. Implement options such as --raw which are defined to be equivalent to some set of +/// other options. +use std::process; + +use structopt::clap; + +use crate::cli; +use crate::config::user_supplied_option; + +pub fn apply_rewrite_rules(opt: &mut cli::Opt, arg_matches: &clap::ArgMatches) { + rewrite_style_strings_to_honor_deprecated_minus_plus_options(opt); + rewrite_options_to_implement_deprecated_commit_and_file_style_box_option(opt); + rewrite_options_to_implement_deprecated_hunk_style_option(opt); + rewrite_options_to_implement_deprecated_theme_option(opt, arg_matches); +} + +/// Honor deprecated --theme +fn rewrite_options_to_implement_deprecated_theme_option( + opt: &mut cli::Opt, + arg_matches: &clap::ArgMatches, +) { + if user_supplied_option("deprecated-theme", arg_matches) { + if let Some(syntax_theme) = opt.deprecated_theme.as_ref() { + opt.syntax_theme = Some(syntax_theme.to_string()); + } + } +} + +/// Honor deprecated arguments by rewriting the canonical --*-style arguments if appropriate. +// TODO: How to avoid repeating the default values for style options here and in +// the structopt definition? +fn rewrite_style_strings_to_honor_deprecated_minus_plus_options(opt: &mut cli::Opt) { + // If --highlight-removed was passed then we should set minus and minus emph foreground to + // "syntax", if they are still at their default values. + let deprecated_minus_foreground_arg = if opt.deprecated_highlight_minus_lines { + Some("syntax") + } else { + None + }; + + if let Some(rewritten) = _get_rewritten_minus_plus_style_string( + &opt.minus_style, + ("normal", "auto"), + ( + deprecated_minus_foreground_arg, + opt.deprecated_minus_background_color.as_deref(), + ), + "minus", + ) { + opt.minus_style = rewritten.to_string(); + } + if let Some(rewritten) = _get_rewritten_minus_plus_style_string( + &opt.minus_emph_style, + ("normal", "auto"), + ( + deprecated_minus_foreground_arg, + opt.deprecated_minus_emph_background_color.as_deref(), + ), + "minus-emph", + ) { + opt.minus_emph_style = rewritten.to_string(); + } + if let Some(rewritten) = _get_rewritten_minus_plus_style_string( + &opt.plus_style, + ("syntax", "auto"), + (None, opt.deprecated_plus_background_color.as_deref()), + "plus", + ) { + opt.plus_style = rewritten.to_string(); + } + if let Some(rewritten) = _get_rewritten_minus_plus_style_string( + &opt.plus_emph_style, + ("syntax", "auto"), + (None, opt.deprecated_plus_emph_background_color.as_deref()), + "plus-emph", + ) { + opt.plus_emph_style = rewritten.to_string(); + } +} + +/// For backwards-compatibility, --{commit,file}-style box means --element-decoration-style 'box ul'. +fn rewrite_options_to_implement_deprecated_commit_and_file_style_box_option(opt: &mut cli::Opt) { + if &opt.commit_style == "box" { + opt.commit_decoration_style = format!("box ul {}", opt.commit_decoration_style); + opt.commit_style.clear(); + } + if &opt.file_style == "box" { + opt.file_decoration_style = format!("box ul {}", opt.file_decoration_style); + opt.file_style.clear(); + } +} + +fn rewrite_options_to_implement_deprecated_hunk_style_option(opt: &mut cli::Opt) { + // Examples of how --hunk-style was originally used are + // --hunk-style box => --hunk-header-decoration-style box + // --hunk-style underline => --hunk-header-decoration-style underline + // --hunk-style plain => --hunk-header-decoration-style '' + if opt.deprecated_hunk_style.is_some() { + // As in the other cases, we only honor the deprecated option if the replacement option has + // apparently been left at its default value. + let hunk_header_decoration_default = "blue box"; + if opt.hunk_header_decoration_style != hunk_header_decoration_default { + eprintln!( + "Deprecated option --hunk-style cannot be used with --hunk-header-decoration-style. \ + Use --hunk-header-decoration-style."); + process::exit(1); + } + match opt.deprecated_hunk_style.as_deref().map(str::to_lowercase) { + Some(attr) if attr == "plain" => opt.hunk_header_decoration_style = "".to_string(), + Some(attr) if attr == "" => {} + Some(attr) => opt.hunk_header_decoration_style = attr, + None => {} + } + opt.deprecated_hunk_style = None; + } +} + +fn _get_rewritten_commit_file_hunk_header_style_string( + style_default_pair: (&str, Option<&str>), + deprecated_args_style_pair: (Option<&str>, Option<&str>), +) -> Option<String> { + let format_style = |pair: (&str, Option<&str>)| { + format!( + "{}{}", + pair.0, + match pair.1 { + Some(s) => format!(" {}", s), + None => "".to_string(), + } + ) + }; + match deprecated_args_style_pair { + (None, None) => None, + deprecated_args_style_pair => Some(format_style(( + deprecated_args_style_pair.0.unwrap_or(style_default_pair.0), + match deprecated_args_style_pair.1 { + Some(s) => Some(s), + None => style_default_pair.1, + }, + ))), + } +} + +fn _get_rewritten_minus_plus_style_string( + style: &str, + style_default_pair: (&str, &str), + deprecated_args_style_pair: (Option<&str>, Option<&str>), + element_name: &str, +) -> Option<String> { + let format_style = |pair: (&str, &str)| format!("{} {}", pair.0, pair.1); + match (style, deprecated_args_style_pair) { + (_, (None, None)) => None, // no rewrite + (style, deprecated_args_style_pair) if style == format_style(style_default_pair) => { + // TODO: We allow the deprecated argument values to have effect if + // the style argument value is equal to its default value. This is + // non-ideal, because the user may have explicitly supplied the + // style argument (i.e. it might just happen to equal the default). + Some(format_style(( + deprecated_args_style_pair.0.unwrap_or(style_default_pair.0), + deprecated_args_style_pair.1.unwrap_or(style_default_pair.1), + ))) + } + (_, (_, Some(_))) => { + eprintln!( + "--{name}-color cannot be used with --{name}-style. \ + Use --{name}-style=\"fg bg attr1 attr2 ...\" to set \ + foreground color, background color, and style attributes. \ + --{name}-color can only be used to set the background color. \ + (It is still available for backwards-compatibility.)", + name = element_name, + ); + process::exit(1); + } + (_, (Some(_), None)) => { + eprintln!( + "Deprecated option --highlight-removed cannot be used with \ + --{name}-style. Use --{name}-style=\"fg bg attr1 attr2 ...\" \ + to set foreground color, background color, and style \ + attributes.", + name = element_name, + ); + process::exit(1); + } + } +} + +#[cfg(test)] +mod tests { + use std::ffi::OsString; + + use structopt::{clap, StructOpt}; + + use crate::cli; + use crate::options::rewrite::apply_rewrite_rules; + + #[test] + fn test_default_is_stable_under_rewrites() { + let mut opt = cli::Opt::from_iter(Vec::<OsString>::new()); + let before = opt.clone(); + + apply_rewrite_rules(&mut opt, &clap::ArgMatches::new()); + + assert_eq!(opt, before); + } + + /// Since --hunk-header-decoration-style is at its default value of "box", + /// the deprecated option is allowed to overwrite it. + #[test] + fn test_deprecated_hunk_style_is_rewritten() { + let mut opt = cli::Opt::from_iter(Vec::<OsString>::new()); + opt.deprecated_hunk_style = Some("underline".to_string()); + let default = "blue box"; + assert_eq!(opt.hunk_header_decoration_style, default); + apply_rewrite_rules(&mut opt, &clap::ArgMatches::new()); + assert_eq!(opt.deprecated_hunk_style, None); + assert_eq!(opt.hunk_header_decoration_style, "underline"); + } + + #[test] + fn test_deprecated_hunk_style_is_not_rewritten() { + let mut opt = cli::Opt::from_iter(Vec::<OsString>::new()); + opt.deprecated_hunk_style = Some("".to_string()); + let default = "blue box"; + assert_eq!(opt.hunk_header_decoration_style, default); + apply_rewrite_rules(&mut opt, &clap::ArgMatches::new()); + assert_eq!(opt.deprecated_hunk_style, None); + assert_eq!(opt.hunk_header_decoration_style, default); + } +} diff --git a/src/options/set.rs b/src/options/set.rs new file mode 100644 index 00000000..c17023b9 --- /dev/null +++ b/src/options/set.rs @@ -0,0 +1,284 @@ +use std::collections::{HashSet, VecDeque}; + +use structopt::clap; + +use crate::cli; +use crate::config; + +use crate::features; +use crate::git_config; + +macro_rules! set_options { + ([$( ($option_name:expr, $field_ident:ident) ),* ], + $opt:expr, $builtin_features:expr, $git_config:expr, $arg_matches:expr) => { + let mut option_names = HashSet::new(); + $( + if !$crate::config::user_supplied_option($option_name, $arg_matches) { + if let Some(value) = $crate::options::get::get_option_value( + $option_name, + &$builtin_features, + $opt, + $git_config + ) { + $opt.$field_ident = value; + } + }; + option_names.insert($option_name); + )* + option_names.extend(&[ + "diff-highlight", // Does not exist as a flag on config + "diff-so-fancy", // Does not exist as a flag on config + "features", + "no-gitconfig", + ]); + let expected_option_names = $crate::cli::Opt::get_option_or_flag_names(); + if option_names != expected_option_names { + $crate::config::delta_unreachable( + &format!("Error processing options.\nUnhandled names: {:?}\nInvalid names: {:?}.\n", + &expected_option_names - &option_names, + &option_names - &expected_option_names)); + } + }; +} + +pub fn set_options( + opt: &mut cli::Opt, + git_config: &mut Option<git_config::GitConfig>, + arg_matches: &clap::ArgMatches, +) { + if let Some(git_config) = git_config { + if opt.no_gitconfig { + git_config.enabled = false; + } + } + let builtin_features = features::make_builtin_features(); + opt.features = gather_features( + opt, + builtin_features.keys().into_iter().collect(), + git_config, + ); + + // Handle options which default to an arbitrary git config value. + // TODO: incorporate this logic into the set_options macro. + if !config::user_supplied_option("whitespace-error-style", arg_matches) { + opt.whitespace_error_style = if let Some(git_config) = git_config { + git_config.get::<String>("color.diff.whitespace") + } else { + None + } + .unwrap_or_else(|| "magenta reverse".to_string()) + } + + set_options!( + [ + ("24-bit-color", true_color), + ("color-only", color_only), + ("commit-decoration-style", commit_decoration_style), + ("commit-style", commit_style), + ("dark", dark), + ("file-added-label", file_added_label), + ("file-decoration-style", file_decoration_style), + ("file-modified-label", file_modified_label), + ("file-removed-label", file_removed_label), + ("file-renamed-label", file_renamed_label), + ("file-style", file_style), + ("hunk-header-decoration-style", hunk_header_decoration_style), + ("hunk-header-style", hunk_header_style), + ("keep-plus-minus-markers", keep_plus_minus_markers), + ("light", light), + ("max-line-distance", max_line_distance), + // Hack: minus-style must come before minus-*emph-style because the latter default + // dynamically to the value of the former. + ("minus-style", minus_style), + ("minus-emph-style", minus_emph_style), + ( + "minus-empty-line-marker-style", + minus_empty_line_marker_style + ), + ("minus-non-emph-style", minus_non_emph_style), + ("minus-non-emph-style", minus_non_emph_style), + ("navigate", navigate), + ("line-numbers", line_numbers), + ("line-numbers-left-format", line_numbers_left_format), + ("line-numbers-left-style", line_numbers_left_style), + ("line-numbers-minus-style", line_numbers_minus_style), + ("line-numbers-plus-style", line_numbers_plus_style), + ("line-numbers-right-format", line_numbers_right_format), + ("line-numbers-right-style", line_numbers_right_style), + ("line-numbers-zero-style", line_numbers_zero_style), + ("paging", paging_mode), + // Hack: plus-style must come before plus-*emph-style because the latter default + // dynamically to the value of the former. + ("plus-style", plus_style), + ("plus-emph-style", plus_emph_style), + ("plus-empty-line-marker-style", plus_empty_line_marker_style), + ("plus-non-emph-style", plus_non_emph_style), + ("raw", raw), + ("syntax-theme", syntax_theme), + ("tabs", tab_width), + ("whitespace-error-style", whitespace_error_style), + ("width", width), + ("word-diff-regex", tokenization_regex), + ("zero-style", zero_style) + ], + opt, + builtin_features, + git_config, + arg_matches + ); +} + +/// Features are processed differently from all other options. The role of this function is to +/// collect all configuration related to features and summarize it as a single list +/// (space-separated string) of enabled features. The list is arranged in order of increasing +/// priority in the sense that, when searching for a option value, one starts at the right-hand end +/// and moves leftward, examining each feature in turn until a feature that associates a value with +/// the option name is encountered. This search is documented in +/// `get_option_value::get_option_value`. +/// +/// The feature list comprises features deriving from the following sources, listed in order of +/// decreasing priority: +/// +/// 1. Suppose the command-line has `--features "a b"`. Then +/// - `b`, followed by b's "ordered descendents" +/// - `a`, followed by a's "ordered descendents" +/// +/// 2. Suppose the command line enables two builtin features via `--navigate --diff-so-fancy`. Then +/// - `diff-so-fancy` +/// - `navigate` +/// +/// 3. Suppose the main [delta] section has `features = d e`. Then +/// - `e`, followed by e's "ordered descendents" +/// - `d`, followed by d's "ordered descendents" +/// +/// 4. Suppose the main [delta] section has `diff-highlight = true` followed by `raw = true`. +/// Then +/// - `diff-highlight` +/// - `raw` +/// +/// The "ordered descendents" of a feature `f` is a list of features obtained via a pre-order +/// traversal of the feature tree rooted at `f`. This tree arises because it is allowed for a +/// feature to contain a (key, value) pair that itself enables features. +/// +/// If a feature has already been included at higher priority, and is encountered again, it is +/// ignored. +/// +/// Thus, for example: +/// +/// delta --features "my-navigate-settings" --navigate => "navigate my-navigate-settings" +/// +/// In the following configuration, the feature names indicate their priority, with `a` having +/// highest priority: +/// +/// delta --g --features "d a" +/// +/// [delta "a"] +/// features = c b +/// +/// [delta "d"] +/// features = f e +fn gather_features<'a>( + opt: &cli::Opt, + builtin_feature_names: Vec<&String>, + git_config: &Option<git_config::GitConfig>, +) -> String { + let mut features = VecDeque::new(); + + // Gather features from command line. + if let Some(git_config) = git_config { + for feature in split_feature_string(&opt.features.to_lowercase()) { + gather_features_recursively(feature, &mut features, &builtin_feature_names, git_config); + } + } else { + for feature in split_feature_string(&opt.features.to_lowercase()) { + features.push_front(feature.to_string()); + } + } + + // Gather builtin feature flags supplied on command line. + // TODO: Iterate over programatically-obtained names of builtin features. + if opt.raw { + features.push_front("raw".to_string()); + } + if opt.color_only { + features.push_front("color-only".to_string()); + } + if opt.diff_highlight { + features.push_front("diff-highlight".to_string()); + } + if opt.diff_so_fancy { + features.push_front("diff-so-fancy".to_string()); + } + if opt.line_numbers { + features.push_front("line-numbers".to_string()); + } + if opt.navigate { + features.push_front("navigate".to_string()); + } + + if let Some(git_config) = git_config { + // Gather features from [delta] section if --features was not passed. + if opt.features.is_empty() { + if let Some(feature_string) = git_config.get::<String>(&format!("delta.features")) { + for feature in split_feature_string(&feature_string.to_lowercase()) { + gather_features_recursively( + feature, + &mut features, + &builtin_feature_names, + git_config, + ) + } + } + } + // Always gather builtin feature flags from [delta] section. + gather_builtin_features("delta", &mut features, &builtin_feature_names, git_config); + } + + Vec::<String>::from(features).join(" ") +} + +fn gather_features_recursively<'a>( + feature: &str, + features: &mut VecDeque<String>, + builtin_feature_names: &Vec<&String>, + git_config: &git_config::GitConfig, +) { + features.push_front(feature.to_string()); + if let Some(child_features) = git_config.get::<String>(&format!("delta.{}.features", feature)) { + for child_feature in split_feature_string(&child_features) { + if !features.contains(&child_feature.to_string()) { + gather_features_recursively( + child_feature, + features, + builtin_feature_names, + git_config, + ) + } + } + } + gather_builtin_features( + &format!("delta.{}", feature), + features, + builtin_feature_names, + git_config, + ); +} + +fn gather_builtin_features<'a>( + git_config_key: &str, + features: &mut VecDeque<String>, + builtin_feature_names: &Vec<&String>, + git_config: &git_config::GitConfig, +) { + for feature in builtin_feature_names { + if let Some(value) = git_config.get::<bool>(&format!("{}.{}", git_config_key, feature)) { + if value { + features.push_front(feature.to_string()); + } + } + } +} + +fn split_feature_string(features: &str) -> impl Iterator<Item = &str> { + features.split_whitespace().rev() +} |