conflict_view.rs

  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::{
 15    ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
 16    StyledTypography as _, Window, div, h_flex, rems,
 17};
 18use util::{ResultExt as _, debug_panic, maybe};
 19
 20pub(crate) struct ConflictAddon {
 21    buffers: HashMap<BufferId, BufferConflicts>,
 22}
 23
 24impl ConflictAddon {
 25    pub(crate) fn conflict_set(&self, buffer_id: BufferId) -> Option<Entity<ConflictSet>> {
 26        self.buffers
 27            .get(&buffer_id)
 28            .map(|entry| entry.conflict_set.clone())
 29    }
 30}
 31
 32struct BufferConflicts {
 33    block_ids: Vec<(Range<Anchor>, CustomBlockId)>,
 34    conflict_set: Entity<ConflictSet>,
 35    _subscription: Subscription,
 36}
 37
 38impl editor::Addon for ConflictAddon {
 39    fn to_any(&self) -> &dyn std::any::Any {
 40        self
 41    }
 42
 43    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
 44        Some(self)
 45    }
 46}
 47
 48pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) {
 49    // Only show conflict UI for singletons and in the project diff.
 50    if !editor.mode().is_full()
 51        || (!editor.buffer().read(cx).is_singleton()
 52            && !editor.buffer().read(cx).all_diff_hunks_expanded())
 53    {
 54        return;
 55    }
 56
 57    editor.register_addon(ConflictAddon {
 58        buffers: Default::default(),
 59    });
 60
 61    let buffers = buffer.read(cx).all_buffers().clone();
 62    for buffer in buffers {
 63        buffer_added(editor, buffer, cx);
 64    }
 65
 66    cx.subscribe(&cx.entity(), |editor, _, event, cx| match event {
 67        EditorEvent::ExcerptsAdded { buffer, .. } => buffer_added(editor, buffer.clone(), cx),
 68        EditorEvent::ExcerptsExpanded { ids } => {
 69            let multibuffer = editor.buffer().read(cx).snapshot(cx);
 70            for excerpt_id in ids {
 71                let Some(buffer) = multibuffer.buffer_for_excerpt(*excerpt_id) else {
 72                    continue;
 73                };
 74                let addon = editor.addon::<ConflictAddon>().unwrap();
 75                let Some(conflict_set) = addon.conflict_set(buffer.remote_id()).clone() else {
 76                    return;
 77                };
 78                excerpt_for_buffer_updated(editor, conflict_set, cx);
 79            }
 80        }
 81        EditorEvent::ExcerptsRemoved {
 82            removed_buffer_ids, ..
 83        } => buffers_removed(editor, removed_buffer_ids, cx),
 84        _ => {}
 85    })
 86    .detach();
 87}
 88
 89fn excerpt_for_buffer_updated(
 90    editor: &mut Editor,
 91    conflict_set: Entity<ConflictSet>,
 92    cx: &mut Context<Editor>,
 93) {
 94    let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
 95    let buffer_id = conflict_set.read(cx).snapshot().buffer_id;
 96    let Some(buffer_conflicts) = editor
 97        .addon_mut::<ConflictAddon>()
 98        .unwrap()
 99        .buffers
100        .get(&buffer_id)
101    else {
102        return;
103    };
104    let addon_conflicts_len = buffer_conflicts.block_ids.len();
105    conflicts_updated(
106        editor,
107        conflict_set,
108        &ConflictSetUpdate {
109            buffer_range: None,
110            old_range: 0..addon_conflicts_len,
111            new_range: 0..conflicts_len,
112        },
113        cx,
114    );
115}
116
117fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
118    let Some(project) = &editor.project else {
119        return;
120    };
121    let git_store = project.read(cx).git_store().clone();
122
123    let buffer_conflicts = editor
124        .addon_mut::<ConflictAddon>()
125        .unwrap()
126        .buffers
127        .entry(buffer.read(cx).remote_id())
128        .or_insert_with(|| {
129            let conflict_set = git_store.update(cx, |git_store, cx| {
130                git_store.open_conflict_set(buffer.clone(), cx)
131            });
132            let subscription = cx.subscribe(&conflict_set, conflicts_updated);
133            BufferConflicts {
134                block_ids: Vec::new(),
135                conflict_set: conflict_set.clone(),
136                _subscription: subscription,
137            }
138        });
139
140    let conflict_set = buffer_conflicts.conflict_set.clone();
141    let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
142    let addon_conflicts_len = buffer_conflicts.block_ids.len();
143    conflicts_updated(
144        editor,
145        conflict_set,
146        &ConflictSetUpdate {
147            buffer_range: None,
148            old_range: 0..addon_conflicts_len,
149            new_range: 0..conflicts_len,
150        },
151        cx,
152    );
153}
154
155fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
156    let mut removed_block_ids = HashSet::default();
157    editor
158        .addon_mut::<ConflictAddon>()
159        .unwrap()
160        .buffers
161        .retain(|buffer_id, buffer| {
162            if removed_buffer_ids.contains(&buffer_id) {
163                removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
164                false
165            } else {
166                true
167            }
168        });
169    editor.remove_blocks(removed_block_ids, None, cx);
170}
171
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
241                .anchor_in_excerpt(excerpt_id, conflict_range.start)
242                .zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
243                .map(|(start, end)| start..end)
244            else {
245                continue;
246            };
247            removed_highlighted_ranges.push(range.clone());
248            removed_block_ids.insert(block_id);
249        }
250
251        editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
252        editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
253        editor
254            .remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx);
255        editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx);
256        editor.remove_highlighted_rows::<ConflictsTheirsMarker>(
257            removed_highlighted_ranges.clone(),
258            cx,
259        );
260        editor.remove_blocks(removed_block_ids, None, cx);
261    }
262
263    // Add new highlights and blocks
264    let editor_handle = cx.weak_entity();
265    let new_conflicts = &conflict_set.conflicts[event.new_range.clone()];
266    let mut blocks = Vec::new();
267    for conflict in new_conflicts {
268        let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
269            let precedes_start = range
270                .context
271                .start
272                .cmp(&conflict.range.start, &buffer_snapshot)
273                .is_le();
274            let follows_end = range
275                .context
276                .end
277                .cmp(&conflict.range.start, &buffer_snapshot)
278                .is_ge();
279            precedes_start && follows_end
280        }) else {
281            continue;
282        };
283        let excerpt_id = *excerpt_id;
284
285        update_conflict_highlighting(editor, conflict, &snapshot, excerpt_id, cx);
286
287        let Some(anchor) = snapshot.anchor_in_excerpt(excerpt_id, conflict.range.start) else {
288            continue;
289        };
290
291        let editor_handle = editor_handle.clone();
292        blocks.push(BlockProperties {
293            placement: BlockPlacement::Above(anchor),
294            height: Some(1),
295            style: BlockStyle::Fixed,
296            render: Arc::new({
297                let conflict = conflict.clone();
298                move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
299            }),
300            priority: 0,
301            render_in_minimap: true,
302        })
303    }
304    let new_block_ids = editor.insert_blocks(blocks, None, cx);
305
306    let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
307    if let Some((buffer_conflicts, old_range)) =
308        conflict_addon.buffers.get_mut(&buffer_id).zip(old_range)
309    {
310        buffer_conflicts.block_ids.splice(
311            old_range,
312            new_conflicts
313                .iter()
314                .map(|conflict| conflict.range.clone())
315                .zip(new_block_ids),
316        );
317    }
318}
319
320fn update_conflict_highlighting(
321    editor: &mut Editor,
322    conflict: &ConflictRegion,
323    buffer: &editor::MultiBufferSnapshot,
324    excerpt_id: editor::ExcerptId,
325    cx: &mut Context<Editor>,
326) {
327    log::debug!("update conflict highlighting for {conflict:?}");
328    let theme = cx.theme().clone();
329    let colors = theme.colors();
330    let outer_start = buffer
331        .anchor_in_excerpt(excerpt_id, conflict.range.start)
332        .unwrap();
333    let outer_end = buffer
334        .anchor_in_excerpt(excerpt_id, conflict.range.end)
335        .unwrap();
336    let our_start = buffer
337        .anchor_in_excerpt(excerpt_id, conflict.ours.start)
338        .unwrap();
339    let our_end = buffer
340        .anchor_in_excerpt(excerpt_id, conflict.ours.end)
341        .unwrap();
342    let their_start = buffer
343        .anchor_in_excerpt(excerpt_id, conflict.theirs.start)
344        .unwrap();
345    let their_end = buffer
346        .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
347        .unwrap();
348
349    let ours_background = colors.version_control_conflict_ours_background;
350    let ours_marker = colors.version_control_conflict_ours_marker_background;
351    let theirs_background = colors.version_control_conflict_theirs_background;
352    let theirs_marker = colors.version_control_conflict_theirs_marker_background;
353    let divider_background = colors.version_control_conflict_divider_background;
354
355    let options = RowHighlightOptions {
356        include_gutter: false,
357        ..Default::default()
358    };
359
360    // Prevent diff hunk highlighting within the entire conflict region.
361    editor.highlight_rows::<ConflictsOuter>(
362        outer_start..outer_end,
363        divider_background,
364        options,
365        cx,
366    );
367    editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
368    editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
369    editor.highlight_rows::<ConflictsTheirs>(
370        their_start..their_end,
371        theirs_background,
372        options,
373        cx,
374    );
375    editor.highlight_rows::<ConflictsTheirsMarker>(
376        their_end..outer_end,
377        theirs_marker,
378        options,
379        cx,
380    );
381}
382
383fn render_conflict_buttons(
384    conflict: &ConflictRegion,
385    excerpt_id: ExcerptId,
386    editor: WeakEntity<Editor>,
387    cx: &mut BlockContext,
388) -> AnyElement {
389    h_flex()
390        .h(cx.line_height)
391        .items_end()
392        .ml(cx.margins.gutter.width)
393        .id(cx.block_id)
394        .gap_0p5()
395        .child(
396            div()
397                .id("ours")
398                .px_1()
399                .child("Take Ours")
400                .rounded_t(rems(0.2))
401                .text_ui_sm(cx)
402                .hover(|this| this.bg(cx.theme().colors().element_background))
403                .cursor_pointer()
404                .on_click({
405                    let editor = editor.clone();
406                    let conflict = conflict.clone();
407                    let ours = conflict.ours.clone();
408                    move |_, window, cx| {
409                        resolve_conflict(
410                            editor.clone(),
411                            excerpt_id,
412                            conflict.clone(),
413                            vec![ours.clone()],
414                            window,
415                            cx,
416                        )
417                        .detach()
418                    }
419                }),
420        )
421        .child(
422            div()
423                .id("theirs")
424                .px_1()
425                .child("Take Theirs")
426                .rounded_t(rems(0.2))
427                .text_ui_sm(cx)
428                .hover(|this| this.bg(cx.theme().colors().element_background))
429                .cursor_pointer()
430                .on_click({
431                    let editor = editor.clone();
432                    let conflict = conflict.clone();
433                    let theirs = conflict.theirs.clone();
434                    move |_, window, cx| {
435                        resolve_conflict(
436                            editor.clone(),
437                            excerpt_id,
438                            conflict.clone(),
439                            vec![theirs.clone()],
440                            window,
441                            cx,
442                        )
443                        .detach()
444                    }
445                }),
446        )
447        .child(
448            div()
449                .id("both")
450                .px_1()
451                .child("Take Both")
452                .rounded_t(rems(0.2))
453                .text_ui_sm(cx)
454                .hover(|this| this.bg(cx.theme().colors().element_background))
455                .cursor_pointer()
456                .on_click({
457                    let editor = editor.clone();
458                    let conflict = conflict.clone();
459                    let ours = conflict.ours.clone();
460                    let theirs = conflict.theirs.clone();
461                    move |_, window, cx| {
462                        resolve_conflict(
463                            editor.clone(),
464                            excerpt_id,
465                            conflict.clone(),
466                            vec![ours.clone(), theirs.clone()],
467                            window,
468                            cx,
469                        )
470                        .detach()
471                    }
472                }),
473        )
474        .into_any()
475}
476
477pub(crate) fn resolve_conflict(
478    editor: WeakEntity<Editor>,
479    excerpt_id: ExcerptId,
480    resolved_conflict: ConflictRegion,
481    ranges: Vec<Range<Anchor>>,
482    window: &mut Window,
483    cx: &mut App,
484) -> Task<()> {
485    window.spawn(cx, async move |cx| {
486        let Some((workspace, project, multibuffer, buffer)) = editor
487            .update(cx, |editor, cx| {
488                let workspace = editor.workspace()?;
489                let project = editor.project.clone()?;
490                let multibuffer = editor.buffer().clone();
491                let buffer_id = resolved_conflict.ours.end.buffer_id?;
492                let buffer = multibuffer.read(cx).buffer(buffer_id)?;
493                resolved_conflict.resolve(buffer.clone(), &ranges, cx);
494                let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
495                let snapshot = multibuffer.read(cx).snapshot(cx);
496                let buffer_snapshot = buffer.read(cx).snapshot();
497                let state = conflict_addon
498                    .buffers
499                    .get_mut(&buffer_snapshot.remote_id())?;
500                let ix = state
501                    .block_ids
502                    .binary_search_by(|(range, _)| {
503                        range
504                            .start
505                            .cmp(&resolved_conflict.range.start, &buffer_snapshot)
506                    })
507                    .ok()?;
508                let &(_, block_id) = &state.block_ids[ix];
509                let start = snapshot
510                    .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
511                    .unwrap();
512                let end = snapshot
513                    .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
514                    .unwrap();
515                editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
516                editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
517                editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
518                editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
519                editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
520                editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
521                Some((workspace, project, multibuffer, buffer))
522            })
523            .ok()
524            .flatten()
525        else {
526            return;
527        };
528        let Some(save) = project
529            .update(cx, |project, cx| {
530                if multibuffer.read(cx).all_diff_hunks_expanded() {
531                    project.save_buffer(buffer.clone(), cx)
532                } else {
533                    Task::ready(Ok(()))
534                }
535            })
536            .ok()
537        else {
538            return;
539        };
540        if save.await.log_err().is_none() {
541            let open_path = maybe!({
542                let path = buffer
543                    .read_with(cx, |buffer, cx| buffer.project_path(cx))
544                    .ok()
545                    .flatten()?;
546                workspace
547                    .update_in(cx, |workspace, window, cx| {
548                        workspace.open_path_preview(path, None, false, false, false, window, cx)
549                    })
550                    .ok()
551            });
552
553            if let Some(open_path) = open_path {
554                open_path.await.log_err();
555            }
556        }
557    })
558}