1use std::fmt;
8use std::time::Duration;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Chunk<'a> {
13 Equal(&'a str),
15 Delete(&'a str),
17 Insert(&'a str),
19}
20
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
25pub enum DiffAlgorithm {
26 #[default]
28 Myers,
29 Patience,
31 Histogram,
33}
34
35impl DiffAlgorithm {
36 pub(crate) fn similar(self) -> similar::Algorithm {
37 match self {
38 Self::Myers => similar::Algorithm::Myers,
39 Self::Patience => similar::Algorithm::Patience,
40 Self::Histogram => similar::Algorithm::Histogram,
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct DiffOptions<'a> {
48 pub context_lines: usize,
50 pub old_label: Option<&'a str>,
52 pub new_label: Option<&'a str>,
54 pub missing_newline_hint: bool,
56 pub algorithm: DiffAlgorithm,
58 pub timeout: Duration,
60 pub inline_timeout: Duration,
62}
63
64impl Default for DiffOptions<'_> {
65 fn default() -> Self {
66 Self {
67 context_lines: 3,
68 old_label: None,
69 new_label: None,
70 missing_newline_hint: true,
71 algorithm: DiffAlgorithm::Myers,
72 timeout: Duration::from_millis(200),
73 inline_timeout: Duration::from_millis(40),
74 }
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub struct DiffHunk {
90 pub old_start: usize,
92 pub old_lines: usize,
94 pub new_start: usize,
96 pub new_lines: usize,
98 pub lines: Vec<DiffLine>,
100}
101
102impl DiffHunk {
103 #[must_use]
111 pub fn counts_match(&self, old_count: usize, new_count: usize) -> bool {
112 let old_side = self.lines.iter().filter(|line| line.kind != DiffLineKind::Addition).count();
113 let new_side = self.lines.iter().filter(|line| line.kind != DiffLineKind::Deletion).count();
114 old_side == old_count && new_side == new_count
115 }
116
117 #[must_use]
120 pub fn old_side_text(&self) -> String {
121 self.lines
122 .iter()
123 .filter(|line| line.kind != DiffLineKind::Addition)
124 .map(|line| line.text.as_str())
125 .collect()
126 }
127
128 #[must_use]
131 pub fn new_side_text(&self) -> String {
132 self.lines
133 .iter()
134 .filter(|line| line.kind != DiffLineKind::Deletion)
135 .map(|line| line.text.as_str())
136 .collect()
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
143#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
144pub enum DiffLineKind {
145 Context,
147 Addition,
149 Deletion,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
156pub struct DiffLine {
157 pub kind: DiffLineKind,
159 pub old_line: Option<u32>,
161 pub new_line: Option<u32>,
163 pub text: String,
165}
166
167#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
169#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
170pub struct DiffStats {
171 pub additions: usize,
173 pub deletions: usize,
175 pub context: usize,
177 pub hunks: usize,
179 pub omitted_rows: usize,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
185#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
186pub struct DiffDocument {
187 pub hunks: Vec<DiffHunk>,
189 pub stats: DiffStats,
191 #[cfg_attr(feature = "serde", serde(default = "default_inline_timeout"))]
192 pub(crate) inline_timeout: Duration,
193}
194
195impl Default for DiffDocument {
196 fn default() -> Self {
197 Self {
198 hunks: Vec::new(),
199 stats: DiffStats::default(),
200 inline_timeout: default_inline_timeout(),
201 }
202 }
203}
204
205pub(crate) fn default_inline_timeout() -> Duration {
206 Duration::from_millis(40)
207}
208
209#[derive(Debug, Clone, PartialEq, Eq)]
211pub struct ParseDiffError {
212 message: &'static str,
213}
214
215impl ParseDiffError {
216 pub(crate) fn new(message: &'static str) -> Self {
217 Self { message }
218 }
219}
220
221impl fmt::Display for ParseDiffError {
222 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
223 formatter.write_str(self.message)
224 }
225}
226
227impl std::error::Error for ParseDiffError {}
228
229#[derive(Debug, Clone)]
231#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
232pub struct DiffBundle {
233 pub hunks: Vec<DiffHunk>,
235 pub formatted: String,
237 pub is_empty: bool,
239}
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
243pub struct IntralineRange {
244 pub start: usize,
246 pub end: usize,
248}
249
250pub type WordChangedRanges = Vec<(usize, usize)>;
252
253#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
255pub struct DiffChangeCounts {
256 pub additions: usize,
258 pub deletions: usize,
260}
261
262impl DiffChangeCounts {
263 #[must_use]
270 pub const fn total(self) -> usize {
271 self.additions.saturating_add(self.deletions)
272 }
273}
274
275#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
277pub enum DiffDisplayKind {
278 Metadata,
280 HunkHeader,
282 Context,
284 Addition,
286 Deletion,
288}
289
290impl DiffDisplayKind {
291 #[must_use]
293 pub const fn is_diff(self) -> bool {
294 matches!(self, Self::Context | Self::Addition | Self::Deletion)
295 }
296}
297
298#[derive(Clone, Debug, Eq, PartialEq)]
300pub struct DiffDisplayLine {
301 pub kind: DiffDisplayKind,
303 pub old_line: Option<u32>,
305 pub new_line: Option<u32>,
307 pub text: String,
309 pub changed: WordChangedRanges,
311}
312
313impl DiffDisplayLine {
314 #[must_use]
316 pub fn body(kind: DiffDisplayKind, old_line: Option<u32>, new_line: Option<u32>, text: String) -> Self {
317 Self {
318 kind,
319 old_line,
320 new_line,
321 text,
322 changed: Vec::new(),
323 }
324 }
325
326 #[must_use]
328 pub const fn is_diff(&self) -> bool {
329 self.kind.is_diff()
330 }
331
332 #[must_use]
334 pub fn numbered_text(&self, line_number_width: usize) -> String {
335 match self.kind {
336 DiffDisplayKind::Metadata | DiffDisplayKind::HunkHeader => self.text.clone(),
337 DiffDisplayKind::Deletion => {
338 format!("-{:>width$} │ {}", self.old_line.unwrap_or_default(), self.text, width = line_number_width)
339 }
340 DiffDisplayKind::Addition => {
341 format!("+{:>width$} │ {}", self.new_line.unwrap_or_default(), self.text, width = line_number_width)
342 }
343 DiffDisplayKind::Context => format!(
344 " {:>width$} │ {}",
345 self.new_line.or(self.old_line).unwrap_or_default(),
346 self.text,
347 width = line_number_width
348 ),
349 }
350 }
351}
352pub const DIFF_MIN_SIDE_BY_SIDE_WIDTH: usize = 60;
357
358#[must_use]
363pub const fn diff_side_by_side_fits(available_width: Option<usize>) -> bool {
364 match available_width {
365 Some(width) => width >= DIFF_MIN_SIDE_BY_SIDE_WIDTH,
366 None => true,
367 }
368}
369
370pub const DIFF_MIN_BODY_WIDTH_WITH_GUTTER: usize = 20;
376
377#[must_use]
383pub const fn diff_gutter_width(line_number_width: usize) -> usize {
384 line_number_width.saturating_add(4)
385}
386
387#[must_use]
390pub const fn diff_gutter_fits(available_width: usize, line_number_width: usize) -> bool {
391 available_width >= diff_gutter_width(line_number_width).saturating_add(DIFF_MIN_BODY_WIDTH_WITH_GUTTER)
392}
393
394#[must_use]
401pub const fn diff_layout_width(available_width: usize, line_number_width: usize, show_gutter: bool) -> usize {
402 if show_gutter {
403 available_width
404 } else {
405 available_width.saturating_add(diff_gutter_width(line_number_width))
406 }
407}
408#[derive(Clone, Debug, Eq, PartialEq)]
410pub struct SideBySideRow {
411 pub left: Option<DiffDisplayLine>,
413 pub right: Option<DiffDisplayLine>,
415}
416
417impl SideBySideRow {
418 #[must_use]
420 pub fn is_full_width(&self) -> bool {
421 self.right.is_none()
422 && self
423 .left
424 .as_ref()
425 .is_some_and(|line| matches!(line.kind, DiffDisplayKind::HunkHeader | DiffDisplayKind::Metadata))
426 }
427}
428#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
430#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
431#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
432pub enum DiffLayout {
433 #[default]
435 Unified,
436 SideBySide,
438}
439
440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
442pub struct LayoutOptions {
443 pub layout: DiffLayout,
445 pub width: usize,
447 pub max_rows: usize,
449 pub wrap: bool,
451 pub min_side_by_side_width: usize,
453}
454
455impl Default for LayoutOptions {
456 fn default() -> Self {
457 Self {
458 layout: DiffLayout::Unified,
459 width: 80,
460 max_rows: 2_000,
461 wrap: true,
462 min_side_by_side_width: DIFF_MIN_SIDE_BY_SIDE_WIDTH,
463 }
464 }
465}
466
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
469pub enum DiffRowKind {
470 Metadata,
472 HunkHeader,
474 Context,
476 Addition,
478 Deletion,
480 Omission,
482}
483
484#[derive(Debug, Clone, PartialEq, Eq)]
486pub struct DiffSegment {
487 pub text: String,
489 pub emphasized: bool,
491}
492
493#[derive(Debug, Clone, PartialEq, Eq)]
495pub struct DiffCell {
496 pub old_line: Option<u32>,
498 pub new_line: Option<u32>,
500 pub marker: char,
502 pub segments: Vec<DiffSegment>,
504}
505
506#[derive(Debug, Clone, PartialEq, Eq)]
508pub struct DiffRow {
509 pub kind: DiffRowKind,
511 pub marker: char,
513 pub hunk_index: Option<usize>,
515 pub continuation: bool,
517 pub left: Option<DiffCell>,
519 pub right: Option<DiffCell>,
521}