pub const DIFF_REMOVED_MARKER: &str = " - ";
pub const DIFF_ADDED_MARKER: &str = " + ";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffLineKind {
Context,
Removed,
Added,
}
pub fn parse_diff_line(line: &str) -> DiffLineKind {
let trimmed = line.trim_start();
let after_num = trimmed.trim_start_matches(|c: char| c.is_ascii_digit());
if after_num.starts_with(DIFF_REMOVED_MARKER) {
DiffLineKind::Removed
} else if after_num.starts_with(DIFF_ADDED_MARKER) {
DiffLineKind::Added
} else {
DiffLineKind::Context
}
}
#[derive(Debug, Clone)]
pub(crate) struct DisplayDiff {
pub display_diff: String,
pub added: usize,
pub removed: usize,
pub truncated: bool,
}
const DIFF_CONTEXT_LINES: usize = 3;
pub(crate) const MAX_DISPLAY_DIFF_LINES: usize = 220;
pub(crate) fn generate_display_diff(old: &str, new: &str) -> DisplayDiff {
let old_lines: Vec<&str> = old.lines().collect();
let new_lines: Vec<&str> = new.lines().collect();
let mut prefix = 0usize;
let min_len = old_lines.len().min(new_lines.len());
while prefix < min_len && old_lines[prefix] == new_lines[prefix] {
prefix += 1;
}
let mut suffix = 0usize;
while suffix < min_len.saturating_sub(prefix)
&& old_lines[old_lines.len() - 1 - suffix] == new_lines[new_lines.len() - 1 - suffix]
{
suffix += 1;
}
let old_changed_end = old_lines.len().saturating_sub(suffix);
let new_changed_end = new_lines.len().saturating_sub(suffix);
let old_changed = &old_lines[prefix..old_changed_end];
let new_changed = &new_lines[prefix..new_changed_end];
let added = new_changed.len();
let removed = old_changed.len();
let context_start = prefix.saturating_sub(DIFF_CONTEXT_LINES);
let context_end_old = (old_changed_end + DIFF_CONTEXT_LINES).min(old_lines.len());
let mut lines = Vec::new();
let mut truncated = false;
let push_line = |line: String, lines: &mut Vec<String>, truncated: &mut bool| {
if lines.len() < MAX_DISPLAY_DIFF_LINES {
lines.push(line);
} else {
*truncated = true;
}
};
for (idx, line) in old_lines[context_start..prefix].iter().enumerate() {
push_line(
format!("{:>4} {}", context_start + idx + 1, line),
&mut lines,
&mut truncated,
);
}
for (idx, line) in old_changed.iter().enumerate() {
push_line(
format!("{:>4}{}{}", prefix + idx + 1, DIFF_REMOVED_MARKER, line),
&mut lines,
&mut truncated,
);
}
for (idx, line) in new_changed.iter().enumerate() {
push_line(
format!("{:>4}{}{}", prefix + idx + 1, DIFF_ADDED_MARKER, line),
&mut lines,
&mut truncated,
);
}
for (idx, line) in old_lines[old_changed_end..context_end_old]
.iter()
.enumerate()
{
push_line(
format!("{:>4} {}", old_changed_end + idx + 1, line),
&mut lines,
&mut truncated,
);
}
if truncated {
lines.push(format!(
"... diff truncated after {MAX_DISPLAY_DIFF_LINES} display lines"
));
}
DisplayDiff {
display_diff: lines.join("\n"),
added,
removed,
truncated,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_added_line() {
let line = format!(" 5{}fn main() {{", DIFF_ADDED_MARKER);
assert_eq!(parse_diff_line(&line), DiffLineKind::Added);
}
#[test]
fn parses_removed_line() {
let line = format!(" 12{}old = true;", DIFF_REMOVED_MARKER);
assert_eq!(parse_diff_line(&line), DiffLineKind::Removed);
}
#[test]
fn parses_context_line() {
let line = " 7 existing line";
assert_eq!(parse_diff_line(line), DiffLineKind::Context);
}
#[test]
fn malformed_falls_through_to_context() {
assert_eq!(parse_diff_line("no number at start"), DiffLineKind::Context);
assert_eq!(parse_diff_line(""), DiffLineKind::Context);
}
}