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 render_in_minimap: true,
301 })
302 }
303 let new_block_ids = editor.insert_blocks(blocks, None, cx);
304
305 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
306 if let Some((buffer_conflicts, old_range)) =
307 conflict_addon.buffers.get_mut(&buffer_id).zip(old_range)
308 {
309 buffer_conflicts.block_ids.splice(
310 old_range,
311 new_conflicts
312 .iter()
313 .map(|conflict| conflict.range.clone())
314 .zip(new_block_ids),
315 );
316 }
317}
318
319fn update_conflict_highlighting(
320 editor: &mut Editor,
321 conflict: &ConflictRegion,
322 buffer: &editor::MultiBufferSnapshot,
323 excerpt_id: editor::ExcerptId,
324 cx: &mut Context<Editor>,
325) {
326 log::debug!("update conflict highlighting for {conflict:?}");
327 let theme = cx.theme().clone();
328 let colors = theme.colors();
329 let outer_start = buffer
330 .anchor_in_excerpt(excerpt_id, conflict.range.start)
331 .unwrap();
332 let outer_end = buffer
333 .anchor_in_excerpt(excerpt_id, conflict.range.end)
334 .unwrap();
335 let our_start = buffer
336 .anchor_in_excerpt(excerpt_id, conflict.ours.start)
337 .unwrap();
338 let our_end = buffer
339 .anchor_in_excerpt(excerpt_id, conflict.ours.end)
340 .unwrap();
341 let their_start = buffer
342 .anchor_in_excerpt(excerpt_id, conflict.theirs.start)
343 .unwrap();
344 let their_end = buffer
345 .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
346 .unwrap();
347
348 let ours_background = colors.version_control_conflict_ours_background;
349 let ours_marker = colors.version_control_conflict_ours_marker_background;
350 let theirs_background = colors.version_control_conflict_theirs_background;
351 let theirs_marker = colors.version_control_conflict_theirs_marker_background;
352 let divider_background = colors.version_control_conflict_divider_background;
353
354 let options = RowHighlightOptions {
355 include_gutter: false,
356 ..Default::default()
357 };
358
359 // Prevent diff hunk highlighting within the entire conflict region.
360 editor.highlight_rows::<ConflictsOuter>(
361 outer_start..outer_end,
362 divider_background,
363 options,
364 cx,
365 );
366 editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
367 editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
368 editor.highlight_rows::<ConflictsTheirs>(
369 their_start..their_end,
370 theirs_background,
371 options,
372 cx,
373 );
374 editor.highlight_rows::<ConflictsTheirsMarker>(
375 their_end..outer_end,
376 theirs_marker,
377 options,
378 cx,
379 );
380}
381
382fn render_conflict_buttons(
383 conflict: &ConflictRegion,
384 excerpt_id: ExcerptId,
385 editor: WeakEntity<Editor>,
386 cx: &mut BlockContext,
387) -> AnyElement {
388 h_flex()
389 .h(cx.line_height)
390 .items_end()
391 .ml(cx.margins.gutter.width)
392 .id(cx.block_id)
393 .gap_0p5()
394 .child(
395 div()
396 .id("ours")
397 .px_1()
398 .child("Take Ours")
399 .rounded_t(rems(0.2))
400 .text_ui_sm(cx)
401 .hover(|this| this.bg(cx.theme().colors().element_background))
402 .cursor_pointer()
403 .on_click({
404 let editor = editor.clone();
405 let conflict = conflict.clone();
406 let ours = conflict.ours.clone();
407 move |_, _, cx| {
408 resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx)
409 }
410 }),
411 )
412 .child(
413 div()
414 .id("theirs")
415 .px_1()
416 .child("Take Theirs")
417 .rounded_t(rems(0.2))
418 .text_ui_sm(cx)
419 .hover(|this| this.bg(cx.theme().colors().element_background))
420 .cursor_pointer()
421 .on_click({
422 let editor = editor.clone();
423 let conflict = conflict.clone();
424 let theirs = conflict.theirs.clone();
425 move |_, _, cx| {
426 resolve_conflict(
427 editor.clone(),
428 excerpt_id,
429 &conflict,
430 &[theirs.clone()],
431 cx,
432 )
433 }
434 }),
435 )
436 .child(
437 div()
438 .id("both")
439 .px_1()
440 .child("Take Both")
441 .rounded_t(rems(0.2))
442 .text_ui_sm(cx)
443 .hover(|this| this.bg(cx.theme().colors().element_background))
444 .cursor_pointer()
445 .on_click({
446 let editor = editor.clone();
447 let conflict = conflict.clone();
448 let ours = conflict.ours.clone();
449 let theirs = conflict.theirs.clone();
450 move |_, _, cx| {
451 resolve_conflict(
452 editor.clone(),
453 excerpt_id,
454 &conflict,
455 &[ours.clone(), theirs.clone()],
456 cx,
457 )
458 }
459 }),
460 )
461 .into_any()
462}
463
464fn resolve_conflict(
465 editor: WeakEntity<Editor>,
466 excerpt_id: ExcerptId,
467 resolved_conflict: &ConflictRegion,
468 ranges: &[Range<Anchor>],
469 cx: &mut App,
470) {
471 let Some(editor) = editor.upgrade() else {
472 return;
473 };
474
475 let multibuffer = editor.read(cx).buffer().read(cx);
476 let snapshot = multibuffer.snapshot(cx);
477 let Some(buffer) = resolved_conflict
478 .ours
479 .end
480 .buffer_id
481 .and_then(|buffer_id| multibuffer.buffer(buffer_id))
482 else {
483 return;
484 };
485 let buffer_snapshot = buffer.read(cx).snapshot();
486
487 resolved_conflict.resolve(buffer, ranges, cx);
488
489 editor.update(cx, |editor, cx| {
490 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
491 let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else {
492 return;
493 };
494 let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| {
495 range
496 .start
497 .cmp(&resolved_conflict.range.start, &buffer_snapshot)
498 }) else {
499 return;
500 };
501 let &(_, block_id) = &state.block_ids[ix];
502 let start = snapshot
503 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
504 .unwrap();
505 let end = snapshot
506 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
507 .unwrap();
508 editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
509 editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
510 editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
511 editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
512 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
513 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
514 })
515}