1135 lines · 42.1 KB · Rust
RawDownload
use std::path::PathBuf;use std::sync::Arc; use anyhow::Context as _;use collections::HashSet;use fuzzy::StringMatchCandidate;use git::repository::Worktree as GitWorktree;use gpui::{    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,    IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, TaskExt,    WeakEntity, Window, actions, rems,};use picker::{Picker, PickerDelegate, PickerEditorPosition};use project::Project;use project::git_store::RepositoryEvent;use ui::{    Button, Divider, HighlightedLabel, IconButton, KeyBinding, ListItem, ListItemSpacing, Tooltip,    prelude::*,};use util::ResultExt as _;use util::paths::PathExt;use workspace::{    ModalView, MultiWorkspace, Workspace, dock::DockPosition, notifications::DetachAndPromptErr,}; use crate::git_panel::show_error_toast;use zed_actions::{    CreateWorktree, NewWorktreeBranchTarget, OpenWorktreeInNewWindow, SwitchWorktree,}; actions!(worktree_picker, [DeleteWorktree]); pub struct WorktreePicker {    picker: Entity<Picker<WorktreePickerDelegate>>,    focus_handle: FocusHandle,    _subscriptions: Vec<Subscription>,} impl WorktreePicker {    pub fn new(        project: Entity<Project>,        workspace: WeakEntity<Workspace>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let focused_dock = workspace            .upgrade()            .and_then(|workspace| workspace.read(cx).focused_dock_position(window, cx));        Self::new_inner(project, workspace, focused_dock, false, window, cx)    }     pub fn new_modal(        project: Entity<Project>,        workspace: WeakEntity<Workspace>,        focused_dock: Option<DockPosition>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        Self::new_inner(project, workspace, focused_dock, true, window, cx)    }     fn new_inner(        project: Entity<Project>,        workspace: WeakEntity<Workspace>,        focused_dock: Option<DockPosition>,        show_footer: bool,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let project_ref = project.read(cx);        let project_worktree_paths: HashSet<PathBuf> = project_ref            .visible_worktrees(cx)            .map(|wt| wt.read(cx).abs_path().to_path_buf())            .collect();         let has_multiple_repositories = project_ref.repositories(cx).len() > 1;        let repository = project_ref.active_repository(cx);         let current_branch_name = repository.as_ref().and_then(|repo| {            repo.read(cx)                .branch                .as_ref()                .map(|branch| branch.name().to_string())        });         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))        });         let initial_matches = vec![WorktreeEntry::CreateFromCurrentBranch];         let delegate = WorktreePickerDelegate {            matches: initial_matches,            all_worktrees: Vec::new(),            project_worktree_paths,            selected_index: 0,            project,            workspace,            focused_dock,            current_branch_name,            default_branch_name: None,            has_multiple_repositories,            focus_handle: cx.focus_handle(),            show_footer,        };         let picker = cx.new(|cx| {            Picker::list(delegate, window, cx)                .list_measure_all()                .show_scrollbar(true)                .modal(false)                .max_height(Some(rems(20.).into()))        });         let picker_focus_handle = picker.focus_handle(cx);        picker.update(cx, |picker, _| {            picker.delegate.focus_handle = picker_focus_handle;        });         let mut subscriptions = Vec::new();         {            let picker_handle = picker.downgrade();            cx.spawn_in(window, async move |_this, cx| {                let all_worktrees: Vec<_> = match all_worktrees_request {                    Some(req) => match req.await {                        Ok(Ok(worktrees)) => {                            worktrees.into_iter().filter(|wt| !wt.is_bare).collect()                        }                        Ok(Err(err)) => {                            log::warn!("WorktreePicker: git worktree list failed: {err}");                            return anyhow::Ok(());                        }                        Err(_) => {                            log::warn!("WorktreePicker: worktree request was cancelled");                            return anyhow::Ok(());                        }                    },                    None => Vec::new(),                };                 let default_branch = match default_branch_request {                    Some(req) => req.await.ok().and_then(Result::ok).flatten(),                    None => None,                };                 picker_handle.update_in(cx, |picker, window, cx| {                    picker.delegate.all_worktrees = all_worktrees;                    picker.delegate.default_branch_name =                        default_branch.map(|branch| branch.to_string());                    picker.refresh(window, cx);                })?;                 anyhow::Ok(())            })            .detach_and_log_err(cx);        }         if let Some(repo) = &repository {            let picker_entity = picker.downgrade();            subscriptions.push(cx.subscribe_in(                repo,                window,                move |_this, repo, event: &RepositoryEvent, window, cx| {                    if matches!(event, RepositoryEvent::GitWorktreeListChanged) {                        let worktrees_request = repo.update(cx, |repo, _| repo.worktrees());                        let picker = picker_entity.clone();                        cx.spawn_in(window, async move |_, cx| {                            let all_worktrees: Vec<_> = worktrees_request                                .await??                                .into_iter()                                .filter(|wt| !wt.is_bare)                                .collect();                            picker.update_in(cx, |picker, window, cx| {                                picker.delegate.all_worktrees = all_worktrees;                                picker.refresh(window, cx);                            })?;                            anyhow::Ok(())                        })                        .detach_and_log_err(cx);                    }                },            ));        }         subscriptions.push(cx.subscribe(&picker, |_, _, _, cx| {            cx.emit(DismissEvent);        }));         Self {            focus_handle: picker.focus_handle(cx),            picker,            _subscriptions: subscriptions,        }    }} impl Focusable for WorktreePicker {    fn focus_handle(&self, _cx: &App) -> FocusHandle {        self.focus_handle.clone()    }} impl ModalView for WorktreePicker {}impl EventEmitter<DismissEvent> for WorktreePicker {} impl Render for WorktreePicker {    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        v_flex()            .key_context("WorktreePicker")            .w(rems(34.))            .elevation_3(cx)            .child(self.picker.clone())            .on_mouse_down_out(cx.listener(|_, _, _, cx| {                cx.emit(DismissEvent);            }))            .on_action(cx.listener(|this, _: &DeleteWorktree, window, cx| {                this.picker.update(cx, |picker, cx| {                    let ix = picker.delegate.selected_index;                    picker.delegate.delete_worktree(ix, window, cx);                });            }))    }} #[derive(Clone)]enum WorktreeEntry {    CreateFromCurrentBranch,    CreateFromDefaultBranch {        default_branch_name: String,    },    Separator,    Worktree {        worktree: GitWorktree,        positions: Vec<usize>,    },    CreateNamed {        name: String,        from_branch: Option<String>,        disabled_reason: Option<String>,    },} struct WorktreePickerDelegate {    matches: Vec<WorktreeEntry>,    all_worktrees: Vec<GitWorktree>,    project_worktree_paths: HashSet<PathBuf>,    selected_index: usize,    project: Entity<Project>,    workspace: WeakEntity<Workspace>,    focused_dock: Option<DockPosition>,    current_branch_name: Option<String>,    default_branch_name: Option<String>,    has_multiple_repositories: bool,    focus_handle: FocusHandle,    show_footer: bool,} impl WorktreePickerDelegate {    fn build_fixed_entries(&self) -> Vec<WorktreeEntry> {        let mut entries = Vec::new();         entries.push(WorktreeEntry::CreateFromCurrentBranch);         if !self.has_multiple_repositories {            if let Some(ref default_branch) = self.default_branch_name {                let is_different = self                    .current_branch_name                    .as_ref()                    .is_none_or(|current| current != default_branch);                if is_different {                    entries.push(WorktreeEntry::CreateFromDefaultBranch {                        default_branch_name: default_branch.clone(),                    });                }            }        }         entries    }     fn all_repo_worktrees(&self) -> &[GitWorktree] {        &self.all_worktrees    }     fn creation_blocked_reason(&self, cx: &App) -> Option<SharedString> {        let project = self.project.read(cx);        if project.is_via_collab() {            Some("Worktree creation is not supported in collaborative projects".into())        } else if project.repositories(cx).is_empty() {            Some("Requires a Git repository in the project".into())        } else {            None        }    }     fn can_delete_worktree(&self, worktree: &GitWorktree) -> bool {        !worktree.is_main && !self.project_worktree_paths.contains(&worktree.path)    }     fn delete_worktree(&self, ix: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {        let Some(entry) = self.matches.get(ix) else {            return;        };        let WorktreeEntry::Worktree { worktree, .. } = entry else {            return;        };        if !self.can_delete_worktree(worktree) {            return;        }         let repo = self.project.read(cx).active_repository(cx);        let Some(repo) = repo else {            return;        };        let path = worktree.path.clone();        let workspace = self.workspace.clone();         cx.spawn_in(window, async move |picker, cx| {            let result = repo                .update(cx, |repo, _| repo.remove_worktree(path.clone(), false))                .await?;             if let Err(error) = result {                log::error!("Failed to remove worktree: {}", error);                 if let Some(workspace) = workspace.upgrade() {                    cx.update(|_window, cx| {                        show_error_toast(                            workspace,                            format!("worktree remove {}", path.display()),                            error,                            cx,                        )                    })?;                }                 return Ok(());            }             picker.update_in(cx, |picker, _window, cx| {                picker.delegate.matches.retain(|e| {                    !matches!(e, WorktreeEntry::Worktree { worktree, .. } if worktree.path == path)                });                picker.delegate.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_and_log_err(cx);    }     fn sync_selected_index(&mut self, has_query: bool) {        if !has_query {            return;        }         if let Some(index) = self            .matches            .iter()            .position(|entry| matches!(entry, WorktreeEntry::Worktree { .. }))        {            self.selected_index = index;        } else if let Some(index) = self            .matches            .iter()            .position(|entry| matches!(entry, WorktreeEntry::CreateNamed { .. }))        {            self.selected_index = index;        } else {            self.selected_index = 0;        }    }} impl PickerDelegate for WorktreePickerDelegate {    type ListItem = AnyElement;     fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {        "Select a worktree…".into()    }     fn editor_position(&self) -> PickerEditorPosition {        PickerEditorPosition::Start    }     fn match_count(&self) -> usize {        self.matches.len()    }     fn selected_index(&self) -> usize {        self.selected_index    }     fn set_selected_index(        &mut self,        ix: usize,        _window: &mut Window,        _cx: &mut Context<Picker<Self>>,    ) {        self.selected_index = ix;    }     fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {        !matches!(self.matches.get(ix), Some(WorktreeEntry::Separator))    }     fn update_matches(        &mut self,        query: String,        window: &mut Window,        cx: &mut Context<Picker<Self>>,    ) -> Task<()> {        let repo_worktrees = self.all_repo_worktrees().to_vec();         let normalized_query = query.replace(' ', "-");        let main_worktree_path = self            .all_worktrees            .iter()            .find(|wt| wt.is_main)            .map(|wt| wt.path.clone());        let has_named_worktree = self.all_worktrees.iter().any(|worktree| {            worktree.directory_name(main_worktree_path.as_deref()) == normalized_query        });        let create_named_disabled_reason: Option<String> = if self.has_multiple_repositories {            Some("Cannot create a named worktree in a project with multiple repositories".into())        } else if has_named_worktree {            Some("A worktree with this name already exists".into())        } else {            None        };         let show_default_branch_create = !self.has_multiple_repositories            && self.default_branch_name.as_ref().is_some_and(|default| {                self.current_branch_name                    .as_ref()                    .is_none_or(|current| current != default)            });        let default_branch_name = self.default_branch_name.clone();         if query.is_empty() {            let mut matches = self.build_fixed_entries();             if !repo_worktrees.is_empty() {                let main_worktree_path = repo_worktrees                    .iter()                    .find(|wt| wt.is_main)                    .map(|wt| wt.path.clone());                 let mut sorted = repo_worktrees;                let project_paths = &self.project_worktree_paths;                 sorted.sort_by(|a, b| {                    let a_is_current = project_paths.contains(&a.path);                    let b_is_current = project_paths.contains(&b.path);                    b_is_current.cmp(&a_is_current).then_with(|| {                        a.directory_name(main_worktree_path.as_deref())                            .cmp(&b.directory_name(main_worktree_path.as_deref()))                    })                });                 matches.push(WorktreeEntry::Separator);                for worktree in sorted {                    matches.push(WorktreeEntry::Worktree {                        worktree,                        positions: Vec::new(),                    });                }            }             self.matches = matches;            self.sync_selected_index(false);            return Task::ready(());        }         let main_worktree_path = repo_worktrees            .iter()            .find(|wt| wt.is_main)            .map(|wt| wt.path.clone());        let candidates: Vec<_> = repo_worktrees            .iter()            .enumerate()            .map(|(ix, worktree)| {                StringMatchCandidate::new(                    ix,                    &worktree.directory_name(main_worktree_path.as_deref()),                )            })            .collect(); 

Lines 1–500 of 1135.