From 97203ce2338a54b62080116f17b9b928279ae8c8 Mon Sep 17 00:00:00 2001 From: Dan Davison Date: Sun, 29 Aug 2021 16:52:48 -0400 Subject: Refactor: state machine handlers module --- src/blame.rs | 122 --------- src/delta.rs | 606 ++--------------------------------------- src/draw.rs | 300 -------------------- src/handlers/blame.rs | 181 ++++++++++++ src/handlers/commit_meta.rs | 59 ++++ src/handlers/diff_stat.rs | 102 +++++++ src/handlers/draw.rs | 300 ++++++++++++++++++++ src/handlers/file_meta.rs | 536 ++++++++++++++++++++++++++++++++++++ src/handlers/file_meta_diff.rs | 20 ++ src/handlers/file_meta_misc.rs | 16 ++ src/handlers/hunk.rs | 102 +++++++ src/handlers/hunk_header.rs | 357 ++++++++++++++++++++++++ src/handlers/mod.rs | 48 ++++ src/handlers/submodule.rs | 62 +++++ src/hunk_header.rs | 208 -------------- src/main.rs | 5 +- src/parse.rs | 536 ------------------------------------ 17 files changed, 1805 insertions(+), 1755 deletions(-) delete mode 100644 src/blame.rs delete mode 100644 src/draw.rs create mode 100644 src/handlers/blame.rs create mode 100644 src/handlers/commit_meta.rs create mode 100644 src/handlers/diff_stat.rs create mode 100644 src/handlers/draw.rs create mode 100644 src/handlers/file_meta.rs create mode 100644 src/handlers/file_meta_diff.rs create mode 100644 src/handlers/file_meta_misc.rs create mode 100644 src/handlers/hunk.rs create mode 100644 src/handlers/hunk_header.rs create mode 100644 src/handlers/mod.rs create mode 100644 src/handlers/submodule.rs delete mode 100644 src/hunk_header.rs delete mode 100644 src/parse.rs (limited to 'src') diff --git a/src/blame.rs b/src/blame.rs deleted file mode 100644 index bf15fdb1..00000000 --- a/src/blame.rs +++ /dev/null @@ -1,122 +0,0 @@ -use chrono::{DateTime, FixedOffset}; -use lazy_static::lazy_static; -use regex::Regex; - -use crate::config; -use crate::delta; -use crate::format; - -#[derive(Debug)] -pub struct BlameLine<'a> { - pub commit: &'a str, - pub author: &'a str, - pub time: DateTime, - 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> { - 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::() { - 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/delta.rs b/src/delta.rs index adec6823..54160712 100644 --- a/src/delta.rs +++ b/src/delta.rs @@ -4,20 +4,13 @@ use std::io::BufRead; use std::io::Write; use bytelines::ByteLines; -use unicode_segmentation::UnicodeSegmentation; use crate::ansi; -use crate::blame; -use crate::cli; -use crate::color; use crate::config::Config; -use crate::draw; use crate::features; -use crate::format; -use crate::hunk_header; +use crate::handlers; use crate::paint::Painter; -use crate::parse; -use crate::style::{self, DecorationStyle, Style}; +use crate::style::DecorationStyle; #[derive(Clone, Debug, PartialEq)] pub enum State { @@ -40,15 +33,6 @@ pub enum Source { Unknown, } -impl State { - fn is_in_hunk(&self) -> bool { - matches!( - *self, - State::HunkHeader(_, _) | State::HunkZero | State::HunkMinus(_) | State::HunkPlus(_) - ) - } -} - // Possible transitions, with actions on entry: // // @@ -61,27 +45,27 @@ impl State { // | HunkMinus | flush, emit | flush, emit | flush, emit | flush, emit | push | push | // | HunkPlus | flush, emit | flush, emit | flush, emit | flush, emit | flush, push | push | -struct StateMachine<'a> { - line: String, - raw_line: String, - state: State, - source: Source, - minus_file: String, - plus_file: String, - minus_file_event: parse::FileEvent, - plus_file_event: parse::FileEvent, - diff_line: String, - painter: Painter<'a>, - config: &'a Config, +pub struct StateMachine<'a> { + pub line: String, + pub raw_line: String, + pub state: State, + pub source: Source, + pub minus_file: String, + pub plus_file: String, + pub minus_file_event: handlers::file_meta::FileEvent, + pub plus_file_event: handlers::file_meta::FileEvent, + pub diff_line: String, + pub painter: Painter<'a>, + pub config: &'a Config, // When a file is modified, we use lines starting with '---' or '+++' to obtain the file name. // When a file is renamed without changes, we use lines starting with 'rename' to obtain the // file name (there is no diff hunk and hence no lines starting with '---' or '+++'). But when // a file is renamed with changes, both are present, and we rely on the following variables to // avoid emitting the file meta header line twice (#245). - current_file_pair: Option<(String, String)>, - handled_file_meta_header_line_file_pair: Option<(String, String)>, - blame_commit_colors: HashMap, + pub current_file_pair: Option<(String, String)>, + pub handled_file_meta_header_line_file_pair: Option<(String, String)>, + pub blame_commit_colors: HashMap, } pub fn delta(lines: ByteLines, writer: &mut dyn Write, config: &Config) -> std::io::Result<()> @@ -100,8 +84,8 @@ impl<'a> StateMachine<'a> { source: Source::Unknown, minus_file: "".to_string(), plus_file: "".to_string(), - minus_file_event: parse::FileEvent::NoEvent, - plus_file_event: parse::FileEvent::NoEvent, + minus_file_event: handlers::file_meta::FileEvent::NoEvent, + plus_file_event: handlers::file_meta::FileEvent::NoEvent, diff_line: "".to_string(), current_file_pair: None, handled_file_meta_header_line_file_pair: None, @@ -129,7 +113,7 @@ impl<'a> StateMachine<'a> { || self.handle_file_meta_minus_line()? || self.handle_file_meta_plus_line()? || self.handle_hunk_header_line()? - || self.handle_additional_file_meta_cases()? + || self.handle_file_meta_misc_lines()? || self.handle_submodule_log_line()? || self.handle_submodule_short_line()? || self.handle_hunk_line()? @@ -177,526 +161,10 @@ impl<'a> StateMachine<'a> { } /// Should a handle_* function be called on this element? - fn should_handle(&self) -> bool { + pub fn should_handle(&self) -> bool { let style = self.config.get_style(&self.state); !(style.is_raw && style.decoration_style == DecorationStyle::NoDecoration) } - - #[inline] - fn test_commit_meta_header_line(&self) -> bool { - self.config.commit_regex.is_match(&self.line) - } - - fn handle_commit_meta_header_line(&mut self) -> std::io::Result { - 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(()) - } - - #[inline] - fn test_diff_stat_line(&self) -> bool { - (self.state == State::CommitMeta || self.state == State::Unknown) - && self.line.starts_with(' ') - } - - fn handle_diff_stat_line(&mut self) -> std::io::Result { - 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) = parse::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) - } - - #[inline] - fn test_file_meta_diff_line(&self) -> bool { - self.line.starts_with("diff ") - } - - #[allow(clippy::unnecessary_wraps)] - fn handle_file_meta_diff_line(&mut self) -> std::io::Result { - if !self.test_file_meta_diff_line() { - return Ok(false); - } - self.painter.paint_buffered_minus_and_plus_lines(); - self.state = State::FileMeta; - self.handled_file_meta_header_line_file_pair = None; - self.diff_line = self.line.clone(); - Ok(false) - } - - #[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 ")) - } - - fn handle_file_meta_minus_line(&mut self) -> std::io::Result { - if !self.test_file_meta_minus_line() { - return Ok(false); - } - let mut handled_line = false; - - let (path_or_mode, file_event) = parse::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 parse::FileEvent::ModeChange(_) = &file_event { - parse::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(parse::get_file_extension_from_marker_line(&self.line)); - } else { - self.painter - .set_syntax(parse::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 ")) - } - - fn handle_file_meta_plus_line(&mut self) -> std::io::Result { - if !self.test_file_meta_plus_line() { - return Ok(false); - } - let mut handled_line = false; - let (path_or_mode, file_event) = parse::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 parse::FileEvent::ModeChange(_) = &file_event { - parse::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(parse::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::DiffUnified)?; - self.handled_file_meta_header_line_file_pair = self.current_file_pair.clone() - } - Ok(handled_line) - } - - /// Construct file change line from minus and plus file and write with FileMeta styling. - fn _handle_file_meta_header_line(&mut self, comparing: bool) -> std::io::Result<()> { - let line = parse::get_file_change_description_from_file_paths( - &self.minus_file, - &self.plus_file, - comparing, - &self.minus_file_event, - &self.plus_file_event, - self.config, - ); - // FIXME: no support for 'raw' - _write_generic_file_meta_header_line(&line, &line, &mut self.painter, self.config) - } - - #[inline] - fn test_additional_file_meta_cases(&self) -> bool { - self.source == Source::DiffUnified && self.line.starts_with("Only in ") - || self.line.starts_with("Binary files ") - } - - fn handle_additional_file_meta_cases(&mut self) -> std::io::Result { - if !self.test_additional_file_meta_cases() { - return Ok(false); - } - self._handle_additional_cases(State::FileMeta) - } - - #[inline] - fn test_submodule_log(&self) -> bool { - self.line.starts_with("Submodule ") - } - - fn handle_submodule_log_line(&mut self) -> std::io::Result { - if !self.test_submodule_log() { - return Ok(false); - } - self._handle_additional_cases(State::SubmoduleLog) - } - - #[inline] - fn test_submodule_short_line(&self) -> bool { - matches!(self.state, State::HunkHeader(_, _)) - && self.line.starts_with("-Subproject commit ") - || matches!(self.state, State::SubmoduleShort(_)) - && self.line.starts_with("+Subproject commit ") - } - - fn handle_submodule_short_line(&mut self) -> std::io::Result { - if !self.test_submodule_short_line() { - return Ok(false); - } - if let Some(commit) = parse::get_submodule_short_commit(&self.line) { - if let State::HunkHeader(_, _) = self.state { - self.state = State::SubmoduleShort(commit.to_owned()); - } else if let State::SubmoduleShort(minus_commit) = &self.state { - self.painter.emit()?; - writeln!( - self.painter.writer, - "{}..{}", - self.config - .minus_style - .paint(minus_commit.chars().take(7).collect::()), - self.config - .plus_style - .paint(commit.chars().take(7).collect::()), - )?; - } - } - Ok(true) - } - - /// 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. - fn handle_blame_line(&mut self) -> std::io::Result { - let mut handled_line = false; - self.painter.emit()?; - if matches!(self.state, State::Unknown | State::Blame(_)) { - if let Some(blame) = - 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::BLAME_PLACEHOLDER_REGEX, - ); - write!( - self.painter.writer, - "{}", - style.paint(blame::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) - } - - fn _handle_additional_cases(&mut self, to_state: State) -> std::io::Result { - let mut handled_line = false; - - // Additional cases: - // - // 1. When comparing directories with diff -u, if filenames match between the - // directories, the files themselves will be compared. However, if an equivalent - // filename is not present, diff outputs a single line (Only in...) starting - // indicating that the file is present in only one of the directories. - // - // 2. Git diff emits lines describing submodule state such as "Submodule x/y/z contains - // untracked content" - // - // See https://github.com/dandavison/delta/issues/60#issuecomment-557485242 for a - // proposal for more robust parsing logic. - - self.painter.paint_buffered_minus_and_plus_lines(); - self.state = to_state; - if self.should_handle() { - self.painter.emit()?; - _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_hunk_header_line(&self) -> bool { - self.line.starts_with("@@") - } - - fn handle_hunk_header_line(&mut self) -> std::io::Result { - if !self.test_hunk_header_line() { - return Ok(false); - } - self.state = State::HunkHeader(self.line.clone(), self.raw_line.clone()); - Ok(true) - } - - /// Emit the hunk header, with any requested decoration. - fn emit_hunk_header_line(&mut self, line: &str, raw_line: &str) -> std::io::Result { - self.painter.paint_buffered_minus_and_plus_lines(); - self.painter.set_highlighter(); - self.painter.emit()?; - - let (code_fragment, line_numbers) = parse::parse_hunk_header(line); - if self.config.line_numbers { - self.painter - .line_numbers_data - .initialize_hunk(&line_numbers, self.plus_file.to_string()); - } - - if self.config.hunk_header_style.is_raw { - hunk_header::write_hunk_header_raw(&mut self.painter, line, raw_line, self.config)?; - } else if self.config.hunk_header_style.is_omitted { - writeln!(self.painter.writer)?; - } else { - // Add a blank line below the hunk-header-line for readability, unless - // color_only mode is active. - if !self.config.color_only { - writeln!(self.painter.writer)?; - } - - hunk_header::write_hunk_header( - &code_fragment, - &line_numbers, - &mut self.painter, - line, - &self.plus_file, - self.config, - )?; - }; - self.painter.set_highlighter(); - Ok(true) - } - - #[inline] - fn test_hunk_line(&self) -> bool { - self.state.is_in_hunk() - } - - /// Handle a hunk line, i.e. a minus line, a plus line, or an unchanged line. - // In the case of a minus or plus line, we store the line in a - // buffer. When we exit the changed region we process the collected - // minus and plus lines jointly, in order to paint detailed - // highlighting according to inferred edit operations. In the case of - // an unchanged line, we paint it immediately. - fn handle_hunk_line(&mut self) -> std::io::Result { - // A true hunk line should start with one of: '+', '-', ' '. However, handle_hunk_line - // handles all lines until the state transitions away from the hunk states. - if !self.test_hunk_line() { - return Ok(false); - } - // Don't let the line buffers become arbitrarily large -- if we - // were to allow that, then for a large deleted/added file we - // would process the entire file before painting anything. - if self.painter.minus_lines.len() > self.config.line_buffer_size - || self.painter.plus_lines.len() > self.config.line_buffer_size - { - self.painter.paint_buffered_minus_and_plus_lines(); - } - if let State::HunkHeader(line, raw_line) = &self.state.clone() { - self.emit_hunk_header_line(line, raw_line)?; - } - self.state = match self.line.chars().next() { - Some('-') => { - if let State::HunkPlus(_) = self.state { - self.painter.paint_buffered_minus_and_plus_lines(); - } - let state = match self.config.inspect_raw_lines { - cli::InspectRawLines::True - if style::line_has_style_other_than( - &self.raw_line, - [*style::GIT_DEFAULT_MINUS_STYLE, self.config.git_minus_style].iter(), - ) => - { - State::HunkMinus(Some(self.painter.prepare_raw_line(&self.raw_line))) - } - _ => State::HunkMinus(None), - }; - self.painter - .minus_lines - .push((self.painter.prepare(&self.line), state.clone())); - state - } - Some('+') => { - let state = match self.config.inspect_raw_lines { - cli::InspectRawLines::True - if style::line_has_style_other_than( - &self.raw_line, - [*style::GIT_DEFAULT_PLUS_STYLE, self.config.git_plus_style].iter(), - ) => - { - State::HunkPlus(Some(self.painter.prepare_raw_line(&self.raw_line))) - } - _ => State::HunkPlus(None), - }; - self.painter - .plus_lines - .push((self.painter.prepare(&self.line), state.clone())); - state - } - Some(' ') => { - self.painter.paint_buffered_minus_and_plus_lines(); - self.painter.paint_zero_line(&self.line); - State::HunkZero - } - _ => { - // The first character here could be e.g. '\' from '\ No newline at end of file'. This - // is not a hunk line, but the parser does not have a more accurate state corresponding - // to this. - self.painter.paint_buffered_minus_and_plus_lines(); - self.painter - .output_buffer - .push_str(&self.painter.expand_tabs(self.raw_line.graphemes(true))); - self.painter.output_buffer.push('\n'); - State::HunkZero - } - }; - self.painter.emit()?; - Ok(true) - } } /// If output is going to a tty, emit hyperlinks if requested. @@ -709,38 +177,6 @@ pub fn format_raw_line<'a>(line: &'a str, config: &Config) -> Cow<'a, str> { } } -/// Write `line` with FileMeta styling. -fn _write_generic_file_meta_header_line( - line: &str, - raw_line: &str, - painter: &mut Painter, - config: &Config, -) -> std::io::Result<()> { - // If file_style is "omit", we'll skip the process and print nothing. - // However in the case of color_only mode, - // we won't skip because we can't change raw_line structure. - if config.file_style.is_omitted && !config.color_only { - return Ok(()); - } - let (mut draw_fn, pad, decoration_ansi_term_style) = - draw::get_draw_function(config.file_style.decoration_style); - // Prints the new line below file-meta-line. - // However in the case of color_only mode, - // we won't print it because we can't change raw_line structure. - if !config.color_only { - writeln!(painter.writer)?; - } - draw_fn( - painter.writer, - &format!("{}{}", line, if pad { " " } else { "" }), - &format!("{}{}", raw_line, if pad { " " } else { "" }), - &config.decorations_width, - config.file_style, - decoration_ansi_term_style, - )?; - Ok(()) -} - /// Try to detect what is producing the input for delta. /// /// Currently can detect: diff --git a/src/draw.rs b/src/draw.rs deleted file mode 100644 index d7edd8cd..00000000 --- a/src/draw.rs +++ /dev/null @@ -1,300 +0,0 @@ -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, 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 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/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 { + 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, + 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> { + 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::() { + 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 { + 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 { + 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 { + 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, 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 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 { + 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 { + 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_li