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