jj_lib/diff_presentation/
mod.rs1#![expect(missing_docs)]
18
19use std::borrow::Borrow;
20use std::mem;
21
22use bstr::BString;
23use itertools::Itertools as _;
24use pollster::FutureExt as _;
25
26use crate::backend::BackendResult;
27use crate::conflicts::MaterializedFileValue;
28use crate::diff::CompareBytesExactly;
29use crate::diff::CompareBytesIgnoreAllWhitespace;
30use crate::diff::CompareBytesIgnoreWhitespaceAmount;
31use crate::diff::ContentDiff;
32use crate::diff::DiffHunk;
33use crate::diff::DiffHunkKind;
34use crate::diff::find_line_ranges;
35use crate::merge::Merge;
36use crate::repo_path::RepoPath;
37
38pub mod unified;
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum DiffTokenType {
44 Matching,
45 Different,
46}
47
48type DiffTokenVec<'content> = Vec<(DiffTokenType, &'content [u8])>;
49
50#[derive(Clone, Debug)]
51pub struct FileContent<T> {
52 pub is_binary: bool,
54 pub contents: T,
55}
56
57impl FileContent<Merge<BString>> {
58 pub fn is_empty(&self) -> bool {
59 self.contents.as_resolved().is_some_and(|c| c.is_empty())
60 }
61}
62
63pub fn file_content_for_diff<T>(
64 path: &RepoPath,
65 file: &mut MaterializedFileValue,
66 map_resolved: impl FnOnce(BString) -> T,
67) -> BackendResult<FileContent<T>> {
68 const PEEK_SIZE: usize = 8000;
72 let contents = BString::new(file.read_all(path).block_on()?);
76 let start = &contents[..PEEK_SIZE.min(contents.len())];
77 Ok(FileContent {
78 is_binary: start.contains(&b'\0'),
79 contents: map_resolved(contents),
80 })
81}
82
83#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
84pub enum LineCompareMode {
85 #[default]
87 Exact,
88 IgnoreAllSpace,
90 IgnoreSpaceChange,
92}
93
94pub fn diff_by_line<'input, T: AsRef<[u8]> + ?Sized + 'input>(
95 inputs: impl IntoIterator<Item = &'input T>,
96 options: &LineCompareMode,
97) -> ContentDiff<'input> {
98 match options {
103 LineCompareMode::Exact => {
104 ContentDiff::for_tokenizer(inputs, find_line_ranges, CompareBytesExactly)
105 }
106 LineCompareMode::IgnoreAllSpace => {
107 ContentDiff::for_tokenizer(inputs, find_line_ranges, CompareBytesIgnoreAllWhitespace)
108 }
109 LineCompareMode::IgnoreSpaceChange => {
110 ContentDiff::for_tokenizer(inputs, find_line_ranges, CompareBytesIgnoreWhitespaceAmount)
111 }
112 }
113}
114
115pub fn unzip_diff_hunks_to_lines<'content, I>(diff_hunks: I) -> [Vec<DiffTokenVec<'content>>; 2]
117where
118 I: IntoIterator,
119 I::Item: Borrow<DiffHunk<'content>>,
120{
121 let mut left_lines: Vec<DiffTokenVec<'content>> = vec![];
122 let mut right_lines: Vec<DiffTokenVec<'content>> = vec![];
123 let mut left_tokens: DiffTokenVec<'content> = vec![];
124 let mut right_tokens: DiffTokenVec<'content> = vec![];
125
126 for hunk in diff_hunks {
127 let hunk = hunk.borrow();
128 match hunk.kind {
129 DiffHunkKind::Matching => {
130 debug_assert!(hunk.contents.iter().all_equal());
132 for token in hunk.contents[0].split_inclusive(|b| *b == b'\n') {
133 left_tokens.push((DiffTokenType::Matching, token));
134 right_tokens.push((DiffTokenType::Matching, token));
135 if token.ends_with(b"\n") {
136 left_lines.push(mem::take(&mut left_tokens));
137 right_lines.push(mem::take(&mut right_tokens));
138 }
139 }
140 }
141 DiffHunkKind::Different => {
142 let [left, right] = hunk.contents[..]
143 .try_into()
144 .expect("hunk should have exactly two inputs");
145 for token in left.split_inclusive(|b| *b == b'\n') {
146 left_tokens.push((DiffTokenType::Different, token));
147 if token.ends_with(b"\n") {
148 left_lines.push(mem::take(&mut left_tokens));
149 }
150 }
151 for token in right.split_inclusive(|b| *b == b'\n') {
152 right_tokens.push((DiffTokenType::Different, token));
153 if token.ends_with(b"\n") {
154 right_lines.push(mem::take(&mut right_tokens));
155 }
156 }
157 }
158 }
159 }
160
161 if !left_tokens.is_empty() {
162 left_lines.push(left_tokens);
163 }
164 if !right_tokens.is_empty() {
165 right_lines.push(right_tokens);
166 }
167 [left_lines, right_lines]
168}