1use collections::{HashMap, HashSet};
2use editor::{
3 ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker,
4 Editor, EditorEvent, ExcerptId, MultiBuffer, RowHighlightOptions,
5 display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
6};
7use gpui::{
8 App, Context, Entity, InteractiveElement as _, ParentElement as _, Subscription, WeakEntity,
9};
10use language::{Anchor, Buffer, BufferId};
11use project::{ConflictRegion, ConflictSet, ConflictSetUpdate};
12use std::{ops::Range, sync::Arc};
13use ui::{
14 ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
15 StyledTypography as _, div, h_flex, rems,
16};
17use util::{debug_panic, maybe};
18
19pub(crate) struct ConflictAddon {
20 buffers: HashMap<BufferId, BufferConflicts>,
21}
22
23impl ConflictAddon {
24 pub(crate) fn conflict_set(&self, buffer_id: BufferId) -> Option<Entity<ConflictSet>> {
25 self.buffers
26 .get(&buffer_id)
27 .map(|entry| entry.conflict_set.clone())
28 }
29}
30
31struct BufferConflicts {
32 block_ids: Vec<(Range<Anchor>, CustomBlockId)>,
33 conflict_set: Entity<ConflictSet>,
34 _subscription: Subscription,
35}
36
37impl editor::Addon for ConflictAddon {
38 fn to_any(&self) -> &dyn std::any::Any {
39 self
40 }
41
42 fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
43 Some(self)
44 }
45}
46
47pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) {
48 // Only show conflict UI for singletons and in the project diff.
49 if !editor.mode().is_full()
50 || (!editor.buffer().read(cx).is_singleton()
51 && !editor.buffer().read(cx).all_diff_hunks_expanded())
52 {
53 return;
54 }
55
56 editor.register_addon(ConflictAddon {
57 buffers: Default::default(),
58 });
59
60 let buffers = buffer.read(cx).all_buffers().clone();
61 for buffer in buffers {
62 buffer_added(editor, buffer, cx);
63 }
64
65 cx.subscribe(&cx.entity(), |editor, _, event, cx| match event {
66 EditorEvent::ExcerptsAdded { buffer, .. } => buffer_added(editor, buffer.clone(), cx),
67 EditorEvent::ExcerptsExpanded { ids } => {
68 let multibuffer = editor.buffer().read(cx).snapshot(cx);
69 for excerpt_id in ids {
70 let Some(buffer) = multibuffer.buffer_for_excerpt(*excerpt_id) else {
71 continue;
72 };
73 let addon = editor.addon::<ConflictAddon>().unwrap();
74 let Some(conflict_set) = addon.conflict_set(buffer.remote_id()).clone() else {
75 return;
76 };
77 excerpt_for_buffer_updated(editor, conflict_set, cx);
78 }
79 }
80 EditorEvent::ExcerptsRemoved {
81 removed_buffer_ids, ..
82 } => buffers_removed(editor, removed_buffer_ids, cx),
83 _ => {}
84 })
85 .detach();
86}
87
88fn excerpt_for_buffer_updated(
89 editor: &mut Editor,
90 conflict_set: Entity<ConflictSet>,
91 cx: &mut Context<Editor>,
92) {
93 let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
94 let buffer_id = conflict_set.read(cx).snapshot().buffer_id;
95 let Some(buffer_conflicts) = editor
96 .addon_mut::<ConflictAddon>()
97 .unwrap()
98 .buffers
99 .get(&buffer_id)
100 else {
101 return;
102 };
103 let addon_conflicts_len = buffer_conflicts.block_ids.len();
104 conflicts_updated(
105 editor,
106 conflict_set,
107 &ConflictSetUpdate {
108 buffer_range: None,
109 old_range: 0..addon_conflicts_len,
110 new_range: 0..conflicts_len,
111 },
112 cx,
113 );
114}
115
116fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
117 let Some(project) = &editor.project else {
118 return;
119 };
120 let git_store = project.read(cx).git_store().clone();
121
122 let buffer_conflicts = editor
123 .addon_mut::<ConflictAddon>()
124 .unwrap()
125 .buffers
126 .entry(buffer.read(cx).remote_id())
127 .or_insert_with(|| {
128 let conflict_set = git_store.update(cx, |git_store, cx| {
129 git_store.open_conflict_set(buffer.clone(), cx)
130 });
131 let subscription = cx.subscribe(&conflict_set, conflicts_updated);
132 BufferConflicts {
133 block_ids: Vec::new(),
134 conflict_set: conflict_set.clone(),
135 _subscription: subscription,
136 }
137 });
138
139 let conflict_set = buffer_conflicts.conflict_set.clone();
140 let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
141 let addon_conflicts_len = buffer_conflicts.block_ids.len();
142 conflicts_updated(
143 editor,
144 conflict_set,
145 &ConflictSetUpdate {
146 buffer_range: None,
147 old_range: 0..addon_conflicts_len,
148 new_range: 0..conflicts_len,
149 },
150 cx,
151 );
152}
153
154fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
155 let mut removed_block_ids = HashSet::default();
156 editor
157 .addon_mut::<ConflictAddon>()
158 .unwrap()
159 .buffers
160 .retain(|buffer_id, buffer| {
161 if removed_buffer_ids.contains(&buffer_id) {
162 removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
163 false
164 } else {
165 true
166 }
167 });
168 editor.remove_blocks(removed_block_ids, None, cx);
169}
170
171fn conflicts_updated(
172 editor: &mut Editor,
173 conflict_set: Entity<ConflictSet>,
174 event: &ConflictSetUpdate,
175 cx: &mut Context<Editor>,
176) {
177 let buffer_id = conflict_set.read(cx).snapshot.buffer_id;
178 let conflict_set = conflict_set.read(cx).snapshot();
179 let multibuffer = editor.buffer().read(cx);
180 let snapshot = multibuffer.snapshot(cx);
181 let excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
182 let Some(buffer_snapshot) = excerpts
183 .first()
184 .and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
185 else {
186 return;
187 };
188
189 let old_range = maybe!({
190 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
191 let buffer_conflicts = conflict_addon.buffers.get(&buffer_id)?;
192 match buffer_conflicts.block_ids.get(event.old_range.clone()) {
193 Some(_) => Some(event.old_range.clone()),
194 None => {
195 debug_panic!(
196 "conflicts updated event old range is invalid for buffer conflicts view (block_ids len is {:?}, old_range is {:?})",
197 buffer_conflicts.block_ids.len(),
198 event.old_range,
199 );
200 if event.old_range.start <= event.old_range.end {
201 Some(
202 event.old_range.start.min(buffer_conflicts.block_ids.len())
203 ..event.old_range.end.min(buffer_conflicts.block_ids.len()),
204 )
205 } else {
206 None
207 }
208 }
209 }
210 });
211
212 // Remove obsolete highlights and blocks
213 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
214 if let Some((buffer_conflicts, old_range)) = conflict_addon
215 .buffers
216 .get_mut(&buffer_id)
217 .zip(old_range.clone())
218 {
219 let old_conflicts = buffer_conflicts.block_ids[old_range].to_owned();
220 let mut removed_highlighted_ranges = Vec::new();
221 let mut removed_block_ids = HashSet::default();
222 for (conflict_range, block_id) in old_conflicts {
223 let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
224 let precedes_start = range
225 .context
226 .start
227 .cmp(&conflict_range.start, &buffer_snapshot)
228 .is_le();
229 let follows_end = range
230 .context
231 .end
232 .cmp(&conflict_range.start, &buffer_snapshot)
233 .is_ge();
234 precedes_start && follows_end
235 }) else {
236 continue;
237 };
238 let excerpt_id = *excerpt_id;
239 let Some(range) = snapshot
240 .anchor_in_excerpt(excerpt_id, conflict_range.start)
241 .zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
242 .map(|(start, end)| start..end)
243 else {
244 continue;
245 };
246 removed_highlighted_ranges.push(range.clone());
247 removed_block_ids.insert(block_id);
248 }
249
250 editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
251 editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
252 editor
253 .remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx);
254 editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx);
255 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(
256 removed_highlighted_ranges.clone(),
257 cx,
258 );
259 editor.remove_blocks(removed_block_ids, None, cx);
260 }
261
262 // Add new highlights and blocks
263 let editor_handle = cx.weak_entity();
264 let new_conflicts = &conflict_set.conflicts[event.new_range.clone()];
265 let mut blocks = Vec::new();
266 for conflict in new_conflicts {
267 let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
268 let precedes_start = range
269 .context
270 .start
271 .cmp(&conflict.range.start, &buffer_snapshot)
272 .is_le();
273 let follows_end = range
274 .context
275 .end
276 .cmp(&conflict.range.start, &buffer_snapshot)
277 .is_ge();
278 precedes_start && follows_end
279 }) else {
280 continue;
281 };
282 let excerpt_id = *excerpt_id;
283
284 update_conflict_highlighting(editor, conflict, &snapshot, excerpt_id, cx);
285
286 let Some(anchor) = snapshot.anchor_in_excerpt(excerpt_id, conflict.range.start) else {
287 continue;
288 };
289
290 let editor_handle = editor_handle.clone();
291 blocks.push(BlockProperties {
292 placement: BlockPlacement::Above(anchor),
293 height: Some(1),
294 style: BlockStyle::Fixed,
295 render: Arc::new({
296 let conflict = conflict.clone();
297 move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
298 }),
299 priority: 0,
300 })
301 }
302 let new_block_ids = editor.insert_blocks(blocks, None, cx);
303
304 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
305 if let Some((buffer_conflicts, old_range)) =
306 conflict_addon.buffers.get_mut(&buffer_id).zip(old_range)
307 {
308 buffer_conflicts.block_ids.splice(
309 old_range,
310 new_conflicts
311 .iter()
312 .map(|conflict| conflict.range.clone())
313 .zip(new_block_ids),
314 );
315 }
316}
317
318fn update_conflict_highlighting(
319 editor: &mut Editor,
320 conflict: &ConflictRegion,
321 buffer: &editor::MultiBufferSnapshot,
322 excerpt_id: editor::ExcerptId,
323 cx: &mut Context<Editor>,
324) {
325 log::debug!("update conflict highlighting for {conflict:?}");
326 let theme = cx.theme().clone();
327 let colors = theme.colors();
328 let outer_start = buffer
329 .anchor_in_excerpt(excerpt_id, conflict.range.start)
330 .unwrap();
331 let outer_end = buffer
332 .anchor_in_excerpt(excerpt_id, conflict.range.end)
333 .unwrap();
334 let our_start = buffer
335 .anchor_in_excerpt(excerpt_id, conflict.ours.start)
336 .unwrap();
337 let our_end = buffer
338 .anchor_in_excerpt(excerpt_id, conflict.ours.end)
339 .unwrap();
340 let their_start = buffer
341 .anchor_in_excerpt(excerpt_id, conflict.theirs.start)
342 .unwrap();
343 let their_end = buffer
344 .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
345 .unwrap();
346
347 let ours_background = colors.version_control_conflict_ours_background;
348 let ours_marker = colors.version_control_conflict_ours_marker_background;
349 let theirs_background = colors.version_control_conflict_theirs_background;
350 let theirs_marker = colors.version_control_conflict_theirs_marker_background;
351 let divider_background = colors.version_control_conflict_divider_background;
352
353 let options = RowHighlightOptions {
354 include_gutter: false,
355 ..Default::default()
356 };
357
358 // Prevent diff hunk highlighting within the entire conflict region.
359 editor.highlight_rows::<ConflictsOuter>(
360 outer_start..outer_end,
361 divider_background,
362 options,
363 cx,
364 );
365 editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
366 editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
367 editor.highlight_rows::<ConflictsTheirs>(
368 their_start..their_end,
369 theirs_background,
370 options,
371 cx,
372 );
373 editor.highlight_rows::<ConflictsTheirsMarker>(
374 their_end..outer_end,
375 theirs_marker,
376 options,
377 cx,
378 );
379}
380
381fn render_conflict_buttons(
382 conflict: &ConflictRegion,
383 excerpt_id: ExcerptId,
384 editor: WeakEntity<Editor>,
385 cx: &mut BlockContext,
386) -> AnyElement {
387 h_flex()
388 .h(cx.line_height)
389 .items_end()
390 .ml(cx.gutter_dimensions.width)
391 .id(cx.block_id)
392 .gap_0p5()
393 .child(
394 div()
395 .id("ours")
396 .px_1()
397 .child("Take Ours")
398 .rounded_t(rems(0.2))
399 .text_ui_sm(cx)
400 .hover(|this| this.bg(cx.theme().colors().element_background))
401 .cursor_pointer()
402 .on_click({
403 let editor = editor.clone();
404 let conflict = conflict.clone();
405 let ours = conflict.ours.clone();
406 move |_, _, cx| {
407 resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx)
408 }
409 }),
410 )
411 .child(
412 div()
413 .id("theirs")
414 .px_1()
415 .child("Take Theirs")
416 .rounded_t(rems(0.2))
417 .text_ui_sm(cx)
418 .hover(|this| this.bg(cx.theme().colors().element_background))
419 .cursor_pointer()
420 .on_click({
421 let editor = editor.clone();
422 let conflict = conflict.clone();
423 let theirs = conflict.theirs.clone();
424 move |_, _, cx| {
425 resolve_conflict(
426 editor.clone(),
427 excerpt_id,
428 &conflict,
429 &[theirs.clone()],
430 cx,
431 )
432 }
433 }),
434 )
435 .child(
436 div()
437 .id("both")
438 .px_1()
439 .child("Take Both")
440 .rounded_t(rems(0.2))
441 .text_ui_sm(cx)
442 .hover(|this| this.bg(cx.theme().colors().element_background))
443 .cursor_pointer()
444 .on_click({
445 let editor = editor.clone();
446 let conflict = conflict.clone();
447 let ours = conflict.ours.clone();
448 let theirs = conflict.theirs.clone();
449 move |_, _, cx| {
450 resolve_conflict(
451 editor.clone(),
452 excerpt_id,
453 &conflict,
454 &[ours.clone(), theirs.clone()],
455 cx,
456 )
457 }
458 }),
459 )
460 .into_any()
461}
462
463fn resolve_conflict(
464 editor: WeakEntity<Editor>,
465 excerpt_id: ExcerptId,
466 resolved_conflict: &ConflictRegion,
467 ranges: &[Range<Anchor>],
468 cx: &mut App,
469) {
470 let Some(editor) = editor.upgrade() else {
471 return;
472 };
473
474 let multibuffer = editor.read(cx).buffer().read(cx);
475 let snapshot = multibuffer.snapshot(cx);
476 let Some(buffer) = resolved_conflict
477 .ours
478 .end
479 .buffer_id
480 .and_then(|buffer_id| multibuffer.buffer(buffer_id))
481 else {
482 return;
483 };
484 let buffer_snapshot = buffer.read(cx).snapshot();
485
486 resolved_conflict.resolve(buffer, ranges, cx);
487
488 editor.update(cx, |editor, cx| {
489 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
490 let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else {
491 return;
492 };
493 let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| {
494 range
495 .start
496 .cmp(&resolved_conflict.range.start, &buffer_snapshot)
497 }) else {
498 return;
499 };
500 let &(_, block_id) = &state.block_ids[ix];
501 let start = snapshot
502 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
503 .unwrap();
504 let end = snapshot
505 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
506 .unwrap();
507 editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
508 editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
509 editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
510 editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
511 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
512 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
513 })
514}