Added initial support for inline diff highlighting
This commit is contained in:
parent
182a998e0f
commit
b601164a60
6 changed files with 245 additions and 50 deletions
|
|
@ -28,6 +28,10 @@ unicode-segmentation = { version = "1.7.1", optional = true }
|
|||
name = "terminal"
|
||||
required-features = ["text"]
|
||||
|
||||
[[example]]
|
||||
name = "terminal-inline"
|
||||
required-features = ["text"]
|
||||
|
||||
[[example]]
|
||||
name = "udiff"
|
||||
required-features = ["text"]
|
||||
|
|
|
|||
30
examples/terminal-inline.rs
Normal file
30
examples/terminal-inline.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use console::Style;
|
||||
use similar::text::{ChangeTag, TextDiff};
|
||||
|
||||
fn main() {
|
||||
let diff = TextDiff::from_lines(
|
||||
"Hello World\nThis is the second line.\nThis is the third.",
|
||||
"Hallo Welt\nThis is the second line.\nThis is life.\nMoar and more",
|
||||
);
|
||||
|
||||
for op in diff.ops() {
|
||||
for change in diff.iter_inline_changes(op) {
|
||||
let (sign, style) = match change.tag() {
|
||||
ChangeTag::Delete => ("-", Style::new().red()),
|
||||
ChangeTag::Insert => ("+", Style::new().green()),
|
||||
ChangeTag::Equal => (" ", Style::new()),
|
||||
};
|
||||
print!("{}", style.apply_to(sign).bold(),);
|
||||
for &(emphasized, value) in change.values() {
|
||||
if emphasized {
|
||||
print!("{}", style.apply_to(value).underlined());
|
||||
} else {
|
||||
print!("{}", style.apply_to(value));
|
||||
}
|
||||
}
|
||||
if change.is_missing_newline() {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,11 +14,7 @@ fn main() {
|
|||
ChangeTag::Insert => ("+", Style::new().green()),
|
||||
ChangeTag::Equal => (" ", Style::new()),
|
||||
};
|
||||
print!(
|
||||
"{}{}",
|
||||
style.apply_to(sign).bold(),
|
||||
style.apply_to(change.value())
|
||||
);
|
||||
print!("{}{}", style.apply_to(sign).bold(), style.apply_to(change),);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
165
src/text/inline.rs
Normal file
165
src/text/inline.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
use std::iter;
|
||||
|
||||
use crate::algorithms::{DiffOp, DiffTag};
|
||||
use crate::text::{Change, ChangeTag, TextDiff};
|
||||
|
||||
use super::split_chars;
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
struct MultiIndex<'a, 's> {
|
||||
seq: &'a [&'s str],
|
||||
value: &'s str,
|
||||
}
|
||||
|
||||
impl<'a, 's> MultiIndex<'a, 's> {
|
||||
pub fn new(seq: &'a [&'s str], value: &'s str) -> MultiIndex<'a, 's> {
|
||||
MultiIndex { seq, value }
|
||||
}
|
||||
|
||||
pub fn get_slice(&self, rng: Range<usize>) -> &'s str {
|
||||
let mut start = 0;
|
||||
for &sseq in &self.seq[..rng.start] {
|
||||
start += sseq.len();
|
||||
}
|
||||
let mut end = start;
|
||||
for &sseq in &self.seq[rng.start..rng.end] {
|
||||
end += sseq.len();
|
||||
}
|
||||
&self.value[start..end]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Ord, PartialOrd)]
|
||||
pub struct InlineChange<'s> {
|
||||
tag: ChangeTag,
|
||||
old_index: Option<usize>,
|
||||
new_index: Option<usize>,
|
||||
values: Vec<(bool, &'s str)>,
|
||||
missing_newline: bool,
|
||||
}
|
||||
|
||||
impl<'s> InlineChange<'s> {
|
||||
/// Returns the change tag.
|
||||
pub fn tag(&self) -> ChangeTag {
|
||||
self.tag
|
||||
}
|
||||
|
||||
/// Returns the old index if available.
|
||||
pub fn old_index(&self) -> Option<usize> {
|
||||
self.old_index
|
||||
}
|
||||
|
||||
/// Returns the new index if available.
|
||||
pub fn new_index(&self) -> Option<usize> {
|
||||
self.new_index
|
||||
}
|
||||
|
||||
/// Returns the changed values.
|
||||
pub fn values(&self) -> &[(bool, &'s str)] {
|
||||
&self.values
|
||||
}
|
||||
|
||||
/// Returns `true` if this change needs to be followed up by a
|
||||
/// missing newline.
|
||||
pub fn is_missing_newline(&self) -> bool {
|
||||
self.missing_newline
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> From<Change<'s>> for InlineChange<'s> {
|
||||
fn from(change: Change<'s>) -> InlineChange<'s> {
|
||||
InlineChange {
|
||||
tag: change.tag(),
|
||||
old_index: change.old_index(),
|
||||
new_index: change.old_index(),
|
||||
values: vec![(false, change.value())],
|
||||
missing_newline: change.is_missing_newline(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn iter_inline_changes<'diff>(
|
||||
diff: &'diff TextDiff,
|
||||
op: &DiffOp,
|
||||
) -> impl Iterator<Item = InlineChange<'diff>> {
|
||||
let mut change_iter = diff.iter_changes(op).peekable();
|
||||
let mut skip_next = false;
|
||||
let newline_terminated = diff.newline_terminated;
|
||||
|
||||
iter::from_fn(move || {
|
||||
if skip_next {
|
||||
change_iter.next();
|
||||
skip_next = false;
|
||||
}
|
||||
if let Some(change) = change_iter.next() {
|
||||
let next_change = change_iter.peek();
|
||||
match (change.tag, next_change.map(|x| x.tag())) {
|
||||
(ChangeTag::Delete, Some(ChangeTag::Insert)) => {
|
||||
let old_value = change.value();
|
||||
let new_value = next_change.unwrap().value();
|
||||
let old_chars = split_chars(&old_value).collect::<Vec<_>>();
|
||||
let new_chars = split_chars(&new_value).collect::<Vec<_>>();
|
||||
let old_mindex = MultiIndex::new(&old_chars, old_value);
|
||||
let new_mindex = MultiIndex::new(&new_chars, new_value);
|
||||
let inline_diff = TextDiff::from_slices(&old_chars, &new_chars);
|
||||
|
||||
if inline_diff.ratio() < 0.5 {
|
||||
return Some(None.into_iter().chain(Some(change.into()).into_iter()));
|
||||
}
|
||||
|
||||
// skip the next element as we handle it here
|
||||
skip_next = true;
|
||||
|
||||
let mut old_values = vec![];
|
||||
let mut new_values = vec![];
|
||||
for op in inline_diff.ops() {
|
||||
match op.tag() {
|
||||
DiffTag::Equal => {
|
||||
old_values.push((false, old_mindex.get_slice(op.old_range())));
|
||||
new_values.push((false, old_mindex.get_slice(op.old_range())));
|
||||
}
|
||||
DiffTag::Delete => {
|
||||
old_values.push((true, old_mindex.get_slice(op.old_range())));
|
||||
}
|
||||
DiffTag::Insert => {
|
||||
new_values.push((true, new_mindex.get_slice(op.new_range())));
|
||||
}
|
||||
DiffTag::Replace => {
|
||||
old_values.push((true, old_mindex.get_slice(op.old_range())));
|
||||
new_values.push((true, new_mindex.get_slice(op.new_range())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(
|
||||
Some(InlineChange {
|
||||
tag: ChangeTag::Delete,
|
||||
old_index: change.old_index(),
|
||||
new_index: change.new_index(),
|
||||
values: old_values,
|
||||
missing_newline: newline_terminated
|
||||
&& !old_value.ends_with(&['\r', '\n'][..]),
|
||||
})
|
||||
.into_iter()
|
||||
.chain(
|
||||
Some(InlineChange {
|
||||
tag: ChangeTag::Insert,
|
||||
old_index: change.old_index(),
|
||||
new_index: change.new_index(),
|
||||
values: new_values,
|
||||
missing_newline: newline_terminated
|
||||
&& !new_value.ends_with(&['\r', '\n'][..]),
|
||||
})
|
||||
.into_iter(),
|
||||
),
|
||||
)
|
||||
}
|
||||
_ => Some(None.into_iter().chain(Some(change.into()).into_iter())),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
|
@ -54,8 +54,12 @@
|
|||
use std::borrow::Cow;
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::{BinaryHeap, HashMap};
|
||||
use std::fmt;
|
||||
|
||||
mod inline;
|
||||
mod udiff;
|
||||
|
||||
pub use self::inline::*;
|
||||
pub use self::udiff::*;
|
||||
|
||||
use crate::algorithms::{capture_diff_slices, group_diff_ops, Algorithm, DiffOp, DiffTag};
|
||||
|
|
@ -209,6 +213,18 @@ pub struct Change<'s> {
|
|||
old_index: Option<usize>,
|
||||
new_index: Option<usize>,
|
||||
value: &'s str,
|
||||
missing_newline: bool,
|
||||
}
|
||||
|
||||
impl<'s> fmt::Display for Change<'s> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}{}",
|
||||
self.value(),
|
||||
if self.missing_newline { "\n" } else { "" }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> Change<'s> {
|
||||
|
|
@ -232,29 +248,16 @@ impl<'s> Change<'s> {
|
|||
self.value
|
||||
}
|
||||
|
||||
/// Returns `true` for virtual changes.
|
||||
/// Returns `true` if this change needs to be followed up by a
|
||||
/// missing newline.
|
||||
///
|
||||
/// Virtual changes are changes that do not exist in either diff but are
|
||||
/// necessary for a consistent user experience. This currently only
|
||||
/// applies to changes related to newline handling. If lines are passed
|
||||
/// to the [`TextDiff`] the [`TextDiff::newline_terminated`] flag is set
|
||||
/// in which case newlines of the input are included in the changes. However
|
||||
/// if the trailing newline is missing it would mess up processing greatly.
|
||||
/// Because of this a trailing virtual newline is automatically added for a
|
||||
/// more consistent user experience. This virtual newline can be detected
|
||||
/// by explicitly checking for this flag.
|
||||
pub fn is_virtual(&self) -> bool {
|
||||
self.old_index.is_none() && self.new_index.is_none()
|
||||
/// The [`std::fmt::Display`] implementation of [`Change`] will automatically
|
||||
/// insert a newline after the value if this is true.
|
||||
pub fn is_missing_newline(&self) -> bool {
|
||||
self.missing_newline
|
||||
}
|
||||
}
|
||||
|
||||
const VIRTUAL_NEWLINE_CHANGE: Change<'static> = Change {
|
||||
tag: ChangeTag::Equal,
|
||||
old_index: None,
|
||||
new_index: None,
|
||||
value: "\n",
|
||||
};
|
||||
|
||||
impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
||||
/// Configures a text differ before diffing.
|
||||
pub fn configure() -> TextDiffConfig {
|
||||
|
|
@ -374,21 +377,6 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
|||
let mut old_slices = &self.old_slices()[op.old_range()];
|
||||
let mut new_slices = &self.new_slices()[op.new_range()];
|
||||
|
||||
// figure out if a virtual newline has to be inserted
|
||||
let mut virtual_newline = if newline_terminated {
|
||||
let last_element = match tag {
|
||||
DiffTag::Equal | DiffTag::Delete | DiffTag::Replace => old_slices.last(),
|
||||
DiffTag::Insert => new_slices.last(),
|
||||
};
|
||||
if !last_element.map_or(false, |x| x.ends_with(&['\r', '\n'][..])) {
|
||||
Some(VIRTUAL_NEWLINE_CHANGE)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
std::iter::from_fn(move || match tag {
|
||||
DiffTag::Equal => {
|
||||
if let Some((&first, rest)) = old_slices.split_first() {
|
||||
|
|
@ -400,9 +388,12 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
|||
old_index: Some(old_index - 1),
|
||||
new_index: Some(new_index - 1),
|
||||
value: first,
|
||||
missing_newline: newline_terminated
|
||||
&& rest.is_empty()
|
||||
&& !first.ends_with(&['\r', '\n'][..]),
|
||||
})
|
||||
} else {
|
||||
virtual_newline.take()
|
||||
None
|
||||
}
|
||||
}
|
||||
DiffTag::Delete => {
|
||||
|
|
@ -414,9 +405,12 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
|||
old_index: Some(old_index - 1),
|
||||
new_index: None,
|
||||
value: first,
|
||||
missing_newline: newline_terminated
|
||||
&& rest.is_empty()
|
||||
&& !first.ends_with(&['\r', '\n'][..]),
|
||||
})
|
||||
} else {
|
||||
virtual_newline.take()
|
||||
None
|
||||
}
|
||||
}
|
||||
DiffTag::Insert => {
|
||||
|
|
@ -428,9 +422,12 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
|||
old_index: None,
|
||||
new_index: Some(new_index - 1),
|
||||
value: first,
|
||||
missing_newline: newline_terminated
|
||||
&& rest.is_empty()
|
||||
&& !first.ends_with(&['\r', '\n'][..]),
|
||||
})
|
||||
} else {
|
||||
virtual_newline.take()
|
||||
None
|
||||
}
|
||||
}
|
||||
DiffTag::Replace => {
|
||||
|
|
@ -442,22 +439,21 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
|||
old_index: Some(old_index - 1),
|
||||
new_index: None,
|
||||
value: first,
|
||||
missing_newline: newline_terminated
|
||||
&& rest.is_empty()
|
||||
&& !first.ends_with(&['\r', '\n'][..]),
|
||||
})
|
||||
} else if let Some(virtual_newline) = virtual_newline.take() {
|
||||
Some(virtual_newline)
|
||||
} else if let Some((&first, rest)) = new_slices.split_first() {
|
||||
new_slices = rest;
|
||||
new_index += 1;
|
||||
// check for another virtual newline
|
||||
if newline_terminated && rest.is_empty() && !first.ends_with(&['\r', '\n'][..])
|
||||
{
|
||||
virtual_newline = Some(VIRTUAL_NEWLINE_CHANGE);
|
||||
}
|
||||
Some(Change {
|
||||
tag: ChangeTag::Insert,
|
||||
old_index: None,
|
||||
new_index: Some(new_index - 1),
|
||||
value: first,
|
||||
missing_newline: newline_terminated
|
||||
&& rest.is_empty()
|
||||
&& !first.ends_with(&['\r', '\n'][..]),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
|
@ -466,6 +462,10 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn iter_inline_changes(&self, op: &DiffOp) -> impl Iterator<Item = InlineChange> {
|
||||
iter_inline_changes(self, op)
|
||||
}
|
||||
|
||||
/// Returns the captured diff ops.
|
||||
pub fn ops(&self) -> &[DiffOp] {
|
||||
&self.ops
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ impl<'diff, 'old, 'new, 'bufs> fmt::Display for UnifiedDiffHunk<'diff, 'old, 'ne
|
|||
ChangeTag::Delete => '-',
|
||||
ChangeTag::Insert => '+',
|
||||
},
|
||||
change.value(),
|
||||
change,
|
||||
nl
|
||||
)?;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue