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 || editor.read_only(cx)
51 {
52 return;
53 }
54
55 editor.register_addon(ConflictAddon {
56 buffers: Default::default(),
57 });
58
59 let buffers = buffer.read(cx).all_buffers();
60 for buffer in buffers {
61 buffer_added(editor, buffer, cx);
62 }
63
64 cx.subscribe(&cx.entity(), |editor, _, event, cx| match event {
65 EditorEvent::ExcerptsAdded { buffer, .. } => buffer_added(editor, buffer.clone(), cx),
66 EditorEvent::ExcerptsExpanded { ids } => {
67 let multibuffer = editor.buffer().read(cx).snapshot(cx);
68 for excerpt_id in ids {
69 let Some(buffer) = multibuffer.buffer_for_excerpt(*excerpt_id) else {
70 continue;
71 };
72 let addon = editor.addon::<ConflictAddon>().unwrap();
73 let Some(conflict_set) = addon.conflict_set(buffer.remote_id()).clone() else {
74 return;
75 };
76 excerpt_for_buffer_updated(editor, conflict_set, cx);
77 }
78 }
79 EditorEvent::ExcerptsRemoved {
80 removed_buffer_ids, ..
81 } => buffers_removed(editor, removed_buffer_ids, cx),
82 _ => {}
83 })
84 .detach();
85}
86
87fn excerpt_for_buffer_updated(
88 editor: &mut Editor,
89 conflict_set: Entity<ConflictSet>,
90 cx: &mut Context<Editor>,
91) {
92 let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
93 let buffer_id = conflict_set.read(cx).snapshot().buffer_id;
94 let Some(buffer_conflicts) = editor
95 .addon_mut::<ConflictAddon>()
96 .unwrap()
97 .buffers
98 .get(&buffer_id)
99 else {
100 return;
101 };
102 let addon_conflicts_len = buffer_conflicts.block_ids.len();
103 conflicts_updated(
104 editor,
105 conflict_set,
106 &ConflictSetUpdate {
107 buffer_range: None,
108 old_range: 0..addon_conflicts_len,
109 new_range: 0..conflicts_len,
110 },
111 cx,
112 );
113}
114
115#[ztracing::instrument(skip_all)]
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,
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
171#[ztracing::instrument(skip_all)]
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.anchor_range_in_excerpt(excerpt_id, conflict_range) else {
241 continue;
242 };
243 removed_highlighted_ranges.push(range.clone());
244 removed_block_ids.insert(block_id);
245 }
246
247 editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
248
249 editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
250 editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
251 editor
252 .remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx);
253 editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx);
254 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(
255 removed_highlighted_ranges.clone(),
256 cx,
257 );
258 editor.remove_blocks(removed_block_ids, None, cx);
259 }
260
261 // Add new highlights and blocks
262 let editor_handle = cx.weak_entity();
263 let new_conflicts = &conflict_set.conflicts[event.new_range.clone()];
264 let mut blocks = Vec::new();
265 for conflict in new_conflicts {
266 let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
267 let precedes_start = range
268 .context
269 .start
270 .cmp(&conflict.range.start, buffer_snapshot)
271 .is_le();
272 let follows_end = range
273 .context
274 .end
275 .cmp(&conflict.range.start, buffer_snapshot)
276 .is_ge();
277 precedes_start && follows_end
278 }) else {
279 continue;
280 };
281 let excerpt_id = *excerpt_id;
282
283 update_conflict_highlighting(editor, conflict, &snapshot, excerpt_id, cx);
284
285 let Some(anchor) = snapshot.anchor_in_excerpt(excerpt_id, conflict.range.start) else {
286 continue;
287 };
288
289 let editor_handle = editor_handle.clone();
290 blocks.push(BlockProperties {
291 placement: BlockPlacement::Above(anchor),
292 height: Some(1),
293 style: BlockStyle::Sticky,
294 render: Arc::new({
295 let conflict = conflict.clone();
296 move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
297 }),
298 priority: 0,
299 })
300 }
301 let new_block_ids = editor.insert_blocks(blocks, None, cx);
302
303 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
304 if let Some((buffer_conflicts, old_range)) =
305 conflict_addon.buffers.get_mut(&buffer_id).zip(old_range)
306 {
307 buffer_conflicts.block_ids.splice(
308 old_range,
309 new_conflicts
310 .iter()
311 .map(|conflict| conflict.range.clone())
312 .zip(new_block_ids),
313 );
314 }
315}
316
317#[ztracing::instrument(skip_all)]
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) -> Option<()> {
325 log::debug!("update conflict highlighting for {conflict:?}");
326
327 let outer = buffer.anchor_range_in_excerpt(excerpt_id, conflict.range.clone())?;
328 let ours = buffer.anchor_range_in_excerpt(excerpt_id, conflict.ours.clone())?;
329 let theirs = buffer.anchor_range_in_excerpt(excerpt_id, conflict.theirs.clone())?;
330
331 let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
332 let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
333
334 let options = RowHighlightOptions {
335 include_gutter: true,
336 ..Default::default()
337 };
338
339 editor.insert_gutter_highlight::<ConflictsOuter>(
340 outer.start..theirs.end,
341 |cx| cx.theme().colors().editor_background,
342 cx,
343 );
344
345 // Prevent diff hunk highlighting within the entire conflict region.
346 editor.highlight_rows::<ConflictsOuter>(outer.clone(), theirs_background, options, cx);
347 editor.highlight_rows::<ConflictsOurs>(ours.clone(), ours_background, options, cx);
348 editor.highlight_rows::<ConflictsOursMarker>(
349 outer.start..ours.start,
350 ours_background,
351 options,
352 cx,
353 );
354 editor.highlight_rows::<ConflictsTheirs>(theirs.clone(), theirs_background, options, cx);
355 editor.highlight_rows::<ConflictsTheirsMarker>(
356 theirs.end..outer.end,
357 theirs_background,
358 options,
359 cx,
360 );
361
362 Some(())
363}
364
365fn render_conflict_buttons(
366 conflict: &ConflictRegion,
367 excerpt_id: ExcerptId,
368 editor: WeakEntity<Editor>,
369 cx: &mut BlockContext,
370) -> AnyElement {
371 h_flex()
372 .id(cx.block_id)
373 .h(cx.line_height)
374 .ml(cx.margins.gutter.width)
375 .items_end()
376 .gap_1()
377 .bg(cx.theme().colors().editor_background)
378 .child(
379 Button::new("head", format!("Use {}", conflict.ours_branch_name))
380 .label_size(LabelSize::Small)
381 .on_click({
382 let editor = editor.clone();
383 let conflict = conflict.clone();
384 let ours = conflict.ours.clone();
385 move |_, window, cx| {
386 resolve_conflict(
387 editor.clone(),
388 excerpt_id,
389 conflict.clone(),
390 vec![ours.clone()],
391 window,
392 cx,
393 )
394 .detach()
395 }
396 }),
397 )
398 .child(
399 Button::new("origin", format!("Use {}", conflict.theirs_branch_name))
400 .label_size(LabelSize::Small)
401 .on_click({
402 let editor = editor.clone();
403 let conflict = conflict.clone();
404 let theirs = conflict.theirs.clone();
405 move |_, window, cx| {
406 resolve_conflict(
407 editor.clone(),
408 excerpt_id,
409 conflict.clone(),
410 vec![theirs.clone()],
411 window,
412 cx,
413 )
414 .detach()
415 }
416 }),
417 )
418 .child(
419 Button::new("both", "Use Both")
420 .label_size(LabelSize::Small)
421 .on_click({
422 let conflict = conflict.clone();
423 let ours = conflict.ours.clone();
424 let theirs = conflict.theirs.clone();
425 move |_, window, cx| {
426 resolve_conflict(
427 editor.clone(),
428 excerpt_id,
429 conflict.clone(),
430 vec![ours.clone(), theirs.clone()],
431 window,
432 cx,
433 )
434 .detach()
435 }
436 }),
437 )
438 .into_any()
439}
440
441pub(crate) fn resolve_conflict(
442 editor: WeakEntity<Editor>,
443 excerpt_id: ExcerptId,
444 resolved_conflict: ConflictRegion,
445 ranges: Vec<Range<Anchor>>,
446 window: &mut Window,
447 cx: &mut App,
448) -> Task<()> {
449 window.spawn(cx, async move |cx| {
450 let Some((workspace, project, multibuffer, buffer)) = editor
451 .update(cx, |editor, cx| {
452 let workspace = editor.workspace()?;
453 let project = editor.project()?.clone();
454 let multibuffer = editor.buffer().clone();
455 let buffer_id = resolved_conflict.ours.end.buffer_id?;
456 let buffer = multibuffer.read(cx).buffer(buffer_id)?;
457 resolved_conflict.resolve(buffer.clone(), &ranges, cx);
458 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
459 let snapshot = multibuffer.read(cx).snapshot(cx);
460 let buffer_snapshot = buffer.read(cx).snapshot();
461 let state = conflict_addon
462 .buffers
463 .get_mut(&buffer_snapshot.remote_id())?;
464 let ix = state
465 .block_ids
466 .binary_search_by(|(range, _)| {
467 range
468 .start
469 .cmp(&resolved_conflict.range.start, &buffer_snapshot)
470 })
471 .ok()?;
472 let &(_, block_id) = &state.block_ids[ix];
473 let range =
474 snapshot.anchor_range_in_excerpt(excerpt_id, resolved_conflict.range)?;
475
476 editor.remove_gutter_highlights::<ConflictsOuter>(vec![range.clone()], cx);
477
478 editor.remove_highlighted_rows::<ConflictsOuter>(vec![range.clone()], cx);
479 editor.remove_highlighted_rows::<ConflictsOurs>(vec![range.clone()], cx);
480 editor.remove_highlighted_rows::<ConflictsTheirs>(vec![range.clone()], cx);
481 editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![range.clone()], cx);
482 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![range], cx);
483 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
484 Some((workspace, project, multibuffer, buffer))
485 })
486 .ok()
487 .flatten()
488 else {
489 return;
490 };
491 let save = project.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 if save.await.log_err().is_none() {
499 let open_path = maybe!({
500 let path = buffer.read_with(cx, |buffer, cx| buffer.project_path(cx))?;
501 workspace
502 .update_in(cx, |workspace, window, cx| {
503 workspace.open_path_preview(path, None, false, false, false, window, cx)
504 })
505 .ok()
506 });
507
508 if let Some(open_path) = open_path {
509 open_path.await.log_err();
510 }
511 }
512 })
513}