Skip to main content

sequoia_buffer/
attr.rs

1use sequoia_core::Color;
2
3#[derive(Clone, Debug, PartialEq)]
4pub struct SpanAttr {
5    pub bold: bool,
6    pub italic: bool,
7    pub underline: bool,
8    pub strike: bool,
9    pub color: Option<Color>,
10    pub bg: Option<Color>,
11    pub size_delta: f32,
12    pub link: Option<String>,
13    pub code: bool,
14    pub mention: Option<u64>,
15}
16
17impl Default for SpanAttr {
18    fn default() -> Self {
19        Self::plain()
20    }
21}
22
23impl SpanAttr {
24    pub fn plain() -> Self {
25        Self {
26            bold: false,
27            italic: false,
28            underline: false,
29            strike: false,
30            color: None,
31            bg: None,
32            size_delta: 0.0,
33            link: None,
34            code: false,
35            mention: None,
36        }
37    }
38
39    pub fn is_plain(&self) -> bool {
40        !self.bold
41            && !self.italic
42            && !self.underline
43            && !self.strike
44            && self.color.is_none()
45            && self.bg.is_none()
46            && self.size_delta == 0.0
47            && self.link.is_none()
48            && !self.code
49            && self.mention.is_none()
50    }
51
52    pub fn bold() -> Self {
53        Self {
54            bold: true,
55            ..Self::plain()
56        }
57    }
58
59    pub fn link(url: impl Into<String>) -> Self {
60        Self {
61            link: Some(url.into()),
62            ..Self::plain()
63        }
64    }
65}
66
67#[derive(Clone, Debug, PartialEq)]
68pub struct StyledSpan {
69    pub start: usize,
70    pub end: usize,
71    pub attr: SpanAttr,
72}
73
74impl StyledSpan {
75    pub fn new(start: usize, end: usize, attr: SpanAttr) -> Self {
76        Self { start, end, attr }
77    }
78
79    pub fn len(&self) -> usize {
80        self.end.saturating_sub(self.start)
81    }
82
83    pub fn is_empty(&self) -> bool {
84        self.start >= self.end
85    }
86
87    pub fn contains_middle(&self, pos: usize) -> bool {
88        self.start < pos && pos < self.end
89    }
90
91    pub fn overlaps(&self, range: std::ops::Range<usize>) -> bool {
92        self.start < range.end && range.start < self.end
93    }
94
95    pub fn clipped(&self, range: std::ops::Range<usize>) -> Option<Self> {
96        let s = self.start.max(range.start);
97        let e = self.end.min(range.end);
98        if s < e {
99            Some(Self {
100                start: s,
101                end: e,
102                attr: self.attr.clone(),
103            })
104        } else {
105            None
106        }
107    }
108}
109
110pub fn normalize_spans(spans: &mut Vec<StyledSpan>) {
111    spans.retain(|s| s.start < s.end);
112    spans.sort_by_key(|s| (s.start, s.end));
113    let mut out: Vec<StyledSpan> = Vec::with_capacity(spans.len());
114    for mut cur in spans.drain(..) {
115        if cur.start >= cur.end {
116            continue;
117        }
118        if let Some(prev) = out.last_mut() {
119            if cur.start < prev.end {
120                if prev.attr == cur.attr {
121                    prev.end = prev.end.max(cur.end);
122                    continue;
123                }
124                cur.start = prev.end;
125                if cur.start >= cur.end {
126                    continue;
127                }
128            }
129            if prev.end == cur.start && prev.attr == cur.attr {
130                prev.end = cur.end;
131                continue;
132            }
133        }
134        out.push(cur);
135    }
136    *spans = out;
137}
138
139pub fn hash_span_attr<H: std::hash::Hasher>(a: &SpanAttr, h: &mut H) {
140    use std::hash::Hash;
141    a.bold.hash(h);
142    a.italic.hash(h);
143    a.underline.hash(h);
144    a.strike.hash(h);
145    a.color
146        .map(|c| (c.r.to_bits(), c.g.to_bits(), c.b.to_bits(), c.a.to_bits()))
147        .hash(h);
148    a.bg.map(|c| (c.r.to_bits(), c.g.to_bits(), c.b.to_bits(), c.a.to_bits()))
149        .hash(h);
150    let q = if a.size_delta.is_finite() {
151        (a.size_delta * 4.0).round() as i32
152    } else {
153        0
154    };
155    q.hash(h);
156    a.link.hash(h);
157    a.code.hash(h);
158    a.mention.hash(h);
159}
160
161pub fn hash_spans_rebased<H: std::hash::Hasher>(spans: &[StyledSpan], base: usize, h: &mut H) {
162    use std::hash::Hash;
163    spans.len().hash(h);
164    for s in spans {
165        if s.is_empty() {
166            continue;
167        }
168        s.start.saturating_sub(base).hash(h);
169        s.end.saturating_sub(base).hash(h);
170        hash_span_attr(&s.attr, h);
171    }
172}
173
174pub fn spans_hash(spans: &[StyledSpan]) -> u64 {
175    use std::collections::hash_map::DefaultHasher;
176    use std::hash::Hasher;
177    let mut h = DefaultHasher::new();
178    hash_spans_rebased(spans, 0, &mut h);
179    h.finish()
180}
181
182pub fn line_hash(text: &str, spans: &[StyledSpan], width: f32) -> u64 {
183    use std::collections::hash_map::DefaultHasher;
184    use std::hash::{Hash, Hasher};
185    let mut h = DefaultHasher::new();
186    text.hash(&mut h);
187    hash_spans_rebased(spans, 0, &mut h);
188    ((width.max(0.0) * 4.0).round() as u32).hash(&mut h);
189    h.finish()
190}
191
192pub fn rebase_spans(spans: &[StyledSpan], base: usize) -> Vec<StyledSpan> {
193    spans
194        .iter()
195        .map(|s| {
196            StyledSpan::new(
197                s.start.saturating_sub(base),
198                s.end.saturating_sub(base),
199                s.attr.clone(),
200            )
201        })
202        .collect()
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn plain_roundtrip() {
211        let p = SpanAttr::plain();
212        assert!(p.is_plain());
213        assert_eq!(p, SpanAttr::default());
214        let b = SpanAttr::bold();
215        assert!(!b.is_plain());
216        assert!(b.bold);
217    }
218
219    #[test]
220    fn neighbours_merge() {
221        let mut v = vec![
222            StyledSpan::new(0, 2, SpanAttr::bold()),
223            StyledSpan::new(2, 5, SpanAttr::bold()),
224            StyledSpan::new(5, 6, SpanAttr::plain()),
225        ];
226
227        normalize_spans(&mut v);
228        assert_eq!(v.len(), 2);
229        assert_eq!((v[0].start, v[0].end), (0, 5));
230    }
231
232    #[test]
233    fn overlap_truncates_left_biased() {
234        let mut v = vec![
235            StyledSpan::new(0, 5, SpanAttr::bold()),
236            StyledSpan::new(3, 8, SpanAttr::link("x")),
237        ];
238        normalize_spans(&mut v);
239        assert_eq!(v.len(), 2);
240        assert_eq!((v[0].start, v[0].end), (0, 5));
241        assert_eq!((v[1].start, v[1].end), (5, 8));
242    }
243
244    #[test]
245    fn empties_dropped_and_sorted() {
246        let mut v = vec![
247            StyledSpan::new(8, 9, SpanAttr::bold()),
248            StyledSpan::new(4, 4, SpanAttr::bold()),
249            StyledSpan::new(0, 2, SpanAttr::bold()),
250        ];
251        normalize_spans(&mut v);
252        assert_eq!(v.len(), 2);
253        assert_eq!((v[0].start, v[0].end), (0, 2));
254        assert_eq!((v[1].start, v[1].end), (8, 9));
255    }
256
257    #[test]
258    fn line_hash_contract() {
259        let a = vec![StyledSpan::new(0, 2, SpanAttr::bold())];
260        let h1 = line_hash("abcdef", &a, 100.0);
261
262        assert_eq!(h1, line_hash("abcdef", &a, 100.0));
263
264        assert_ne!(h1, line_hash("abcdeg", &a, 100.0));
265        assert_ne!(
266            h1,
267            line_hash(
268                "abcdef",
269                &[StyledSpan::new(0, 2, SpanAttr::link("x"))],
270                100.0
271            )
272        );
273        assert_ne!(
274            h1,
275            line_hash("abcdef", &[StyledSpan::new(1, 3, SpanAttr::bold())], 100.0)
276        );
277        assert_ne!(h1, line_hash("abcdef", &[], 100.0));
278        assert_ne!(h1, line_hash("abcdef", &a, 200.0));
279
280        let abs_a = vec![StyledSpan::new(100, 102, SpanAttr::bold())];
281        let abs_b = vec![StyledSpan::new(200, 202, SpanAttr::bold())];
282        assert_eq!(
283            line_hash("abcdef", &rebase_spans(&abs_a, 100), 0.0),
284            line_hash("abcdef", &rebase_spans(&abs_b, 200), 0.0)
285        );
286    }
287}