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
|
||||
|
||||
* Add `DiffOp::apply_to_hook` to apply a captured op to a diff hook.
|
||||
* Added virtual newline handling to `iter_changes`.
|
||||
|
||||
## 0.4.0
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ unicode = ["text", "unicode-segmentation"]
|
|||
|
||||
[dev-dependencies]
|
||||
insta = "1.5.2"
|
||||
console = "0.14.0"
|
||||
|
||||
[dependencies]
|
||||
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 {
|
||||
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 {
|
||||
/// 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
|
||||
/// up the value from the appropriate slice and also handle correct index
|
||||
/// 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> {
|
||||
let newline_terminated = self.newline_terminated;
|
||||
let (tag, old_range, new_range) = op.as_tag_tuple();
|
||||
let mut old_index = old_range.start;
|
||||
let mut new_index = new_range.start;
|
||||
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() {
|
||||
|
|
@ -376,7 +419,7 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
|||
value: first,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
virtual_newline.take()
|
||||
}
|
||||
}
|
||||
DiffTag::Delete => {
|
||||
|
|
@ -390,7 +433,7 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
|||
value: first,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
virtual_newline.take()
|
||||
}
|
||||
}
|
||||
DiffTag::Insert => {
|
||||
|
|
@ -404,7 +447,7 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
|||
value: first,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
virtual_newline.take()
|
||||
}
|
||||
}
|
||||
DiffTag::Replace => {
|
||||
|
|
@ -417,9 +460,16 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
|
|||
new_index: None,
|
||||
value: first,
|
||||
})
|
||||
} 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,
|
||||
|
|
@ -785,6 +835,18 @@ fn test_line_ops() {
|
|||
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]
|
||||
fn test_char_diff() {
|
||||
let diff = TextDiff::from_chars("Hello World", "Hallo Welt");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue