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}