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