From ecab03237df71387e82ff890b084e36c64979891 Mon Sep 17 00:00:00 2001 From: Peter Hebden Date: Wed, 19 Jul 2023 23:38:50 +0100 Subject: [PATCH] feat: Highlight WS errors based on git/gex config (#45) First checks the gex config, then if nothing is found it falls back on the git config. Close #45 --- CHANGELOG.md | 2 ++ README.md | 1 + src/config.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 2 +- src/status.rs | 71 +++++++++++++++++++++++++++++++++++------- 5 files changed, 148 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a204aae..bf105d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased](https://github.com/Piturnah/gex/compare/v0.5.0...main) ### Added +- Trailing whitespace detection based on setting in either gitconfig or gex config ([#45](https://github.com/Piturnah/gex/issues/45)) + - New config option: `options.ws_error_highlight` - Arbitrary process execution with ! ([#25](https://github.com/Piturnah/gex/issues/25)) ### Changed - Item expansion no longer resets on updating status ([#39](https://github.com/Piturnah/gex/issues/39)) diff --git a/README.md b/README.md index 5ecc78b..e539b2f 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ auto_expand_files = false auto_expand_hunks = true lookahead_lines = 5 truncate_lines = true # `false` is not recommended - see #37 +ws_error_highlight = "new" # override git's diff.wsErrorHighlight ``` ## Versioning diff --git a/src/config.rs b/src/config.rs index 4b7e63f..7e08fc7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ //! Gex configuration. #![allow(clippy::derivable_impls)] -use std::{fs, path::PathBuf}; +use std::{fs, path::PathBuf, str::FromStr}; use anyhow::{Context, Result}; use clap::Parser; @@ -36,6 +36,15 @@ pub struct Options { pub auto_expand_hunks: bool, pub lookahead_lines: usize, pub truncate_lines: bool, + pub ws_error_highlight: WsErrorHighlight, +} + +#[derive(Deserialize, Clone, Copy, Debug)] +#[serde(try_from = "String")] +pub struct WsErrorHighlight { + pub old: bool, + pub new: bool, + pub context: bool, } impl Default for Options { @@ -45,6 +54,7 @@ impl Default for Options { auto_expand_hunks: true, lookahead_lines: 5, truncate_lines: true, + ws_error_highlight: WsErrorHighlight::default(), } } } @@ -79,3 +89,76 @@ impl Config { Ok(Some((config, unused_keys))) } } + +impl WsErrorHighlight { + /// The default value defined by git. + const GIT_DEFAULT: Self = Self { + old: false, + new: true, + context: false, + }; + const NONE: Self = Self { + old: false, + new: false, + context: false, + }; + const ALL: Self = Self { + old: true, + new: true, + context: true, + }; +} + +impl Default for WsErrorHighlight { + /// If none was provided by the gex config, we will look in the git config. If we couldn't get + /// that one then we'll just provide `Self::GIT_DEFAULT`. + fn default() -> Self { + let Ok(Ok(git_config)) = git2::Config::open_default().map(|mut config| config.snapshot()) + else { + return Self::GIT_DEFAULT; + }; + + let Ok(value) = git_config.get_str("diff.wsErrorHighlight") else { + return Self::GIT_DEFAULT; + }; + + Self::from_str(value).unwrap_or(Self::GIT_DEFAULT) + } +} + +// NOTE: If anyone is reading this, do you happen to know why this impl is even needed? Really +// feels like this should be provided by default is `FromStr` is implemented on the type. +impl TryFrom for WsErrorHighlight { + type Error = anyhow::Error; + fn try_from(s: String) -> std::result::Result { + Self::from_str(&s) + } +} + +impl FromStr for WsErrorHighlight { + type Err = anyhow::Error; + /// Highlight whitespace errors in the context, old or new lines of the diff. Multiple values + /// are separated by by comma, none resets previous values, default reset the list to new and + /// all is a shorthand for old,new,context. + /// + /// + fn from_str(s: &str) -> std::result::Result { + let mut result = Self::GIT_DEFAULT; + for opt in s.split(',') { + match opt { + "all" => result = Self::ALL, + "default" => result = Self::GIT_DEFAULT, + "none" => result = Self::NONE, + "old" => result.old = true, + "new" => result.new = true, + "context" => result.context = true, + otherwise => { + return Err(anyhow::Error::msg(format!( + "unrecognised option in `ws_error_highlight`: {otherwise}" + ))) + } + } + } + Ok(result) + } +} diff --git a/src/main.rs b/src/main.rs index 4e569e6..ab8378f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -347,7 +347,7 @@ See https://github.com/Piturnah/gex/issues/13.", MessageType::Error); .context("failed to leave alternate screen")?; process::exit(0); } - KeyCode::Char(c) => cmd.handle_input(c, &mut state, &config)?, + KeyCode::Char(c) => cmd.handle_input(c, &mut state, config)?, _ => {} }, }; diff --git a/src/status.rs b/src/status.rs index 153b8fb..0a02d9c 100644 --- a/src/status.rs +++ b/src/status.rs @@ -1,6 +1,7 @@ //! Module relating to the Status display, including diffs of files. use std::{ + borrow::Cow, fmt, fs, io::{stdout, Read, Write}, process::{Command, Output, Stdio}, @@ -12,7 +13,7 @@ use git2::{ErrorCode::UnbornBranch, Repository}; use nom::{bytes::complete::take_until, IResult}; use crate::{ - config::Options, + config::{Options, CONFIG}, git_process, minibuffer::{MessageType, MiniBuffer}, parse::{self, parse_hunk_new, parse_hunk_old}, @@ -55,23 +56,71 @@ impl fmt::Display for Hunk { ); if self.expanded { + let ws_error_highlight = CONFIG + .get() + .expect("config is initialised at the start of the program") + .options + .ws_error_highlight; for line in lines { - write!( - &mut outbuf, - "\r\n{}{}", - match line.chars().next() { - Some('+') => style::SetForegroundColor(Color::DarkGreen), - Some('-') => style::SetForegroundColor(Color::DarkRed), - _ => style::SetForegroundColor(Color::Reset), - }, - line - )?; + match line.chars().next() { + Some('+') => write!( + &mut outbuf, + "\r\n{}{}", + style::SetForegroundColor(Color::DarkGreen), + if ws_error_highlight.new { + format_trailing_whitespace(line) + } else { + Cow::Borrowed(line) + } + ), + Some('-') => write!( + &mut outbuf, + "\r\n{}{}", + style::SetForegroundColor(Color::DarkRed), + if ws_error_highlight.old { + format_trailing_whitespace(line) + } else { + Cow::Borrowed(line) + } + ), + _ => write!( + &mut outbuf, + "\r\n{}{}", + style::SetForegroundColor(Color::Reset), + if ws_error_highlight.context { + format_trailing_whitespace(line) + } else { + Cow::Borrowed(line) + } + ), + }?; } } write!(f, "{outbuf}") } } +fn format_trailing_whitespace(s: &str) -> Cow<'_, str> { + let count_trailing_whitespace = s + .bytes() + .skip(1) + .rev() + .take_while(|c| c.is_ascii_whitespace()) + .count(); + if count_trailing_whitespace > 0 { + Cow::Owned({ + let mut line = s.to_string(); + line.insert_str( + line.len() - count_trailing_whitespace, + &format!("{}", style::SetBackgroundColor(Color::Red)), + ); + line + }) + } else { + Cow::Borrowed(s) + } +} + impl Hunk { pub const fn new(diff: String, expanded: bool) -> Self { Self { diff, expanded }