Skip to content

Commit 8228fc0

Browse files
CopilotByron
andcommitted
Add imara-diff v0.2 support with slider heuristics
- Add imara-diff v0.2.0 as dependency alongside v0.1.8 - Expose v0.2 API through blob::v0_2 module - Add diff_with_slider_heuristics() function for convenient usage - Add comprehensive tests demonstrating UnifiedDiffPrinter usage - Tests cover various scenarios including multi-hunk diffs and custom context sizes Co-authored-by: Byron <63622+Byron@users.noreply.github.com>
1 parent 9f49dd3 commit 8228fc0

File tree

5 files changed

+315
-3
lines changed

5 files changed

+315
-3
lines changed

Cargo.lock

Lines changed: 13 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-diff/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ autotests = false
1515
[features]
1616
default = ["blob", "index"]
1717
## Enable diffing of blobs using imara-diff.
18-
blob = ["dep:imara-diff", "dep:gix-filter", "dep:gix-worktree", "dep:gix-path", "dep:gix-fs", "dep:gix-command", "dep:gix-tempfile", "dep:gix-trace", "dep:gix-traverse"]
18+
blob = ["dep:imara-diff", "dep:imara-diff-v0_2", "dep:gix-filter", "dep:gix-worktree", "dep:gix-path", "dep:gix-fs", "dep:gix-command", "dep:gix-tempfile", "dep:gix-trace", "dep:gix-traverse"]
1919
## Enable diffing of two indices, which also allows for a generic rewrite tracking implementation.
2020
index = ["dep:gix-index", "dep:gix-pathspec", "dep:gix-attributes"]
2121
## Data structures implement `serde::Serialize` and `serde::Deserialize`.
@@ -43,6 +43,7 @@ gix-traverse = { version = "^0.49.0", path = "../gix-traverse", optional = true
4343

4444
thiserror = "2.0.17"
4545
imara-diff = { version = "0.1.8", optional = true }
46+
imara-diff-v0_2 = { version = "0.2.0", optional = true, package = "imara-diff" }
4647
serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] }
4748
getrandom = { version = "0.2.8", optional = true, default-features = false, features = ["js"] }
4849
bstr = { version = "1.12.0", default-features = false }

gix-diff/src/blob/mod.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,45 @@ use std::{collections::HashMap, path::PathBuf};
55
use bstr::BString;
66
pub use imara_diff::*;
77

8+
/// Re-export imara-diff v0.2 types for use with slider heuristics.
9+
///
10+
/// This module provides access to the v0.2 API of imara-diff, which includes
11+
/// support for Git's slider heuristics to produce more intuitive diffs.
12+
pub mod v0_2 {
13+
pub use imara_diff_v0_2::*;
14+
}
15+
16+
/// Compute a diff with Git's slider heuristics to produce more intuitive diffs.
17+
///
18+
/// This function uses `imara-diff` v0.2 which provides the [`v0_2::Diff`] structure
19+
/// that supports postprocessing with slider heuristics. The slider heuristics move
20+
/// diff hunks to more intuitive locations based on indentation and other factors,
21+
/// resulting in diffs that are more readable and match Git's output more closely.
22+
///
23+
/// # Examples
24+
///
25+
/// ```
26+
/// use gix_diff::blob::{diff_with_slider_heuristics, v0_2::{Algorithm, InternedInput}};
27+
///
28+
/// let before = "fn foo() {\n let x = 1;\n}\n";
29+
/// let after = "fn foo() {\n let x = 2;\n}\n";
30+
///
31+
/// let input = InternedInput::new(before, after);
32+
/// let diff = diff_with_slider_heuristics(Algorithm::Histogram, &input);
33+
///
34+
/// // The diff now has slider heuristics applied
35+
/// assert_eq!(diff.count_removals(), 1);
36+
/// assert_eq!(diff.count_additions(), 1);
37+
/// ```
38+
pub fn diff_with_slider_heuristics<T: AsRef<[u8]>>(
39+
algorithm: v0_2::Algorithm,
40+
input: &v0_2::InternedInput<T>,
41+
) -> v0_2::Diff {
42+
let mut diff = v0_2::Diff::compute(algorithm, input);
43+
diff.postprocess_lines(input);
44+
diff
45+
}
46+
847
///
948
pub mod pipeline;
1049

