feat: basic pr diff rendering
This commit is contained in:
284
src/util/diff.rs
284
src/util/diff.rs
@@ -1,12 +1,7 @@
|
||||
use std::ops::Range;
|
||||
use std::{ops::Range, slice::Iter, sync::Arc, thread::current};
|
||||
|
||||
pub(crate) struct ContentDiff {
|
||||
pub(crate) old_content: bytes::Bytes,
|
||||
pub(crate) new_content: bytes::Bytes,
|
||||
pub(crate) spans: Vec<Span>,
|
||||
old_line_ranges: Vec<Range<usize>>,
|
||||
new_line_ranges: Vec<Range<usize>>,
|
||||
}
|
||||
use memchr::{memchr2, memchr2_iter, memchr3_iter};
|
||||
use similar::DiffableStr;
|
||||
|
||||
pub(crate) struct Span {
|
||||
pub(crate) op: Op,
|
||||
@@ -14,7 +9,7 @@ pub(crate) struct Span {
|
||||
pub(crate) new_range: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Op {
|
||||
Equal,
|
||||
Delete,
|
||||
@@ -37,147 +32,184 @@ pub(crate) struct DiffRow {
|
||||
pub(crate) new_content_range: Option<Range<usize>>,
|
||||
}
|
||||
|
||||
pub(crate) fn diff_content(old_content: bytes::Bytes, new_content: bytes::Bytes) -> ContentDiff {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DiffLine {
|
||||
pub(crate) op: Op,
|
||||
pub(crate) old_content: Option<Arc<str>>,
|
||||
pub(crate) old_line: usize,
|
||||
pub(crate) new_content: Option<Arc<str>>,
|
||||
pub(crate) new_line: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ContentDiff(Vec<DiffLine>);
|
||||
|
||||
pub(crate) fn diff_content(
|
||||
old_content: bytes::Bytes,
|
||||
new_content: bytes::Bytes,
|
||||
) -> Option<ContentDiff> {
|
||||
let old_line_ranges = line_ranges(&old_content);
|
||||
let new_line_ranges = line_ranges(&new_content);
|
||||
let diff = similar::TextDiff::from_lines::<[u8]>(&old_content, &new_content);
|
||||
|
||||
let spans = diff
|
||||
.ops()
|
||||
.iter()
|
||||
.map(|op| match op {
|
||||
| &similar::DiffOp::Equal {
|
||||
old_index,
|
||||
new_index,
|
||||
len,
|
||||
} => Span {
|
||||
op: Op::Equal,
|
||||
old_range: old_index..(old_index + len),
|
||||
new_range: new_index..(new_index + len),
|
||||
},
|
||||
let mut diff_lines: Vec<DiffLine> = Vec::new();
|
||||
|
||||
| &similar::DiffOp::Delete {
|
||||
old_index,
|
||||
old_len,
|
||||
new_index,
|
||||
} => Span {
|
||||
op: Op::Delete,
|
||||
old_range: old_index..(old_index + old_len),
|
||||
new_range: new_index..new_index,
|
||||
},
|
||||
for op in diff.ops() {
|
||||
match op {
|
||||
| &similar::DiffOp::Equal {
|
||||
old_index,
|
||||
new_index,
|
||||
len,
|
||||
} => {
|
||||
for i in 0..len {
|
||||
let old_line = old_index + i;
|
||||
let new_line = new_index + i;
|
||||
let old_line_range = &old_line_ranges[old_line];
|
||||
let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?);
|
||||
diff_lines.push(DiffLine {
|
||||
op: Op::Equal,
|
||||
old_line,
|
||||
old_content: Some(Arc::clone(&content)),
|
||||
new_line,
|
||||
new_content: Some(content),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
| &similar::DiffOp::Insert {
|
||||
old_index,
|
||||
new_index,
|
||||
new_len,
|
||||
} => Span {
|
||||
op: Op::Insert,
|
||||
old_range: old_index..old_index,
|
||||
new_range: new_index..(new_index + new_len),
|
||||
},
|
||||
| &similar::DiffOp::Insert {
|
||||
old_index,
|
||||
new_index,
|
||||
new_len,
|
||||
} => {
|
||||
for i in 0..new_len {
|
||||
let new_line_range = &new_line_ranges[new_index + i];
|
||||
let content = Arc::from(new_content.slice(new_line_range.clone()).as_str()?);
|
||||
diff_lines.push(DiffLine {
|
||||
op: Op::Insert,
|
||||
old_line: old_index,
|
||||
old_content: None,
|
||||
new_line: new_index + i,
|
||||
new_content: Some(content),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
| &similar::DiffOp::Replace {
|
||||
old_index,
|
||||
old_len,
|
||||
new_index,
|
||||
new_len,
|
||||
} => Span {
|
||||
op: Op::Replace,
|
||||
old_range: old_index..(old_index + old_len),
|
||||
new_range: new_index..(new_index + new_len),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
| &similar::DiffOp::Replace {
|
||||
old_index,
|
||||
old_len,
|
||||
new_index,
|
||||
new_len,
|
||||
} => {
|
||||
for i in 0..new_len.max(old_len) {
|
||||
let old_line = old_index + i;
|
||||
let new_line = new_index + i;
|
||||
|
||||
ContentDiff {
|
||||
old_content,
|
||||
new_content,
|
||||
spans,
|
||||
old_line_ranges,
|
||||
new_line_ranges,
|
||||
let diff_line = match (old_line_ranges.get(old_line), new_line_ranges.get(new_line))
|
||||
{
|
||||
| (Some(old_range), Some(new_range)) => DiffLine {
|
||||
op: Op::Replace,
|
||||
old_line,
|
||||
old_content: Some(Arc::from(old_content.slice(old_range.clone()).as_str()?)),
|
||||
new_line: new_index + i,
|
||||
new_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)),
|
||||
},
|
||||
|
||||
| (None, Some(new_range)) => DiffLine {
|
||||
op: Op::Replace,
|
||||
old_line: old_index + old_len,
|
||||
old_content: None,
|
||||
new_line: new_index + i,
|
||||
new_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)),
|
||||
},
|
||||
|
||||
| (Some(old_range), None) => DiffLine {
|
||||
op: Op::Replace,
|
||||
old_line: old_index + i,
|
||||
old_content: Some(Arc::from(old_content.slice(old_range.clone()).as_str()?)),
|
||||
new_line: new_index + new_len,
|
||||
new_content: None,
|
||||
},
|
||||
|
||||
| (None, None) => {
|
||||
// unlickly to happen, but if it does, idk
|
||||
panic!(
|
||||
"the unlikely happened: both old & new index of DiffOps::Replace don't point to any line in the parsed line ranges."
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
diff_lines.push(diff_line);
|
||||
}
|
||||
}
|
||||
|
||||
| &similar::DiffOp::Delete {
|
||||
old_index,
|
||||
old_len,
|
||||
new_index,
|
||||
} => {
|
||||
for i in 0..old_len {
|
||||
let old_line_range = &old_line_ranges[old_index];
|
||||
let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?);
|
||||
diff_lines.push(DiffLine {
|
||||
op: Op::Delete,
|
||||
old_line: old_index + i,
|
||||
old_content: Some(content),
|
||||
new_line: new_index,
|
||||
new_content: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(ContentDiff(diff_lines))
|
||||
}
|
||||
|
||||
impl ContentDiff {
|
||||
pub(crate) fn spans(&self) -> &[Span] {
|
||||
&self.spans
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub(crate) fn old_line_count(&self) -> usize {
|
||||
self.old_line_ranges.len()
|
||||
pub(crate) fn get(&self, i: usize) -> &DiffLine {
|
||||
&self.0[i]
|
||||
}
|
||||
|
||||
pub(crate) fn new_line_count(&self) -> usize {
|
||||
self.new_line_ranges.len()
|
||||
}
|
||||
|
||||
pub(crate) fn line_slice(&self, side: DiffSide, range: &Range<usize>) -> &[u8] {
|
||||
match side {
|
||||
| DiffSide::Old => &self.old_content[range.clone()],
|
||||
| DiffSide::New => &self.new_content[range.clone()],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn line_slice_at(&self, side: DiffSide, line: usize) -> &[u8] {
|
||||
match side {
|
||||
| DiffSide::Old => self.line_slice(DiffSide::Old, &self.old_line_ranges[line]),
|
||||
| DiffSide::New => self.line_slice(DiffSide::New, &self.new_line_ranges[line]),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rows_for_span(&self, span_index: usize) -> Vec<DiffRow> {
|
||||
let span = &self.spans[span_index];
|
||||
let old_len = span.old_range.end.saturating_sub(span.old_range.start);
|
||||
let new_len = span.new_range.end.saturating_sub(span.new_range.start);
|
||||
let row_count = old_len.max(new_len);
|
||||
|
||||
let mut rows = Vec::with_capacity(row_count);
|
||||
for offset in 0..row_count {
|
||||
let old_line = (offset < old_len).then_some(span.old_range.start + offset);
|
||||
let new_line = (offset < new_len).then_some(span.new_range.start + offset);
|
||||
|
||||
rows.push(DiffRow {
|
||||
op_index: span_index,
|
||||
op: span.op,
|
||||
old_line,
|
||||
old_content_range: old_line.map(|line| self.old_line_ranges[line].clone()),
|
||||
new_line,
|
||||
new_content_range: new_line.map(|line| self.new_line_ranges[line].clone()),
|
||||
});
|
||||
}
|
||||
|
||||
rows
|
||||
pub(crate) fn last(&self) -> Option<&DiffLine> {
|
||||
self.0.last()
|
||||
}
|
||||
}
|
||||
|
||||
fn line_ranges(content: &[u8]) -> Vec<Range<usize>> {
|
||||
let mut ranges = Vec::new();
|
||||
let mut start = 0;
|
||||
let mut index = 0;
|
||||
let mut ranges: Vec<std::ops::Range<usize>> = Vec::new();
|
||||
let mut line_start: usize = 0;
|
||||
let mut skip_next = false;
|
||||
|
||||
while index < content.len() {
|
||||
match content[index] {
|
||||
| b'\r' => {
|
||||
index += 1;
|
||||
if index < content.len() && content[index] == b'\n' {
|
||||
index += 1;
|
||||
}
|
||||
ranges.push(start..index);
|
||||
start = index;
|
||||
}
|
||||
| b'\n' => {
|
||||
index += 1;
|
||||
ranges.push(start..index);
|
||||
start = index;
|
||||
}
|
||||
| _ => {
|
||||
index += 1;
|
||||
}
|
||||
for i in memchr2_iter(b'\n', b'\r', content) {
|
||||
if skip_next {
|
||||
skip_next = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let c = content[i];
|
||||
|
||||
match (c, content.get(i + 1)) {
|
||||
| (b'\r', Some(b'\n')) => {
|
||||
// if \r found, check if its \r\n or if its a lone \r
|
||||
// if \r\n, then treat as one line break
|
||||
ranges.push(line_start..i + 1);
|
||||
// because we already counted the \n byte, the next iter into it needs to be skipped
|
||||
skip_next = true;
|
||||
line_start = i + 2;
|
||||
}
|
||||
| _ => {
|
||||
ranges.push(line_start..i);
|
||||
line_start = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if start < content.len() {
|
||||
ranges.push(start..content.len());
|
||||
if line_start < content.len() {
|
||||
ranges.push(line_start..content.len());
|
||||
}
|
||||
|
||||
ranges
|
||||
|
||||
@@ -5,18 +5,6 @@ pub(crate) enum ContentType {
|
||||
Binary,
|
||||
}
|
||||
|
||||
pub(crate) struct ContentDiff {
|
||||
old_content: bytes::Bytes,
|
||||
new_content: bytes::Bytes,
|
||||
}
|
||||
|
||||
pub(crate) struct LineDiff {
|
||||
old_line: Option<usize>,
|
||||
old_content_range: std::ops::Range<usize>,
|
||||
new_line: Option<usize>,
|
||||
new_content_range: std::ops::Range<usize>,
|
||||
}
|
||||
|
||||
pub(crate) fn classify_content(content: &[u8]) -> ContentType {
|
||||
if content.is_empty() {
|
||||
ContentType::Text
|
||||
@@ -28,9 +16,9 @@ pub(crate) fn classify_content(content: &[u8]) -> ContentType {
|
||||
{
|
||||
ContentType::Text
|
||||
} else {
|
||||
match memchr(0, &content[0..8192]) {
|
||||
| None => ContentType::Text,
|
||||
| Some(_) => ContentType::Binary,
|
||||
match memchr(0, &content[..content.len().min(8192)]) {
|
||||
| None => ContentType::Text,
|
||||
| Some(_) => ContentType::Binary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user