nightshade_api/web/
diff.rs1use leptos::prelude::*;
4
5#[derive(Clone, Copy, PartialEq, Eq)]
7pub enum LineKind {
8 Equal,
10 Insert,
12 Delete,
14}
15
16#[derive(Clone)]
19pub struct DiffLine {
20 pub kind: LineKind,
22 pub text: String,
24 pub old_line: Option<usize>,
26 pub new_line: Option<usize>,
28}
29
30pub fn diff_lines(old: &str, new: &str) -> Vec<DiffLine> {
34 let left: Vec<&str> = old.lines().collect();
35 let right: Vec<&str> = new.lines().collect();
36 let rows = left.len();
37 let cols = right.len();
38
39 let mut lcs = vec![vec![0usize; cols + 1]; rows + 1];
40 for index in (0..rows).rev() {
41 for other in (0..cols).rev() {
42 lcs[index][other] = if left[index] == right[other] {
43 lcs[index + 1][other + 1] + 1
44 } else {
45 lcs[index + 1][other].max(lcs[index][other + 1])
46 };
47 }
48 }
49
50 let mut out = Vec::new();
51 let mut index = 0;
52 let mut other = 0;
53 let mut old_line = 1;
54 let mut new_line = 1;
55 while index < rows && other < cols {
56 if left[index] == right[other] {
57 out.push(DiffLine {
58 kind: LineKind::Equal,
59 text: left[index].to_string(),
60 old_line: Some(old_line),
61 new_line: Some(new_line),
62 });
63 index += 1;
64 other += 1;
65 old_line += 1;
66 new_line += 1;
67 } else if lcs[index + 1][other] >= lcs[index][other + 1] {
68 out.push(DiffLine {
69 kind: LineKind::Delete,
70 text: left[index].to_string(),
71 old_line: Some(old_line),
72 new_line: None,
73 });
74 index += 1;
75 old_line += 1;
76 } else {
77 out.push(DiffLine {
78 kind: LineKind::Insert,
79 text: right[other].to_string(),
80 old_line: None,
81 new_line: Some(new_line),
82 });
83 other += 1;
84 new_line += 1;
85 }
86 }
87 while index < rows {
88 out.push(DiffLine {
89 kind: LineKind::Delete,
90 text: left[index].to_string(),
91 old_line: Some(old_line),
92 new_line: None,
93 });
94 index += 1;
95 old_line += 1;
96 }
97 while other < cols {
98 out.push(DiffLine {
99 kind: LineKind::Insert,
100 text: right[other].to_string(),
101 old_line: None,
102 new_line: Some(new_line),
103 });
104 other += 1;
105 new_line += 1;
106 }
107 out
108}
109
110fn marker(kind: LineKind) -> &'static str {
111 match kind {
112 LineKind::Equal => " ",
113 LineKind::Insert => "+",
114 LineKind::Delete => "-",
115 }
116}
117
118fn kind_class(kind: LineKind) -> &'static str {
119 match kind {
120 LineKind::Equal => "equal",
121 LineKind::Insert => "insert",
122 LineKind::Delete => "delete",
123 }
124}
125
126#[component]
129pub fn Diff(#[prop(into)] old: Signal<String>, #[prop(into)] new: Signal<String>) -> impl IntoView {
130 view! {
131 <div class="nightshade-diff">
132 {move || {
133 diff_lines(&old.get(), &new.get())
134 .into_iter()
135 .map(|line| {
136 let old_number = line
137 .old_line
138 .map(|value| value.to_string())
139 .unwrap_or_default();
140 let new_number = line
141 .new_line
142 .map(|value| value.to_string())
143 .unwrap_or_default();
144 view! {
145 <div class=format!("nightshade-diff-line {}", kind_class(line.kind))>
146 <span class="nightshade-diff-num">{old_number}</span>
147 <span class="nightshade-diff-num">{new_number}</span>
148 <span class="nightshade-diff-marker">{marker(line.kind)}</span>
149 <span class="nightshade-diff-text">{line.text}</span>
150 </div>
151 }
152 })
153 .collect_view()
154 }}
155 </div>
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::{LineKind, diff_lines};
162
163 #[test]
164 fn identical_text_is_all_equal() {
165 let lines = diff_lines("a\nb\nc", "a\nb\nc");
166 assert!(lines.iter().all(|line| line.kind == LineKind::Equal));
167 assert_eq!(lines.len(), 3);
168 }
169
170 #[test]
171 fn detects_insert_and_delete() {
172 let lines = diff_lines("a\nb\nc", "a\nc\nd");
173 let inserts = lines.iter().filter(|l| l.kind == LineKind::Insert).count();
174 let deletes = lines.iter().filter(|l| l.kind == LineKind::Delete).count();
175 assert_eq!(deletes, 1);
176 assert_eq!(inserts, 1);
177 assert!(
178 lines
179 .iter()
180 .any(|l| l.kind == LineKind::Equal && l.text == "a")
181 );
182 }
183}