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_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
252 editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
253 editor
254 .remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx);
255 editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx);
256 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(
257 removed_highlighted_ranges.clone(),
258 cx,
259 );
260 editor.remove_blocks(removed_block_ids, None, cx);
261 }
262
263 // Add new highlights and blocks
264 let editor_handle = cx.weak_entity();
265 let new_conflicts = &conflict_set.conflicts[event.new_range.clone()];
266 let mut blocks = Vec::new();
267 for conflict in new_conflicts {
268 let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
269 let precedes_start = range
270 .context
271 .start
272 .cmp(&conflict.range.start, &buffer_snapshot)
273 .is_le();
274 let follows_end = range
275 .context
276 .end
277 .cmp(&conflict.range.start, &buffer_snapshot)
278 .is_ge();
279 precedes_start && follows_end
280 }) else {
281 continue;
282 };
283 let excerpt_id = *excerpt_id;
284
285 update_conflict_highlighting(editor, conflict, &snapshot, excerpt_id, cx);
286
287 let Some(anchor) = snapshot.anchor_in_excerpt(excerpt_id, conflict.range.start) else {
288 continue;
289 };
290
291 let editor_handle = editor_handle.clone();
292 blocks.push(BlockProperties {
293 placement: BlockPlacement::Above(anchor),
294 height: Some(1),
295 style: BlockStyle::Fixed,
296 render: Arc::new({
297 let conflict = conflict.clone();
298 move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
299 }),
300 priority: 0,
301 render_in_minimap: true,
302 })
303 }
304 let new_block_ids = editor.insert_blocks(blocks, None, cx);
305
306 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
307 if let Some((buffer_conflicts, old_range)) =
308 conflict_addon.buffers.get_mut(&buffer_id).zip(old_range)
309 {
310 buffer_conflicts.block_ids.splice(
311 old_range,
312 new_conflicts
313 .iter()
314 .map(|conflict| conflict.range.clone())
315 .zip(new_block_ids),
316 );
317 }
318}
319
320fn update_conflict_highlighting(
321 editor: &mut Editor,
322 conflict: &ConflictRegion,
323 buffer: &editor::MultiBufferSnapshot,
324 excerpt_id: editor::ExcerptId,
325 cx: &mut Context<Editor>,
326) {
327 log::debug!("update conflict highlighting for {conflict:?}");
328 let theme = cx.theme().clone();
329 let colors = theme.colors();
330 let outer_start = buffer
331 .anchor_in_excerpt(excerpt_id, conflict.range.start)
332 .unwrap();
333 let outer_end = buffer
334 .anchor_in_excerpt(excerpt_id, conflict.range.end)
335 .unwrap();
336 let our_start = buffer
337 .anchor_in_excerpt(excerpt_id, conflict.ours.start)
338 .unwrap();
339 let our_end = buffer
340 .anchor_in_excerpt(excerpt_id, conflict.ours.end)
341 .unwrap();
342 let their_start = buffer
343 .anchor_in_excerpt(excerpt_id, conflict.theirs.start)
344 .unwrap();
345 let their_end = buffer
346 .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
347 .unwrap();
348
349 let ours_background = colors.version_control_conflict_ours_background;
350 let ours_marker = colors.version_control_conflict_ours_marker_background;
351 let theirs_background = colors.version_control_conflict_theirs_background;
352 let theirs_marker = colors.version_control_conflict_theirs_marker_background;
353 let divider_background = colors.version_control_conflict_divider_background;
354
355 let options = RowHighlightOptions {
356 include_gutter: false,
357 ..Default::default()
358 };
359
360 // Prevent diff hunk highlighting within the entire conflict region.
361 editor.highlight_rows::<ConflictsOuter>(
362 outer_start..outer_end,
363 divider_background,
364 options,
365 cx,
366 );
367 editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
368 editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
369 editor.highlight_rows::<ConflictsTheirs>(
370 their_start..their_end,
371 theirs_background,
372 options,
373 cx,
374 );
375 editor.highlight_rows::<ConflictsTheirsMarker>(
376 their_end..outer_end,
377 theirs_marker,
378 options,
379 cx,
380 );
381}
382
383fn render_conflict_buttons(
384 conflict: &ConflictRegion,
385 excerpt_id: ExcerptId,
386 editor: WeakEntity<Editor>,
387 cx: &mut BlockContext,
388) -> AnyElement {
389 h_flex()
390 .h(cx.line_height)
391 .items_end()
392 .ml(cx.margins.gutter.width)
393 .id(cx.block_id)
394 .gap_0p5()
395 .child(
396 div()
397 .id("ours")
398 .px_1()
399 .child("Take Ours")
400 .rounded_t(rems(0.2))
401 .text_ui_sm(cx)
402 .hover(|this| this.bg(cx.theme().colors().element_background))
403 .cursor_pointer()
404 .on_click({
405 let editor = editor.clone();
406 let conflict = conflict.clone();
407 let ours = conflict.ours.clone();
408 move |_, window, cx| {
409 resolve_conflict(
410 editor.clone(),
411 excerpt_id,
412 conflict.clone(),
413 vec![ours.clone()],
414 window,
415 cx,
416 )
417 .detach()
418 }
419 }),
420 )
421 .child(
422 div()
423 .id("theirs")
424 .px_1()
425 .child("Take Theirs")
426 .rounded_t(rems(0.2))
427 .text_ui_sm(cx)
428 .hover(|this| this.bg(cx.theme().colors().element_background))
429 .cursor_pointer()
430 .on_click({
431 let editor = editor.clone();
432 let conflict = conflict.clone();
433 let theirs = conflict.theirs.clone();
434 move |_, window, cx| {
435 resolve_conflict(
436 editor.clone(),
437 excerpt_id,
438 conflict.clone(),
439 vec![theirs.clone()],
440 window,
441 cx,
442 )
443 .detach()
444 }
445 }),
446 )
447 .child(
448 div()
449 .id("both")
450 .px_1()
451 .child("Take Both")
452 .rounded_t(rems(0.2))
453 .text_ui_sm(cx)
454 .hover(|this| this.bg(cx.theme().colors().element_background))
455 .cursor_pointer()
456 .on_click({
457 let editor = editor.clone();
458 let conflict = conflict.clone();
459 let ours = conflict.ours.clone();
460 let theirs = conflict.theirs.clone();
461 move |_, window, cx| {
462 resolve_conflict(
463 editor.clone(),
464 excerpt_id,
465 conflict.clone(),
466 vec![ours.clone(), theirs.clone()],
467 window,
468 cx,
469 )
470 .detach()
471 }
472 }),
473 )
474 .into_any()
475}
476
477pub(crate) fn resolve_conflict(
478 editor: WeakEntity<Editor>,
479 excerpt_id: ExcerptId,
480 resolved_conflict: ConflictRegion,
481 ranges: Vec<Range<Anchor>>,
482 window: &mut Window,
483 cx: &mut App,
484) -> Task<()> {
485 window.spawn(cx, async move |cx| {
486 let Some((workspace, project, multibuffer, buffer)) = editor
487 .update(cx, |editor, cx| {
488 let workspace = editor.workspace()?;
489 let project = editor.project.clone()?;
490 let multibuffer = editor.buffer().clone();
491 let buffer_id = resolved_conflict.ours.end.buffer_id?;
492 let buffer = multibuffer.read(cx).buffer(buffer_id)?;
493 resolved_conflict.resolve(buffer.clone(), &ranges, cx);
494 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
495 let snapshot = multibuffer.read(cx).snapshot(cx);
496 let buffer_snapshot = buffer.read(cx).snapshot();
497 let state = conflict_addon
498 .buffers
499 .get_mut(&buffer_snapshot.remote_id())?;
500 let ix = state
501 .block_ids
502 .binary_search_by(|(range, _)| {
503 range
504 .start
505 .cmp(&resolved_conflict.range.start, &buffer_snapshot)
506 })
507 .ok()?;
508 let &(_, block_id) = &state.block_ids[ix];
509 let start = snapshot
510 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
511 .unwrap();
512 let end = snapshot
513 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
514 .unwrap();
515 editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
516 editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
517 editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
518 editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
519 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
520 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
521 Some((workspace, project, multibuffer, buffer))
522 })
523 .ok()
524 .flatten()
525 else {
526 return;
527 };
528 let Some(save) = project
529 .update(cx, |project, cx| {
530 if multibuffer.read(cx).all_diff_hunks_expanded() {
531 project.save_buffer(buffer.clone(), cx)
532 } else {
533 Task::ready(Ok(()))
534 }
535 })
536 .ok()
537 else {
538 return;
539 };
540 if save.await.log_err().is_none() {
541 let open_path = maybe!({
542 let path = buffer
543 .read_with(cx, |buffer, cx| buffer.project_path(cx))
544 .ok()
545 .flatten()?;
546 workspace
547 .update_in(cx, |workspace, window, cx| {
548 workspace.open_path_preview(path, None, false, false, false, window, cx)
549 })
550 .ok()
551 });
552
553 if let Some(open_path) = open_path {
554 open_path.await.log_err();
555 }
556 }
557 })
558}