diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index a29c54f759..40bf9739b5 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -30,10 +30,11 @@ use git::{ TrashUntrackedFiles, UnstageAll, }; use gpui::{ - Action, AsyncApp, AsyncWindowContext, ClickEvent, Corner, DismissEvent, Entity, EventEmitter, - FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, - MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task, - UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list, + Action, AppContext, AsyncApp, AsyncWindowContext, ClickEvent, Corner, DismissEvent, Entity, + EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, + ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, + Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, + uniform_list, }; use itertools::Itertools; use language::{Buffer, File}; @@ -311,6 +312,9 @@ pub struct GitPanel { bulk_staging: Option, stash_entries: GitStash, _settings_subscription: Subscription, + /// On clicking an entry in a the git_panel this will + /// trigger loading it + open_diff_task: Option>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -471,6 +475,7 @@ impl GitPanel { bulk_staging: None, stash_entries: Default::default(), _settings_subscription, + open_diff_task: None, }; this.schedule_update(window, cx); @@ -750,11 +755,25 @@ impl GitPanel { fn open_diff(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { maybe!({ - let entry = self.entries.get(self.selected_entry?)?.status_entry()?; + let entry = self + .entries + .get(self.selected_entry?)? + .status_entry()? + .clone(); let workspace = self.workspace.upgrade()?; - let git_repo = self.active_repository.as_ref()?; + let git_repo = self.active_repository.as_ref()?.clone(); + let focus_handle = self.focus_handle.clone(); - if let Some(project_diff) = workspace.read(cx).active_item_as::(cx) + // let panel = panel.upgrade().unwrap(); // TODO FIXME + // cx.read_entity(&panel, |panel, cx| { + // panel + // }) + // .unwrap(); // TODO FIXME + + // how do we get the projectdiff here? + + let project_diff = if let Some(project_diff) = + workspace.read(cx).active_item_as::(cx) && let Some(project_path) = project_diff.read(cx).active_path(cx) && Some(&entry.repo_path) == git_repo @@ -764,16 +783,20 @@ impl GitPanel { { project_diff.focus_handle(cx).focus(window); project_diff.update(cx, |project_diff, cx| project_diff.autoscroll(cx)); - return None; - }; - - self.workspace - .update(cx, |workspace, cx| { - ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx); + project_diff + } else { + workspace.update(cx, |workspace, cx| { + ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx) }) - .ok(); - self.focus_handle.focus(window); + }; + focus_handle.focus(window); // TODO: should we focus before the file is loaded or wait for that? + let project_diff = project_diff.downgrade(); + self.open_diff_task = Some(cx.spawn_in(window, async move |_, cx| { + ProjectDiff::refresh_one(project_diff, entry.repo_path, entry.status, cx) + .await + .unwrap(); // TODO FIXME + })); Some(()) }); } @@ -807,11 +830,7 @@ impl GitPanel { .notify_async_err(&mut cx) .ok_or_else(|| anyhow::anyhow!("Failed to open file"))?; if let Some(active_editor) = item.downcast::() { - // active_editor.update(cx, |editor, _cx| editor.load_diff())?; if let Some(diff_task) = - // this needs to become load the diff - // this just waits for the load_diff_task to be completed - // later we will make all files load for small diffs active_editor.update(cx, |editor, _cx| editor.wait_for_diff_to_load())? { diff_task.await; diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index b234f9d3b2..232214e077 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -14,7 +14,7 @@ use editor::{ multibuffer_context_lines, scroll::Autoscroll, }; -use futures::{FutureExt, stream::FuturesUnordered}; +use futures::stream::FuturesUnordered; use git::{ Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext, repository::{Branch, RepoPath, Upstream, UpstreamTracking, UpstreamTrackingStatus}, @@ -29,7 +29,7 @@ use multi_buffer::{MultiBuffer, PathKey}; use project::{ Project, ProjectPath, git_store::{ - self, Repository, + self, Repository, StatusEntry, branch_diff::{self, BranchDiffEvent, DiffBase}, }, }; @@ -97,7 +97,7 @@ impl ProjectDiff { window: &mut Window, cx: &mut Context, ) { - Self::deploy_at(workspace, None, window, cx) + Self::deploy_at(workspace, None, window, cx); } fn deploy_branch_diff( @@ -139,7 +139,7 @@ impl ProjectDiff { entry: Option, window: &mut Window, cx: &mut Context, - ) { + ) -> Entity { telemetry::event!( "Git Diff Opened", source = if entry.is_some() { @@ -171,7 +171,8 @@ impl ProjectDiff { project_diff.update(cx, |project_diff, cx| { project_diff.move_to_entry(entry, window, cx); }) - } + }; + project_diff } pub fn autoscroll(&self, cx: &mut Context) { @@ -277,12 +278,14 @@ impl ProjectDiff { window, move |this, _git_store, event, window, cx| match event { BranchDiffEvent::FileListChanged => { - if this.number_of_paths(cx) < 100 { - this._task = window.spawn(cx, { - let this = cx.weak_entity(); - async |cx| Self::refresh(this, cx).await - }) - } + // TODO this does not account for size of paths + // maybe a quick fs metadata could get us info on that? + // would make number of paths async but thats fine here + let entries = this.first_n_entries(cx, 100); + this._task = window.spawn(cx, { + let this = cx.weak_entity(); + async |cx| Self::refresh(this, entries, cx).await + }) } }, ); @@ -310,9 +313,18 @@ impl ProjectDiff { }) .detach(); + // let entries = cx.read_entity(&cx.entity(), |project_diff, cx| { + // project_diff.first_n_entries(cx, 100) + // }); + let task = window.spawn(cx, { let this = cx.weak_entity(); - async |cx| Self::refresh(this, cx).await + async |cx| { + let entries = this + .read_with(cx, |project_diff, cx| project_diff.first_n_entries(cx, 100)) + .unwrap(); + Self::refresh(this, entries, cx).await + } }); Self { @@ -479,10 +491,11 @@ impl ProjectDiff { cx: &mut Context, ) { let subscription = cx.subscribe_in(&diff, window, move |this, _, _, window, cx| { - this._task = window.spawn(cx, { - let this = cx.weak_entity(); - async |cx| Self::refresh(this, cx).await - }) + // TODO fix this + // this._task = window.spawn(cx, { + // let this = cx.weak_entity(); + // async |cx| Self::refresh(this, cx).await + // }) }); self.buffer_diff_subscriptions .insert(path_key.path.clone(), (diff.clone(), subscription)); @@ -558,64 +571,21 @@ impl ProjectDiff { } } - pub async fn old_refresh(this: WeakEntity, cx: &mut AsyncWindowContext) -> Result<()> { - let mut path_keys = Vec::new(); - let buffers_to_load = this.update(cx, |this, cx| { - let (repo, buffers_to_load) = this.branch_diff.update(cx, |branch_diff, cx| { - let load_buffers = branch_diff.load_buffers(cx); - (branch_diff.repo().cloned(), load_buffers) - }); - let mut previous_paths = this.multibuffer.read(cx).paths().collect::>(); - - if let Some(repo) = repo { - let repo = repo.read(cx); - - path_keys = Vec::with_capacity(buffers_to_load.len()); - for entry in buffers_to_load.iter() { - let sort_prefix = sort_prefix(&repo, &entry.repo_path, entry.file_status, cx); - let path_key = - PathKey::with_sort_prefix(sort_prefix, entry.repo_path.as_ref().clone()); - previous_paths.remove(&path_key); - path_keys.push(path_key) - } - } - - this.multibuffer.update(cx, |multibuffer, cx| { - for path in previous_paths { - this.buffer_diff_subscriptions.remove(&path.path); - multibuffer.remove_excerpts_for_path(path, cx); - } - }); - buffers_to_load - })?; - - for (entry, path_key) in buffers_to_load.into_iter().zip(path_keys.into_iter()) { - if let Some((buffer, diff)) = entry.load.await.log_err() { - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.register_buffer(path_key, entry.file_status, buffer, diff, window, cx) - }) - .ok(); - })?; - } - } - this.update(cx, |this, cx| { - this.pending_scroll.take(); - cx.notify(); - })?; - - Ok(()) - } - - pub fn number_of_paths(&self, cx: &App) -> usize { + pub fn first_n_entries(&self, cx: &App, n: usize) -> Vec { let Some(ref repo) = self.branch_diff.read(cx).repo else { - return 0; + return Vec::new(); }; - repo.read(cx).cached_status().count() + repo.read(cx).cached_status().take(n).collect() } - pub async fn refresh(this: WeakEntity, cx: &mut AsyncWindowContext) -> Result<()> { + pub async fn refresh_one( + this: WeakEntity, + repo_path: RepoPath, + status: FileStatus, + cx: &mut AsyncWindowContext, + ) -> Result<()> { use git_store::branch_diff::BranchDiff; + let Some(this) = this.upgrade() else { return Ok(()); }; @@ -627,8 +597,90 @@ impl ProjectDiff { }; let project = cx.read_entity(&branch_diff, |bd, _| bd.project.clone())?; - let cached_status: Vec = - cx.read_entity(&repo, |repo: &Repository, _| repo.cached_status().collect())?; + let mut previous_paths = + cx.read_entity(&multibuffer, |mb, _| mb.paths().collect::>())?; + + let tree_diff_status = cx.read_entity(&branch_diff, |branch_diff, _| { + branch_diff + .tree_diff + .as_ref() + .and_then(|t| t.entries.get(&repo_path)) + .cloned() + })?; + + let Some(status) = cx.read_entity(&branch_diff, |bd, _| { + bd.merge_statuses(Some(status), tree_diff_status.as_ref()) + })? + else { + return Ok(()); + }; + if !status.has_changes() { + return Ok(()); + } + + let Some(project_path) = cx.read_entity(&repo, |repo, cx| { + repo.repo_path_to_project_path(&repo_path, cx) + })? + else { + return Ok(()); + }; + + let sort_prefix = + cx.read_entity(&repo, |repo, cx| sort_prefix(repo, &repo_path, status, cx))?; + + let path_key = PathKey::with_sort_prefix(sort_prefix, repo_path.into_arc()); + previous_paths.remove(&path_key); + + let repo = repo.clone(); + let Some((buffer, diff)) = BranchDiff::load_buffer( + tree_diff_status, + project_path, + repo, + project.downgrade(), + &mut cx.to_app(), + ) + .await + .log_err() else { + return Ok(()); + }; + + cx.update(|window, cx| { + this.update(cx, |this, cx| { + this.register_buffer(path_key, status, buffer, diff, window, cx) + }); + })?; + + // TODO LL clear multibuff on open? + // // remove anything not part of the diff in the multibuffer + // this.update(cx, |this, cx| { + // multibuffer.update(cx, |multibuffer, cx| { + // for path in previous_paths { + // this.buffer_diff_subscriptions.remove(&path.path); + // multibuffer.remove_excerpts_for_path(path, cx); + // } + // }); + // })?; + + Ok(()) + } + + pub async fn refresh( + this: WeakEntity, + cached_status: Vec, + cx: &mut AsyncWindowContext, + ) -> Result<()> { + dbg!("refreshing all"); + use git_store::branch_diff::BranchDiff; + let Some(this) = this.upgrade() else { + return Ok(()); + }; + let multibuffer = cx.read_entity(&this, |this, _| this.multibuffer.clone())?; + let branch_diff = cx.read_entity(&this, |pd, _| pd.branch_diff.clone())?; + + let Some(repo) = cx.read_entity(&branch_diff, |bd, _| bd.repo.clone())? else { + return Ok(()); + }; + let project = cx.read_entity(&branch_diff, |bd, _| bd.project.clone())?; let mut previous_paths = cx.read_entity(&multibuffer, |mb, _| mb.paths().collect::>())?; @@ -674,10 +726,18 @@ impl ProjectDiff { previous_paths.remove(&path_key); let repo = repo.clone(); - let task = project.update(cx, move |_, cx| { - BranchDiff::load_buffer(tree_diff_status, project_path, repo, cx) - .map(move |res| (res, path_key, entry.status)) - })?; + let project = project.downgrade(); + let task = cx.spawn(async move |cx| { + let res = BranchDiff::load_buffer( + tree_diff_status, + project_path, + repo, + project, + &mut cx.to_app(), + ) + .await; + (res, path_key, entry.status) + }); tasks.push(task) } diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index 381541d4b1..eb01fedebf 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -310,6 +310,11 @@ impl AsyncWindowContext { .update(self, |_, window, cx| read(cx.global(), window, cx)) } + /// Returns an `AsyncApp` by cloning the one used by Self + pub fn to_app(&self) -> AsyncApp { + self.app.clone() + } + /// A convenience method for [`App::update_global`](BorrowAppContext::update_global). /// for updating the global state of the specified type. pub fn update_global( diff --git a/crates/gpui/src/app/context.rs b/crates/gpui/src/app/context.rs index 41d6cac82b..c0ca5462fd 100644 --- a/crates/gpui/src/app/context.rs +++ b/crates/gpui/src/app/context.rs @@ -233,6 +233,9 @@ impl<'a, T: 'static> Context<'a, T> { /// Spawn the future returned by the given function. /// The function is provided a weak handle to the entity owned by this context and a context that can be held across await points. /// The returned task must be held or detached. + /// + /// # Example + /// `cx.spawn(async move |some_weak_entity, cx| ...)` #[track_caller] pub fn spawn(&self, f: AsyncFn) -> Task where diff --git a/crates/project/src/git_store/branch_diff.rs b/crates/project/src/git_store/branch_diff.rs index 95af4b552f..0f37e6d93b 100644 --- a/crates/project/src/git_store/branch_diff.rs +++ b/crates/project/src/git_store/branch_diff.rs @@ -283,7 +283,11 @@ impl BranchDiff { else { continue; }; - let task = Self::load_buffer(branch_diff, project_path, repo.clone(), cx); + + let repo = repo.clone(); + let task = cx.spawn(async move |project, cx| { + Self::load_buffer(branch_diff, project_path, repo.clone(), project, cx).await + }); output.push(DiffBuffer { repo_path: item.repo_path.clone(), @@ -303,8 +307,11 @@ impl BranchDiff { let Some(project_path) = repo.read(cx).repo_path_to_project_path(&path, cx) else { continue; }; - let task = - Self::load_buffer(Some(branch_diff.clone()), project_path, repo.clone(), cx); + let repo = repo.clone(); + let branch_diff2 = Some(branch_diff.clone()); + let task = cx.spawn(async move |project, cx| { + Self::load_buffer(branch_diff2, project_path, repo, project, cx).await + }); let file_status = diff_status_to_file_status(branch_diff); @@ -318,42 +325,40 @@ impl BranchDiff { output } - pub fn load_buffer( + pub async fn load_buffer( branch_diff: Option, project_path: crate::ProjectPath, repo: Entity, - cx: &Context<'_, Project>, - ) -> Task, Entity)>> { - let task = cx.spawn(async move |project, cx| { - let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx))? - .await?; + project: WeakEntity, + cx: &mut gpui::AsyncApp, // making this generic over AppContext hangs the compiler + ) -> Result<(Entity, Entity)> { + let buffer = project + .update(cx, |project, cx| project.open_buffer(project_path, cx))? + .await?; - let languages = project.update(cx, |project, _cx| project.languages().clone())?; + let languages = project.update(cx, |project, _cx| project.languages().clone())?; - let changes = if let Some(entry) = branch_diff { - let oid = match entry { - git::status::TreeDiffStatus::Added { .. } => None, - git::status::TreeDiffStatus::Modified { old, .. } - | git::status::TreeDiffStatus::Deleted { old } => Some(old), - }; - project - .update(cx, |project, cx| { - project.git_store().update(cx, |git_store, cx| { - git_store.open_diff_since(oid, buffer.clone(), repo, languages, cx) - }) - })? - .await? - } else { - project - .update(cx, |project, cx| { - project.open_uncommitted_diff(buffer.clone(), cx) - })? - .await? + let changes = if let Some(entry) = branch_diff { + let oid = match entry { + git::status::TreeDiffStatus::Added { .. } => None, + git::status::TreeDiffStatus::Modified { old, .. } + | git::status::TreeDiffStatus::Deleted { old } => Some(old), }; - Ok((buffer, changes)) - }); - task + project + .update(cx, |project, cx| { + project.git_store().update(cx, |git_store, cx| { + git_store.open_diff_since(oid, buffer.clone(), repo, languages, cx) + }) + })? + .await? + } else { + project + .update(cx, |project, cx| { + project.open_uncommitted_diff(buffer.clone(), cx) + })? + .await? + }; + Ok((buffer, changes)) } }