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};
17
18pub(crate) struct ConflictAddon {
19 buffers: HashMap<BufferId, BufferConflicts>,
20}
21
22impl ConflictAddon {
23 pub(crate) fn conflict_set(&self, buffer_id: BufferId) -> Option<Entity<ConflictSet>> {
24 self.buffers
25 .get(&buffer_id)
26 .map(|entry| entry.conflict_set.clone())
27 }
28}
29
30struct BufferConflicts {
31 block_ids: Vec<(Range<Anchor>, CustomBlockId)>,
32 conflict_set: Entity<ConflictSet>,
33 _subscription: Subscription,
34}
35
36impl editor::Addon for ConflictAddon {
37 fn to_any(&self) -> &dyn std::any::Any {
38 self
39 }
40
41 fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
42 Some(self)
43 }
44}
45
46pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) {
47 // Only show conflict UI for singletons and in the project diff.
48 if !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().clone();
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 conflicts_updated(
93 editor,
94 conflict_set,
95 &ConflictSetUpdate {
96 buffer_range: None,
97 old_range: 0..conflicts_len,
98 new_range: 0..conflicts_len,
99 },
100 cx,
101 );
102}
103
104fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
105 let Some(project) = &editor.project else {
106 return;
107 };
108 let git_store = project.read(cx).git_store().clone();
109
110 let buffer_conflicts = editor
111 .addon_mut::<ConflictAddon>()
112 .unwrap()
113 .buffers
114 .entry(buffer.read(cx).remote_id())
115 .or_insert_with(|| {
116 let conflict_set = git_store.update(cx, |git_store, cx| {
117 git_store.open_conflict_set(buffer.clone(), cx)
118 });
119 let subscription = cx.subscribe(&conflict_set, conflicts_updated);
120 BufferConflicts {
121 block_ids: Vec::new(),
122 conflict_set: conflict_set.clone(),
123 _subscription: subscription,
124 }
125 });
126
127 let conflict_set = buffer_conflicts.conflict_set.clone();
128 let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
129 let addon_conflicts_len = buffer_conflicts.block_ids.len();
130 conflicts_updated(
131 editor,
132 conflict_set,
133 &ConflictSetUpdate {
134 buffer_range: None,
135 old_range: 0..addon_conflicts_len,
136 new_range: 0..conflicts_len,
137 },
138 cx,
139 );
140}
141
142fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
143 let mut removed_block_ids = HashSet::default();
144 editor
145 .addon_mut::<ConflictAddon>()
146 .unwrap()
147 .buffers
148 .retain(|buffer_id, buffer| {
149 if removed_buffer_ids.contains(&buffer_id) {
150 removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
151 false
152 } else {
153 true
154 }
155 });
156 editor.remove_blocks(removed_block_ids, None, cx);
157}
158
159fn conflicts_updated(
160 editor: &mut Editor,
161 conflict_set: Entity<ConflictSet>,
162 event: &ConflictSetUpdate,
163 cx: &mut Context<Editor>,
164) {
165 let buffer_id = conflict_set.read(cx).snapshot.buffer_id;
166 let conflict_set = conflict_set.read(cx).snapshot();
167 let multibuffer = editor.buffer().read(cx);
168 let snapshot = multibuffer.snapshot(cx);
169 let excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
170 let Some(buffer_snapshot) = excerpts
171 .first()
172 .and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
173 else {
174 return;
175 };
176
177 // Remove obsolete highlights and blocks
178 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
179 if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
180 let old_conflicts = buffer_conflicts.block_ids[event.old_range.clone()].to_owned();
181 let mut removed_highlighted_ranges = Vec::new();
182 let mut removed_block_ids = HashSet::default();
183 for (conflict_range, block_id) in old_conflicts {
184 let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
185 let precedes_start = range
186 .context
187 .start
188 .cmp(&conflict_range.start, &buffer_snapshot)
189 .is_le();
190 let follows_end = range
191 .context
192 .end
193 .cmp(&conflict_range.start, &buffer_snapshot)
194 .is_ge();
195 precedes_start && follows_end
196 }) else {
197 continue;
198 };
199 let excerpt_id = *excerpt_id;
200 let Some(range) = snapshot
201 .anchor_in_excerpt(excerpt_id, conflict_range.start)
202 .zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
203 .map(|(start, end)| start..end)
204 else {
205 continue;
206 };
207 removed_highlighted_ranges.push(range.clone());
208 removed_block_ids.insert(block_id);
209 }
210
211 editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
212 editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
213 editor
214 .remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx);
215 editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx);
216 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(
217 removed_highlighted_ranges.clone(),
218 cx,
219 );
220 editor.remove_blocks(removed_block_ids, None, cx);
221 }
222
223 // Add new highlights and blocks
224 let editor_handle = cx.weak_entity();
225 let new_conflicts = &conflict_set.conflicts[event.new_range.clone()];
226 let mut blocks = Vec::new();
227 for conflict in new_conflicts {
228 let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
229 let precedes_start = range
230 .context
231 .start
232 .cmp(&conflict.range.start, &buffer_snapshot)
233 .is_le();
234 let follows_end = range
235 .context
236 .end
237 .cmp(&conflict.range.start, &buffer_snapshot)
238 .is_ge();
239 precedes_start && follows_end
240 }) else {
241 continue;
242 };
243 let excerpt_id = *excerpt_id;
244
245 update_conflict_highlighting(editor, conflict, &snapshot, excerpt_id, cx);
246
247 let Some(anchor) = snapshot.anchor_in_excerpt(excerpt_id, conflict.range.start) else {
248 continue;
249 };
250
251 let editor_handle = editor_handle.clone();
252 blocks.push(BlockProperties {
253 placement: BlockPlacement::Above(anchor),
254 height: Some(1),
255 style: BlockStyle::Fixed,
256 render: Arc::new({
257 let conflict = conflict.clone();
258 move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
259 }),
260 priority: 0,
261 })
262 }
263 let new_block_ids = editor.insert_blocks(blocks, None, cx);
264
265 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
266 if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
267 buffer_conflicts.block_ids.splice(
268 event.old_range.clone(),
269 new_conflicts
270 .iter()
271 .map(|conflict| conflict.range.clone())
272 .zip(new_block_ids),
273 );
274 }
275}
276
277fn update_conflict_highlighting(
278 editor: &mut Editor,
279 conflict: &ConflictRegion,
280 buffer: &editor::MultiBufferSnapshot,
281 excerpt_id: editor::ExcerptId,
282 cx: &mut Context<Editor>,
283) {
284 log::debug!("update conflict highlighting for {conflict:?}");
285 let theme = cx.theme().clone();
286 let colors = theme.colors();
287 let outer_start = buffer
288 .anchor_in_excerpt(excerpt_id, conflict.range.start)
289 .unwrap();
290 let outer_end = buffer
291 .anchor_in_excerpt(excerpt_id, conflict.range.end)
292 .unwrap();
293 let our_start = buffer
294 .anchor_in_excerpt(excerpt_id, conflict.ours.start)
295 .unwrap();
296 let our_end = buffer
297 .anchor_in_excerpt(excerpt_id, conflict.ours.end)
298 .unwrap();
299 let their_start = buffer
300 .anchor_in_excerpt(excerpt_id, conflict.theirs.start)
301 .unwrap();
302 let their_end = buffer
303 .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
304 .unwrap();
305
306 let ours_background = colors.version_control_conflict_ours_background;
307 let ours_marker = colors.version_control_conflict_ours_marker_background;
308 let theirs_background = colors.version_control_conflict_theirs_background;
309 let theirs_marker = colors.version_control_conflict_theirs_marker_background;
310 let divider_background = colors.version_control_conflict_divider_background;
311
312 let options = RowHighlightOptions {
313 include_gutter: false,
314 ..Default::default()
315 };
316
317 // Prevent diff hunk highlighting within the entire conflict region.
318 editor.highlight_rows::<ConflictsOuter>(
319 outer_start..outer_end,
320 divider_background,
321 options,
322 cx,
323 );
324 editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
325 editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
326 editor.highlight_rows::<ConflictsTheirs>(
327 their_start..their_end,
328 theirs_background,
329 options,
330 cx,
331 );
332 editor.highlight_rows::<ConflictsTheirsMarker>(
333 their_end..outer_end,
334 theirs_marker,
335 options,
336 cx,
337 );
338}
339
340fn render_conflict_buttons(
341 conflict: &ConflictRegion,
342 excerpt_id: ExcerptId,
343 editor: WeakEntity<Editor>,
344 cx: &mut BlockContext,
345) -> AnyElement {
346 h_flex()
347 .h(cx.line_height)
348 .items_end()
349 .ml(cx.gutter_dimensions.width)
350 .id(cx.block_id)
351 .gap_0p5()
352 .child(
353 div()
354 .id("ours")
355 .px_1()
356 .child("Take Ours")
357 .rounded_t(rems(0.2))
358 .text_ui_sm(cx)
359 .hover(|this| this.bg(cx.theme().colors().element_background))
360 .cursor_pointer()
361 .on_click({
362 let editor = editor.clone();
363 let conflict = conflict.clone();
364 let ours = conflict.ours.clone();
365 move |_, _, cx| {
366 resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx)
367 }
368 }),
369 )
370 .child(
371 div()
372 .id("theirs")
373 .px_1()
374 .child("Take Theirs")
375 .rounded_t(rems(0.2))
376 .text_ui_sm(cx)
377 .hover(|this| this.bg(cx.theme().colors().element_background))
378 .cursor_pointer()
379 .on_click({
380 let editor = editor.clone();
381 let conflict = conflict.clone();
382 let theirs = conflict.theirs.clone();
383 move |_, _, cx| {
384 resolve_conflict(
385 editor.clone(),
386 excerpt_id,
387 &conflict,
388 &[theirs.clone()],
389 cx,
390 )
391 }
392 }),
393 )
394 .child(
395 div()
396 .id("both")
397 .px_1()
398 .child("Take Both")
399 .rounded_t(rems(0.2))
400 .text_ui_sm(cx)
401 .hover(|this| this.bg(cx.theme().colors().element_background))
402 .cursor_pointer()
403 .on_click({
404 let editor = editor.clone();
405 let conflict = conflict.clone();
406 let ours = conflict.ours.clone();
407 let theirs = conflict.theirs.clone();
408 move |_, _, cx| {
409 resolve_conflict(
410 editor.clone(),
411 excerpt_id,
412 &conflict,
413 &[ours.clone(), theirs.clone()],
414 cx,
415 )
416 }
417 }),
418 )
419 .into_any()
420}
421
422fn resolve_conflict(
423 editor: WeakEntity<Editor>,
424 excerpt_id: ExcerptId,
425 resolved_conflict: &ConflictRegion,
426 ranges: &[Range<Anchor>],
427 cx: &mut App,
428) {
429 let Some(editor) = editor.upgrade() else {
430 return;
431 };
432
433 let multibuffer = editor.read(cx).buffer().read(cx);
434 let snapshot = multibuffer.snapshot(cx);
435 let Some(buffer) = resolved_conflict
436 .ours
437 .end
438 .buffer_id
439 .and_then(|buffer_id| multibuffer.buffer(buffer_id))
440 else {
441 return;
442 };
443 let buffer_snapshot = buffer.read(cx).snapshot();
444
445 resolved_conflict.resolve(buffer, ranges, cx);
446
447 editor.update(cx, |editor, cx| {
448 let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
449 let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else {
450 return;
451 };
452 let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| {
453 range
454 .start
455 .cmp(&resolved_conflict.range.start, &buffer_snapshot)
456 }) else {
457 return;
458 };
459 let &(_, block_id) = &state.block_ids[ix];
460 let start = snapshot
461 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
462 .unwrap();
463 let end = snapshot
464 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
465 .unwrap();
466 editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
467 editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
468 editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
469 editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
470 editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
471 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
472 })
473}