A simple TUI Library written in Rust
at main 109 lines 3.5 kB view raw
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}