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::{ActiveTheme, Element as _, Styled, Window, prelude::*};
15use util::{ResultExt as _, debug_panic, maybe};
16
17pub(crate) struct ConflictAddon {
18 buffers: HashMap<BufferId, BufferConflicts>,
19}
20
21impl ConflictAddon {
22 pub(crate) fn conflict_set(&self, buffer_id: BufferId) -> Option<Entity<ConflictSet>> {
23 self.buffers
24 .get(&buffer_id)
25 .map(|entry| entry.conflict_set.clone())
26 }
27}
28
29struct BufferConflicts {
30 block_ids: Vec<(Range<Anchor>, CustomBlockId)>,
31 conflict_set: Entity<ConflictSet>,
32 _subscription: Subscription,
33}
34
35impl editor::Addon for ConflictAddon {
36 fn to_any(&self) -> &dyn std::any::Any {
37 self
38 }
39
40 fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
41 Some(self)
42 }
43}
44
45pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) {
46 // Only show conflict UI for singletons and in the project diff.
47 if !editor.mode().is_full()
48 || (!editor.buffer().read(cx).is_singleton()
49 && !editor.buffer().read(cx).all_diff_hunks_expanded())
50 {
51 return;
52 }
53
54 editor.register_addon(ConflictAddon {
55 buffers: Default::default(),
56 });
57
58 let buffers = buffer.read(cx).all_buffers();
59 for buffer in buffers {
60 buffer_added(editor, buffer, cx);
61 }
62
63 cx.subscribe(&cx.entity(), |editor, _, event, cx| match event {
64 EditorEvent::ExcerptsAdded { buffer, .. } => buffer_added(editor, buffer.clone(), cx),
65 EditorEvent::ExcerptsExpanded { ids } => {
66 let multibuffer = editor.buffer().read(cx).snapshot(cx);
67 for excerpt_id in ids {
68 let Some(buffer) = multibuffer.buffer_for_excerpt(*excerpt_id) else {
69 continue;
70 };
71 let addon = editor.addon::<ConflictAddon>().unwrap();
72 let Some(conflict_set) = addon.conflict_set(buffer.remote_id()).clone() else {
73 return;
74 };
75 excerpt_for_buffer_updated(editor, conflict_set, cx);
76 }
77 }
78 EditorEvent::ExcerptsRemoved {
79 removed_buffer_ids, ..
80 } => buffers_removed(editor, removed_buffer_ids, cx),
81 _ => {}
82 })
83 .detach();
84}
85
86fn excerpt_for_buffer_updated(
87 editor: &mut Editor,
88 conflict_set: Entity<ConflictSet>,
89 cx: &mut Context<Editor>,
90) {
91 let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
92 let buffer_id = conflict_set.read(cx).snapshot().buffer_id;
93 let Some(buffer_conflicts) = editor
94 .addon_mut::<ConflictAddon>()
95 .unwrap()
96 .buffers
97 .get(&buffer_id)
98 else {
99 return;
100 };
101 let addon_conflicts_len = buffer_conflicts.block_ids.len();
102 conflicts_updated(
103 editor,
104 conflict_set,
105 &ConflictSetUpdate {
106 buffer_range: None,
107 old_range: 0..addon_conflicts_len,
108 new_range: 0..conflicts_len,
109 },
110 cx,
111 );
112}
113
114fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
115 let Some(project) = editor.project() else {
116 return;
117 };
118 let git_store = project.read(cx).git_store().clone();
119
120 let buffer_conflicts = editor
121 .addon_mut::<ConflictAddon>()
122 .unwrap()
123 .buffers
124 .entry(buffer.read(cx).remote_id())
125 .or_insert_with(|| {
126 let conflict_set = git_store.update(cx, |git_store, cx| {
127 git_store.open_conflict_set(buffer.clone(), cx)
128 });
129 let subscription = cx.subscribe(&conflict_set, conflicts_updated);
130 BufferConflicts {
131 block_ids: Vec::new(),
132 conflict_set,
133 _subscription: subscription,
134 }
135 });
136
137 let conflict_set = buffer_conflicts.conflict_set.clone();
138 let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
139 let addon_conflicts_len = buffer_conflicts.block_ids.len();
140 conflicts_updated(
141 editor,
142 conflict_set,
143 &ConflictSetUpdate {
144 buffer_range: None,
145 old_range: 0..addon_conflicts_len,
146 new_range: 0..conflicts_len,
147 },
148 cx,
149 );
150}
151
152fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
153 let mut removed_block_ids = HashSet::default();
154 editor
155 .addon_mut::<ConflictAddon>()
156 .unwrap()
157 .buffers
158 .retain(|buffer_id, buffer| {
159 if removed_buffer_ids.contains(buffer_id) {
160 removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
161 false
162 } else {
163 true
164 }
165 });
166 editor.remove_blocks(removed_block_ids, None, cx);
167}
168
169fn conflicts_updated(
170 editor: &mut Editor,
171 conflict_set: Entity<ConflictSet>,
172 event: &ConflictSetUpdate,
173 cx: &mut Context<Editor>,
174) {
175 let buffer_id = conflict_set.read(cx).snapshot.buffer_id;
176 let conflict_set = conflict_set.read(cx).snapshot();
177 let multibuffer = editor.buffer().read(cx);
178 let snapshot = multibuffer.snapshot(cx);
179 let excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
180 let Some(buffer_snapshot) = excerpts
181 .first()
182 .and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
183 else {
184 return;
185 };
186
187 let old_range = maybe!({
188 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
189 let buffer_conflicts = conflict_addon.buffers.get(&buffer_id)?;
190 match buffer_conflicts.block_ids.get(event.old_range.clone()) {
191 Some(_) => Some(event.old_range.clone()),
192 None => {
193 debug_panic!(
194 "conflicts updated event old range is invalid for buffer conflicts view (block_ids len is {:?}, old_range is {:?})",
195 buffer_conflicts.block_ids.len(),
196 event.old_range,
197 );
198 if event.old_range.start <= event.old_range.end {
199 Some(
200 event.old_range.start.min(buffer_conflicts.block_ids.len())
201 ..event.old_range.end.min(buffer_conflicts.block_ids.len()),
202 )
203 } else {
204 None
205 }
206 }
207 }
208 });
209
210 // Remove obsolete highlights and blocks
211 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
212 if let Some((buffer_conflicts, old_range)) = conflict_addon
213 .buffers
214 .get_mut(&buffer_id)
215 .zip(old_range.clone())
216 {
217 let old_conflicts = buffer_conflicts.block_ids[old_range].to_owned();
218 let mut removed_highlighted_ranges = Vec::new();
219 let mut removed_block_ids = HashSet::default();
220 for (conflict_range, block_id) in old_conflicts {
221 let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
222 let precedes_start = range
223 .context
224 .start
225 .cmp(&conflict_range.start, buffer_snapshot)
226 .is_le();
227 let follows_end = range
228 .context
229 .end
230 .cmp(&conflict_range.start, buffer_snapshot)
231 .is_ge();
232 precedes_start && follows_end
233 }) else {
234 continue;
235 };
236 let excerpt_id = *excerpt_id;
237 let Some(range) = snapshot
238 .anchor_in_excerpt(excerpt_id, conflict_range.start)
239 .zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
240 .map(|(start, end)| start..end)
241 else {
242 continue;
243 };
244 removed_highlighted_ranges.push(range.clone());
245 removed_block_ids.insert(block_id);
246 }
247
248 editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
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
327 let outer_start = buffer
328 .anchor_in_excerpt(excerpt_id, conflict.range.start)
329 .unwrap();
330 let outer_end = buffer
331 .anchor_in_excerpt(excerpt_id, conflict.range.end)
332 .unwrap();
333 let our_start = buffer
334 .anchor_in_excerpt(excerpt_id, conflict.ours.start)
335 .unwrap();
336 let our_end = buffer
337 .anchor_in_excerpt(excerpt_id, conflict.ours.end)
338 .unwrap();
339 let their_start = buffer
340 .anchor_in_excerpt(excerpt_id, conflict.theirs.start)
341 .unwrap();
342 let their_end = buffer
343 .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
344 .unwrap();
345
346 let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
347 let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
348
349 let options = RowHighlightOptions {
350 include_gutter: true,
351 ..Default::default()
352 };
353
354 editor.insert_gutter_highlight::<ConflictsOuter>(
355 outer_start..their_end,
356 |cx| cx.theme().colors().editor_background,
357 cx,
358 );
359
360 // Prevent diff hunk highlighting within the entire conflict region.
361 editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
362 editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
363 editor.highlight_rows::<ConflictsOursMarker>(
364 outer_start..our_start,
365 ours_background,
366 options,
367 cx,
368 );
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_background,
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 .id(cx.block_id)
391 .h(cx.line_height)
392 .ml(cx.margins.gutter.width)
393 .items_end()
394 .gap_1()
395 .bg(cx.theme().colors().editor_background)
396 .child(
397 Button::new("head", "Use HEAD")
398 .label_size(LabelSize::Small)
399 .on_click({
400 let editor = editor.clone();
401 let conflict = conflict.clone();
402 let ours = conflict.ours.clone();
403 move |_, window, cx| {
404 resolve_conflict(
405 editor.clone(),
406 excerpt_id,
407 conflict.clone(),
408 vec![ours.clone()],
409 window,
410 cx,
411 )
412 .detach()
413 }
414 }),
415 )
416 .child(
417 Button::new("origin", "Use Origin")
418 .label_size(LabelSize::Small)
419 .on_click({
420 let editor = editor.clone();
421 let conflict = conflict.clone();
422 let theirs = conflict.theirs.clone();
423 move |_, window, cx| {
424 resolve_conflict(
425 editor.clone(),
426 excerpt_id,
427 conflict.clone(),
428 vec![theirs.clone()],
429 window,
430 cx,
431 )
432 .detach()
433 }
434 }),
435 )
436 .child(
437 Button::new("both", "Use Both")
438 .label_size(LabelSize::Small)
439 .on_click({
440 let conflict = conflict.clone();
441 let ours = conflict.ours.clone();
442 let theirs = conflict.theirs.clone();
443 move |_, window, cx| {
444 resolve_conflict(
445 editor.clone(),
446 excerpt_id,
447 conflict.clone(),
448 vec![ours.clone(), theirs.clone()],
449 window,
450 cx,
451 )
452 .detach()
453 }
454 }),
455 )
456 .into_any()
457}
458
459pub(crate) fn resolve_conflict(
460 editor: WeakEntity<Editor>,
461 excerpt_id: ExcerptId,
462 resolved_conflict: ConflictRegion,
463 ranges: Vec<Range<Anchor>>,
464 window: &mut Window,
465 cx: &mut App,
466) -> Task<()> {
467 window.spawn(cx, async move |cx| {
468 let Some((workspace, project, multibuffer, buffer)) = editor
469 .update(cx, |editor, cx| {
470 let workspace = editor.workspace()?;
471 let project = editor.project()?.clone();
472 let multibuffer = editor.buffer().clone();
473 let buffer_id = resolved_conflict.ours.end.buffer_id?;
474 let buffer = multibuffer.read(cx).buffer(buffer_id)?;
475 resolved_conflict.resolve(buffer.clone(), &ranges, cx);
476 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
477 let snapshot = multibuffer.read(cx).snapshot(cx);
478 let buffer_snapshot = buffer.read(cx).snapshot();
479 let state = conflict_addon
480 .buffers
481 .get_mut(&buffer_snapshot.remote_id())?;
482 let ix = state
483 .block_ids
484 .binary_search_by(|(range, _)| {
485 range
486 .start
487 .cmp(&resolved_conflict.range.start, &buffer_snapshot)
488 })
489 .ok()?;
490 let &(_, block_id) = &state.block_ids[ix];
491 let start = snapshot
492 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
493 .unwrap();
494 let end = snapshot
495 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
496 .unwrap();
497
498 editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx);
499
500 editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
501 editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
502 editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
503 editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
504 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
505 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
506 Some((workspace, project, multibuffer, buffer))
507 })
508 .ok()
509 .flatten()
510 else {
511 return;
512 };
513 let Some(save) = project
514 .update(cx, |project, cx| {
515 if multibuffer.read(cx).all_diff_hunks_expanded() {
516 project.save_buffer(buffer.clone(), cx)
517 } else {
518 Task::ready(Ok(()))
519 }
520 })
521 .ok()
522 else {
523 return;
524 };
525 if save.await.log_err().is_none() {
526 let open_path = maybe!({
527 let path = buffer
528 .read_with(cx, |buffer, cx| buffer.project_path(cx))
529 .ok()
530 .flatten()?;
531 workspace
532 .update_in(cx, |workspace, window, cx| {
533 workspace.open_path_preview(path, None, false, false, false, window, cx)
534 })
535 .ok()
536 });
537
538 if let Some(open_path) = open_path {
539 open_path.await.log_err();
540 }
541 }
542 })
543}