Skip to main content

strop_engine/editor/git_memory/
presentation.rs

1//! Worker-built Git display data. UI clones share immutable text and indexes;
2//! replay records source data and reconstructs the same checked projection.
3use 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]; // stats row
33        let mut max_lineno = 0;
34        for hunk in &hunks {
35            starts.push(emphasis.len());
36            emphasis.push(None); // hunk header
37            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
121// Pair whole deletion/addition runs once, rather than rescanning a run for
122// every visible row. Each range belongs to its own side's UTF-8 byte domain.
123fn 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}