Basic side-by-side diff implementation (#43586)
Release Notes: - N/A --------- Co-authored-by: cameron <cameron.studdstreet@gmail.com> Co-authored-by: Cameron <cameron@zed.dev>
This commit is contained in:
co-authored by
cameron
Cameron
parent
ca6e64d451
commit
2e00f40c54
@@ -8,7 +8,7 @@ use anyhow::{Context as _, Result, anyhow};
|
||||
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Addon, Editor, EditorEvent, SelectionEffects,
|
||||
Addon, Editor, EditorEvent, SelectionEffects, SplittableEditor,
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
multibuffer_context_lines,
|
||||
scroll::Autoscroll,
|
||||
@@ -56,7 +56,8 @@ actions!(
|
||||
Add,
|
||||
/// Shows the diff between the working directory and your default
|
||||
/// branch (typically main or master).
|
||||
BranchDiff
|
||||
BranchDiff,
|
||||
LeaderAndFollower,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -64,7 +65,7 @@ pub struct ProjectDiff {
|
||||
project: Entity<Project>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
branch_diff: Entity<branch_diff::BranchDiff>,
|
||||
editor: Entity<Editor>,
|
||||
editor: Entity<SplittableEditor>,
|
||||
buffer_diff_subscriptions: HashMap<Arc<RelPath>, (Entity<BufferDiff>, Subscription)>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -172,7 +173,9 @@ impl ProjectDiff {
|
||||
|
||||
pub fn autoscroll(&self, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::fit(), cx);
|
||||
editor.primary_editor().update(cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::fit(), cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -226,44 +229,44 @@ impl ProjectDiff {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
let multibuffer = cx.new(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
|
||||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut diff_display_editor =
|
||||
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
||||
diff_display_editor.disable_diagnostics(cx);
|
||||
diff_display_editor.set_expand_all_diff_hunks(cx);
|
||||
|
||||
match branch_diff.read(cx).diff_base() {
|
||||
DiffBase::Head => {
|
||||
diff_display_editor.register_addon(GitPanelAddon {
|
||||
workspace: workspace.downgrade(),
|
||||
});
|
||||
}
|
||||
DiffBase::Merge { .. } => {
|
||||
diff_display_editor.register_addon(BranchDiffAddon {
|
||||
branch_diff: branch_diff.clone(),
|
||||
});
|
||||
diff_display_editor.start_temporary_diff_override();
|
||||
diff_display_editor.set_render_diff_hunk_controls(
|
||||
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
|
||||
cx,
|
||||
);
|
||||
//
|
||||
}
|
||||
}
|
||||
let diff_display_editor = SplittableEditor::new_unsplit(
|
||||
multibuffer.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
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);
|
||||
})
|
||||
.primary_editor()
|
||||
.update(cx, |editor, cx| {
|
||||
editor.disable_diagnostics(cx);
|
||||
|
||||
match branch_diff.read(cx).diff_base() {
|
||||
DiffBase::Head => {
|
||||
editor.register_addon(GitPanelAddon {
|
||||
workspace: workspace.downgrade(),
|
||||
});
|
||||
}
|
||||
DiffBase::Merge { .. } => {
|
||||
editor.register_addon(BranchDiffAddon {
|
||||
branch_diff: branch_diff.clone(),
|
||||
});
|
||||
editor.start_temporary_diff_override();
|
||||
editor.set_render_diff_hunk_controls(
|
||||
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
diff_display_editor
|
||||
});
|
||||
cx.subscribe_in(&editor, window, Self::handle_editor_event)
|
||||
.detach();
|
||||
@@ -343,7 +346,7 @@ impl ProjectDiff {
|
||||
}
|
||||
|
||||
pub fn active_path(&self, cx: &App) -> Option<ProjectPath> {
|
||||
let editor = self.editor.read(cx);
|
||||
let editor = self.editor.read(cx).last_selected_editor().read(cx);
|
||||
let position = editor.selections.newest_anchor().head();
|
||||
let multi_buffer = editor.buffer().read(cx);
|
||||
let (_, buffer, _) = multi_buffer.excerpt_containing(position, cx)?;
|
||||
@@ -358,14 +361,16 @@ impl ProjectDiff {
|
||||
fn move_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::focused()),
|
||||
window,
|
||||
cx,
|
||||
|s| {
|
||||
s.select_ranges([position..position]);
|
||||
},
|
||||
)
|
||||
editor.primary_editor().update(cx, |editor, cx| {
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::focused()),
|
||||
window,
|
||||
cx,
|
||||
|s| {
|
||||
s.select_ranges([position..position]);
|
||||
},
|
||||
)
|
||||
})
|
||||
});
|
||||
} else {
|
||||
self.pending_scroll = Some(path_key);
|
||||
@@ -373,7 +378,7 @@ impl ProjectDiff {
|
||||
}
|
||||
|
||||
fn button_states(&self, cx: &App) -> ButtonStates {
|
||||
let editor = self.editor.read(cx);
|
||||
let editor = self.editor.read(cx).primary_editor().read(cx);
|
||||
let snapshot = self.multibuffer.read(cx).snapshot(cx);
|
||||
let prev_next = snapshot.diff_hunks().nth(1).is_some();
|
||||
let mut selection = true;
|
||||
@@ -384,7 +389,13 @@ impl ProjectDiff {
|
||||
.collect::<Vec<_>>();
|
||||
if !ranges.iter().any(|range| range.start != range.end) {
|
||||
selection = false;
|
||||
if let Some((excerpt_id, _, range)) = self.editor.read(cx).active_excerpt(cx) {
|
||||
if let Some((excerpt_id, _, range)) = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.primary_editor()
|
||||
.read(cx)
|
||||
.active_excerpt(cx)
|
||||
{
|
||||
ranges = vec![multi_buffer::Anchor::range_in_buffer(excerpt_id, range)];
|
||||
} else {
|
||||
ranges = Vec::default();
|
||||
@@ -432,7 +443,7 @@ impl ProjectDiff {
|
||||
|
||||
fn handle_editor_event(
|
||||
&mut self,
|
||||
editor: &Entity<Editor>,
|
||||
editor: &Entity<SplittableEditor>,
|
||||
event: &EditorEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -476,9 +487,12 @@ impl ProjectDiff {
|
||||
self.buffer_diff_subscriptions
|
||||
.insert(path_key.path.clone(), (diff.clone(), subscription));
|
||||
|
||||
// TODO(split-diff) we shouldn't have a conflict addon when split
|
||||
let conflict_addon = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.primary_editor()
|
||||
.read(cx)
|
||||
.addon::<ConflictAddon>()
|
||||
.expect("project diff editor should have a conflict addon");
|
||||
|
||||
@@ -518,20 +532,27 @@ impl ProjectDiff {
|
||||
});
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if was_empty {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
|
||||
// TODO select the very beginning (possibly inside a deletion)
|
||||
selections
|
||||
.select_ranges([multi_buffer::Anchor::min()..multi_buffer::Anchor::min()])
|
||||
});
|
||||
}
|
||||
if is_excerpt_newly_added
|
||||
&& (file_status.is_deleted()
|
||||
|| (file_status.is_untracked()
|
||||
&& GitPanelSettings::get_global(cx).collapse_untracked_diff))
|
||||
{
|
||||
editor.fold_buffer(snapshot.text.remote_id(), cx)
|
||||
}
|
||||
editor.primary_editor().update(cx, |editor, cx| {
|
||||
if was_empty {
|
||||
editor.change_selections(
|
||||
SelectionEffects::no_scroll(),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
selections.select_ranges([
|
||||
multi_buffer::Anchor::min()..multi_buffer::Anchor::min()
|
||||
])
|
||||
},
|
||||
);
|
||||
}
|
||||
if is_excerpt_newly_added
|
||||
&& (file_status.is_deleted()
|
||||
|| (file_status.is_untracked()
|
||||
&& GitPanelSettings::get_global(cx).collapse_untracked_diff))
|
||||
{
|
||||
editor.fold_buffer(snapshot.text.remote_id(), cx)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if self.multibuffer.read(cx).is_empty()
|
||||
@@ -650,8 +671,11 @@ impl Item for ProjectDiff {
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.primary_editor().update(cx, |primary_editor, cx| {
|
||||
primary_editor.deactivated(window, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
@@ -660,8 +684,11 @@ impl Item for ProjectDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.primary_editor().update(cx, |primary_editor, cx| {
|
||||
primary_editor.navigate(data, window, cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
|
||||
@@ -689,8 +716,9 @@ impl Item for ProjectDiff {
|
||||
Some("Project Diff Opened")
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
fn as_searchable(&self, _: &Entity<Self>, cx: &App) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
// TODO(split-diff) SplitEditor should be searchable
|
||||
Some(Box::new(self.editor.read(cx).primary_editor().clone()))
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
@@ -698,7 +726,11 @@ impl Item for ProjectDiff {
|
||||
cx: &App,
|
||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
||||
) {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
self.editor
|
||||
.read(cx)
|
||||
.primary_editor()
|
||||
.read(cx)
|
||||
.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
@@ -707,8 +739,10 @@ impl Item for ProjectDiff {
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.primary_editor().update(cx, |primary_editor, _| {
|
||||
primary_editor.set_nav_history(Some(nav_history));
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -752,7 +786,11 @@ impl Item for ProjectDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.save(options, project, window, cx)
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.primary_editor().update(cx, |primary_editor, cx| {
|
||||
primary_editor.save(options, project, window, cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
@@ -771,19 +809,23 @@ impl Item for ProjectDiff {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.reload(project, window, cx)
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.primary_editor().update(cx, |primary_editor, cx| {
|
||||
primary_editor.reload(project, window, cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a Entity<Self>,
|
||||
_: &'a App,
|
||||
cx: &'a App,
|
||||
) -> Option<gpui::AnyEntity> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.clone().into())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.clone().into())
|
||||
Some(self.editor.read(cx).primary_editor().clone().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -794,7 +836,11 @@ impl Item for ProjectDiff {
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
|
||||
self.editor.breadcrumbs(theme, cx)
|
||||
self.editor
|
||||
.read(cx)
|
||||
.last_selected_editor()
|
||||
.read(cx)
|
||||
.breadcrumbs(theme, cx)
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
@@ -1629,7 +1675,7 @@ mod tests {
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
let editor = diff.read_with(cx, |diff, _| diff.editor.clone());
|
||||
let editor = diff.read_with(cx, |diff, cx| diff.editor.read(cx).primary_editor().clone());
|
||||
assert_state_with_diff(
|
||||
&editor,
|
||||
cx,
|
||||
@@ -1685,7 +1731,7 @@ mod tests {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
diff.editor.clone()
|
||||
diff.editor.read(cx).primary_editor().clone()
|
||||
});
|
||||
assert_state_with_diff(
|
||||
&editor,
|
||||
@@ -1706,7 +1752,7 @@ mod tests {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
diff.editor.clone()
|
||||
diff.editor.read(cx).primary_editor().clone()
|
||||
});
|
||||
assert_state_with_diff(
|
||||
&editor,
|
||||
@@ -1759,7 +1805,8 @@ mod tests {
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
let diff_editor = diff.read_with(cx, |diff, _| diff.editor.clone());
|
||||
let diff_editor =
|
||||
diff.read_with(cx, |diff, cx| diff.editor.read(cx).primary_editor().clone());
|
||||
|
||||
assert_state_with_diff(
|
||||
&diff_editor,
|
||||
@@ -1883,7 +1930,7 @@ mod tests {
|
||||
workspace.active_item_as::<ProjectDiff>(cx).unwrap()
|
||||
});
|
||||
cx.focus(&item);
|
||||
let editor = item.read_with(cx, |item, _| item.editor.clone());
|
||||
let editor = item.read_with(cx, |item, cx| item.editor.read(cx).primary_editor().clone());
|
||||
|
||||
let mut cx = EditorTestContext::for_editor_in(editor, cx).await;
|
||||
|
||||
@@ -1997,7 +2044,7 @@ mod tests {
|
||||
workspace.active_item_as::<ProjectDiff>(cx).unwrap()
|
||||
});
|
||||
cx.focus(&item);
|
||||
let editor = item.read_with(cx, |item, _| item.editor.clone());
|
||||
let editor = item.read_with(cx, |item, cx| item.editor.read(cx).primary_editor().clone());
|
||||
|
||||
let mut cx = EditorTestContext::for_editor_in(editor, cx).await;
|
||||
|
||||
@@ -2044,7 +2091,7 @@ mod tests {
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update(|window, cx| {
|
||||
let editor = diff.read(cx).editor.clone();
|
||||
let editor = diff.read(cx).editor.read(cx).primary_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];
|
||||
@@ -2061,6 +2108,8 @@ mod tests {
|
||||
.read(cx)
|
||||
.editor
|
||||
.read(cx)
|
||||
.primary_editor()
|
||||
.read(cx)
|
||||
.addon::<ConflictAddon>()
|
||||
.unwrap()
|
||||
.conflict_set(buffer_id)
|
||||
@@ -2144,7 +2193,7 @@ mod tests {
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
let editor = diff.read_with(cx, |diff, _| diff.editor.clone());
|
||||
let editor = diff.read_with(cx, |diff, cx| diff.editor.read(cx).primary_editor().clone());
|
||||
|
||||
assert_state_with_diff(
|
||||
&editor,
|
||||
@@ -2255,7 +2304,7 @@ mod tests {
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
let editor = diff.read_with(cx, |diff, _| diff.editor.clone());
|
||||
let editor = diff.read_with(cx, |diff, cx| diff.editor.read(cx).primary_editor().clone());
|
||||
|
||||
assert_state_with_diff(
|
||||
&editor,
|
||||
@@ -2349,7 +2398,7 @@ mod tests {
|
||||
workspace.active_item_as::<ProjectDiff>(cx).unwrap()
|
||||
});
|
||||
cx.focus(&item);
|
||||
let editor = item.read_with(cx, |item, _| item.editor.clone());
|
||||
let editor = item.read_with(cx, |item, cx| item.editor.read(cx).primary_editor().clone());
|
||||
|
||||
fs.set_head_and_index_for_repo(
|
||||
Path::new(path!("/project/.git")),
|
||||
|
||||
Reference in New Issue
Block a user