worktree_picker.rs

  1use anyhow::Context as _;
  2use collections::HashSet;
  3use fuzzy::StringMatchCandidate;
  4
  5use git::repository::Worktree as GitWorktree;
  6use gpui::{
  7    Action, App, AsyncApp, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
  8    InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement,
  9    PathPromptOptions, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window,
 10    actions, rems,
 11};
 12use picker::{Picker, PickerDelegate, PickerEditorPosition};
 13use project::{
 14    DirectoryLister,
 15    git_store::Repository,
 16    trusted_worktrees::{PathTrust, TrustedWorktrees},
 17};
 18use recent_projects::{RemoteConnectionModal, connect};
 19use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};
 20use std::{path::PathBuf, sync::Arc};
 21use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*};
 22use util::ResultExt;
 23use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr};
 24
 25actions!(git, [WorktreeFromDefault, WorktreeFromDefaultOnWindow]);
 26
 27pub fn open(
 28    workspace: &mut Workspace,
 29    _: &zed_actions::git::Worktree,
 30    window: &mut Window,
 31    cx: &mut Context<Workspace>,
 32) {
 33    let repository = workspace.project().read(cx).active_repository(cx);
 34    let workspace_handle = workspace.weak_handle();
 35    workspace.toggle_modal(window, cx, |window, cx| {
 36        WorktreeList::new(repository, workspace_handle, rems(34.), window, cx)
 37    })
 38}
 39
 40pub fn create_embedded(
 41    repository: Option<Entity<Repository>>,
 42    workspace: WeakEntity<Workspace>,
 43    width: Rems,
 44    window: &mut Window,
 45    cx: &mut Context<WorktreeList>,
 46) -> WorktreeList {
 47    WorktreeList::new_embedded(repository, workspace, width, window, cx)
 48}
 49
 50pub struct WorktreeList {
 51    width: Rems,
 52    pub picker: Entity<Picker<WorktreeListDelegate>>,
 53    picker_focus_handle: FocusHandle,
 54    _subscription: Option<Subscription>,
 55    embedded: bool,
 56}
 57
 58impl WorktreeList {
 59    fn new(
 60        repository: Option<Entity<Repository>>,
 61        workspace: WeakEntity<Workspace>,
 62        width: Rems,
 63        window: &mut Window,
 64        cx: &mut Context<Self>,
 65    ) -> Self {
 66        let mut this = Self::new_inner(repository, workspace, width, false, window, cx);
 67        this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| {
 68            cx.emit(DismissEvent);
 69        }));
 70        this
 71    }
 72
 73    fn new_inner(
 74        repository: Option<Entity<Repository>>,
 75        workspace: WeakEntity<Workspace>,
 76        width: Rems,
 77        embedded: bool,
 78        window: &mut Window,
 79        cx: &mut Context<Self>,
 80    ) -> Self {
 81        let all_worktrees_request = repository
 82            .clone()
 83            .map(|repository| repository.update(cx, |repository, _| repository.worktrees()));
 84
 85        let default_branch_request = repository.clone().map(|repository| {
 86            repository.update(cx, |repository, _| repository.default_branch(false))
 87        });
 88
 89        cx.spawn_in(window, async move |this, cx| {
 90            let all_worktrees = all_worktrees_request
 91                .context("No active repository")?
 92                .await??;
 93
 94            let default_branch = default_branch_request
 95                .context("No active repository")?
 96                .await
 97                .map(Result::ok)
 98                .ok()
 99                .flatten()
100                .flatten();
101
102            this.update_in(cx, |this, window, cx| {
103                this.picker.update(cx, |picker, cx| {
104                    picker.delegate.all_worktrees = Some(all_worktrees);
105                    picker.delegate.default_branch = default_branch;
106                    picker.refresh(window, cx);
107                })
108            })?;
109
110            anyhow::Ok(())
111        })
112        .detach_and_log_err(cx);
113
114        let delegate = WorktreeListDelegate::new(workspace, repository, window, cx);
115        let picker = cx.new(|cx| {
116            Picker::uniform_list(delegate, window, cx)
117                .show_scrollbar(true)
118                .modal(!embedded)
119        });
120        let picker_focus_handle = picker.focus_handle(cx);
121        picker.update(cx, |picker, _| {
122            picker.delegate.focus_handle = picker_focus_handle.clone();
123        });
124
125        Self {
126            picker,
127            picker_focus_handle,
128            width,
129            _subscription: None,
130            embedded,
131        }
132    }
133
134    fn new_embedded(
135        repository: Option<Entity<Repository>>,
136        workspace: WeakEntity<Workspace>,
137        width: Rems,
138        window: &mut Window,
139        cx: &mut Context<Self>,
140    ) -> Self {
141        let mut this = Self::new_inner(repository, workspace, width, true, window, cx);
142        this._subscription = Some(cx.subscribe(&this.picker, |_, _, _, cx| {
143            cx.emit(DismissEvent);
144        }));
145        this
146    }
147
148    pub fn handle_modifiers_changed(
149        &mut self,
150        ev: &ModifiersChangedEvent,
151        _: &mut Window,
152        cx: &mut Context<Self>,
153    ) {
154        self.picker
155            .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)
156    }
157
158    pub fn handle_new_worktree(
159        &mut self,
160        replace_current_window: bool,
161        window: &mut Window,
162        cx: &mut Context<Self>,
163    ) {
164        self.picker.update(cx, |picker, cx| {
165            let ix = picker.delegate.selected_index();
166            let Some(entry) = picker.delegate.matches.get(ix) else {
167                return;
168            };
169            let Some(default_branch) = picker.delegate.default_branch.clone() else {
170                return;
171            };
172            if !entry.is_new {
173                return;
174            }
175            picker.delegate.create_worktree(
176                entry.worktree.branch(),
177                replace_current_window,
178                Some(default_branch.into()),
179                window,
180                cx,
181            );
182        })
183    }
184}
185impl ModalView for WorktreeList {}
186impl EventEmitter<DismissEvent> for WorktreeList {}
187
188impl Focusable for WorktreeList {
189    fn focus_handle(&self, _: &App) -> FocusHandle {
190        self.picker_focus_handle.clone()
191    }
192}
193
194impl Render for WorktreeList {
195    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
196        v_flex()
197            .key_context("GitWorktreeSelector")
198            .w(self.width)
199            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
200            .on_action(cx.listener(|this, _: &WorktreeFromDefault, w, cx| {
201                this.handle_new_worktree(false, w, cx)
202            }))
203            .on_action(cx.listener(|this, _: &WorktreeFromDefaultOnWindow, w, cx| {
204                this.handle_new_worktree(true, w, cx)
205            }))
206            .child(self.picker.clone())
207            .when(!self.embedded, |el| {
208                el.on_mouse_down_out({
209                    cx.listener(move |this, _, window, cx| {
210                        this.picker.update(cx, |this, cx| {
211                            this.cancel(&Default::default(), window, cx);
212                        })
213                    })
214                })
215            })
216    }
217}
218
219#[derive(Debug, Clone)]
220struct WorktreeEntry {
221    worktree: GitWorktree,
222    positions: Vec<usize>,
223    is_new: bool,
224}
225
226pub struct WorktreeListDelegate {
227    matches: Vec<WorktreeEntry>,
228    all_worktrees: Option<Vec<GitWorktree>>,
229    workspace: WeakEntity<Workspace>,
230    repo: Option<Entity<Repository>>,
231    selected_index: usize,
232    last_query: String,
233    modifiers: Modifiers,
234    focus_handle: FocusHandle,
235    default_branch: Option<SharedString>,
236}
237
238impl WorktreeListDelegate {
239    fn new(
240        workspace: WeakEntity<Workspace>,
241        repo: Option<Entity<Repository>>,
242        _window: &mut Window,
243        cx: &mut Context<WorktreeList>,
244    ) -> Self {
245        Self {
246            matches: vec![],
247            all_worktrees: None,
248            workspace,
249            selected_index: 0,
250            repo,
251            last_query: Default::default(),
252            modifiers: Default::default(),
253            focus_handle: cx.focus_handle(),
254            default_branch: None,
255        }
256    }
257
258    fn create_worktree(
259        &self,
260        worktree_branch: &str,
261        replace_current_window: bool,
262        commit: Option<String>,
263        window: &mut Window,
264        cx: &mut Context<Picker<Self>>,
265    ) {
266        let Some(repo) = self.repo.clone() else {
267            return;
268        };
269
270        let worktree_path = self
271            .workspace
272            .clone()
273            .update(cx, |this, cx| {
274                this.prompt_for_open_path(
275                    PathPromptOptions {
276                        files: false,
277                        directories: true,
278                        multiple: false,
279                        prompt: Some("Select directory for new worktree".into()),
280                    },
281                    DirectoryLister::Project(this.project().clone()),
282                    window,
283                    cx,
284                )
285            })
286            .log_err();
287        let Some(worktree_path) = worktree_path else {
288            return;
289        };
290
291        let branch = worktree_branch.to_string();
292        let window_handle = window.window_handle();
293        let workspace = self.workspace.clone();
294        cx.spawn_in(window, async move |_, cx| {
295            let Some(paths) = worktree_path.await? else {
296                return anyhow::Ok(());
297            };
298            let path = paths.get(0).cloned().context("No path selected")?;
299
300            repo.update(cx, |repo, _| {
301                repo.create_worktree(branch.clone(), path.clone(), commit)
302            })
303            .await??;
304            let new_worktree_path = path.join(branch);
305
306            workspace.update(cx, |workspace, cx| {
307                if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
308                    let repo_path = &repo.read(cx).snapshot().work_directory_abs_path;
309                    let project = workspace.project();
310                    if let Some((parent_worktree, _)) =
311                        project.read(cx).find_worktree(repo_path, cx)
312                    {
313                        let worktree_store = project.read(cx).worktree_store();
314                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
315                            if trusted_worktrees.can_trust(
316                                &worktree_store,
317                                parent_worktree.read(cx).id(),
318                                cx,
319                            ) {
320                                trusted_worktrees.trust(
321                                    &worktree_store,
322                                    HashSet::from_iter([PathTrust::AbsPath(
323                                        new_worktree_path.clone(),
324                                    )]),
325                                    cx,
326                                );
327                            }
328                        });
329                    }
330                }
331            })?;
332
333            let (connection_options, app_state, is_local) =
334                workspace.update(cx, |workspace, cx| {
335                    let project = workspace.project().clone();
336                    let connection_options = project.read(cx).remote_connection_options(cx);
337                    let app_state = workspace.app_state().clone();
338                    let is_local = project.read(cx).is_local();
339                    (connection_options, app_state, is_local)
340                })?;
341
342            if is_local {
343                workspace
344                    .update_in(cx, |workspace, window, cx| {
345                        workspace.open_workspace_for_paths(
346                            replace_current_window,
347                            vec![new_worktree_path],
348                            window,
349                            cx,
350                        )
351                    })?
352                    .await?;
353            } else if let Some(connection_options) = connection_options {
354                open_remote_worktree(
355                    connection_options,
356                    vec![new_worktree_path],
357                    app_state,
358                    window_handle,
359                    replace_current_window,
360                    cx,
361                )
362                .await?;
363            }
364
365            anyhow::Ok(())
366        })
367        .detach_and_prompt_err("Failed to create worktree", window, cx, |e, _, _| {
368            Some(e.to_string())
369        });
370    }
371
372    fn open_worktree(
373        &self,
374        worktree_path: &PathBuf,
375        replace_current_window: bool,
376        window: &mut Window,
377        cx: &mut Context<Picker<Self>>,
378    ) {
379        let workspace = self.workspace.clone();
380        let path = worktree_path.clone();
381
382        let Some((connection_options, app_state, is_local)) = workspace
383            .update(cx, |workspace, cx| {
384                let project = workspace.project().clone();
385                let connection_options = project.read(cx).remote_connection_options(cx);
386                let app_state = workspace.app_state().clone();
387                let is_local = project.read(cx).is_local();
388                (connection_options, app_state, is_local)
389            })
390            .log_err()
391        else {
392            return;
393        };
394
395        if is_local {
396            let open_task = workspace.update(cx, |workspace, cx| {
397                workspace.open_workspace_for_paths(replace_current_window, vec![path], window, cx)
398            });
399            cx.spawn(async move |_, _| {
400                open_task?.await?;
401                anyhow::Ok(())
402            })
403            .detach_and_prompt_err(
404                "Failed to open worktree",
405                window,
406                cx,
407                |e, _, _| Some(e.to_string()),
408            );
409        } else if let Some(connection_options) = connection_options {
410            let window_handle = window.window_handle();
411            cx.spawn_in(window, async move |_, cx| {
412                open_remote_worktree(
413                    connection_options,
414                    vec![path],
415                    app_state,
416                    window_handle,
417                    replace_current_window,
418                    cx,
419                )
420                .await
421            })
422            .detach_and_prompt_err(
423                "Failed to open worktree",
424                window,
425                cx,
426                |e, _, _| Some(e.to_string()),
427            );
428        }
429
430        cx.emit(DismissEvent);
431    }
432
433    fn base_branch<'a>(&'a self, cx: &'a mut Context<Picker<Self>>) -> Option<&'a str> {
434        self.repo
435            .as_ref()
436            .and_then(|repo| repo.read(cx).branch.as_ref().map(|b| b.name()))
437    }
438}
439
440async fn open_remote_worktree(
441    connection_options: RemoteConnectionOptions,
442    paths: Vec<PathBuf>,
443    app_state: Arc<workspace::AppState>,
444    window: gpui::AnyWindowHandle,
445    replace_current_window: bool,
446    cx: &mut AsyncApp,
447) -> anyhow::Result<()> {
448    let workspace_window = window
449        .downcast::<Workspace>()
450        .ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?;
451
452    let connect_task = workspace_window.update(cx, |workspace, window, cx| {
453        workspace.toggle_modal(window, cx, |window, cx| {
454            RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
455        });
456
457        let prompt = workspace
458            .active_modal::<RemoteConnectionModal>(cx)
459            .expect("Modal just created")
460            .read(cx)
461            .prompt
462            .clone();
463
464        connect(
465            ConnectionIdentifier::setup(),
466            connection_options.clone(),
467            prompt,
468            window,
469            cx,
470        )
471        .prompt_err("Failed to connect", window, cx, |_, _, _| None)
472    })?;
473
474    let session = connect_task.await;
475
476    workspace_window.update(cx, |workspace, _window, cx| {
477        if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
478            prompt.update(cx, |prompt, cx| prompt.finished(cx))
479        }
480    })?;
481
482    let Some(Some(session)) = session else {
483        return Ok(());
484    };
485
486    let new_project: Entity<project::Project> = cx.update(|cx| {
487        project::Project::remote(
488            session,
489            app_state.client.clone(),
490            app_state.node_runtime.clone(),
491            app_state.user_store.clone(),
492            app_state.languages.clone(),
493            app_state.fs.clone(),
494            true,
495            cx,
496        )
497    });
498
499    let window_to_use = if replace_current_window {
500        workspace_window
501    } else {
502        let workspace_position = cx
503            .update(|cx| {
504                workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
505            })
506            .await
507            .context("fetching workspace position from db")?;
508
509        let mut options =
510            cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx));
511        options.window_bounds = workspace_position.window_bounds;
512
513        cx.open_window(options, |window, cx| {
514            cx.new(|cx| {
515                let mut workspace =
516                    Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
517                workspace.centered_layout = workspace_position.centered_layout;
518                workspace
519            })
520        })?
521    };
522
523    workspace::open_remote_project_with_existing_connection(
524        connection_options,
525        new_project,
526        paths,
527        app_state,
528        window_to_use,
529        cx,
530    )
531    .await?;
532
533    Ok(())
534}
535
536impl PickerDelegate for WorktreeListDelegate {
537    type ListItem = ListItem;
538
539    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
540        "Select worktree…".into()
541    }
542
543    fn editor_position(&self) -> PickerEditorPosition {
544        PickerEditorPosition::Start
545    }
546
547    fn match_count(&self) -> usize {
548        self.matches.len()
549    }
550
551    fn selected_index(&self) -> usize {
552        self.selected_index
553    }
554
555    fn set_selected_index(
556        &mut self,
557        ix: usize,
558        _window: &mut Window,
559        _: &mut Context<Picker<Self>>,
560    ) {
561        self.selected_index = ix;
562    }
563
564    fn update_matches(
565        &mut self,
566        query: String,
567        window: &mut Window,
568        cx: &mut Context<Picker<Self>>,
569    ) -> Task<()> {
570        let Some(all_worktrees) = self.all_worktrees.clone() else {
571            return Task::ready(());
572        };
573
574        cx.spawn_in(window, async move |picker, cx| {
575            let mut matches: Vec<WorktreeEntry> = if query.is_empty() {
576                all_worktrees
577                    .into_iter()
578                    .map(|worktree| WorktreeEntry {
579                        worktree,
580                        positions: Vec::new(),
581                        is_new: false,
582                    })
583                    .collect()
584            } else {
585                let candidates = all_worktrees
586                    .iter()
587                    .enumerate()
588                    .map(|(ix, worktree)| StringMatchCandidate::new(ix, worktree.branch()))
589                    .collect::<Vec<StringMatchCandidate>>();
590                fuzzy::match_strings(
591                    &candidates,
592                    &query,
593                    true,
594                    true,
595                    10000,
596                    &Default::default(),
597                    cx.background_executor().clone(),
598                )
599                .await
600                .into_iter()
601                .map(|candidate| WorktreeEntry {
602                    worktree: all_worktrees[candidate.candidate_id].clone(),
603                    positions: candidate.positions,
604                    is_new: false,
605                })
606                .collect()
607            };
608            picker
609                .update(cx, |picker, _| {
610                    if !query.is_empty()
611                        && !matches
612                            .first()
613                            .is_some_and(|entry| entry.worktree.branch() == query)
614                    {
615                        let query = query.replace(' ', "-");
616                        matches.push(WorktreeEntry {
617                            worktree: GitWorktree {
618                                path: Default::default(),
619                                ref_name: format!("refs/heads/{query}").into(),
620                                sha: Default::default(),
621                            },
622                            positions: Vec::new(),
623                            is_new: true,
624                        })
625                    }
626                    let delegate = &mut picker.delegate;
627                    delegate.matches = matches;
628                    if delegate.matches.is_empty() {
629                        delegate.selected_index = 0;
630                    } else {
631                        delegate.selected_index =
632                            core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
633                    }
634                    delegate.last_query = query;
635                })
636                .log_err();
637        })
638    }
639
640    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
641        let Some(entry) = self.matches.get(self.selected_index()) else {
642            return;
643        };
644        if entry.is_new {
645            self.create_worktree(&entry.worktree.branch(), secondary, None, window, cx);
646        } else {
647            self.open_worktree(&entry.worktree.path, secondary, window, cx);
648        }
649
650        cx.emit(DismissEvent);
651    }
652
653    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
654        cx.emit(DismissEvent);
655    }
656
657    fn render_match(
658        &self,
659        ix: usize,
660        selected: bool,
661        _window: &mut Window,
662        cx: &mut Context<Picker<Self>>,
663    ) -> Option<Self::ListItem> {
664        let entry = &self.matches.get(ix)?;
665        let path = entry.worktree.path.to_string_lossy().to_string();
666        let sha = entry
667            .worktree
668            .sha
669            .clone()
670            .chars()
671            .take(7)
672            .collect::<String>();
673
674        let (branch_name, sublabel) = if entry.is_new {
675            (
676                Label::new(format!("Create Worktree: \"{}\"", entry.worktree.branch()))
677                    .truncate()
678                    .into_any_element(),
679                format!(
680                    "based off {}",
681                    self.base_branch(cx).unwrap_or("the current branch")
682                ),
683            )
684        } else {
685            let branch = entry.worktree.branch();
686            let branch_first_line = branch.lines().next().unwrap_or(branch);
687            let positions: Vec<_> = entry
688                .positions
689                .iter()
690                .copied()
691                .filter(|&pos| pos < branch_first_line.len())
692                .collect();
693
694            (
695                HighlightedLabel::new(branch_first_line.to_owned(), positions)
696                    .truncate()
697                    .into_any_element(),
698                path,
699            )
700        };
701
702        Some(
703            ListItem::new(format!("worktree-menu-{ix}"))
704                .inset(true)
705                .spacing(ListItemSpacing::Sparse)
706                .toggle_state(selected)
707                .child(
708                    v_flex()
709                        .w_full()
710                        .child(
711                            h_flex()
712                                .gap_2()
713                                .justify_between()
714                                .overflow_x_hidden()
715                                .child(branch_name)
716                                .when(!entry.is_new, |this| {
717                                    this.child(
718                                        Label::new(sha)
719                                            .size(LabelSize::Small)
720                                            .color(Color::Muted)
721                                            .buffer_font(cx)
722                                            .into_element(),
723                                    )
724                                }),
725                        )
726                        .child(
727                            Label::new(sublabel)
728                                .size(LabelSize::Small)
729                                .color(Color::Muted)
730                                .truncate()
731                                .into_any_element(),
732                        ),
733                ),
734        )
735    }
736
737    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
738        Some("No worktrees found".into())
739    }
740
741    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
742        let focus_handle = self.focus_handle.clone();
743        let selected_entry = self.matches.get(self.selected_index);
744        let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
745
746        let footer_container = h_flex()
747            .w_full()
748            .p_1p5()
749            .gap_0p5()
750            .justify_end()
751            .border_t_1()
752            .border_color(cx.theme().colors().border_variant);
753
754        if is_creating {
755            let from_default_button = self.default_branch.as_ref().map(|default_branch| {
756                Button::new(
757                    "worktree-from-default",
758                    format!("Create from: {default_branch}"),
759                )
760                .key_binding(
761                    KeyBinding::for_action_in(&WorktreeFromDefault, &focus_handle, cx)
762                        .map(|kb| kb.size(rems_from_px(12.))),
763                )
764                .on_click(|_, window, cx| {
765                    window.dispatch_action(WorktreeFromDefault.boxed_clone(), cx)
766                })
767            });
768
769            let current_branch = self.base_branch(cx).unwrap_or("current branch");
770
771            Some(
772                footer_container
773                    .when_some(from_default_button, |this, button| this.child(button))
774                    .child(
775                        Button::new(
776                            "worktree-from-current",
777                            format!("Create from: {current_branch}"),
778                        )
779                        .key_binding(
780                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
781                                .map(|kb| kb.size(rems_from_px(12.))),
782                        )
783                        .on_click(|_, window, cx| {
784                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
785                        }),
786                    )
787                    .into_any(),
788            )
789        } else {
790            Some(
791                footer_container
792                    .child(
793                        Button::new("open-in-new-window", "Open in New Window")
794                            .key_binding(
795                                KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
796                                    .map(|kb| kb.size(rems_from_px(12.))),
797                            )
798                            .on_click(|_, window, cx| {
799                                window.dispatch_action(menu::Confirm.boxed_clone(), cx)
800                            }),
801                    )
802                    .child(
803                        Button::new("open-in-window", "Open")
804                            .key_binding(
805                                KeyBinding::for_action_in(
806                                    &menu::SecondaryConfirm,
807                                    &focus_handle,
808                                    cx,
809                                )
810                                .map(|kb| kb.size(rems_from_px(12.))),
811                            )
812                            .on_click(|_, window, cx| {
813                                window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
814                            }),
815                    )
816                    .into_any(),
817            )
818        }
819    }
820}