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.buffer().read(cx).is_singleton()
 50        && !editor.buffer().read(cx).all_diff_hunks_expanded()
 51    {
 52        return;
 53    }
 54
 55    editor.register_addon(ConflictAddon {
 56        buffers: Default::default(),
 57    });
 58
 59    let buffers = buffer.read(cx).all_buffers().clone();
 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
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: conflict_set.clone(),
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
170fn conflicts_updated(
171    editor: &mut Editor,
172    conflict_set: Entity<ConflictSet>,
173    event: &ConflictSetUpdate,
174    cx: &mut Context<Editor>,
175) {
176    let buffer_id = conflict_set.read(cx).snapshot.buffer_id;
177    let conflict_set = conflict_set.read(cx).snapshot();
178    let multibuffer = editor.buffer().read(cx);
179    let snapshot = multibuffer.snapshot(cx);
180    let excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
181    let Some(buffer_snapshot) = excerpts
182        .first()
183        .and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
184    else {
185        return;
186    };
187
188    let old_range = maybe!({
189        let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
190        let buffer_conflicts = conflict_addon.buffers.get(&buffer_id)?;
191        match buffer_conflicts.block_ids.get(event.old_range.clone()) {
192            Some(_) => Some(event.old_range.clone()),
193            None => {
194                debug_panic!(
195                    "conflicts updated event old range is invalid for buffer conflicts view (block_ids len is {:?}, old_range is {:?})",
196                    buffer_conflicts.block_ids.len(),
197                    event.old_range,
198                );
199                if event.old_range.start <= event.old_range.end {
200                    Some(
201                        event.old_range.start.min(buffer_conflicts.block_ids.len())
202                            ..event.old_range.end.min(buffer_conflicts.block_ids.len()),
203                    )
204                } else {
205                    None
206                }
207            }
208        }
209    });
210
211    // Remove obsolete highlights and blocks
212    let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
213    if let Some((buffer_conflicts, old_range)) = conflict_addon
214        .buffers
215        .get_mut(&buffer_id)
216        .zip(old_range.clone())
217    {
218        let old_conflicts = buffer_conflicts.block_ids[old_range].to_owned();
219        let mut removed_highlighted_ranges = Vec::new();
220        let mut removed_block_ids = HashSet::default();
221        for (conflict_range, block_id) in old_conflicts {
222            let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
223                let precedes_start = range
224                    .context
225                    .start
226                    .cmp(&conflict_range.start, &buffer_snapshot)
227                    .is_le();
228                let follows_end = range
229                    .context
230                    .end
231                    .cmp(&conflict_range.start, &buffer_snapshot)
232                    .is_ge();
233                precedes_start && follows_end
234            }) else {
235                continue;
236            };
237            let excerpt_id = *excerpt_id;
238            let Some(range) = snapshot
239                .anchor_in_excerpt(excerpt_id, conflict_range.start)
240                .zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
241                .map(|(start, end)| start..end)
242            else {
243                continue;
244            };
245            removed_highlighted_ranges.push(range.clone());
246            removed_block_ids.insert(block_id);
247        }
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::Fixed,
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
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) {
324    log::debug!("update conflict highlighting for {conflict:?}");
325    let theme = cx.theme().clone();
326    let colors = theme.colors();
327    let outer_start = buffer
328        .anchor_in_excerpt(excerpt_id, conflict.range.start)
329        .unwrap();
330    let outer_end = buffer
331        .anchor_in_excerpt(excerpt_id, conflict.range.end)
332        .unwrap();
333    let our_start = buffer
334        .anchor_in_excerpt(excerpt_id, conflict.ours.start)
335        .unwrap();
336    let our_end = buffer
337        .anchor_in_excerpt(excerpt_id, conflict.ours.end)
338        .unwrap();
339    let their_start = buffer
340        .anchor_in_excerpt(excerpt_id, conflict.theirs.start)
341        .unwrap();
342    let their_end = buffer
343        .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
344        .unwrap();
345
346    let ours_background = colors.version_control_conflict_ours_background;
347    let ours_marker = colors.version_control_conflict_ours_marker_background;
348    let theirs_background = colors.version_control_conflict_theirs_background;
349    let theirs_marker = colors.version_control_conflict_theirs_marker_background;
350    let divider_background = colors.version_control_conflict_divider_background;
351
352    let options = RowHighlightOptions {
353        include_gutter: false,
354        ..Default::default()
355    };
356
357    // Prevent diff hunk highlighting within the entire conflict region.
358    editor.highlight_rows::<ConflictsOuter>(
359        outer_start..outer_end,
360        divider_background,
361        options,
362        cx,
363    );
364    editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
365    editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
366    editor.highlight_rows::<ConflictsTheirs>(
367        their_start..their_end,
368        theirs_background,
369        options,
370        cx,
371    );
372    editor.highlight_rows::<ConflictsTheirsMarker>(
373        their_end..outer_end,
374        theirs_marker,
375        options,
376        cx,
377    );
378}
379
380fn render_conflict_buttons(
381    conflict: &ConflictRegion,
382    excerpt_id: ExcerptId,
383    editor: WeakEntity<Editor>,
384    cx: &mut BlockContext,
385) -> AnyElement {
386    h_flex()
387        .h(cx.line_height)
388        .items_end()
389        .ml(cx.gutter_dimensions.width)
390        .id(cx.block_id)
391        .gap_0p5()
392        .child(
393            div()
394                .id("ours")
395                .px_1()
396                .child("Take Ours")
397                .rounded_t(rems(0.2))
398                .text_ui_sm(cx)
399                .hover(|this| this.bg(cx.theme().colors().element_background))
400                .cursor_pointer()
401                .on_click({
402                    let editor = editor.clone();
403                    let conflict = conflict.clone();
404                    let ours = conflict.ours.clone();
405                    move |_, _, cx| {
406                        resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx)
407                    }
408                }),
409        )
410        .child(
411            div()
412                .id("theirs")
413                .px_1()
414                .child("Take Theirs")
415                .rounded_t(rems(0.2))
416                .text_ui_sm(cx)
417                .hover(|this| this.bg(cx.theme().colors().element_background))
418                .cursor_pointer()
419                .on_click({
420                    let editor = editor.clone();
421                    let conflict = conflict.clone();
422                    let theirs = conflict.theirs.clone();
423                    move |_, _, cx| {
424                        resolve_conflict(
425                            editor.clone(),
426                            excerpt_id,
427                            &conflict,
428                            &[theirs.clone()],
429                            cx,
430                        )
431                    }
432                }),
433        )
434        .child(
435            div()
436                .id("both")
437                .px_1()
438                .child("Take Both")
439                .rounded_t(rems(0.2))
440                .text_ui_sm(cx)
441                .hover(|this| this.bg(cx.theme().colors().element_background))
442                .cursor_pointer()
443                .on_click({
444                    let editor = editor.clone();
445                    let conflict = conflict.clone();
446                    let ours = conflict.ours.clone();
447                    let theirs = conflict.theirs.clone();
448                    move |_, _, cx| {
449                        resolve_conflict(
450                            editor.clone(),
451                            excerpt_id,
452                            &conflict,
453                            &[ours.clone(), theirs.clone()],
454                            cx,
455                        )
456                    }
457                }),
458        )
459        .into_any()
460}
461
462fn resolve_conflict(
463    editor: WeakEntity<Editor>,
464    excerpt_id: ExcerptId,
465    resolved_conflict: &ConflictRegion,
466    ranges: &[Range<Anchor>],
467    cx: &mut App,
468) {
469    let Some(editor) = editor.upgrade() else {
470        return;
471    };
472
473    let multibuffer = editor.read(cx).buffer().read(cx);
474    let snapshot = multibuffer.snapshot(cx);
475    let Some(buffer) = resolved_conflict
476        .ours
477        .end
478        .buffer_id
479        .and_then(|buffer_id| multibuffer.buffer(buffer_id))
480    else {
481        return;
482    };
483    let buffer_snapshot = buffer.read(cx).snapshot();
484
485    resolved_conflict.resolve(buffer, ranges, cx);
486
487    editor.update(cx, |editor, cx| {
488        let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
489        let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else {
490            return;
491        };
492        let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| {
493            range
494                .start
495                .cmp(&resolved_conflict.range.start, &buffer_snapshot)
496        }) else {
497            return;
498        };
499        let &(_, block_id) = &state.block_ids[ix];
500        let start = snapshot
501            .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
502            .unwrap();
503        let end = snapshot
504            .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
505            .unwrap();
506        editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
507        editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
508        editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
509        editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
510        editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
511        editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
512    })
513}