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#[derive(Clone, Debug, PartialEq)]
15pub struct TextDecoration {
16 pub range: Range<usize>,
17 pub style: HighlightStyle,
18}
19
20impl TextDecoration {
21 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#[derive(Clone, Debug)]
35pub struct TextDecorationCollection {
36 state: WeakEntity<InputBaseState<EditorMode>>,
37 id: TextDecorationCollectionId,
38}
39
40impl TextDecorationCollection {
41 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 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 pub fn clear(&self, cx: &mut App) {
72 self.set(Vec::new(), cx);
73 }
74
75 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 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}