
Following feedback that "Take Ours" and "Take Theirs" was confusing, leading to users not knowing what exactly happened with each of these buttons. It's now "Use HEAD" and "Use Origin", which also match what is written in Git markers, helping parse them out more easily. Future improvement is to have the actual branch target name in the "Use Origin" button. Release Notes: - git: Improved merge conflict buttons clarity by changing labels to "Use HEAD" and "Use Origin".
545 lines
19 KiB
Rust
545 lines
19 KiB
Rust
use collections::{HashMap, HashSet};
|
|
use editor::{
|
|
ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs, ConflictsTheirsMarker,
|
|
Editor, EditorEvent, ExcerptId, MultiBuffer, RowHighlightOptions,
|
|
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
|
|
};
|
|
use gpui::{
|
|
App, Context, Entity, InteractiveElement as _, ParentElement as _, Subscription, Task,
|
|
WeakEntity,
|
|
};
|
|
use language::{Anchor, Buffer, BufferId};
|
|
use project::{ConflictRegion, ConflictSet, ConflictSetUpdate, ProjectItem as _};
|
|
use std::{ops::Range, sync::Arc};
|
|
use ui::{ActiveTheme, Element as _, Styled, Window, prelude::*};
|
|
use util::{ResultExt as _, debug_panic, maybe};
|
|
|
|
pub(crate) struct ConflictAddon {
|
|
buffers: HashMap<BufferId, BufferConflicts>,
|
|
}
|
|
|
|
impl ConflictAddon {
|
|
pub(crate) fn conflict_set(&self, buffer_id: BufferId) -> Option<Entity<ConflictSet>> {
|
|
self.buffers
|
|
.get(&buffer_id)
|
|
.map(|entry| entry.conflict_set.clone())
|
|
}
|
|
}
|
|
|
|
struct BufferConflicts {
|
|
block_ids: Vec<(Range<Anchor>, CustomBlockId)>,
|
|
conflict_set: Entity<ConflictSet>,
|
|
_subscription: Subscription,
|
|
}
|
|
|
|
impl editor::Addon for ConflictAddon {
|
|
fn to_any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
|
|
Some(self)
|
|
}
|
|
}
|
|
|
|
pub fn register_editor(editor: &mut Editor, buffer: Entity<MultiBuffer>, cx: &mut Context<Editor>) {
|
|
// Only show conflict UI for singletons and in the project diff.
|
|
if !editor.mode().is_full()
|
|
|| (!editor.buffer().read(cx).is_singleton()
|
|
&& !editor.buffer().read(cx).all_diff_hunks_expanded())
|
|
{
|
|
return;
|
|
}
|
|
|
|
editor.register_addon(ConflictAddon {
|
|
buffers: Default::default(),
|
|
});
|
|
|
|
let buffers = buffer.read(cx).all_buffers().clone();
|
|
for buffer in buffers {
|
|
buffer_added(editor, buffer, cx);
|
|
}
|
|
|
|
cx.subscribe(&cx.entity(), |editor, _, event, cx| match event {
|
|
EditorEvent::ExcerptsAdded { buffer, .. } => buffer_added(editor, buffer.clone(), cx),
|
|
EditorEvent::ExcerptsExpanded { ids } => {
|
|
let multibuffer = editor.buffer().read(cx).snapshot(cx);
|
|
for excerpt_id in ids {
|
|
let Some(buffer) = multibuffer.buffer_for_excerpt(*excerpt_id) else {
|
|
continue;
|
|
};
|
|
let addon = editor.addon::<ConflictAddon>().unwrap();
|
|
let Some(conflict_set) = addon.conflict_set(buffer.remote_id()).clone() else {
|
|
return;
|
|
};
|
|
excerpt_for_buffer_updated(editor, conflict_set, cx);
|
|
}
|
|
}
|
|
EditorEvent::ExcerptsRemoved {
|
|
removed_buffer_ids, ..
|
|
} => buffers_removed(editor, removed_buffer_ids, cx),
|
|
_ => {}
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
fn excerpt_for_buffer_updated(
|
|
editor: &mut Editor,
|
|
conflict_set: Entity<ConflictSet>,
|
|
cx: &mut Context<Editor>,
|
|
) {
|
|
let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
|
|
let buffer_id = conflict_set.read(cx).snapshot().buffer_id;
|
|
let Some(buffer_conflicts) = editor
|
|
.addon_mut::<ConflictAddon>()
|
|
.unwrap()
|
|
.buffers
|
|
.get(&buffer_id)
|
|
else {
|
|
return;
|
|
};
|
|
let addon_conflicts_len = buffer_conflicts.block_ids.len();
|
|
conflicts_updated(
|
|
editor,
|
|
conflict_set,
|
|
&ConflictSetUpdate {
|
|
buffer_range: None,
|
|
old_range: 0..addon_conflicts_len,
|
|
new_range: 0..conflicts_len,
|
|
},
|
|
cx,
|
|
);
|
|
}
|
|
|
|
fn buffer_added(editor: &mut Editor, buffer: Entity<Buffer>, cx: &mut Context<Editor>) {
|
|
let Some(project) = &editor.project else {
|
|
return;
|
|
};
|
|
let git_store = project.read(cx).git_store().clone();
|
|
|
|
let buffer_conflicts = editor
|
|
.addon_mut::<ConflictAddon>()
|
|
.unwrap()
|
|
.buffers
|
|
.entry(buffer.read(cx).remote_id())
|
|
.or_insert_with(|| {
|
|
let conflict_set = git_store.update(cx, |git_store, cx| {
|
|
git_store.open_conflict_set(buffer.clone(), cx)
|
|
});
|
|
let subscription = cx.subscribe(&conflict_set, conflicts_updated);
|
|
BufferConflicts {
|
|
block_ids: Vec::new(),
|
|
conflict_set: conflict_set.clone(),
|
|
_subscription: subscription,
|
|
}
|
|
});
|
|
|
|
let conflict_set = buffer_conflicts.conflict_set.clone();
|
|
let conflicts_len = conflict_set.read(cx).snapshot().conflicts.len();
|
|
let addon_conflicts_len = buffer_conflicts.block_ids.len();
|
|
conflicts_updated(
|
|
editor,
|
|
conflict_set,
|
|
&ConflictSetUpdate {
|
|
buffer_range: None,
|
|
old_range: 0..addon_conflicts_len,
|
|
new_range: 0..conflicts_len,
|
|
},
|
|
cx,
|
|
);
|
|
}
|
|
|
|
fn buffers_removed(editor: &mut Editor, removed_buffer_ids: &[BufferId], cx: &mut Context<Editor>) {
|
|
let mut removed_block_ids = HashSet::default();
|
|
editor
|
|
.addon_mut::<ConflictAddon>()
|
|
.unwrap()
|
|
.buffers
|
|
.retain(|buffer_id, buffer| {
|
|
if removed_buffer_ids.contains(&buffer_id) {
|
|
removed_block_ids.extend(buffer.block_ids.iter().map(|(_, block_id)| *block_id));
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
editor.remove_blocks(removed_block_ids, None, cx);
|
|
}
|
|
|
|
fn conflicts_updated(
|
|
editor: &mut Editor,
|
|
conflict_set: Entity<ConflictSet>,
|
|
event: &ConflictSetUpdate,
|
|
cx: &mut Context<Editor>,
|
|
) {
|
|
let buffer_id = conflict_set.read(cx).snapshot.buffer_id;
|
|
let conflict_set = conflict_set.read(cx).snapshot();
|
|
let multibuffer = editor.buffer().read(cx);
|
|
let snapshot = multibuffer.snapshot(cx);
|
|
let excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
|
|
let Some(buffer_snapshot) = excerpts
|
|
.first()
|
|
.and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
|
|
else {
|
|
return;
|
|
};
|
|
|
|
let old_range = maybe!({
|
|
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
|
|
let buffer_conflicts = conflict_addon.buffers.get(&buffer_id)?;
|
|
match buffer_conflicts.block_ids.get(event.old_range.clone()) {
|
|
Some(_) => Some(event.old_range.clone()),
|
|
None => {
|
|
debug_panic!(
|
|
"conflicts updated event old range is invalid for buffer conflicts view (block_ids len is {:?}, old_range is {:?})",
|
|
buffer_conflicts.block_ids.len(),
|
|
event.old_range,
|
|
);
|
|
if event.old_range.start <= event.old_range.end {
|
|
Some(
|
|
event.old_range.start.min(buffer_conflicts.block_ids.len())
|
|
..event.old_range.end.min(buffer_conflicts.block_ids.len()),
|
|
)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Remove obsolete highlights and blocks
|
|
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
|
|
if let Some((buffer_conflicts, old_range)) = conflict_addon
|
|
.buffers
|
|
.get_mut(&buffer_id)
|
|
.zip(old_range.clone())
|
|
{
|
|
let old_conflicts = buffer_conflicts.block_ids[old_range].to_owned();
|
|
let mut removed_highlighted_ranges = Vec::new();
|
|
let mut removed_block_ids = HashSet::default();
|
|
for (conflict_range, block_id) in old_conflicts {
|
|
let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
|
|
let precedes_start = range
|
|
.context
|
|
.start
|
|
.cmp(&conflict_range.start, &buffer_snapshot)
|
|
.is_le();
|
|
let follows_end = range
|
|
.context
|
|
.end
|
|
.cmp(&conflict_range.start, &buffer_snapshot)
|
|
.is_ge();
|
|
precedes_start && follows_end
|
|
}) else {
|
|
continue;
|
|
};
|
|
let excerpt_id = *excerpt_id;
|
|
let Some(range) = snapshot
|
|
.anchor_in_excerpt(excerpt_id, conflict_range.start)
|
|
.zip(snapshot.anchor_in_excerpt(excerpt_id, conflict_range.end))
|
|
.map(|(start, end)| start..end)
|
|
else {
|
|
continue;
|
|
};
|
|
removed_highlighted_ranges.push(range.clone());
|
|
removed_block_ids.insert(block_id);
|
|
}
|
|
|
|
editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
|
|
|
|
editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
|
|
editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
|
|
editor
|
|
.remove_highlighted_rows::<ConflictsOursMarker>(removed_highlighted_ranges.clone(), cx);
|
|
editor.remove_highlighted_rows::<ConflictsTheirs>(removed_highlighted_ranges.clone(), cx);
|
|
editor.remove_highlighted_rows::<ConflictsTheirsMarker>(
|
|
removed_highlighted_ranges.clone(),
|
|
cx,
|
|
);
|
|
editor.remove_blocks(removed_block_ids, None, cx);
|
|
}
|
|
|
|
// Add new highlights and blocks
|
|
let editor_handle = cx.weak_entity();
|
|
let new_conflicts = &conflict_set.conflicts[event.new_range.clone()];
|
|
let mut blocks = Vec::new();
|
|
for conflict in new_conflicts {
|
|
let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
|
|
let precedes_start = range
|
|
.context
|
|
.start
|
|
.cmp(&conflict.range.start, &buffer_snapshot)
|
|
.is_le();
|
|
let follows_end = range
|
|
.context
|
|
.end
|
|
.cmp(&conflict.range.start, &buffer_snapshot)
|
|
.is_ge();
|
|
precedes_start && follows_end
|
|
}) else {
|
|
continue;
|
|
};
|
|
let excerpt_id = *excerpt_id;
|
|
|
|
update_conflict_highlighting(editor, conflict, &snapshot, excerpt_id, cx);
|
|
|
|
let Some(anchor) = snapshot.anchor_in_excerpt(excerpt_id, conflict.range.start) else {
|
|
continue;
|
|
};
|
|
|
|
let editor_handle = editor_handle.clone();
|
|
blocks.push(BlockProperties {
|
|
placement: BlockPlacement::Above(anchor),
|
|
height: Some(1),
|
|
style: BlockStyle::Fixed,
|
|
render: Arc::new({
|
|
let conflict = conflict.clone();
|
|
move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx)
|
|
}),
|
|
priority: 0,
|
|
render_in_minimap: true,
|
|
})
|
|
}
|
|
let new_block_ids = editor.insert_blocks(blocks, None, cx);
|
|
|
|
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
|
|
if let Some((buffer_conflicts, old_range)) =
|
|
conflict_addon.buffers.get_mut(&buffer_id).zip(old_range)
|
|
{
|
|
buffer_conflicts.block_ids.splice(
|
|
old_range,
|
|
new_conflicts
|
|
.iter()
|
|
.map(|conflict| conflict.range.clone())
|
|
.zip(new_block_ids),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn update_conflict_highlighting(
|
|
editor: &mut Editor,
|
|
conflict: &ConflictRegion,
|
|
buffer: &editor::MultiBufferSnapshot,
|
|
excerpt_id: editor::ExcerptId,
|
|
cx: &mut Context<Editor>,
|
|
) {
|
|
log::debug!("update conflict highlighting for {conflict:?}");
|
|
|
|
let outer_start = buffer
|
|
.anchor_in_excerpt(excerpt_id, conflict.range.start)
|
|
.unwrap();
|
|
let outer_end = buffer
|
|
.anchor_in_excerpt(excerpt_id, conflict.range.end)
|
|
.unwrap();
|
|
let our_start = buffer
|
|
.anchor_in_excerpt(excerpt_id, conflict.ours.start)
|
|
.unwrap();
|
|
let our_end = buffer
|
|
.anchor_in_excerpt(excerpt_id, conflict.ours.end)
|
|
.unwrap();
|
|
let their_start = buffer
|
|
.anchor_in_excerpt(excerpt_id, conflict.theirs.start)
|
|
.unwrap();
|
|
let their_end = buffer
|
|
.anchor_in_excerpt(excerpt_id, conflict.theirs.end)
|
|
.unwrap();
|
|
|
|
let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
|
|
let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
|
|
|
|
let options = RowHighlightOptions {
|
|
include_gutter: true,
|
|
..Default::default()
|
|
};
|
|
|
|
editor.insert_gutter_highlight::<ConflictsOuter>(
|
|
outer_start..their_end,
|
|
|cx| cx.theme().colors().editor_background,
|
|
cx,
|
|
);
|
|
|
|
// Prevent diff hunk highlighting within the entire conflict region.
|
|
editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
|
|
editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
|
|
editor.highlight_rows::<ConflictsOursMarker>(
|
|
outer_start..our_start,
|
|
ours_background,
|
|
options,
|
|
cx,
|
|
);
|
|
editor.highlight_rows::<ConflictsTheirs>(
|
|
their_start..their_end,
|
|
theirs_background,
|
|
options,
|
|
cx,
|
|
);
|
|
editor.highlight_rows::<ConflictsTheirsMarker>(
|
|
their_end..outer_end,
|
|
theirs_background,
|
|
options,
|
|
cx,
|
|
);
|
|
}
|
|
|
|
fn render_conflict_buttons(
|
|
conflict: &ConflictRegion,
|
|
excerpt_id: ExcerptId,
|
|
editor: WeakEntity<Editor>,
|
|
cx: &mut BlockContext,
|
|
) -> AnyElement {
|
|
h_flex()
|
|
.id(cx.block_id)
|
|
.h(cx.line_height)
|
|
.ml(cx.margins.gutter.width)
|
|
.items_end()
|
|
.gap_1()
|
|
.bg(cx.theme().colors().editor_background)
|
|
.child(
|
|
Button::new("head", "Use HEAD")
|
|
.label_size(LabelSize::Small)
|
|
.on_click({
|
|
let editor = editor.clone();
|
|
let conflict = conflict.clone();
|
|
let ours = conflict.ours.clone();
|
|
move |_, window, cx| {
|
|
resolve_conflict(
|
|
editor.clone(),
|
|
excerpt_id,
|
|
conflict.clone(),
|
|
vec![ours.clone()],
|
|
window,
|
|
cx,
|
|
)
|
|
.detach()
|
|
}
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("origin", "Use Origin")
|
|
.label_size(LabelSize::Small)
|
|
.on_click({
|
|
let editor = editor.clone();
|
|
let conflict = conflict.clone();
|
|
let theirs = conflict.theirs.clone();
|
|
move |_, window, cx| {
|
|
resolve_conflict(
|
|
editor.clone(),
|
|
excerpt_id,
|
|
conflict.clone(),
|
|
vec![theirs.clone()],
|
|
window,
|
|
cx,
|
|
)
|
|
.detach()
|
|
}
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("both", "Use Both")
|
|
.label_size(LabelSize::Small)
|
|
.on_click({
|
|
let editor = editor.clone();
|
|
let conflict = conflict.clone();
|
|
let ours = conflict.ours.clone();
|
|
let theirs = conflict.theirs.clone();
|
|
move |_, window, cx| {
|
|
resolve_conflict(
|
|
editor.clone(),
|
|
excerpt_id,
|
|
conflict.clone(),
|
|
vec![ours.clone(), theirs.clone()],
|
|
window,
|
|
cx,
|
|
)
|
|
.detach()
|
|
}
|
|
}),
|
|
)
|
|
.into_any()
|
|
}
|
|
|
|
pub(crate) fn resolve_conflict(
|
|
editor: WeakEntity<Editor>,
|
|
excerpt_id: ExcerptId,
|
|
resolved_conflict: ConflictRegion,
|
|
ranges: Vec<Range<Anchor>>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) -> Task<()> {
|
|
window.spawn(cx, async move |cx| {
|
|
let Some((workspace, project, multibuffer, buffer)) = editor
|
|
.update(cx, |editor, cx| {
|
|
let workspace = editor.workspace()?;
|
|
let project = editor.project.clone()?;
|
|
let multibuffer = editor.buffer().clone();
|
|
let buffer_id = resolved_conflict.ours.end.buffer_id?;
|
|
let buffer = multibuffer.read(cx).buffer(buffer_id)?;
|
|
resolved_conflict.resolve(buffer.clone(), &ranges, cx);
|
|
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
|
|
let snapshot = multibuffer.read(cx).snapshot(cx);
|
|
let buffer_snapshot = buffer.read(cx).snapshot();
|
|
let state = conflict_addon
|
|
.buffers
|
|
.get_mut(&buffer_snapshot.remote_id())?;
|
|
let ix = state
|
|
.block_ids
|
|
.binary_search_by(|(range, _)| {
|
|
range
|
|
.start
|
|
.cmp(&resolved_conflict.range.start, &buffer_snapshot)
|
|
})
|
|
.ok()?;
|
|
let &(_, block_id) = &state.block_ids[ix];
|
|
let start = snapshot
|
|
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
|
|
.unwrap();
|
|
let end = snapshot
|
|
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
|
|
.unwrap();
|
|
|
|
editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx);
|
|
|
|
editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
|
|
editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
|
|
editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
|
|
editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
|
|
editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
|
|
editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
|
|
Some((workspace, project, multibuffer, buffer))
|
|
})
|
|
.ok()
|
|
.flatten()
|
|
else {
|
|
return;
|
|
};
|
|
let Some(save) = project
|
|
.update(cx, |project, cx| {
|
|
if multibuffer.read(cx).all_diff_hunks_expanded() {
|
|
project.save_buffer(buffer.clone(), cx)
|
|
} else {
|
|
Task::ready(Ok(()))
|
|
}
|
|
})
|
|
.ok()
|
|
else {
|
|
return;
|
|
};
|
|
if save.await.log_err().is_none() {
|
|
let open_path = maybe!({
|
|
let path = buffer
|
|
.read_with(cx, |buffer, cx| buffer.project_path(cx))
|
|
.ok()
|
|
.flatten()?;
|
|
workspace
|
|
.update_in(cx, |workspace, window, cx| {
|
|
workspace.open_path_preview(path, None, false, false, false, window, cx)
|
|
})
|
|
.ok()
|
|
});
|
|
|
|
if let Some(open_path) = open_path {
|
|
open_path.await.log_err();
|
|
}
|
|
}
|
|
})
|
|
}
|