git: Save buffer when resolving a conflict from the project diff (#30762)

Closes #30555

Release Notes:

- Changed the project diff to autosave the targeted buffer after
resolving a merge conflict.
This commit is contained in:
Cole Miller 2025-05-19 19:32:31 +02:00 committed by GitHub
parent 844c7ad22e
commit 9041f734fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 190 additions and 56 deletions

View file

@ -5,16 +5,17 @@ use editor::{
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
}; };
use gpui::{ use gpui::{
App, Context, Entity, InteractiveElement as _, ParentElement as _, Subscription, WeakEntity, App, Context, Entity, InteractiveElement as _, ParentElement as _, Subscription, Task,
WeakEntity,
}; };
use language::{Anchor, Buffer, BufferId}; use language::{Anchor, Buffer, BufferId};
use project::{ConflictRegion, ConflictSet, ConflictSetUpdate}; use project::{ConflictRegion, ConflictSet, ConflictSetUpdate, ProjectItem as _};
use std::{ops::Range, sync::Arc}; use std::{ops::Range, sync::Arc};
use ui::{ use ui::{
ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled, ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled,
StyledTypography as _, div, h_flex, rems, StyledTypography as _, Window, div, h_flex, rems,
}; };
use util::{debug_panic, maybe}; use util::{ResultExt as _, debug_panic, maybe};
pub(crate) struct ConflictAddon { pub(crate) struct ConflictAddon {
buffers: HashMap<BufferId, BufferConflicts>, buffers: HashMap<BufferId, BufferConflicts>,
@ -404,8 +405,16 @@ fn render_conflict_buttons(
let editor = editor.clone(); let editor = editor.clone();
let conflict = conflict.clone(); let conflict = conflict.clone();
let ours = conflict.ours.clone(); let ours = conflict.ours.clone();
move |_, _, cx| { move |_, window, cx| {
resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx) resolve_conflict(
editor.clone(),
excerpt_id,
conflict.clone(),
vec![ours.clone()],
window,
cx,
)
.detach()
} }
}), }),
) )
@ -422,14 +431,16 @@ fn render_conflict_buttons(
let editor = editor.clone(); let editor = editor.clone();
let conflict = conflict.clone(); let conflict = conflict.clone();
let theirs = conflict.theirs.clone(); let theirs = conflict.theirs.clone();
move |_, _, cx| { move |_, window, cx| {
resolve_conflict( resolve_conflict(
editor.clone(), editor.clone(),
excerpt_id, excerpt_id,
&conflict, conflict.clone(),
&[theirs.clone()], vec![theirs.clone()],
window,
cx, cx,
) )
.detach()
} }
}), }),
) )
@ -447,69 +458,101 @@ fn render_conflict_buttons(
let conflict = conflict.clone(); let conflict = conflict.clone();
let ours = conflict.ours.clone(); let ours = conflict.ours.clone();
let theirs = conflict.theirs.clone(); let theirs = conflict.theirs.clone();
move |_, _, cx| { move |_, window, cx| {
resolve_conflict( resolve_conflict(
editor.clone(), editor.clone(),
excerpt_id, excerpt_id,
&conflict, conflict.clone(),
&[ours.clone(), theirs.clone()], vec![ours.clone(), theirs.clone()],
window,
cx, cx,
) )
.detach()
} }
}), }),
) )
.into_any() .into_any()
} }
fn resolve_conflict( pub(crate) fn resolve_conflict(
editor: WeakEntity<Editor>, editor: WeakEntity<Editor>,
excerpt_id: ExcerptId, excerpt_id: ExcerptId,
resolved_conflict: &ConflictRegion, resolved_conflict: ConflictRegion,
ranges: &[Range<Anchor>], ranges: Vec<Range<Anchor>>,
window: &mut Window,
cx: &mut App, cx: &mut App,
) { ) -> Task<()> {
let Some(editor) = editor.upgrade() else { window.spawn(cx, async move |cx| {
return; let Some((workspace, project, multibuffer, buffer)) = editor
}; .update(cx, |editor, cx| {
let workspace = editor.workspace()?;
let multibuffer = editor.read(cx).buffer().read(cx); let project = editor.project.clone()?;
let snapshot = multibuffer.snapshot(cx); let multibuffer = editor.buffer().clone();
let Some(buffer) = resolved_conflict let buffer_id = resolved_conflict.ours.end.buffer_id?;
.ours let buffer = multibuffer.read(cx).buffer(buffer_id)?;
.end resolved_conflict.resolve(buffer.clone(), &ranges, cx);
.buffer_id let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
.and_then(|buffer_id| multibuffer.buffer(buffer_id)) let snapshot = multibuffer.read(cx).snapshot(cx);
else { let buffer_snapshot = buffer.read(cx).snapshot();
return; let state = conflict_addon
}; .buffers
let buffer_snapshot = buffer.read(cx).snapshot(); .get_mut(&buffer_snapshot.remote_id())?;
let ix = state
resolved_conflict.resolve(buffer, ranges, cx); .block_ids
.binary_search_by(|(range, _)| {
editor.update(cx, |editor, cx| { range
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap(); .start
let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else { .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_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; return;
}; };
let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| { let Some(save) = project
range .update(cx, |project, cx| {
.start if multibuffer.read(cx).all_diff_hunks_expanded() {
.cmp(&resolved_conflict.range.start, &buffer_snapshot) project.save_buffer(buffer.clone(), cx)
}) else { } else {
Task::ready(Ok(()))
}
})
.ok()
else {
return; return;
}; };
let &(_, block_id) = &state.block_ids[ix]; if save.await.log_err().is_none() {
let start = snapshot let open_path = maybe!({
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.start) let path = buffer
.unwrap(); .read_with(cx, |buffer, cx| buffer.project_path(cx))
let end = snapshot .ok()
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.end) .flatten()?;
.unwrap(); workspace
editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx); .update_in(cx, |workspace, window, cx| {
editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx); workspace.open_path_preview(path, None, false, false, false, window, cx)
editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx); })
editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx); .ok()
editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx); });
editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
if let Some(open_path) = open_path {
open_path.await.log_err();
}
}
}) })
} }

