linked_editing_ranges.rs

  1use collections::HashMap;
  2use gpui::{AppContext, Context, Entity, Window};
  3use itertools::Itertools;
  4use language::Buffer;
  5use std::{ops::Range, sync::Arc, time::Duration};
  6use text::{Anchor, AnchorRangeExt, Bias, BufferId, ToOffset, ToPoint};
  7use util::ResultExt;
  8
  9use crate::Editor;
 10
 11#[derive(Clone, Default)]
 12pub(super) struct LinkedEditingRanges(
 13    /// Ranges are non-overlapping and sorted by .0 (thus, [x + 1].start > [x].end must hold)
 14    pub HashMap<BufferId, Vec<(Range<Anchor>, Vec<Range<Anchor>>)>>,
 15);
 16
 17impl LinkedEditingRanges {
 18    pub(super) fn get(
 19        &self,
 20        id: BufferId,
 21        anchor: Range<Anchor>,
 22        snapshot: &text::BufferSnapshot,
 23    ) -> Option<&(Range<Anchor>, Vec<Range<Anchor>>)> {
 24        let ranges_for_buffer = self.0.get(&id)?;
 25        let lower_bound = ranges_for_buffer
 26            .partition_point(|(range, _)| range.start.cmp(&anchor.start, snapshot).is_le());
 27        if lower_bound == 0 {
 28            // None of the linked ranges contains `anchor`.
 29            return None;
 30        }
 31        ranges_for_buffer
 32            .get(lower_bound - 1)
 33            .filter(|(range, _)| range.end.cmp(&anchor.end, snapshot).is_ge())
 34    }
 35    pub(super) fn is_empty(&self) -> bool {
 36        self.0.is_empty()
 37    }
 38
 39    pub(super) fn clear(&mut self) {
 40        self.0.clear();
 41    }
 42}
 43
 44const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
 45
 46// TODO do not refresh anything at all, if the settings/capabilities do not have it enabled.
 47pub(super) fn refresh_linked_ranges(
 48    editor: &mut Editor,
 49    window: &mut Window,
 50    cx: &mut Context<Editor>,
 51) -> Option<()> {
 52    if !editor.lsp_data_enabled() || editor.pending_rename.is_some() {
 53        return None;
 54    }
 55    let project = editor.project()?.downgrade();
 56
 57    editor.linked_editing_range_task = Some(cx.spawn_in(window, async move |editor, cx| {
 58        cx.background_executor().timer(UPDATE_DEBOUNCE).await;
 59
 60        let mut applicable_selections = Vec::new();
 61        editor
 62            .update(cx, |editor, cx| {
 63                let display_snapshot = editor.display_snapshot(cx);
 64                let selections = editor.selections.all_anchors(&display_snapshot);
 65                let snapshot = display_snapshot.buffer_snapshot();
 66                let buffer = editor.buffer.read(cx);
 67                for selection in selections.iter() {
 68                    if let Some((_, range)) =
 69                        snapshot.anchor_range_to_buffer_anchor_range(selection.range())
 70                        && let Some(buffer) = buffer.buffer(range.start.buffer_id)
 71                    {
 72                        applicable_selections.push((buffer, range.start, range.end));
 73                    }
 74                }
 75            })
 76            .ok()?;
 77
 78        if applicable_selections.is_empty() {
 79            return None;
 80        }
 81
 82        let highlights = project
 83            .update(cx, |project, cx| {
 84                let mut linked_edits_tasks = vec![];
 85                for (buffer, start, end) in &applicable_selections {
 86                    let linked_edits_task = project.linked_edits(buffer, *start, cx);
 87                    let cx = cx.to_async();
 88                    let highlights = async move {
 89                        let edits = linked_edits_task.await.log_err()?;
 90                        let snapshot = cx.read_entity(&buffer, |buffer, _| buffer.snapshot());
 91                        let buffer_id = snapshot.remote_id();
 92
 93                        // Find the range containing our current selection.
 94                        // We might not find one, because the selection contains both the start and end of the contained range
 95                        // (think of selecting <`html>foo`</html> - even though there's a matching closing tag, the selection goes beyond the range of the opening tag)
 96                        // or the language server may not have returned any ranges.
 97
 98                        let start_point = start.to_point(&snapshot);
 99                        let end_point = end.to_point(&snapshot);
100                        let _current_selection_contains_range = edits.iter().find(|range| {
101                            range.start.to_point(&snapshot) <= start_point
102                                && range.end.to_point(&snapshot) >= end_point
103                        });
104                        _current_selection_contains_range?;
105                        // Now link every range as each-others sibling.
106                        let mut siblings: HashMap<Range<Anchor>, Vec<_>> = Default::default();
107                        let mut insert_sorted_anchor =
108                            |key: &Range<Anchor>, value: &Range<Anchor>| {
109                                siblings.entry(key.clone()).or_default().push(value.clone());
110                            };
111                        for items in edits.into_iter().combinations(2) {
112                            let Ok([first, second]): Result<[_; 2], _> = items.try_into() else {
113                                unreachable!()
114                            };
115
116                            insert_sorted_anchor(&first, &second);
117                            insert_sorted_anchor(&second, &first);
118                        }
119                        let mut siblings: Vec<(_, _)> = siblings.into_iter().collect();
120                        siblings.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
121                        Some((buffer_id, siblings))
122                    };
123                    linked_edits_tasks.push(highlights);
124                }
125                linked_edits_tasks
126            })
127            .ok()?;
128
129        let highlights = futures::future::join_all(highlights).await;
130
131        editor
132            .update(cx, |this, cx| {
133                this.linked_edit_ranges.0.clear();
134                if this.pending_rename.is_some() {
135                    return;
136                }
137                for (buffer_id, ranges) in highlights.into_iter().flatten() {
138                    this.linked_edit_ranges
139                        .0
140                        .entry(buffer_id)
141                        .or_default()
142                        .extend(ranges);
143                }
144                for (buffer_id, values) in this.linked_edit_ranges.0.iter_mut() {
145                    let Some(snapshot) = this
146                        .buffer
147                        .read(cx)
148                        .buffer(*buffer_id)
149                        .map(|buffer| buffer.read(cx).snapshot())
150                    else {
151                        continue;
152                    };
153                    values.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0, &snapshot));
154                }
155
156                cx.notify();
157            })
158            .ok()?;
159
160        Some(())
161    }));
162    None
163}
164
165/// Accumulates edits destined for linked editing ranges, for example, matching
166/// HTML/JSX tags, across one or more buffers. Edits are stored as anchor ranges
167/// so they track buffer changes and are only resolved to concrete points at
168/// apply time.
169pub struct LinkedEdits(HashMap<Entity<Buffer>, Vec<(Range<Anchor>, Arc<str>)>>);
170
171impl LinkedEdits {
172    pub fn new() -> Self {
173        Self(HashMap::default())
174    }
175
176    /// Queries the editor's linked editing ranges for the given anchor range and, if any
177    /// are found, records them paired with `text` for later application.
178    pub(crate) fn push(
179        &mut self,
180        editor: &Editor,
181        anchor_range: Range<Anchor>,
182        text: Arc<str>,
183        cx: &gpui::App,
184    ) {
185        if let Some(editing_ranges) = editor.linked_editing_ranges_for(anchor_range, cx) {
186            for (buffer, ranges) in editing_ranges {
187                self.0
188                    .entry(buffer)
189                    .or_default()
190                    .extend(ranges.into_iter().map(|range| (range, text.clone())));
191            }
192        }
193    }
194
195    /// Resolves all stored anchor ranges to points using the current buffer snapshot,
196    /// sorts them, and applies the edits.
197    pub fn apply(self, cx: &mut Context<Editor>) {
198        self.apply_inner(false, cx);
199    }
200
201    /// Like [`apply`](Self::apply), but empty ranges (where start == end) are
202    /// expanded one character to the left before applying. For context, this
203    /// was introduced in order to be available to `backspace` so as to delete a
204    /// character in each linked range even when the selection was a cursor.
205    pub fn apply_with_left_expansion(self, cx: &mut Context<Editor>) {
206        self.apply_inner(true, cx);
207    }
208
209    fn apply_inner(self, expand_empty_ranges_left: bool, cx: &mut Context<Editor>) {
210        for (buffer, ranges_edits) in self.0 {
211            buffer.update(cx, |buffer, cx| {
212                let snapshot = buffer.snapshot();
213                let edits = ranges_edits
214                    .into_iter()
215                    .map(|(range, text)| {
216                        let mut start = range.start.to_point(&snapshot);
217                        let end = range.end.to_point(&snapshot);
218
219                        if expand_empty_ranges_left && start == end {
220                            let offset = range.start.to_offset(&snapshot).saturating_sub(1);
221                            start = snapshot.clip_point(offset.to_point(&snapshot), Bias::Left);
222                        }
223
224                        (start..end, text)
225                    })
226                    .sorted_by_key(|(range, _)| range.start);
227
228                buffer.edit(edits, None, cx);
229            });
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use crate::{editor_tests::init_test, test::editor_test_context::EditorTestContext};
237    use gpui::TestAppContext;
238    use text::Point;
239
240    #[gpui::test]
241    async fn test_linked_edits_push_and_apply(cx: &mut TestAppContext) {
242        init_test(cx, |_| {});
243        let mut cx = EditorTestContext::new(cx).await;
244
245        cx.set_state("<diˇv></div>");
246        cx.update_editor(|editor, _window, cx| {
247            editor
248                .set_linked_edit_ranges_for_testing(
249                    vec![(
250                        Point::new(0, 1)..Point::new(0, 4),
251                        vec![Point::new(0, 7)..Point::new(0, 10)],
252                    )],
253                    cx,
254                )
255                .unwrap();
256        });
257
258        cx.simulate_keystroke("x");
259        cx.assert_editor_state("<dixˇv></dixv>");
260    }
261
262    #[gpui::test]
263    async fn test_linked_edits_backspace(cx: &mut TestAppContext) {
264        init_test(cx, |_| {});
265        let mut cx = EditorTestContext::new(cx).await;
266
267        cx.set_state("<divˇ></div>");
268        cx.update_editor(|editor, _window, cx| {
269            editor
270                .set_linked_edit_ranges_for_testing(
271                    vec![(
272                        Point::new(0, 1)..Point::new(0, 4),
273                        vec![Point::new(0, 7)..Point::new(0, 10)],
274                    )],
275                    cx,
276                )
277                .unwrap();
278        });
279
280        cx.update_editor(|editor, window, cx| {
281            editor.backspace(&Default::default(), window, cx);
282        });
283        cx.assert_editor_state("<diˇ></di>");
284    }
285
286    #[gpui::test]
287    async fn test_linked_edits_delete(cx: &mut TestAppContext) {
288        init_test(cx, |_| {});
289        let mut cx = EditorTestContext::new(cx).await;
290
291        cx.set_state("<ˇdiv></div>");
292        cx.update_editor(|editor, _window, cx| {
293            editor
294                .set_linked_edit_ranges_for_testing(
295                    vec![(
296                        Point::new(0, 1)..Point::new(0, 4),
297                        vec![Point::new(0, 7)..Point::new(0, 10)],
298                    )],
299                    cx,
300                )
301                .unwrap();
302        });
303
304        cx.update_editor(|editor, window, cx| {
305            editor.delete(&Default::default(), window, cx);
306        });
307        cx.assert_editor_state("<ˇiv></iv>");
308    }
309
310    #[gpui::test]
311    async fn test_linked_edits_selection(cx: &mut TestAppContext) {
312        init_test(cx, |_| {});
313        let mut cx = EditorTestContext::new(cx).await;
314
315        cx.set_state("<«divˇ»></div>");
316        cx.update_editor(|editor, _window, cx| {
317            editor
318                .set_linked_edit_ranges_for_testing(
319                    vec![(
320                        Point::new(0, 1)..Point::new(0, 4),
321                        vec![Point::new(0, 7)..Point::new(0, 10)],
322                    )],
323                    cx,
324                )
325                .unwrap();
326        });
327
328        cx.simulate_keystrokes("s p a n");
329        cx.assert_editor_state("<spanˇ></span>");
330    }
331}