aft/hashline/apply/
region.rs1use crate::hashline::snapshot::{AffectedRegion, LineRange};
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum RegionDelta {
12 OutputSpan { start: usize, count: usize },
15 Deletion { at: usize },
19}
20
21pub fn build_affected_region(deltas: impl IntoIterator<Item = RegionDelta>) -> AffectedRegion {
27 let mut ranges = Vec::new();
28 for delta in deltas {
29 match delta {
30 RegionDelta::OutputSpan { start, count } if count > 0 && start > 0 => {
31 ranges.push(LineRange::new(start, start + count - 1));
32 }
33 RegionDelta::Deletion { at } if at > 0 => {
34 ranges.push(LineRange::new(at, at));
39 }
40 _ => {}
41 }
42 }
43 AffectedRegion::new(ranges)
44}
45
46pub fn affected_from_line_diff(before: &[String], after: &[String]) -> AffectedRegion {
50 if before.is_empty() && after.is_empty() {
51 return AffectedRegion::default();
52 }
53 if before.is_empty() {
54 return AffectedRegion::insertion(1, after.len());
55 }
56 if after.is_empty() {
57 return AffectedRegion::default();
59 }
60
61 let mut prefix = 0usize;
63 while prefix < before.len() && prefix < after.len() && before[prefix] == after[prefix] {
64 prefix += 1;
65 }
66 let mut suffix = 0usize;
67 while suffix < before.len().saturating_sub(prefix)
68 && suffix < after.len().saturating_sub(prefix)
69 && before[before.len() - 1 - suffix] == after[after.len() - 1 - suffix]
70 {
71 suffix += 1;
72 }
73 let after_mid = after.len().saturating_sub(prefix + suffix);
74 if after_mid == 0 {
75 let at = if prefix < after.len() {
78 prefix + 1
79 } else {
80 after.len().max(1)
81 };
82 return build_affected_region([RegionDelta::Deletion { at }]);
83 }
84 AffectedRegion::insertion(prefix + 1, after_mid)
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn adjacent_spans_coalesce() {
93 let region = build_affected_region([
94 RegionDelta::OutputSpan { start: 2, count: 2 },
95 RegionDelta::OutputSpan { start: 4, count: 1 },
96 ]);
97 assert_eq!(region.ranges, vec![LineRange::new(2, 4)]);
98 }
99
100 #[test]
101 fn pure_insertion_into_empty_file() {
102 let region = affected_from_line_diff(&[], &["a".into(), "b".into()]);
103 assert_eq!(region.ranges, vec![LineRange::new(1, 2)]);
104 }
105
106 #[test]
107 fn deletion_to_empty_has_no_rows() {
108 let region = affected_from_line_diff(&["a".into()], &[]);
109 assert!(region.is_empty());
110 }
111
112 #[test]
113 fn middle_replacement_marks_new_rows() {
114 let before = vec!["a".into(), "b".into(), "c".into()];
115 let after = vec!["a".into(), "B".into(), "C".into(), "c".into()];
116 let region = affected_from_line_diff(&before, &after);
117 assert_eq!(region.ranges, vec![LineRange::new(2, 3)]);
118 }
119}