use super::{DIMMED_STYLE, LEFT_STYLE, RIGHT_STYLE};
pub struct MultiLineDiff<'a> {
line_diffs: Vec<LineDiff<'a>>,
}
impl<'a> MultiLineDiff<'a> {
pub fn new(left: &'a str, right: &'a str) -> Self {
let line_diffs = LineDiff::from_diff(diff::lines(left, right));
Self {
line_diffs
}
}
pub fn write_interleaved(&self, writer: &mut super::writer::WrappingWriter<'_>) {
for diff in &self.line_diffs {
match *diff {
LineDiff::LeftOnly(left) => {
writer.write_styled("< ", LEFT_STYLE);
writer.write_styled(left, LEFT_STYLE);
writer.flush_line();
},
LineDiff::RightOnly(right) => {
writer.write_styled("> ", RIGHT_STYLE);
writer.write_styled(right, RIGHT_STYLE);
writer.flush_line();
},
LineDiff::Different(left, right) => {
let diff = SingleLineDiff::new(left, right);
writer.write_styled("< ", diff.left_highlights.normal);
diff.write_left(writer);
writer.flush_line();
writer.write_styled("> ", diff.right_highlights.normal);
diff.write_right(writer);
writer.flush_line();
},
LineDiff::Equal(text) => {
writer.write(" ");
writer.write_styled(text, DIMMED_STYLE);
writer.flush_line();
},
}
}
writer.buffer_mut().pop();
}
}
enum LineDiff<'a> {
LeftOnly(&'a str),
RightOnly(&'a str),
Different(&'a str, &'a str),
Equal(&'a str),
}
impl<'a> LineDiff<'a> {
fn from_diff(diffs: Vec<diff::Result<&'a str>>) -> Vec<Self> {
let mut output = Vec::with_capacity(diffs.len());
let mut seen_left = 0;
for item in diffs {
match item {
diff::Result::Left(l) => {
output.push(LineDiff::LeftOnly(l));
seen_left += 1;
},
diff::Result::Right(r) => {
if let Some(last) = output.last_mut() {
match last {
Self::LeftOnly(old_l) if seen_left == 1 => {
*last = Self::Different(old_l, r);
seen_left = 0;
continue;
},
Self::Different(old_l, old_r) => {
let old_r = *old_r;
*last = Self::LeftOnly(old_l);
output.push(Self::RightOnly(old_r));
output.push(Self::RightOnly(r));
seen_left = 0;
continue;
},
Self::LeftOnly(_) => (),
Self::RightOnly(_) => (),
Self::Equal(_) => (),
}
}
output.push(LineDiff::RightOnly(r));
seen_left = 0;
},
diff::Result::Both(l, _r) => {
output.push(Self::Equal(l));
seen_left = 0;
}
}
}
output
}
}
pub struct SingleLineDiff<'a> {
left: &'a str,
right: &'a str,
left_highlights: Highlighter,
right_highlights: Highlighter,
}
impl<'a> SingleLineDiff<'a> {
pub fn new(left: &'a str, right: &'a str) -> Self {
let left_words = Self::split_words(left);
let right_words = Self::split_words(right);
let diffs = diff::slice(&left_words, &right_words);
let mut left_highlights = Highlighter::new(yansi::Color::Cyan);
let mut right_highlights = Highlighter::new(yansi::Color::Yellow);
for diff in &diffs {
match diff {
diff::Result::Left(left) => {
left_highlights.push(left.len(), true);
},
diff::Result::Right(right) => {
right_highlights.push(right.len(), true);
},
diff::Result::Both(left, right) => {
left_highlights.push(left.len(), false);
right_highlights.push(right.len(), false);
}
}
}
Self {
left,
right,
left_highlights,
right_highlights,
}
}
pub fn write_left(&self, writer: &mut super::writer::WrappingWriter) {
self.left_highlights.write_highlighted(writer, self.left);
}
pub fn write_right(&self, writer: &mut super::writer::WrappingWriter) {
self.right_highlights.write_highlighted(writer, self.right);
}
fn split_words(mut input: &str) -> Vec<&str> {
fn is_break_point(a: char, b: char) -> bool {
if a.is_alphabetic() {
!b.is_alphabetic() || (a.is_lowercase() && !b.is_lowercase())
} else if a.is_ascii_digit() {
!b.is_ascii_digit()
} else if a.is_whitespace() {
!b.is_whitespace()
} else {
true
}
}
let mut output = Vec::new();
while !input.is_empty() {
let split = input.chars()
.zip(input.char_indices().skip(1))
.find_map(|(a, (pos, b))| Some(pos).filter(|_| is_break_point(a, b)))
.unwrap_or(input.len());
let (head, tail) = input.split_at(split);
output.push(head);
input = tail;
}
output
}
}
struct Highlighter {
ranges: Vec<(bool, std::ops::Range<usize>)>,
total_highlighted: usize,
normal: yansi::Style,
highlight: yansi::Style,
}
impl Highlighter {
fn new(color: yansi::Color) -> Self {
let normal = yansi::Style::new().fg(color);
let highlight = yansi::Style::new().fg(yansi::Color::Black).bg(color).bold();
Self {
ranges: Vec::new(),
total_highlighted: 0,
normal,
highlight,
}
}
fn push(&mut self, len: usize, highlight: bool) {
if highlight {
self.total_highlighted += len;
}
if let Some(last) = self.ranges.last_mut() {
if last.0 == highlight {
last.1.end += len;
} else {
let start = last.1.end;
self.ranges.push((highlight, start..start + len));
}
} else {
self.ranges.push((highlight, 0..len))
}
}
fn write_highlighted(&self, writer: &mut super::writer::WrappingWriter, data: &str) {
let not_highlighted = data.len() - self.total_highlighted;
if not_highlighted < div_ceil(self.total_highlighted, 2) {
writer.write_styled(data, self.normal);
} else {
for (highlight, range) in self.ranges.iter().cloned() {
if highlight {
writer.write_styled(&data[range], self.highlight);
} else {
writer.write_styled(&data[range], self.normal);
};
}
}
}
}
fn div_ceil(a: usize, b: usize) -> usize {
if b == 0 {
a / b
} else {
let d = a / b;
let r = a % b;
if r > 0 {
d + 1
} else {
d
}
}
}
#[test]
fn test_div_ceil() {
use crate::assert;
assert!(div_ceil(0, 2) == 0);
assert!(div_ceil(1, 2) == 1);
assert!(div_ceil(2, 2) == 1);
assert!(div_ceil(3, 2) == 2);
assert!(div_ceil(4, 2) == 2);
assert!(div_ceil(20, 7) == 3);
assert!(div_ceil(21, 7) == 3);
assert!(div_ceil(22, 7) == 4);
assert!(div_ceil(27, 7) == 4);
assert!(div_ceil(28, 7) == 4);
assert!(div_ceil(29, 7) == 5);
}