Compare commits

...

4 Commits

Author SHA1 Message Date
Conrad Irwin
323511f3c2 TEMP 2025-09-03 22:07:34 -07:00
Conrad Irwin
eb0436e84e branch diff 2025-09-03 17:43:23 -07:00
Conrad Irwin
ddb467de90 WIP
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-09-03 15:57:41 -07:00
Conrad Irwin
6dc68e02a7 TEMP 2025-09-03 15:42:10 -07:00
12 changed files with 456 additions and 656 deletions

View File

@@ -636,8 +636,11 @@ impl EditFileToolCard {
// Create a buffer diff with the current text as the base
let buffer_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&text_snapshot, cx);
let base_text = buffer_snapshot.text();
let language = buffer_snapshot.language().cloned();
let _ = diff.set_base_text(
buffer_snapshot.clone(),
Some(Arc::new(base_text)),
language,
language_registry,
text_snapshot,
cx,

View File

@@ -1158,34 +1158,22 @@ impl BufferDiff {
self.hunks_intersecting_range(start..end, buffer, cx)
}
pub fn set_base_text_buffer(
&mut self,
base_buffer: Entity<language::Buffer>,
buffer: text::BufferSnapshot,
cx: &mut Context<Self>,
) -> oneshot::Receiver<()> {
let base_buffer = base_buffer.read(cx);
let language_registry = base_buffer.language_registry();
let base_buffer = base_buffer.snapshot();
self.set_base_text(base_buffer, language_registry, buffer, cx)
}
/// Used in cases where the change set isn't derived from git.
pub fn set_base_text(
&mut self,
base_buffer: language::BufferSnapshot,
base_text: Option<Arc<String>>,
language: Option<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
buffer: text::BufferSnapshot,
cx: &mut Context<Self>,
) -> oneshot::Receiver<()> {
let (tx, rx) = oneshot::channel();
let this = cx.weak_entity();
let base_text = Arc::new(base_buffer.text());
let snapshot = BufferDiffSnapshot::new_with_base_text(
buffer.clone(),
Some(base_text),
base_buffer.language().cloned(),
base_text,
language,
language_registry,
cx,
);

View File

@@ -34,7 +34,6 @@ mod lsp_ext;
mod mouse_context_menu;
pub mod movement;
mod persistence;
mod proposed_changes_editor;
mod rust_analyzer_ext;
pub mod scroll;
mod selections_collection;
@@ -70,9 +69,7 @@ pub use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey,
RowInfo, ToOffset, ToPoint,
};
pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
pub use text::Bias;
use ::git::{
@@ -20472,65 +20469,6 @@ impl Editor {
self.searchable
}
fn open_proposed_changes_editor(
&mut self,
_: &OpenProposedChangesEditor,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace() else {
cx.propagate();
return;
};
let selections = self.selections.all::<usize>(cx);
let multi_buffer = self.buffer.read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let mut new_selections_by_buffer = HashMap::default();
for selection in selections {
for (buffer, range, _) in
multi_buffer_snapshot.range_to_buffer_ranges(selection.start..selection.end)
{
let mut range = range.to_point(buffer);
range.start.column = 0;
range.end.column = buffer.line_len(range.end.row);
new_selections_by_buffer
.entry(multi_buffer.buffer(buffer.remote_id()).unwrap())
.or_insert(Vec::new())
.push(range)
}
}
let proposed_changes_buffers = new_selections_by_buffer
.into_iter()
.map(|(buffer, ranges)| ProposedChangeLocation { buffer, ranges })
.collect::<Vec<_>>();
let proposed_changes_editor = cx.new(|cx| {
ProposedChangesEditor::new(
"Proposed changes",
proposed_changes_buffers,
self.project.clone(),
window,
cx,
)
});
window.defer(cx, move |window, cx| {
workspace.update(cx, |workspace, cx| {
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
Box::new(proposed_changes_editor),
true,
true,
None,
window,
cx,
);
});
});
});
}
pub fn open_excerpts_in_split(
&mut self,
_: &OpenExcerptsSplit,

View File

@@ -440,7 +440,6 @@ impl EditorElement {
register_action(editor, window, Editor::toggle_code_actions);
register_action(editor, window, Editor::open_excerpts);
register_action(editor, window, Editor::open_excerpts_in_split);
register_action(editor, window, Editor::open_proposed_changes_editor);
register_action(editor, window, Editor::toggle_soft_wrap);
register_action(editor, window, Editor::toggle_tab_bar);
register_action(editor, window, Editor::toggle_line_numbers);

View File

@@ -1,516 +0,0 @@
use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider};
use buffer_diff::BufferDiff;
use collections::HashSet;
use futures::{channel::mpsc, future::join_all};
use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task};
use language::{Buffer, BufferEvent, Capability};
use multi_buffer::{ExcerptRange, MultiBuffer};
use project::Project;
use smol::stream::StreamExt;
use std::{any::TypeId, ops::Range, rc::Rc, time::Duration};
use text::ToOffset;
use ui::{ButtonLike, KeyBinding, prelude::*};
use workspace::{
Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
item::SaveOptions, searchable::SearchableItemHandle,
};
pub struct ProposedChangesEditor {
editor: Entity<Editor>,
multibuffer: Entity<MultiBuffer>,
title: SharedString,
buffer_entries: Vec<BufferEntry>,
_recalculate_diffs_task: Task<Option<()>>,
recalculate_diffs_tx: mpsc::UnboundedSender<RecalculateDiff>,
}
pub struct ProposedChangeLocation<T> {
pub buffer: Entity<Buffer>,
pub ranges: Vec<Range<T>>,
}
struct BufferEntry {
base: Entity<Buffer>,
branch: Entity<Buffer>,
_subscription: Subscription,
}
pub struct ProposedChangesEditorToolbar {
current_editor: Option<Entity<ProposedChangesEditor>>,
}
struct RecalculateDiff {
buffer: Entity<Buffer>,
debounce: bool,
}
/// A provider of code semantics for branch buffers.
///
/// Requests in edited regions will return nothing, but requests in unchanged
/// regions will be translated into the base buffer's coordinates.
struct BranchBufferSemanticsProvider(Rc<dyn SemanticsProvider>);
impl ProposedChangesEditor {
pub fn new<T: Clone + ToOffset>(
title: impl Into<SharedString>,
locations: Vec<ProposedChangeLocation<T>>,
project: Option<Entity<Project>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let (recalculate_diffs_tx, mut recalculate_diffs_rx) = mpsc::unbounded();
let mut this = Self {
editor: cx.new(|cx| {
let mut editor = Editor::for_multibuffer(multibuffer.clone(), project, window, cx);
editor.set_expand_all_diff_hunks(cx);
editor.set_completion_provider(None);
editor.clear_code_action_providers();
editor.set_semantics_provider(
editor
.semantics_provider()
.map(|provider| Rc::new(BranchBufferSemanticsProvider(provider)) as _),
);
editor
}),
multibuffer,
title: title.into(),
buffer_entries: Vec::new(),
recalculate_diffs_tx,
_recalculate_diffs_task: cx.spawn_in(window, async move |this, cx| {
let mut buffers_to_diff = HashSet::default();
while let Some(mut recalculate_diff) = recalculate_diffs_rx.next().await {
buffers_to_diff.insert(recalculate_diff.buffer);
while recalculate_diff.debounce {
cx.background_executor()
.timer(Duration::from_millis(50))
.await;
let mut had_further_changes = false;
while let Ok(next_recalculate_diff) = recalculate_diffs_rx.try_next() {
let next_recalculate_diff = next_recalculate_diff?;
recalculate_diff.debounce &= next_recalculate_diff.debounce;
buffers_to_diff.insert(next_recalculate_diff.buffer);
had_further_changes = true;
}
if !had_further_changes {
break;
}
}
let recalculate_diff_futures = this
.update(cx, |this, cx| {
buffers_to_diff
.drain()
.filter_map(|buffer| {
let buffer = buffer.read(cx);
let base_buffer = buffer.base_buffer()?;
let buffer = buffer.text_snapshot();
let diff =
this.multibuffer.read(cx).diff_for(buffer.remote_id())?;
Some(diff.update(cx, |diff, cx| {
diff.set_base_text_buffer(base_buffer.clone(), buffer, cx)
}))
})
.collect::<Vec<_>>()
})
.ok()?;
join_all(recalculate_diff_futures).await;
}
None
}),
};
this.reset_locations(locations, window, cx);
this
}
pub fn branch_buffer_for_base(&self, base_buffer: &Entity<Buffer>) -> Option<Entity<Buffer>> {
self.buffer_entries.iter().find_map(|entry| {
if &entry.base == base_buffer {
Some(entry.branch.clone())
} else {
None
}
})
}
pub fn set_title(&mut self, title: SharedString, cx: &mut Context<Self>) {
self.title = title;
cx.notify();
}
pub fn reset_locations<T: Clone + ToOffset>(
&mut self,
locations: Vec<ProposedChangeLocation<T>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
// Undo all branch changes
for entry in &self.buffer_entries {
let base_version = entry.base.read(cx).version();
entry.branch.update(cx, |buffer, cx| {
let undo_counts = buffer
.operations()
.iter()
.filter_map(|(timestamp, _)| {
if !base_version.observed(*timestamp) {
Some((*timestamp, u32::MAX))
} else {
None
}
})
.collect();
buffer.undo_operations(undo_counts, cx);
});
}
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.clear(cx);
});
let mut buffer_entries = Vec::new();
let mut new_diffs = Vec::new();
for location in locations {
let branch_buffer;
if let Some(ix) = self
.buffer_entries
.iter()
.position(|entry| entry.base == location.buffer)
{
let entry = self.buffer_entries.remove(ix);
branch_buffer = entry.branch.clone();
buffer_entries.push(entry);
} else {
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
new_diffs.push(cx.new(|cx| {
let mut diff = BufferDiff::new(&branch_buffer.read(cx).snapshot(), cx);
let _ = diff.set_base_text_buffer(
location.buffer.clone(),
branch_buffer.read(cx).text_snapshot(),
cx,
);
diff
}));
buffer_entries.push(BufferEntry {
branch: branch_buffer.clone(),
base: location.buffer.clone(),
_subscription: cx.subscribe(&branch_buffer, Self::on_buffer_event),
});
}
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.push_excerpts(
branch_buffer,
location
.ranges
.into_iter()
.map(|range| ExcerptRange::new(range)),
cx,
);
});
}
self.buffer_entries = buffer_entries;
self.editor.update(cx, |editor, cx| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.refresh()
});
editor.buffer.update(cx, |buffer, cx| {
for diff in new_diffs {
buffer.add_diff(diff, cx)
}
})
});
}
pub fn recalculate_all_buffer_diffs(&self) {
for (ix, entry) in self.buffer_entries.iter().enumerate().rev() {
self.recalculate_diffs_tx
.unbounded_send(RecalculateDiff {
buffer: entry.branch.clone(),
debounce: ix > 0,
})
.ok();
}
}
fn on_buffer_event(
&mut self,
buffer: Entity<Buffer>,
event: &BufferEvent,
_cx: &mut Context<Self>,
) {
if let BufferEvent::Operation { .. } = event {
self.recalculate_diffs_tx
.unbounded_send(RecalculateDiff {
buffer,
debounce: true,
})
.ok();
}
}
}
impl Render for ProposedChangesEditor {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.key_context("ProposedChangesEditor")
.child(self.editor.clone())
}
}
impl Focusable for ProposedChangesEditor {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.editor.focus_handle(cx)
}
}
impl EventEmitter<EditorEvent> for ProposedChangesEditor {}
impl Item for ProposedChangesEditor {
type Event = EditorEvent;
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
Some(Icon::new(IconName::Diff))
}
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
self.title.clone()
}
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<gpui::AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
} else {
None
}
}
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
Item::added_to_workspace(editor, workspace, window, cx)
});
}
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor
.update(cx, |editor, cx| editor.deactivated(window, cx));
}
fn navigate(
&mut self,
data: Box<dyn std::any::Any>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
self.editor
.update(cx, |editor, cx| Item::navigate(editor, data, window, cx))
}
fn set_nav_history(
&mut self,
nav_history: workspace::ItemNavHistory,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
Item::set_nav_history(editor, nav_history, window, cx)
});
}
fn can_save(&self, cx: &App) -> bool {
self.editor.read(cx).can_save(cx)
}
fn save(
&mut self,
options: SaveOptions,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<anyhow::Result<()>> {
self.editor.update(cx, |editor, cx| {
Item::save(editor, options, project, window, cx)
})
}
}
impl ProposedChangesEditorToolbar {
pub fn new() -> Self {
Self {
current_editor: None,
}
}
fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
if self.current_editor.is_some() {
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
}
}
}
impl Render for ProposedChangesEditorToolbar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All"));
match &self.current_editor {
Some(editor) => {
let focus_handle = editor.focus_handle(cx);
let keybinding =
KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, window, cx)
.map(|binding| binding.into_any_element());
button_like.children(keybinding).on_click({
move |_event, window, cx| {
focus_handle.dispatch_action(&ApplyAllDiffHunks, window, cx)
}
})
}
None => button_like.disabled(true),
}
}
}
impl EventEmitter<ToolbarItemEvent> for ProposedChangesEditorToolbar {}
impl ToolbarItemView for ProposedChangesEditorToolbar {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> workspace::ToolbarItemLocation {
self.current_editor =
active_pane_item.and_then(|item| item.downcast::<ProposedChangesEditor>());
self.get_toolbar_item_location()
}
}
impl BranchBufferSemanticsProvider {
fn to_base(
&self,
buffer: &Entity<Buffer>,
positions: &[text::Anchor],
cx: &App,
) -> Option<Entity<Buffer>> {
let base_buffer = buffer.read(cx).base_buffer()?;
let version = base_buffer.read(cx).version();
if positions
.iter()
.any(|position| !version.observed(position.timestamp))
{
return None;
}
Some(base_buffer)
}
}
impl SemanticsProvider for BranchBufferSemanticsProvider {
fn hover(
&self,
buffer: &Entity<Buffer>,
position: text::Anchor,
cx: &mut App,
) -> Option<Task<Option<Vec<project::Hover>>>> {
let buffer = self.to_base(buffer, &[position], cx)?;
self.0.hover(&buffer, position, cx)
}
fn inlay_hints(
&self,
buffer: Entity<Buffer>,
range: Range<text::Anchor>,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?;
self.0.inlay_hints(buffer, range, cx)
}
fn inline_values(
&self,
_: Entity<Buffer>,
_: Range<text::Anchor>,
_: &mut App,
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
None
}
fn resolve_inlay_hint(
&self,
hint: project::InlayHint,
buffer: Entity<Buffer>,
server_id: lsp::LanguageServerId,
cx: &mut App,
) -> Option<Task<anyhow::Result<project::InlayHint>>> {
let buffer = self.to_base(&buffer, &[], cx)?;
self.0.resolve_inlay_hint(hint, buffer, server_id, cx)
}
fn supports_inlay_hints(&self, buffer: &Entity<Buffer>, cx: &mut App) -> bool {
if let Some(buffer) = self.to_base(buffer, &[], cx) {
self.0.supports_inlay_hints(&buffer, cx)
} else {
false
}
}
fn document_highlights(
&self,
buffer: &Entity<Buffer>,
position: text::Anchor,
cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<project::DocumentHighlight>>>> {
let buffer = self.to_base(buffer, &[position], cx)?;
self.0.document_highlights(&buffer, position, cx)
}
fn definitions(
&self,
buffer: &Entity<Buffer>,
position: text::Anchor,
kind: crate::GotoDefinitionKind,
cx: &mut App,
) -> Option<Task<anyhow::Result<Option<Vec<project::LocationLink>>>>> {
let buffer = self.to_base(buffer, &[position], cx)?;
self.0.definitions(&buffer, position, kind, cx)
}
fn range_for_rename(
&self,
_: &Entity<Buffer>,
_: text::Anchor,
_: &mut App,
) -> Option<Task<anyhow::Result<Option<Range<text::Anchor>>>>> {
None
}
fn perform_rename(
&self,
_: &Entity<Buffer>,
_: text::Anchor,
_: String,
_: &mut App,
) -> Option<Task<anyhow::Result<project::ProjectTransaction>>> {
None
}
}

View File

@@ -107,6 +107,18 @@ impl GitRepository for FakeGitRepository {
.boxed()
}
fn diff_to_commit(
&self,
_commit: String,
_cx: AsyncApp,
) -> BoxFuture<'_, Result<git::repository::CommitDiff>> {
unimplemented!()
}
fn merge_base(&self, _commit_a: String, _commit_b: String) -> BoxFuture<'_, Option<String>> {
unimplemented!()
}
fn load_commit(
&self,
_commit: String,

View File

@@ -309,6 +309,8 @@ pub trait GitRepository: Send + Sync {
/// Also returns `None` for symlinks.
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>>;
fn merge_base(&self, commit_a: String, commit_b: String) -> BoxFuture<'_, Option<String>>;
fn set_index_text(
&self,
path: RepoPath,
@@ -360,6 +362,7 @@ pub trait GitRepository: Send + Sync {
fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>>;
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>>;
fn diff_to_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>>;
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>>;
/// Returns the absolute path to the repository. For worktrees, this will be the path to the
@@ -614,6 +617,115 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn merge_base(&self, commit_a: String, commit_b: String) -> BoxFuture<'_, Option<String>> {
let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
else {
return future::ready(None).boxed();
};
let git = GitBinary::new(
self.git_binary_path.clone(),
working_directory,
self.executor.clone(),
);
async move {
let merge_base = git
.run(&["merge-base", &commit_a, &commit_b])
.await
.log_err()?;
Some(merge_base.to_string())
}
.boxed()
}
fn diff_to_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>> {
let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
else {
return future::ready(Err(anyhow!("no working directory"))).boxed();
};
cx.background_spawn(async move {
let diff_output = util::command::new_std_command("git")
.current_dir(&working_directory)
.args([
"--no-optional-locks",
"diff",
"--format=%P",
"-z",
"--no-renames",
"--name-status",
])
.arg(&commit)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.context("starting git show process")?;
let diff_stdout = String::from_utf8_lossy(&diff_output.stdout);
dbg!(&diff_stdout);
let changes = parse_git_diff_name_status(&diff_stdout);
let mut cat_file_process = util::command::new_std_command("git")
.current_dir(&working_directory)
.args(["--no-optional-locks", "cat-file", "--batch=%(objectsize)"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("starting git cat-file process")?;
use std::io::Write as _;
let mut files = Vec::<CommitFile>::new();
let mut stdin = BufWriter::with_capacity(512, cat_file_process.stdin.take().unwrap());
let mut stdout = BufReader::new(cat_file_process.stdout.take().unwrap());
let mut info_line = String::new();
let mut newline = [b'\0'];
for (path, status_code) in changes {
match status_code {
StatusCode::Modified => {
writeln!(&mut stdin, "{commit}:{}", path.display())?;
}
StatusCode::Added => {
files.push(CommitFile {
path: path.into(),
old_text: None,
new_text: None,
});
continue;
}
StatusCode::Deleted => {
writeln!(&mut stdin, "{commit}:{}", path.display())?;
}
_ => continue,
}
stdin.flush()?;
info_line.clear();
stdout.read_line(&mut info_line)?;
dbg!(&info_line);
let len = info_line.trim_end().parse().with_context(|| {
format!("invalid object size output from cat-file {info_line}")
})?;
let mut text = vec![0; len];
stdout.read_exact(&mut text)?;
stdout.read_exact(&mut newline)?;
let text = String::from_utf8_lossy(&text).to_string();
dbg!(&text);
files.push(CommitFile {
path: path.into(),
old_text: Some(text),
new_text: None,
})
}
Ok(CommitDiff { files })
})
.boxed()
}
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>> {
let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
else {

View File

@@ -3,7 +3,7 @@ use crate::commit_modal::CommitModal;
use crate::commit_tooltip::CommitTooltip;
use crate::commit_view::CommitView;
use crate::git_panel_settings::StatusStyle;
use crate::project_diff::{self, Diff, ProjectDiff};
use crate::project_diff::{self, Diff, DiffBaseKind, ProjectDiff};
use crate::remote_output::{self, RemoteAction, SuccessMessage};
use crate::{branch_picker, picker_prompt, render_remote_button};
use crate::{
@@ -937,7 +937,13 @@ impl GitPanel {
self.workspace
.update(cx, |workspace, cx| {
ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
ProjectDiff::deploy_at(
workspace,
DiffBaseKind::Head,
Some(entry.clone()),
window,
cx,
);
})
.ok();
self.focus_handle.focus(window);

View File

@@ -5,7 +5,7 @@ use crate::{
remote_button::{render_publish_button, render_push_button},
};
use anyhow::Result;
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
use buffer_diff::{BufferDiff, BufferDiffSnapshot, DiffHunkSecondaryStatus};
use collections::HashSet;
use editor::{
Editor, EditorEvent, SelectionEffects,
@@ -17,7 +17,7 @@ use futures::StreamExt;
use git::{
Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
status::FileStatus,
status::{FileStatus, TrackedStatus},
};
use gpui::{
Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter,
@@ -30,8 +30,11 @@ use project::{
git_store::{GitStore, GitStoreEvent, RepositoryEvent},
};
use settings::{Settings, SettingsStore};
use std::any::{Any, TypeId};
use std::ops::Range;
use std::{
any::{Any, TypeId},
sync::Arc,
};
use theme::ActiveTheme;
use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::ResultExt as _;
@@ -48,7 +51,9 @@ actions!(
/// Shows the diff between the working directory and the index.
Diff,
/// Adds files to the git staging area.
Add
Add,
/// Shows the diff between the working directory and the default branch.
DiffToDefaultBranch,
]
);
@@ -61,10 +66,17 @@ pub struct ProjectDiff {
focus_handle: FocusHandle,
update_needed: postage::watch::Sender<()>,
pending_scroll: Option<PathKey>,
diff_base_kind: DiffBaseKind,
_task: Task<Result<()>>,
_subscription: Subscription,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum DiffBaseKind {
Head,
MergeBaseOfDefaultBranch,
}
#[derive(Debug)]
struct DiffBuffer {
path_key: PathKey,
@@ -80,6 +92,7 @@ const NEW_NAMESPACE: u32 = 3;
impl ProjectDiff {
pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) {
workspace.register_action(Self::deploy);
workspace.register_action(Self::diff_to_default_branch);
workspace.register_action(|workspace, _: &Add, window, cx| {
Self::deploy(workspace, &Diff, window, cx);
});
@@ -92,39 +105,66 @@ impl ProjectDiff {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
Self::deploy_at(workspace, None, window, cx)
Self::deploy_at(workspace, DiffBaseKind::Head, None, window, cx)
}
fn diff_to_default_branch(
workspace: &mut Workspace,
_: &DiffToDefaultBranch,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
Self::deploy_at(
workspace,
DiffBaseKind::MergeBaseOfDefaultBranch,
None,
window,
cx,
);
}
pub fn deploy_at(
workspace: &mut Workspace,
diff_base_kind: DiffBaseKind,
entry: Option<GitStatusEntry>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
telemetry::event!(
"Git Diff Opened",
match diff_base_kind {
DiffBaseKind::MergeBaseOfDefaultBranch => "Git Branch Diff Opened",
DiffBaseKind::Head => "Git Diff Opened",
},
source = if entry.is_some() {
"Git Panel"
} else {
"Action"
}
);
let project_diff = if let Some(existing) = workspace.item_of_type::<Self>(cx) {
workspace.activate_item(&existing, true, true, window, cx);
existing
} else {
let workspace_handle = cx.entity();
let project_diff =
cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx));
workspace.add_item_to_active_pane(
Box::new(project_diff.clone()),
None,
true,
window,
cx,
);
project_diff
};
let project_diff =
if let Some(existing) = Self::existing_project_diff(workspace, diff_base_kind, cx) {
workspace.activate_item(&existing, true, true, window, cx);
existing
} else {
let workspace_handle = cx.entity();
let project_diff = cx.new(|cx| {
Self::new(
workspace.project().clone(),
workspace_handle,
diff_base_kind,
window,
cx,
)
});
workspace.add_item_to_active_pane(
Box::new(project_diff.clone()),
None,
true,
window,
cx,
);
project_diff
};
if let Some(entry) = entry {
project_diff.update(cx, |project_diff, cx| {
project_diff.move_to_entry(entry, window, cx);
@@ -132,6 +172,16 @@ impl ProjectDiff {
}
}
pub fn existing_project_diff(
workspace: &mut Workspace,
diff_base_kind: DiffBaseKind,
cx: &mut Context<Workspace>,
) -> Option<Entity<Self>> {
workspace
.items_of_type::<Self>(cx)
.find(|item| item.read(cx).diff_base_kind == diff_base_kind)
}
pub fn autoscroll(&self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.request_autoscroll(Autoscroll::fit(), cx);
@@ -141,6 +191,7 @@ impl ProjectDiff {
fn new(
project: Entity<Project>,
workspace: Entity<Workspace>,
diff_base_kind: DiffBaseKind,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -152,9 +203,21 @@ impl ProjectDiff {
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);
diff_display_editor.register_addon(GitPanelAddon {
workspace: workspace.downgrade(),
});
match diff_base_kind {
DiffBaseKind::Head => {
diff_display_editor.register_addon(GitPanelAddon {
workspace: workspace.downgrade(),
});
}
DiffBaseKind::MergeBaseOfDefaultBranch => {
diff_display_editor.start_temporary_diff_override();
diff_display_editor.set_render_diff_hunk_controls(
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
cx,
);
//
}
}
diff_display_editor
});
window.defer(cx, {
@@ -205,7 +268,7 @@ impl ProjectDiff {
let (mut send, recv) = postage::watch::channel::<()>();
let worker = window.spawn(cx, {
let this = cx.weak_entity();
async |cx| Self::handle_status_updates(this, recv, cx).await
async move |cx| Self::handle_status_updates(this, diff_base_kind, recv, cx).await
});
// Kick off a refresh immediately
*send.borrow_mut() = ();
@@ -214,6 +277,7 @@ impl ProjectDiff {
project,
git_store: git_store.clone(),
workspace: workspace.downgrade(),
diff_base_kind,
focus_handle,
editor,
multibuffer,
@@ -351,6 +415,9 @@ impl ProjectDiff {
let Some(project_path) = self.active_path(cx) else {
return;
};
if self.diff_base_kind != DiffBaseKind::Head {
return;
}
self.workspace
.update(cx, |workspace, cx| {
if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {
@@ -506,21 +573,149 @@ impl ProjectDiff {
}
}
fn refresh_merge_base_of_default_branch(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let project = self.project.clone();
let language_registry = project.read(cx).languages().clone();
let Some(repo) = self.git_store.read(cx).active_repository() else {
self.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.clear(cx);
});
return Task::ready(Ok(()));
};
let default_branch = repo.update(cx, |repo, _| repo.default_branch());
cx.spawn_in(window, async move |this, cx| {
let Some(default_branch) = default_branch.await?? else {
return Ok(());
};
let Some(merge_base) = repo
.update(cx, |repo, _| {
repo.merge_base("HEAD".to_string(), default_branch.into())
})?
.await?
else {
return Ok(());
};
let diff = repo
.update(cx, |repo, _| repo.diff_to_commit(merge_base))?
.await??;
for file in diff.files {
let Some(path) = repo.update(cx, |repo, cx| {
repo.repo_path_to_project_path(&file.path, cx)
})?
else {
continue;
};
let open_buffer = project
.update(cx, |project, cx| project.open_buffer(path.clone(), cx))?
.await;
let mut status = FileStatus::Tracked(TrackedStatus {
index_status: git::status::StatusCode::Unmodified,
worktree_status: git::status::StatusCode::Modified,
});
let buffer = match open_buffer {
Ok(buffer) => buffer,
Err(err) => {
let exists = project.read_with(cx, |project, cx| {
project.entry_for_path(&path, cx).is_some()
})?;
if exists {
return Err(err);
}
status = FileStatus::Tracked(TrackedStatus {
index_status: git::status::StatusCode::Unmodified,
worktree_status: git::status::StatusCode::Deleted,
});
cx.new(|cx| Buffer::local("", cx))?
}
};
let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let namespace = if file.old_text.is_none() {
NEW_NAMESPACE
} else {
TRACKED_NAMESPACE
};
let buffer_diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, cx))?;
buffer_diff
.update(cx, |buffer_diff, cx| {
buffer_diff.set_base_text(
file.old_text.map(Arc::new),
buffer_snapshot.language().cloned(),
Some(language_registry.clone()),
buffer_snapshot.text,
cx,
)
})?
.await?;
this.read_with(cx, |this, cx| {
BufferDiffSnapshot::new_with_base_buffer(
buffer.clone(),
base_text,
this.base_text().clone(),
cx,
)
})?
.await;
this.update_in(cx, |this, window, cx| {
this.multibuffer.update(cx, |multibuffer, cx| {
multibuffer.add_diff(buffer_diff.clone(), cx);
});
this.register_buffer(
DiffBuffer {
path_key: PathKey::namespaced(namespace, file.path.0),
buffer,
diff: buffer_diff,
file_status: status,
},
window,
cx,
);
})?;
}
Ok(())
})
}
pub async fn handle_status_updates(
this: WeakEntity<Self>,
diff_base_kind: DiffBaseKind,
mut recv: postage::watch::Receiver<()>,
cx: &mut AsyncWindowContext,
) -> Result<()> {
while (recv.next().await).is_some() {
let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?;
for buffer_to_load in buffers_to_load {
if let Some(buffer) = buffer_to_load.await.log_err() {
cx.update(|window, cx| {
this.update(cx, |this, cx| this.register_buffer(buffer, window, cx))
.ok();
})?;
match diff_base_kind {
DiffBaseKind::Head => {
let buffers_to_load = this.update(cx, |this, cx| this.load_buffers(cx))?;
for buffer_to_load in buffers_to_load {
if let Some(buffer) = buffer_to_load.await.log_err() {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.register_buffer(buffer, window, cx)
})
.ok();
})?;
}
}
}
}
DiffBaseKind::MergeBaseOfDefaultBranch => {
this.update_in(cx, |this, window, cx| {
this.refresh_merge_base_of_default_branch(window, cx)
})?
.await
.log_err();
}
};
this.update(cx, |this, cx| {
this.pending_scroll.take();
cx.notify();
@@ -637,7 +832,15 @@ impl Item for ProjectDiff {
Self: Sized,
{
let workspace = self.workspace.upgrade()?;
Some(cx.new(|cx| ProjectDiff::new(self.project.clone(), workspace, window, cx)))
Some(cx.new(|cx| {
ProjectDiff::new(
self.project.clone(),
workspace,
self.diff_base_kind,
window,
cx,
)
}))
}
fn is_dirty(&self, cx: &App) -> bool {
@@ -805,7 +1008,16 @@ impl SerializableItem for ProjectDiff {
window.spawn(cx, async move |cx| {
workspace.update_in(cx, |workspace, window, cx| {
let workspace_handle = cx.entity();
cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx))
// todo!()
cx.new(|cx| {
Self::new(
workspace.project().clone(),
workspace_handle,
DiffBaseKind::Head,
window,
cx,
)
})
})
})
}
@@ -1399,7 +1611,7 @@ mod tests {
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)
ProjectDiff::new(project.clone(), workspace, DiffBaseKind::Head, window, cx)
});
cx.run_until_parked();
@@ -1454,7 +1666,7 @@ mod tests {
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)
ProjectDiff::new(project.clone(), workspace, DiffBaseKind::Head, window, cx)
});
cx.run_until_parked();
@@ -1536,7 +1748,7 @@ mod tests {
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
});
let diff = cx.new_window_entity(|window, cx| {
ProjectDiff::new(project.clone(), workspace, window, cx)
ProjectDiff::new(project.clone(), workspace, DiffBaseKind::Head, window, cx)
});
cx.run_until_parked();
@@ -1827,7 +2039,7 @@ mod tests {
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)
ProjectDiff::new(project.clone(), workspace, DiffBaseKind::Head, window, cx)
});
cx.run_until_parked();

View File

@@ -1249,15 +1249,6 @@ impl BufferStore {
.log_err();
}
// TODO(max): do something
// client
// .send(proto::UpdateStagedText {
// project_id,
// buffer_id: buffer_id.into(),
// diff_base: buffer.diff_base().map(ToString::to_string),
// })
// .log_err();
client
.send(proto::BufferReloaded {
project_id,

View File

@@ -91,6 +91,7 @@ struct SharedDiffs {
struct BufferGitState {
unstaged_diff: Option<WeakEntity<BufferDiff>>,
uncommitted_diff: Option<WeakEntity<BufferDiff>>,
branch_diff: Option<WeakEntity<BufferDiff>>,
conflict_set: Option<WeakEntity<ConflictSet>>,
recalculate_diff_task: Option<Task<Result<()>>>,
reparse_conflict_markers_task: Option<Task<Result<()>>>,
@@ -592,6 +593,12 @@ impl GitStore {
cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
}
pub fn open_diff_from_default_branch(
&mut self,
cx: &mut Context<Self>,
) -> Task<Result<Vec<(Entity<Buffer>, Entity<BufferDiff>)>>> {
}
pub fn open_uncommitted_diff(
&mut self,
buffer: Entity<Buffer>,
@@ -670,11 +677,10 @@ impl GitStore {
let text_snapshot = buffer.text_snapshot();
this.loading_diffs.remove(&(buffer_id, kind));
let git_store = cx.weak_entity();
let diff_state = this
.diffs
.entry(buffer_id)
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
.or_insert_with(|| cx.new(|_| BufferGitState::new()));
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
@@ -755,11 +761,10 @@ impl GitStore {
let is_unmerged = self
.repository_and_path_for_buffer_id(buffer_id, cx)
.is_some_and(|(repo, path)| repo.read(cx).snapshot.has_conflict(&path));
let git_store = cx.weak_entity();
let buffer_git_state = self
.diffs
.entry(buffer_id)
.or_insert_with(|| cx.new(|_| BufferGitState::new(git_store)));
.or_insert_with(|| cx.new(|_| BufferGitState::new()));
let conflict_set = cx.new(|cx| ConflictSet::new(buffer_id, is_unmerged, cx));
self._subscriptions
@@ -2347,10 +2352,11 @@ impl GitStore {
}
impl BufferGitState {
fn new(_git_store: WeakEntity<GitStore>) -> Self {
fn new() -> Self {
Self {
unstaged_diff: Default::default(),
uncommitted_diff: Default::default(),
branch_diff: Default::default(),
recalculate_diff_task: Default::default(),
language: Default::default(),
language_registry: Default::default(),
@@ -3467,6 +3473,58 @@ impl Repository {
})
}
pub fn merge_base(
&mut self,
commit_a: String,
commit_b: String,
) -> oneshot::Receiver<Option<String>> {
let id = self.id;
self.send_job(None, move |git_repo, cx| async move {
match git_repo {
RepositoryState::Local { backend, .. } => {
backend.merge_base(commit_a, commit_b).await
}
RepositoryState::Remote {
client, project_id, ..
} => {
todo!();
}
}
})
}
pub fn diff_to_commit(&mut self, commit: String) -> oneshot::Receiver<Result<CommitDiff>> {
let id = self.id;
self.send_job(None, move |git_repo, cx| async move {
match git_repo {
RepositoryState::Local { backend, .. } => backend.diff_to_commit(commit, cx).await,
RepositoryState::Remote {
client, project_id, ..
} => {
todo!();
let response = client
.request(proto::LoadCommitDiff {
project_id: project_id.0,
repository_id: id.to_proto(),
commit,
})
.await?;
Ok(CommitDiff {
files: response
.files
.into_iter()
.map(|file| CommitFile {
path: Path::new(&file.path).into(),
old_text: file.old_text,
new_text: file.new_text,
})
.collect(),
})
}
}
})
}
fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
}

View File

@@ -17,7 +17,6 @@ use breadcrumbs::Breadcrumbs;
use client::zed_urls;
use collections::VecDeque;
use debugger_ui::debugger_panel::DebugPanel;
use editor::ProposedChangesEditorToolbar;
use editor::{Editor, MultiBuffer};
use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag};
use futures::future::Either;
@@ -980,8 +979,6 @@ fn initialize_pane(
)
});
toolbar.add_item(buffer_search_bar.clone(), window, cx);
let proposed_change_bar = cx.new(|_| ProposedChangesEditorToolbar::new());
toolbar.add_item(proposed_change_bar, window, cx);
let quick_action_bar =
cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
toolbar.add_item(quick_action_bar, window, cx);