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, WeakEntity,
9};
10use language::{Anchor, Buffer, BufferId};
11use project::{ConflictRegion, ConflictSet, ConflictSetUpdate};
12use std::{ops::Range, sync::Arc};
13use ui::{
14 ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
15 StyledTypography as _, div, h_flex, rems,
16};
17use util::{debug_panic, maybe};
18
19pub(crate) struct ConflictAddon {
20 buffers: HashMap<BufferId, BufferConflicts>,
21}
22
23impl ConflictAddon {
24 pub(crate) fn conflict_set(&self, buffer_id: BufferId) -> Option<Entity<ConflictSet>> {
25 self.buffers
26 .get(&buffer_id)
27 .map(|entry| entry.conflict_set.clone())
28 }
29}
30
31struct BufferConflicts {
32 block_ids: Vec<(Range<Anchor>, CustomBlockId)>,
33 conflict_set: Entity<ConflictSet>,
34 _subscription: Subscription,
35}
36
37impl editor::Addon for ConflictAddon {
38 fn to_any(&self) -> &dyn std::any::Any {
39 self
40 }
41
42 fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
43 Some(self)
44 }
45}
46
47pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) {
48 // Only show conflict UI for singletons and in the project diff.
49 if !editor.buffer().read(cx).is_singleton()
50 && !editor.buffer().read(cx).all_diff_hunks_expanded()
51 {
52 return;
53 }
54
55 editor.register_addon(ConflictAddon {
56 buffers: Default::default(),
57 });
58
59 let buffers = buffer.read(cx).all_buffers().clone();
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
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: conflict_set.clone(),
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
170fn conflicts_updated(
171 editor: &mut Editor,
172 conflict_set: Entity<ConflictSet>,
173 event: &ConflictSetUpdate,
174 cx: &mut Context<Editor>,
175) {
176 let buffer_id = conflict_set.read(cx).snapshot.buffer_id;
177 let conflict_set = conflict_set.read(cx).snapshot();
178 let multibuffer = editor.buffer().read(cx);
179 let snapshot = multibuffer.snapshot(cx);
180 let excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
181 let Some(buffer_snapshot) = excerpts
182 .first()
183 .and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
184 else {
185 return;
186 };
187
188 let old_range = maybe!({
189 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
190 let buffer_conflicts = conflict_addon.buffers.get(&buffer_id)?;
191 match buffer_conflicts.block_ids.get(event.old_range.clone()) {
192 Some(_) => Some(event.old_range.clone()),
193 None => {
194 debug_panic!(
195 "conflicts updated event old range is invalid for buffer conflicts view (block_ids len is {:?}, old_range is {:?})",
196 buffer_conflicts.block_ids.len(),
197 event.old_range,
198 );
199 if event.old_range.start <= event.old_range.end {
200 Some(
201 event.old_range.start.min(buffer_conflicts.block_ids.len())
202 ..event.old_range.end.min(buffer_conflicts.block_ids.len()),
203 )
204 } else {
205 None
206 }
207 }
208 }
209 });
210
211 // Remove obsolete highlights and blocks
212 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
213 if let Some((buffer_conflicts, old_range)) = conflict_addon
214 .buffers
215 .get_mut(&buffer_id)
216 .zip(old_range.clone())
217 {
218 let old_conflicts = buffer_conflicts.block_ids[old_range].to_owned();
219 let mut removed_highlighted_ranges = Vec::new();
220 let mut removed_block_ids = HashSet::default();
221 for (conflict_range, block_id) in old_conflicts {
222 let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
223 let precedes_start = range
224 .context
225 .start
226 .cmp(&conflict_range.start, &buffer_snapshot)
227 .is_le();
228 let follows_end = range
229 .context
230 .end
231 .cmp(&conflict_range.start, &buffer_snapshot)
232 .is_ge();
233 precedes_start && follows_end
234 }) else {
235 continue;
236 };
237 let excerpt_id = *excerpt_id;
238 let Some(range) = snapshot
239 .anchor_in_excerpt(excerpt_id, conflict_range.start)
240 .zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
241 .map(|(start, end)| start..end)
242 else {
243 continue;
244 };
245 removed_highlighted_ranges.push(range.clone());
246 removed_block_ids.insert(block_id);
247 }
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::Fixed,
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
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) {
324 log::debug!("update conflict highlighting for {conflict:?}");
325 let theme = cx.theme().clone();
326 let colors = theme.colors();
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 = colors.version_control_conflict_ours_background;
347 let ours_marker = colors.version_control_conflict_ours_marker_background;
348 let theirs_background = colors.version_control_conflict_theirs_background;
349 let theirs_marker = colors.version_control_conflict_theirs_marker_background;
350 let divider_background = colors.version_control_conflict_divider_background;
351
352 let options = RowHighlightOptions {
353 include_gutter: false,
354 ..Default::default()
355 };
356
357 // Prevent diff hunk highlighting within the entire conflict region.
358 editor.highlight_rows::<ConflictsOuter>(
359 outer_start..outer_end,
360 divider_background,
361 options,
362 cx,
363 );
364 editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
365 editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
366 editor.highlight_rows::<ConflictsTheirs>(
367 their_start..their_end,
368 theirs_background,
369 options,
370 cx,
371 );
372 editor.highlight_rows::<ConflictsTheirsMarker>(
373 their_end..outer_end,
374 theirs_marker,
375 options,
376 cx,
377 );
378}
379
380fn render_conflict_buttons(
381 conflict: &ConflictRegion,
382 excerpt_id: ExcerptId,
383 editor: WeakEntity<Editor>,
384 cx: &mut BlockContext,
385) -> AnyElement {
386 h_flex()
387 .h(cx.line_height)
388 .items_end()
389 .ml(cx.gutter_dimensions.width)
390 .id(cx.block_id)
391 .gap_0p5()
392 .child(
393 div()
394 .id("ours")
395 .px_1()
396 .child("Take Ours")
397 .rounded_t(rems(0.2))
398 .text_ui_sm(cx)
399 .hover(|this| this.bg(cx.theme().colors().element_background))
400 .cursor_pointer()
401 .on_click({
402 let editor = editor.clone();
403 let conflict = conflict.clone();
404 let ours = conflict.ours.clone();
405 move |_, _, cx| {
406 resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx)
407 }
408 }),
409 )
410 .child(
411 div()
412 .id("theirs")
413 .px_1()
414 .child("Take Theirs")
415 .rounded_t(rems(0.2))
416 .text_ui_sm(cx)
417 .hover(|this| this.bg(cx.theme().colors().element_background))
418 .cursor_pointer()
419 .on_click({
420 let editor = editor.clone();
421 let conflict = conflict.clone();
422 let theirs = conflict.theirs.clone();
423 move |_, _, cx| {
424 resolve_conflict(
425 editor.clone(),
426 excerpt_id,
427 &conflict,
428 &[theirs.clone()],
429 cx,
430 )
431 }
432 }),
433 )
434 .child(
435 div()
436 .id("both")
437 .px_1()
438 .child("Take Both")
439 .rounded_t(rems(0.2))
440 .text_ui_sm(cx)
441 .hover(|this| this.bg(cx.theme().colors().element_background))
442 .cursor_pointer()
443 .on_click({
444 let editor = editor.clone();
445 let conflict = conflict.clone();
446 let ours = conflict.ours.clone();
447 let theirs = conflict.theirs.clone();
448 move |_, _, cx| {
449 resolve_conflict(
450 editor.clone(),
451 excerpt_id,
452 &conflict,
453 &[ours.clone(), theirs.clone()],
454 cx,
455 )
456 }
457 }),
458 )
459 .into_any()
460}
461
462fn resolve_conflict(
463 editor: WeakEntity<Editor>,
464 excerpt_id: ExcerptId,
465 resolved_conflict: &ConflictRegion,
466 ranges: &[Range<Anchor>],
467 cx: &mut App,
468) {
469 let Some(editor) = editor.upgrade() else {
470 return;
471 };
472
473 let multibuffer = editor.read(cx).buffer().read(cx);
474 let snapshot = multibuffer.snapshot(cx);
475 let Some(buffer) = resolved_conflict
476 .ours
477 .end
478 .buffer_id
479 .and_then(|buffer_id| multibuffer.buffer(buffer_id))
480 else {
481 return;
482 };
483 let buffer_snapshot = buffer.read(cx).snapshot();
484
485 resolved_conflict.resolve(buffer, ranges, cx);
486
487 editor.update(cx, |editor, cx| {
488 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
489 let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else {
490 return;
491 };
492 let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| {
493 range
494 .start
495 .cmp(&resolved_conflict.range.start, &buffer_snapshot)
496 }) else {
497 return;
498 };
499 let &(_, block_id) = &state.block_ids[ix];
500 let start = snapshot
501 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
502 .unwrap();
503 let end = snapshot
504 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
505 .unwrap();
506 editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
507 editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
508 editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
509 editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
510 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
511 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
512 })
513}