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