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        || editor.read_only(cx)
 51    {
 52        return;
 53    }
 54
 55    editor.register_addon(ConflictAddon {
 56        buffers: Default::default(),
 57    });
 58
 59    let buffers = buffer.read(cx).all_buffers();
 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
115#[ztracing::instrument(skip_all)]
116fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
117    let Some(project) = editor.project() else {
118        return;
119    };
120    let git_store = project.read(cx).git_store().clone();
121
122    let buffer_conflicts = editor
123        .addon_mut::<ConflictAddon>()
124        .unwrap()
125        .buffers
126        .entry(buffer.read(cx).remote_id())
127        .or_insert_with(|| {
128            let conflict_set = git_store.update(cx, |git_store, cx| {
129                git_store.open_conflict_set(buffer.clone(), cx)
130            });
131            let subscription = cx.subscribe(&conflict_set, conflicts_updated);
132            BufferConflicts {
133                block_ids: Vec::new(),
134                conflict_set,
135                _subscription: subscription,
136            }
137        });
138
139    let conflict_set = buffer_conflicts.conflict_set.clone();
140    let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
141    let addon_conflicts_len = buffer_conflicts.block_ids.len();
142    conflicts_updated(
143        editor,
144        conflict_set,
145        &ConflictSetUpdate {
146            buffer_range: None,
147            old_range: 0..addon_conflicts_len,
148            new_range: 0..conflicts_len,
149        },
150        cx,
151    );
152}
153
154fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
155    let mut removed_block_ids = HashSet::default();
156    editor
157        .addon_mut::<ConflictAddon>()
158        .unwrap()
159        .buffers
160        .retain(|buffer_id, buffer| {
161            if removed_buffer_ids.contains(buffer_id) {
162                removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
163                false
164            } else {
165                true
166            }
167        });
168    editor.remove_blocks(removed_block_ids, None, cx);
169}
170
171#[ztracing::instrument(skip_all)]
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.anchor_range_in_excerpt(excerpt_id, conflict_range) else {
241                continue;
242            };
243            removed_highlighted_ranges.push(range.clone());
244            removed_block_ids.insert(block_id);
245        }
246
247        editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
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::Sticky,
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
317#[ztracing::instrument(skip_all)]
318fn update_conflict_highlighting(
319    editor: &mut Editor,
320    conflict: &ConflictRegion,
321    buffer: &editor::MultiBufferSnapshot,
322    excerpt_id: editor::ExcerptId,
323    cx: &mut Context<Editor>,
324) -> Option<()> {
325    log::debug!("update conflict highlighting for {conflict:?}");
326
327    let outer = buffer.anchor_range_in_excerpt(excerpt_id, conflict.range.clone())?;
328    let ours = buffer.anchor_range_in_excerpt(excerpt_id, conflict.ours.clone())?;
329    let theirs = buffer.anchor_range_in_excerpt(excerpt_id, conflict.theirs.clone())?;
330
331    let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
332    let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
333
334    let options = RowHighlightOptions {
335        include_gutter: true,
336        ..Default::default()
337    };
338
339    editor.insert_gutter_highlight::<ConflictsOuter>(
340        outer.start..theirs.end,
341        |cx| cx.theme().colors().editor_background,
342        cx,
343    );
344
345    // Prevent diff hunk highlighting within the entire conflict region.
346    editor.highlight_rows::<ConflictsOuter>(outer.clone(), theirs_background, options, cx);
347    editor.highlight_rows::<ConflictsOurs>(ours.clone(), ours_background, options, cx);
348    editor.highlight_rows::<ConflictsOursMarker>(
349        outer.start..ours.start,
350        ours_background,
351        options,
352        cx,
353    );
354    editor.highlight_rows::<ConflictsTheirs>(theirs.clone(), theirs_background, options, cx);
355    editor.highlight_rows::<ConflictsTheirsMarker>(
356        theirs.end..outer.end,
357        theirs_background,
358        options,
359        cx,
360    );
361
362    Some(())
363}
364
365fn render_conflict_buttons(
366    conflict: &ConflictRegion,
367    excerpt_id: ExcerptId,
368    editor: WeakEntity<Editor>,
369    cx: &mut BlockContext,
370) -> AnyElement {
371    h_flex()
372        .id(cx.block_id)
373        .h(cx.line_height)
374        .ml(cx.margins.gutter.width)
375        .items_end()
376        .gap_1()
377        .bg(cx.theme().colors().editor_background)
378        .child(
379            Button::new("head", format!("Use {}", conflict.ours_branch_name))
380                .label_size(LabelSize::Small)
381                .on_click({
382                    let editor = editor.clone();
383                    let conflict = conflict.clone();
384                    let ours = conflict.ours.clone();
385                    move |_, window, cx| {
386                        resolve_conflict(
387                            editor.clone(),
388                            excerpt_id,
389                            conflict.clone(),
390                            vec![ours.clone()],
391                            window,
392                            cx,
393                        )
394                        .detach()
395                    }
396                }),
397        )
398        .child(
399            Button::new("origin", format!("Use {}", conflict.theirs_branch_name))
400                .label_size(LabelSize::Small)
401                .on_click({
402                    let editor = editor.clone();
403                    let conflict = conflict.clone();
404                    let theirs = conflict.theirs.clone();
405                    move |_, window, cx| {
406                        resolve_conflict(
407                            editor.clone(),
408                            excerpt_id,
409                            conflict.clone(),
410                            vec![theirs.clone()],
411                            window,
412                            cx,
413                        )
414                        .detach()
415                    }
416                }),
417        )
418        .child(
419            Button::new("both", "Use Both")
420                .label_size(LabelSize::Small)
421                .on_click({
422                    let conflict = conflict.clone();
423                    let ours = conflict.ours.clone();
424                    let theirs = conflict.theirs.clone();
425                    move |_, window, cx| {
426                        resolve_conflict(
427                            editor.clone(),
428                            excerpt_id,
429                            conflict.clone(),
430                            vec![ours.clone(), theirs.clone()],
431                            window,
432                            cx,
433                        )
434                        .detach()
435                    }
436                }),
437        )
438        .into_any()
439}
440
441pub(crate) fn resolve_conflict(
442    editor: WeakEntity<Editor>,
443    excerpt_id: ExcerptId,
444    resolved_conflict: ConflictRegion,
445    ranges: Vec<Range<Anchor>>,
446    window: &mut Window,
447    cx: &mut App,
448) -> Task<()> {
449    window.spawn(cx, async move |cx| {
450        let Some((workspace, project, multibuffer, buffer)) = editor
451            .update(cx, |editor, cx| {
452                let workspace = editor.workspace()?;
453                let project = editor.project()?.clone();
454                let multibuffer = editor.buffer().clone();
455                let buffer_id = resolved_conflict.ours.end.buffer_id?;
456                let buffer = multibuffer.read(cx).buffer(buffer_id)?;
457                resolved_conflict.resolve(buffer.clone(), &ranges, cx);
458                let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
459                let snapshot = multibuffer.read(cx).snapshot(cx);
460                let buffer_snapshot = buffer.read(cx).snapshot();
461                let state = conflict_addon
462                    .buffers
463                    .get_mut(&buffer_snapshot.remote_id())?;
464                let ix = state
465                    .block_ids
466                    .binary_search_by(|(range, _)| {
467                        range
468                            .start
469                            .cmp(&resolved_conflict.range.start, &buffer_snapshot)
470                    })
471                    .ok()?;
472                let &(_, block_id) = &state.block_ids[ix];
473                let range =
474                    snapshot.anchor_range_in_excerpt(excerpt_id, resolved_conflict.range)?;
475
476                editor.remove_gutter_highlights::<ConflictsOuter>(vec![range.clone()], cx);
477
478                editor.remove_highlighted_rows::<ConflictsOuter>(vec![range.clone()], cx);
479                editor.remove_highlighted_rows::<ConflictsOurs>(vec![range.clone()], cx);
480                editor.remove_highlighted_rows::<ConflictsTheirs>(vec![range.clone()], cx);
481                editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![range.clone()], cx);
482                editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![range], cx);
483                editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
484                Some((workspace, project, multibuffer, buffer))
485            })
486            .ok()
487            .flatten()
488        else {
489            return;
490        };
491        let save = project.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        if save.await.log_err().is_none() {
499            let open_path = maybe!({
500                let path = buffer.read_with(cx, |buffer, cx| buffer.project_path(cx))?;
501                workspace
502                    .update_in(cx, |workspace, window, cx| {
503                        workspace.open_path_preview(path, None, false, false, false, window, cx)
504                    })
505                    .ok()
506            });
507
508            if let Some(open_path) = open_path {
509                open_path.await.log_err();
510            }
511        }
512    })
513}