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