gix-diff/tests/diff/blob/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub(crate) mod pipeline;
22
mod platform;
33
mod slider;
4+
mod slider_heuristics;
45
mod unified_diff;
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
use gix_diff::blob::{diff_with_slider_heuristics, v0_2};
2+
3+
/// Test basic slider heuristics functionality
4+
#[test]
5+
fn basic_slider_heuristics() -> crate::Result {
6+
let before = "fn foo() {\n let x = 1;\n x\n}\n";
7+
let after = "fn foo() {\n let x = 2;\n x\n}\n";
8+
9+
let input = v0_2::InternedInput::new(before, after);
10+
let diff = diff_with_slider_heuristics(v0_2::Algorithm::Histogram, &input);
11+
12+
assert_eq!(diff.count_removals(), 1);
13+
assert_eq!(diff.count_additions(), 1);
14+
assert!(diff.is_removed(1)); // " let x = 1;\n" is removed
15+
assert!(diff.is_added(1)); // " let x = 2;\n" is added
16+
17+
Ok(())
18+
}
19+
20+
/// Test that the UnifiedDiffPrinter can be used with the v0.2 API
21+
#[test]
22+
fn unified_diff_printer_usage() -> crate::Result {
23+
let before = r#"fn foo() {
24+
let x = 1;
25+
println!("x = {}", x);
26+
}
27+
"#;
28+
29+
let after = r#"fn foo() {
30+
let x = 2;
31+
println!("x = {}", x);
32+
println!("done");
33+
}
34+
"#;
35+
36+
let input = v0_2::InternedInput::new(before, after);
37+
let diff = diff_with_slider_heuristics(v0_2::Algorithm::Histogram, &input);
38+
39+
// Use the UnifiedDiffPrinter to generate a unified diff
40+
let printer = v0_2::BasicLineDiffPrinter(&input.interner);
41+
let config = v0_2::UnifiedDiffConfig::default();
42+
let unified = diff.unified_diff(&printer, config, &input);
43+
44+
let output = unified.to_string();
45+
46+
// Verify the output contains expected diff markers
47+
assert!(output.contains("@@"), "should contain hunk header");
48+
assert!(output.contains("- let x = 1;"), "should show removal");
49+
assert!(output.contains("+ let x = 2;"), "should show addition");
50+
assert!(output.contains("+ println!(\"done\");"), "should show new line");
51+
52+
Ok(())
53+
}
54+
55+
/// Test slider heuristics with indentation
56+
#[test]
57+
fn slider_heuristics_with_indentation() -> crate::Result {
58+
let before = r#"fn main() {
59+
if true {
60+
println!("hello");
61+
}
62+
}
63+
"#;
64+
65+
let after = r#"fn main() {
66+
if true {
67+
println!("hello");
68+
println!("world");
69+
}
70+
}
71+
"#;
72+
73+
let input = v0_2::InternedInput::new(before, after);
74+
let diff = diff_with_slider_heuristics(v0_2::Algorithm::Histogram, &input);
75+
76+
// Verify that only one line was added
77+
assert_eq!(diff.count_additions(), 1);
78+
assert_eq!(diff.count_removals(), 0);
79+
80+
Ok(())
81+
}
82+
83+
/// Test that Myers algorithm also works with slider heuristics
84+
#[test]
85+
fn myers_with_slider_heuristics() -> crate::Result {
86+
let before = "a\nb\nc\n";
87+
let after = "a\nx\nc\n";
88+
89+
let input = v0_2::InternedInput::new(before, after);
90+
let diff = diff_with_slider_heuristics(v0_2::Algorithm::Myers, &input);
91+
92+
assert_eq!(diff.count_removals(), 1);
93+
assert_eq!(diff.count_additions(), 1);
94+
95+
Ok(())
96+
}
97+
98+
/// Test empty diff
99+
#[test]
100+
fn empty_diff_with_slider_heuristics() -> crate::Result {
101+
let before = "unchanged\n";
102+
let after = "unchanged\n";
103+
104+
let input = v0_2::InternedInput::new(before, after);
105+
let diff = diff_with_slider_heuristics(v0_2::Algorithm::Histogram, &input);
106+
107+
assert_eq!(diff.count_removals(), 0);
108+
assert_eq!(diff.count_additions(), 0);
109+
110+
Ok(())
111+
}
112+
113+
/// Test complex multi-hunk diff with slider heuristics
114+
#[test]
115+
fn multi_hunk_diff_with_slider_heuristics() -> crate::Result {
116+
let before = r#"struct Foo {
117+
x: i32,
118+
}
119+
120+
impl Foo {
121+
fn new() -> Self {
122+
Foo { x: 0 }
123+
}
124+
}
125+
"#;
126+
127+
let after = r#"struct Foo {
128+
x: i32,
129+
y: i32,
130+
}
131+
132+
impl Foo {
133+
fn new() -> Self {
134+
Foo { x: 0, y: 0 }
135+
}
136+
}
137+
"#;
138+
139+
let input = v0_2::InternedInput::new(before, after);
140+
let diff = diff_with_slider_heuristics(v0_2::Algorithm::Histogram, &input);
141+
142+
// Should have additions for the new field and modified constructor
143+
assert!(diff.count_additions() > 0);
144+
assert!(diff.count_removals() > 0);
145+
146+
let printer = v0_2::BasicLineDiffPrinter(&input.interner);
147+
let config = v0_2::UnifiedDiffConfig::default();
148+
let unified = diff.unified_diff(&printer, config, &input);
149+
150+
let output = unified.to_string();
151+
152+
// Verify the structure
153+
assert!(output.contains("@@"), "should have hunk headers");
154+
assert!(output.contains("+ y: i32,"), "should show new field");
155+
156+
Ok(())
157+
}
158+
159+
/// Test custom context size in UnifiedDiffConfig
160+
#[test]
161+
fn custom_context_size() -> crate::Result {
162+
let before = "line1\nline2\nline3\nline4\nline5\nline6\nline7\n";
163+
let after = "line1\nline2\nline3\nMODIFIED\nline5\nline6\nline7\n";
164+
165+
let input = v0_2::InternedInput::new(before, after);
166+
let diff = diff_with_slider_heuristics(v0_2::Algorithm::Histogram, &input);
167+
168+
let printer = v0_2::BasicLineDiffPrinter(&input.interner);
169+
170+
// Test with context size of 1
171+
let mut config = v0_2::UnifiedDiffConfig::default();
172+
config.context_len(1);
173+
let unified = diff.unified_diff(&printer, config, &input);
174+
let output_small = unified.to_string();
175+
176+
// Test with context size of 3 (default)
177+
let config_default = v0_2::UnifiedDiffConfig::default();
178+
let unified_default = diff.unified_diff(&printer, config_default, &input);
179+
let output_default = unified_default.to_string();
180+
181+
// Smaller context should have fewer lines
182+
assert!(
183+
output_small.lines().count() <= output_default.lines().count(),
184+
"smaller context should have fewer or equal lines"
185+
);
186+
187+
Ok(())
188+
}
189+
190+
/// Test that hunks iterator works correctly
191+
#[test]
192+
fn hunks_iterator() -> crate::Result {
193+
let before = "a\nb\nc\nd\ne\n";
194+
let after = "a\nX\nc\nY\ne\n";
195+
196+
let input = v0_2::InternedInput::new(before, after);
197+
let diff = diff_with_slider_heuristics(v0_2::Algorithm::Histogram, &input);
198+
199+
let hunks: Vec<_> = diff.hunks().collect();
200+
201+
// Should have two separate hunks
202+
assert_eq!(hunks.len(), 2, "should have two hunks");
203+
204+
// First hunk: b -> X
205+
assert_eq!(hunks[0].before.start, 1);
206+
assert_eq!(hunks[0].before.end, 2);
207+
assert_eq!(hunks[0].after.start, 1);
208+
assert_eq!(hunks[0].after.end, 2);
209+
210+
// Second hunk: d -> Y
211+
assert_eq!(hunks[1].before.start, 3);
212+
assert_eq!(hunks[1].before.end, 4);
213+
assert_eq!(hunks[1].after.start, 3);
214+
assert_eq!(hunks[1].after.end, 4);
215+
216+
Ok(())
217+
}
218+
219+
/// Test postprocessing without heuristic
220+
#[test]
221+
fn postprocess_no_heuristic() -> crate::Result {
222+
let before = "a\nb\nc\n";
223+
let after = "a\nX\nc\n";
224+
225+
let input = v0_2::InternedInput::new(before, after);
226+
227+
// Create diff but postprocess without heuristic
228+
let mut diff = v0_2::Diff::compute(v0_2::Algorithm::Histogram, &input);
229+
diff.postprocess_no_heuristic(&input);
230+
231+
assert_eq!(diff.count_removals(), 1);
232+
assert_eq!(diff.count_additions(), 1);
233+
234+
Ok(())
235+
}
236+
237+
/// Test that the v0.2 API exposes the IndentHeuristic
238+
#[test]
239+
fn indent_heuristic_available() -> crate::Result {
240+
let before = "fn foo() {\n x\n}\n";
241+
let after = "fn foo() {\n y\n}\n";
242+
243+
let input = v0_2::InternedInput::new(before, after);
244+
245+
// Test with custom indent heuristic
246+
let mut diff = v0_2::Diff::compute(v0_2::Algorithm::Histogram, &input);
247+
248+
// Create custom heuristic
249+
let heuristic = v0_2::IndentHeuristic::new(|token| {
250+
let line: &str = &input.interner[token];
251+
v0_2::IndentLevel::for_ascii_line(line.as_bytes().iter().copied(), 4)
252+
});
253+
254+
diff.postprocess_with_heuristic(&input, heuristic);
255+
256+
assert_eq!(diff.count_removals(), 1);
257+
assert_eq!(diff.count_additions(), 1);
258+
259+
Ok(())
260+
}

0 commit comments

Comments
 (0)