mod render;
mod structured;
pub use render::{DiffColors, DiffRenderer, DiffStyle};
pub use structured::{DemoSnapshot, DemosDiff, FieldsDiff, IterationDiffBuilder, ModuleDiff};
use similar::{ChangeTag, TextDiff as SimilarTextDiff};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeKind {
Equal,
Insert,
Delete,
}
impl From<ChangeTag> for ChangeKind {
fn from(tag: ChangeTag) -> Self {
match tag {
ChangeTag::Equal => ChangeKind::Equal,
ChangeTag::Insert => ChangeKind::Insert,
ChangeTag::Delete => ChangeKind::Delete,
}
}
}
#[derive(Debug, Clone)]
pub struct Change<'a> {
pub kind: ChangeKind,
pub value: &'a str,
pub old_line: Option<usize>,
pub new_line: Option<usize>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum DiffAlgorithm {
#[default]
Myers,
Patience,
Lcs,
}
#[derive(Debug)]
pub struct TextDiff<'a> {
old: &'a str,
new: &'a str,
changes: Vec<Change<'a>>,
algorithm: DiffAlgorithm,
word_level: bool,
}
impl<'a> TextDiff<'a> {
pub fn new(old: &'a str, new: &'a str) -> Self {
Self::with_algorithm(old, new, DiffAlgorithm::default())
}
pub fn with_algorithm(old: &'a str, new: &'a str, algorithm: DiffAlgorithm) -> Self {
let similar_diff = match algorithm {
DiffAlgorithm::Myers => SimilarTextDiff::from_lines(old, new),
DiffAlgorithm::Patience => SimilarTextDiff::configure()
.algorithm(similar::Algorithm::Patience)
.diff_lines(old, new),
DiffAlgorithm::Lcs => SimilarTextDiff::configure()
.algorithm(similar::Algorithm::Lcs)
.diff_lines(old, new),
};
let changes = Self::extract_changes(&similar_diff);
Self {
old,
new,
changes,
algorithm,
word_level: false,
}
}
pub fn word_diff(old: &'a str, new: &'a str) -> Self {
let similar_diff = SimilarTextDiff::from_words(old, new);
let changes = Self::extract_word_changes(&similar_diff);
Self {
old,
new,
changes,
algorithm: DiffAlgorithm::Myers,
word_level: true,
}
}
pub fn char_diff(old: &'a str, new: &'a str) -> Self {
let similar_diff = SimilarTextDiff::from_chars(old, new);
let changes = Self::extract_char_changes(&similar_diff);
Self {
old,
new,
changes,
algorithm: DiffAlgorithm::Myers,
word_level: true, }
}
fn extract_changes(diff: &SimilarTextDiff<'a, 'a, 'a, str>) -> Vec<Change<'a>> {
let mut changes = Vec::new();
let mut old_line = 1usize;
let mut new_line = 1usize;
for change in diff.iter_all_changes() {
let kind = ChangeKind::from(change.tag());
let value = change.value();
let (old_ln, new_ln) = match kind {
ChangeKind::Equal => {
let result = (Some(old_line), Some(new_line));
old_line += 1;
new_line += 1;
result
}
ChangeKind::Delete => {
let result = (Some(old_line), None);
old_line += 1;
result
}
ChangeKind::Insert => {
let result = (None, Some(new_line));
new_line += 1;
result
}
};
changes.push(Change {
kind,
value,
old_line: old_ln,
new_line: new_ln,
});
}
changes
}
fn extract_word_changes(diff: &SimilarTextDiff<'a, 'a, 'a, str>) -> Vec<Change<'a>> {
diff.iter_all_changes()
.map(|change| Change {
kind: ChangeKind::from(change.tag()),
value: change.value(),
old_line: None,
new_line: None,
})
.collect()
}
fn extract_char_changes(diff: &SimilarTextDiff<'a, 'a, 'a, str>) -> Vec<Change<'a>> {
diff.iter_all_changes()
.map(|change| Change {
kind: ChangeKind::from(change.tag()),
value: change.value(),
old_line: None,
new_line: None,
})
.collect()
}
pub fn has_changes(&self) -> bool {
self.changes.iter().any(|c| c.kind != ChangeKind::Equal)
}
pub fn changes(&self) -> &[Change<'a>] {
&self.changes
}
pub fn old_text(&self) -> &'a str {
self.old
}
pub fn new_text(&self) -> &'a str {
self.new
}
pub fn algorithm(&self) -> DiffAlgorithm {
self.algorithm
}
pub fn is_word_level(&self) -> bool {
self.word_level
}
pub fn stats(&self) -> DiffStats {
let mut lines_added = 0usize;
let mut lines_removed = 0usize;
for change in &self.changes {
match change.kind {
ChangeKind::Insert => lines_added += 1,
ChangeKind::Delete => lines_removed += 1,
ChangeKind::Equal => {}
}
}
DiffStats {
lines_added,
lines_removed,
lines_changed: lines_added.min(lines_removed),
total_changes: lines_added + lines_removed,
}
}
pub fn unified(&self, context_lines: usize) -> String {
let diff = SimilarTextDiff::from_lines(self.old, self.new);
diff.unified_diff()
.context_radius(context_lines)
.to_string()
}
pub fn hunks(&self, context_lines: usize) -> Vec<DiffHunk<'a>> {
let mut hunks = Vec::new();
let mut current_hunk: Option<DiffHunk<'a>> = None;
let mut context_buffer: Vec<Change<'a>> = Vec::new();
for change in &self.changes {
match change.kind {
ChangeKind::Equal => {
if let Some(ref mut hunk) = current_hunk {
if context_buffer.len() < context_lines {
hunk.changes.push(change.clone());
context_buffer.push(change.clone());
} else {
hunks.push(current_hunk.take().unwrap());
context_buffer.clear();
}
}
context_buffer.push(change.clone());
if context_buffer.len() > context_lines {
context_buffer.remove(0);
}
}
_ => {
if current_hunk.is_none() {
let start_old =
context_buffer.first().and_then(|c| c.old_line).unwrap_or(1);
let start_new =
context_buffer.first().and_then(|c| c.new_line).unwrap_or(1);
current_hunk = Some(DiffHunk {
old_start: start_old,
new_start: start_new,
changes: context_buffer.clone(),
});
context_buffer.clear();
}
if let Some(ref mut hunk) = current_hunk {
hunk.changes.push(change.clone());
}
}
}
}
if let Some(hunk) = current_hunk {
hunks.push(hunk);
}
hunks
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct DiffStats {
pub lines_added: usize,
pub lines_removed: usize,
pub lines_changed: usize,
pub total_changes: usize,
}
impl DiffStats {
pub fn has_changes(&self) -> bool {
self.total_changes > 0
}
pub fn compact(&self) -> String {
format!("+{} -{}", self.lines_added, self.lines_removed)
}
}
#[derive(Debug, Clone)]
pub struct DiffHunk<'a> {
pub old_start: usize,
pub new_start: usize,
pub changes: Vec<Change<'a>>,
}
impl<'a> DiffHunk<'a> {
pub fn old_range(&self) -> (usize, usize) {
let count = self
.changes
.iter()
.filter(|c| c.kind != ChangeKind::Insert)
.count();
(self.old_start, count)
}
pub fn new_range(&self) -> (usize, usize) {
let count = self
.changes
.iter()
.filter(|c| c.kind != ChangeKind::Delete)
.count();
(self.new_start, count)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_diff_no_changes() {
let text = "hello\nworld\n";
let diff = TextDiff::new(text, text);
assert!(!diff.has_changes());
assert_eq!(diff.stats().total_changes, 0);
}
#[test]
fn test_text_diff_simple_addition() {
let old = "hello\n";
let new = "hello\nworld\n";
let diff = TextDiff::new(old, new);
assert!(diff.has_changes());
let stats = diff.stats();
assert_eq!(stats.lines_added, 1);
assert_eq!(stats.lines_removed, 0);
}
#[test]
fn test_text_diff_simple_removal() {
let old = "hello\nworld\n";
let new = "hello\n";
let diff = TextDiff::new(old, new);
assert!(diff.has_changes());
let stats = diff.stats();
assert_eq!(stats.lines_added, 0);
assert_eq!(stats.lines_removed, 1);
}
#[test]
fn test_text_diff_modification() {
let old = "hello\nworld\n";
let new = "hello\nearth\n";
let diff = TextDiff::new(old, new);
assert!(diff.has_changes());
let stats = diff.stats();
assert_eq!(stats.lines_added, 1);
assert_eq!(stats.lines_removed, 1);
}
#[test]
fn test_word_diff() {
let old = "hello world";
let new = "hello earth";
let diff = TextDiff::word_diff(old, new);
assert!(diff.has_changes());
assert!(diff.is_word_level());
}
#[test]
fn test_diff_stats_compact() {
let stats = DiffStats {
lines_added: 10,
lines_removed: 5,
lines_changed: 5,
total_changes: 15,
};
assert_eq!(stats.compact(), "+10 -5");
}
#[test]
fn test_diff_unified() {
let old = "line1\nline2\nline3\n";
let new = "line1\nmodified\nline3\n";
let diff = TextDiff::new(old, new);
let unified = diff.unified(1);
assert!(unified.contains("-line2"));
assert!(unified.contains("+modified"));
}
#[test]
fn test_diff_hunks() {
let old = "a\nb\nc\nd\ne\n";
let new = "a\nx\nc\nd\ne\n";
let diff = TextDiff::new(old, new);
let hunks = diff.hunks(1);
assert!(!hunks.is_empty());
}
#[test]
fn test_change_kind_from_tag() {
assert_eq!(ChangeKind::from(ChangeTag::Equal), ChangeKind::Equal);
assert_eq!(ChangeKind::from(ChangeTag::Insert), ChangeKind::Insert);
assert_eq!(ChangeKind::from(ChangeTag::Delete), ChangeKind::Delete);
}
}