Skip to main content

neco_decor/
lib.rs

1//! Decoration data model for text editor highlight, marker, and widget ranges.
2
3use std::fmt;
4
5pub use neco_textview::RangeChange;
6
7/// Errors returned by decoration operations.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum DecorError {
10    InvalidRange { start: usize, end: usize },
11    EmptyHighlight { offset: usize },
12}
13
14impl fmt::Display for DecorError {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        match self {
17            DecorError::InvalidRange { start, end } => {
18                write!(f, "invalid range: start {start} > end {end}")
19            }
20            DecorError::EmptyHighlight { offset } => {
21                write!(f, "empty highlight at offset {offset}")
22            }
23        }
24    }
25}
26
27impl std::error::Error for DecorError {}
28
29/// Classification of a decoration range.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum DecorationKind {
32    Highlight,
33    Marker,
34    Widget { block: bool },
35}
36
37/// One decoration instance with range, kind, tag, and stacking priority.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Decoration {
40    start: usize,
41    end: usize,
42    kind: DecorationKind,
43    tag: u32,
44    priority: i16,
45}
46
47impl Decoration {
48    pub fn highlight(start: usize, end: usize, tag: u32) -> Result<Self, DecorError> {
49        if start > end {
50            return Err(DecorError::InvalidRange { start, end });
51        }
52        if start == end {
53            return Err(DecorError::EmptyHighlight { offset: start });
54        }
55        Ok(Self {
56            start,
57            end,
58            kind: DecorationKind::Highlight,
59            tag,
60            priority: 0,
61        })
62    }
63
64    pub fn marker(line_start: usize, tag: u32) -> Self {
65        Self {
66            start: line_start,
67            end: line_start,
68            kind: DecorationKind::Marker,
69            tag,
70            priority: 0,
71        }
72    }
73
74    pub fn widget(start: usize, end: usize, tag: u32, block: bool) -> Result<Self, DecorError> {
75        if start > end {
76            return Err(DecorError::InvalidRange { start, end });
77        }
78        Ok(Self {
79            start,
80            end,
81            kind: DecorationKind::Widget { block },
82            tag,
83            priority: 0,
84        })
85    }
86
87    pub fn with_priority(mut self, priority: i16) -> Self {
88        self.priority = priority;
89        self
90    }
91
92    pub fn start(&self) -> usize {
93        self.start
94    }
95
96    pub fn end(&self) -> usize {
97        self.end
98    }
99
100    pub fn kind(&self) -> DecorationKind {
101        self.kind
102    }
103
104    pub fn tag(&self) -> u32 {
105        self.tag
106    }
107
108    pub fn priority(&self) -> i16 {
109        self.priority
110    }
111}
112
113/// Stable identifier assigned when a decoration is added to a set.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
115pub struct DecorationId(u64);
116
117impl DecorationId {
118    /// Returns the underlying raw identifier for external boundaries.
119    pub fn into_raw(self) -> u64 {
120        self.0
121    }
122
123    /// Reconstructs a decoration identifier from a raw value.
124    pub fn from_raw(raw: u64) -> Self {
125        Self(raw)
126    }
127}
128
129#[derive(Debug, Clone)]
130struct DecorationEntry {
131    id: DecorationId,
132    decoration: Decoration,
133}
134
135/// Sorted container of decorations with range query and text-change tracking.
136#[derive(Debug, Clone)]
137pub struct DecorationSet {
138    entries: Vec<DecorationEntry>,
139    next_id: u64,
140}
141
142impl DecorationSet {
143    pub fn new() -> Self {
144        Self {
145            entries: Vec::new(),
146            next_id: 0,
147        }
148    }
149
150    pub fn add(&mut self, decoration: Decoration) -> DecorationId {
151        let id = DecorationId(self.next_id);
152        self.next_id += 1;
153        let pos = self
154            .entries
155            .partition_point(|e| e.decoration.start <= decoration.start);
156        self.entries.push(DecorationEntry { id, decoration });
157        let len = self.entries.len();
158        self.entries[pos..len].rotate_right(1);
159        id
160    }
161
162    pub fn remove(&mut self, id: DecorationId) -> bool {
163        if let Some(pos) = self.entries.iter().position(|e| e.id == id) {
164            self.entries.remove(pos);
165            true
166        } else {
167            false
168        }
169    }
170
171    pub fn query_range(&self, start: usize, end: usize) -> Vec<(DecorationId, &Decoration)> {
172        self.entries
173            .iter()
174            .filter(|e| {
175                let d = &e.decoration;
176                if d.start == d.end {
177                    d.start >= start && d.start < end
178                } else {
179                    d.start < end && d.end > start
180                }
181            })
182            .map(|e| (e.id, &e.decoration))
183            .collect()
184    }
185
186    pub fn query_tag(&self, tag: u32) -> Vec<(DecorationId, &Decoration)> {
187        self.entries
188            .iter()
189            .filter(|e| e.decoration.tag == tag)
190            .map(|e| (e.id, &e.decoration))
191            .collect()
192    }
193
194    pub fn iter(&self) -> impl Iterator<Item = (DecorationId, &Decoration)> {
195        self.entries.iter().map(|e| (e.id, &e.decoration))
196    }
197
198    pub fn len(&self) -> usize {
199        self.entries.len()
200    }
201
202    pub fn is_empty(&self) -> bool {
203        self.entries.is_empty()
204    }
205
206    pub fn clear(&mut self) {
207        self.entries.clear();
208    }
209
210    pub fn map_through_change(&mut self, change_start: usize, old_end: usize, new_end: usize) {
211        let delta = new_end as isize - old_end as isize;
212
213        self.entries.retain_mut(|entry| {
214            let d = &mut entry.decoration;
215
216            // Before the changed range.
217            if d.end <= change_start {
218                return true;
219            }
220
221            // After the changed range.
222            if d.start >= old_end {
223                d.start = (d.start as isize + delta) as usize;
224                d.end = (d.end as isize + delta) as usize;
225                return true;
226            }
227
228            // Overlapping the changed range.
229            match d.kind {
230                DecorationKind::Highlight => {
231                    if d.start >= change_start {
232                        d.start = change_start;
233                    }
234                    if d.end > old_end {
235                        d.end = (d.end as isize + delta) as usize;
236                    } else {
237                        d.end = new_end;
238                    }
239                    d.start < d.end
240                }
241                DecorationKind::Marker => {
242                    if d.start >= change_start && d.start < old_end {
243                        return false;
244                    }
245                    true
246                }
247                DecorationKind::Widget { .. } => {
248                    // Drop widgets fully covered by the change.
249                    if d.start >= change_start && d.end <= old_end {
250                        return false;
251                    }
252                    // Clamp partially overlapping widgets to the boundary.
253                    if d.start < change_start {
254                        // Keep start.
255                    } else {
256                        d.start = change_start;
257                    }
258                    if d.end > old_end {
259                        d.end = (d.end as isize + delta) as usize;
260                    } else {
261                        d.end = new_end;
262                    }
263                    true
264                }
265            }
266        });
267    }
268
269    pub fn map_through_changes(&mut self, changes: &[RangeChange]) {
270        for change in changes {
271            self.map_through_change(change.start(), change.old_end(), change.new_end());
272        }
273    }
274}
275
276impl Default for DecorationSet {
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn highlight_ok() {
288        let d = Decoration::highlight(0, 10, 1).unwrap();
289        assert_eq!(d.start(), 0);
290        assert_eq!(d.end(), 10);
291        assert_eq!(d.kind(), DecorationKind::Highlight);
292        assert_eq!(d.tag(), 1);
293        assert_eq!(d.priority(), 0);
294    }
295
296    #[test]
297    fn highlight_empty_range() {
298        let err = Decoration::highlight(5, 5, 1).unwrap_err();
299        assert_eq!(err, DecorError::EmptyHighlight { offset: 5 });
300    }
301
302    #[test]
303    fn highlight_invalid_range() {
304        let err = Decoration::highlight(10, 5, 1).unwrap_err();
305        assert_eq!(err, DecorError::InvalidRange { start: 10, end: 5 });
306    }
307
308    #[test]
309    fn marker_ok() {
310        let d = Decoration::marker(42, 2);
311        assert_eq!(d.start(), 42);
312        assert_eq!(d.end(), 42);
313        assert_eq!(d.kind(), DecorationKind::Marker);
314        assert_eq!(d.tag(), 2);
315    }
316
317    #[test]
318    fn widget_ok() {
319        let d = Decoration::widget(10, 20, 3, true).unwrap();
320        assert_eq!(d.kind(), DecorationKind::Widget { block: true });
321    }
322
323    #[test]
324    fn widget_empty_range_ok() {
325        let d = Decoration::widget(5, 5, 3, false).unwrap();
326        assert_eq!(d.start(), 5);
327        assert_eq!(d.end(), 5);
328    }
329
330    #[test]
331    fn widget_invalid_range() {
332        let err = Decoration::widget(20, 10, 3, false).unwrap_err();
333        assert_eq!(err, DecorError::InvalidRange { start: 20, end: 10 });
334    }
335
336    #[test]
337    fn with_priority() {
338        let d = Decoration::marker(0, 1).with_priority(5);
339        assert_eq!(d.priority(), 5);
340    }
341
342    #[test]
343    fn set_add_iter_sorted() {
344        let mut set = DecorationSet::new();
345        set.add(Decoration::marker(20, 1));
346        set.add(Decoration::marker(5, 2));
347        set.add(Decoration::marker(10, 3));
348
349        let starts: Vec<usize> = set.iter().map(|(_, d)| d.start()).collect();
350        assert_eq!(starts, vec![5, 10, 20]);
351    }
352
353    #[test]
354    fn set_add_remove_len() {
355        let mut set = DecorationSet::new();
356        let id1 = set.add(Decoration::marker(0, 1));
357        let id2 = set.add(Decoration::marker(10, 2));
358        assert_eq!(set.len(), 2);
359
360        assert!(set.remove(id1));
361        assert_eq!(set.len(), 1);
362        assert!(!set.remove(id1));
363
364        assert!(set.remove(id2));
365        assert!(set.is_empty());
366    }
367
368    #[test]
369    fn decoration_id_roundtrips_through_raw_value() {
370        let mut set = DecorationSet::new();
371        let id = set.add(Decoration::marker(0, 1));
372        let raw = id.into_raw();
373
374        assert!(set.remove(DecorationId::from_raw(raw)));
375        assert!(set.is_empty());
376    }
377
378    #[test]
379    fn set_query_range() {
380        let mut set = DecorationSet::new();
381        set.add(Decoration::highlight(0, 5, 1).unwrap());
382        set.add(Decoration::highlight(10, 20, 2).unwrap());
383        set.add(Decoration::highlight(30, 40, 3).unwrap());
384
385        let results = set.query_range(3, 15);
386        assert_eq!(results.len(), 2);
387        assert_eq!(results[0].1.tag(), 1);
388        assert_eq!(results[1].1.tag(), 2);
389    }
390
391    #[test]
392    fn set_query_tag() {
393        let mut set = DecorationSet::new();
394        set.add(Decoration::marker(0, 1));
395        set.add(Decoration::marker(10, 2));
396        set.add(Decoration::marker(20, 1));
397
398        let results = set.query_tag(1);
399        assert_eq!(results.len(), 2);
400    }
401
402    #[test]
403    fn set_query_range_includes_marker() {
404        let mut set = DecorationSet::new();
405        set.add(Decoration::marker(10, 1));
406        set.add(Decoration::marker(20, 2));
407
408        let results = set.query_range(10, 15);
409        assert_eq!(results.len(), 1);
410        assert_eq!(results[0].1.tag(), 1);
411
412        let results = set.query_range(5, 10);
413        assert_eq!(results.len(), 0);
414
415        let results = set.query_range(5, 11);
416        assert_eq!(results.len(), 1);
417    }
418
419    #[test]
420    fn map_before_change_unchanged() {
421        let mut set = DecorationSet::new();
422        set.add(Decoration::highlight(0, 5, 1).unwrap());
423        set.map_through_change(10, 15, 20);
424
425        let d = set.iter().next().unwrap().1;
426        assert_eq!(d.start(), 0);
427        assert_eq!(d.end(), 5);
428    }
429
430    #[test]
431    fn map_after_change_shifted() {
432        let mut set = DecorationSet::new();
433        set.add(Decoration::highlight(20, 30, 1).unwrap());
434        // Insertion: [10, 15) -> [10, 20), delta = +5.
435        set.map_through_change(10, 15, 20);
436
437        let d = set.iter().next().unwrap().1;
438        assert_eq!(d.start(), 25);
439        assert_eq!(d.end(), 35);
440    }
441
442    #[test]
443    fn map_highlight_overlap_clamp() {
444        let mut set = DecorationSet::new();
445        // Highlight [5, 15) overlaps change [10, 20) -> [10, 12).
446        set.add(Decoration::highlight(5, 15, 1).unwrap());
447        set.map_through_change(10, 20, 12);
448
449        let d = set.iter().next().unwrap().1;
450        assert_eq!(d.start(), 5);
451        // End is inside the changed range, so clamp it to new_end=12.
452        assert_eq!(d.end(), 12);
453    }
454
455    #[test]
456    fn map_marker_deleted_in_change() {
457        let mut set = DecorationSet::new();
458        set.add(Decoration::marker(12, 1));
459        // Change [10, 20).
460        set.map_through_change(10, 20, 15);
461        assert!(set.is_empty());
462    }
463
464    #[test]
465    fn map_widget_fully_contained_deleted() {
466        let mut set = DecorationSet::new();
467        set.add(Decoration::widget(12, 18, 1, true).unwrap());
468        // Change [10, 20).
469        set.map_through_change(10, 20, 15);
470        assert!(set.is_empty());
471    }
472
473    #[test]
474    fn map_widget_partial_overlap_clamped() {
475        let mut set = DecorationSet::new();
476        // Widget [5, 15) partially overlaps change [10, 20) -> [10, 12).
477        set.add(Decoration::widget(5, 15, 1, false).unwrap());
478        set.map_through_change(10, 20, 12);
479
480        let d = set.iter().next().unwrap().1;
481        assert_eq!(d.start(), 5);
482        assert_eq!(d.end(), 12);
483    }
484
485    #[test]
486    fn map_through_changes_sequential() {
487        let mut set = DecorationSet::new();
488        set.add(Decoration::highlight(20, 30, 1).unwrap());
489
490        let changes = [
491            RangeChange::new(0, 5, 10),   // delta +5 -> [25, 35)
492            RangeChange::new(0, 3, 3),    // delta 0 -> [25, 35)
493            RangeChange::new(40, 40, 42), // delta +2, after decoration, no change
494        ];
495        set.map_through_changes(&changes);
496
497        let d = set.iter().next().unwrap().1;
498        assert_eq!(d.start(), 25);
499        assert_eq!(d.end(), 35);
500    }
501
502    #[test]
503    fn decor_error_display() {
504        let e = DecorError::InvalidRange { start: 10, end: 5 };
505        assert_eq!(e.to_string(), "invalid range: start 10 > end 5");
506
507        let e = DecorError::EmptyHighlight { offset: 7 };
508        assert_eq!(e.to_string(), "empty highlight at offset 7");
509    }
510
511    #[test]
512    fn set_clear() {
513        let mut set = DecorationSet::new();
514        set.add(Decoration::marker(0, 1));
515        set.add(Decoration::marker(10, 2));
516        assert_eq!(set.len(), 2);
517        set.clear();
518        assert!(set.is_empty());
519    }
520}