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:
parent
844c7ad22e
commit
9041f734fd
2 changed files with 190 additions and 56 deletions
|
@ -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,57 +458,53 @@ 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
|
|
||||||
.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::<ConflictAddon>().unwrap();
|
let conflict_addon = editor.addon_mut::<ConflictAddon>().unwrap();
|
||||||
let Some(state) = conflict_addon.buffers.get_mut(&buffer_snapshot.remote_id()) else {
|
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||||
return;
|
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||||
};
|
let state = conflict_addon
|
||||||
let Ok(ix) = state.block_ids.binary_search_by(|(range, _)| {
|
.buffers
|
||||||
|
.get_mut(&buffer_snapshot.remote_id())?;
|
||||||
|
let ix = state
|
||||||
|
.block_ids
|
||||||
|
.binary_search_by(|(range, _)| {
|
||||||
range
|
range
|
||||||
.start
|
.start
|
||||||
.cmp(&resolved_conflict.range.start, &buffer_snapshot)
|
.cmp(&resolved_conflict.range.start, &buffer_snapshot)
|
||||||
}) else {
|
})
|
||||||
return;
|
.ok()?;
|
||||||
};
|
|
||||||
let &(_, block_id) = &state.block_ids[ix];
|
let &(_, block_id) = &state.block_ids[ix];
|
||||||
let start = snapshot
|
let start = snapshot
|
||||||
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
|
.anchor_in_excerpt(excerpt_id, resolved_conflict.range.start)
|
||||||
|
@ -511,5 +518,41 @@ fn resolve_conflict(
|
||||||
editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
|
editor.remove_highlighted_rows::<ConflictsOursMarker>(vec![start..end], cx);
|
||||||
editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
|
editor.remove_highlighted_rows::<ConflictsTheirsMarker>(vec![start..end], cx);
|
||||||
editor.remove_blocks(HashSet::from_iter([block_id]), None, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue