900 lines · 30.5 KB · Rust
RawDownload
use anyhow::Context as _;use collections::HashSet;use fuzzy::StringMatchCandidate; use git::repository::Worktree as GitWorktree;use gpui::{    Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle,    Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement,    Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems,};use picker::{Picker, PickerDelegate, PickerEditorPosition};use project::project_settings::ProjectSettings;use project::{    git_store::Repository,    trusted_worktrees::{PathTrust, TrustedWorktrees},};use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};use remote_connection::{RemoteConnectionModal, connect};use settings::Settings;use std::{path::PathBuf, sync::Arc};use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*};use util::ResultExt;use workspace::{ModalView, MultiWorkspace, Workspace, notifications::DetachAndPromptErr}; use crate::git_panel::show_error_toast; actions!(    git,    [        WorktreeFromDefault,        WorktreeFromDefaultOnWindow,        DeleteWorktree    ]); pub fn open(    workspace: &mut Workspace,    _: &zed_actions::git::Worktree,    window: &mut Window,    cx: &mut Context<Workspace>,) {    let repository = workspace.project().read(cx).active_repository(cx);    let workspace_handle = workspace.weak_handle();    workspace.toggle_modal(window, cx, |window, cx| {        WorktreeList::new(repository, workspace_handle, rems(34.), window, cx)    })} pub fn create_embedded(    repository: Option<Entity<Repository>>,    workspace: WeakEntity<Workspace>,    width: Rems,    window: &mut Window,    cx: &mut Context<WorktreeList>,) -> WorktreeList {    WorktreeList::new_embedded(repository, workspace, width, window, cx)} pub struct WorktreeList {    width: Rems,    pub picker: Entity<Picker<WorktreeListDelegate>>,    picker_focus_handle: FocusHandle,    _subscription: Option<Subscription>,    embedded: bool,} impl WorktreeList {    fn new(        repository: Option<Entity<Repository>>,        workspace: WeakEntity<Workspace>,        width: Rems,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let mut this = Self::new_inner(repository, workspace, width, false, window, cx);        this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| {            cx.emit(DismissEvent);        }));        this    }     fn new_inner(        repository: Option<Entity<Repository>>,        workspace: WeakEntity<Workspace>,        width: Rems,        embedded: bool,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let all_worktrees_request = repository            .clone()            .map(|repository| repository.update(cx, |repository, _| repository.worktrees()));         let default_branch_request = repository.clone().map(|repository| {            repository.update(cx, |repository, _| repository.default_branch(false))        });         cx.spawn_in(window, async move |this, cx| {            let all_worktrees: Vec<_> = all_worktrees_request                .context("No active repository")?                .await??                .into_iter()                .filter(|worktree| worktree.ref_name.is_some()) // hide worktrees without a branch                .collect();             let default_branch = default_branch_request                .context("No active repository")?                .await                .map(Result::ok)                .ok()                .flatten()                .flatten();             this.update_in(cx, |this, window, cx| {                this.picker.update(cx, |picker, cx| {                    picker.delegate.all_worktrees = Some(all_worktrees);                    picker.delegate.default_branch = default_branch;                    picker.refresh(window, cx);                })            })?;             anyhow::Ok(())        })        .detach_and_log_err(cx);         let delegate = WorktreeListDelegate::new(workspace, repository, window, cx);        let picker = cx.new(|cx| {            Picker::uniform_list(delegate, window, cx)                .show_scrollbar(true)                .modal(!embedded)        });        let picker_focus_handle = picker.focus_handle(cx);        picker.update(cx, |picker, _| {            picker.delegate.focus_handle = picker_focus_handle.clone();        });         Self {            picker,            picker_focus_handle,            width,            _subscription: None,            embedded,        }    }     fn new_embedded(        repository: Option<Entity<Repository>>,        workspace: WeakEntity<Workspace>,        width: Rems,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let mut this = Self::new_inner(repository, workspace, width, true, window, cx);        this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| {            cx.emit(DismissEvent);        }));        this    }     pub fn handle_modifiers_changed(        &mut self,        ev: &ModifiersChangedEvent,        _: &mut Window,        cx: &mut Context<Self>,    ) {        self.picker            .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)    }     pub fn handle_new_worktree(        &mut self,        replace_current_window: bool,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.picker.update(cx, |picker, cx| {            let ix = picker.delegate.selected_index();            let Some(entry) = picker.delegate.matches.get(ix) else {                return;            };            let Some(default_branch) = picker.delegate.default_branch.clone() else {                return;            };            if !entry.is_new {                return;            }            picker.delegate.create_worktree(                entry.worktree.display_name(),                replace_current_window,                Some(default_branch.into()),                window,                cx,            );        })    }     pub fn handle_delete(        &mut self,        _: &DeleteWorktree,        window: &mut Window,        cx: &mut Context<Self>,    ) {        self.picker.update(cx, |picker, cx| {            picker                .delegate                .delete_at(picker.delegate.selected_index, window, cx)        })    }}impl ModalView for WorktreeList {}impl EventEmitter<DismissEvent> for WorktreeList {} impl Focusable for WorktreeList {    fn focus_handle(&self, _: &App) -> FocusHandle {        self.picker_focus_handle.clone()    }} impl Render for WorktreeList {    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        v_flex()            .key_context("GitWorktreeSelector")            .w(self.width)            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))            .on_action(cx.listener(|this, _: &WorktreeFromDefault, w, cx| {                this.handle_new_worktree(false, w, cx)            }))            .on_action(cx.listener(|this, _: &WorktreeFromDefaultOnWindow, w, cx| {                this.handle_new_worktree(true, w, cx)            }))            .on_action(cx.listener(|this, _: &DeleteWorktree, window, cx| {                this.handle_delete(&DeleteWorktree, window, cx)            }))            .child(self.picker.clone())            .when(!self.embedded, |el| {                el.on_mouse_down_out({                    cx.listener(move |this, _, window, cx| {                        this.picker.update(cx, |this, cx| {                            this.cancel(&Default::default(), window, cx);                        })                    })                })            })    }} #[derive(Debug, Clone)]struct WorktreeEntry {    worktree: GitWorktree,    positions: Vec<usize>,    is_new: bool,} pub struct WorktreeListDelegate {    matches: Vec<WorktreeEntry>,    all_worktrees: Option<Vec<GitWorktree>>,    workspace: WeakEntity<Workspace>,    repo: Option<Entity<Repository>>,    selected_index: usize,    last_query: String,    modifiers: Modifiers,    focus_handle: FocusHandle,    default_branch: Option<SharedString>,} impl WorktreeListDelegate {    fn new(        workspace: WeakEntity<Workspace>,        repo: Option<Entity<Repository>>,        _window: &mut Window,        cx: &mut Context<WorktreeList>,    ) -> Self {        Self {            matches: vec![],            all_worktrees: None,            workspace,            selected_index: 0,            repo,            last_query: Default::default(),            modifiers: Default::default(),            focus_handle: cx.focus_handle(),            default_branch: None,        }    }     fn create_worktree(        &self,        worktree_branch: &str,        replace_current_window: bool,        commit: Option<String>,        window: &mut Window,        cx: &mut Context<Picker<Self>>,    ) {        let Some(repo) = self.repo.clone() else {            return;        };         let branch = worktree_branch.to_string();        let workspace = self.workspace.clone();        cx.spawn_in(window, async move |_, cx| {            let (receiver, new_worktree_path) = repo.update(cx, |repo, cx| {                let worktree_directory_setting = ProjectSettings::get_global(cx)                    .git                    .worktree_directory                    .clone();                let new_worktree_path =                    repo.path_for_new_linked_worktree(&branch, &worktree_directory_setting)?;                let receiver =                    repo.create_worktree(branch.clone(), new_worktree_path.clone(), commit);                anyhow::Ok((receiver, new_worktree_path))            })?;            receiver.await??;             workspace.update(cx, |workspace, cx| {                if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {                    let repo_path = &repo.read(cx).snapshot().work_directory_abs_path;                    let project = workspace.project();                    if let Some((parent_worktree, _)) =                        project.read(cx).find_worktree(repo_path, cx)                    {                        let worktree_store = project.read(cx).worktree_store();                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {                            if trusted_worktrees.can_trust(                                &worktree_store,                                parent_worktree.read(cx).id(),                                cx,                            ) {                                trusted_worktrees.trust(                                    &worktree_store,                                    HashSet::from_iter([PathTrust::AbsPath(                                        new_worktree_path.clone(),                                    )]),                                    cx,                                );                            }                        });                    }                }            })?;             let (connection_options, app_state, is_local) =                workspace.update(cx, |workspace, cx| {                    let project = workspace.project().clone();                    let connection_options = project.read(cx).remote_connection_options(cx);                    let app_state = workspace.app_state().clone();                    let is_local = project.read(cx).is_local();                    (connection_options, app_state, is_local)                })?;             if is_local {                workspace                    .update_in(cx, |workspace, window, cx| {                        workspace.open_workspace_for_paths(                            replace_current_window,                            vec![new_worktree_path],                            window,                            cx,                        )                    })?                    .await?;            } else if let Some(connection_options) = connection_options {                open_remote_worktree(                    connection_options,                    vec![new_worktree_path],                    app_state,                    workspace.clone(),                    replace_current_window,                    cx,                )                .await?;            }             anyhow::Ok(())        })        .detach_and_prompt_err("Failed to create worktree", window, cx, |e, _, _| {            let msg = e.to_string();            if msg.contains("git.worktree_directory") {                Some(format!("Invalid git.worktree_directory setting: {}", e))            } else {                Some(msg)            }        });    }     fn open_worktree(        &self,        worktree_path: &PathBuf,        replace_current_window: bool,        window: &mut Window,        cx: &mut Context<Picker<Self>>,    ) {        let workspace = self.workspace.clone();        let path = worktree_path.clone();         let Some((connection_options, app_state, is_local)) = workspace            .update(cx, |workspace, cx| {                let project = workspace.project().clone();                let connection_options = project.read(cx).remote_connection_options(cx);                let app_state = workspace.app_state().clone();                let is_local = project.read(cx).is_local();                (connection_options, app_state, is_local)            })            .log_err()        else {            return;        };         if is_local {            let open_task = workspace.update(cx, |workspace, cx| {                workspace.open_workspace_for_paths(replace_current_window, vec![path], window, cx)            });            cx.spawn(async move |_, _| {                open_task?.await?;                anyhow::Ok(())            })            .detach_and_prompt_err(                "Failed to open worktree",                window,                cx,                |e, _, _| Some(e.to_string()),            );        } else if let Some(connection_options) = connection_options {            cx.spawn_in(window, async move |_, cx| {                open_remote_worktree(                    connection_options,                    vec![path],                    app_state,                    workspace,                    replace_current_window,                    cx,                )                .await            })            .detach_and_prompt_err(                "Failed to open worktree",                window,                cx,                |e, _, _| Some(e.to_string()),            );        }         cx.emit(DismissEvent);    }     fn base_branch<'a>(&'a self, cx: &'a mut Context<Picker<Self>>) -> Option<&'a str> {        self.repo            .as_ref()            .and_then(|repo| repo.read(cx).branch.as_ref().map(|b| b.name()))    }     fn delete_at(&self, idx: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {        let Some(entry) = self.matches.get(idx).cloned() else {            return;        };        if entry.is_new {            return;        }        let Some(repo) = self.repo.clone() else {            return;        };        let workspace = self.workspace.clone();        let path = entry.worktree.path;         cx.spawn_in(window, async move |picker, cx| {            let result = repo                .update(cx, |repo, _| repo.remove_worktree(path.clone(), false))                .await?;             if let Err(e) = result {                log::error!("Failed to remove worktree: {}", e);                if let Some(workspace) = workspace.upgrade() {                    cx.update(|_window, cx| {                        show_error_toast(                            workspace,                            format!("worktree remove {}", path.display()),                            e,                            cx,                        )                    })?;                }                return Ok(());            }             picker.update_in(cx, |picker, _, cx| {                picker.delegate.matches.retain(|e| e.worktree.path != path);                if let Some(all_worktrees) = &mut picker.delegate.all_worktrees {                    all_worktrees.retain(|w| w.path != path);                }                if picker.delegate.matches.is_empty() {                    picker.delegate.selected_index = 0;                } else if picker.delegate.selected_index >= picker.delegate.matches.len() {                    picker.delegate.selected_index = picker.delegate.matches.len() - 1;                }                cx.notify();            })?;             anyhow::Ok(())        })        .detach();    }

Lines 1–500 of 900.