use crate::{ conflict_view::ConflictAddon, git_panel::{GitPanel, GitPanelAddon, GitStatusEntry}, git_panel_settings::GitPanelSettings, remote_button::{render_publish_button, render_push_button},};use anyhow::Result;use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};use collections::HashSet;use editor::{ Editor, EditorEvent, actions::{GoToHunk, GoToPreviousHunk}, scroll::Autoscroll,};use futures::StreamExt;use git::{ Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext, repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus}, status::FileStatus,};use gpui::{ Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, actions,};use language::{Anchor, Buffer, Capability, OffsetRangeExt};use multi_buffer::{MultiBuffer, PathKey};use project::{ Project, ProjectPath, git_store::{GitStore, GitStoreEvent, RepositoryEvent},};use settings::{Settings, SettingsStore};use std::any::{Any, TypeId};use std::ops::Range;use theme::ActiveTheme;use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};use util::ResultExt as _;use workspace::{ CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, searchable::SearchableItemHandle,}; actions!(git, [Diff, Add]); pub struct ProjectDiff { project: Entity<Project>, multibuffer: Entity<MultiBuffer>, editor: Entity<Editor>, 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: u32 = 1;const TRACKED_NAMESPACE: u32 = 2;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(|workspace, _: &Add, window, cx| { Self::deploy(workspace, &Diff, window, cx); }); 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>, ) { telemetry::event!( "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 }; if let Some(entry) = entry { project_diff.update(cx, |project_diff, cx| { project_diff.move_to_entry(entry, window, cx); }) } } pub fn autoscroll(&self, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.request_autoscroll(Autoscroll::fit(), 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()), window, cx); diff_display_editor.disable_inline_diagnostics(); 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| match event { GitStoreEvent::ActiveRepositoryChanged(_) | GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, true) | GitStoreEvent::ConflictsUpdated => { *this.update_needed.borrow_mut() = (); } _ => {} }, ); let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; cx.observe_global::<SettingsStore>(move |this, cx| { let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; if is_sort_by_path != was_sort_by_path { *this.update_needed.borrow_mut() = (); } was_sort_by_path = is_sort_by_path }) .detach(); 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 }); // Kick off 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::NoSecondaryHunk | 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, editor: &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(); } _ => {} } if editor.focus_handle(cx).contains_focused(window, cx) { if self.multibuffer.read(cx).is_empty() { self.focus_handle.focus(window) } } } 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.cached_status() { if !entry.status.has_changes() { continue; } let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path, cx) else { continue; }; let namespace = if GitPanelSettings::get_global(cx).sort_by_path { TRACKED_NAMESPACE } else 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(async move |_, cx| { let buffer = load_buffer.await?; let changes = project .update(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 conflict_addon = self .editor .read(cx) .addon::<ConflictAddon>() .expect("project diff editor should have a conflict addon"); 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.clone()); let conflicts = conflict_addon .conflict_set(snapshot.remote_id()) .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts.clone()) .unwrap_or_default(); let conflicts = conflicts.iter().map(|conflict| conflict.range.clone()); let excerpt_ranges = merge_anchor_ranges(diff_hunk_ranges, conflicts, &snapshot) .map(|range| 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, excerpt_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<()>, cx: &mut AsyncWindowContext, ) -> Result<()> { while let Some(_) = recv.next().await { 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(); })?; } } this.update(cx, |this, cx| { this.pending_scroll.take(); cx.notify(); })?; } Ok(()) } #[cfg(any(test, feature = "test-support"))]Lines 1–500 of 1757. Load more
use crate::{ conflict_view::ConflictAddon, git_panel::{GitPanel, GitPanelAddon, GitStatusEntry}, git_panel_settings::GitPanelSettings, remote_button::{render_publish_button, render_push_button}, }; use anyhow::Result; use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus}; use collections::HashSet; use editor::{ Editor, EditorEvent, actions::{GoToHunk, GoToPreviousHunk}, scroll::Autoscroll, }; use futures::StreamExt; use git::{ Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext, repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus}, status::FileStatus, }; use gpui::{ Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, actions, }; use language::{Anchor, Buffer, Capability, OffsetRangeExt}; use multi_buffer::{MultiBuffer, PathKey}; use project::{ Project, ProjectPath, git_store::{GitStore, GitStoreEvent, RepositoryEvent}, }; use settings::{Settings, SettingsStore}; use std::any::{Any, TypeId}; use std::ops::Range; use theme::ActiveTheme; use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider}; use util::ResultExt as _; use workspace::{ CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, searchable::SearchableItemHandle, }; actions!(git, [Diff, Add]); pub struct ProjectDiff { project: Entity<Project>, multibuffer: Entity<MultiBuffer>, editor: Entity<Editor>, 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: u32 = 1; const TRACKED_NAMESPACE: u32 = 2; 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(|workspace, _: &Add, window, cx| { Self::deploy(workspace, &Diff, window, cx); }); 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>, ) { telemetry::event!( "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 }; if let Some(entry) = entry { project_diff.update(cx, |project_diff, cx| { project_diff.move_to_entry(entry, window, cx); }) } } pub fn autoscroll(&self, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.request_autoscroll(Autoscroll::fit(), 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()), window, cx); diff_display_editor.disable_inline_diagnostics(); 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| match event { GitStoreEvent::ActiveRepositoryChanged(_) | GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, true) | GitStoreEvent::ConflictsUpdated => { *this.update_needed.borrow_mut() = (); } _ => {} }, ); let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; cx.observe_global::<SettingsStore>(move |this, cx| { let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; if is_sort_by_path != was_sort_by_path { *this.update_needed.borrow_mut() = (); } was_sort_by_path = is_sort_by_path }) .detach(); 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 }); // Kick off 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::NoSecondaryHunk | 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, editor: &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(); } _ => {} } if editor.focus_handle(cx).contains_focused(window, cx) { if self.multibuffer.read(cx).is_empty() { self.focus_handle.focus(window) } } } 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.cached_status() { if !entry.status.has_changes() { continue; } let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path, cx) else { continue; }; let namespace = if GitPanelSettings::get_global(cx).sort_by_path { TRACKED_NAMESPACE } else 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(async move |_, cx| { let buffer = load_buffer.await?; let changes = project .update(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 conflict_addon = self .editor .read(cx) .addon::<ConflictAddon>() .expect("project diff editor should have a conflict addon"); 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.clone()); let conflicts = conflict_addon .conflict_set(snapshot.remote_id()) .map(|conflict_set| conflict_set.read(cx).snapshot().conflicts.clone()) .unwrap_or_default(); let conflicts = conflicts.iter().map(|conflict| conflict.range.clone()); let excerpt_ranges = merge_anchor_ranges(diff_hunk_ranges, conflicts, &snapshot) .map(|range| 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, excerpt_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<()>, cx: &mut AsyncWindowContext, ) -> Result<()> { while let Some(_) = recv.next().await { 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(); })?; } } this.update(cx, |this, cx| { this.pending_scroll.take(); cx.notify(); })?; } Ok(()) } #[cfg(any(test, feature = "test-support"))]