Change behavior of inline diff to be word based

This also fixes a bug with bad indexes and updates the inline terminal
example.
This commit is contained in:
Armin Ronacher 2021-01-31 22:02:08 +01:00
parent 459fdfdf9d
commit f3e401fc17
4 changed files with 208 additions and 43 deletions

View file

@ -1,39 +1,56 @@
use console::Style; use std::fmt;
use std::fs::read_to_string;
use std::process::exit;
use console::{style, Style};
use similar::text::{ChangeTag, TextDiff}; use similar::text::{ChangeTag, TextDiff};
fn main() { struct Line(Option<usize>);
let diff = TextDiff::from_lines(
"schtzngrmm\nschtzngrmm\nt-t-t-t\nt-t-t-t\ngrrrmmmmm\nt-t-t-t\n\
s---------c---------h\ntzngrmm\ntzngrmm\ntzngrmm\ngrrrmmmmm\n\
schtzn\nschtzn\nt-t-t-t\nt-t-t-t\nschtzngrmm\nschtzngrmm\n\
tssssssssssssss\ngrrt\ngrrrrrt\ngrrrrrrrrrt\nscht\nscht\n\
t-t-t-t-t-t-t-t-t-t\nscht\ntzngrmm\ntzngrmm\nt-t-t-t-t-t-t-t-t-t\n\
scht\nscht\nscht\nscht\nscht\ngrrrrrrrrrrrrrrrrrrrrrrrrrrrr\nt-tt",
"schützengraben\nschützengraben\nt-t-t-t\nt-t-t-t\ngrrrmmmmm\nt-t-t-t\n\
s---------c---------h\ntzngrmm\ntzngrmm\ntzngrmm\ngrrrmmmmm\nschützen\n\
schützen\nt-t-t-t\nt-t-t-t\nschützengraben\nschützengraben\n\
tssssssssssssss\ngrrt\ngrrrrrt\ngrrrrrrrrrt\nscht\nscht\n\
t-t-t-t-t-t-t-t-t-t\nscht\ntzngrmm\ntzngrmm\nt-t-t-t-t-t-t-t-t-t\n\
scht\nscht\nscht\nscht\nscht\ngrrrrrrrrrrrrrrrrrrrrrrrrrrrr\nt-tt",
);
for op in diff.ops() { impl fmt::Display for Line {
for change in diff.iter_inline_changes(op) { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let (sign, style) = match change.tag() { match self.0 {
ChangeTag::Delete => ("-", Style::new().red()), None => write!(f, " "),
ChangeTag::Insert => ("+", Style::new().green()), Some(idx) => write!(f, "{:<4}", idx + 1),
ChangeTag::Equal => (" ", Style::new()), }
}; }
print!("{}", style.apply_to(sign).bold(),); }
for &(emphasized, value) in change.values() {
if emphasized { fn main() {
print!("{}", style.apply_to(value).underlined().on_black()); let args: Vec<_> = std::env::args_os().collect();
} else { if args.len() != 3 {
print!("{}", style.apply_to(value)); eprintln!("usage: terminal-inline [old] [new]");
exit(1);
}
let old = read_to_string(&args[1]).unwrap();
let new = read_to_string(&args[2]).unwrap();
let diff = TextDiff::from_lines(&old, &new);
for group in diff.grouped_ops(5) {
for op in group {
for change in diff.iter_inline_changes(&op) {
let (sign, s) = match change.tag() {
ChangeTag::Delete => ("-", Style::new().red()),
ChangeTag::Insert => ("+", Style::new().green()),
ChangeTag::Equal => (" ", Style::new()),
};
print!(
"{}{} |{}",
style(Line(change.old_index())).dim(),
style(Line(change.new_index())).dim(),
s.apply_to(sign).bold(),
);
for &(emphasized, value) in change.values() {
if emphasized {
print!("{}", s.apply_to(value).underlined().on_black());
} else {
print!("{}", s.apply_to(value));
}
}
if change.is_missing_newline() {
println!();
} }
}
if change.is_missing_newline() {
println!();
} }
} }
} }

View file

@ -3,7 +3,7 @@ use std::{fmt, iter};
use crate::algorithms::{Algorithm, DiffOp, DiffTag}; use crate::algorithms::{Algorithm, DiffOp, DiffTag};
use crate::text::{Change, ChangeTag, TextDiff}; use crate::text::{Change, ChangeTag, TextDiff};
use super::split_chars; use super::split_words;
use std::ops::Range; use std::ops::Range;
@ -75,7 +75,7 @@ impl<'s> From<Change<'s>> for InlineChange<'s> {
InlineChange { InlineChange {
tag: change.tag(), tag: change.tag(),
old_index: change.old_index(), old_index: change.old_index(),
new_index: change.old_index(), new_index: change.new_index(),
values: vec![(false, change.value())], values: vec![(false, change.value())],
missing_newline: change.missing_newline(), missing_newline: change.missing_newline(),
} }
@ -118,8 +118,8 @@ pub(crate) fn iter_inline_changes<'diff>(
(ChangeTag::Delete, Some(ChangeTag::Insert)) => { (ChangeTag::Delete, Some(ChangeTag::Insert)) => {
let old_value = change.value(); let old_value = change.value();
let new_value = next_change.unwrap().value(); let new_value = next_change.unwrap().value();
let old_chars = split_chars(&old_value).collect::<Vec<_>>(); let old_chars = split_words(&old_value).collect::<Vec<_>>();
let new_chars = split_chars(&new_value).collect::<Vec<_>>(); let new_chars = split_words(&new_value).collect::<Vec<_>>();
let old_mindex = MultiIndex::new(&old_chars, old_value); let old_mindex = MultiIndex::new(&old_chars, old_value);
let new_mindex = MultiIndex::new(&new_chars, new_value); let new_mindex = MultiIndex::new(&new_chars, new_value);
let inline_diff = TextDiff::configure() let inline_diff = TextDiff::configure()
@ -158,7 +158,7 @@ pub(crate) fn iter_inline_changes<'diff>(
Some(InlineChange { Some(InlineChange {
tag: ChangeTag::Delete, tag: ChangeTag::Delete,
old_index: change.old_index(), old_index: change.old_index(),
new_index: change.new_index(), new_index: None,
values: old_values, values: old_values,
missing_newline: newline_terminated missing_newline: newline_terminated
&& !old_value.ends_with(&['\r', '\n'][..]), && !old_value.ends_with(&['\r', '\n'][..]),
@ -167,8 +167,8 @@ pub(crate) fn iter_inline_changes<'diff>(
.chain( .chain(
Some(InlineChange { Some(InlineChange {
tag: ChangeTag::Insert, tag: ChangeTag::Insert,
old_index: change.old_index(), old_index: None,
new_index: change.new_index(), new_index: next_change.unwrap().new_index(),
values: new_values, values: new_values,
missing_newline: newline_terminated missing_newline: newline_terminated
&& !new_value.ends_with(&['\r', '\n'][..]), && !new_value.ends_with(&['\r', '\n'][..]),

View file

@ -483,10 +483,9 @@ impl<'old, 'new, 'bufs> TextDiff<'old, 'new, 'bufs> {
/// Iterates over the changes the op expands to with inline emphasis. /// Iterates over the changes the op expands to with inline emphasis.
/// ///
/// This is very similar to [`iter_changes`] but it performs a second /// This is very similar to [`iter_changes`] but it performs a second
/// level per-character diff on adjacent line replacements. The exact /// level diff on adjacent line replacements. The exact behavior of
/// behavior of this function with regards to how it detects those /// this function with regards to how it detects those inline changes
/// inline changes is currently not defined and will likely change /// is currently not defined and will likely change over time.
/// over time.
pub fn iter_inline_changes(&self, op: &DiffOp) -> impl Iterator<Item = InlineChange> { pub fn iter_inline_changes(&self, op: &DiffOp) -> impl Iterator<Item = InlineChange> {
iter_inline_changes(self, op) iter_inline_changes(self, op)
} }
@ -783,6 +782,21 @@ fn test_virtual_newlines() {
insta::assert_debug_snapshot!(&changes); insta::assert_debug_snapshot!(&changes);
} }
#[test]
fn test_line_ops_inline() {
let diff = TextDiff::from_lines(
"Hello World\nsome stuff here\nsome more stuff here\n\nAha stuff here\nand more stuff",
"Stuff\nHello World\nsome amazing stuff here\nsome more stuff here\n",
);
assert_eq!(diff.newline_terminated(), true);
let changes = diff
.ops()
.iter()
.flat_map(|op| diff.iter_inline_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");

View file

@ -0,0 +1,134 @@
---
source: src/text/mod.rs
expression: "&changes"
---
[
InlineChange {
tag: Insert,
old_index: None,
new_index: Some(
0,
),
values: [
(
false,
"Stuff\n",
),
],
missing_newline: false,
},
InlineChange {
tag: Equal,
old_index: Some(
0,
),
new_index: Some(
1,
),
values: [
(
false,
"Hello World\n",
),
],
missing_newline: false,
},
InlineChange {
tag: Delete,
old_index: Some(
1,
),
new_index: None,
values: [
(
false,
"some ",
),
(
false,
"stuff here\n",
),
],
missing_newline: false,
},
InlineChange {
tag: Insert,
old_index: None,
new_index: Some(
2,
),
values: [
(
false,
"some ",
),
(
true,
"amazing ",
),
(
false,
"stuff here\n",
),
],
missing_newline: false,
},
InlineChange {
tag: Equal,
old_index: Some(
2,
),
new_index: Some(
3,
),
values: [
(
false,
"some more stuff here\n",
),
],
missing_newline: false,
},
InlineChange {
tag: Delete,
old_index: Some(
3,
),
new_index: None,
values: [
(
false,
"\n",
),
],
missing_newline: false,
},
InlineChange {
tag: Delete,
old_index: Some(
4,
),
new_index: None,
values: [
(
false,
"Aha stuff here\n",
),
],
missing_newline: false,
},
InlineChange {
tag: Delete,
old_index: Some(
5,
),
new_index: None,
values: [
(
false,
"and more stuff",
),
],
missing_newline: true,
},
]