Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:32:48.929Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

worktree_picker.rs

2302 lines · 88.5 KB · rust
1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use anyhow::Context as _;
5use collections::HashSet;
6use fuzzy::StringMatchCandidate;
7use git::repository::Worktree as GitWorktree;
8use gpui::{
9    Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
10    InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, PromptLevel,
11    Render, SharedString, Styled, Subscription, Task, TaskExt, WeakEntity, Window, actions,
12};
13use picker::{Picker, PickerDelegate, PickerEditorPosition};
14use project::Project;
15use project::git_store::RepositoryEvent;
16use ui::{
17    Button, CommonAnimationExt as _, Divider, HighlightedLabel, IconButton, KeyBinding, ListItem,
18    ListItemSpacing, ListSubHeader, Tooltip, prelude::*,
19};
20use util::ResultExt as _;
21use util::paths::PathExt;
22use workspace::{
23    ModalView, MultiWorkspace, Workspace, dock::DockPosition, notifications::DetachAndPromptErr,
24};
25
26use crate::git_panel::show_error_toast;
27use crate::worktree_service::{RemoteBranchName, WorktreeCreateTarget, worktree_create_targets};
28use zed_actions::{
29    CreateWorktree, NewWorktreeBranchTarget, OpenWorktreeInNewWindow, OpenWorktreeSetupTasks,
30    SwitchWorktree,
31};
32
33actions!(
34    worktree_picker,
35    [
36        /// Deletes the selected git worktree.
37        DeleteWorktree,
38        /// Force deletes the selected git worktree.
39        ForceDeleteWorktree
40    ]
41);
42
43pub struct WorktreePicker {
44    picker: Entity<Picker<WorktreePickerDelegate>>,
45    focus_handle: FocusHandle,
46    _subscriptions: Vec<Subscription>,
47}
48
49impl WorktreePicker {
50    pub fn new(
51        project: Entity<Project>,
52        workspace: WeakEntity<Workspace>,
53        window: &mut Window,
54        cx: &mut Context<Self>,
55    ) -> Self {
56        let focused_dock = workspace
57            .upgrade()
58            .and_then(|workspace| workspace.read(cx).focused_dock_position(window, cx));
59        Self::new_inner(project, workspace, focused_dock, false, window, cx)
60    }
61
62    pub fn new_modal(
63        project: Entity<Project>,
64        workspace: WeakEntity<Workspace>,
65        focused_dock: Option<DockPosition>,
66        window: &mut Window,
67        cx: &mut Context<Self>,
68    ) -> Self {
69        Self::new_inner(project, workspace, focused_dock, true, window, cx)
70    }
71
72    fn new_inner(
73        project: Entity<Project>,
74        workspace: WeakEntity<Workspace>,
75        focused_dock: Option<DockPosition>,
76        show_footer: bool,
77        window: &mut Window,
78        cx: &mut Context<Self>,
79    ) -> Self {
80        let project_ref = project.read(cx);
81
82        let active_worktree_paths: HashSet<PathBuf> = project_ref
83            .visible_worktrees(cx)
84            .map(|wt| wt.read(cx).abs_path().to_path_buf())
85            .collect();
86
87        let project_worktree_paths = active_worktree_paths.clone();
88
89        let has_multiple_repositories = project_ref.repositories(cx).len() > 1;
90        let repository = project_ref.active_repository(cx);
91
92        let current_branch_name = repository.as_ref().and_then(|repo| {
93            repo.read(cx)
94                .branch
95                .as_ref()
96                .map(|branch| branch.name().to_string())
97        });
98
99        let all_worktrees_request = repository
100            .clone()
101            .map(|repository| repository.update(cx, |repository, _| repository.worktrees()));
102
103        let default_branch_request = repository.clone().map(|repository| {
104            repository.update(cx, |repository, _| repository.default_branch(true))
105        });
106
107        let initial_matches = vec![WorktreeEntry::CreateFromCurrentBranch];
108
109        let delegate = WorktreePickerDelegate {
110            matches: initial_matches,
111            all_worktrees: Vec::new(),
112            project_worktree_paths,
113            selected_index: 0,
114            project,
115            workspace,
116            focused_dock,
117            current_branch_name,
118            default_branch: None,
119            has_multiple_repositories,
120            focus_handle: cx.focus_handle(),
121            show_footer,
122            modifiers: Modifiers::default(),
123            active_worktree_paths,
124            hovered_delete_index: None,
125            deleting_worktree_paths: HashSet::default(),
126        };
127
128        let picker = cx.new(|cx| {
129            Picker::list(delegate, window, cx)
130                .list_measure_all()
131                .show_scrollbar(true)
132                .embedded()
133        });
134
135        let picker_focus_handle = picker.focus_handle(cx);
136        picker.update(cx, |picker, _| {
137            picker.delegate.focus_handle = picker_focus_handle;
138        });
139
140        let mut subscriptions = Vec::new();
141
142        {
143            let picker_handle = picker.downgrade();
144            cx.spawn_in(window, async move |_this, cx| {
145                let all_worktrees: Vec<_> = match all_worktrees_request {
146                    Some(req) => match req.await {
147                        Ok(Ok(worktrees)) => {
148                            worktrees.into_iter().filter(|wt| !wt.is_bare).collect()
149                        }
150                        Ok(Err(err)) => {
151                            log::warn!("WorktreePicker: git worktree list failed: {err}");
152                            return anyhow::Ok(());
153                        }
154                        Err(_) => {
155                            log::warn!("WorktreePicker: worktree request was cancelled");
156                            return anyhow::Ok(());
157                        }
158                    },
159                    None => Vec::new(),
160                };
161
162                let default_branch = match default_branch_request {
163                    Some(req) => req.await.ok().and_then(Result::ok).flatten(),
164                    None => None,
165                };
166
167                picker_handle.update_in(cx, |picker, window, cx| {
168                    picker.delegate.all_worktrees = all_worktrees;
169                    picker.delegate.default_branch =
170                        default_branch.and_then(|branch| RemoteBranchName::parse(&branch));
171                    picker.delegate.refresh_project_worktree_paths(window, cx);
172                    picker.refresh(window, cx);
173                })?;
174
175                anyhow::Ok(())
176            })
177            .detach_and_log_err(cx);
178        }
179
180        if let Some(repo) = &repository {
181            let picker_entity = picker.downgrade();
182            subscriptions.push(cx.subscribe_in(
183                repo,
184                window,
185                move |_this, repo, event: &RepositoryEvent, window, cx| {
186                    if matches!(event, RepositoryEvent::GitWorktreeListChanged) {
187                        let worktrees_request = repo.update(cx, |repo, _| repo.worktrees());
188                        let picker = picker_entity.clone();
189                        cx.spawn_in(window, async move |_, cx| {
190                            let all_worktrees: Vec<_> = worktrees_request
191                                .await??
192                                .into_iter()
193                                .filter(|wt| !wt.is_bare)
194                                .collect();
195                            picker.update_in(cx, |picker, window, cx| {
196                                picker.delegate.all_worktrees = all_worktrees;
197                                picker.refresh(window, cx);
198                            })?;
199                            anyhow::Ok(())
200                        })
201                        .detach_and_log_err(cx);
202                    }
203                },
204            ));
205        }
206
207        subscriptions.push(cx.subscribe(&picker, |_, _, _, cx| {
208            cx.emit(DismissEvent);
209        }));
210
211        Self {
212            focus_handle: picker.focus_handle(cx),
213            picker,
214            _subscriptions: subscriptions,
215        }
216    }
217
218    fn handle_modifiers_changed(
219        &mut self,
220        ev: &ModifiersChangedEvent,
221        _: &mut Window,
222        cx: &mut Context<Self>,
223    ) {
224        self.picker.update(cx, |picker, cx| {
225            picker.delegate.modifiers = ev.modifiers;
226            cx.notify();
227        });
228    }
229}
230
231impl Focusable for WorktreePicker {
232    fn focus_handle(&self, _cx: &App) -> FocusHandle {
233        self.focus_handle.clone()
234    }
235}
236
237impl ModalView for WorktreePicker {}
238impl EventEmitter<DismissEvent> for WorktreePicker {}
239
240impl Render for WorktreePicker {
241    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
242        v_flex()
243            .key_context("WorktreePicker")
244            .elevation_3(cx)
245            .child(self.picker.clone())
246            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
247            .on_mouse_down_out(cx.listener(|_, _, _, cx| {
248                cx.emit(DismissEvent);
249            }))
250            .on_action(cx.listener(|_, _: &OpenWorktreeSetupTasks, _, cx| {
251                cx.emit(DismissEvent);
252                cx.propagate();
253            }))
254            .on_action(cx.listener(|this, _: &DeleteWorktree, window, cx| {
255                this.picker.update(cx, |picker, cx| {
256                    let ix = picker.delegate.selected_index;
257                    picker.delegate.delete_worktree(ix, false, window, cx);
258                });
259            }))
260            .on_action(cx.listener(|this, _: &ForceDeleteWorktree, window, cx| {
261                this.picker.update(cx, |picker, cx| {
262                    let ix = picker.delegate.selected_index;
263                    picker.delegate.delete_worktree(ix, true, window, cx);
264                });
265            }))
266    }
267}
268
269#[derive(Clone)]
270enum WorktreeEntry {
271    CreateFromCurrentBranch,
272    CreateFromDefaultBranch {
273        default_branch: RemoteBranchName,
274    },
275    Separator,
276    SectionHeader(SharedString),
277    Worktree {
278        worktree: GitWorktree,
279        positions: Vec<usize>,
280    },
281    CreateNamed {
282        name: String,
283        from_branch: Option<RemoteBranchName>,
284        disabled_reason: Option<String>,
285    },
286}
287
288struct WorktreePickerDelegate {
289    matches: Vec<WorktreeEntry>,
290    all_worktrees: Vec<GitWorktree>,
291    project_worktree_paths: HashSet<PathBuf>,
292    active_worktree_paths: HashSet<PathBuf>,
293    selected_index: usize,
294    project: Entity<Project>,
295    workspace: WeakEntity<Workspace>,
296    focused_dock: Option<DockPosition>,
297    current_branch_name: Option<String>,
298    default_branch: Option<RemoteBranchName>,
299    has_multiple_repositories: bool,
300    focus_handle: FocusHandle,
301    show_footer: bool,
302    modifiers: Modifiers,
303    hovered_delete_index: Option<usize>,
304    deleting_worktree_paths: HashSet<PathBuf>,
305}
306
307fn remove_worktree_command(path: &Path, force: bool) -> String {
308    if force {
309        format!("worktree remove --force {}", path.display())
310    } else {
311        format!("worktree remove {}", path.display())
312    }
313}
314
315struct WorktreeRemoveForceDeletePrompt {
316    required_error_substrings: &'static [&'static str],
317    message: fn(&str) -> String,
318}
319
320impl WorktreeRemoveForceDeletePrompt {
321    fn matches(&self, normalized_error_message: &str) -> bool {
322        self.required_error_substrings
323            .iter()
324            .all(|substring| normalized_error_message.contains(substring))
325    }
326}
327
328const WORKTREE_REMOVE_FORCE_DELETE_PROMPTS: &[WorktreeRemoveForceDeletePrompt] =
329    &[WorktreeRemoveForceDeletePrompt {
330        required_error_substrings: &[
331            "contains modified or untracked files",
332            "use --force to delete it",
333        ],
334        message: dirty_worktree_force_delete_prompt,
335    }];
336
337fn dirty_worktree_force_delete_prompt(display_name: &str) -> String {
338    format!("Worktree \"{display_name}\" contains modified or untracked files. Force delete it?")
339}
340
341fn force_delete_prompt_for_worktree_remove_error(
342    error: &anyhow::Error,
343    display_name: &str,
344) -> Option<String> {
345    let normalized_error_message = error.to_string().to_lowercase();
346    WORKTREE_REMOVE_FORCE_DELETE_PROMPTS
347        .iter()
348        .find(|prompt| prompt.matches(&normalized_error_message))
349        .map(|prompt| (prompt.message)(display_name))
350}
351
352struct DeleteWorktreeTooltip {
353    picker: WeakEntity<Picker<WorktreePickerDelegate>>,
354    focus_handle: FocusHandle,
355    delete_index: usize,
356    _subscription: Subscription,
357}
358
359impl DeleteWorktreeTooltip {
360    fn new(
361        picker: Entity<Picker<WorktreePickerDelegate>>,
362        focus_handle: FocusHandle,
363        delete_index: usize,
364        cx: &mut Context<Self>,
365    ) -> Self {
366        let subscription = cx.observe(&picker, |_, _, cx| cx.notify());
367        Self {
368            picker: picker.downgrade(),
369            focus_handle,
370            delete_index,
371            _subscription: subscription,
372        }
373    }
374}
375
376impl Render for DeleteWorktreeTooltip {
377    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
378        let force_delete = self
379            .picker
380            .read_with(cx, |picker, _| {
381                picker
382                    .delegate
383                    .is_force_delete_hovering_index(self.delete_index)
384            })
385            .unwrap_or(false);
386
387        if force_delete {
388            Tooltip::for_action_in(
389                "Force Delete Worktree",
390                &ForceDeleteWorktree,
391                &self.focus_handle,
392                cx,
393            )
394            .into_any_element()
395        } else {
396            Tooltip::with_meta_in(
397                "Delete Worktree",
398                Some(&DeleteWorktree),
399                "Hold alt to force delete",
400                &self.focus_handle,
401                cx,
402            )
403            .into_any_element()
404        }
405    }
406}
407
408impl WorktreePickerDelegate {
409    fn build_fixed_entries(&self) -> Vec<WorktreeEntry> {
410        worktree_create_targets(
411            self.has_multiple_repositories,
412            self.default_branch.clone(),
413            self.current_branch_name.as_deref(),
414        )
415        .into_iter()
416        .map(|target| match target {
417            WorktreeCreateTarget::CurrentBranch => WorktreeEntry::CreateFromCurrentBranch,
418            WorktreeCreateTarget::DefaultBranch(default_branch) => {
419                WorktreeEntry::CreateFromDefaultBranch { default_branch }
420            }
421        })
422        .collect()
423    }
424
425    fn all_repo_worktrees(&self) -> &[GitWorktree] {
426        &self.all_worktrees
427    }
428
429    fn creation_blocked_reason(&self, cx: &App) -> Option<SharedString> {
430        let project = self.project.read(cx);
431        if project.is_via_collab() {
432            Some("Worktree creation is not supported in collaborative projects".into())
433        } else if project.repositories(cx).is_empty() {
434            Some("Requires a Git repository in the project".into())
435        } else {
436            None
437        }
438    }
439
440    fn can_delete_worktree(&self, worktree: &GitWorktree) -> bool {
441        !worktree.is_main && !self.project_worktree_paths.contains(&worktree.path)
442    }
443
444    fn refresh_project_worktree_paths(&mut self, window: &mut Window, cx: &mut App) {
445        let mut paths = self.active_worktree_paths.clone();
446
447        if let Some(multi_workspace) = window.root::<MultiWorkspace>().flatten()
448            && let Some(workspace) = self.workspace.upgrade()
449        {
450            let group_key = workspace.read(cx).project_group_key(cx);
451            if let Some(group_workspaces) = multi_workspace
452                .read(cx)
453                .workspaces_for_project_group(&group_key, cx)
454            {
455                for group_workspace in group_workspaces {
456                    for worktree in group_workspace
457                        .read(cx)
458                        .project()
459                        .read(cx)
460                        .visible_worktrees(cx)
461                    {
462                        paths.insert(worktree.read(cx).abs_path().to_path_buf());
463                    }
464                }
465            }
466        }
467
468        self.project_worktree_paths = paths;
469    }
470
471    fn is_force_delete_hovering_index(&self, index: usize) -> bool {
472        self.modifiers.alt && self.hovered_delete_index == Some(index)
473    }
474
475    fn delete_worktree(
476        &mut self,
477        ix: usize,
478        force: bool,
479        window: &mut Window,
480        cx: &mut Context<Picker<Self>>,
481    ) {
482        let Some(entry) = self.matches.get(ix) else {
483            return;
484        };
485        let WorktreeEntry::Worktree { worktree, .. } = entry else {
486            return;
487        };
488        if !self.can_delete_worktree(worktree)
489            || self.deleting_worktree_paths.contains(&worktree.path)
490        {
491            return;
492        }
493
494        let repo = self.project.read(cx).active_repository(cx);
495        let Some(repo) = repo else {
496            return;
497        };
498        let path = worktree.path.clone();
499        let display_name = worktree.directory_name(
500            self.all_worktrees
501                .iter()
502                .find(|worktree| worktree.is_main)
503                .map(|worktree| worktree.path.as_path()),
504        );
505        let workspace = self.workspace.clone();
506
507        self.deleting_worktree_paths.insert(path.clone());
508        if self.hovered_delete_index == Some(ix) {
509            self.hovered_delete_index = None;
510        }
511        cx.notify();
512
513        cx.spawn_in(window, async move |picker, cx| {
514            let initial_result = match repo
515                .update(cx, |repo, _| repo.remove_worktree(path.clone(), force))
516                .await
517            {
518                Ok(result) => result,
519                Err(error) => {
520                    picker.update_in(cx, |picker, _window, cx| {
521                        if picker.delegate.deleting_worktree_paths.remove(&path) {
522                            cx.notify();
523                        }
524                    })?;
525                    return Err(error.into());
526                }
527            };
528
529            let (result, attempted_force) = match initial_result {
530                Ok(()) => (Ok(()), force),
531                Err(error) => {
532                    log::error!("Failed to remove worktree: {}", error);
533
534                    let force_delete_prompt = (!force)
535                        .then(|| {
536                            force_delete_prompt_for_worktree_remove_error(&error, &display_name)
537                        })
538                        .flatten();
539
540                    if let Some(prompt_message) = force_delete_prompt {
541                        picker.update_in(cx, |picker, _window, cx| {
542                            if picker.delegate.deleting_worktree_paths.remove(&path) {
543                                cx.notify();
544                            }
545                        })?;
546
547                        let answer = cx.update(|window, cx| {
548                            window.prompt(
549                                PromptLevel::Warning,
550                                &prompt_message,
551                                None,
552                                &["Force Delete", "Cancel"],
553                                cx,
554                            )
555                        })?;
556
557                        if answer.await != Ok(0) {
558                            return Ok(());
559                        }
560
561                        let should_retry = picker.update_in(cx, |picker, _window, cx| {
562                            let worktree_still_exists = picker
563                                .delegate
564                                .all_worktrees
565                                .iter()
566                                .any(|worktree| worktree.path == path);
567                            if !worktree_still_exists
568                                || !picker.delegate.deleting_worktree_paths.insert(path.clone())
569                            {
570                                return false;
571                            }
572                            cx.notify();
573                            true
574                        })?;
575
576                        if !should_retry {
577                            return Ok(());
578                        }
579
580                        let retry = match repo
581                            .update(cx, |repo, _| repo.remove_worktree(path.clone(), true))
582                            .await
583                        {
584                            Ok(result) => result,
585                            Err(error) => {
586                                picker.update_in(cx, |picker, _window, cx| {
587                                    if picker.delegate.deleting_worktree_paths.remove(&path) {
588                                        cx.notify();
589                                    }
590                                })?;
591                                return Err(error.into());
592                            }
593                        };
594
595                        if let Err(error) = &retry {
596                            log::error!("Failed to force remove worktree: {error}");
597                        }
598
599                        (retry, true)
600                    } else {
601                        (Err(error), force)
602                    }
603                }
604            };
605
606            if let Err(error) = result {
607                picker.update_in(cx, |picker, _window, cx| {
608                    if picker.delegate.deleting_worktree_paths.remove(&path) {
609                        cx.notify();
610                    }
611                })?;
612
613                if let Some(workspace) = workspace.upgrade() {
614                    cx.update(|_window, cx| {
615                        show_error_toast(
616                            workspace,
617                            remove_worktree_command(&path, attempted_force),
618                            error,
619                            cx,
620                        )
621                    })?;
622                }
623
624                return Ok(());
625            }
626
627            picker.update_in(cx, |picker, _window, cx| {
628                picker.delegate.deleting_worktree_paths.remove(&path);
629                picker.delegate.matches.retain(|e| {
630                    !matches!(e, WorktreeEntry::Worktree { worktree, .. } if worktree.path == path)
631                });
632                picker.delegate.all_worktrees.retain(|w| w.path != path);
633                if picker.delegate.matches.is_empty() {
634                    picker.delegate.selected_index = 0;
635                } else if picker.delegate.selected_index >= picker.delegate.matches.len() {
636                    picker.delegate.selected_index = picker.delegate.matches.len() - 1;
637                }
638                picker.delegate.hovered_delete_index = None;
639                cx.notify();
640            })?;
641
642            anyhow::Ok(())
643        })
644        .detach_and_log_err(cx);
645    }
646
647    /// Finds the workspace in this window (other than the picker's own
648    /// workspace) that has `worktree_path` open as a visible worktree.
649    fn workspace_for_open_worktree(
650        &self,
651        worktree_path: &Path,
652        window: &Window,
653        cx: &App,
654    ) -> Option<Entity<Workspace>> {
655        if self.active_worktree_paths.contains(worktree_path) {
656            return None;
657        }
658        let multi_workspace = window.root::<MultiWorkspace>().flatten()?;
659        let workspace = self.workspace.upgrade()?;
660        let group_key = workspace.read(cx).project_group_key(cx);
661        multi_workspace
662            .read(cx)
663            .workspaces_for_project_group(&group_key, cx)?
664            .into_iter()
665            .find(|group_workspace| {
666                *group_workspace != workspace
667                    && group_workspace
668                        .read(cx)
669                        .project()
670                        .read(cx)
671                        .visible_worktrees(cx)
672                        .any(|worktree| worktree.read(cx).abs_path().as_ref() == worktree_path)
673            })
674    }
675
676    fn remove_worktree_from_window(
677        &mut self,
678        worktree_path: &Path,
679        window: &mut Window,
680        cx: &mut Context<Picker<Self>>,
681    ) {
682        if self.deleting_worktree_paths.contains(worktree_path) {
683            return;
684        }
685        let Some(workspace_to_remove) = self.workspace_for_open_worktree(worktree_path, window, cx)
686        else {
687            return;
688        };
689        let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
690            return;
691        };
692
693        cx.spawn_in(window, async move |picker, cx| {
694            let removed = window_handle
695                .update(cx, |multi_workspace, window, cx| {
696                    multi_workspace.close_workspace(&workspace_to_remove, window, cx)
697                })?
698                .await?;
699
700            if removed {
701                picker.update_in(cx, |picker, window, cx| {
702                    picker.delegate.refresh_project_worktree_paths(window, cx);
703                    picker.refresh(window, cx);
704                })?;
705            }
706
707            anyhow::Ok(())
708        })
709        .detach_and_log_err(cx);
710    }
711
712    fn sync_selected_index(&mut self, has_query: bool) {
713        if !has_query {
714            return;
715        }
716
717        if let Some(index) = self
718            .matches
719            .iter()
720            .position(|entry| matches!(entry, WorktreeEntry::Worktree { .. }))
721        {
722            self.selected_index = index;
723        } else if let Some(index) = self
724            .matches
725            .iter()
726            .position(|entry| matches!(entry, WorktreeEntry::CreateNamed { .. }))
727        {
728            self.selected_index = index;
729        } else {
730            self.selected_index = 0;
731        }
732    }
733}
734
735impl PickerDelegate for WorktreePickerDelegate {
736    type ListItem = AnyElement;
737
738    fn name() -> &'static str {
739        "worktree picker"
740    }
741
742    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
743        "Select or type to create a worktree…".into()
744    }
745
746    fn editor_position(&self) -> PickerEditorPosition {
747        PickerEditorPosition::Start
748    }
749
750    fn match_count(&self) -> usize {
751        self.matches.len()
752    }
753
754    fn selected_index(&self) -> usize {
755        self.selected_index
756    }
757
758    fn set_selected_index(
759        &mut self,
760        ix: usize,
761        _window: &mut Window,
762        _cx: &mut Context<Picker<Self>>,
763    ) {
764        self.selected_index = ix;
765    }
766
767    fn can_select(&self, ix: usize, _window: &mut Window, _cx: &mut Context<Picker<Self>>) -> bool {
768        !matches!(
769            self.matches.get(ix),
770            Some(WorktreeEntry::Separator | WorktreeEntry::SectionHeader(_))
771        )
772    }
773
774    fn update_matches(
775        &mut self,
776        query: String,
777        window: &mut Window,
778        cx: &mut Context<Picker<Self>>,
779    ) -> Task<()> {
780        let repo_worktrees = self.all_repo_worktrees().to_vec();
781
782        let normalized_query = query.replace(' ', "-");
783        let main_worktree_path = self
784            .all_worktrees
785            .iter()
786            .find(|wt| wt.is_main)
787            .map(|wt| wt.path.clone());
788        let has_named_worktree = self.all_worktrees.iter().any(|worktree| {
789            worktree.directory_name(main_worktree_path.as_deref()) == normalized_query
790        });
791        let create_named_disabled_reason: Option<String> = if self.has_multiple_repositories {
792            Some("Cannot create a named worktree in a project with multiple repositories".into())
793        } else if has_named_worktree {
794            Some("A worktree with this name already exists".into())
795        } else {
796            None
797        };
798
799        let show_default_branch_create =
800            !self.has_multiple_repositories && self.default_branch.is_some();
801        let default_branch = self.default_branch.clone();
802
803        if query.is_empty() {
804            let mut matches = self.build_fixed_entries();
805
806            if !repo_worktrees.is_empty() {
807                let main_worktree_path = repo_worktrees
808                    .iter()
809                    .find(|wt| wt.is_main)
810                    .map(|wt| wt.path.clone());
811
812                let project_paths = &self.project_worktree_paths;
813
814                let sort_by_name = |a: &GitWorktree, b: &GitWorktree| {
815                    a.directory_name(main_worktree_path.as_deref())
816                        .cmp(&b.directory_name(main_worktree_path.as_deref()))
817                };
818
819                let (mut open_here, mut others): (Vec<_>, Vec<_>) = repo_worktrees
820                    .into_iter()
821                    .partition(|worktree| project_paths.contains(&worktree.path));
822                open_here.sort_by(sort_by_name);
823                others.sort_by(sort_by_name);
824
825                matches.push(WorktreeEntry::Separator);
826
827                if open_here.len() > 1 {
828                    matches.push(WorktreeEntry::SectionHeader("This Window".into()));
829                    for worktree in open_here {
830                        matches.push(WorktreeEntry::Worktree {
831                            worktree,
832                            positions: Vec::new(),
833                        });
834                    }
835
836                    if !others.is_empty() {
837                        matches.push(WorktreeEntry::Separator);
838                    }
839
840                    for worktree in others {
841                        matches.push(WorktreeEntry::Worktree {
842                            worktree,
843                            positions: Vec::new(),
844                        });
845                    }
846                } else {
847                    for worktree in open_here.into_iter().chain(others) {
848                        matches.push(WorktreeEntry::Worktree {
849                            worktree,
850                            positions: Vec::new(),
851                        });
852                    }
853                }
854            }
855
856            self.matches = matches;
857            self.sync_selected_index(false);
858            return Task::ready(());
859        }
860
861        let main_worktree_path = repo_worktrees
862            .iter()
863            .find(|wt| wt.is_main)
864            .map(|wt| wt.path.clone());
865        let candidates: Vec<_> = repo_worktrees
866            .iter()
867            .enumerate()
868            .map(|(ix, worktree)| {
869                StringMatchCandidate::new(
870                    ix,
871                    &worktree.directory_name(main_worktree_path.as_deref()),
872                )
873            })
874            .collect();
875
876        let executor = cx.background_executor().clone();
877
878        let task = cx.background_executor().spawn(async move {
879            fuzzy::match_strings(
880                &candidates,
881                &query,
882                true,
883                true,
884                10000,
885                &Default::default(),
886                executor,
887            )
888            .await
889        });
890
891        let repo_worktrees_clone = repo_worktrees;
892        cx.spawn_in(window, async move |picker, cx| {
893            let fuzzy_matches = task.await;
894
895            picker
896                .update_in(cx, |picker, _window, cx| {
897                    let mut new_matches: Vec<WorktreeEntry> = Vec::new();
898
899                    for candidate in &fuzzy_matches {
900                        new_matches.push(WorktreeEntry::Worktree {
901                            worktree: repo_worktrees_clone[candidate.candidate_id].clone(),
902                            positions: candidate.positions.clone(),
903                        });
904                    }
905
906                    if !new_matches.is_empty() {
907                        new_matches.push(WorktreeEntry::Separator);
908                    }
909                    if show_default_branch_create {
910                        if let Some(ref default_branch) = default_branch {
911                            new_matches.push(WorktreeEntry::CreateNamed {
912                                name: normalized_query.clone(),
913                                from_branch: Some(default_branch.clone()),
914                                disabled_reason: create_named_disabled_reason.clone(),
915                            });
916                        }
917                    } else {
918                        new_matches.push(WorktreeEntry::CreateNamed {
919                            name: normalized_query.clone(),
920                            from_branch: None,
921                            disabled_reason: create_named_disabled_reason.clone(),
922                        });
923                    }
924
925                    picker.delegate.matches = new_matches;
926                    picker.delegate.sync_selected_index(true);
927
928                    cx.notify();
929                })
930                .log_err();
931        })
932    }
933
934    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
935        let Some(entry) = self.matches.get(self.selected_index) else {
936            return;
937        };
938
939        match entry {
940            WorktreeEntry::Separator | WorktreeEntry::SectionHeader(_) => return,
941            WorktreeEntry::CreateFromCurrentBranch => {
942                if self.creation_blocked_reason(cx).is_some() {
943                    return;
944                }
945                if let Some(workspace) = self.workspace.upgrade() {
946                    workspace.update(cx, |workspace, cx| {
947                        crate::worktree_service::handle_create_worktree(
948                            workspace,
949                            &CreateWorktree {
950                                worktree_name: None,
951                                branch_target: NewWorktreeBranchTarget::CurrentBranch,
952                            },
953                            window,
954                            self.focused_dock,
955                            cx,
956                        );
957                    });
958                }
959            }
960            WorktreeEntry::CreateFromDefaultBranch { default_branch } => {
961                if self.creation_blocked_reason(cx).is_some() {
962                    return;
963                }
964                if let Some(workspace) = self.workspace.upgrade() {
965                    workspace.update(cx, |workspace, cx| {
966                        crate::worktree_service::handle_create_worktree(
967                            workspace,
968                            &CreateWorktree {
969                                worktree_name: None,
970                                branch_target: NewWorktreeBranchTarget::RemoteBranch {
971                                    remote_name: default_branch.remote_name.clone(),
972                                    branch_name: default_branch.branch_name.clone(),
973                                },
974                            },
975                            window,
976                            self.focused_dock,
977                            cx,
978                        );
979                    });
980                }
981            }
982            WorktreeEntry::Worktree { worktree, .. } => {
983                if self.deleting_worktree_paths.contains(&worktree.path) {
984                    return;
985                }
986
987                let is_current = self.active_worktree_paths.contains(&worktree.path);
988
989                if !is_current {
990                    if secondary {
991                        window.dispatch_action(
992                            Box::new(OpenWorktreeInNewWindow {
993                                path: worktree.path.clone(),
994                            }),
995                            cx,
996                        );
997                    } else {
998                        let main_worktree_path = self
999                            .all_worktrees
1000                            .iter()
1001                            .find(|wt| wt.is_main)
1002                            .map(|wt| wt.path.as_path());
1003                        if let Some(workspace) = self.workspace.upgrade() {
1004                            workspace.update(cx, |workspace, cx| {
1005                                crate::worktree_service::handle_switch_worktree(
1006                                    workspace,
1007                                    &SwitchWorktree {
1008                                        path: worktree.path.clone(),
1009                                        display_name: worktree.directory_name(main_worktree_path),
1010                                    },
1011                                    window,
1012                                    self.focused_dock,
1013                                    cx,
1014                                );
1015                            });
1016                        }
1017                    }
1018                }
1019            }
1020            WorktreeEntry::CreateNamed {
1021                name,
1022                from_branch,
1023                disabled_reason: None,
1024            } => {
1025                let branch_target = match from_branch {
1026                    Some(branch) => NewWorktreeBranchTarget::RemoteBranch {
1027                        remote_name: branch.remote_name.clone(),
1028                        branch_name: branch.branch_name.clone(),
1029                    },
1030                    None => NewWorktreeBranchTarget::CurrentBranch,
1031                };
1032                if let Some(workspace) = self.workspace.upgrade() {
1033                    workspace.update(cx, |workspace, cx| {
1034                        crate::worktree_service::handle_create_worktree(
1035                            workspace,
1036                            &CreateWorktree {
1037                                worktree_name: Some(name.clone()),
1038                                branch_target,
1039                            },
1040                            window,
1041                            self.focused_dock,
1042                            cx,
1043                        );
1044                    });
1045                }
1046            }
1047            WorktreeEntry::CreateNamed {
1048                disabled_reason: Some(_),
1049                ..
1050            } => {
1051                return;
1052            }
1053        }
1054
1055        cx.emit(DismissEvent);
1056    }
1057
1058    fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
1059
1060    fn render_match(
1061        &self,
1062        ix: usize,
1063        selected: bool,
1064        _window: &mut Window,
1065        cx: &mut Context<Picker<Self>>,
1066    ) -> Option<Self::ListItem> {
1067        let entry = self.matches.get(ix)?;
1068
1069        match entry {
1070            WorktreeEntry::Separator => Some(
1071                div()
1072                    .py(DynamicSpacing::Base04.rems(cx))
1073                    .child(Divider::horizontal())
1074                    .into_any_element(),
1075            ),
1076            WorktreeEntry::SectionHeader(label) => Some(
1077                ListSubHeader::new(label.clone())
1078                    .inset(true)
1079                    .into_any_element(),
1080            ),
1081            WorktreeEntry::CreateFromCurrentBranch => {
1082                let branch_label = WorktreeCreateTarget::CurrentBranch.branch_label(
1083                    self.has_multiple_repositories,
1084                    self.current_branch_name.as_deref(),
1085                );
1086
1087                let label = format!("Create new worktree based on {branch_label}");
1088
1089                let item = create_new_list_item(
1090                    "create-from-current".to_string().into(),
1091                    label.into(),
1092                    self.creation_blocked_reason(cx),
1093                    selected,
1094                );
1095
1096                Some(item.into_any_element())
1097            }
1098            WorktreeEntry::CreateFromDefaultBranch { default_branch } => {
1099                let branch_label = WorktreeCreateTarget::DefaultBranch(default_branch.clone())
1100                    .branch_label(
1101                        self.has_multiple_repositories,
1102                        self.current_branch_name.as_deref(),
1103                    );
1104                let label = format!("Create new worktree based on {branch_label}");
1105
1106                let item = create_new_list_item(
1107                    "create-from-main".to_string().into(),
1108                    label.into(),
1109                    self.creation_blocked_reason(cx),
1110                    selected,
1111                );
1112
1113                Some(item.into_any_element())
1114            }
1115            WorktreeEntry::Worktree {
1116                worktree,
1117                positions,
1118            } => {
1119                let main_worktree_path = self
1120                    .all_worktrees
1121                    .iter()
1122                    .find(|wt| wt.is_main)
1123                    .map(|wt| wt.path.as_path());
1124                let display_name = worktree.directory_name(main_worktree_path);
1125                let first_line = display_name.lines().next().unwrap_or(&display_name);
1126                let positions: Vec<_> = positions
1127                    .iter()
1128                    .copied()
1129                    .filter(|&pos| pos < first_line.len())
1130                    .collect();
1131                let path = worktree.path.compact().to_string_lossy().to_string();
1132                let sha = worktree.sha.chars().take(7).collect::<String>();
1133
1134                let is_current = self.active_worktree_paths.contains(&worktree.path);
1135                let is_deleting = self.deleting_worktree_paths.contains(&worktree.path);
1136                let can_delete = self.can_delete_worktree(worktree);
1137                let can_remove_from_window =
1138                    !is_current && self.project_worktree_paths.contains(&worktree.path);
1139
1140                let entry_icon = if is_current {
1141                    IconName::Check
1142                } else {
1143                    IconName::GitWorktree
1144                };
1145                let picker = cx.entity();
1146
1147                Some(
1148                    ListItem::new(SharedString::from(format!("worktree-{ix}")))
1149                        .inset(true)
1150                        .spacing(ListItemSpacing::Sparse)
1151                        .toggle_state(selected)
1152                        .child(
1153                            h_flex()
1154                                .w_full()
1155                                .gap_2p5()
1156                                .child(
1157                                    Icon::new(entry_icon)
1158                                        .color(if is_current {
1159                                            Color::Accent
1160                                        } else {
1161                                            Color::Muted
1162                                        })
1163                                        .size(IconSize::Small),
1164                                )
1165                                .child(
1166                                    v_flex()
1167                                        .w_full()
1168                                        .min_w_0()
1169                                        .child(
1170                                            HighlightedLabel::new(first_line.to_owned(), positions)
1171                                                .truncate(),
1172                                        )
1173                                        .child(
1174                                            h_flex()
1175                                                .w_full()
1176                                                .min_w_0()
1177                                                .gap_1p5()
1178                                                .when_some(
1179                                                    worktree.branch_name().map(|b| b.to_string()),
1180                                                    |this, branch| {
1181                                                        this.child(
1182                                                            Label::new(branch)
1183                                                                .size(LabelSize::Small)
1184                                                                .color(Color::Muted),
1185                                                        )
1186                                                        .child(
1187                                                            Label::new("\u{2022}")
1188                                                                .alpha(0.5)
1189                                                                .color(Color::Muted)
1190                                                                .size(LabelSize::Small),
1191                                                        )
1192                                                    },
1193                                                )
1194                                                .when(!sha.is_empty(), |this| {
1195                                                    this.child(
1196                                                        Label::new(sha)
1197                                                            .size(LabelSize::Small)
1198                                                            .color(Color::Muted),
1199                                                    )
1200                                                    .child(
1201                                                        Label::new("\u{2022}")
1202                                                            .alpha(0.5)
1203                                                            .color(Color::Muted)
1204                                                            .size(LabelSize::Small),
1205                                                    )
1206                                                })
1207                                                .child(
1208                                                    Label::new(path)
1209                                                        .truncate_start()
1210                                                        .color(Color::Muted)
1211                                                        .size(LabelSize::Small)
1212                                                        .flex_1(),
1213                                                ),
1214                                        ),
1215                                ),
1216                        )
1217                        .when(is_deleting, |this| {
1218                            this.end_slot(
1219                                h_flex()
1220                                    .gap_1()
1221                                    .child(
1222                                        Icon::new(IconName::LoadCircle)
1223                                            .size(IconSize::Small)
1224                                            .color(Color::Muted)
1225                                            .with_rotate_animation(2),
1226                                    )
1227                                    .child(
1228                                        Label::new("Deleting…")
1229                                            .size(LabelSize::Small)
1230                                            .color(Color::Muted),
1231                                    ),
1232                            )
1233                        })
1234                        .when(!is_deleting && !is_current, |this| {
1235                            let open_in_new_window_button =
1236                                IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
1237                                    .icon_size(IconSize::Small)
1238                                    .tooltip(Tooltip::text("Open in New Window"))
1239                                    .on_click(cx.listener(move |picker, _, window, cx| {
1240                                        let Some(entry) = picker.delegate.matches.get(ix) else {
1241                                            return;
1242                                        };
1243                                        if let WorktreeEntry::Worktree { worktree, .. } = entry {
1244                                            if picker
1245                                                .delegate
1246                                                .deleting_worktree_paths
1247                                                .contains(&worktree.path)
1248                                            {
1249                                                return;
1250                                            }
1251                                            window.dispatch_action(
1252                                                Box::new(OpenWorktreeInNewWindow {
1253                                                    path: worktree.path.clone(),
1254                                                }),
1255                                                cx,
1256                                            );
1257                                            cx.emit(DismissEvent);
1258                                        }
1259                                    }));
1260
1261                            let focus_handle_delete = self.focus_handle.clone();
1262                            let force_delete = self.is_force_delete_hovering_index(ix);
1263                            let delete_button = div()
1264                                .id(("delete-worktree-hover", ix))
1265                                .on_hover(cx.listener(move |picker, hovered: &bool, _, cx| {
1266                                    if *hovered {
1267                                        picker.delegate.hovered_delete_index = Some(ix);
1268                                    } else if picker.delegate.hovered_delete_index == Some(ix) {
1269                                        picker.delegate.hovered_delete_index = None;
1270                                    }
1271                                    cx.notify();
1272                                }))
1273                                .child(
1274                                    IconButton::new(("delete-worktree", ix), IconName::Trash)
1275                                        .icon_size(IconSize::Small)
1276                                        .when(force_delete, |this| this.icon_color(Color::Error))
1277                                        .tooltip(move |_, cx| {
1278                                            cx.new(|cx| {
1279                                                DeleteWorktreeTooltip::new(
1280                                                    picker.clone(),
1281                                                    focus_handle_delete.clone(),
1282                                                    ix,
1283                                                    cx,
1284                                                )
1285                                            })
1286                                            .into()
1287                                        })
1288                                        .on_click(cx.listener(move |picker, _, window, cx| {
1289                                            let force = picker.delegate.modifiers.alt;
1290                                            picker.delegate.delete_worktree(ix, force, window, cx);
1291                                        })),
1292                                );
1293
1294                            this.end_slot(
1295                                h_flex()
1296                                    .gap_0p5()
1297                                    .child(open_in_new_window_button)
1298                                    .when(can_remove_from_window, |this| {
1299                                        let worktree_path = worktree.path.clone();
1300                                        this.child(
1301                                            IconButton::new(
1302                                                ("remove-worktree-from-window", ix),
1303                                                IconName::Close,
1304                                            )
1305                                            .icon_size(IconSize::Small)
1306                                            .tooltip(Tooltip::text("Remove Worktree from Window"))
1307                                            .on_click(
1308                                                cx.listener(move |picker, _, window, cx| {
1309                                                    picker.delegate.remove_worktree_from_window(
1310                                                        &worktree_path,
1311                                                        window,
1312                                                        cx,
1313                                                    );
1314                                                }),
1315                                            ),
1316                                        )
1317                                    })
1318                                    .when(can_delete, |this| this.child(delete_button)),
1319                            )
1320                            .show_end_slot_on_hover()
1321                        })
1322                        .into_any_element(),
1323                )
1324            }
1325            WorktreeEntry::CreateNamed {
1326                name,
1327                from_branch,
1328                disabled_reason,
1329            } => {
1330                let branch_label = from_branch
1331                    .as_ref()
1332                    .map(RemoteBranchName::display_name)
1333                    .unwrap_or_else(|| {
1334                        self.current_branch_name
1335                            .clone()
1336                            .unwrap_or_else(|| "HEAD".to_string())
1337                    });
1338                let label = format!("Create \"{name}\" based on {branch_label}");
1339                let element_id = match from_branch {
1340                    Some(branch) => format!("create-named-from-{}", branch.display_name()),
1341                    None => "create-named-from-current".to_string(),
1342                };
1343
1344                let item = create_new_list_item(
1345                    element_id.into(),
1346                    label.into(),
1347                    disabled_reason.clone().map(SharedString::from),
1348                    selected,
1349                );
1350
1351                Some(item.into_any_element())
1352            }
1353        }
1354    }
1355
1356    fn searchbar_trailer(
1357        &self,
1358        _window: &mut Window,
1359        _cx: &mut Context<Picker<Self>>,
1360    ) -> Option<AnyElement> {
1361        if self.show_footer {
1362            return None;
1363        }
1364
1365        let focus_handle = self.focus_handle.clone();
1366
1367        Some(
1368            IconButton::new("configure-worktree-tasks", IconName::Settings)
1369                .icon_size(IconSize::Small)
1370                .tooltip(move |_window, cx| {
1371                    Tooltip::for_action_in(
1372                        "Automate Worktree Setup",
1373                        &OpenWorktreeSetupTasks,
1374                        &focus_handle,
1375                        cx,
1376                    )
1377                })
1378                .on_click(|_, window, cx| {
1379                    window.dispatch_action(OpenWorktreeSetupTasks.boxed_clone(), cx)
1380                })
1381                .into_any_element(),
1382        )
1383    }
1384
1385    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
1386        if !self.show_footer {
1387            return None;
1388        }
1389
1390        let focus_handle = self.focus_handle.clone();
1391        let selected_entry = self.matches.get(self.selected_index);
1392
1393        let is_creating = selected_entry.is_some_and(|e| {
1394            matches!(
1395                e,
1396                WorktreeEntry::CreateFromCurrentBranch
1397                    | WorktreeEntry::CreateFromDefaultBranch { .. }
1398                    | WorktreeEntry::CreateNamed { .. }
1399            )
1400        });
1401
1402        let is_existing_worktree =
1403            selected_entry.is_some_and(|e| matches!(e, WorktreeEntry::Worktree { .. }));
1404
1405        let can_delete = selected_entry.is_some_and(|e| {
1406            matches!(e, WorktreeEntry::Worktree { worktree, .. } if self.can_delete_worktree(worktree))
1407        });
1408
1409        let is_current = selected_entry.is_some_and(|e| {
1410            matches!(e, WorktreeEntry::Worktree { worktree, .. } if self.project_worktree_paths.contains(&worktree.path))
1411        });
1412
1413        let is_deleting = selected_entry.is_some_and(|e| {
1414            matches!(e, WorktreeEntry::Worktree { worktree, .. } if self.deleting_worktree_paths.contains(&worktree.path))
1415        });
1416
1417        let footer = h_flex()
1418            .w_full()
1419            .p_1p5()
1420            .gap_0p5()
1421            .justify_between()
1422            .border_t_1()
1423            .border_color(cx.theme().colors().border_variant)
1424            .child(
1425                Button::new("configure-worktree-tasks", "Automate Setup")
1426                    .key_binding(
1427                        KeyBinding::for_action_in(&OpenWorktreeSetupTasks, &focus_handle, cx)
1428                            .map(|kb| kb.size(rems_from_px(12.))),
1429                    )
1430                    .on_click(|_, window, cx| {
1431                        window.dispatch_action(OpenWorktreeSetupTasks.boxed_clone(), cx)
1432                    }),
1433            );
1434
1435        if is_creating {
1436            Some(
1437                footer
1438                    .child(
1439                        Button::new("create-worktree", "Create")
1440                            .key_binding(
1441                                KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
1442                                    .map(|kb| kb.size(rems_from_px(12.))),
1443                            )
1444                            .on_click(|_, window, cx| {
1445                                window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1446                            }),
1447                    )
1448                    .into_any(),
1449            )
1450        } else if is_existing_worktree {
1451            Some(
1452                footer
1453                    .child(
1454                        h_flex()
1455                            .gap_0p5()
1456                            .when(is_deleting, |this| {
1457                                this.child(
1458                                    Button::new("delete-worktree", "Deleting…")
1459                                        .loading(true)
1460                                        .disabled(true),
1461                                )
1462                            })
1463                            .when(!is_deleting && can_delete, |this| {
1464                                let focus_handle = focus_handle.clone();
1465                                this.child(
1466                                    Button::new("delete-worktree", "Delete")
1467                                        .key_binding(
1468                                            KeyBinding::for_action_in(
1469                                                &DeleteWorktree,
1470                                                &focus_handle,
1471                                                cx,
1472                                            )
1473                                            .map(|kb| kb.size(rems_from_px(12.))),
1474                                        )
1475                                        .on_click(|_, window, cx| {
1476                                            window.dispatch_action(DeleteWorktree.boxed_clone(), cx)
1477                                        }),
1478                                )
1479                            })
1480                            .when(!is_deleting && !is_current, |this| {
1481                                let focus_handle = focus_handle.clone();
1482                                this.child(
1483                                    Button::new("open-in-new-window", "Open in New Window")
1484                                        .key_binding(
1485                                            KeyBinding::for_action_in(
1486                                                &menu::SecondaryConfirm,
1487                                                &focus_handle,
1488                                                cx,
1489                                            )
1490                                            .map(|kb| kb.size(rems_from_px(12.))),
1491                                        )
1492                                        .on_click(|_, window, cx| {
1493                                            window.dispatch_action(
1494                                                menu::SecondaryConfirm.boxed_clone(),
1495                                                cx,
1496                                            )
1497                                        }),
1498                                )
1499                            })
1500                            .when(!is_deleting, |this| {
1501                                this.child(
1502                                    Button::new("open-worktree", "Open")
1503                                        .key_binding(
1504                                            KeyBinding::for_action_in(
1505                                                &menu::Confirm,
1506                                                &focus_handle,
1507                                                cx,
1508                                            )
1509                                            .map(|kb| kb.size(rems_from_px(12.))),
1510                                        )
1511                                        .on_click(|_, window, cx| {
1512                                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1513                                        }),
1514                                )
1515                            }),
1516                    )
1517                    .into_any(),
1518            )
1519        } else {
1520            None
1521        }
1522    }
1523}
1524
1525fn create_new_list_item(
1526    id: SharedString,
1527    label: SharedString,
1528    disabled_tooltip: Option<SharedString>,
1529    selected: bool,
1530) -> AnyElement {
1531    let is_disabled = disabled_tooltip.is_some();
1532
1533    ListItem::new(id)
1534        .inset(true)
1535        .spacing(ListItemSpacing::Sparse)
1536        .toggle_state(selected)
1537        .child(
1538            h_flex()
1539                .w_full()
1540                .gap_2p5()
1541                .child(
1542                    Icon::new(IconName::Plus)
1543                        .map(|this| {
1544                            if is_disabled {
1545                                this.color(Color::Disabled)
1546                            } else {
1547                                this.color(Color::Muted)
1548                            }
1549                        })
1550                        .size(IconSize::Small),
1551                )
1552                .child(Label::new(label).when(is_disabled, |this| this.color(Color::Disabled))),
1553        )
1554        .when_some(disabled_tooltip, |this, reason| {
1555            this.tooltip(Tooltip::text(reason))
1556        })
1557        .into_any_element()
1558}
1559
1560pub async fn open_remote_worktree(
1561    connection_options: remote::RemoteConnectionOptions,
1562    paths: Vec<PathBuf>,
1563    app_state: Arc<workspace::AppState>,
1564    workspace: gpui::WeakEntity<Workspace>,
1565    cx: &mut gpui::AsyncWindowContext,
1566) -> anyhow::Result<()> {
1567    let connect_task = workspace.update_in(cx, |workspace, window, cx| {
1568        workspace.toggle_modal(window, cx, |window, cx| {
1569            remote_connection::RemoteConnectionModal::new(
1570                &connection_options,
1571                Vec::new(),
1572                window,
1573                cx,
1574            )
1575        });
1576
1577        let prompt = workspace
1578            .active_modal::<remote_connection::RemoteConnectionModal>(cx)
1579            .expect("Modal just created")
1580            .read(cx)
1581            .prompt
1582            .clone();
1583
1584        remote_connection::connect(
1585            remote::remote_client::ConnectionIdentifier::setup(),
1586            connection_options.clone(),
1587            prompt,
1588            window,
1589            cx,
1590        )
1591        .prompt_err("Failed to connect", window, cx, |_, _, _| None)
1592    })?;
1593
1594    let session = connect_task.await;
1595
1596    workspace
1597        .update_in(cx, |workspace, _window, cx| {
1598            if let Some(prompt) =
1599                workspace.active_modal::<remote_connection::RemoteConnectionModal>(cx)
1600            {
1601                prompt.update(cx, |prompt, cx| prompt.finished(cx))
1602            }
1603        })
1604        .ok();
1605
1606    let Some(Some(session)) = session else {
1607        return Ok(());
1608    };
1609
1610    let new_project = cx.update(|_, cx| {
1611        project::Project::remote(
1612            session,
1613            app_state.client.clone(),
1614            app_state.node_runtime.clone(),
1615            app_state.user_store.clone(),
1616            app_state.languages.clone(),
1617            app_state.fs.clone(),
1618            true,
1619            cx,
1620        )
1621    })?;
1622
1623    let workspace_position = cx
1624        .update(|_, cx| {
1625            workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
1626        })?
1627        .await
1628        .context("fetching workspace position from db")?;
1629
1630    let mut options =
1631        cx.update(|_, cx| (app_state.build_window_options)(workspace_position.display, cx))?;
1632    options.window_bounds = workspace_position.window_bounds;
1633
1634    let new_window = cx.open_window(options, |window, cx| {
1635        let workspace = cx.new(|cx| {
1636            let mut workspace =
1637                Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
1638            workspace.centered_layout = workspace_position.centered_layout;
1639            workspace
1640        });
1641        cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
1642    })?;
1643
1644    workspace::open_remote_project_with_existing_connection(
1645        connection_options,
1646        new_project,
1647        paths,
1648        app_state,
1649        new_window,
1650        None,
1651        None,
1652        cx,
1653    )
1654    .await?;
1655
1656    Ok(())
1657}
1658
1659#[cfg(test)]
1660mod tests {
1661    use super::*;
1662    use fs::FakeFs;
1663    use gpui::{AppContext, TestAppContext, VisualTestContext};
1664    use project::project_settings::ProjectSettings;
1665    use project::{Project, WorktreeSettings};
1666    use serde_json::json;
1667    use settings::Settings as _;
1668    use settings::SettingsStore;
1669    use util::path;
1670    use workspace::MultiWorkspace;
1671
1672    fn init_test(cx: &mut TestAppContext) {
1673        cx.update(|cx| {
1674            let settings_store = SettingsStore::test(cx);
1675            cx.set_global(settings_store);
1676            theme_settings::init(theme::LoadThemes::JustBase, cx);
1677            editor::init(cx);
1678            ProjectSettings::register(cx);
1679            WorktreeSettings::register(cx);
1680        });
1681    }
1682
1683    async fn init_worktree_picker_test(
1684        cx: &mut TestAppContext,
1685    ) -> (
1686        Arc<FakeFs>,
1687        Entity<WorktreePicker>,
1688        Entity<project::git_store::Repository>,
1689        PathBuf,
1690        VisualTestContext,
1691    ) {
1692        init_test(cx);
1693
1694        let fs = FakeFs::new(cx.executor());
1695        fs.insert_tree(
1696            path!("/root"),
1697            json!({
1698                "project": {
1699                    ".git": {},
1700                    "file.txt": "buffer_text",
1701                },
1702                "worktrees": {},
1703            }),
1704        )
1705        .await;
1706        fs.set_head_for_repo(
1707            path!("/root/project/.git").as_ref(),
1708            &[("file.txt", "buffer_text".to_string())],
1709            "deadbeef",
1710        );
1711
1712        let project = Project::test(fs.clone(), [path!("/root/project").as_ref()], cx).await;
1713        cx.executor().run_until_parked();
1714
1715        let repository = project.read_with(cx, |project, cx| {
1716            project.repositories(cx).values().next().unwrap().clone()
1717        });
1718        let worktree_path = PathBuf::from(path!("/root/worktrees/dirty-wt"));
1719
1720        cx.update(|cx| {
1721            repository.update(cx, |repository, _| {
1722                repository.create_worktree(
1723                    git::repository::CreateWorktreeTarget::NewBranch {
1724                        branch_name: "dirty-wt".to_string(),
1725                        base_sha: Some("deadbeef".to_string()),
1726                    },
1727                    worktree_path.clone(),
1728                )
1729            })
1730        })
1731        .await
1732        .unwrap()
1733        .unwrap();
1734
1735        let window_handle =
1736            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1737        let workspace = window_handle
1738            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
1739            .unwrap();
1740
1741        let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
1742
1743        let worktree_picker = cx.update(|window, cx| {
1744            cx.new(|cx| WorktreePicker::new(project, workspace.downgrade(), window, cx))
1745        });
1746
1747        cx.run_until_parked();
1748
1749        (fs, worktree_picker, repository, worktree_path, cx)
1750    }
1751
1752    fn worktree_index(
1753        worktree_picker: &Entity<WorktreePicker>,
1754        worktree_path: &Path,
1755        cx: &mut VisualTestContext,
1756    ) -> usize {
1757        worktree_picker.update(cx, |worktree_picker, cx| {
1758            worktree_picker.picker.update(cx, |picker, _| {
1759                picker
1760                    .delegate
1761                    .matches
1762                    .iter()
1763                    .position(|entry| {
1764                        matches!(entry, WorktreeEntry::Worktree { worktree, .. } if worktree.path == *worktree_path)
1765                    })
1766                    .expect("worktree should appear in picker")
1767            })
1768        })
1769    }
1770
1771    fn picker_contains_worktree(
1772        worktree_picker: &Entity<WorktreePicker>,
1773        worktree_path: &Path,
1774        cx: &mut VisualTestContext,
1775    ) -> bool {
1776        worktree_picker.update(cx, |worktree_picker, cx| {
1777            worktree_picker.picker.update(cx, |picker, _| {
1778                picker.delegate.all_worktrees.iter().any(|worktree| {
1779                    worktree.path == *worktree_path
1780                }) && picker.delegate.matches.iter().any(|entry| {
1781                    matches!(entry, WorktreeEntry::Worktree { worktree, .. } if worktree.path == *worktree_path)
1782                })
1783            })
1784        })
1785    }
1786
1787    fn deleting_worktree_paths(
1788        worktree_picker: &Entity<WorktreePicker>,
1789        cx: &mut VisualTestContext,
1790    ) -> HashSet<PathBuf> {
1791        worktree_picker.update(cx, |worktree_picker, cx| {
1792            worktree_picker.picker.update(cx, |picker, _| {
1793                picker.delegate.deleting_worktree_paths.clone()
1794            })
1795        })
1796    }
1797
1798    async fn repo_contains_worktree(
1799        repository: &Entity<project::git_store::Repository>,
1800        worktree_path: &Path,
1801        cx: &mut VisualTestContext,
1802    ) -> bool {
1803        let worktrees = repository
1804            .update(cx, |repository, _| repository.worktrees())
1805            .await
1806            .unwrap()
1807            .unwrap();
1808        worktrees
1809            .iter()
1810            .any(|worktree| worktree.path == *worktree_path)
1811    }
1812
1813    #[gpui::test]
1814    async fn test_delete_worktree_marks_row_pending_immediately(cx: &mut TestAppContext) {
1815        let (_, worktree_picker, _repository, worktree_path, mut cx) =
1816            init_worktree_picker_test(cx).await;
1817
1818        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
1819        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
1820            worktree_picker.picker.update(cx, |picker, cx| {
1821                picker.delegate.delete_worktree(index, false, window, cx);
1822            })
1823        });
1824
1825        let pending_paths = deleting_worktree_paths(&worktree_picker, &mut cx);
1826        assert_eq!(pending_paths.len(), 1);
1827        assert!(pending_paths.contains(&worktree_path));
1828
1829        cx.run_until_parked();
1830    }
1831
1832    #[gpui::test]
1833    async fn test_delete_worktree_clears_pending_and_removes_row_on_success(
1834        cx: &mut TestAppContext,
1835    ) {
1836        let (_, worktree_picker, repository, worktree_path, mut cx) =
1837            init_worktree_picker_test(cx).await;
1838
1839        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
1840        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
1841            worktree_picker.picker.update(cx, |picker, cx| {
1842                picker.delegate.delete_worktree(index, false, window, cx);
1843            })
1844        });
1845        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));
1846
1847        cx.run_until_parked();
1848
1849        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());
1850        assert!(!picker_contains_worktree(
1851            &worktree_picker,
1852            &worktree_path,
1853            &mut cx
1854        ));
1855        assert!(
1856            !repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
1857            "worktree should be removed after successful delete"
1858        );
1859    }
1860
1861    #[gpui::test]
1862    async fn test_remote_default_branch_is_preferred_create_target(cx: &mut TestAppContext) {
1863        let (_fs, worktree_picker, _repository, _worktree_path, mut cx) =
1864            init_worktree_picker_test(cx).await;
1865
1866        worktree_picker.update(&mut cx, |worktree_picker, cx| {
1867            worktree_picker.picker.update(cx, |picker, _| {
1868                assert_eq!(picker.delegate.selected_index, 0);
1869                match picker.delegate.matches.first() {
1870                    Some(WorktreeEntry::CreateFromDefaultBranch { default_branch }) => {
1871                        assert_eq!(default_branch.display_name(), "origin/main");
1872                    }
1873                    _ => panic!("remote default branch should be the first create target"),
1874                }
1875            })
1876        });
1877
1878        let update_matches = worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
1879            worktree_picker.picker.update(cx, |picker, cx| {
1880                picker
1881                    .delegate
1882                    .update_matches("feature".to_string(), window, cx)
1883            })
1884        });
1885        update_matches.await;
1886        cx.run_until_parked();
1887
1888        worktree_picker.update(&mut cx, |worktree_picker, cx| {
1889            worktree_picker
1890                .picker
1891                .update(cx, |picker, _| match picker.delegate.matches.first() {
1892                    Some(WorktreeEntry::CreateNamed {
1893                        from_branch: Some(default_branch),
1894                        ..
1895                    }) => {
1896                        assert_eq!(default_branch.display_name(), "origin/main");
1897                    }
1898                    _ => panic!("named worktree creation should prefer the remote default branch"),
1899                })
1900        });
1901    }
1902
1903    #[gpui::test]
1904    async fn test_current_branch_create_target_is_shown_without_default_branch(
1905        cx: &mut TestAppContext,
1906    ) {
1907        let (_fs, worktree_picker, _repository, _worktree_path, mut cx) =
1908            init_worktree_picker_test(cx).await;
1909
1910        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
1911            worktree_picker.picker.update(cx, |picker, cx| {
1912                picker.delegate.default_branch = None;
1913                picker.refresh(window, cx);
1914            });
1915        });
1916        cx.run_until_parked();
1917
1918        worktree_picker.update(&mut cx, |worktree_picker, cx| {
1919            worktree_picker.picker.update(cx, |picker, _| {
1920                assert!(matches!(
1921                    picker.delegate.matches.first(),
1922                    Some(WorktreeEntry::CreateFromCurrentBranch)
1923                ));
1924                assert!(
1925                    !picker.delegate.matches.iter().any(|entry| matches!(
1926                        entry,
1927                        WorktreeEntry::CreateFromDefaultBranch { .. }
1928                    ))
1929                );
1930            });
1931        });
1932    }
1933
1934    #[gpui::test]
1935    async fn test_delete_dirty_worktree_prompts_for_force_delete(cx: &mut TestAppContext) {
1936        let (fs, worktree_picker, repository, worktree_path, mut cx) =
1937            init_worktree_picker_test(cx).await;
1938
1939        fs.with_git_state(path!("/root/project/.git").as_ref(), true, |state| {
1940            state
1941                .worktrees_requiring_force_delete
1942                .insert(worktree_path.clone());
1943        })
1944        .expect("failed to mark test worktree as requiring force delete");
1945
1946        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
1947        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
1948            worktree_picker.picker.update(cx, |picker, cx| {
1949                picker.delegate.delete_worktree(index, false, window, cx);
1950            })
1951        });
1952        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));
1953
1954        cx.run_until_parked();
1955        assert!(cx.has_pending_prompt());
1956        assert!(
1957            !deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path),
1958            "pending delete state should clear while waiting for force-delete confirmation"
1959        );
1960
1961        cx.simulate_prompt_answer("Force Delete");
1962        cx.run_until_parked();
1963
1964        assert!(!cx.has_pending_prompt());
1965        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());
1966        assert!(!picker_contains_worktree(
1967            &worktree_picker,
1968            &worktree_path,
1969            &mut cx
1970        ));
1971        assert!(
1972            !repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
1973            "worktree should be removed after confirming force delete"
1974        );
1975    }
1976
1977    #[gpui::test]
1978    async fn test_duplicate_delete_worktree_is_ignored_while_pending(cx: &mut TestAppContext) {
1979        let (fs, worktree_picker, _repository, worktree_path, mut cx) =
1980            init_worktree_picker_test(cx).await;
1981
1982        fs.with_git_state(path!("/root/project/.git").as_ref(), true, |state| {
1983            state
1984                .worktrees_requiring_force_delete
1985                .insert(worktree_path.clone());
1986        })
1987        .expect("failed to mark test worktree as requiring force delete");
1988
1989        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
1990        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
1991            worktree_picker.picker.update(cx, |picker, cx| {
1992                picker.delegate.delete_worktree(index, false, window, cx);
1993                picker.delegate.delete_worktree(index, false, window, cx);
1994            })
1995        });
1996
1997        let pending_paths = deleting_worktree_paths(&worktree_picker, &mut cx);
1998        assert_eq!(pending_paths.len(), 1);
1999        assert!(pending_paths.contains(&worktree_path));
2000
2001        cx.run_until_parked();
2002        assert!(cx.has_pending_prompt());
2003        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());
2004
2005        cx.simulate_prompt_answer("Cancel");
2006        cx.run_until_parked();
2007
2008        assert!(!cx.has_pending_prompt());
2009        assert!(picker_contains_worktree(
2010            &worktree_picker,
2011            &worktree_path,
2012            &mut cx
2013        ));
2014    }
2015
2016    #[gpui::test]
2017    async fn test_selected_deleting_worktree_cannot_be_opened(cx: &mut TestAppContext) {
2018        let (_, worktree_picker, _repository, worktree_path, mut cx) =
2019            init_worktree_picker_test(cx).await;
2020
2021        let subscription = cx.update(|_, cx| {
2022            cx.subscribe(&worktree_picker, |_, _: &DismissEvent, _| {
2023                panic!("DismissEvent should not be emitted for a deleting worktree");
2024            })
2025        });
2026
2027        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
2028        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
2029            worktree_picker.picker.update(cx, |picker, cx| {
2030                picker.delegate.selected_index = index;
2031                picker.delegate.delete_worktree(index, false, window, cx);
2032                picker.delegate.confirm(false, window, cx);
2033            })
2034        });
2035
2036        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));
2037
2038        drop(subscription);
2039        cx.run_until_parked();
2040    }
2041
2042    #[gpui::test]
2043    async fn test_force_delete_worktree_deletes_without_prompt(cx: &mut TestAppContext) {
2044        let (fs, worktree_picker, repository, worktree_path, mut cx) =
2045            init_worktree_picker_test(cx).await;
2046
2047        fs.with_git_state(path!("/root/project/.git").as_ref(), true, |state| {
2048            state
2049                .worktrees_requiring_force_delete
2050                .insert(worktree_path.clone());
2051        })
2052        .expect("failed to mark test worktree as requiring force delete");
2053
2054        let index = worktree_index(&worktree_picker, &worktree_path, &mut cx);
2055        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
2056            worktree_picker.picker.update(cx, |picker, cx| {
2057                picker.delegate.modifiers = Modifiers::alt();
2058                picker.delegate.delete_worktree(index, true, window, cx);
2059            })
2060        });
2061        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).contains(&worktree_path));
2062
2063        cx.run_until_parked();
2064
2065        assert!(!cx.has_pending_prompt());
2066        assert!(deleting_worktree_paths(&worktree_picker, &mut cx).is_empty());
2067        assert!(!picker_contains_worktree(
2068            &worktree_picker,
2069            &worktree_path,
2070            &mut cx
2071        ));
2072        assert!(
2073            !repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
2074            "worktree should be removed by explicit force delete"
2075        );
2076    }
2077
2078    #[gpui::test]
2079    async fn test_open_worktrees_are_grouped_under_section_header(cx: &mut TestAppContext) {
2080        init_test(cx);
2081
2082        let fs = FakeFs::new(cx.executor());
2083        fs.insert_tree(
2084            path!("/root"),
2085            json!({
2086                "project": {
2087                    ".git": {},
2088                    "file.txt": "buffer_text",
2089                },
2090                "worktrees": {},
2091            }),
2092        )
2093        .await;
2094        fs.set_head_for_repo(
2095            path!("/root/project/.git").as_ref(),
2096            &[("file.txt", "buffer_text".to_string())],
2097            "deadbeef",
2098        );
2099
2100        let project = Project::test(fs.clone(), [path!("/root/project").as_ref()], cx).await;
2101        cx.executor().run_until_parked();
2102
2103        let repository = project.read_with(cx, |project, cx| {
2104            project.repositories(cx).values().next().unwrap().clone()
2105        });
2106        let second_worktree_path = PathBuf::from(path!("/root/worktrees/second-wt"));
2107
2108        cx.update(|cx| {
2109            repository.update(cx, |repository, _| {
2110                repository.create_worktree(
2111                    git::repository::CreateWorktreeTarget::NewBranch {
2112                        branch_name: "second-wt".to_string(),
2113                        base_sha: Some("deadbeef".to_string()),
2114                    },
2115                    second_worktree_path.clone(),
2116                )
2117            })
2118        })
2119        .await
2120        .unwrap()
2121        .unwrap();
2122
2123        // Open the second worktree as a visible worktree of the active project so
2124        // that two worktrees of the same repo are open in this window.
2125        project
2126            .update(cx, |project, cx| {
2127                project.create_worktree(&second_worktree_path, true, cx)
2128            })
2129            .await
2130            .unwrap();
2131        cx.executor().run_until_parked();
2132
2133        let window_handle =
2134            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2135        let workspace = window_handle
2136            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
2137            .unwrap();
2138
2139        let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
2140        let worktree_picker = cx.update(|window, cx| {
2141            cx.new(|cx| WorktreePicker::new(project, workspace.downgrade(), window, cx))
2142        });
2143        cx.run_until_parked();
2144
2145        let project_path = PathBuf::from(path!("/root/project"));
2146        worktree_picker.update(&mut cx, |worktree_picker, cx| {
2147            worktree_picker.picker.update(cx, |picker, _| {
2148                let matches = &picker.delegate.matches;
2149
2150                let header_index = matches
2151                    .iter()
2152                    .position(|entry| {
2153                        matches!(entry, WorktreeEntry::SectionHeader(label) if label.as_ref() == "This Window")
2154                    })
2155                    .expect("section header should be present when multiple worktrees are open");
2156
2157                let grouped_paths: Vec<&Path> = matches[header_index + 1..]
2158                    .iter()
2159                    .map_while(|entry| match entry {
2160                        WorktreeEntry::Worktree { worktree, .. } => Some(worktree.path.as_path()),
2161                        _ => None,
2162                    })
2163                    .collect();
2164
2165                assert!(
2166                    grouped_paths.contains(&project_path.as_path()),
2167                    "main worktree should be grouped under the header"
2168                );
2169                assert!(
2170                    grouped_paths.contains(&second_worktree_path.as_path()),
2171                    "second open worktree should be grouped under the header"
2172                );
2173            })
2174        });
2175    }
2176
2177    #[gpui::test]
2178    async fn test_remove_open_worktree_workspace_from_window(cx: &mut TestAppContext) {
2179        init_test(cx);
2180
2181        let fs = FakeFs::new(cx.executor());
2182        fs.insert_tree(
2183            path!("/root"),
2184            json!({
2185                "project": {
2186                    ".git": {},
2187                    "file.txt": "buffer_text",
2188                },
2189                "worktrees": {},
2190            }),
2191        )
2192        .await;
2193        fs.set_head_for_repo(
2194            path!("/root/project/.git").as_ref(),
2195            &[("file.txt", "buffer_text".to_string())],
2196            "deadbeef",
2197        );
2198
2199        let project = Project::test(fs.clone(), [path!("/root/project").as_ref()], cx).await;
2200        cx.executor().run_until_parked();
2201
2202        let repository = project.read_with(cx, |project, cx| {
2203            project.repositories(cx).values().next().unwrap().clone()
2204        });
2205        let worktree_path = PathBuf::from(path!("/root/worktrees/open-wt"));
2206        cx.update(|cx| {
2207            repository.update(cx, |repository, _| {
2208                repository.create_worktree(
2209                    git::repository::CreateWorktreeTarget::NewBranch {
2210                        branch_name: "open-wt".to_string(),
2211                        base_sha: Some("deadbeef".to_string()),
2212                    },
2213                    worktree_path.clone(),
2214                )
2215            })
2216        })
2217        .await
2218        .unwrap()
2219        .unwrap();
2220
2221        let worktree_project = Project::test(fs.clone(), [worktree_path.as_path()], cx).await;
2222        cx.executor().run_until_parked();
2223
2224        let main_group_key = project.read_with(cx, |project, cx| project.project_group_key(cx));
2225        let worktree_group_key =
2226            worktree_project.read_with(cx, |project, cx| project.project_group_key(cx));
2227        assert_eq!(
2228            main_group_key, worktree_group_key,
2229            "the worktree workspace should belong to the same project group as the main repo"
2230        );
2231
2232        let window_handle =
2233            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2234        let workspace = window_handle
2235            .read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone())
2236            .unwrap();
2237        let worktree_workspace = window_handle
2238            .update(cx, |multi_workspace, window, cx| {
2239                let worktree_workspace =
2240                    cx.new(|cx| Workspace::test_new(worktree_project.clone(), window, cx));
2241                multi_workspace.add(worktree_workspace.clone(), window, cx);
2242                worktree_workspace
2243            })
2244            .unwrap();
2245
2246        let mut cx = VisualTestContext::from_window(window_handle.into(), cx);
2247        let worktree_picker = cx.update(|window, cx| {
2248            cx.new(|cx| WorktreePicker::new(project, workspace.downgrade(), window, cx))
2249        });
2250        cx.run_until_parked();
2251
2252        worktree_picker.update(&mut cx, |worktree_picker, cx| {
2253            worktree_picker.picker.update(cx, |picker, _| {
2254                assert!(
2255                    picker
2256                        .delegate
2257                        .project_worktree_paths
2258                        .contains(&worktree_path),
2259                    "the worktree should be considered open in this window"
2260                );
2261            })
2262        });
2263
2264        worktree_picker.update_in(&mut cx, |worktree_picker, window, cx| {
2265            worktree_picker.picker.update(cx, |picker, cx| {
2266                picker
2267                    .delegate
2268                    .remove_worktree_from_window(&worktree_path, window, cx);
2269            })
2270        });
2271        cx.run_until_parked();
2272
2273        window_handle
2274            .read_with(&cx, |multi_workspace, _| {
2275                assert!(
2276                    multi_workspace
2277                        .workspaces()
2278                        .all(|workspace| *workspace != worktree_workspace),
2279                    "the worktree workspace should be removed from the window"
2280                );
2281            })
2282            .unwrap();
2283
2284        worktree_picker.update(&mut cx, |worktree_picker, cx| {
2285            worktree_picker.picker.update(cx, |picker, _| {
2286                assert!(
2287                    !picker
2288                        .delegate
2289                        .project_worktree_paths
2290                        .contains(&worktree_path),
2291                    "the worktree should no longer be considered open in this window"
2292                );
2293            })
2294        });
2295
2296        assert!(
2297            repo_contains_worktree(&repository, &worktree_path, &mut cx).await,
2298            "removing the worktree from the window should not delete the git worktree"
2299        );
2300    }
2301}
2302
Served at tenant.openagents/omega Member data and write actions are omitted.