Added virtual newline handling and example
This commit is contained in:
parent
da32711e1a
commit
96bbaf1fdf
5 changed files with 129 additions and 3 deletions
|
|
@ -5,6 +5,7 @@ All notable changes to similar are documented here.
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|
||||||
* Add `DiffOp::apply_to_hook` to apply a captured op to a diff hook.
|
* Add `DiffOp::apply_to_hook` to apply a captured op to a diff hook.
|
||||||
|
* Added virtual newline handling to `iter_changes`.
|
||||||
|
|
||||||
## 0.4.0
|
## 0.4.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ unicode = ["text", "unicode-segmentation"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
insta = "1.5.2"
|
insta = "1.5.2"
|
||||||
|
console = "0.14.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
unicode-segmentation = { version = "1.7.1", optional = true }
|
unicode-segmentation = { version = "1.7.1", optional = true }
|
||||||
|
|
|
||||||
24
examples/terminal.rs
Normal file
24
examples/terminal.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
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_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(),
|
||||||
|
style.apply_to(change.value())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/snapshots/similar__text__virtual_newlines.snap
Normal file
38
src/snapshots/similar__text__virtual_newlines.snap
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
source: src/text.rs
|
||||||
|
expression: "&changes"
|
||||||
|
---
|
||||||
|
[
|
||||||
|
Change {
|
||||||
|
tag: Equal,
|
||||||
|
old_index: Some(
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
new_index: Some(
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
value: "a\n",
|
||||||
|
},
|
||||||
|
Change {
|
||||||
|
tag: Delete,
|
||||||
|
old_index: Some(
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
new_index: None,
|
||||||
|
value: "b",
|
||||||
|
},
|
||||||
|
Change {
|
||||||
|
tag: Equal,
|
||||||
|
old_index: None,
|
||||||
|
new_index: None,
|
||||||
|
value: "\n",
|
||||||
|
},
|
||||||
|
Change {
|
||||||
|
tag: Insert,
|
||||||
|
old_index: None,
|
||||||
|
new_index: Some(
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
value: "c\n",
|
||||||
|
},
|
||||||
|
]
|
||||||
68
src/text.rs
68
src/text.rs
|
|
@ -231,8 +231,30 @@ impl<'s> Change<'s> {
|
||||||
pub fn value(&self) -> &'s str {
|
pub fn value(&self) -> &'s str {
|
||||||
self.value
|
self.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` for virtual changes.
|
||||||
|
///
|
||||||
|
/// 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VIRTUAL_NEWLINE_CHANGE: Change<'static> = Change {
|
||||||
|
tag: ChangeTag::Equal,
|
||||||
|
old_index: None,
|
||||||
|
new_index: None,
|
||||||
|
value: "\n",
|
||||||
|
};
|
||||||
|
|
||||||
impl ChangeTag {
|
impl ChangeTag {
|
||||||
/// Returns the unified sign of this change.
|
/// Returns the unified sign of this change.
|
||||||
///
|
///
|
||||||
|
|
@ -356,13 +378,34 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
||||||
/// ways in which a change could be encoded (insert/delete vs replace), look
|
/// ways in which a change could be encoded (insert/delete vs replace), look
|
||||||
/// up the value from the appropriate slice and also handle correct index
|
/// up the value from the appropriate slice and also handle correct index
|
||||||
/// handling.
|
/// handling.
|
||||||
|
///
|
||||||
|
/// In addition it has some custom handling to insert "virtual" newlines
|
||||||
|
/// for diffs where [`TextDiff::newline_terminated`] is `true` but the
|
||||||
|
/// diff does not end in newlines in the right places. For more information
|
||||||
|
/// see [`Change::is_virtual`].
|
||||||
pub fn iter_changes(&self, op: &DiffOp) -> impl Iterator<Item = Change> {
|
pub fn iter_changes(&self, op: &DiffOp) -> impl Iterator<Item = Change> {
|
||||||
|
let newline_terminated = self.newline_terminated;
|
||||||
let (tag, old_range, new_range) = op.as_tag_tuple();
|
let (tag, old_range, new_range) = op.as_tag_tuple();
|
||||||
let mut old_index = old_range.start;
|
let mut old_index = old_range.start;
|
||||||
let mut new_index = new_range.start;
|
let mut new_index = new_range.start;
|
||||||
let mut old_slices = &self.old_slices()[op.old_range()];
|
let mut old_slices = &self.old_slices()[op.old_range()];
|
||||||
let mut new_slices = &self.new_slices()[op.new_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 {
|
std::iter::from_fn(move || match tag {
|
||||||
DiffTag::Equal => {
|
DiffTag::Equal => {
|
||||||
if let Some((&first, rest)) = old_slices.split_first() {
|
if let Some((&first, rest)) = old_slices.split_first() {
|
||||||
|
|
@ -376,7 +419,7 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
||||||
value: first,
|
value: first,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
virtual_newline.take()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DiffTag::Delete => {
|
DiffTag::Delete => {
|
||||||
|
|
@ -390,7 +433,7 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
||||||
value: first,
|
value: first,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
virtual_newline.take()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DiffTag::Insert => {
|
DiffTag::Insert => {
|
||||||
|
|
@ -404,7 +447,7 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
||||||
value: first,
|
value: first,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
virtual_newline.take()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DiffTag::Replace => {
|
DiffTag::Replace => {
|
||||||
|
|
@ -417,9 +460,16 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
||||||
new_index: None,
|
new_index: None,
|
||||||
value: first,
|
value: first,
|
||||||
})
|
})
|
||||||
|
} else if let Some(virtual_newline) = virtual_newline.take() {
|
||||||
|
Some(virtual_newline)
|
||||||
} else if let Some((&first, rest)) = new_slices.split_first() {
|
} else if let Some((&first, rest)) = new_slices.split_first() {
|
||||||
new_slices = rest;
|
new_slices = rest;
|
||||||
new_index += 1;
|
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 {
|
Some(Change {
|
||||||
tag: ChangeTag::Insert,
|
tag: ChangeTag::Insert,
|
||||||
old_index: None,
|
old_index: None,
|
||||||
|
|
@ -785,6 +835,18 @@ fn test_line_ops() {
|
||||||
insta::assert_debug_snapshot!(&changes);
|
insta::assert_debug_snapshot!(&changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_virtual_newlines() {
|
||||||
|
let diff = TextDiff::from_lines("a\nb", "a\nc\n");
|
||||||
|
assert_eq!(diff.newline_terminated(), true);
|
||||||
|
let changes = diff
|
||||||
|
.ops()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|op| diff.iter_changes(op))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
insta::assert_debug_snapshot!(&changes);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_char_diff() {
|
fn test_char_diff() {
|
||||||
let diff = TextDiff::from_chars("Hello World", "Hallo Welt");
|
let diff = TextDiff::from_chars("Hello World", "Hallo Welt");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue