use crate::{ conflict_view::ConflictAddon, git_panel::{GitPanel, GitPanelAddon, GitStatusEntry}, git_panel_settings::GitPanelSettings, remote_button::{render_publish_button, render_push_button}, resolve_active_repository,};use anyhow::{Context as _, Result, anyhow};use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};use collections::{HashMap, HashSet};use editor::{ Addon, Editor, EditorEvent, EditorSettings, SelectionEffects, SplittableEditor, actions::{GoToHunk, GoToPreviousHunk, SendReviewToAgent}, multibuffer_context_lines, scroll::Autoscroll,}; use git::{ Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext, repository::{Branch, RepoPath, Upstream, UpstreamTracking, UpstreamTrackingStatus}, status::FileStatus,};use gpui::{ Action, AnyElement, 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::{ Repository, branch_diff::{self, BranchDiffEvent, DiffBase}, },};use settings::{Settings, SettingsStore};use smol::future::yield_now;use std::any::{Any, TypeId};use std::sync::Arc;use theme::ActiveTheme;use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider};use util::{ResultExt as _, rel_path::RelPath};use workspace::{ CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::{Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, notifications::NotifyTaskExt, searchable::SearchableItemHandle,};use ztracing::instrument; actions!( git, [ /// Shows the diff between the working directory and the index. Diff, /// Adds files to the git staging area. Add, /// Shows the diff between the working directory and your default /// branch (typically main or master). BranchDiff, LeaderAndFollower, ]); pub struct ProjectDiff { project: Entity<Project>, multibuffer: Entity<MultiBuffer>, branch_diff: Entity<branch_diff::BranchDiff>, editor: Entity<SplittableEditor>, buffer_diff_subscriptions: HashMap<Arc<RelPath>, (Entity<BufferDiff>, Subscription)>, workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, pending_scroll: Option<PathKey>, review_comment_count: usize, _task: Task<Result<()>>, _subscription: Subscription,} #[derive(Clone, Copy, Debug, PartialEq, Eq)]pub enum RefreshReason { DiffChanged, StatusesChanged, EditorSaved,} const CONFLICT_SORT_PREFIX: u64 = 1;const TRACKED_SORT_PREFIX: u64 = 2;const NEW_SORT_PREFIX: u64 = 3; impl ProjectDiff { pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) { workspace.register_action(Self::deploy); workspace.register_action(Self::deploy_branch_diff); 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) } fn deploy_branch_diff( workspace: &mut Workspace, _: &BranchDiff, window: &mut Window, cx: &mut Context<Workspace>, ) { telemetry::event!("Git Branch Diff Opened"); let project = workspace.project().clone(); let existing = workspace .items_of_type::<Self>(cx) .find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Merge { .. })); if let Some(existing) = existing { workspace.activate_item(&existing, true, true, window, cx); return; } let workspace = cx.entity(); let workspace_weak = workspace.downgrade(); window .spawn(cx, async move |cx| { let this = cx .update(|window, cx| { Self::new_with_default_branch(project, workspace.clone(), window, cx) })? .await?; workspace .update_in(cx, |workspace, window, cx| { workspace.add_item_to_active_pane(Box::new(this), None, true, window, cx); }) .ok(); anyhow::Ok(()) }) .detach_and_notify_err(workspace_weak, 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 intended_repo = resolve_active_repository(workspace, cx); let existing = workspace .items_of_type::<Self>(cx) .find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Head)); let project_diff = if let Some(existing) = existing { existing.update(cx, |project_diff, cx| { project_diff.move_to_beginning(window, 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(intended) = &intended_repo { let needs_switch = project_diff .read(cx) .branch_diff .read(cx) .repo() .map_or(true, |current| current.read(cx).id != intended.read(cx).id); if needs_switch { project_diff.update(cx, |project_diff, cx| { project_diff.branch_diff.update(cx, |branch_diff, cx| { branch_diff.set_repo(Some(intended.clone()), cx); }); }); } } if let Some(entry) = entry { project_diff.update(cx, |project_diff, cx| { project_diff.move_to_entry(entry, window, cx); }) } } pub fn deploy_at_project_path( workspace: &mut Workspace, project_path: ProjectPath, window: &mut Window, cx: &mut Context<Workspace>, ) { telemetry::event!("Git Diff Opened", source = "Agent Panel"); let existing = workspace .items_of_type::<Self>(cx) .find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Head)); let project_diff = if let Some(existing) = existing { 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 }; project_diff.update(cx, |project_diff, cx| { project_diff.move_to_project_path(&project_path, window, cx); }); } pub fn autoscroll(&self, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.rhs_editor().update(cx, |editor, cx| { editor.request_autoscroll(Autoscroll::fit(), cx); }) }) } fn new_with_default_branch( project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut App, ) -> Task<Result<Entity<Self>>> { let Some(repo) = project.read(cx).git_store().read(cx).active_repository() else { return Task::ready(Err(anyhow!("No active repository"))); }; let main_branch = repo.update(cx, |repo, _| repo.default_branch(true)); window.spawn(cx, async move |cx| { let main_branch = main_branch .await?? .context("Could not determine default branch")?; let branch_diff = cx.new_window_entity(|window, cx| { branch_diff::BranchDiff::new( DiffBase::Merge { base_ref: main_branch, }, project.clone(), window, cx, ) })?; cx.new_window_entity(|window, cx| { Self::new_impl(branch_diff, project, workspace, window, cx) }) }) } fn new( project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let branch_diff = cx.new(|cx| branch_diff::BranchDiff::new(DiffBase::Head, project.clone(), window, cx)); Self::new_impl(branch_diff, project, workspace, window, cx) } fn new_impl( branch_diff: Entity<branch_diff::BranchDiff>, project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); multibuffer.set_all_diff_hunks_expanded(cx); multibuffer }); let editor = cx.new(|cx| { let diff_display_editor = SplittableEditor::new( EditorSettings::get_global(cx).diff_view_style, multibuffer.clone(), project.clone(), workspace.clone(), window, cx, ); match branch_diff.read(cx).diff_base() { DiffBase::Head => {} DiffBase::Merge { .. } => diff_display_editor.set_render_diff_hunk_controls( Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()), cx, ), } diff_display_editor.rhs_editor().update(cx, |editor, cx| { editor.disable_diagnostics(cx); editor.set_show_diff_review_button(true, cx); match branch_diff.read(cx).diff_base() { DiffBase::Head => { editor.register_addon(GitPanelAddon { workspace: workspace.downgrade(), }); } DiffBase::Merge { .. } => { editor.register_addon(BranchDiffAddon { branch_diff: branch_diff.clone(), }); editor.start_temporary_diff_override(); } } }); diff_display_editor }); let editor_subscription = cx.subscribe_in(&editor, window, Self::handle_editor_event); let primary_editor = editor.read(cx).rhs_editor().clone(); let review_comment_subscription = cx.subscribe(&primary_editor, |this, _editor, event: &EditorEvent, cx| { if let EditorEvent::ReviewCommentsChanged { total_count } = event { this.review_comment_count = *total_count; cx.notify(); } }); let branch_diff_subscription = cx.subscribe_in( &branch_diff, window, move |this, _git_store, event, window, cx| match event { BranchDiffEvent::FileListChanged => { this._task = window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, RefreshReason::StatusesChanged, cx).await }) } }, ); let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; let mut was_collapse_untracked_diff = GitPanelSettings::get_global(cx).collapse_untracked_diff; cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| { let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; let is_collapse_untracked_diff = GitPanelSettings::get_global(cx).collapse_untracked_diff; if is_sort_by_path != was_sort_by_path || is_collapse_untracked_diff != was_collapse_untracked_diff { this._task = { window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, RefreshReason::StatusesChanged, cx).await }) } } was_sort_by_path = is_sort_by_path; was_collapse_untracked_diff = is_collapse_untracked_diff; }) .detach(); let task = window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, RefreshReason::StatusesChanged, cx).await }); Self { project, workspace: workspace.downgrade(), branch_diff, focus_handle, editor, multibuffer, buffer_diff_subscriptions: Default::default(), pending_scroll: None, review_comment_count: 0, _task: task, _subscription: Subscription::join( branch_diff_subscription, Subscription::join(editor_subscription, review_comment_subscription), ), } } pub fn diff_base<'a>(&'a self, cx: &'a App) -> &'a DiffBase { self.branch_diff.read(cx).diff_base() } pub fn move_to_entry( &mut self, entry: GitStatusEntry, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.branch_diff.read(cx).repo() else { return; }; let repo = git_repo.read(cx); let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.status, cx); let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.as_ref().clone()); self.move_to_path(path_key, window, cx) } pub fn move_to_project_path( &mut self, project_path: &ProjectPath, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.branch_diff.read(cx).repo() else { return; }; let Some(repo_path) = git_repo .read(cx) .project_path_to_repo_path(project_path, cx) else { return; }; let status = git_repo .read(cx) .status_for_path(&repo_path) .map(|entry| entry.status) .unwrap_or(FileStatus::Untracked); let sort_prefix = sort_prefix(&git_repo.read(cx), &repo_path, status, cx); let path_key = PathKey::with_sort_prefix(sort_prefix, repo_path.as_ref().clone()); self.move_to_path(path_key, window, cx) } pub fn active_path(&self, cx: &App) -> Option<ProjectPath> { let editor = self.editor.read(cx).focused_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_beginning(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.rhs_editor().update(cx, |editor, cx| { editor.move_to_beginning(&Default::default(), window, cx); }); }); } 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.rhs_editor().update(cx, |editor, cx| { editor.change_selections( SelectionEffects::scroll(Autoscroll::focused()), window, cx, |s| { s.select_ranges([position..position]); }, ) }) }); } else { self.pending_scroll = Some(path_key); } } /// Returns the total count of review comments across all hunks/files. pub fn total_review_comment_count(&self) -> usize { self.review_comment_count } /// Returns a reference to the splittable editor. pub fn editor(&self) -> &Entity<SplittableEditor> { &self.editorLines 1–500 of 2749. Load more
use crate::{ conflict_view::ConflictAddon, git_panel::{GitPanel, GitPanelAddon, GitStatusEntry}, git_panel_settings::GitPanelSettings, remote_button::{render_publish_button, render_push_button}, resolve_active_repository, }; use anyhow::{Context as _, Result, anyhow}; use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus}; use collections::{HashMap, HashSet}; use editor::{ Addon, Editor, EditorEvent, EditorSettings, SelectionEffects, SplittableEditor, actions::{GoToHunk, GoToPreviousHunk, SendReviewToAgent}, multibuffer_context_lines, scroll::Autoscroll, }; use git::{ Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext, repository::{Branch, RepoPath, Upstream, UpstreamTracking, UpstreamTrackingStatus}, status::FileStatus, }; use gpui::{ Action, AnyElement, 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::{ Repository, branch_diff::{self, BranchDiffEvent, DiffBase}, }, }; use settings::{Settings, SettingsStore}; use smol::future::yield_now; use std::any::{Any, TypeId}; use std::sync::Arc; use theme::ActiveTheme; use ui::{KeyBinding, Tooltip, prelude::*, vertical_divider}; use util::{ResultExt as _, rel_path::RelPath}; use workspace::{ CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::{Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams}, notifications::NotifyTaskExt, searchable::SearchableItemHandle, }; use ztracing::instrument; actions!( git, [ /// Shows the diff between the working directory and the index. Diff, /// Adds files to the git staging area. Add, /// Shows the diff between the working directory and your default /// branch (typically main or master). BranchDiff, LeaderAndFollower, ] ); pub struct ProjectDiff { project: Entity<Project>, multibuffer: Entity<MultiBuffer>, branch_diff: Entity<branch_diff::BranchDiff>, editor: Entity<SplittableEditor>, buffer_diff_subscriptions: HashMap<Arc<RelPath>, (Entity<BufferDiff>, Subscription)>, workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, pending_scroll: Option<PathKey>, review_comment_count: usize, _task: Task<Result<()>>, _subscription: Subscription, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RefreshReason { DiffChanged, StatusesChanged, EditorSaved, } const CONFLICT_SORT_PREFIX: u64 = 1; const TRACKED_SORT_PREFIX: u64 = 2; const NEW_SORT_PREFIX: u64 = 3; impl ProjectDiff { pub(crate) fn register(workspace: &mut Workspace, cx: &mut Context<Workspace>) { workspace.register_action(Self::deploy); workspace.register_action(Self::deploy_branch_diff); 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) } fn deploy_branch_diff( workspace: &mut Workspace, _: &BranchDiff, window: &mut Window, cx: &mut Context<Workspace>, ) { telemetry::event!("Git Branch Diff Opened"); let project = workspace.project().clone(); let existing = workspace .items_of_type::<Self>(cx) .find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Merge { .. })); if let Some(existing) = existing { workspace.activate_item(&existing, true, true, window, cx); return; } let workspace = cx.entity(); let workspace_weak = workspace.downgrade(); window .spawn(cx, async move |cx| { let this = cx .update(|window, cx| { Self::new_with_default_branch(project, workspace.clone(), window, cx) })? .await?; workspace .update_in(cx, |workspace, window, cx| { workspace.add_item_to_active_pane(Box::new(this), None, true, window, cx); }) .ok(); anyhow::Ok(()) }) .detach_and_notify_err(workspace_weak, 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 intended_repo = resolve_active_repository(workspace, cx); let existing = workspace .items_of_type::<Self>(cx) .find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Head)); let project_diff = if let Some(existing) = existing { existing.update(cx, |project_diff, cx| { project_diff.move_to_beginning(window, 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(intended) = &intended_repo { let needs_switch = project_diff .read(cx) .branch_diff .read(cx) .repo() .map_or(true, |current| current.read(cx).id != intended.read(cx).id); if needs_switch { project_diff.update(cx, |project_diff, cx| { project_diff.branch_diff.update(cx, |branch_diff, cx| { branch_diff.set_repo(Some(intended.clone()), cx); }); }); } } if let Some(entry) = entry { project_diff.update(cx, |project_diff, cx| { project_diff.move_to_entry(entry, window, cx); }) } } pub fn deploy_at_project_path( workspace: &mut Workspace, project_path: ProjectPath, window: &mut Window, cx: &mut Context<Workspace>, ) { telemetry::event!("Git Diff Opened", source = "Agent Panel"); let existing = workspace .items_of_type::<Self>(cx) .find(|item| matches!(item.read(cx).diff_base(cx), DiffBase::Head)); let project_diff = if let Some(existing) = existing { 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 }; project_diff.update(cx, |project_diff, cx| { project_diff.move_to_project_path(&project_path, window, cx); }); } pub fn autoscroll(&self, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.rhs_editor().update(cx, |editor, cx| { editor.request_autoscroll(Autoscroll::fit(), cx); }) }) } fn new_with_default_branch( project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut App, ) -> Task<Result<Entity<Self>>> { let Some(repo) = project.read(cx).git_store().read(cx).active_repository() else { return Task::ready(Err(anyhow!("No active repository"))); }; let main_branch = repo.update(cx, |repo, _| repo.default_branch(true)); window.spawn(cx, async move |cx| { let main_branch = main_branch .await?? .context("Could not determine default branch")?; let branch_diff = cx.new_window_entity(|window, cx| { branch_diff::BranchDiff::new( DiffBase::Merge { base_ref: main_branch, }, project.clone(), window, cx, ) })?; cx.new_window_entity(|window, cx| { Self::new_impl(branch_diff, project, workspace, window, cx) }) }) } fn new( project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let branch_diff = cx.new(|cx| branch_diff::BranchDiff::new(DiffBase::Head, project.clone(), window, cx)); Self::new_impl(branch_diff, project, workspace, window, cx) } fn new_impl( branch_diff: Entity<branch_diff::BranchDiff>, project: Entity<Project>, workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|cx| { let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); multibuffer.set_all_diff_hunks_expanded(cx); multibuffer }); let editor = cx.new(|cx| { let diff_display_editor = SplittableEditor::new( EditorSettings::get_global(cx).diff_view_style, multibuffer.clone(), project.clone(), workspace.clone(), window, cx, ); match branch_diff.read(cx).diff_base() { DiffBase::Head => {} DiffBase::Merge { .. } => diff_display_editor.set_render_diff_hunk_controls( Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()), cx, ), } diff_display_editor.rhs_editor().update(cx, |editor, cx| { editor.disable_diagnostics(cx); editor.set_show_diff_review_button(true, cx); match branch_diff.read(cx).diff_base() { DiffBase::Head => { editor.register_addon(GitPanelAddon { workspace: workspace.downgrade(), }); } DiffBase::Merge { .. } => { editor.register_addon(BranchDiffAddon { branch_diff: branch_diff.clone(), }); editor.start_temporary_diff_override(); } } }); diff_display_editor }); let editor_subscription = cx.subscribe_in(&editor, window, Self::handle_editor_event); let primary_editor = editor.read(cx).rhs_editor().clone(); let review_comment_subscription = cx.subscribe(&primary_editor, |this, _editor, event: &EditorEvent, cx| { if let EditorEvent::ReviewCommentsChanged { total_count } = event { this.review_comment_count = *total_count; cx.notify(); } }); let branch_diff_subscription = cx.subscribe_in( &branch_diff, window, move |this, _git_store, event, window, cx| match event { BranchDiffEvent::FileListChanged => { this._task = window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, RefreshReason::StatusesChanged, cx).await }) } }, ); let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; let mut was_collapse_untracked_diff = GitPanelSettings::get_global(cx).collapse_untracked_diff; cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| { let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; let is_collapse_untracked_diff = GitPanelSettings::get_global(cx).collapse_untracked_diff; if is_sort_by_path != was_sort_by_path || is_collapse_untracked_diff != was_collapse_untracked_diff { this._task = { window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, RefreshReason::StatusesChanged, cx).await }) } } was_sort_by_path = is_sort_by_path; was_collapse_untracked_diff = is_collapse_untracked_diff; }) .detach(); let task = window.spawn(cx, { let this = cx.weak_entity(); async |cx| Self::refresh(this, RefreshReason::StatusesChanged, cx).await }); Self { project, workspace: workspace.downgrade(), branch_diff, focus_handle, editor, multibuffer, buffer_diff_subscriptions: Default::default(), pending_scroll: None, review_comment_count: 0, _task: task, _subscription: Subscription::join( branch_diff_subscription, Subscription::join(editor_subscription, review_comment_subscription), ), } } pub fn diff_base<'a>(&'a self, cx: &'a App) -> &'a DiffBase { self.branch_diff.read(cx).diff_base() } pub fn move_to_entry( &mut self, entry: GitStatusEntry, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.branch_diff.read(cx).repo() else { return; }; let repo = git_repo.read(cx); let sort_prefix = sort_prefix(repo, &entry.repo_path, entry.status, cx); let path_key = PathKey::with_sort_prefix(sort_prefix, entry.repo_path.as_ref().clone()); self.move_to_path(path_key, window, cx) } pub fn move_to_project_path( &mut self, project_path: &ProjectPath, window: &mut Window, cx: &mut Context<Self>, ) { let Some(git_repo) = self.branch_diff.read(cx).repo() else { return; }; let Some(repo_path) = git_repo .read(cx) .project_path_to_repo_path(project_path, cx) else { return; }; let status = git_repo .read(cx) .status_for_path(&repo_path) .map(|entry| entry.status) .unwrap_or(FileStatus::Untracked); let sort_prefix = sort_prefix(&git_repo.read(cx), &repo_path, status, cx); let path_key = PathKey::with_sort_prefix(sort_prefix, repo_path.as_ref().clone()); self.move_to_path(path_key, window, cx) } pub fn active_path(&self, cx: &App) -> Option<ProjectPath> { let editor = self.editor.read(cx).focused_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_beginning(&mut self, window: &mut Window, cx: &mut Context<Self>) { self.editor.update(cx, |editor, cx| { editor.rhs_editor().update(cx, |editor, cx| { editor.move_to_beginning(&Default::default(), window, cx); }); }); } 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.rhs_editor().update(cx, |editor, cx| { editor.change_selections( SelectionEffects::scroll(Autoscroll::focused()), window, cx, |s| { s.select_ranges([position..position]); }, ) }) }); } else { self.pending_scroll = Some(path_key); } } /// Returns the total count of review comments across all hunks/files. pub fn total_review_comment_count(&self) -> usize { self.review_comment_count } /// Returns a reference to the splittable editor. pub fn editor(&self) -> &Entity<SplittableEditor> { &self.editor