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