diff options
author | Dan Davison <dandavison7@gmail.com> | 2021-08-29 16:52:48 -0400 |
---|---|---|
committer | Dan Davison <dandavison7@gmail.com> | 2021-08-29 22:51:06 -0400 |
commit | 97203ce2338a54b62080116f17b9b928279ae8c8 (patch) | |
tree | 09bbc096f9fba24fe7b9045f3aa459fce20dd5ea /src/handlers | |
parent | 93aabce9490d566d184579a0f9a182fd3cbf1d48 (diff) |
Refactor: state machine handlers module
Diffstat (limited to 'src/handlers')
-rw-r--r-- | src/handlers/blame.rs | 181 | ||||
-rw-r--r-- | src/handlers/commit_meta.rs | 59 | ||||
-rw-r--r-- | src/handlers/diff_stat.rs | 102 | ||||
-rw-r--r-- | src/handlers/draw.rs | 300 | ||||
-rw-r--r-- | src/handlers/file_meta.rs | 536 | ||||
-rw-r--r-- | src/handlers/file_meta_diff.rs | 20 | ||||
-rw-r--r-- | src/handlers/file_meta_misc.rs | 16 | ||||
-rw-r--r-- | src/handlers/hunk.rs | 102 | ||||
-rw-r--r-- | src/handlers/hunk_header.rs | 357 | ||||
-rw-r--r-- | src/handlers/mod.rs | 48 | ||||
-rw-r--r-- | src/handlers/submodule.rs | 62 |
11 files changed, 1783 insertions, 0 deletions
diff --git a/src/handlers/blame.rs b/src/handlers/blame.rs new file mode 100644 index 00000000..d7486303 --- /dev/null +++ b/src/handlers/blame.rs @@ -0,0 +1,181 @@ +use chrono::{DateTime, FixedOffset}; +use lazy_static::lazy_static; +use regex::Regex; + +use crate::color; +use crate::config; +use crate::delta::{self, State, StateMachine}; +use crate::format; +use crate::style::Style; + +impl<'a> StateMachine<'a> { + /// If this is a line of git blame output then render it accordingly. If + /// this is the first blame line, then set the syntax-highlighter language + /// according to delta.default-language. + pub fn handle_blame_line(&mut self) -> std::io::Result<bool> { + let mut handled_line = false; + self.painter.emit()?; + if matches!(self.state, State::Unknown | State::Blame(_)) { + if let Some(blame) = + parse_git_blame_line(&self.line, &self.config.blame_timestamp_format) + { + // Determine color for this line + let color = if let Some(color) = self.blame_commit_colors.get(blame.commit) { + color + } else { + let n_commits = self.blame_commit_colors.len(); + let n_colors = self.config.blame_palette.len(); + let new_color = &self.config.blame_palette[(n_commits + 1) % n_colors]; + self.blame_commit_colors + .insert(blame.commit.to_owned(), new_color.to_owned()); + new_color + }; + let mut style = Style::from_colors(None, color::parse_color(color, true)); + style.is_syntax_highlighted = true; + + // Construct commit metadata, paint, and emit + let format_data = format::parse_line_number_format( + &self.config.blame_format, + &*BLAME_PLACEHOLDER_REGEX, + ); + write!( + self.painter.writer, + "{}", + style.paint(format_blame_metadata(&format_data, &blame, self.config)) + )?; + + // Emit syntax-highlighted code + if matches!(self.state, State::Unknown) { + if let Some(lang) = self.config.default_language.as_ref() { + self.painter.set_syntax(Some(lang)); + self.painter.set_highlighter(); + } + self.state = State::Blame(blame.commit.to_owned()); + } + self.painter.syntax_highlight_and_paint_line( + blame.code, + style, + self.state.clone(), + true, + ); + handled_line = true + } + } + Ok(handled_line) + } +} + +#[derive(Debug)] +pub struct BlameLine<'a> { + pub commit: &'a str, + pub author: &'a str, + pub time: DateTime<FixedOffset>, + pub line_number: usize, + pub code: &'a str, +} + +// E.g. +//ea82f2d0 (Dan Davison 2021-08-22 18:20:19 -0700 120) let mut handled_line = self.handle_commit_meta_header_line()? + +lazy_static! { + static ref BLAME_LINE_REGEX: Regex = Regex::new( + r"(?x) +^ +( + [0-9a-f]{8} # commit hash +) +[\ ] +\( # open ( +( + [^\ ].*[^\ ] # author name +) +[\ ]+ +( # timestamp + [0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}\ [-+][0-9]{4} +) +[\ ]+ +( + [0-9]+ # line number +) +\) # close ) +( + .* # code, with leading space +) +$ +" + ) + .unwrap(); +} + +pub fn parse_git_blame_line<'a>(line: &'a str, timestamp_format: &str) -> Option<BlameLine<'a>> { + if let Some(caps) = BLAME_LINE_REGEX.captures(line) { + let commit = caps.get(1).unwrap().as_str(); + let author = caps.get(2).unwrap().as_str(); + let timestamp = caps.get(3).unwrap().as_str(); + if let Ok(time) = DateTime::parse_from_str(timestamp, timestamp_format) { + let line_number_str = caps.get(4).unwrap().as_str(); + if let Ok(line_number) = line_number_str.parse::<usize>() { + let code = caps.get(5).unwrap().as_str(); + Some(BlameLine { + commit, + author, + time, + line_number, + code, + }) + } else { + None + } + } else { + None + } + } else { + None + } +} + +lazy_static! { + pub static ref BLAME_PLACEHOLDER_REGEX: Regex = + format::make_placeholder_regex(&["timestamp", "author", "commit"]); +} + +pub fn format_blame_metadata( + format_data: &[format::FormatStringPlaceholderData], + blame: &BlameLine, + config: &config::Config, +) -> String { + let mut s = String::new(); + let mut suffix = ""; + for placeholder in format_data { + s.push_str(placeholder.prefix); + + let alignment_spec = placeholder.alignment_spec.unwrap_or("<"); + let width = placeholder.width.unwrap_or(15); + + let pad = |s| format::pad(s, width, alignment_spec); + match placeholder.placeholder { + Some("timestamp") => s.push_str(&pad( + &chrono_humanize::HumanTime::from(blame.time).to_string() + )), + Some("author") => s.push_str(&pad(blame.author)), + Some("commit") => s.push_str(&pad(&delta::format_raw_line(blame.commit, config))), + None => {} + Some(_) => unreachable!(), + } + suffix = placeholder.suffix; + } + s.push_str(suffix); + s +} + +#[test] +fn test_blame_line_regex() { + for line in &[ + "ea82f2d0 (Dan Davison 2021-08-22 18:20:19 -0700 120) let mut handled_line = self.handle_commit_meta_header_line()?", + "b2257cfa (Dan Davison 2020-07-18 15:34:43 -0400 1) use std::borrow::Cow;" + ] { + let caps = BLAME_LINE_REGEX.captures(line); + assert!(caps.is_some()); + assert!(parse_git_blame_line(line, "%Y-%m-%d %H:%M:%S %z").is_some()); + } +} diff --git a/src/handlers/commit_meta.rs b/src/handlers/commit_meta.rs new file mode 100644 index 00000000..2ad59bf1 --- /dev/null +++ b/src/handlers/commit_meta.rs @@ -0,0 +1,59 @@ +use std::borrow::Cow; + +use super::draw; +use crate::delta::{State, StateMachine}; +use crate::features; + +impl<'a> StateMachine<'a> { + #[inline] + fn test_commit_meta_header_line(&self) -> bool { + self.config.commit_regex.is_match(&self.line) + } + + pub fn handle_commit_meta_header_line(&mut self) -> std::io::Result<bool> { + if !self.test_commit_meta_header_line() { + return Ok(false); + } + let mut handled_line = false; + self.painter.paint_buffered_minus_and_plus_lines(); + self.state = State::CommitMeta; + if self.should_handle() { + self.painter.emit()?; + self._handle_commit_meta_header_line()?; + handled_line = true + } + Ok(handled_line) + } + + fn _handle_commit_meta_header_line(&mut self) -> std::io::Result<()> { + if self.config.commit_style.is_omitted { + return Ok(()); + } + let (mut draw_fn, pad, decoration_ansi_term_style) = + draw::get_draw_function(self.config.commit_style.decoration_style); + let (formatted_line, formatted_raw_line) = if self.config.hyperlinks { + ( + features::hyperlinks::format_commit_line_with_osc8_commit_hyperlink( + &self.line, + self.config, + ), + features::hyperlinks::format_commit_line_with_osc8_commit_hyperlink( + &self.raw_line, + self.config, + ), + ) + } else { + (Cow::from(&self.line), Cow::from(&self.raw_line)) + }; + + draw_fn( + self.painter.writer, + &format!("{}{}", formatted_line, if pad { " " } else { "" }), + &format!("{}{}", formatted_raw_line, if pad { " " } else { "" }), + &self.config.decorations_width, + self.config.commit_style, + decoration_ansi_term_style, + )?; + Ok(()) + } +} diff --git a/src/handlers/diff_stat.rs b/src/handlers/diff_stat.rs new file mode 100644 index 00000000..39a2130f --- /dev/null +++ b/src/handlers/diff_stat.rs @@ -0,0 +1,102 @@ +use lazy_static::lazy_static; +use regex::Regex; + +use crate::delta::{State, StateMachine}; + +impl<'a> StateMachine<'a> { + #[inline] + fn test_diff_stat_line(&self) -> bool { + (self.state == State::CommitMeta || self.state == State::Unknown) + && self.line.starts_with(' ') + } + + pub fn handle_diff_stat_line(&mut self) -> std::io::Result<bool> { + if !self.test_diff_stat_line() { + return Ok(false); + } + let mut handled_line = false; + if self.config.relative_paths { + if let Some(cwd) = self.config.cwd_relative_to_repo_root.as_deref() { + if let Some(replacement_line) = relativize_path_in_diff_stat_line( + &self.raw_line, + cwd, + self.config.diff_stat_align_width, + ) { + self.painter.emit()?; + writeln!(self.painter.writer, "{}", replacement_line)?; + handled_line = true + } + } + } + Ok(handled_line) + } +} + +// A regex to capture the path, and the content from the pipe onwards, in lines +// like these: +// " src/delta.rs | 14 ++++++++++----" +// " src/config.rs | 2 ++" +lazy_static! { + static ref DIFF_STAT_LINE_REGEX: Regex = + Regex::new(r" ([^\| ][^\|]+[^\| ]) +(\| +[0-9]+ .+)").unwrap(); +} + +pub fn relativize_path_in_diff_stat_line( + line: &str, + cwd_relative_to_repo_root: &str, + diff_stat_align_width: usize, +) -> Option<String> { + if let Some(caps) = DIFF_STAT_LINE_REGEX.captures(line) { + let path_relative_to_repo_root = caps.get(1).unwrap().as_str(); + if let Some(relative_path) = + pathdiff::diff_paths(path_relative_to_repo_root, cwd_relative_to_repo_root) + { + if let Some(relative_path) = relative_path.to_str() { + let suffix = caps.get(2).unwrap().as_str(); + let pad_width = diff_stat_align_width.saturating_sub(relative_path.len()); + let padding = " ".repeat(pad_width); + return Some(format!(" {}{}{}", relative_path, padding, suffix)); + } + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_diff_stat_line_regex_1() { + let caps = DIFF_STAT_LINE_REGEX.captures(" src/delta.rs | 14 ++++++++++----"); + assert!(caps.is_some()); + let caps = caps.unwrap(); + assert_eq!(caps.get(1).unwrap().as_str(), "src/delta.rs"); + assert_eq!(caps.get(2).unwrap().as_str(), "| 14 ++++++++++----"); + } + + #[test] + fn test_diff_stat_line_regex_2() { + let caps = DIFF_STAT_LINE_REGEX.captures(" src/config.rs | 2 ++"); + assert!(caps.is_some()); + let caps = caps.unwrap(); + assert_eq!(caps.get(1).unwrap().as_str(), "src/config.rs"); + assert_eq!(caps.get(2).unwrap().as_str(), "| 2 ++"); + } + + #[test] + fn test_relative_path() { + for (path, cwd_relative_to_repo_root, expected) in &[ + ("file.rs", "", "file.rs"), + ("file.rs", "a/", "../file.rs"), + ("a/file.rs", "a/", "file.rs"), + ("a/b/file.rs", "a", "b/file.rs"), + ("c/d/file.rs", "a/b/", "../../c/d/file.rs"), + ] { + assert_eq!( + pathdiff::diff_paths(path, cwd_relative_to_repo_root), + Some(expected.into()) + ) + } + } +} diff --git a/src/handlers/draw.rs b/src/handlers/draw.rs new file mode 100644 index 00000000..d7edd8cd --- /dev/null +++ b/src/handlers/draw.rs @@ -0,0 +1,300 @@ +use std::cmp::max; +use std::io::Write; + +use crate::ansi; +use crate::cli::Width; +use crate::style::{DecorationStyle, Style}; + +pub type DrawFunction = + dyn FnMut(&mut dyn Write, &str, &str, &Width, Style, ansi_term::Style) -> std::io::Result<()>; + +pub fn get_draw_function( + decoration_style: DecorationStyle, +) -> (Box<DrawFunction>, bool, ansi_term::Style) { + match decoration_style { + DecorationStyle::Box(style) => (Box::new(write_boxed), true, style), + DecorationStyle::BoxWithUnderline(style) => { + (Box::new(write_boxed_with_underline), true, style) + } + DecorationStyle::BoxWithOverline(style) => { + // TODO: not implemented + (Box::new(write_boxed), true, style) + } + DecorationStyle::BoxWithUnderOverline(style) => { + // TODO: not implemented + (Box::new(write_boxed), true, style) + } + DecorationStyle::Underline(style) => (Box::new(write_underlined), false, style), + DecorationStyle::Overline(style) => (Box::new(write_overlined), false, style), + DecorationStyle::UnderOverline(style) => (Box::new(write_underoverlined), false, style), + DecorationStyle::NoDecoration => ( + Box::new(write_no_decoration), + false, + ansi_term::Style::new(), + ), + } +} + +fn write_no_decoration( + writer: &mut dyn Write, + text: &str, + raw_text: &str, + _line_width: &Width, // ignored + text_style: Style, + _decoration_style: ansi_term::Style, +) -> std::io::Result<()> { + if text_style.is_raw { + writeln!(writer, "{}", raw_text)?; + } else { + writeln!(writer, "{}", text_style.paint(text))?; + } + Ok(()) +} + +/// Write text to stream, surrounded by a box, leaving the cursor just +/// beyond the bottom right corner. +fn write_boxed( + writer: &mut dyn Write, + text: &str, + raw_text: &str, + _line_width: &Width, // ignored + text_style: Style, + decoration_style: ansi_term::Style, +) -> std::io::Result<()> { + let up_left = if decoration_style.is_bold { + box_drawing::heavy::UP_LEFT + } else { + box_drawing::light::UP_LEFT + }; + let box_width = ansi::measure_text_width(text); + write_boxed_partial( + writer, + text, + raw_text, + box_width, + text_style, + decoration_style, + )?; + writeln!(writer, "{}", decoration_style.paint(up_left))?; + Ok(()) +} + +/// Write text to stream, surrounded by a box, and extend a line from +/// the bottom right corner. +fn write_boxed_with_underline( + writer: &mut dyn Write, + text: &str, + raw_text: &str, + line_width: &Width, + text_style: Style, + decoration_style: ansi_term::Style, +) -> std::io::Result<()> { + let box_width = ansi::measure_text_width(text); + write_boxed_with_horizontal_whisker( + writer, + text, + raw_text, + box_width, + text_style, + decoration_style, + )?; + let line_width = match *line_width { + Width::Fixed(n) => n, + Width::Variable => box_width, + }; + write_horizontal_line( + writer, + if line_width > box_width { + line_width - box_width - 1 + } else { + 0 + }, + text_style, + decoration_style, + )?; + writeln!(writer)?; + Ok(()) +} + +enum UnderOverline { + Under, + Over, + Underover, +} + +fn write_underlined( + writer: &mut dyn Write, + text: &str, + raw_text: &str, + line_width: &Width, + text_style: Style, + decoration_style: ansi_term::Style, +) -> std::io::Result<()> { + _write_under_or_over_lined( + UnderOverline::Under, + writer, + text, + raw_text, + line_width, + text_style, + decoration_style, + ) +} + +fn write_overlined( + writer: &mut dyn Write, + text: &str, + raw_text: &str, + line_width: &Width, + text_style: Style, + decoration_style: ansi_term::Style, +) -> std::io::Result<()> { + _write_under_or_over_lined( + UnderOverline::Over, + writer, + text, + raw_text, + line_width, + text_style, + decoration_style, + ) +} + +fn write_underoverlined( + writer: &mut dyn Write, + text: &str, + raw_text: &str, + line_width: &Width, + text_style: Style, + decoration_style: ansi_term::Style, +) -> std::io::Result<()> { + _write_under_or_over_lined( + UnderOverline::Underover, + writer, + text, + raw_text, + line_width, + text_style, + decoration_style, + ) +} + +fn _write_under_or_over_lined( + underoverline: UnderOverline, + writer: &mut dyn Write, + text: &str, + raw_text: &str, + line_width: &Width, + text_style: Style, + decoration_style: ansi_term::Style, +) -> std::io::Result<()> { + let text_width = ansi::measure_text_width(text); + let line_width = match *line_width { + Width::Fixed(n) => max(n, text_width), + Width::Variable => text_width, + }; + let mut write_line: Box<dyn FnMut(&mut dyn Write) -> std::io::Result<()>> = + Box::new(|writer| { + write_horizontal_line(writer, line_width, text_style, decoration_style)?; + writeln!(writer)?; + Ok(()) + }); + match underoverline { + UnderOverline::Under => {} + _ => write_line(writer)?, + } + if text_style.is_raw { + writeln!(writer, "{}", raw_text)?; + } else { + writeln!(writer, "{}", text_style.paint(text))?; + } + match underoverline { + UnderOverline::Over => {} + _ => write_line(writer)?, + } + Ok(()) +} + +fn write_horizontal_line( + writer: &mut dyn Write, + width: usize, + _text_style: Style, + decoration_style: ansi_term::Style, +) -> std::io::Result<()> { + let horizontal = if decoration_style.is_bold { + box_drawing::heavy::HORIZONTAL + } else { + box_drawing::light::HORIZONTAL + }; + write!( + writer, + "{}", + decoration_style.paint(horizontal.repeat(width)) + ) +} + +fn write_boxed_with_horizontal_whisker( + writer: &mut dyn Write, + text: &str, + raw_text: &str, + box_width: usize, + text_style: Style, + decoration_style: ansi_term::Style, +) -> std::io::Result<()> { + let up_horizontal = if decoration_style.is_bold { + box_drawing::heavy::UP_HORIZONTAL + } else { + box_drawing::light::UP_HORIZONTAL + }; + write_boxed_partial( + writer, + text, + raw_text, + box_width, + text_style, + decoration_style, + )?; + write!(writer, "{}", decoration_style.paint(up_horizontal))?; + Ok(()) +} + +fn write_boxed_partial( + writer: &mut dyn Write, + text: &str, + raw_text: &str, + box_width: usize, + text_style: Style, + decoration_style: ansi_term::Style, +) -> std::io::Result<()> { + let (horizontal, down_left, vertical) = if decoration_style.is_bold { + ( + box_drawing::heavy::HORIZONTAL, + box_drawing::heavy::DOWN_LEFT, + box_drawing::heavy::VERTICAL, + ) + } else { + ( + box_drawing::light::HORIZONTAL, + box_drawing::light::DOWN_LEFT, + box_drawing::light::VERTICAL, + ) + }; + let horizontal_edge = horizontal.repeat(box_width); + writeln!( + writer, + "{}{}", + decoration_style.paint(&horizontal_edge), + decoration_style.paint(down_left), + )?; + if text_style.is_raw { + write!(writer, "{}", raw_text)?; + } else { + write!(writer, "{}", text_style.paint(text))?; + } + write!( + writer, + "{}\n{}", + decoration_style.paint(vertical), + decoration_style.paint(&horizontal_edge), + ) +} diff --git a/src/handlers/file_meta.rs b/src/handlers/file_meta.rs new file mode 100644 index 00000000..82109a91 --- /dev/null +++ b/src/handlers/file_meta.rs @@ -0,0 +1,536 @@ +use std::borrow::Cow; +use std::path::Path; + +use unicode_segmentation::UnicodeSegmentation; + +use super::draw; +use crate::config::Config; +use crate::delta::{Source, State, StateMachine}; +use crate::features; +use crate::paint::Painter; + +// https://git-scm.com/docs/git-config#Documentation/git-config.txt-diffmnemonicPrefix +const DIFF_PREFIXES: [&str; 6] = ["a/", "b/", "c/", "i/", "o/", "w/"]; + +#[derive(Debug, PartialEq)] +pub enum FileEvent { + Change, + Copy, + Rename, + ModeChange(String), + NoEvent, +} + +impl<'a> StateMachine<'a> { + #[inline] + fn test_file_meta_minus_line(&self) -> bool { + (self.state == State::FileMeta || self.source == Source::DiffUnified) + && (self.line.starts_with("--- ") + || self.line.starts_with("rename from ") + || self.line.starts_with("copy from ") + || self.line.starts_with("old mode ")) + } + + pub fn handle_file_meta_minus_line(&mut self) -> std::io::Result<bool> { + if !self.test_file_meta_minus_line() { + return Ok(false); + } + let mut handled_line = false; + + let (path_or_mode, file_event) = parse_file_meta_line( + &self.line, + self.source == Source::GitDiff, + if self.config.relative_paths { + self.config.cwd_relative_to_repo_root.as_deref() + } else { + None + }, + ); + // In the case of ModeChange only, the file path is taken from the diff + // --git line (since that is the only place the file path occurs); + // otherwise it is taken from the --- / +++ line. + self.minus_file = if let FileEvent::ModeChange(_) = &file_event { + get_repeated_file_path_from_diff_line(&self.diff_line).unwrap_or(path_or_mode) + } else { + path_or_mode + }; + self.minus_file_event = file_event; + + if self.source == Source::DiffUnified { + self.state = State::FileMeta; + self.painter + .set_syntax(get_file_extension_from_marker_line(&self.line)); + } else { + self.painter + .set_syntax(get_file_extension_from_file_meta_line_file_path( + &self.minus_file, + )); + } + + // In color_only mode, raw_line's structure shouldn't be changed. + // So it needs to avoid fn _handle_file_meta_header_line + // (it connects the plus_file and minus_file), + // and to call fn handle_generic_file_meta_header_line directly. + if self.config.color_only { + write_generic_file_meta_header_line( + &self.line, + &self.raw_line, + &mut self.painter, + self.config, + )?; + handled_line = true; + } + Ok(handled_line) + } + + #[inline] + fn test_file_meta_plus_line(&self) -> bool { + (self.state == State::FileMeta || self.source == Source::DiffUnified) + && (self.line.starts_with("+++ ") + || self.line.starts_with("rename to ") + || self.line.starts_with("copy to ") + || self.line.starts_with("new mode ")) + } + + pub fn handle_file_meta_plus_line(&mut self) -> std::io::Result<bool> { + if !self.test_file_meta_plus_line() { + return Ok(false); + } + let mut handled_line = false; + let (path_or_mode, file_event) = parse_file_meta_line( + &self.line, + self.source == Source::GitDiff, + if self.config.relative_paths { + self.config.cwd_relative_to_repo_root.as_deref() + } else { + None + }, + ); + // In the case of ModeChange only, the file path is taken from the diff + // --git line (since that is the only place the file path occurs); + // otherwise it is taken from the --- / +++ line. + self.plus_file = if let FileEvent::ModeChange(_) = &file_event { + get_repeated_file_path_from_diff_line(&self.diff_line).unwrap_or(path_or_mode) + } else { + path_or_mode + }; + self.plus_file_event = file_event; + self.painter + .set_syntax(get_file_extension_from_file_meta_line_file_path( + &self.plus_file, + )); + self.current_file_pair = Some((self.minus_file.clone(), self.plus_file.clone())); + + // In color_only mode, raw_line's structure shouldn't be changed. + // So it needs to avoid fn _handle_file_meta_header_line + // (it connects the plus_file and minus_file), + // and to call fn handle_generic_file_meta_header_line directly. + if self.config.color_only { + write_generic_file_meta_header_line( + &self.line, + &self.raw_line, + &mut self.painter, + self.config, + )?; + handled_line = true + } else if self.should_handle() + && self.handled_file_meta_header_line_file_pair != self.current_file_pair + { + self.painter.emit()?; + self._handle_file_meta_header_line(self.source == Source::DiffUnifi |