7853 lines · 278.7 KB · Rust
RawDownload
use crate::askpass_modal::AskPassModal;use crate::commit_modal::CommitModal;use crate::commit_tooltip::CommitTooltip;use crate::commit_view::CommitView;use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff};use crate::remote_output::{self, RemoteAction, SuccessMessage};use crate::{branch_picker, picker_prompt, render_remote_button};use crate::{    file_history_view::FileHistoryView, git_panel_settings::GitPanelSettings, git_status_icon,    repository_selector::RepositorySelector,};use agent_settings::AgentSettings;use anyhow::Context as _;use askpass::AskPassDelegate;use cloud_llm_client::CompletionIntent;use collections::{BTreeMap, HashMap, HashSet};use db::kvp::KEY_VALUE_STORE;use editor::{    Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset,    actions::ExpandAllDiffHunks,};use editor::{EditorStyle, RewrapOptions};use file_icons::FileIcons;use futures::StreamExt as _;use git::commit::ParsedCommitMessage;use git::repository::{    Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter,    PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,    UpstreamTrackingStatus, get_git_committer,};use git::stash::GitStash;use git::status::{DiffStat, StageStatus};use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus};use git::{    ExpandCommitEditor, GitHostingProviderRegistry, RestoreTrackedFiles, StageAll, StashAll,    StashApply, StashPop, TrashUntrackedFiles, UnstageAll,};use gpui::{    Action, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, Corner, DismissEvent, Empty, Entity,    EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent, Point,    PromptLevel, ScrollStrategy, Subscription, Task, TextStyle, UniformListScrollHandle,    WeakEntity, actions, anchored, deferred, point, size, uniform_list,};use itertools::Itertools;use language::{Buffer, File};use language_model::{    ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,};use menu;use multi_buffer::ExcerptInfo;use notifications::status_toast::{StatusToast, ToastIcon};use panel::{PanelHeader, panel_button, panel_filled_button, panel_icon_button};use project::{    Fs, Project, ProjectPath,    git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},    project_settings::{GitPathStyle, ProjectSettings},};use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES};use proto::RpcError;use serde::{Deserialize, Serialize};use settings::{Settings, SettingsStore, StatusStyle};use smallvec::SmallVec;use std::future::Future;use std::ops::Range;use std::path::Path;use std::{sync::Arc, time::Duration, usize};use strum::{IntoEnumIterator, VariantNames};use theme::ThemeSettings;use time::OffsetDateTime;use ui::{    ButtonLike, Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IndentGuideColors,    PopoverMenu, RenderedIndentGuide, ScrollAxes, Scrollbars, SplitButton, Tooltip, WithScrollbar,    prelude::*,};use util::paths::PathStyle;use util::{ResultExt, TryFutureExt, maybe, rel_path::RelPath};use workspace::SERIALIZATION_THROTTLE_TIME;use workspace::{    Workspace,    dock::{DockPosition, Panel, PanelEvent},    notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt},}; actions!(    git_panel,    [        /// Closes the git panel.        Close,        /// Toggles the git panel.        Toggle,        /// Toggles focus on the git panel.        ToggleFocus,        /// Opens the git panel menu.        OpenMenu,        /// Focuses on the commit message editor.        FocusEditor,        /// Focuses on the changes list.        FocusChanges,        /// Select next git panel menu item, and show it in the diff view        NextEntry,        /// Select previous git panel menu item, and show it in the diff view        PreviousEntry,        /// Select first git panel menu item, and show it in the diff view        FirstEntry,        /// Select last git panel menu item, and show it in the diff view        LastEntry,        /// Toggles automatic co-author suggestions.        ToggleFillCoAuthors,        /// Toggles sorting entries by path vs status.        ToggleSortByPath,        /// Toggles showing entries in tree vs flat view.        ToggleTreeView,        /// Expands the selected entry to show its children.        ExpandSelectedEntry,        /// Collapses the selected entry to hide its children.        CollapseSelectedEntry,    ]); actions!(    git_graph,    [        /// Opens the Git Graph Tab.        Open,    ]); /// Opens the Git Graph Tab at a specific commit.#[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, gpui::Action)]#[action(namespace = git_graph)]pub struct OpenAtCommit {    pub sha: String,} fn prompt<T>(    msg: &str,    detail: Option<&str>,    window: &mut Window,    cx: &mut App,) -> Task<anyhow::Result<T>>where    T: IntoEnumIterator + VariantNames + 'static,{    let rx = window.prompt(PromptLevel::Info, msg, detail, T::VARIANTS, cx);    cx.spawn(async move |_| Ok(T::iter().nth(rx.await?).unwrap()))} #[derive(strum::EnumIter, strum::VariantNames)]#[strum(serialize_all = "title_case")]enum TrashCancel {    Trash,    Cancel,} struct GitMenuState {    has_tracked_changes: bool,    has_staged_changes: bool,    has_unstaged_changes: bool,    has_new_changes: bool,    sort_by_path: bool,    has_stash_items: bool,    tree_view: bool,} fn git_panel_context_menu(    focus_handle: FocusHandle,    state: GitMenuState,    window: &mut Window,    cx: &mut App,) -> Entity<ContextMenu> {    ContextMenu::build(window, cx, move |context_menu, _, _| {        context_menu            .context(focus_handle)            .action_disabled_when(                !state.has_unstaged_changes,                "Stage All",                StageAll.boxed_clone(),            )            .action_disabled_when(                !state.has_staged_changes,                "Unstage All",                UnstageAll.boxed_clone(),            )            .separator()            .action_disabled_when(                !(state.has_new_changes || state.has_tracked_changes),                "Stash All",                StashAll.boxed_clone(),            )            .action_disabled_when(!state.has_stash_items, "Stash Pop", StashPop.boxed_clone())            .action("View Stash", zed_actions::git::ViewStash.boxed_clone())            .separator()            .action("Open Diff", project_diff::Diff.boxed_clone())            .separator()            .action_disabled_when(                !state.has_tracked_changes,                "Discard Tracked Changes",                RestoreTrackedFiles.boxed_clone(),            )            .action_disabled_when(                !state.has_new_changes,                "Trash Untracked Files",                TrashUntrackedFiles.boxed_clone(),            )            .separator()            .entry(                if state.tree_view {                    "Flat View"                } else {                    "Tree View"                },                Some(Box::new(ToggleTreeView)),                move |window, cx| window.dispatch_action(Box::new(ToggleTreeView), cx),            )            .when(!state.tree_view, |this| {                this.entry(                    if state.sort_by_path {                        "Sort by Status"                    } else {                        "Sort by Path"                    },                    Some(Box::new(ToggleSortByPath)),                    move |window, cx| window.dispatch_action(Box::new(ToggleSortByPath), cx),                )            })    })} const GIT_PANEL_KEY: &str = "GitPanel"; const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);// TODO: We should revise this part. It seems the indentation width is not aligned with the one in project panelconst TREE_INDENT: f32 = 16.0; pub fn register(workspace: &mut Workspace) {    workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {        workspace.toggle_panel_focus::<GitPanel>(window, cx);    });    workspace.register_action(|workspace, _: &Toggle, window, cx| {        if !workspace.toggle_panel_focus::<GitPanel>(window, cx) {            workspace.close_panel::<GitPanel>(window, cx);        }    });    workspace.register_action(|workspace, _: &ExpandCommitEditor, window, cx| {        CommitModal::toggle(workspace, None, window, cx)    });    workspace.register_action(|workspace, _: &git::Init, window, cx| {        if let Some(panel) = workspace.panel::<GitPanel>(cx) {            panel.update(cx, |panel, cx| panel.git_init(window, cx));        }    });} #[derive(Debug, Clone)]pub enum Event {    Focus,} #[derive(Serialize, Deserialize)]struct SerializedGitPanel {    width: Option<Pixels>,    #[serde(default)]    amend_pending: bool,    #[serde(default)]    signoff_enabled: bool,} #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]enum Section {    Conflict,    Tracked,    New,} #[derive(Debug, PartialEq, Eq, Clone)]struct GitHeaderEntry {    header: Section,} impl GitHeaderEntry {    pub fn contains(&self, status_entry: &GitStatusEntry, repo: &Repository) -> bool {        let this = &self.header;        let status = status_entry.status;        match this {            Section::Conflict => {                repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path)            }            Section::Tracked => !status.is_created(),            Section::New => status.is_created(),        }    }    pub fn title(&self) -> &'static str {        match self.header {            Section::Conflict => "Conflicts",            Section::Tracked => "Tracked",            Section::New => "Untracked",        }    }} #[derive(Debug, PartialEq, Eq, Clone)]enum GitListEntry {    Status(GitStatusEntry),    TreeStatus(GitTreeStatusEntry),    Directory(GitTreeDirEntry),    Header(GitHeaderEntry),} impl GitListEntry {    fn status_entry(&self) -> Option<&GitStatusEntry> {        match self {            GitListEntry::Status(entry) => Some(entry),            GitListEntry::TreeStatus(entry) => Some(&entry.entry),            _ => None,        }    }     fn directory_entry(&self) -> Option<&GitTreeDirEntry> {        match self {            GitListEntry::Directory(entry) => Some(entry),            _ => None,        }    }     /// Returns the tree indentation depth for this entry.    fn depth(&self) -> usize {        match self {            GitListEntry::Directory(dir) => dir.depth,            GitListEntry::TreeStatus(status) => status.depth,            _ => 0,        }    }} enum GitPanelViewMode {    Flat,    Tree(TreeViewState),} impl GitPanelViewMode {    fn from_settings(cx: &App) -> Self {        if GitPanelSettings::get_global(cx).tree_view {            GitPanelViewMode::Tree(TreeViewState::default())        } else {            GitPanelViewMode::Flat        }    }     fn tree_state(&self) -> Option<&TreeViewState> {        match self {            GitPanelViewMode::Tree(state) => Some(state),            GitPanelViewMode::Flat => None,        }    }     fn tree_state_mut(&mut self) -> Option<&mut TreeViewState> {        match self {            GitPanelViewMode::Tree(state) => Some(state),            GitPanelViewMode::Flat => None,        }    }} #[derive(Default)]struct TreeViewState {    // Maps visible index to actual entry index.    // Length equals the number of visible entries.    // This is needed because some entries (like collapsed directories) may be hidden.    logical_indices: Vec<usize>,    expanded_dirs: HashMap<TreeKey, bool>,    directory_descendants: HashMap<TreeKey, Vec<GitStatusEntry>>,} impl TreeViewState {    fn build_tree_entries(        &mut self,        section: Section,        mut entries: Vec<GitStatusEntry>,        seen_directories: &mut HashSet<TreeKey>,    ) -> Vec<(GitListEntry, bool)> {        if entries.is_empty() {            return Vec::new();        }         entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path));         let mut root = TreeNode::default();        for entry in entries {            let components: Vec<&str> = entry.repo_path.components().collect();            if components.is_empty() {                root.files.push(entry);                continue;            }             let mut current = &mut root;            let mut current_path = String::new();             for (ix, component) in components.iter().enumerate() {                if ix == components.len() - 1 {                    current.files.push(entry.clone());                } else {                    if !current_path.is_empty() {                        current_path.push('/');                    }                    current_path.push_str(component);                    let dir_path = RepoPath::new(&current_path)                        .expect("repo path from status entry component");                     let component = SharedString::from(component.to_string());                     current = current                        .children                        .entry(component.clone())                        .or_insert_with(|| TreeNode {                            name: component,                            path: Some(dir_path),                            ..Default::default()                        });                }            }        }         let (flattened, _) = self.flatten_tree(&root, section, 0, seen_directories);        flattened    }     fn flatten_tree(        &mut self,        node: &TreeNode,        section: Section,        depth: usize,        seen_directories: &mut HashSet<TreeKey>,    ) -> (Vec<(GitListEntry, bool)>, Vec<GitStatusEntry>) {        let mut all_statuses = Vec::new();        let mut flattened = Vec::new();         for child in node.children.values() {            let (terminal, name) = Self::compact_directory_chain(child);            let Some(path) = terminal.path.clone().or_else(|| child.path.clone()) else {                continue;            };            let (child_flattened, mut child_statuses) =                self.flatten_tree(terminal, section, depth + 1, seen_directories);            let key = TreeKey { section, path };            let expanded = *self.expanded_dirs.get(&key).unwrap_or(&true);            self.expanded_dirs.entry(key.clone()).or_insert(true);            seen_directories.insert(key.clone());             self.directory_descendants                .insert(key.clone(), child_statuses.clone());             flattened.push((                GitListEntry::Directory(GitTreeDirEntry {                    key,                    name,                    depth,                    expanded,                }),                true,            ));             if expanded {                flattened.extend(child_flattened);            } else {                flattened.extend(child_flattened.into_iter().map(|(child, _)| (child, false)));            }             all_statuses.append(&mut child_statuses);        }         for file in &node.files {            all_statuses.push(file.clone());            flattened.push((                GitListEntry::TreeStatus(GitTreeStatusEntry {                    entry: file.clone(),                    depth,                }),                true,            ));        }         (flattened, all_statuses)    }     fn compact_directory_chain(mut node: &TreeNode) -> (&TreeNode, SharedString) {        let mut parts = vec![node.name.clone()];        while node.files.is_empty() && node.children.len() == 1 {            let Some(child) = node.children.values().next() else {                continue;            };            if child.path.is_none() {                break;            }            parts.push(child.name.clone());            node = child;        }        let name = parts.join("/");        (node, SharedString::from(name))    }}

Lines 1–500 of 7853.