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