1757 lines · 58.2 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, 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.