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        })
301    }
302    let new_block_ids = editor.insert_blocks(blocks, None, cx);
303
304    let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
305    if let Some((buffer_conflicts, old_range)) =
306        conflict_addon.buffers.get_mut(&buffer_id).zip(old_range)
307    {
308        buffer_conflicts.block_ids.splice(
309            old_range,
310            new_conflicts
311                .iter()
312                .map(|conflict| conflict.range.clone())
313                .zip(new_block_ids),
314        );
315    }
316}
317
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) {
325    log::debug!("update conflict highlighting for {conflict:?}");
326    let theme = cx.theme().clone();
327    let colors = theme.colors();
328    let outer_start = buffer
329        .anchor_in_excerpt(excerpt_id, conflict.range.start)
330        .unwrap();
331    let outer_end = buffer
332        .anchor_in_excerpt(excerpt_id, conflict.range.end)
333        .unwrap();
334    let our_start = buffer
335        .anchor_in_excerpt(excerpt_id, conflict.ours.start)
336        .unwrap();
337    let our_end = buffer
338        .anchor_in_excerpt(excerpt_id, conflict.ours.end)
339        .unwrap();
340    let their_start = buffer
341        .anchor_in_excerpt(excerpt_id, conflict.theirs.start)
342        .unwrap();
343    let their_end = buffer
344        .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
345        .unwrap();
346
347    let ours_background = colors.version_control_conflict_ours_background;
348    let ours_marker = colors.version_control_conflict_ours_marker_background;
349    let theirs_background = colors.version_control_conflict_theirs_background;
350    let theirs_marker = colors.version_control_conflict_theirs_marker_background;
351    let divider_background = colors.version_control_conflict_divider_background;
352
353    let options = RowHighlightOptions {
354        include_gutter: false,
355        ..Default::default()
356    };
357
358    // Prevent diff hunk highlighting within the entire conflict region.
359    editor.highlight_rows::<ConflictsOuter>(
360        outer_start..outer_end,
361        divider_background,
362        options,
363        cx,
364    );
365    editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
366    editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
367    editor.highlight_rows::<ConflictsTheirs>(
368        their_start..their_end,
369        theirs_background,
370        options,
371        cx,
372    );
373    editor.highlight_rows::<ConflictsTheirsMarker>(
374        their_end..outer_end,
375        theirs_marker,
376        options,
377        cx,
378    );
379}
380
381fn render_conflict_buttons(
382    conflict: &ConflictRegion,
383    excerpt_id: ExcerptId,
384    editor: WeakEntity<Editor>,
385    cx: &mut BlockContext,
386) -> AnyElement {
387    h_flex()
388        .h(cx.line_height)
389        .items_end()
390        .ml(cx.gutter_dimensions.width)
391        .id(cx.block_id)
392        .gap_0p5()
393        .child(
394            div()
395                .id("ours")
396                .px_1()
397                .child("Take Ours")
398                .rounded_t(rems(0.2))
399                .text_ui_sm(cx)
400                .hover(|this| this.bg(cx.theme().colors().element_background))
401                .cursor_pointer()
402                .on_click({
403                    let editor = editor.clone();
404                    let conflict = conflict.clone();
405                    let ours = conflict.ours.clone();
406                    move |_, _, cx| {
407                        resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx)
408                    }
409                }),
410        )
411        .child(
412            div()
413                .id("theirs")
414                .px_1()
415                .child("Take Theirs")
416                .rounded_t(rems(0.2))
417                .text_ui_sm(cx)
418                .hover(|this| this.bg(cx.theme().colors().element_background))
419                .cursor_pointer()
420                .on_click({
421                    let editor = editor.clone();
422                    let conflict = conflict.clone();
423                    let theirs = conflict.theirs.clone();
424                    move |_, _, cx| {
425                        resolve_conflict(
426                            editor.clone(),
427                            excerpt_id,
428                            &conflict,
429                            &[theirs.clone()],
430                            cx,
431                        )
432                    }
433                }),
434        )
435        .child(
436            div()
437                .id("both")
438                .px_1()
439                .child("Take Both")
440                .rounded_t(rems(0.2))
441                .text_ui_sm(cx)
442                .hover(|this| this.bg(cx.theme().colors().element_background))
443                .cursor_pointer()
444                .on_click({
445                    let editor = editor.clone();
446                    let conflict = conflict.clone();
447                    let ours = conflict.ours.clone();
448                    let theirs = conflict.theirs.clone();
449                    move |_, _, cx| {
450                        resolve_conflict(
451                            editor.clone(),
452                            excerpt_id,
453                            &conflict,
454                            &[ours.clone(), theirs.clone()],
455                            cx,
456                        )
457                    }
458                }),
459        )
460        .into_any()
461}
462
463fn resolve_conflict(
464    editor: WeakEntity<Editor>,
465    excerpt_id: ExcerptId,
466    resolved_conflict: &ConflictRegion,
467    ranges: &[Range<Anchor>],
468    cx: &mut App,
469) {
470    let Some(editor) = editor.upgrade() else {
471        return;
472    };
473
474    let multibuffer = editor.read(cx).buffer().read(cx);
475    let snapshot = multibuffer.snapshot(cx);
476    let Some(buffer) = resolved_conflict
477        .ours
478        .end
479        .buffer_id
480        .and_then(|buffer_id| multibuffer.buffer(buffer_id))
481    else {
482        return;
483    };
484    let buffer_snapshot = buffer.read(cx).snapshot();
485
486    resolved_conflict.resolve(buffer, ranges, cx);
487
488    editor.update(cx, |editor, cx| {
489        let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
490        let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else {
491            return;
492        };
493        let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| {
494            range
495                .start
496                .cmp(&resolved_conflict.range.start, &buffer_snapshot)
497        }) else {
498            return;
499        };
500        let &(_, block_id) = &state.block_ids[ix];
501        let start = snapshot
502            .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
503            .unwrap();
504        let end = snapshot
505            .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
506            .unwrap();
507        editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
508        editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
509        editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
510        editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
511        editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
512        editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
513    })
514}