summaryrefslogtreecommitdiffstats
path: root/src/options
diff options
context:
space:
mode:
authorDan Davison <dandavison7@gmail.com>2020-06-28 12:10:24 -0400
committerDan Davison <dandavison7@gmail.com>2020-06-30 08:00:30 -0400
commitdb25d92cba169359b8cd2bb315742a6d36a0a3dd (patch)
tree3d59ab866adb39bfc9d3c86567d20902952412d8 /src/options
parent38a9e9baf5395f34e4d6d7310929fe2941151103 (diff)
Refactor: options module
Diffstat (limited to 'src/options')
-rw-r--r--src/options/get.rs111
-rw-r--r--src/options/mod.rs4
-rw-r--r--src/options/option_value.rs97
-rw-r--r--src/options/rewrite.rs231
-rw-r--r--src/options/set.rs284
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()
+}