From 9041f734fde17ab2356698ca3bc5d412e23d243c Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 19 May 2025 19:32:31 +0200 Subject: [PATCH] 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. --- crates/git_ui/src/conflict_view.rs | 153 ++++++++++++++++++----------- crates/git_ui/src/project_diff.rs | 93 +++++++++++++++++- 2 files changed, 190 insertions(+), 56 deletions(-) diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index d2086e425aecdaa25233011d08879a8bfcc87386..a7fbcb9bec4574fd5fe7cf9e23fe81e592ea6a8f 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -5,16 +5,17 @@ use editor::{ display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, }; 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 project::{ConflictRegion, ConflictSet, ConflictSetUpdate}; +use project::{ConflictRegion, ConflictSet, ConflictSetUpdate, ProjectItem as _}; use std::{ops::Range, sync::Arc}; use ui::{ 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 { buffers: HashMap, @@ -404,8 +405,16 @@ fn render_conflict_buttons( let editor = editor.clone(); let conflict = conflict.clone(); let ours = conflict.ours.clone(); - move |_, _, cx| { - resolve_conflict(editor.clone(), excerpt_id, &conflict, &[ours.clone()], cx) + move |_, window, 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 conflict = conflict.clone(); let theirs = conflict.theirs.clone(); - move |_, _, cx| { + move |_, window, cx| { resolve_conflict( editor.clone(), excerpt_id, - &conflict, - &[theirs.clone()], + conflict.clone(), + vec![theirs.clone()], + window, cx, ) + .detach() } }), ) @@ -447,69 +458,101 @@ fn render_conflict_buttons( let conflict = conflict.clone(); let ours = conflict.ours.clone(); let theirs = conflict.theirs.clone(); - move |_, _, cx| { + move |_, window, cx| { resolve_conflict( editor.clone(), excerpt_id, - &conflict, - &[ours.clone(), theirs.clone()], + conflict.clone(), + vec![ours.clone(), theirs.clone()], + window, cx, ) + .detach() } }), ) .into_any() } -fn resolve_conflict( +pub(crate) fn resolve_conflict( editor: WeakEntity, excerpt_id: ExcerptId, - resolved_conflict: &ConflictRegion, - ranges: &[Range], + resolved_conflict: ConflictRegion, + ranges: Vec>, + window: &mut Window, cx: &mut App, -) { - let Some(editor) = editor.upgrade() else { - return; - }; - - let multibuffer = editor.read(cx).buffer().read(cx); - let snapshot = multibuffer.snapshot(cx); - let Some(buffer) = resolved_conflict - .ours - .end - .buffer_id - .and_then(|buffer_id| multibuffer.buffer(buffer_id)) - else { - return; - }; - let buffer_snapshot = buffer.read(cx).snapshot(); - - resolved_conflict.resolve(buffer, ranges, cx); - - editor.update(cx, |editor, cx| { - let conflict_addon = editor.addon_mut::().unwrap(); - let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else { +) -> 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::().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_highlighted_rows::(vec![start..end], cx); + editor.remove_highlighted_rows::(vec![start..end], cx); + editor.remove_highlighted_rows::(vec![start..end], cx); + editor.remove_highlighted_rows::(vec![start..end], cx); + editor.remove_highlighted_rows::(vec![start..end], cx); + editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); + Some((workspace, project, multibuffer, buffer)) + }) + .ok() + .flatten() + else { return; }; - let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| { - range - .start - .cmp(&resolved_conflict.range.start, &buffer_snapshot) - }) else { + 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; }; - 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::(vec![start..end], cx); - editor.remove_highlighted_rows::(vec![start..end], cx); - editor.remove_highlighted_rows::(vec![start..end], cx); - editor.remove_highlighted_rows::(vec![start..end], cx); - editor.remove_highlighted_rows::(vec![start..end], cx); - editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); + 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(); + } + } }) } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index f3f2caf3dfffba53d89029060859a2e18223d731..dd81065ed57ee897e0ac1b458ac70fba60703f35 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -148,6 +148,17 @@ impl ProjectDiff { }); 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) .detach(); @@ -1323,6 +1334,7 @@ fn merge_anchor_ranges<'a>( mod tests { use db::indoc; use editor::test::editor_test_context::{EditorTestContext, assert_state_with_diff}; + use git::status::{UnmergedStatus, UnmergedStatusCode}; use gpui::TestAppContext; use project::FakeFs; 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] 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}")); } + + #[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::() + .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"); + } }