strop_engine/editor/git_memory/
presentation.rs1use std::ops::Deref;
4use std::sync::Arc;
5
6use ropey::Rope;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use strop_git::{DiffLine, Hunk, LineOrigin};
9
10use crate::editor::DiffRow;
11
12#[derive(Debug, Clone)]
13pub struct PreparedDiff(Arc<DiffData>);
14
15#[derive(Debug)]
16struct DiffData {
17 label: String,
18 hunks: Vec<Arc<Hunk>>,
19 text: Rope,
20 starts: Vec<usize>,
21 emphasis: Vec<Option<(usize, usize)>>,
22 added: usize,
23 deleted: usize,
24 gutter_width: usize,
25}
26
27impl PreparedDiff {
28 pub fn new(label: String, hunks: Vec<Hunk>) -> Self {
29 let (added, deleted) = super::hunk_stats(&hunks);
30 let text = Rope::from_str(&super::diff_surface_text(&label, &hunks));
31 let mut starts = Vec::with_capacity(hunks.len());
32 let mut emphasis = vec![None]; let mut max_lineno = 0;
34 for hunk in &hunks {
35 starts.push(emphasis.len());
36 emphasis.push(None); let base = emphasis.len();
38 emphasis.resize(base + hunk.lines.len(), None);
39 prepare_emphasis(&hunk.lines, &mut emphasis[base..]);
40 for line in &hunk.lines {
41 max_lineno = max_lineno
42 .max(line.old_lineno.unwrap_or(0))
43 .max(line.new_lineno.unwrap_or(0));
44 }
45 }
46 let digits = (max_lineno.checked_ilog10().unwrap_or(0) as usize + 1).max(3);
47 Self(Arc::new(DiffData {
48 label,
49 hunks: hunks.into_iter().map(Arc::new).collect(),
50 text,
51 starts,
52 emphasis,
53 added,
54 deleted,
55 gutter_width: 2 * digits + 3,
56 }))
57 }
58
59 pub fn label(&self) -> &str {
60 &self.0.label
61 }
62 pub(crate) fn text(&self) -> Rope {
63 self.0.text.clone()
64 }
65 pub fn added(&self) -> usize {
66 self.0.added
67 }
68 pub fn deleted(&self) -> usize {
69 self.0.deleted
70 }
71 pub fn gutter_width(&self) -> usize {
72 self.0.gutter_width
73 }
74 pub fn emphasis(&self, row: usize) -> Option<(usize, usize)> {
75 self.0.emphasis.get(row).copied().flatten()
76 }
77 pub(crate) fn row(&self, row: usize) -> Option<DiffRow<'_>> {
78 if row == 0 {
79 return Some(DiffRow::Stats);
80 }
81 let index = self
82 .0
83 .starts
84 .partition_point(|start| *start <= row)
85 .checked_sub(1)?;
86 let hunk = &self.0.hunks[index];
87 let relative = row - self.0.starts[index];
88 if relative == 0 {
89 Some(DiffRow::HunkHeader(hunk))
90 } else {
91 hunk.lines.get(relative - 1).map(DiffRow::Line)
92 }
93 }
94}
95
96impl Deref for PreparedDiff {
97 type Target = [Arc<Hunk>];
98 fn deref(&self) -> &Self::Target {
99 &self.0.hunks
100 }
101}
102impl<'a> IntoIterator for &'a PreparedDiff {
103 type Item = &'a Arc<Hunk>;
104 type IntoIter = std::slice::Iter<'a, Arc<Hunk>>;
105 fn into_iter(self) -> Self::IntoIter {
106 self.iter()
107 }
108}
109impl Serialize for PreparedDiff {
110 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
111 (&self.0.label, &self.0.hunks).serialize(serializer)
112 }
113}
114impl<'de> Deserialize<'de> for PreparedDiff {
115 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
116 let (label, hunks) = Deserialize::deserialize(deserializer)?;
117 Ok(Self::new(label, hunks))
118 }
119}
120
121fn prepare_emphasis(lines: &[DiffLine], output: &mut [Option<(usize, usize)>]) {
124 let mut at = 0;
125 while at < lines.len() {
126 if lines[at].origin != LineOrigin::Deletion {
127 at += 1;
128 continue;
129 }
130 let deleted = at;
131 while at < lines.len() && lines[at].origin == LineOrigin::Deletion {
132 at += 1;
133 }
134 let added = at;
135 while at < lines.len() && lines[at].origin == LineOrigin::Addition {
136 at += 1;
137 }
138 for offset in 0..(added - deleted).min(at - added) {
139 let left = lines[deleted + offset].text_str();
140 let right = lines[added + offset].text_str();
141 output[deleted + offset] = Some(changed_range(&left, &right));
142 output[added + offset] = Some(changed_range(&right, &left));
143 }
144 }
145}
146
147fn changed_range(a: &str, b: &str) -> (usize, usize) {
148 let prefix: usize = a
149 .chars()
150 .zip(b.chars())
151 .take_while(|(x, y)| x == y)
152 .map(|(c, _)| c.len_utf8())
153 .sum();
154 let suffix: usize = a[prefix..]
155 .chars()
156 .rev()
157 .zip(b[prefix..].chars().rev())
158 .take_while(|(x, y)| x == y)
159 .map(|(c, _)| c.len_utf8())
160 .sum();
161 (prefix, a.len() - suffix)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 #[test]
168 fn changed_spans_use_each_sides_unicode_offsets() {
169 let lines = vec![
170 DiffLine {
171 origin: LineOrigin::Deletion,
172 old_lineno: Some(4),
173 new_lineno: None,
174 text: "a界z".into(),
175 has_newline: true,
176 },
177 DiffLine {
178 origin: LineOrigin::Addition,
179 old_lineno: None,
180 new_lineno: Some(4),
181 text: "aééz".into(),
182 has_newline: true,
183 },
184 ];
185 let mut spans = vec![None; 2];
186 prepare_emphasis(&lines, &mut spans);
187 assert_eq!(
188 &lines[0].text_str()[spans[0].unwrap().0..spans[0].unwrap().1],
189 "界"
190 );
191 assert_eq!(
192 &lines[1].text_str()[spans[1].unwrap().0..spans[1].unwrap().1],
193 "éé"
194 );
195 }
196}