Skip to main content

gix_imara_diff/
postprocess.rs

1// Modified for gitoxide from the upstream imara-diff crate.
2// Upstream source: git cat-file -p 32d1e45d3df061e6ccba6db7fdce92db29e345d8:src/postprocess.rs
3
4use crate::intern::{InternedInput, Token};
5use crate::slider_heuristic::SliderHeuristic;
6use crate::util::{find_hunk_end, find_hunk_start};
7use crate::{Diff, Hunk};
8
9impl Diff {
10    /// Postprocesses the diff using explicit token sequences and a custom heuristic.
11    ///
12    /// This is a lower-level method that works directly with token sequences rather than
13    /// an `InternedInput`. Use [`postprocess_with_heuristic`](Self::postprocess_with_heuristic)
14    /// for a more convenient API.
15    ///
16    /// # Parameters
17    ///
18    /// * `before` - The token sequence from the first file, before changes
19    /// * `after` - The token sequence from the second file, after changes
20    /// * `heuristic` - The slider heuristic to use for positioning hunks
21    pub fn postprocess_with(&mut self, before: &[Token], after: &[Token], mut heuristic: impl SliderHeuristic) {
22        Postprocessor {
23            added: &mut self.added,
24            removed: &mut self.removed,
25            tokens: after,
26            hunk: Hunk {
27                before: 0..0,
28                after: 0..0,
29            },
30            heuristic: &mut heuristic,
31        }
32        .run();
33        Postprocessor {
34            added: &mut self.removed,
35            removed: &mut self.added,
36            tokens: before,
37            hunk: Hunk {
38                before: 0..0,
39                after: 0..0,
40            },
41            heuristic: &mut heuristic,
42        }
43        .run()
44    }
45
46    /// Postprocesses the diff using an `InternedInput` and a custom heuristic.
47    ///
48    /// This is a convenience wrapper around [`postprocess_with`](Self::postprocess_with)
49    /// that extracts the token sequences from the input automatically.
50    ///
51    /// # Parameters
52    ///
53    /// * `input` - The interned input containing the token sequences
54    /// * `heuristic` - The slider heuristic to use for positioning hunks
55    pub fn postprocess_with_heuristic<T>(&mut self, input: &InternedInput<T>, heuristic: impl SliderHeuristic) {
56        self.postprocess_with(&input.before, &input.after, heuristic);
57    }
58}
59
60/// Internal state for postprocessing a diff to improve readability.
61struct Postprocessor<'a, H> {
62    /// The mutable array tracking which tokens were added.
63    added: &'a mut [bool],
64    /// The immutable array tracking which tokens were removed.
65    removed: &'a [bool],
66    /// The token sequence being processed.
67    tokens: &'a [Token],
68    /// The current hunk being processed in the iteration.
69    hunk: Hunk,
70    /// The heuristic used to determine optimal hunk positions.
71    heuristic: &'a mut H,
72}
73
74impl<H: SliderHeuristic> Postprocessor<'_, H> {
75    fn run(mut self) {
76        loop {
77            // find a hunk
78            if !self.hunk.next_hunk(self.removed, self.added) {
79                break;
80            }
81
82            let mut earliest_end;
83            let mut is_modification;
84            loop {
85                // move hunk up as far as possible to possibly merge it with other hunks
86                // and discover if there are other possible positions
87                while self.slide_up() {}
88                earliest_end = self.hunk.after.end;
89                is_modification = self.hunk.before.start != self.hunk.before.end;
90
91                let hunk_size_unexpanded = self.hunk.after.len();
92                // move hunk down as far as possible (and merge with other hunks it if
93                // possible) sliding down is often the most preferred position
94                while self.slide_down() {
95                    is_modification |= self.hunk.before.start != self.hunk.before.end;
96                }
97                // if this hunk was merged with another hunk while sliding down we might
98                // be able to slide up more otherwise we are done
99                if hunk_size_unexpanded == self.hunk.after.len() {
100                    break;
101                }
102            }
103
104            if self.hunk.after.end == earliest_end {
105                continue;
106            }
107            if is_modification {
108                // hunk can be moved and there is a removed hunk in the same region
109                // move the hunk so it align with the other hunk to produce a single
110                // MODIFIED hunk instead of two separate ADDED/REMOVED hunks
111                while self.hunk.before.start == self.hunk.before.end {
112                    let success = self.slide_up();
113                    debug_assert!(success);
114                }
115            } else {
116                let slider_end = self
117                    .heuristic
118                    .best_slider_end(self.tokens, self.hunk.after.clone(), earliest_end);
119                for _ in slider_end..self.hunk.after.end {
120                    let success = self.slide_up();
121                    debug_assert!(success);
122                }
123            }
124        }
125    }
126
127    /// Slides a hunk down by one token/line, potentially merging it with a subsequent hunk.
128    fn slide_down(&mut self) -> bool {
129        let Some(&next_token) = self.tokens.get(self.hunk.after.end as usize) else {
130            return false;
131        };
132        if self.tokens[self.hunk.after.start as usize] != next_token {
133            return false;
134        }
135        self.added[self.hunk.after.start as usize] = false;
136        self.added[self.hunk.after.end as usize] = true;
137        self.hunk.after.start += 1;
138        self.hunk.after.end = find_hunk_end(self.added, self.hunk.after.end);
139        // move the end of the remove range one down to keep the unchanged lines aligned
140        self.hunk.before.start = self.hunk.before.end + 1;
141        self.hunk.before.end = find_hunk_end(self.removed, self.hunk.before.start);
142        true
143    }
144
145    /// Slides a hunk up by one token/line, potentially merging it with a previous hunk.
146    fn slide_up(&mut self) -> bool {
147        if self.hunk.after.start == 0 {
148            return false;
149        }
150        if self.tokens[self.hunk.after.start as usize - 1] != self.tokens[self.hunk.after.end as usize - 1] {
151            return false;
152        }
153        self.added[self.hunk.after.start as usize - 1] = true;
154        self.added[self.hunk.after.end as usize - 1] = false;
155        self.hunk.after.end -= 1;
156        self.hunk.after.start = find_hunk_start(self.added, self.hunk.after.start - 1);
157        // move the start of the remove range one up to keep the unchanged lines aligned
158        self.hunk.before.end = self.hunk.before.start - 1;
159        self.hunk.before.start = find_hunk_start(self.removed, self.hunk.before.start - 1);
160        true
161    }
162}