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    {
 51        return;
 52    }
 53
 54    editor.register_addon(ConflictAddon {
 55        buffers: Default::default(),
 56    });
 57
 58    let buffers = buffer.read(cx).all_buffers();
 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    let buffer_id = conflict_set.read(cx).snapshot().buffer_id;
 93    let Some(buffer_conflicts) = editor
 94        .addon_mut::<ConflictAddon>()
 95        .unwrap()
 96        .buffers
 97        .get(&buffer_id)
 98    else {
 99        return;
100    };
101    let addon_conflicts_len = buffer_conflicts.block_ids.len();
102    conflicts_updated(
103        editor,
104        conflict_set,
105        &ConflictSetUpdate {
106            buffer_range: None,
107            old_range: 0..addon_conflicts_len,
108            new_range: 0..conflicts_len,
109        },
110        cx,
111    );
112}
113
114fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
115    let Some(project) = editor.project() else {
116        return;
117    };
118    let git_store = project.read(cx).git_store().clone();
119
120    let buffer_conflicts = editor
121        .addon_mut::<ConflictAddon>()
122        .unwrap()
123        .buffers
124        .entry(buffer.read(cx).remote_id())
125        .or_insert_with(|| {
126            let conflict_set = git_store.update(cx, |git_store, cx| {
127                git_store.open_conflict_set(buffer.clone(), cx)
128            });
129            let subscription = cx.subscribe(&conflict_set, conflicts_updated);
130            BufferConflicts {
131                block_ids: Vec::new(),
132                conflict_set,
133                _subscription: subscription,
134            }
135        });
136
137    let conflict_set = buffer_conflicts.conflict_set.clone();
138    let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
139    let addon_conflicts_len = buffer_conflicts.block_ids.len();
140    conflicts_updated(
141        editor,
142        conflict_set,
143        &ConflictSetUpdate {
144            buffer_range: None,
145            old_range: 0..addon_conflicts_len,
146            new_range: 0..conflicts_len,
147        },
148        cx,
149    );
150}
151
152fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
153    let mut removed_block_ids = HashSet::default();
154    editor
155        .addon_mut::<ConflictAddon>()
156        .unwrap()
157        .buffers
158        .retain(|buffer_id, buffer| {
159            if removed_buffer_ids.contains(buffer_id) {
160                removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
161                false
162            } else {
163                true
164            }
165        });
166    editor.remove_blocks(removed_block_ids, None, cx);
167}
168
169fn conflicts_updated(
170    editor: &mut Editor,
171    conflict_set: Entity<ConflictSet>,
172    event: &ConflictSetUpdate,
173    cx: &mut Context<Editor>,
174) {
175    let buffer_id = conflict_set.read(cx).snapshot.buffer_id;
176    let conflict_set = conflict_set.read(cx).snapshot();
177    let multibuffer = editor.buffer().read(cx);
178    let snapshot = multibuffer.snapshot(cx);
179    let excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
180    let Some(buffer_snapshot) = excerpts
181        .first()
182        .and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
183    else {
184        return;
185    };
186
187    let old_range = maybe!({
188        let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
189        let buffer_conflicts = conflict_addon.buffers.get(&buffer_id)?;
190        match buffer_conflicts.block_ids.get(event.old_range.clone()) {
191            Some(_) => Some(event.old_range.clone()),
192            None => {
193                debug_panic!(
194                    "conflicts updated event old range is invalid for buffer conflicts view (block_ids len is {:?}, old_range is {:?})",
195                    buffer_conflicts.block_ids.len(),
196                    event.old_range,
197                );
198                if event.old_range.start <= event.old_range.end {
199                    Some(
200                        event.old_range.start.min(buffer_conflicts.block_ids.len())
201                            ..event.old_range.end.min(buffer_conflicts.block_ids.len()),
202                    )
203                } else {
204                    None
205                }
206            }
207        }
208    });
209
210    // Remove obsolete highlights and blocks
211    let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
212    if let Some((buffer_conflicts, old_range)) = conflict_addon
213        .buffers
214        .get_mut(&buffer_id)
215        .zip(old_range.clone())
216    {
217        let old_conflicts = buffer_conflicts.block_ids[old_range].to_owned();
218        let mut removed_highlighted_ranges = Vec::new();
219        let mut removed_block_ids = HashSet::default();
220        for (conflict_range, block_id) in old_conflicts {
221            let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
222                let precedes_start = range
223                    .context
224                    .start
225                    .cmp(&conflict_range.start, buffer_snapshot)
226                    .is_le();
227                let follows_end = range
228                    .context
229                    .end
230                    .cmp(&conflict_range.start, buffer_snapshot)
231                    .is_ge();
232                precedes_start && follows_end
233            }) else {
234                continue;
235            };
236            let excerpt_id = *excerpt_id;
237            let Some(range) = snapshot
238                .anchor_in_excerpt(excerpt_id, conflict_range.start)
239                .zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
240                .map(|(start, end)| start..end)
241            else {
242                continue;
243            };
244            removed_highlighted_ranges.push(range.clone());
245            removed_block_ids.insert(block_id);
246        }
247
248        editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
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
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 = cx.theme().colors().version_control_conflict_marker_ours;
347    let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
348
349    let options = RowHighlightOptions {
350        include_gutter: true,
351        ..Default::default()
352    };
353
354    editor.insert_gutter_highlight::<ConflictsOuter>(
355        outer_start..their_end,
356        |cx| cx.theme().colors().editor_background,
357        cx,
358    );
359
360    // Prevent diff hunk highlighting within the entire conflict region.
361    editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
362    editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
363    editor.highlight_rows::<ConflictsOursMarker>(
364        outer_start..our_start,
365        ours_background,
366        options,
367        cx,
368    );
369    editor.highlight_rows::<ConflictsTheirs>(
370        their_start..their_end,
371        theirs_background,
372        options,
373        cx,
374    );
375    editor.highlight_rows::<ConflictsTheirsMarker>(
376        their_end..outer_end,
377        theirs_background,
378        options,
379        cx,
380    );
381}
382
383fn render_conflict_buttons(
384    conflict: &ConflictRegion,
385    excerpt_id: ExcerptId,
386    editor: WeakEntity<Editor>,
387    cx: &mut BlockContext,
388) -> AnyElement {
389    h_flex()
390        .id(cx.block_id)
391        .h(cx.line_height)
392        .ml(cx.margins.gutter.width)
393        .items_end()
394        .gap_1()
395        .bg(cx.theme().colors().editor_background)
396        .child(
397            Button::new("head", "Use HEAD")
398                .label_size(LabelSize::Small)
399                .on_click({
400                    let editor = editor.clone();
401                    let conflict = conflict.clone();
402                    let ours = conflict.ours.clone();
403                    move |_, window, cx| {
404                        resolve_conflict(
405                            editor.clone(),
406                            excerpt_id,
407                            conflict.clone(),
408                            vec![ours.clone()],
409                            window,
410                            cx,
411                        )
412                        .detach()
413                    }
414                }),
415        )
416        .child(
417            Button::new("origin", "Use Origin")
418                .label_size(LabelSize::Small)
419                .on_click({
420                    let editor = editor.clone();
421                    let conflict = conflict.clone();
422                    let theirs = conflict.theirs.clone();
423                    move |_, window, cx| {
424                        resolve_conflict(
425                            editor.clone(),
426                            excerpt_id,
427                            conflict.clone(),
428                            vec![theirs.clone()],
429                            window,
430                            cx,
431                        )
432                        .detach()
433                    }
434                }),
435        )
436        .child(
437            Button::new("both", "Use Both")
438                .label_size(LabelSize::Small)
439                .on_click({
440                    let conflict = conflict.clone();
441                    let ours = conflict.ours.clone();
442                    let theirs = conflict.theirs.clone();
443                    move |_, window, cx| {
444                        resolve_conflict(
445                            editor.clone(),
446                            excerpt_id,
447                            conflict.clone(),
448                            vec![ours.clone(), theirs.clone()],
449                            window,
450                            cx,
451                        )
452                        .detach()
453                    }
454                }),
455        )
456        .into_any()
457}
458
459pub(crate) fn resolve_conflict(
460    editor: WeakEntity<Editor>,
461    excerpt_id: ExcerptId,
462    resolved_conflict: ConflictRegion,
463    ranges: Vec<Range<Anchor>>,
464    window: &mut Window,
465    cx: &mut App,
466) -> Task<()> {
467    window.spawn(cx, async move |cx| {
468        let Some((workspace, project, multibuffer, buffer)) = editor
469            .update(cx, |editor, cx| {
470                let workspace = editor.workspace()?;
471                let project = editor.project()?.clone();
472                let multibuffer = editor.buffer().clone();
473                let buffer_id = resolved_conflict.ours.end.buffer_id?;
474                let buffer = multibuffer.read(cx).buffer(buffer_id)?;
475                resolved_conflict.resolve(buffer.clone(), &ranges, cx);
476                let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
477                let snapshot = multibuffer.read(cx).snapshot(cx);
478                let buffer_snapshot = buffer.read(cx).snapshot();
479                let state = conflict_addon
480                    .buffers
481                    .get_mut(&buffer_snapshot.remote_id())?;
482                let ix = state
483                    .block_ids
484                    .binary_search_by(|(range, _)| {
485                        range
486                            .start
487                            .cmp(&resolved_conflict.range.start, &buffer_snapshot)
488                    })
489                    .ok()?;
490                let &(_, block_id) = &state.block_ids[ix];
491                let start = snapshot
492                    .anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
493                    .unwrap();
494                let end = snapshot
495                    .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
496                    .unwrap();
497
498                editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx);
499
500                editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
501                editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
502                editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
503                editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
504                editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
505                editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
506                Some((workspace, project, multibuffer, buffer))
507            })
508            .ok()
509            .flatten()
510        else {
511            return;
512        };
513        let Some(save) = project
514            .update(cx, |project, cx| {
515                if multibuffer.read(cx).all_diff_hunks_expanded() {
516                    project.save_buffer(buffer.clone(), cx)
517                } else {
518                    Task::ready(Ok(()))
519                }
520            })
521            .ok()
522        else {
523            return;
524        };
525        if save.await.log_err().is_none() {
526            let open_path = maybe!({
527                let path = buffer
528                    .read_with(cx, |buffer, cx| buffer.project_path(cx))
529                    .ok()
530                    .flatten()?;
531                workspace
532                    .update_in(cx, |workspace, window, cx| {
533                        workspace.open_path_preview(path, None, false, false, false, window, cx)
534                    })
535                    .ok()
536            });
537
538            if let Some(open_path) = open_path {
539                open_path.await.log_err();
540            }
541        }
542    })
543}