use similar::{ChangeTag, TextDiff};
use crate::model::{Line, LineKind};
#[must_use]
pub fn as_text(bytes: &[u8]) -> Option<&str> {
let text = str::from_utf8(bytes).ok()?;
if text.contains('\0') {
return None;
}
Some(text)
}
pub const NO_NEWLINE_MARKER: &str = "\\ no newline at end of file";
#[must_use]
pub fn diff_lines(before: &str, after: &str) -> Vec<Line> {
let mut lines = Vec::new();
for change in TextDiff::from_lines(before, after).iter_all_changes() {
let kind = match change.tag() {
ChangeTag::Delete => LineKind::Removed,
ChangeTag::Insert => LineKind::Added,
ChangeTag::Equal => continue,
};
let raw = change.value();
let text = match raw.strip_suffix('\n') {
Some(stripped) => stripped,
None => raw,
};
let newline_missing = !raw.ends_with('\n');
lines.push(Line {
kind,
text: text.to_owned(),
});
if newline_missing {
lines.push(Line {
kind: LineKind::NoNewline,
text: NO_NEWLINE_MARKER.to_owned(),
});
}
}
lines
}
#[cfg(test)]
mod tests {
use super::*;
fn line(kind: LineKind, text: &str) -> Line {
Line {
kind,
text: text.to_owned(),
}
}
#[test]
fn valid_utf8_is_text() {
assert_eq!(as_text(b"hello\n"), Some("hello\n"));
}
#[test]
fn invalid_utf8_and_nul_bytes_are_not_text() {
assert_eq!(as_text(&[0xff, 0xfe]), None);
assert_eq!(as_text(b"he\0llo"), None);
}
#[test]
fn identical_texts_yield_no_lines() {
assert!(diff_lines("a\nb\n", "a\nb\n").is_empty());
}
#[test]
fn an_edited_line_yields_its_removal_and_addition() {
let lines = diff_lines("a\nold\nc\n", "a\nnew\nc\n");
assert_eq!(
lines,
vec![line(LineKind::Removed, "old"), line(LineKind::Added, "new")]
);
}
#[test]
fn growth_from_empty_yields_only_additions() {
let lines = diff_lines("", "a\nb\n");
assert_eq!(
lines,
vec![line(LineKind::Added, "a"), line(LineKind::Added, "b")]
);
}
#[test]
fn carriage_returns_stay_visible_in_carried_lines() {
let lines = diff_lines("same\n", "same\r\n");
assert_eq!(
lines,
vec![
line(LineKind::Removed, "same"),
line(LineKind::Added, "same\r")
]
);
}
#[test]
fn a_missing_terminal_newline_carries_the_git_marker() {
let lines = diff_lines("same\n", "same");
assert_eq!(
lines,
vec![
line(LineKind::Removed, "same"),
line(LineKind::Added, "same"),
line(LineKind::NoNewline, NO_NEWLINE_MARKER),
]
);
}
}