A simple TUI Library written in Rust
1/// A patch operation for a single display line.
2#[derive(Debug, Clone, PartialEq, Eq)]
3pub enum LinePatch {
4 /// Line is identical — skip.
5 Keep,
6 /// Line changed — emit new content.
7 Replace(String),
8}
9
10/// Compute a minimal line-by-line diff between `old` and `new` line slices.
11///
12/// Returns one `LinePatch` per line in the *longer* of the two slices.
13/// Lines beyond the shorter length are `Replace` (new) or implied deletions
14/// (handled by the caller via height management).
15pub fn diff_lines(old: &[String], new: &[String]) -> Vec<LinePatch> {
16 let max_len = old.len().max(new.len());
17 let mut patches = Vec::with_capacity(max_len);
18 for i in 0..max_len {
19 match (old.get(i), new.get(i)) {
20 (Some(o), Some(n)) => {
21 if o == n {
22 patches.push(LinePatch::Keep);
23 } else {
24 patches.push(LinePatch::Replace(n.clone()));
25 }
26 }
27 (None, Some(n)) => {
28 patches.push(LinePatch::Replace(n.clone()));
29 }
30 (Some(_), None) => {
31 // New view is shorter — the caller clears extra lines.
32 // Represent as replacing with empty.
33 patches.push(LinePatch::Replace(String::new()));
34 }
35 (None, None) => {}
36 }
37 }
38 patches
39}
40
41/// Count how many lines actually need rewriting.
42pub fn changed_count(patches: &[LinePatch]) -> usize {
43 patches
44 .iter()
45 .filter(|p| matches!(p, LinePatch::Replace(_)))
46 .count()
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
54 fn identical_lines_are_kept() {
55 let old = vec!["hello".to_string(), "world".to_string()];
56 let new = old.clone();
57 let patches = diff_lines(&old, &new);
58 assert_eq!(patches, vec![LinePatch::Keep, LinePatch::Keep]);
59 }
60
61 #[test]
62 fn changed_line_is_replaced() {
63 let old = vec!["hello".to_string(), "world".to_string()];
64 let new = vec!["hello".to_string(), "rust".to_string()];
65 let patches = diff_lines(&old, &new);
66 assert_eq!(patches[0], LinePatch::Keep);
67 assert_eq!(patches[1], LinePatch::Replace("rust".to_string()));
68 }
69
70 #[test]
71 fn new_is_longer() {
72 let old = vec!["a".to_string()];
73 let new = vec!["a".to_string(), "b".to_string()];
74 let patches = diff_lines(&old, &new);
75 assert_eq!(patches.len(), 2);
76 assert_eq!(patches[0], LinePatch::Keep);
77 assert_eq!(patches[1], LinePatch::Replace("b".to_string()));
78 }
79
80 #[test]
81 fn new_is_shorter() {
82 let old = vec!["a".to_string(), "b".to_string()];
83 let new = vec!["a".to_string()];
84 let patches = diff_lines(&old, &new);
85 assert_eq!(patches.len(), 2);
86 assert_eq!(patches[0], LinePatch::Keep);
87 assert_eq!(patches[1], LinePatch::Replace(String::new()));
88 }
89
90 #[test]
91 fn all_changed() {
92 let old = vec!["x".to_string()];
93 let new = vec!["y".to_string()];
94 assert_eq!(changed_count(&diff_lines(&old, &new)), 1);
95 }
96
97 #[test]
98 fn empty_slices() {
99 let patches = diff_lines(&[], &[]);
100 assert!(patches.is_empty());
101 }
102
103 #[test]
104 fn changed_count_correct() {
105 let old = vec!["a".to_string(), "b".to_string(), "c".to_string()];
106 let new = vec!["a".to_string(), "X".to_string(), "c".to_string()];
107 assert_eq!(changed_count(&diff_lines(&old, &new)), 1);
108 }
109}