mermaid_cli/render/
diff.rs1pub const DIFF_REMOVED_MARKER: &str = " - ";
11
12pub const DIFF_ADDED_MARKER: &str = " + ";
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DiffLineKind {
19 Context,
20 Removed,
21 Added,
22}
23
24pub fn parse_diff_line(line: &str) -> DiffLineKind {
29 let trimmed = line.trim_start();
30 let after_num = trimmed.trim_start_matches(|c: char| c.is_ascii_digit());
31 if after_num.starts_with(DIFF_REMOVED_MARKER) {
32 DiffLineKind::Removed
33 } else if after_num.starts_with(DIFF_ADDED_MARKER) {
34 DiffLineKind::Added
35 } else {
36 DiffLineKind::Context
37 }
38}
39
40#[derive(Debug, Clone)]
43pub(crate) struct DisplayDiff {
44 pub display_diff: String,
45 pub added: usize,
46 pub removed: usize,
47 pub truncated: bool,
48}
49
50const DIFF_CONTEXT_LINES: usize = 3;
52pub(crate) const MAX_DISPLAY_DIFF_LINES: usize = 220;
54
55pub(crate) fn generate_display_diff(old: &str, new: &str) -> DisplayDiff {
61 let old_lines: Vec<&str> = old.lines().collect();
62 let new_lines: Vec<&str> = new.lines().collect();
63 let mut prefix = 0usize;
64 let min_len = old_lines.len().min(new_lines.len());
65 while prefix < min_len && old_lines[prefix] == new_lines[prefix] {
66 prefix += 1;
67 }
68
69 let mut suffix = 0usize;
70 while suffix < min_len.saturating_sub(prefix)
71 && old_lines[old_lines.len() - 1 - suffix] == new_lines[new_lines.len() - 1 - suffix]
72 {
73 suffix += 1;
74 }
75
76 let old_changed_end = old_lines.len().saturating_sub(suffix);
77 let new_changed_end = new_lines.len().saturating_sub(suffix);
78 let old_changed = &old_lines[prefix..old_changed_end];
79 let new_changed = &new_lines[prefix..new_changed_end];
80 let added = new_changed.len();
81 let removed = old_changed.len();
82
83 let context_start = prefix.saturating_sub(DIFF_CONTEXT_LINES);
84 let context_end_old = (old_changed_end + DIFF_CONTEXT_LINES).min(old_lines.len());
85 let mut lines = Vec::new();
86
87 let mut truncated = false;
88 let push_line = |line: String, lines: &mut Vec<String>, truncated: &mut bool| {
89 if lines.len() < MAX_DISPLAY_DIFF_LINES {
90 lines.push(line);
91 } else {
92 *truncated = true;
93 }
94 };
95
96 for (idx, line) in old_lines[context_start..prefix].iter().enumerate() {
97 push_line(
98 format!("{:>4} {}", context_start + idx + 1, line),
99 &mut lines,
100 &mut truncated,
101 );
102 }
103 for (idx, line) in old_changed.iter().enumerate() {
104 push_line(
105 format!("{:>4}{}{}", prefix + idx + 1, DIFF_REMOVED_MARKER, line),
106 &mut lines,
107 &mut truncated,
108 );
109 }
110 for (idx, line) in new_changed.iter().enumerate() {
111 push_line(
112 format!("{:>4}{}{}", prefix + idx + 1, DIFF_ADDED_MARKER, line),
113 &mut lines,
114 &mut truncated,
115 );
116 }
117 for (idx, line) in old_lines[old_changed_end..context_end_old]
118 .iter()
119 .enumerate()
120 {
121 push_line(
122 format!("{:>4} {}", old_changed_end + idx + 1, line),
123 &mut lines,
124 &mut truncated,
125 );
126 }
127 if truncated {
128 lines.push(format!(
129 "... diff truncated after {MAX_DISPLAY_DIFF_LINES} display lines"
130 ));
131 }
132
133 DisplayDiff {
134 display_diff: lines.join("\n"),
135 added,
136 removed,
137 truncated,
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn parses_added_line() {
147 let line = format!(" 5{}fn main() {{", DIFF_ADDED_MARKER);
148 assert_eq!(parse_diff_line(&line), DiffLineKind::Added);
149 }
150
151 #[test]
152 fn parses_removed_line() {
153 let line = format!(" 12{}old = true;", DIFF_REMOVED_MARKER);
154 assert_eq!(parse_diff_line(&line), DiffLineKind::Removed);
155 }
156
157 #[test]
158 fn parses_context_line() {
159 let line = " 7 existing line";
160 assert_eq!(parse_diff_line(line), DiffLineKind::Context);
161 }
162
163 #[test]
164 fn malformed_falls_through_to_context() {
165 assert_eq!(parse_diff_line("no number at start"), DiffLineKind::Context);
166 assert_eq!(parse_diff_line(""), DiffLineKind::Context);
167 }
168}