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
114#[ztracing::instrument(skip_all)]
115fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
116 let Some(project) = editor.project() else {
117 return;
118 };
119 let git_store = project.read(cx).git_store().clone();
120
121 let buffer_conflicts = editor
122 .addon_mut::<ConflictAddon>()
123 .unwrap()
124 .buffers
125 .entry(buffer.read(cx).remote_id())
126 .or_insert_with(|| {
127 let conflict_set = git_store.update(cx, |git_store, cx| {
128 git_store.open_conflict_set(buffer.clone(), cx)
129 });
130 let subscription = cx.subscribe(&conflict_set, conflicts_updated);
131 BufferConflicts {
132 block_ids: Vec::new(),
133 conflict_set,
134 _subscription: subscription,
135 }
136 });
137
138 let conflict_set = buffer_conflicts.conflict_set.clone();
139 let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
140 let addon_conflicts_len = buffer_conflicts.block_ids.len();
141 conflicts_updated(
142 editor,
143 conflict_set,
144 &ConflictSetUpdate {
145 buffer_range: None,
146 old_range: 0..addon_conflicts_len,
147 new_range: 0..conflicts_len,
148 },
149 cx,
150 );
151}
152
153fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
154 let mut removed_block_ids = HashSet::default();
155 editor
156 .addon_mut::<ConflictAddon>()
157 .unwrap()
158 .buffers
159 .retain(|buffer_id, buffer| {
160 if removed_buffer_ids.contains(buffer_id) {
161 removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
162 false
163 } else {
164 true
165 }
166 });
167 editor.remove_blocks(removed_block_ids, None, cx);
168}
169
170#[ztracing::instrument(skip_all)]
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.anchor_range_in_excerpt(excerpt_id, conflict_range) else {
240 continue;
241 };
242 removed_highlighted_ranges.push(range.clone());
243 removed_block_ids.insert(block_id);
244 }
245
246 editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
247
248 editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
249 editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
250 editor
251 .remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx);
252 editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx);
253 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(
254 removed_highlighted_ranges.clone(),
255 cx,
256 );
257 editor.remove_blocks(removed_block_ids, None, cx);
258 }
259
260 // Add new highlights and blocks
261 let editor_handle = cx.weak_entity();
262 let new_conflicts = &conflict_set.conflicts[event.new_range.clone()];
263 let mut blocks = Vec::new();
264 for conflict in new_conflicts {
265 let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
266 let precedes_start = range
267 .context
268 .start
269 .cmp(&conflict.range.start, buffer_snapshot)
270 .is_le();
271 let follows_end = range
272 .context
273 .end
274 .cmp(&conflict.range.start, buffer_snapshot)
275 .is_ge();
276 precedes_start && follows_end
277 }) else {
278 continue;
279 };
280 let excerpt_id = *excerpt_id;
281
282 update_conflict_highlighting(editor, conflict, &snapshot, excerpt_id, cx);
283
284 let Some(anchor) = snapshot.anchor_in_excerpt(excerpt_id, conflict.range.start) else {
285 continue;
286 };
287
288 let editor_handle = editor_handle.clone();
289 blocks.push(BlockProperties {
290 placement: BlockPlacement::Above(anchor),
291 height: Some(1),
292 style: BlockStyle::Fixed,
293 render: Arc::new({
294 let conflict = conflict.clone();
295 move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
296 }),
297 priority: 0,
298 })
299 }
300 let new_block_ids = editor.insert_blocks(blocks, None, cx);
301
302 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
303 if let Some((buffer_conflicts, old_range)) =
304 conflict_addon.buffers.get_mut(&buffer_id).zip(old_range)
305 {
306 buffer_conflicts.block_ids.splice(
307 old_range,
308 new_conflicts
309 .iter()
310 .map(|conflict| conflict.range.clone())
311 .zip(new_block_ids),
312 );
313 }
314}
315
316#[ztracing::instrument(skip_all)]
317fn update_conflict_highlighting(
318 editor: &mut Editor,
319 conflict: &ConflictRegion,
320 buffer: &editor::MultiBufferSnapshot,
321 excerpt_id: editor::ExcerptId,
322 cx: &mut Context<Editor>,
323) -> Option<()> {
324 log::debug!("update conflict highlighting for {conflict:?}");
325
326 let outer = buffer.anchor_range_in_excerpt(excerpt_id, conflict.range.clone())?;
327 let ours = buffer.anchor_range_in_excerpt(excerpt_id, conflict.ours.clone())?;
328 let theirs = buffer.anchor_range_in_excerpt(excerpt_id, conflict.theirs.clone())?;
329
330 let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
331 let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
332
333 let options = RowHighlightOptions {
334 include_gutter: true,
335 ..Default::default()
336 };
337
338 editor.insert_gutter_highlight::<ConflictsOuter>(
339 outer.start..theirs.end,
340 |cx| cx.theme().colors().editor_background,
341 cx,
342 );
343
344 // Prevent diff hunk highlighting within the entire conflict region.
345 editor.highlight_rows::<ConflictsOuter>(outer.clone(), theirs_background, options, cx);
346 editor.highlight_rows::<ConflictsOurs>(ours.clone(), ours_background, options, cx);
347 editor.highlight_rows::<ConflictsOursMarker>(
348 outer.start..ours.start,
349 ours_background,
350 options,
351 cx,
352 );
353 editor.highlight_rows::<ConflictsTheirs>(theirs.clone(), theirs_background, options, cx);
354 editor.highlight_rows::<ConflictsTheirsMarker>(
355 theirs.end..outer.end,
356 theirs_background,
357 options,
358 cx,
359 );
360
361 Some(())
362}
363
364fn render_conflict_buttons(
365 conflict: &ConflictRegion,
366 excerpt_id: ExcerptId,
367 editor: WeakEntity<Editor>,
368 cx: &mut BlockContext,
369) -> AnyElement {
370 h_flex()
371 .id(cx.block_id)
372 .h(cx.line_height)
373 .ml(cx.margins.gutter.width)
374 .items_end()
375 .gap_1()
376 .bg(cx.theme().colors().editor_background)
377 .child(
378 Button::new("head", format!("Use {}", conflict.ours_branch_name))
379 .label_size(LabelSize::Small)
380 .on_click({
381 let editor = editor.clone();
382 let conflict = conflict.clone();
383 let ours = conflict.ours.clone();
384 move |_, window, cx| {
385 resolve_conflict(
386 editor.clone(),
387 excerpt_id,
388 conflict.clone(),
389 vec![ours.clone()],
390 window,
391 cx,
392 )
393 .detach()
394 }
395 }),
396 )
397 .child(
398 Button::new("origin", format!("Use {}", conflict.theirs_branch_name))
399 .label_size(LabelSize::Small)
400 .on_click({
401 let editor = editor.clone();
402 let conflict = conflict.clone();
403 let theirs = conflict.theirs.clone();
404 move |_, window, cx| {
405 resolve_conflict(
406 editor.clone(),
407 excerpt_id,
408 conflict.clone(),
409 vec![theirs.clone()],
410 window,
411 cx,
412 )
413 .detach()
414 }
415 }),
416 )
417 .child(
418 Button::new("both", "Use Both")
419 .label_size(LabelSize::Small)
420 .on_click({
421 let conflict = conflict.clone();
422 let ours = conflict.ours.clone();
423 let theirs = conflict.theirs.clone();
424 move |_, window, cx| {
425 resolve_conflict(
426 editor.clone(),
427 excerpt_id,
428 conflict.clone(),
429 vec![ours.clone(), theirs.clone()],
430 window,
431 cx,
432 )
433 .detach()
434 }
435 }),
436 )
437 .into_any()
438}
439
440pub(crate) fn resolve_conflict(
441 editor: WeakEntity<Editor>,
442 excerpt_id: ExcerptId,
443 resolved_conflict: ConflictRegion,
444 ranges: Vec<Range<Anchor>>,
445 window: &mut Window,
446 cx: &mut App,
447) -> Task<()> {
448 window.spawn(cx, async move |cx| {
449 let Some((workspace, project, multibuffer, buffer)) = editor
450 .update(cx, |editor, cx| {
451 let workspace = editor.workspace()?;
452 let project = editor.project()?.clone();
453 let multibuffer = editor.buffer().clone();
454 let buffer_id = resolved_conflict.ours.end.buffer_id?;
455 let buffer = multibuffer.read(cx).buffer(buffer_id)?;
456 resolved_conflict.resolve(buffer.clone(), &ranges, cx);
457 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
458 let snapshot = multibuffer.read(cx).snapshot(cx);
459 let buffer_snapshot = buffer.read(cx).snapshot();
460 let state = conflict_addon
461 .buffers
462 .get_mut(&buffer_snapshot.remote_id())?;
463 let ix = state
464 .block_ids
465 .binary_search_by(|(range, _)| {
466 range
467 .start
468 .cmp(&resolved_conflict.range.start, &buffer_snapshot)
469 })
470 .ok()?;
471 let &(_, block_id) = &state.block_ids[ix];
472 let range =
473 snapshot.anchor_range_in_excerpt(excerpt_id, resolved_conflict.range)?;
474
475 editor.remove_gutter_highlights::<ConflictsOuter>(vec![range.clone()], cx);
476
477 editor.remove_highlighted_rows::<ConflictsOuter>(vec![range.clone()], cx);
478 editor.remove_highlighted_rows::<ConflictsOurs>(vec![range.clone()], cx);
479 editor.remove_highlighted_rows::<ConflictsTheirs>(vec![range.clone()], cx);
480 editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![range.clone()], cx);
481 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![range], cx);
482 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
483 Some((workspace, project, multibuffer, buffer))
484 })
485 .ok()
486 .flatten()
487 else {
488 return;
489 };
490 let Some(save) = project
491 .update(cx, |project, cx| {
492 if multibuffer.read(cx).all_diff_hunks_expanded() {
493 project.save_buffer(buffer.clone(), cx)
494 } else {
495 Task::ready(Ok(()))
496 }
497 })
498 .ok()
499 else {
500 return;
501 };
502 if save.await.log_err().is_none() {
503 let open_path = maybe!({
504 let path = buffer
505 .read_with(cx, |buffer, cx| buffer.project_path(cx))
506 .ok()
507 .flatten()?;
508 workspace
509 .update_in(cx, |workspace, window, cx| {
510 workspace.open_path_preview(path, None, false, false, false, window, cx)
511 })
512 .ok()
513 });
514
515 if let Some(open_path) = open_path {
516 open_path.await.log_err();
517 }
518 }
519 })
520}