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
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
122pub enum DiffLineKind {
123 Context,
125 Addition,
127 Deletion,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
133#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
134pub struct DiffLine {
135 pub kind: DiffLineKind,
137 pub old_line: Option<u32>,
139 pub new_line: Option<u32>,
141 pub text: String,
143}
144
145#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
147#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
148pub struct DiffStats {
149 pub additions: usize,
151 pub deletions: usize,
153 pub context: usize,
155 pub hunks: usize,
157 pub omitted_rows: usize,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
164pub struct DiffDocument {
165 pub hunks: Vec<DiffHunk>,
167 pub stats: DiffStats,
169 #[cfg_attr(feature = "serde", serde(default = "default_inline_timeout"))]
170 pub(crate) inline_timeout: Duration,
171}
172
173impl Default for DiffDocument {
174 fn default() -> Self {
175 Self {
176 hunks: Vec::new(),
177 stats: DiffStats::default(),
178 inline_timeout: default_inline_timeout(),
179 }
180 }
181}
182
183pub(crate) fn default_inline_timeout() -> Duration {
184 Duration::from_millis(40)
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct ParseDiffError {
190 message: &'static str,
191}
192
193impl ParseDiffError {
194 pub(crate) fn new(message: &'static str) -> Self {
195 Self { message }
196 }
197}
198
199impl fmt::Display for ParseDiffError {
200 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
201 formatter.write_str(self.message)
202 }
203}
204
205impl std::error::Error for ParseDiffError {}
206
207#[derive(Debug, Clone)]
209#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
210pub struct DiffBundle {
211 pub hunks: Vec<DiffHunk>,
213 pub formatted: String,
215 pub is_empty: bool,
217}
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
221pub struct IntralineRange {
222 pub start: usize,
224 pub end: usize,
226}
227
228pub type WordChangedRanges = Vec<(usize, usize)>;
230
231#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
233pub struct DiffChangeCounts {
234 pub additions: usize,
236 pub deletions: usize,
238}
239
240impl DiffChangeCounts {
241 #[must_use]
248 pub const fn total(self) -> usize {
249 self.additions.saturating_add(self.deletions)
250 }
251}
252
253#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
255pub enum DiffDisplayKind {
256 Metadata,
258 HunkHeader,
260 Context,
262 Addition,
264 Deletion,
266}
267
268impl DiffDisplayKind {
269 #[must_use]
271 pub const fn is_diff(self) -> bool {
272 matches!(self, Self::Context | Self::Addition | Self::Deletion)
273 }
274}
275
276#[derive(Clone, Debug, Eq, PartialEq)]
278pub struct DiffDisplayLine {
279 pub kind: DiffDisplayKind,
281 pub old_line: Option<u32>,
283 pub new_line: Option<u32>,
285 pub text: String,
287 pub changed: WordChangedRanges,
289}
290
291impl DiffDisplayLine {
292 #[must_use]
294 pub fn body(kind: DiffDisplayKind, old_line: Option<u32>, new_line: Option<u32>, text: String) -> Self {
295 Self {
296 kind,
297 old_line,
298 new_line,
299 text,
300 changed: Vec::new(),
301 }
302 }
303
304 #[must_use]
306 pub const fn is_diff(&self) -> bool {
307 self.kind.is_diff()
308 }
309
310 #[must_use]
312 pub fn numbered_text(&self, line_number_width: usize) -> String {
313 match self.kind {
314 DiffDisplayKind::Metadata | DiffDisplayKind::HunkHeader => self.text.clone(),
315 DiffDisplayKind::Deletion => {
316 format!("-{:>width$} │ {}", self.old_line.unwrap_or_default(), self.text, width = line_number_width)
317 }
318 DiffDisplayKind::Addition => {
319 format!("+{:>width$} │ {}", self.new_line.unwrap_or_default(), self.text, width = line_number_width)
320 }
321 DiffDisplayKind::Context => format!(
322 " {:>width$} │ {}",
323 self.new_line.or(self.old_line).unwrap_or_default(),
324 self.text,
325 width = line_number_width
326 ),
327 }
328 }
329}
330pub const DIFF_MIN_SIDE_BY_SIDE_WIDTH: usize = 60;
335
336#[must_use]
341pub const fn diff_side_by_side_fits(available_width: Option<usize>) -> bool {
342 match available_width {
343 Some(width) => width >= DIFF_MIN_SIDE_BY_SIDE_WIDTH,
344 None => true,
345 }
346}
347
348pub const DIFF_MIN_BODY_WIDTH_WITH_GUTTER: usize = 20;
354
355#[must_use]
361pub const fn diff_gutter_width(line_number_width: usize) -> usize {
362 line_number_width.saturating_add(4)
363}
364
365#[must_use]
368pub const fn diff_gutter_fits(available_width: usize, line_number_width: usize) -> bool {
369 available_width >= diff_gutter_width(line_number_width).saturating_add(DIFF_MIN_BODY_WIDTH_WITH_GUTTER)
370}
371
372#[must_use]
379pub const fn diff_layout_width(available_width: usize, line_number_width: usize, show_gutter: bool) -> usize {
380 if show_gutter {
381 available_width
382 } else {
383 available_width.saturating_add(diff_gutter_width(line_number_width))
384 }
385}
386#[derive(Clone, Debug, Eq, PartialEq)]
388pub struct SideBySideRow {
389 pub left: Option<DiffDisplayLine>,
391 pub right: Option<DiffDisplayLine>,
393}
394
395impl SideBySideRow {
396 #[must_use]
398 pub fn is_full_width(&self) -> bool {
399 self.right.is_none()
400 && self
401 .left
402 .as_ref()
403 .is_some_and(|line| matches!(line.kind, DiffDisplayKind::HunkHeader | DiffDisplayKind::Metadata))
404 }
405}
406#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
408#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
409#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
410pub enum DiffLayout {
411 #[default]
413 Unified,
414 SideBySide,
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub struct LayoutOptions {
421 pub layout: DiffLayout,
423 pub width: usize,
425 pub max_rows: usize,
427 pub wrap: bool,
429 pub min_side_by_side_width: usize,
431}
432
433impl Default for LayoutOptions {
434 fn default() -> Self {
435 Self {
436 layout: DiffLayout::Unified,
437 width: 80,
438 max_rows: 2_000,
439 wrap: true,
440 min_side_by_side_width: DIFF_MIN_SIDE_BY_SIDE_WIDTH,
441 }
442 }
443}
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub enum DiffRowKind {
448 Metadata,
450 HunkHeader,
452 Context,
454 Addition,
456 Deletion,
458 Omission,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq)]
464pub struct DiffSegment {
465 pub text: String,
467 pub emphasized: bool,
469}
470
471#[derive(Debug, Clone, PartialEq, Eq)]
473pub struct DiffCell {
474 pub old_line: Option<u32>,
476 pub new_line: Option<u32>,
478 pub marker: char,
480 pub segments: Vec<DiffSegment>,
482}
483
484#[derive(Debug, Clone, PartialEq, Eq)]
486pub struct DiffRow {
487 pub kind: DiffRowKind,
489 pub marker: char,
491 pub hunk_index: Option<usize>,
493 pub continuation: bool,
495 pub left: Option<DiffCell>,
497 pub right: Option<DiffCell>,
499}