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};
 17
 18pub(crate) struct ConflictAddon {
 19    buffers: HashMap<BufferId, BufferConflicts>,
 20}
 21
 22impl ConflictAddon {
 23    pub(crate) fn conflict_set(&self, buffer_id: BufferId) -> Option<Entity<ConflictSet>> {
 24        self.buffers
 25            .get(&buffer_id)
 26            .map(|entry| entry.conflict_set.clone())
 27    }
 28}
 29
 30struct BufferConflicts {
 31    block_ids: Vec<(Range<Anchor>, CustomBlockId)>,
 32    conflict_set: Entity<ConflictSet>,
 33    _subscription: Subscription,
 34}
 35
 36impl editor::Addon for ConflictAddon {
 37    fn to_any(&self) -> &dyn std::any::Any {
 38        self
 39    }
 40
 41    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
 42        Some(self)
 43    }
 44}
 45
 46pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) {
 47    // Only show conflict UI for singletons and in the project diff.
 48    if !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().clone();
 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    conflicts_updated(
 93        editor,
 94        conflict_set,
 95        &ConflictSetUpdate {
 96            buffer_range: None,
 97            old_range: 0..conflicts_len,
 98            new_range: 0..conflicts_len,
 99        },
100        cx,
101    );
102}
103
104fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
105    let Some(project) = &editor.project else {
106        return;
107    };
108    let git_store = project.read(cx).git_store().clone();
109
110    let buffer_conflicts = editor
111        .addon_mut::<ConflictAddon>()
112        .unwrap()
113        .buffers
114        .entry(buffer.read(cx).remote_id())
115        .or_insert_with(|| {
116            let conflict_set = git_store.update(cx, |git_store, cx| {
117                git_store.open_conflict_set(buffer.clone(), cx)
118            });
119            let subscription = cx.subscribe(&conflict_set, conflicts_updated);
120            BufferConflicts {
121                block_ids: Vec::new(),
122                conflict_set: conflict_set.clone(),
123                _subscription: subscription,
124            }
125        });
126
127    let conflict_set = buffer_conflicts.conflict_set.clone();
128    let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
129    let addon_conflicts_len = buffer_conflicts.block_ids.len();
130    conflicts_updated(
131        editor,
132        conflict_set,
133        &ConflictSetUpdate {
134            buffer_range: None,
135            old_range: 0..addon_conflicts_len,
136            new_range: 0..conflicts_len,
137        },
138        cx,
139    );
140}
141
142fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
143    let mut removed_block_ids = HashSet::default();
144    editor
145        .addon_mut::<ConflictAddon>()
146        .unwrap()
147        .buffers
148        .retain(|buffer_id, buffer| {
149            if removed_buffer_ids.contains(&buffer_id) {
150                removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
151                false
152            } else {
153                true
154            }
155        });
156    editor.remove_blocks(removed_block_ids, None, cx);
157}
158
159fn conflicts_updated(
160    editor: &mut Editor,
161    conflict_set: Entity<ConflictSet>,
162    event: &ConflictSetUpdate,
163    cx: &mut Context<Editor>,
164) {
165    let buffer_id = conflict_set.read(cx).snapshot.buffer_id;
166    let conflict_set = conflict_set.read(cx).snapshot();
167    let multibuffer = editor.buffer().read(cx);
168    let snapshot = multibuffer.snapshot(cx);
169    let excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
170    let Some(buffer_snapshot) = excerpts
171        .first()
172        .and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
173    else {
174        return;
175    };
176
177    // Remove obsolete highlights and blocks
178    let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
179    if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
180        let old_conflicts = buffer_conflicts.block_ids[event.old_range.clone()].to_owned();
181        let mut removed_highlighted_ranges = Vec::new();
182        let mut removed_block_ids = HashSet::default();
183        for (conflict_range, block_id) in old_conflicts {
184            let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
185                let precedes_start = range
186                    .context
187                    .start
188                    .cmp(&conflict_range.start, &buffer_snapshot)
189                    .is_le();
190                let follows_end = range
191                    .context
192                    .end
193                    .cmp(&conflict_range.start, &buffer_snapshot)
194                    .is_ge();
195                precedes_start && follows_end
196            }) else {
197                continue;
198            };
199            let excerpt_id = *excerpt_id;
200            let Some(range) = snapshot
201                .anchor_in_excerpt(excerpt_id, conflict_range.start)
202                .zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
203                .map(|(start, end)| start..end)
204            else {
205                continue;
206            };
207            removed_highlighted_ranges.push(range.clone());
208            removed_block_ids.insert(block_id);
209        }
210
211        editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
212        editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
213        editor
214            .remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx);
215        editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx);
216        editor.remove_highlighted_rows::<ConflictsTheirsMarker>(
217            removed_highlighted_ranges.clone(),
218            cx,
219        );
220        editor.remove_blocks(removed_block_ids, None, cx);
221    }
222
223    // Add new highlights and blocks
224    let editor_handle = cx.weak_entity();
225    let new_conflicts = &conflict_set.conflicts[event.new_range.clone()];
226    let mut blocks = Vec::new();
227    for conflict in new_conflicts {
228        let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
229            let precedes_start = range
230                .context
231                .start
232                .cmp(&conflict.range.start, &buffer_snapshot)
233                .is_le();
234            let follows_end = range
235                .context
236                .end
237                .cmp(&conflict.range.start, &buffer_snapshot)
238                .is_ge();
239            precedes_start && follows_end
240        }) else {
241            continue;
242        };
243        let excerpt_id = *excerpt_id;
244
245        update_conflict_highlighting(editor, conflict, &snapshot, excerpt_id, cx);
246
247        let Some(anchor) = snapshot.anchor_in_excerpt(excerpt_id, conflict.range.start) else {
248            continue;
249        };
250
251        let editor_handle = editor_handle.clone();
252        blocks.push(BlockProperties {
253            placement: BlockPlacement::Above(anchor),
254            height: Some(1),
255            style: BlockStyle::Fixed,
256            render: Arc::new({
257                let conflict = conflict.clone();
258                move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
259            }),
260            priority: 0,
261        })
262    }
263    let new_block_ids = editor.insert_blocks(blocks, None, cx);
264
265    let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
266    if let Some(buffer_conflicts) = conflict_addon.buffers.get_mut(&buffer_id) {
267        buffer_conflicts.block_ids.splice(
268            event.old_range.clone(),
269            new_conflicts
270                .iter()
271                .map(|conflict| conflict.range.clone())
272                .zip(new_block_ids),
273        );
274    }
275}
276
277fn update_conflict_highlighting(
278    editor: &mut Editor,
279    conflict: &ConflictRegion,
280    buffer: &editor::MultiBufferSnapshot,
281    excerpt_id: editor::ExcerptId,
282    cx: &mut Context<Editor>,
283) {
284    log::debug!("update conflict highlighting for {conflict:?}");
285    let theme = cx.theme().clone();
286    let colors = theme.colors();
287    let outer_start = buffer
288        .anchor_in_excerpt(excerpt_id, conflict.range.start)
289        .unwrap();
290    let outer_end = buffer
291        .anchor_in_excerpt(excerpt_id, conflict.range.end)
292        .unwrap();
293    let our_start = buffer
294        .anchor_in_excerpt(excerpt_id, conflict.ours.start)
295        .unwrap();
296    let our_end = buffer
297        .anchor_in_excerpt(excerpt_id, conflict.ours.end)
298        .unwrap();
299    let their_start = buffer
300        .anchor_in_excerpt(excerpt_id, conflict.theirs.start)
301        .unwrap();
302    let their_end = buffer
303        .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
304        .unwrap();
305
306    let ours_background = colors.version_control_conflict_ours_background;
307    let ours_marker = colors.version_control_conflict_ours_marker_background;
308    let theirs_background = colors.version_control_conflict_theirs_background;
309    let theirs_marker = colors.version_control_conflict_theirs_marker_background;
310    let divider_background = colors.version_control_conflict_divider_background;
311
312    let options = RowHighlightOptions {
313        include_gutter: false,
314        ..Default::default()
315    };
316
317    // Prevent diff hunk highlighting within the entire conflict region.
318    editor.highlight_rows::<ConflictsOuter>(
319        outer_start..outer_end,
320        divider_background,
321        options,
322        cx,
323    );
324    editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
325    editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
326    editor.highlight_rows::<ConflictsTheirs>(
327        their_start..their_end,
328        theirs_background,
329        options,
330        cx,
331    );
332    editor.highlight_rows::<ConflictsTheirsMarker>(
333        their_end..outer_end,
334        theirs_marker,
335        options,
336        cx,
337    );
338}
339
340fn render_conflict_buttons(
341    conflict: &ConflictRegion,
342    excerpt_id: ExcerptId,
343    editor: WeakEntity<Editor>,
344    cx: &mut BlockContext,
345) -> AnyElement {
346    h_flex()
347        .h(cx.line_height)
348        .items_end()
349        .ml(cx.gutter_dimensions.width)
350        .id(cx.block_id)
351        .gap_0p5()
352        .child(
353            div()
354                .id("ours")
355                .px_1()
356                .child("Take Ours")
357                .rounded_t(rems(0.2))
358                .text_ui_sm(cx)
359                .hover(|this| this.bg(cx.theme().colors().element_background))
360                .cursor_pointer()
361                .on_click({
362                    let editor = editor.clone();
363                    let conflict = conflict.clone();
364                    let ours = conflict.ours.clone();
365                    move |_, _, cx| {
366                        resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx)
367                    }
368                }),
369        )
370        .child(
371            div()
372                .id("theirs")
373                .px_1()
374                .child("Take Theirs")
375                .rounded_t(rems(0.2))
376                .text_ui_sm(cx)
377                .hover(|this| this.bg(cx.theme().colors().element_background))
378                .cursor_pointer()
379                .on_click({
380                    let editor = editor.clone();
381                    let conflict = conflict.clone();
382                    let theirs = conflict.theirs.clone();
383                    move |_, _, cx| {
384                        resolve_conflict(
385                            editor.clone(),
386                            excerpt_id,
387                            &conflict,
388                            &[theirs.clone()],
389                            cx,
390                        )
391                    }
392                }),
393        )
394        .child(
395            div()
396                .id("both")
397                .px_1()
398                .child("Take Both")
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                    let theirs = conflict.theirs.clone();
408                    move |_, _, cx| {
409                        resolve_conflict(
410                            editor.clone(),
411                            excerpt_id,
412                            &conflict,
413                            &[ours.clone(), theirs.clone()],
414                            cx,
415                        )
416                    }
417                }),
418        )
419        .into_any()
420}
421
422fn resolve_conflict(
423    editor: WeakEntity<Editor>,
424    excerpt_id: ExcerptId,
425    resolved_conflict: &ConflictRegion,
426    ranges: &[Range<Anchor>],
427    cx: &mut App,
428) {
429    let Some(editor) = editor.upgrade() else {
430        return;
431    };
432
433    let multibuffer = editor.read(cx).buffer().read(cx);
434    let snapshot = multibuffer.snapshot(cx);
435    let Some(buffer) = resolved_conflict
436        .ours
437        .end
438        .buffer_id
439        .and_then(|buffer_id| multibuffer.buffer(buffer_id))
440    else {
441        return;
442    };
443    let buffer_snapshot = buffer.read(cx).snapshot();
444
445    resolved_conflict.resolve(buffer, ranges, cx);
446
447    editor.update(cx, |editor, cx| {
448        let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
449        let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else {
450            return;
451        };
452        let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| {
453            range
454                .start
455                .cmp(&resolved_conflict.range.start, &buffer_snapshot)
456        }) else {
457            return;
458        };
459        let &(_, block_id) = &state.block_ids[ix];
460        let start = snapshot
461            .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
462            .unwrap();
463        let end = snapshot
464            .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
465            .unwrap();
466        editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
467        editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
468        editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
469        editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
470        editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
471        editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
472    })
473}