pratdiff 3.0.0

A colorfull diff tool based on the patience diff algorithm
Documentation
use std::error::Error;
use std::io::Result;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;

use diff::DiffItem;
use diff::DiffItem::*;
use diff::Side;
use owo_colors::OwoColorize;
use owo_colors::Style;

use crate::cluster::DiffCluster;
use crate::diff;
use crate::files::FilePairEvent;
use crate::hunks::Hunk;
use crate::styles::Styles;
use crate::tokenize_lines;
use crate::tokens::split_lines;

pub struct Printer<'a> {
  styles: Styles,
  writer: &'a mut dyn Write,
  context: usize,
  common_prefix: PathBuf,
}

impl<'a> Printer<'a> {
  pub fn default(
    writer: &'a mut dyn Write,
    context: usize,
    common_prefix: PathBuf,
  ) -> Printer<'a> {
    Printer {
      styles: Styles::simple(),
      writer,
      context,
      common_prefix,
    }
  }

  pub fn print_file_pair_event(&mut self, event: FilePairEvent) -> Result<()> {
    match event {
      FilePairEvent::TextDiff {
        lhs_path,
        rhs_path,
        lhs_content,
        rhs_content,
      } => {
        self.print_file_header(lhs_path.as_deref(), rhs_path.as_deref())?;
        self.print_diff(true, &lhs_content, &rhs_content)?;
      }
      FilePairEvent::Binary { lhs_path, rhs_path } => {
        self.print_binary_files_differ(
          lhs_path.as_deref(),
          rhs_path.as_deref(),
        )?;
      }
      FilePairEvent::TypeMismatch { lhs_path, rhs_path } => {
        self.print_directory_mismatch(&lhs_path, &rhs_path)?;
      }
      FilePairEvent::IoError { lhs_path, rhs_path, err } => {
        writeln!(
          self.writer,
          "Error diffing {} and {}:\n{}",
          self
            .display_name(lhs_path.as_deref())
            .style(self.styles.old),
          self
            .display_name(rhs_path.as_deref())
            .style(self.styles.new),
          err,
        )?;
      }
    }
    Ok(())
  }

  fn display_name(&self, p: Option<&Path>) -> String {
    let Some(p) = p else {
      return "/dev/null".into();
    };
    let stripped = p.strip_prefix(&self.common_prefix).unwrap_or(p);
    if let Ok(link) = std::fs::read_link(p) {
      let stripped_link =
        link.strip_prefix(&self.common_prefix).unwrap_or(&link);
      return format!("{} -> {}", stripped.display(), stripped_link.display());
    }
    stripped.display().to_string()
  }

  pub fn print_error(
    &mut self,
    lhs: Option<&Path>,
    rhs: Option<&Path>,
    err: Box<dyn Error>,
  ) -> Result<()> {
    writeln!(
      self.writer,
      "Error diffing {} and {}:\n{}",
      self.display_name(lhs).style(self.styles.old),
      self.display_name(rhs).style(self.styles.new),
      err
    )
  }

  pub fn print_directory_mismatch(
    &mut self,
    lhs: &Path,
    rhs: &Path,
  ) -> Result<()> {
    fn ft(p: &Path) -> &str {
      if p.metadata().unwrap().is_dir() { "directory" } else { "file" }
    }
    writeln!(
      self.writer,
      "File/directory mistmatch:\n  {} is a {}\n  {} is a {}",
      self.display_name(Some(lhs)).style(self.styles.old),
      ft(lhs),
      self.display_name(Some(rhs)).style(self.styles.new),
      ft(rhs),
    )
  }

  pub fn print_binary_files_differ(
    &mut self,
    lhs: Option<&Path>,
    rhs: Option<&Path>,
  ) -> Result<()> {
    writeln!(
      self.writer,
      "Binary files {} and {} differ",
      self.display_name(lhs).style(self.styles.old),
      self.display_name(rhs).style(self.styles.new),
    )?;
    Ok(())
  }

  pub fn print_file_header(
    &mut self,
    lhs: Option<&Path>,
    rhs: Option<&Path>,
  ) -> Result<()> {
    writeln!(
      self.writer,
      "{} {}",
      "---".style(self.styles.old),
      self.display_name(lhs).style(self.styles.header),
    )?;
    writeln!(
      self.writer,
      "{} {}",
      "+++".style(self.styles.new),
      self.display_name(rhs).style(self.styles.header)
    )?;
    Ok(())
  }

  pub fn print_diff(
    &mut self,
    include_headers: bool,
    lhs_all: &[u8],
    rhs_all: &[u8],
  ) -> Result<()> {
    let lhs = split_lines(lhs_all);
    let rhs = split_lines(rhs_all);
    let diffs = diff(&lhs, &rhs);
    let hunks = Hunk::build(self.context, &diffs);

    for h in hunks {
      if include_headers {
        self.print_hunk_header(&h)?;
      }
      self.print_hunk_body(&lhs, &rhs, &h.diffs)?;
    }
    Ok(())
  }

  fn print_hunk_header(&mut self, h: &Hunk) -> Result<()> {
    let (l, r) = (h.lhs(), h.rhs());
    writeln!(
      self.writer,
      "{}",
      format!(
        "@@ -{},{} +{},{} @@",
        l.start + 1,
        l.len(),
        r.start + 1,
        r.len()
      )
      .style(self.styles.separator)
    )?;
    Ok(())
  }

  fn print_hunk_body(
    &mut self,
    lhs_lines: &[&[u8]],
    rhs_lines: &[&[u8]],
    diffs: &[DiffItem],
  ) -> Result<()> {
    for d in diffs {
      match &d {
        Mutation { lhs, rhs } => {
          if rhs.is_empty() {
            self.print_lines(&lhs_lines[lhs.clone()], "-", self.styles.old)?;
          } else if lhs.is_empty() {
            self.print_lines(&rhs_lines[rhs.clone()], "+", self.styles.new)?;
          } else {
            self.print_mutation(
              &lhs_lines[lhs.clone()],
              &rhs_lines[rhs.clone()],
            )?;
          }
        }
        Match { lhs, .. } => {
          self.print_lines(&lhs_lines[lhs.clone()], " ", self.styles.both)?;
        }
      }
    }
    Ok(())
  }

  fn print_lines(
    &mut self,
    lines: &[&[u8]],
    prefix: &str,
    style: Style,
  ) -> Result<()> {
    for line in lines {
      let s = String::from_utf8_lossy(line);
      writeln!(self.writer, "{}{}", prefix.style(style), s.style(style))?;
    }
    Ok(())
  }

  fn print_mutation(
    &mut self,
    lhs_lines: &[&[u8]],
    rhs_lines: &[&[u8]],
  ) -> Result<()> {
    let lhs_tokens = tokenize_lines(lhs_lines);
    let rhs_tokens = tokenize_lines(rhs_lines);
    let diffs = diff(&lhs_tokens, &rhs_tokens);
    self.print_mutation_side(
      &lhs_tokens,
      &diffs,
      "-",
      Side::Lhs,
      self.styles.old,
      self.styles.old_dim,
    )?;
    self.print_mutation_side(
      &rhs_tokens,
      &diffs,
      "+",
      Side::Rhs,
      self.styles.new,
      self.styles.new_dim,
    )?;
    Ok(())
  }

  fn print_mutation_side(
    &mut self,
    tokens: &[&[u8]],
    diffs: &[DiffItem],
    prefix: &str,
    side: Side,
    mutation: Style,
    matching: Style,
  ) -> Result<()> {
    write!(self.writer, "{}", prefix.style(mutation))?;
    for d in diffs {
      let style = if matches!(d, Match { .. }) { matching } else { mutation };
      for &t in &tokens[d.side(side)] {
        let s = String::from_utf8_lossy(t);
        write!(self.writer, "{}", s.style(style))?;
        if t == b"\n" {
          write!(self.writer, "{}", prefix.style(mutation))?;
        }
      }
    }
    writeln!(self.writer)?;
    Ok(())
  }

  pub fn print_clusters(&mut self, clusters: &[DiffCluster]) -> Result<()> {
    for cluster in clusters {
      self.print_cluster(cluster)?;
    }
    Ok(())
  }

  fn print_cluster(&mut self, cluster: &DiffCluster) -> Result<()> {
    let total: usize = cluster.entries.values().sum();
    let entry_count =
      |n| format!("{} {}", n, if 1 == n { "entry" } else { "entries" });
    writeln!(
      self.writer,
      "{}",
      format!("=== cluster contains {}", entry_count(total))
        .style(self.styles.header),
    )?;
    for (entry, &count) in &cluster.entries {
      let lhs = self.display_name(entry.lhs_path.as_deref());
      let rhs = self.display_name(entry.rhs_path.as_deref());
      write!(self.writer, "{}", "= ".style(self.styles.separator))?;
      write!(self.writer, "{}", lhs.style(self.styles.old))?;
      write!(self.writer, "{}", " => ".style(self.styles.separator))?;
      write!(self.writer, "{}", rhs.style(self.styles.new))?;
      writeln!(
        self.writer,
        "{}",
        format!(": {}", entry_count(count)).style(self.styles.separator)
      )?;
    }
    writeln!(
      self.writer,
      "{}",
      "=== example diff: ".style(self.styles.separator)
    )?;
    self.print_diff(false, &cluster.exemplar_lhs, &cluster.exemplar_rhs)?;
    Ok(())
  }
}