Conrad IrwinTEMP323511f1 year agoHistory
2089 lines · 69.7 KB · Rust
RawDownload
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, BufferDiffSnapshot, DiffHunkSecondaryStatus};use collections::HashSet;use editor::{    Editor, EditorEvent, SelectionEffects,    actions::{GoToHunk, GoToPreviousHunk},    multibuffer_context_lines,    scroll::Autoscroll,};use futures::StreamExt;use git::{    Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,    repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},    status::{FileStatus, TrackedStatus},};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::ops::Range;use std::{    any::{Any, TypeId},    sync::Arc,};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, SaveOptions, TabContentParams},    searchable::SearchableItemHandle,}; 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 the default branch.        DiffToDefaultBranch,    ]); 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>,    diff_base_kind: DiffBaseKind,    _task: Task<Result<()>>,    _subscription: Subscription,} #[derive(Copy, Clone, PartialEq, Eq)]pub enum DiffBaseKind {    Head,    MergeBaseOfDefaultBranch,} #[derive(Debug)]struct DiffBuffer {    path_key: PathKey,    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(Self::diff_to_default_branch);        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, DiffBaseKind::Head, None, window, cx)    }     fn diff_to_default_branch(        workspace: &mut Workspace,        _: &DiffToDefaultBranch,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        Self::deploy_at(            workspace,            DiffBaseKind::MergeBaseOfDefaultBranch,            None,            window,            cx,        );    }     pub fn deploy_at(        workspace: &mut Workspace,        diff_base_kind: DiffBaseKind,        entry: Option<GitStatusEntry>,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        telemetry::event!(            match diff_base_kind {                DiffBaseKind::MergeBaseOfDefaultBranch => "Git Branch Diff Opened",                DiffBaseKind::Head => "Git Diff Opened",            },            source = if entry.is_some() {                "Git Panel"            } else {                "Action"            }        );        let project_diff =            if let Some(existing) = Self::existing_project_diff(workspace, diff_base_kind, cx) {                workspace.activate_item(&existing, true, true, window, cx);                existing            } else {                let workspace_handle = cx.entity();                let project_diff = cx.new(|cx| {                    Self::new(                        workspace.project().clone(),                        workspace_handle,                        diff_base_kind,                        window,                        cx,                    )                });                workspace.add_item_to_active_pane(                    Box::new(project_diff.clone()),                    None,                    true,                    window,                    cx,                );                project_diff            };        if let Some(entry) = entry {            project_diff.update(cx, |project_diff, cx| {                project_diff.move_to_entry(entry, window, cx);            })        }    }     pub fn existing_project_diff(        workspace: &mut Workspace,        diff_base_kind: DiffBaseKind,        cx: &mut Context<Workspace>,    ) -> Option<Entity<Self>> {        workspace            .items_of_type::<Self>(cx)            .find(|item| item.read(cx).diff_base_kind == diff_base_kind)    }     pub fn autoscroll(&self, cx: &mut Context<Self>) {        self.editor.update(cx, |editor, cx| {            editor.request_autoscroll(Autoscroll::fit(), cx);        })    }     fn new(        project: Entity<Project>,        workspace: Entity<Workspace>,        diff_base_kind: DiffBaseKind,        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_diagnostics(cx);            diff_display_editor.set_expand_all_diff_hunks(cx);            match diff_base_kind {                DiffBaseKind::Head => {                    diff_display_editor.register_addon(GitPanelAddon {                        workspace: workspace.downgrade(),                    });                }                DiffBaseKind::MergeBaseOfDefaultBranch => {                    diff_display_editor.start_temporary_diff_override();                    diff_display_editor.set_render_diff_hunk_controls(                        Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),                        cx,                    );                    //                }            }            diff_display_editor        });        window.defer(cx, {            let workspace = workspace.clone();            let editor = editor.clone();            move |window, cx| {                workspace.update(cx, |workspace, cx| {                    editor.update(cx, |editor, cx| {                        editor.added_to_workspace(workspace, window, cx);                    })                });            }        });        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;        let mut was_collapse_untracked_diff =            GitPanelSettings::get_global(cx).collapse_untracked_diff;        cx.observe_global::<SettingsStore>(move |this, 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.update_needed.borrow_mut() = ();            }            was_sort_by_path = is_sort_by_path;            was_collapse_untracked_diff = is_collapse_untracked_diff;        })        .detach();         let (mut send, recv) = postage::watch::channel::<()>();        let worker = window.spawn(cx, {            let this = cx.weak_entity();            async move |cx| Self::handle_status_updates(this, diff_base_kind, recv, cx).await        });        // Kick off a refresh immediately        *send.borrow_mut() = ();         Self {            project,            git_store: git_store.clone(),            workspace: workspace.downgrade(),            diff_base_kind,            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.had_conflict_on_last_merge_head_change(&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);         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(                    SelectionEffects::scroll(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().nth(1).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();         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>,    ) {        if let EditorEvent::SelectionsChanged { local: true } = event {            let Some(project_path) = self.active_path(cx) else {                return;            };            if self.diff_base_kind != DiffBaseKind::Head {                return;            }            self.workspace                .update(cx, |workspace, cx| {                    if let Some(git_panel) = workspace.panel::<GitPanel>(cx) {                        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)            && 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.had_conflict_on_last_merge_head_change(&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,

Lines 1–500 of 2089.