summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Davison <dandavison7@gmail.com>2020-12-28 15:45:10 +0000
committerGitHub <noreply@github.com>2020-12-28 10:45:10 -0500
commitb9e577aac0a81f759806a8fcbf8c29df2aa8c347 (patch)
treeb41226a6360f5c1acff5912bc1322999ec6d87d3
parent279e9da882619fd57fe62b12bc7481c69b4b8c12 (diff)
State machine refactor (#477)
-rw-r--r--src/delta.rs715
1 files changed, 393 insertions, 322 deletions
diff --git a/src/delta.rs b/src/delta.rs
index 620e00e8..f72302e4 100644
--- a/src/delta.rs
+++ b/src/delta.rs
@@ -52,254 +52,414 @@ 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 |
-pub fn delta<I>(
- mut lines: ByteLines<I>,
- writer: &mut dyn Write,
- config: &Config,
-) -> std::io::Result<()>
-where
- I: BufRead,
-{
- let mut painter = Painter::new(writer, config);
- let mut minus_file = "".to_string();
- let mut plus_file = "".to_string();
- let mut file_event = parse::FileEvent::NoEvent;
- let mut state = State::Unknown;
- let mut source = Source::Unknown;
+struct StateMachine<'a> {
+ line: String,
+ raw_line: String,
+ state: State,
+ source: Source,
+ minus_file: String,
+ plus_file: String,
+ file_event: parse::FileEvent,
+ painter: Painter<'a>,
+ 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).
- let mut current_file_pair;
- let mut handled_file_meta_header_line_file_pair = None;
+ current_file_pair: Option<(String, String)>,
+ handled_file_meta_header_line_file_pair: Option<(String, String)>,
+}
- while let Some(Ok(raw_line_bytes)) = lines.next() {
- let raw_line = String::from_utf8_lossy(&raw_line_bytes);
- let raw_line = if config.max_line_length > 0 && raw_line.len() > config.max_line_length {
- ansi::truncate_str(&raw_line, config.max_line_length, &config.truncation_symbol)
- } else {
- raw_line
- };
- let line = ansi::strip_ansi_codes(&raw_line).to_string();
- if source == Source::Unknown {
- source = detect_source(&line);
+pub fn delta<I>(lines: ByteLines<I>, writer: &mut dyn Write, config: &Config) -> std::io::Result<()>
+where
+ I: BufRead,
+{
+ StateMachine::new(writer, config).consume(lines)
+}
+
+impl<'a> StateMachine<'a> {
+ pub fn new(writer: &'a mut dyn Write, config: &'a Config) -> Self {
+ Self {
+ line: "".to_string(),
+ raw_line: "".to_string(),
+ state: State::Unknown,
+ source: Source::Unknown,
+ minus_file: "".to_string(),
+ plus_file: "".to_string(),
+ file_event: parse::FileEvent::NoEvent,
+ current_file_pair: None,
+ handled_file_meta_header_line_file_pair: None,
+ painter: Painter::new(writer, config),
+ config,
}
- if line.starts_with("commit ") {
- painter.paint_buffered_minus_and_plus_lines();
- state = State::CommitMeta;
- if should_handle(&state, config) {
- painter.emit()?;
- handle_commit_meta_header_line(&mut painter, &line, &raw_line, config)?;
- continue;
- }
- } else if line.starts_with("diff ") {
- painter.paint_buffered_minus_and_plus_lines();
- state = State::FileMeta;
- handled_file_meta_header_line_file_pair = None;
- } else if (state == State::FileMeta || source == Source::DiffUnified)
- && (line.starts_with("--- ")
- || line.starts_with("rename from ")
- || line.starts_with("copy from "))
- {
- let parsed_file_meta_line =
- parse::parse_file_meta_line(&line, source == Source::GitDiff);
- minus_file = parsed_file_meta_line.0;
- file_event = parsed_file_meta_line.1;
-
- if source == Source::DiffUnified {
- state = State::FileMeta;
- painter.set_syntax(parse::get_file_extension_from_marker_line(&line));
- } else {
- painter.set_syntax(parse::get_file_extension_from_file_meta_line_file_path(
- &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 config.color_only {
- handle_generic_file_meta_header_line(&mut painter, &line, &raw_line, config)?;
- continue;
- }
- } else if (state == State::FileMeta || source == Source::DiffUnified)
- && (line.starts_with("+++ ")
- || line.starts_with("rename to ")
- || line.starts_with("copy to "))
- {
- let parsed_file_meta_line =
- parse::parse_file_meta_line(&line, source == Source::GitDiff);
- plus_file = parsed_file_meta_line.0;
- painter.set_syntax(parse::get_file_extension_from_file_meta_line_file_path(
- &plus_file,
- ));
- current_file_pair = Some((minus_file.clone(), 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 config.color_only {
- handle_generic_file_meta_header_line(&mut painter, &line, &raw_line, config)?;
- continue;
+ fn consume<I>(&mut self, mut lines: ByteLines<I>) -> std::io::Result<()>
+ where
+ I: BufRead,
+ {
+ while let Some(Ok(raw_line_bytes)) = lines.next() {
+ self.ingest_line(raw_line_bytes);
+ let line = &self.line;
+
+ if self.source == Source::Unknown {
+ self.source = detect_source(&line);
}
- if should_handle(&State::FileMeta, config)
- && handled_file_meta_header_line_file_pair != current_file_pair
+
+ let mut handled_line = if line.starts_with("commit ") {
+ self.handle_commit_meta_header_line()?
+ } else if line.starts_with("diff ") {
+ self.handle_file_meta_diff_line()?
+ } else if (self.state == State::FileMeta || self.source == Source::DiffUnified)
+ && (line.starts_with("--- ")
+ || line.starts_with("rename from ")
+ || line.starts_with("copy from "))
{
- painter.emit()?;
- handle_file_meta_header_line(
- &mut painter,
- &minus_file,
- &plus_file,
- config,
- &file_event,
- source == Source::DiffUnified,
- )?;
- handled_file_meta_header_line_file_pair = current_file_pair
+ self.handle_file_meta_minus_line()?
+ } else if (self.state == State::FileMeta || self.source == Source::DiffUnified)
+ && (line.starts_with("+++ ")
+ || line.starts_with("rename to ")
+ || line.starts_with("copy to "))
+ {
+ self.handle_file_meta_plus_line()?
+ } else if line.starts_with("@@") {
+ self.handle_hunk_header_line()?
+ } else if self.source == Source::DiffUnified && line.starts_with("Only in ")
+ || line.starts_with("Submodule ")
+ || line.starts_with("Binary files ")
+ {
+ self.handle_additional_file_meta_cases()?
+ } else if self.state.is_in_hunk() {
+ // A true hunk line should start with one of: '+', '-', ' '. However, handle_hunk_line
+ // handles all lines until the state self transitions away from the hunk states.
+ self.handle_hunk_line()?
+ } else {
+ false
+ };
+ if self.state == State::FileMeta && self.should_handle() && !self.config.color_only {
+ // The file metadata section is 4 lines. Skip them under non-plain file-styles.
+ // However in the case of color_only mode,
+ // we won't skip because we can't change raw_line structure.
+ handled_line = true
}
- } else if line.starts_with("@@") {
- painter.paint_buffered_minus_and_plus_lines();
- state = State::HunkHeader;
- painter.set_highlighter();
- painter.emit()?;
- handle_hunk_header_line(&mut painter, &line, &raw_line, &plus_file, config)?;
- painter.set_highlighter();
- continue;
- } else if source == Source::DiffUnified && line.starts_with("Only in ")
- || line.starts_with("Submodule ")
- || line.starts_with("Binary files ")
- {
- // Additional FileMeta 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.
-
- painter.paint_buffered_minus_and_plus_lines();
- state = State::FileMeta;
- if should_handle(&State::FileMeta, config) {
- painter.emit()?;
- handle_generic_file_meta_header_line(&mut painter, &line, &raw_line, config)?;
- continue;
+ if !handled_line {
+ self.painter.emit()?;
+ writeln!(
+ self.painter.writer,
+ "{}",
+ format::format_raw_line(&self.raw_line, self.config)
+ )?;
}
- } else if state.is_in_hunk() {
- // A true hunk line should start with one of: '+', '-', ' '. However, handle_hunk_line
- // handles all lines until the state machine transitions away from the hunk states.
- state = handle_hunk_line(&mut painter, &line, &raw_line, state, config);
- painter.emit()?;
- continue;
}
- if state == State::FileMeta && should_handle(&State::FileMeta, config) && !config.color_only
- {
- // The file metadata section is 4 lines. Skip them under non-plain file-styles.
- // However in the case of color_only mode,
- // we won't skip because we can't change raw_line structure.
- continue;
+ self.painter.paint_buffered_minus_and_plus_lines();
+ self.painter.emit()?;
+ Ok(())
+ }
+
+ fn ingest_line(&mut self, raw_line_bytes: &[u8]) {
+ // TODO: retain raw_line as Cow
+ self.raw_line = String::from_utf8_lossy(&raw_line_bytes).to_string();
+ if self.config.max_line_length > 0 && self.raw_line.len() > self.config.max_line_length {
+ self.raw_line = ansi::truncate_str(
+ &self.raw_line,
+ self.config.max_line_length,
+ &self.config.truncation_symbol,
+ )
+ .to_string()
+ };
+ self.line = ansi::strip_ansi_codes(&self.raw_line);
+ }
+
+ /// Should a handle_* function be called on this element?
+ fn should_handle(&self) -> bool {
+ let style = self.config.get_style(&self.state);
+ !(style.is_raw && style.decoration_style == DecorationStyle::NoDecoration)
+ }
+
+ fn handle_commit_meta_header_line(&mut self) -> std::io::Result<bool> {
+ 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(())
+ }
+
+ fn handle_file_meta_diff_line(&mut self) -> std::io::Result<bool> {
+ self.painter.paint_buffered_minus_and_plus_lines();
+ self.state = State::FileMeta;
+ self.handled_file_meta_header_line_file_pair = None;
+ Ok(false)
+ }
+
+ fn handle_file_meta_minus_line(&mut self) -> std::io::Result<bool> {
+ let mut handled_line = false;
+
+ let parsed_file_meta_line =
+ parse::parse_file_meta_line(&self.line, self.source == Source::GitDiff);
+ self.minus_file = parsed_file_meta_line.0;
+ self.file_event = parsed_file_meta_line.1;
+
+ if self.source == Source::DiffUnified {
+ self.state = State::FileMeta;
+ self.painter
+ .set_syntax(parse::get_file_extension_from_marker_line(&self.line));
} else {
- painter.emit()?;
- writeln!(
- painter.writer,
- "{}",
- format::format_raw_line(&raw_line, config)
+ 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)
}
- painter.paint_buffered_minus_and_plus_lines();
- painter.emit()?;
- Ok(())
-}
+ fn handle_file_meta_plus_line(&mut self) -> std::io::Result<bool> {
+ let mut handled_line = false;
+ let parsed_file_meta_line =
+ parse::parse_file_meta_line(&self.line, self.source == Source::GitDiff);
+ self.plus_file = parsed_file_meta_line.0;
+ 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()));
-/// Should a handle_* function be called on this element?
-fn should_handle(state: &State, config: &Config) -> bool {
- let style = config.get_style(state);
- !(style.is_raw && style.decoration_style == DecorationStyle::NoDecoration)
-}
+ // 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)
+ }
-/// Try to detect what is producing the input for delta.
-///
-/// Currently can detect:
-/// * git diff
-/// * diff -u
-fn detect_source(line: &str) -> Source {
- if line.starts_with("commit ") || line.starts_with("diff --git ") {
- Source::GitDiff
- } else if line.starts_with("diff -u")
- || line.starts_with("diff -ru")
- || line.starts_with("diff -r -u")
- || line.starts_with("diff -U")
- || line.starts_with("--- ")
- || line.starts_with("Only in ")
- {
- Source::DiffUnified
- } else {
- Source::Unknown
+ /// 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.file_event,
+ self.config,
+ );
+ // FIXME: no support for 'raw'
+ _write_generic_file_meta_header_line(&line, &line, &mut self.painter, self.config)
}
-}
-fn handle_commit_meta_header_line(
- painter: &mut Painter,
- line: &str,
- raw_line: &str,
- config: &Config,
-) -> std::io::Result<()> {
- if config.commit_style.is_omitted {
- return Ok(());
+ fn handle_additional_file_meta_cases(&mut self) -> std::io::Result<bool> {
+ let mut handled_line = false;
+
+ // Additional FileMeta 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 = State::FileMeta;
+ 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)
}
- let (mut draw_fn, pad, decoration_ansi_term_style) =
- draw::get_draw_function(config.commit_style.decoration_style);
- let (formatted_line, formatted_raw_line) = if config.hyperlinks {
- (
- features::hyperlinks::format_commit_line_with_osc8_commit_hyperlink(line, config),
- features::hyperlinks::format_commit_line_with_osc8_commit_hyperlink(raw_line, config),
- )
- } else {
- (Cow::from(line), Cow::from(raw_line))
- };
- draw_fn(
- painter.writer,
- &format!("{}{}", formatted_line, if pad { " " } else { "" }),
- &format!("{}{}", formatted_raw_line, if pad { " " } else { "" }),
- &config.decorations_width,
- config.commit_style,
- decoration_ansi_term_style,
- )?;
- Ok(())
-}
+ /// Emit the hunk header, with any requested decoration.
+ fn handle_hunk_header_line(&mut self) -> std::io::Result<bool> {
+ self.painter.paint_buffered_minus_and_plus_lines();
+ self.state = State::HunkHeader;
+ self.painter.set_highlighter();
+ self.painter.emit()?;
-/// Construct file change line from minus and plus file and write with FileMeta styling.
-fn handle_file_meta_header_line(
- painter: &mut Painter,
- minus_file: &str,
- plus_file: &str,
- config: &Config,
- file_event: &parse::FileEvent,
- comparing: bool,
-) -> std::io::Result<()> {
- let line = parse::get_file_change_description_from_file_paths(
- minus_file, plus_file, comparing, file_event, config,
- );
- // FIXME: no support for 'raw'
- handle_generic_file_meta_header_line(painter, &line, &line, config)
+ let (raw_code_fragment, line_numbers) = parse::parse_hunk_header(&self.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,
+ &self.line,
+ &self.raw_line,
+ self.config,
+ )?;
+ } else if self.config.hunk_header_style.is_omitted {
+ writeln!(self.painter.writer)?;
+ } else {
+ hunk_header::write_hunk_header(
+ &raw_code_fragment,
+ &line_numbers,
+ &mut self.painter,
+ &self.line,
+ &self.plus_file,
+ self.config,
+ )?;
+ };
+ self.painter.set_highlighter();
+ Ok(true)
+ }
+
+ /// 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<bool> {
+ // 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();
+ }
+ 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)))
+ }
+