Skip to main content

gpui_base/input/editor/
decorations.rs

1use crate::input::EditorMode;
2use std::ops::Range;
3
4use gpui::{App, Context, HighlightStyle, WeakEntity};
5use ropey::Rope;
6use sum_tree::Bias;
7
8use super::{InputBaseState, RopeExt as _};
9
10/// A presentation style applied to a UTF-8 byte range in an input.
11///
12/// This is the GPUI [`HighlightStyle`] counterpart of Monaco's
13/// [`IModelDeltaDecoration`](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor_editor_api.editor.IModelDeltaDecoration.html).
14#[derive(Clone, Debug, PartialEq)]
15pub struct TextDecoration {
16    pub range: Range<usize>,
17    pub style: HighlightStyle,
18}
19
20impl TextDecoration {
21    /// Create a text decoration from a UTF-8 byte range and a GPUI style.
22    pub fn new(range: Range<usize>, style: HighlightStyle) -> Self {
23        Self { range, style }
24    }
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28struct TextDecorationCollectionId(usize);
29
30/// An independently managed collection of [`TextDecoration`]s.
31///
32/// This is the GPUI Component counterpart of Monaco's
33/// [`IEditorDecorationsCollection`](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor_editor_api.editor.IEditorDecorationsCollection.html).
34#[derive(Clone, Debug)]
35pub struct TextDecorationCollection {
36    state: WeakEntity<InputBaseState<EditorMode>>,
37    id: TextDecorationCollectionId,
38}
39
40impl TextDecorationCollection {
41    /// Replace all decorations in this collection.
42    ///
43    /// This corresponds to Monaco's
44    /// [`IEditorDecorationsCollection.set`](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor_editor_api.editor.IEditorDecorationsCollection.html#set).
45    pub fn set(&self, decorations: Vec<TextDecoration>, cx: &mut App) {
46        let _ = self.state.update(cx, |state, cx| {
47            let decorations = normalize(&state.text, decorations);
48            if state.extras.decorations.set(self.id, decorations) {
49                cx.notify();
50            }
51        });
52    }
53
54    /// Add decorations to this collection.
55    ///
56    /// This corresponds to Monaco's
57    /// [`IEditorDecorationsCollection.append`](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor_editor_api.editor.IEditorDecorationsCollection.html#append).
58    pub fn append(&self, decorations: Vec<TextDecoration>, cx: &mut App) {
59        let _ = self.state.update(cx, |state, cx| {
60            let decorations = normalize(&state.text, decorations);
61            if state.extras.decorations.append(self.id, decorations) {
62                cx.notify();
63            }
64        });
65    }
66
67    /// Remove all decorations from this collection.
68    ///
69    /// This corresponds to Monaco's
70    /// [`IEditorDecorationsCollection.clear`](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor_editor_api.editor.IEditorDecorationsCollection.html#clear).
71    pub fn clear(&self, cx: &mut App) {
72        self.set(Vec::new(), cx);
73    }
74
75    /// Return the UTF-8 byte ranges in this collection.
76    ///
77    /// This corresponds to Monaco's
78    /// [`IEditorDecorationsCollection.getRanges`](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor_editor_api.editor.IEditorDecorationsCollection.html#getRanges).
79    pub fn get_ranges(&self, cx: &App) -> Vec<Range<usize>> {
80        self.state
81            .read_with(cx, |state, _| {
82                state
83                    .extras
84                    .decorations
85                    .get(self.id)
86                    .unwrap_or_default()
87                    .iter()
88                    .map(|decoration| decoration.range.clone())
89                    .collect()
90            })
91            .unwrap_or_default()
92    }
93}
94
95#[derive(Default)]
96pub(crate) struct DecorationCollections {
97    entries: Vec<(TextDecorationCollectionId, Vec<TextDecoration>)>,
98}
99
100impl DecorationCollections {
101    fn create(&mut self, decorations: Vec<TextDecoration>) -> TextDecorationCollectionId {
102        let id = TextDecorationCollectionId(self.entries.len());
103        self.entries.push((id, decorations));
104        id
105    }
106
107    fn set(&mut self, id: TextDecorationCollectionId, decorations: Vec<TextDecoration>) -> bool {
108        let Some((_, current)) = self
109            .entries
110            .iter_mut()
111            .find(|(entry_id, _)| *entry_id == id)
112        else {
113            return false;
114        };
115        *current = decorations;
116        true
117    }
118
119    fn append(&mut self, id: TextDecorationCollectionId, decorations: Vec<TextDecoration>) -> bool {
120        let Some((_, current)) = self
121            .entries
122            .iter_mut()
123            .find(|(entry_id, _)| *entry_id == id)
124        else {
125            return false;
126        };
127        current.extend(decorations);
128        true
129    }
130
131    fn get(&self, id: TextDecorationCollectionId) -> Option<&[TextDecoration]> {
132        self.entries
133            .iter()
134            .find(|(entry_id, _)| *entry_id == id)
135            .map(|(_, decorations)| decorations.as_slice())
136    }
137
138    pub(super) fn adjust_for_edit(&mut self, edited_range: &Range<usize>, inserted_len: usize) {
139        for (_, decorations) in &mut self.entries {
140            decorations.retain_mut(|decoration| {
141                decoration.range =
142                    adjust_range_for_edit(&decoration.range, edited_range, inserted_len);
143                !decoration.range.is_empty()
144            });
145        }
146    }
147
148    pub(super) fn clear(&mut self) {
149        for (_, decorations) in &mut self.entries {
150            decorations.clear();
151        }
152    }
153
154    pub(super) fn iter(&self) -> impl Iterator<Item = &[TextDecoration]> {
155        self.entries
156            .iter()
157            .map(|(_, decorations)| decorations.as_slice())
158    }
159}
160
161fn adjust_range_for_edit(
162    range: &Range<usize>,
163    edited_range: &Range<usize>,
164    inserted_len: usize,
165) -> Range<usize> {
166    let removed_len = edited_range.end.saturating_sub(edited_range.start);
167    let shift = |offset: usize| {
168        if inserted_len >= removed_len {
169            offset.saturating_add(inserted_len - removed_len)
170        } else {
171            offset.saturating_sub(removed_len - inserted_len)
172        }
173    };
174
175    if edited_range.is_empty() {
176        let start = if range.start < edited_range.start {
177            range.start
178        } else {
179            shift(range.start)
180        };
181        let end = if range.end <= edited_range.start {
182            range.end
183        } else {
184            shift(range.end)
185        };
186        return start..end;
187    }
188
189    let inserted_end = edited_range.start + inserted_len;
190    let start = if range.start <= edited_range.start {
191        range.start
192    } else if range.start >= edited_range.end {
193        shift(range.start)
194    } else {
195        edited_range.start
196    };
197    let end = if range.end <= edited_range.start {
198        range.end
199    } else if range.end >= edited_range.end {
200        shift(range.end)
201    } else {
202        inserted_end
203    };
204    start..end
205}
206
207fn normalize(text: &Rope, decorations: Vec<TextDecoration>) -> Vec<TextDecoration> {
208    decorations
209        .into_iter()
210        .filter_map(|decoration| {
211            let range = text.clip_offset(decoration.range.start, Bias::Left)
212                ..text.clip_offset(decoration.range.end, Bias::Right);
213            (!range.is_empty()).then_some(TextDecoration {
214                range,
215                style: decoration.style,
216            })
217        })
218        .collect()
219}
220
221impl InputBaseState<EditorMode> {
222    /// Create an independently managed collection of text decorations.
223    ///
224    /// This follows Monaco's
225    /// [`createDecorationsCollection`](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor_editor_api.editor.ICodeEditor.html#createDecorationsCollection)
226    /// ownership model. Ranges use UTF-8 byte offsets into [`Self::value`].
227    ///
228    /// Decoration ranges follow text edits and do not need to be set again
229    /// after each change. Insertions at a range boundary do not expand the
230    /// range, matching Monaco's
231    /// [`NeverGrowsWhenTypingAtEdges`](https://microsoft.github.io/monaco-editor/typedoc/enums/editor_editor_api.editor.TrackedRangeStickiness.html#NeverGrowsWhenTypingAtEdges)
232    /// behavior. Decorations are not rendered while the input is masked.
233    /// Collections live until their [`InputBaseState`] is dropped.
234    ///
235    /// Collections are layered in insertion order; the first collection wins
236    /// when overlapping decorations set the same [`HighlightStyle`] property.
237    /// Callers should avoid conflicting overlaps within one collection.
238    pub fn create_decorations_collection(
239        &mut self,
240        decorations: Vec<TextDecoration>,
241        cx: &mut Context<Self>,
242    ) -> TextDecorationCollection {
243        let decorations = normalize(&self.text, decorations);
244        let id = self.extras.decorations.create(decorations);
245        cx.notify();
246        TextDecorationCollection {
247            state: cx.entity().downgrade(),
248            id,
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn collections_are_independent_and_ranges_are_clipped() {
259        let text = Rope::from("héllo");
260        let first_style = HighlightStyle {
261            font_weight: Some(gpui::FontWeight::BOLD),
262            ..Default::default()
263        };
264        let second_style = HighlightStyle {
265            background_color: Some(gpui::red()),
266            ..Default::default()
267        };
268        let mut collections = DecorationCollections::default();
269
270        let first = collections.create(normalize(
271            &text,
272            vec![TextDecoration::new(2..4, first_style)],
273        ));
274        let second = collections.create(normalize(
275            &text,
276            vec![TextDecoration::new(5..100, second_style)],
277        ));
278
279        assert_ne!(first, second);
280        assert_eq!(
281            collections.get(first),
282            Some(&[TextDecoration::new(1..4, first_style)][..])
283        );
284        assert_eq!(
285            collections.get(second),
286            Some(&[TextDecoration::new(5..6, second_style)][..])
287        );
288
289        assert!(collections.append(first, vec![TextDecoration::new(4..5, second_style)]));
290        assert_eq!(
291            collections.get(first),
292            Some(
293                &[
294                    TextDecoration::new(1..4, first_style),
295                    TextDecoration::new(4..5, second_style),
296                ][..]
297            )
298        );
299
300        assert!(collections.set(first, Vec::new()));
301        assert_eq!(collections.get(first), Some(&[][..]));
302        assert_eq!(
303            collections.get(second),
304            Some(&[TextDecoration::new(5..6, second_style)][..])
305        );
306    }
307
308    #[test]
309    fn decoration_ranges_follow_text_edits() {
310        let style = HighlightStyle::default();
311        let mut collections = DecorationCollections::default();
312        let collection = collections.create(vec![TextDecoration::new(2..6, style)]);
313
314        collections.adjust_for_edit(&(0..0), 2);
315        assert_eq!(
316            collections.get(collection),
317            Some(&[TextDecoration::new(4..8, style)][..])
318        );
319
320        collections.adjust_for_edit(&(6..6), 2);
321        assert_eq!(
322            collections.get(collection),
323            Some(&[TextDecoration::new(4..10, style)][..])
324        );
325
326        collections.adjust_for_edit(&(4..10), 3);
327        assert_eq!(
328            collections.get(collection),
329            Some(&[TextDecoration::new(4..7, style)][..])
330        );
331
332        assert_eq!(adjust_range_for_edit(&(2..6), &(2..2), 2), 4..8);
333        assert_eq!(adjust_range_for_edit(&(2..6), &(6..6), 2), 2..6);
334        assert_eq!(adjust_range_for_edit(&(2..6), &(2..6), 3), 2..5);
335    }
336}