View file

@ -148,6 +148,17 @@ impl ProjectDiff {
}); });
diff_display_editor diff_display_editor
}); });
window.defer(cx, {
let workspace = workspace.clone();
let editor = editor.clone();
move |window, cx| {
workspace.update(cx, |workspace, cx| {
editor.update(cx, |editor, cx| {
editor.added_to_workspace(workspace, window, cx);
})
});
}
});
cx.subscribe_in(&editor, window, Self::handle_editor_event) cx.subscribe_in(&editor, window, Self::handle_editor_event)
.detach(); .detach();
@ -1323,6 +1334,7 @@ fn merge_anchor_ranges<'a>(
mod tests { mod tests {
use db::indoc; use db::indoc;
use editor::test::editor_test_context::{EditorTestContext, assert_state_with_diff}; use editor::test::editor_test_context::{EditorTestContext, assert_state_with_diff};
use git::status::{UnmergedStatus, UnmergedStatusCode};
use gpui::TestAppContext; use gpui::TestAppContext;
use project::FakeFs; use project::FakeFs;
use serde_json::json; use serde_json::json;
@ -1583,7 +1595,10 @@ mod tests {
); );
} }
use crate::project_diff::{self, ProjectDiff}; use crate::{
conflict_view::resolve_conflict,
project_diff::{self, ProjectDiff},
};
#[gpui::test] #[gpui::test]
async fn test_go_to_prev_hunk_multibuffer(cx: &mut TestAppContext) { async fn test_go_to_prev_hunk_multibuffer(cx: &mut TestAppContext) {
@ -1754,4 +1769,80 @@ mod tests {
cx.assert_excerpts_with_selections(&format!("[EXCERPT]\nˇ{git_contents}")); cx.assert_excerpts_with_selections(&format!("[EXCERPT]\nˇ{git_contents}"));
} }
#[gpui::test]
async fn test_saving_resolved_conflicts(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/project"),
json!({
".git": {},
"foo": "<<<<<<< x\nours\n=======\ntheirs\n>>>>>>> y\n",
}),
)
.await;
fs.set_status_for_repo(
Path::new(path!("/project/.git")),
&[(
Path::new("foo"),
UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
}
.into(),
)],
);
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let diff = cx.new_window_entity(|window, cx| {
ProjectDiff::new(project.clone(), workspace, window, cx)
});
cx.run_until_parked();
cx.update(|window, cx| {
let editor = diff.read(cx).editor.clone();
let excerpt_ids = editor.read(cx).buffer().read(cx).excerpt_ids();
assert_eq!(excerpt_ids.len(), 1);
let excerpt_id = excerpt_ids[0];
let buffer = editor
.read(cx)
.buffer()
.read(cx)
.all_buffers()
.into_iter()
.next()
.unwrap();
let buffer_id = buffer.read(cx).remote_id();
let conflict_set = diff
.read(cx)
.editor
.read(cx)
.addon::<ConflictAddon>()
.unwrap()
.conflict_set(buffer_id)
.unwrap();
assert!(conflict_set.read(cx).has_conflict);
let snapshot = conflict_set.read(cx).snapshot();
assert_eq!(snapshot.conflicts.len(), 1);
let ours_range = snapshot.conflicts[0].ours.clone();
resolve_conflict(
editor.downgrade(),
excerpt_id,
snapshot.conflicts[0].clone(),
vec![ours_range],
window,
cx,
)
})
.await;
let contents = fs.read_file_sync(path!("/project/foo")).unwrap();
let contents = String::from_utf8(contents).unwrap();
assert_eq!(contents, "ours\n");
}
} }