use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry};use anyhow::Result;use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};use collections::HashSet;use editor::{ actions::{GoToHunk, GoToPreviousHunk}, scroll::Autoscroll, Editor, EditorEvent,};use feature_flags::FeatureFlagViewExt;use futures::StreamExt;use git::{ status::FileStatus, ShowCommitEditor, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,};use gpui::{ actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Render, Subscription, Task, WeakEntity,};use language::{Anchor, Buffer, Capability, OffsetRangeExt};use multi_buffer::{MultiBuffer, PathKey};use project::{git::GitStore, Project, ProjectPath};use std::any::{Any, TypeId};use theme::ActiveTheme;use ui::{prelude::*, vertical_divider, Tooltip};use util::ResultExt as _;use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, searchable::SearchableItemHandle, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,}; actions!(git, [Diff]); pub struct ProjectDiff { multibuffer: Entity<MultiBuffer>, editor: Entity<Editor>, project: Entity<Project>, git_store: Entity<GitStore>, workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, update_needed: postage::watch::Sender<()>, pending_scroll: Option<PathKey>, _task: Task<Result<()>>, _subscription: Subscription,} #[derive(Debug)]struct DiffBuffer { path_key: PathKey, buffer: Entity<Buffer>, diff: Entity<BufferDiff>, file_status: FileStatus,} const CONFLICT_NAMESPACE: &'static str = "0";const TRACKED_NAMESPACE: &'static str = "1";const NEW_NAMESPACE: &'static str = "2"; impl ProjectDiff { pub(crate) fn register( _: &mut Workspace, window: Option<&mut Window>, cx: &mut Context<Workspace>, ) { let Some(window) = window else { return }; cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| { workspace.register_action(Self::deploy); }); workspace::register_serializable_item::<ProjectDiff>(cx); } fn deploy( workspace: &mut Workspace, _: &Diff, window: &mut Window, cx: &mut Context<Workspace>, ) { Self::deploy_at(workspace, None, window, cx) } pub fn deploy_at( workspace: &mut Workspace, entry: Option<GitStatusEntry>, window: &mut Window, cx: &mut Context<Workspace>, ) { 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 }; if let Some(entry) = entry { project_diff.update(cx, |project_diff, cx| { project_diff.move_to_entry(entry, window, cx); }) } } fn new( project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); let editor = cx.new(|cx| { let mut diff_display_editor = Editor::for_multibuffer( multibuffer.clone(), Some(project.clone()), true, window, cx, ); diff_display_editor.set_expand_all_diff_hunks(cx); diff_display_editor.register_addon(GitPanelAddon { workspace: workspace.downgrade(), }); diff_display_editor }); cx.subscribe_in(&editor, window, Self::handle_editor_event) .detach(); let git_store = project.read(cx).git_store().clone(); let git_store_subscription = cx.subscribe_in( &git_store, window, move |this, _git_store, _event, _window, _cx| { *this.update_needed.borrow_mut() = (); }, ); let (mut send, recv) = postage::watch::channel::<()>(); let worker = window.spawn(cx, { let this = cx.weak_entity(); |cx| Self::handle_status_updates(this, recv, cx) }); // Kick of a refresh immediately *send.borrow_mut() = (); Self { project, git_store: git_store.clone(), workspace: workspace.downgrade(), focus_handle, editor, multibuffer, pending_scroll: None, update_needed: send, _task: worker, _subscription: git_store_subscription, } } pub fn move_to_entry( &mut self, entry: GitStatusEntry, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.git_store.read(cx).active_repository() else { return; }; let repo = git_repo.read(cx); let namespace = if repo.has_conflict(&entry.repo_path) { CONFLICT_NAMESPACE } else if entry.status.is_created() { NEW_NAMESPACE } else { TRACKED_NAMESPACE }; let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone()); self.move_to_path(path_key, window, cx) } pub fn active_path(&self, cx: &App) -> Option<ProjectPath> { let editor = self.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)?; let file = buffer.read(cx).file()?; Some(ProjectPath { worktree_id: file.worktree_id(cx), path: file.path().clone(), }) } 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(Some(Autoscroll::focused()), window, cx, |s| { s.select_ranges([position..position]); }) }); } else { self.pending_scroll = Some(path_key); } } fn button_states(&self, cx: &App) -> ButtonStates { let editor = self.editor.read(cx); let snapshot = self.multibuffer.read(cx).snapshot(cx); let prev_next = snapshot.diff_hunks().skip(1).next().is_some(); let mut selection = true; let mut ranges = editor .selections .disjoint_anchor_ranges() .collect::<Vec<_>>(); if !ranges.iter().any(|range| range.start != range.end) { selection = false; if let Some((excerpt_id, buffer, range)) = self.editor.read(cx).active_excerpt(cx) { ranges = vec![multi_buffer::Anchor::range_in_buffer( excerpt_id, buffer.read(cx).remote_id(), range, )]; } else { ranges = Vec::default(); } } let mut has_staged_hunks = false; let mut has_unstaged_hunks = false; for hunk in editor.diff_hunks_in_ranges(&ranges, &snapshot) { match hunk.secondary_status { DiffHunkSecondaryStatus::HasSecondaryHunk | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => { has_unstaged_hunks = true; } DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk => { has_staged_hunks = true; has_unstaged_hunks = true; } DiffHunkSecondaryStatus::None | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => { has_staged_hunks = true; } } } let mut stage_all = false; let mut unstage_all = false; self.workspace .read_with(cx, |workspace, cx| { if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { let git_panel = git_panel.read(cx); stage_all = git_panel.can_stage_all(); unstage_all = git_panel.can_unstage_all(); } }) .ok(); return ButtonStates { stage: has_unstaged_hunks, unstage: has_staged_hunks, prev_next, selection, stage_all, unstage_all, }; } fn handle_editor_event( &mut self, _: &Entity<Editor>, event: &EditorEvent, window: &mut Window, cx: &mut Context<Self>, ) { match event { EditorEvent::SelectionsChanged { local: true } => { let Some(project_path) = self.active_path(cx) else { return; }; self.workspace .update(cx, |workspace, cx| { if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { git_panel.update(cx, |git_panel, cx| { git_panel.select_entry_by_path(project_path, window, cx) }) } }) .ok(); } _ => {} } } fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> { let Some(repo) = self.git_store.read(cx).active_repository() else { self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.clear(cx); }); return vec![]; }; let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>(); let mut result = vec![]; repo.update(cx, |repo, cx| { for entry in repo.status() { if !entry.status.has_changes() { continue; } let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else { continue; }; let namespace = if repo.has_conflict(&entry.repo_path) { CONFLICT_NAMESPACE } else if entry.status.is_created() { NEW_NAMESPACE } else { TRACKED_NAMESPACE }; let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone()); previous_paths.remove(&path_key); let load_buffer = self .project .update(cx, |project, cx| project.open_buffer(project_path, cx)); let project = self.project.clone(); result.push(cx.spawn(|_, mut cx| async move { let buffer = load_buffer.await?; let changes = project .update(&mut cx, |project, cx| { project.open_uncommitted_diff(buffer.clone(), cx) })? .await?; Ok(DiffBuffer { path_key, buffer, diff: changes, file_status: entry.status, }) })); } }); self.multibuffer.update(cx, |multibuffer, cx| { for path in previous_paths { multibuffer.remove_excerpts_for_path(path, cx); } }); result } fn register_buffer( &mut self, diff_buffer: DiffBuffer, window: &mut Window, cx: &mut Context<Self>, ) { let path_key = diff_buffer.path_key; let buffer = diff_buffer.buffer; let diff = diff_buffer.diff; let snapshot = buffer.read(cx).snapshot(); let diff = diff.read(cx); let diff_hunk_ranges = diff .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) .collect::<Vec<_>>(); let (was_empty, is_excerpt_newly_added) = self.multibuffer.update(cx, |multibuffer, cx| { let was_empty = multibuffer.is_empty(); let is_newly_added = multibuffer.set_excerpts_for_path( path_key.clone(), buffer, diff_hunk_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ); (was_empty, is_newly_added) }); self.editor.update(cx, |editor, cx| { if was_empty { editor.change_selections(None, window, cx, |selections| { // TODO select the very beginning (possibly inside a deletion) selections.select_ranges([0..0]) }); } if is_excerpt_newly_added && diff_buffer.file_status.is_deleted() { editor.fold_buffer(snapshot.text.remote_id(), cx) } }); if self.multibuffer.read(cx).is_empty() && self .editor .read(cx) .focus_handle(cx) .contains_focused(window, cx) { self.focus_handle.focus(window); } else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() { self.editor.update(cx, |editor, cx| { editor.focus_handle(cx).focus(window); }); } if self.pending_scroll.as_ref() == Some(&path_key) { self.move_to_path(path_key, window, cx); } } pub async fn handle_status_updates( this: WeakEntity<Self>, mut recv: postage::watch::Receiver<()>, mut cx: AsyncWindowContext, ) -> Result<()> { while let Some(_) = recv.next().await { let buffers_to_load = this.update(&mut 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(); })?; } } this.update(&mut cx, |this, _| this.pending_scroll.take())?; } Ok(()) } #[cfg(any(test, feature = "test-support"))] pub fn excerpt_paths(&self, cx: &App) -> Vec<String> { self.multibuffer .read(cx) .excerpt_paths() .map(|key| key.path().to_string_lossy().to_string()) .collect() }} impl EventEmitter<EditorEvent> for ProjectDiff {} impl Focusable for ProjectDiff { fn focus_handle(&self, cx: &App) -> FocusHandle { if self.multibuffer.read(cx).is_empty() { self.focus_handle.clone() } else { self.editor.focus_handle(cx) } }} impl Item for ProjectDiff { type Event = EditorEvent; fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> { Some(Icon::new(IconName::GitBranch).color(Color::Muted)) } fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { Editor::to_item_events(event, f) } 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 Any>, window: &mut Window, cx: &mut Context<Self>, ) -> bool { self.editor .update(cx, |editor, cx| editor.navigate(data, window, cx)) } fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> { Some("Project Diff".into()) } fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement { Label::new("Uncommitted Changes") .color(if params.selected {Lines 1–500 of 1316. Load more
use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry}; use anyhow::Result; use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus}; use collections::HashSet; use editor::{ actions::{GoToHunk, GoToPreviousHunk}, scroll::Autoscroll, Editor, EditorEvent, }; use feature_flags::FeatureFlagViewExt; use futures::StreamExt; use git::{ status::FileStatus, ShowCommitEditor, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext, }; use gpui::{ actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, }; use language::{Anchor, Buffer, Capability, OffsetRangeExt}; use multi_buffer::{MultiBuffer, PathKey}; use project::{git::GitStore, Project, ProjectPath}; use std::any::{Any, TypeId}; use theme::ActiveTheme; use ui::{prelude::*, vertical_divider, Tooltip}; use util::ResultExt as _; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, searchable::SearchableItemHandle, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; actions!(git, [Diff]); pub struct ProjectDiff { multibuffer: Entity<MultiBuffer>, editor: Entity<Editor>, project: Entity<Project>, git_store: Entity<GitStore>, workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, update_needed: postage::watch::Sender<()>, pending_scroll: Option<PathKey>, _task: Task<Result<()>>, _subscription: Subscription, } #[derive(Debug)] struct DiffBuffer { path_key: PathKey, buffer: Entity<Buffer>, diff: Entity<BufferDiff>, file_status: FileStatus, } const CONFLICT_NAMESPACE: &'static str = "0"; const TRACKED_NAMESPACE: &'static str = "1"; const NEW_NAMESPACE: &'static str = "2"; impl ProjectDiff { pub(crate) fn register( _: &mut Workspace, window: Option<&mut Window>, cx: &mut Context<Workspace>, ) { let Some(window) = window else { return }; cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| { workspace.register_action(Self::deploy); }); workspace::register_serializable_item::<ProjectDiff>(cx); } fn deploy( workspace: &mut Workspace, _: &Diff, window: &mut Window, cx: &mut Context<Workspace>, ) { Self::deploy_at(workspace, None, window, cx) } pub fn deploy_at( workspace: &mut Workspace, entry: Option<GitStatusEntry>, window: &mut Window, cx: &mut Context<Workspace>, ) { 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 }; if let Some(entry) = entry { project_diff.update(cx, |project_diff, cx| { project_diff.move_to_entry(entry, window, cx); }) } } fn new( project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); let editor = cx.new(|cx| { let mut diff_display_editor = Editor::for_multibuffer( multibuffer.clone(), Some(project.clone()), true, window, cx, ); diff_display_editor.set_expand_all_diff_hunks(cx); diff_display_editor.register_addon(GitPanelAddon { workspace: workspace.downgrade(), }); diff_display_editor }); cx.subscribe_in(&editor, window, Self::handle_editor_event) .detach(); let git_store = project.read(cx).git_store().clone(); let git_store_subscription = cx.subscribe_in( &git_store, window, move |this, _git_store, _event, _window, _cx| { *this.update_needed.borrow_mut() = (); }, ); let (mut send, recv) = postage::watch::channel::<()>(); let worker = window.spawn(cx, { let this = cx.weak_entity(); |cx| Self::handle_status_updates(this, recv, cx) }); // Kick of a refresh immediately *send.borrow_mut() = (); Self { project, git_store: git_store.clone(), workspace: workspace.downgrade(), focus_handle, editor, multibuffer, pending_scroll: None, update_needed: send, _task: worker, _subscription: git_store_subscription, } } pub fn move_to_entry( &mut self, entry: GitStatusEntry, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.git_store.read(cx).active_repository() else { return; }; let repo = git_repo.read(cx); let namespace = if repo.has_conflict(&entry.repo_path) { CONFLICT_NAMESPACE } else if entry.status.is_created() { NEW_NAMESPACE } else { TRACKED_NAMESPACE }; let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone()); self.move_to_path(path_key, window, cx) } pub fn active_path(&self, cx: &App) -> Option<ProjectPath> { let editor = self.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)?; let file = buffer.read(cx).file()?; Some(ProjectPath { worktree_id: file.worktree_id(cx), path: file.path().clone(), }) } 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(Some(Autoscroll::focused()), window, cx, |s| { s.select_ranges([position..position]); }) }); } else { self.pending_scroll = Some(path_key); } } fn button_states(&self, cx: &App) -> ButtonStates { let editor = self.editor.read(cx); let snapshot = self.multibuffer.read(cx).snapshot(cx); let prev_next = snapshot.diff_hunks().skip(1).next().is_some(); let mut selection = true; let mut ranges = editor .selections .disjoint_anchor_ranges() .collect::<Vec<_>>(); if !ranges.iter().any(|range| range.start != range.end) { selection = false; if let Some((excerpt_id, buffer, range)) = self.editor.read(cx).active_excerpt(cx) { ranges = vec![multi_buffer::Anchor::range_in_buffer( excerpt_id, buffer.read(cx).remote_id(), range, )]; } else { ranges = Vec::default(); } } let mut has_staged_hunks = false; let mut has_unstaged_hunks = false; for hunk in editor.diff_hunks_in_ranges(&ranges, &snapshot) { match hunk.secondary_status { DiffHunkSecondaryStatus::HasSecondaryHunk | DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => { has_unstaged_hunks = true; } DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk => { has_staged_hunks = true; has_unstaged_hunks = true; } DiffHunkSecondaryStatus::None | DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => { has_staged_hunks = true; } } } let mut stage_all = false; let mut unstage_all = false; self.workspace .read_with(cx, |workspace, cx| { if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { let git_panel = git_panel.read(cx); stage_all = git_panel.can_stage_all(); unstage_all = git_panel.can_unstage_all(); } }) .ok(); return ButtonStates { stage: has_unstaged_hunks, unstage: has_staged_hunks, prev_next, selection, stage_all, unstage_all, }; } fn handle_editor_event( &mut self, _: &Entity<Editor>, event: &EditorEvent, window: &mut Window, cx: &mut Context<Self>, ) { match event { EditorEvent::SelectionsChanged { local: true } => { let Some(project_path) = self.active_path(cx) else { return; }; self.workspace .update(cx, |workspace, cx| { if let Some(git_panel) = workspace.panel::<GitPanel>(cx) { git_panel.update(cx, |git_panel, cx| { git_panel.select_entry_by_path(project_path, window, cx) }) } }) .ok(); } _ => {} } } fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> { let Some(repo) = self.git_store.read(cx).active_repository() else { self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.clear(cx); }); return vec![]; }; let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>(); let mut result = vec![]; repo.update(cx, |repo, cx| { for entry in repo.status() { if !entry.status.has_changes() { continue; } let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else { continue; }; let namespace = if repo.has_conflict(&entry.repo_path) { CONFLICT_NAMESPACE } else if entry.status.is_created() { NEW_NAMESPACE } else { TRACKED_NAMESPACE }; let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone()); previous_paths.remove(&path_key); let load_buffer = self .project .update(cx, |project, cx| project.open_buffer(project_path, cx)); let project = self.project.clone(); result.push(cx.spawn(|_, mut cx| async move { let buffer = load_buffer.await?; let changes = project .update(&mut cx, |project, cx| { project.open_uncommitted_diff(buffer.clone(), cx) })? .await?; Ok(DiffBuffer { path_key, buffer, diff: changes, file_status: entry.status, }) })); } }); self.multibuffer.update(cx, |multibuffer, cx| { for path in previous_paths { multibuffer.remove_excerpts_for_path(path, cx); } }); result } fn register_buffer( &mut self, diff_buffer: DiffBuffer, window: &mut Window, cx: &mut Context<Self>, ) { let path_key = diff_buffer.path_key; let buffer = diff_buffer.buffer; let diff = diff_buffer.diff; let snapshot = buffer.read(cx).snapshot(); let diff = diff.read(cx); let diff_hunk_ranges = diff .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) .collect::<Vec<_>>(); let (was_empty, is_excerpt_newly_added) = self.multibuffer.update(cx, |multibuffer, cx| { let was_empty = multibuffer.is_empty(); let is_newly_added = multibuffer.set_excerpts_for_path( path_key.clone(), buffer, diff_hunk_ranges, editor::DEFAULT_MULTIBUFFER_CONTEXT, cx, ); (was_empty, is_newly_added) }); self.editor.update(cx, |editor, cx| { if was_empty { editor.change_selections(None, window, cx, |selections| { // TODO select the very beginning (possibly inside a deletion) selections.select_ranges([0..0]) }); } if is_excerpt_newly_added && diff_buffer.file_status.is_deleted() { editor.fold_buffer(snapshot.text.remote_id(), cx) } }); if self.multibuffer.read(cx).is_empty() && self .editor .read(cx) .focus_handle(cx) .contains_focused(window, cx) { self.focus_handle.focus(window); } else if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() { self.editor.update(cx, |editor, cx| { editor.focus_handle(cx).focus(window); }); } if self.pending_scroll.as_ref() == Some(&path_key) { self.move_to_path(path_key, window, cx); } } pub async fn handle_status_updates( this: WeakEntity<Self>, mut recv: postage::watch::Receiver<()>, mut cx: AsyncWindowContext, ) -> Result<()> { while let Some(_) = recv.next().await { let buffers_to_load = this.update(&mut 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(); })?; } } this.update(&mut cx, |this, _| this.pending_scroll.take())?; } Ok(()) } #[cfg(any(test, feature = "test-support"))] pub fn excerpt_paths(&self, cx: &App) -> Vec<String> { self.multibuffer .read(cx) .excerpt_paths() .map(|key| key.path().to_string_lossy().to_string()) .collect() } } impl EventEmitter<EditorEvent> for ProjectDiff {} impl Focusable for ProjectDiff { fn focus_handle(&self, cx: &App) -> FocusHandle { if self.multibuffer.read(cx).is_empty() { self.focus_handle.clone() } else { self.editor.focus_handle(cx) } } } impl Item for ProjectDiff { type Event = EditorEvent; fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> { Some(Icon::new(IconName::GitBranch).color(Color::Muted)) } fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { Editor::to_item_events(event, f) } 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 Any>, window: &mut Window, cx: &mut Context<Self>, ) -> bool { self.editor .update(cx, |editor, cx| editor.navigate(data, window, cx)) } fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> { Some("Project Diff".into()) } fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement { Label::new("Uncommitted Changes") .color(if params.selected {