use bstr::{BString, ByteSlice, ByteVec}; use imara_diff::{intern, Sink}; use intern::{InternedInput, Interner, Token}; use std::fmt::Write; use std::{hash::Hash, ops::Range}; use super::{ConsumeBinaryHunk, ConsumeBinaryHunkDelegate, ConsumeHunk, ContextSize, DiffLineKind, HunkHeader}; /// A [`Sink`] that creates a unified diff. It can be used to create a textual diff in the /// format typically output by `git` or `gnu-diff` if the `-u` option is used. pub struct UnifiedDiff<'a, T, D> where T: Hash + Eq + AsRef<[u8]>, D: ConsumeHunk, { before: &'a [Token], after: &'a [Token], interner: &'a Interner, /// The 0-based start position in the 'before' tokens for the accumulated hunk for display in the header. before_hunk_start: u32, /// The size of the accumulated 'before' hunk in lines for display in the header. before_hunk_len: u32, /// The 0-based start position in the 'after' tokens for the accumulated hunk for display in the header. after_hunk_start: u32, /// The size of the accumulated 'after' hunk in lines. after_hunk_len: u32, // An index into `before` and the context line to print next, // or `None` if this value was never computed to be the correct starting point for an accumulated hunk. ctx_pos: Option, /// Symmetrical context before and after the changed hunk. ctx_size: u32, buffer: Vec<(DiffLineKind, &'a [u8])>, delegate: D, err: Option, } impl<'a, T, D> UnifiedDiff<'a, T, D> where T: Hash + Eq + AsRef<[u8]>, D: ConsumeHunk, { /// Create a new instance to create a unified diff using the lines in `input`, /// which also must be used when running the diff algorithm. /// `context_size` is the amount of lines around each hunk which will be passed /// to `consume_hunk`. /// /// `consume_hunk` is called for each hunk with all the information required to create a /// unified diff. pub fn new(input: &'a InternedInput, consume_hunk: D, context_size: ContextSize) -> Self { Self { interner: &input.interner, before: &input.before, after: &input.after, before_hunk_start: 0, before_hunk_len: 0, after_hunk_len: 0, after_hunk_start: 0, ctx_pos: None, ctx_size: context_size.symmetrical, buffer: Vec::with_capacity(8), delegate: consume_hunk, err: None, } } fn print_tokens(&mut self, tokens: &[Token], line_type: DiffLineKind) { for &token in tokens { let content = self.interner[token].as_ref(); self.buffer.push((line_type, content)); } } fn flush_accumulated_hunk(&mut self) -> std::io::Result<()> { if self.nothing_to_flush() { return Ok(()); } let ctx_pos = self.ctx_pos.expect("has been set if we started a hunk"); let end = (ctx_pos + self.ctx_size).min(self.before.len() as u32); self.print_context_and_update_pos(ctx_pos..end, end); let hunk_start = self.before_hunk_start + 1; let hunk_end = self.after_hunk_start + 1; let header = HunkHeader { before_hunk_start: hunk_start, before_hunk_len: self.before_hunk_len, after_hunk_start: hunk_end, after_hunk_len: self.after_hunk_len, }; self.delegate.consume_hunk(header, &self.buffer)?; self.reset_hunks(); Ok(()) } fn print_context_and_update_pos(&mut self, print: Range, move_to: u32) { self.print_tokens( &self.before[print.start as usize..print.end as usize], DiffLineKind::Context, ); let len = print.end - print.start; self.ctx_pos = Some(move_to); self.before_hunk_len += len; self.after_hunk_len += len; } fn reset_hunks(&mut self) { self.buffer.clear(); self.before_hunk_len = 0; self.after_hunk_len = 0; } fn nothing_to_flush(&self) -> bool { self.before_hunk_len == 0 && self.after_hunk_len == 0 } } impl Sink for UnifiedDiff<'_, T, D> where T: Hash + Eq + AsRef<[u8]>, D: ConsumeHunk, { type Out = std::io::Result; fn process_change(&mut self, before: Range, after: Range) { if self.err.is_some() { return; } let start_next_hunk = self .ctx_pos .is_some_and(|ctx_pos| before.start - ctx_pos > 2 * self.ctx_size); if start_next_hunk { if let Err(err) = self.flush_accumulated_hunk() { self.err = Some(err); return; } let ctx_pos = before.start - self.ctx_size; self.ctx_pos = Some(ctx_pos); self.before_hunk_start = ctx_pos; self.after_hunk_start = after.start - self.ctx_size; } let ctx_pos = match self.ctx_pos { None => { // TODO: can this be made so the code above does the job? let ctx_pos = before.start.saturating_sub(self.ctx_size); self.before_hunk_start = ctx_pos; self.after_hunk_start = after.start.saturating_sub(self.ctx_size); ctx_pos } Some(pos) => pos, }; self.print_context_and_update_pos(ctx_pos..before.start, before.end); self.before_hunk_len += before.end - before.start; self.after_hunk_len += after.end - after.start; self.print_tokens( &self.before[before.start as usize..before.end as usize], DiffLineKind::Remove, ); self.print_tokens(&self.after[after.start as usize..after.end as usize], DiffLineKind::Add); } fn finish(mut self) -> Self::Out { if let Err(err) = self.flush_accumulated_hunk() { self.err = Some(err); } if let Some(err) = self.err { return Err(err); } Ok(self.delegate.finish()) } } /// An implementation that fails if the input isn't UTF-8. impl ConsumeHunk for ConsumeBinaryHunk<'_, D> where D: ConsumeBinaryHunkDelegate, { type Out = D; fn consume_hunk(&mut self, header: HunkHeader, lines: &[(DiffLineKind, &[u8])]) -> std::io::Result<()> { self.header_buf.clear(); self.header_buf .write_fmt(format_args!("{header}{nl}", nl = self.newline)) .map_err(std::io::Error::other)?; let buf = &mut self.hunk_buf; buf.clear(); for &(line_type, content) in lines { buf.push(line_type.to_prefix() as u8); buf.extend_from_slice(content); if !content.ends_with_str(self.newline) { buf.push_str(self.newline); } } self.delegate.consume_binary_hunk(header, &self.header_buf, buf)?; Ok(()) } fn finish(self) -> Self::Out { self.delegate } } /// An implementation that fails if the input isn't UTF-8. impl ConsumeBinaryHunkDelegate for String { fn consume_binary_hunk(&mut self, _header: HunkHeader, header_str: &str, hunk: &[u8]) -> std::io::Result<()> { self.push_str(header_str); self.push_str(hunk.to_str().map_err(std::io::Error::other)?); Ok(()) } } /// An implementation that writes hunks into a byte buffer. impl ConsumeBinaryHunkDelegate for Vec { fn consume_binary_hunk(&mut self, _header: HunkHeader, header_str: &str, hunk: &[u8]) -> std::io::Result<()> { self.push_str(header_str); self.extend_from_slice(hunk); Ok(()) } } /// An implementation that writes hunks into a hunman-readable byte buffer. impl ConsumeBinaryHunkDelegate for BString { fn consume_binary_hunk(&mut self, _header: HunkHeader, header_str: &str, hunk: &[u8]) -> std::io::Result<()> { self.push_str(header_str); self.extend_from_slice(hunk); Ok(()) } } impl<'a, D> ConsumeBinaryHunk<'a, D> where D: ConsumeBinaryHunkDelegate, { /// Create a new instance that writes stringified hunks to `delegate`, which uses `newline` to separate header and hunk, /// as well as hunk lines that don't naturally end in a newline. pub fn new(delegate: D, newline: &'a str) -> ConsumeBinaryHunk<'a, D> { ConsumeBinaryHunk { newline, delegate, header_buf: String::new(), hunk_buf: Vec::with_capacity(128), } } } impl DiffLineKind { /// Returns a one-character representation for use in unified diffs. pub const fn to_prefix(self) -> char { match self { DiffLineKind::Context => ' ', DiffLineKind::Add => '+', DiffLineKind::Remove => '-', } } } impl std::fmt::Display for HunkHeader { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "@@ -{},{} +{},{} @@", self.before_hunk_start, self.before_hunk_len, self.after_hunk_start, self.after_hunk_len ) } }