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, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
  8    Focusable, 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 remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};
 19use remote_connection::{RemoteConnectionModal, connect};
 20use std::{path::PathBuf, sync::Arc};
 21use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*};
 22use util::ResultExt;
 23use workspace::{ModalView, MultiWorkspace, 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 workspace = self.workspace.clone();
293        cx.spawn_in(window, async move |_, cx| {
294            let Some(paths) = worktree_path.await? else {
295                return anyhow::Ok(());
296            };
297            let path = paths.get(0).cloned().context("No path selected")?;
298
299            repo.update(cx, |repo, _| {
300                repo.create_worktree(branch.clone(), path.clone(), commit)
301            })
302            .await??;
303            let new_worktree_path = path.join(branch);
304
305            workspace.update(cx, |workspace, cx| {
306                if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
307                    let repo_path = &repo.read(cx).snapshot().work_directory_abs_path;
308                    let project = workspace.project();
309                    if let Some((parent_worktree, _)) =
310                        project.read(cx).find_worktree(repo_path, cx)
311                    {
312                        let worktree_store = project.read(cx).worktree_store();
313                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
314                            if trusted_worktrees.can_trust(
315                                &worktree_store,
316                                parent_worktree.read(cx).id(),
317                                cx,
318                            ) {
319                                trusted_worktrees.trust(
320                                    &worktree_store,
321                                    HashSet::from_iter([PathTrust::AbsPath(
322                                        new_worktree_path.clone(),
323                                    )]),
324                                    cx,
325                                );
326                            }
327                        });
328                    }
329                }
330            })?;
331
332            let (connection_options, app_state, is_local) =
333                workspace.update(cx, |workspace, cx| {
334                    let project = workspace.project().clone();
335                    let connection_options = project.read(cx).remote_connection_options(cx);
336                    let app_state = workspace.app_state().clone();
337                    let is_local = project.read(cx).is_local();
338                    (connection_options, app_state, is_local)
339                })?;
340
341            if is_local {
342                workspace
343                    .update_in(cx, |workspace, window, cx| {
344                        workspace.open_workspace_for_paths(
345                            replace_current_window,
346                            vec![new_worktree_path],
347                            window,
348                            cx,
349                        )
350                    })?
351                    .await?;
352            } else if let Some(connection_options) = connection_options {
353                open_remote_worktree(
354                    connection_options,
355                    vec![new_worktree_path],
356                    app_state,
357                    workspace.clone(),
358                    replace_current_window,
359                    cx,
360                )
361                .await?;
362            }
363
364            anyhow::Ok(())
365        })
366        .detach_and_prompt_err("Failed to create worktree", window, cx, |e, _, _| {
367            Some(e.to_string())
368        });
369    }
370
371    fn open_worktree(
372        &self,
373        worktree_path: &PathBuf,
374        replace_current_window: bool,
375        window: &mut Window,
376        cx: &mut Context<Picker<Self>>,
377    ) {
378        let workspace = self.workspace.clone();
379        let path = worktree_path.clone();
380
381        let Some((connection_options, app_state, is_local)) = workspace
382            .update(cx, |workspace, cx| {
383                let project = workspace.project().clone();
384                let connection_options = project.read(cx).remote_connection_options(cx);
385                let app_state = workspace.app_state().clone();
386                let is_local = project.read(cx).is_local();
387                (connection_options, app_state, is_local)
388            })
389            .log_err()
390        else {
391            return;
392        };
393
394        if is_local {
395            let open_task = workspace.update(cx, |workspace, cx| {
396                workspace.open_workspace_for_paths(replace_current_window, vec![path], window, cx)
397            });
398            cx.spawn(async move |_, _| {
399                open_task?.await?;
400                anyhow::Ok(())
401            })
402            .detach_and_prompt_err(
403                "Failed to open worktree",
404                window,
405                cx,
406                |e, _, _| Some(e.to_string()),
407            );
408        } else if let Some(connection_options) = connection_options {
409            cx.spawn_in(window, async move |_, cx| {
410                open_remote_worktree(
411                    connection_options,
412                    vec![path],
413                    app_state,
414                    workspace,
415                    replace_current_window,
416                    cx,
417                )
418                .await
419            })
420            .detach_and_prompt_err(
421                "Failed to open worktree",
422                window,
423                cx,
424                |e, _, _| Some(e.to_string()),
425            );
426        }
427
428        cx.emit(DismissEvent);
429    }
430
431    fn base_branch<'a>(&'a self, cx: &'a mut Context<Picker<Self>>) -> Option<&'a str> {
432        self.repo
433            .as_ref()
434            .and_then(|repo| repo.read(cx).branch.as_ref().map(|b| b.name()))
435    }
436}
437
438async fn open_remote_worktree(
439    connection_options: RemoteConnectionOptions,
440    paths: Vec<PathBuf>,
441    app_state: Arc<workspace::AppState>,
442    workspace: WeakEntity<Workspace>,
443    replace_current_window: bool,
444    cx: &mut AsyncWindowContext,
445) -> anyhow::Result<()> {
446    let workspace_window = cx
447        .window_handle()
448        .downcast::<MultiWorkspace>()
449        .ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?;
450
451    let connect_task = workspace.update_in(cx, |workspace, window, cx| {
452        workspace.toggle_modal(window, cx, |window, cx| {
453            RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
454        });
455
456        let prompt = workspace
457            .active_modal::<RemoteConnectionModal>(cx)
458            .expect("Modal just created")
459            .read(cx)
460            .prompt
461            .clone();
462
463        connect(
464            ConnectionIdentifier::setup(),
465            connection_options.clone(),
466            prompt,
467            window,
468            cx,
469        )
470        .prompt_err("Failed to connect", window, cx, |_, _, _| None)
471    })?;
472
473    let session = connect_task.await;
474
475    workspace
476        .update_in(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        .ok();
482
483    let Some(Some(session)) = session else {
484        return Ok(());
485    };
486
487    let new_project: Entity<project::Project> = cx.update(|_, cx| {
488        project::Project::remote(
489            session,
490            app_state.client.clone(),
491            app_state.node_runtime.clone(),
492            app_state.user_store.clone(),
493            app_state.languages.clone(),
494            app_state.fs.clone(),
495            true,
496            cx,
497        )
498    })?;
499
500    let window_to_use = if replace_current_window {
501        workspace_window
502    } else {
503        let workspace_position = cx
504            .update(|_, cx| {
505                workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
506            })?
507            .await
508            .context("fetching workspace position from db")?;
509
510        let mut options =
511            cx.update(|_, cx| (app_state.build_window_options)(workspace_position.display, cx))?;
512        options.window_bounds = workspace_position.window_bounds;
513
514        cx.open_window(options, |window, cx| {
515            let workspace = cx.new(|cx| {
516                let mut workspace =
517                    Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
518                workspace.centered_layout = workspace_position.centered_layout;
519                workspace
520            });
521            cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
522        })?
523    };
524
525    workspace::open_remote_project_with_existing_connection(
526        connection_options,
527        new_project,
528        paths,
529        app_state,
530        window_to_use,
531        cx,
532    )
533    .await?;
534
535    Ok(())
536}
537
538impl PickerDelegate for WorktreeListDelegate {
539    type ListItem = ListItem;
540
541    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
542        "Select worktree…".into()
543    }
544
545    fn editor_position(&self) -> PickerEditorPosition {
546        PickerEditorPosition::Start
547    }
548
549    fn match_count(&self) -> usize {
550        self.matches.len()
551    }
552
553    fn selected_index(&self) -> usize {
554        self.selected_index
555    }
556
557    fn set_selected_index(
558        &mut self,
559        ix: usize,
560        _window: &mut Window,
561        _: &mut Context<Picker<Self>>,
562    ) {
563        self.selected_index = ix;
564    }
565
566    fn update_matches(
567        &mut self,
568        query: String,
569        window: &mut Window,
570        cx: &mut Context<Picker<Self>>,
571    ) -> Task<()> {
572        let Some(all_worktrees) = self.all_worktrees.clone() else {
573            return Task::ready(());
574        };
575
576        cx.spawn_in(window, async move |picker, cx| {
577            let mut matches: Vec<WorktreeEntry> = if query.is_empty() {
578                all_worktrees
579                    .into_iter()
580                    .map(|worktree| WorktreeEntry {
581                        worktree,
582                        positions: Vec::new(),
583                        is_new: false,
584                    })
585                    .collect()
586            } else {
587                let candidates = all_worktrees
588                    .iter()
589                    .enumerate()
590                    .map(|(ix, worktree)| StringMatchCandidate::new(ix, worktree.branch()))
591                    .collect::<Vec<StringMatchCandidate>>();
592                fuzzy::match_strings(
593                    &candidates,
594                    &query,
595                    true,
596                    true,
597                    10000,
598                    &Default::default(),
599                    cx.background_executor().clone(),
600                )
601                .await
602                .into_iter()
603                .map(|candidate| WorktreeEntry {
604                    worktree: all_worktrees[candidate.candidate_id].clone(),
605                    positions: candidate.positions,
606                    is_new: false,
607                })
608                .collect()
609            };
610            picker
611                .update(cx, |picker, _| {
612                    if !query.is_empty()
613                        && !matches
614                            .first()
615                            .is_some_and(|entry| entry.worktree.branch() == query)
616                    {
617                        let query = query.replace(' ', "-");
618                        matches.push(WorktreeEntry {
619                            worktree: GitWorktree {
620                                path: Default::default(),
621                                ref_name: format!("refs/heads/{query}").into(),
622                                sha: Default::default(),
623                            },
624                            positions: Vec::new(),
625                            is_new: true,
626                        })
627                    }
628                    let delegate = &mut picker.delegate;
629                    delegate.matches = matches;
630                    if delegate.matches.is_empty() {
631                        delegate.selected_index = 0;
632                    } else {
633                        delegate.selected_index =
634                            core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
635                    }
636                    delegate.last_query = query;
637                })
638                .log_err();
639        })
640    }
641
642    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
643        let Some(entry) = self.matches.get(self.selected_index()) else {
644            return;
645        };
646        if entry.is_new {
647            self.create_worktree(&entry.worktree.branch(), secondary, None, window, cx);
648        } else {
649            self.open_worktree(&entry.worktree.path, secondary, window, cx);
650        }
651
652        cx.emit(DismissEvent);
653    }
654
655    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
656        cx.emit(DismissEvent);
657    }
658
659    fn render_match(
660        &self,
661        ix: usize,
662        selected: bool,
663        _window: &mut Window,
664        cx: &mut Context<Picker<Self>>,
665    ) -> Option<Self::ListItem> {
666        let entry = &self.matches.get(ix)?;
667        let path = entry.worktree.path.to_string_lossy().to_string();
668        let sha = entry
669            .worktree
670            .sha
671            .clone()
672            .chars()
673            .take(7)
674            .collect::<String>();
675
676        let (branch_name, sublabel) = if entry.is_new {
677            (
678                Label::new(format!("Create Worktree: \"{}\"", entry.worktree.branch()))
679                    .truncate()
680                    .into_any_element(),
681                format!(
682                    "based off {}",
683                    self.base_branch(cx).unwrap_or("the current branch")
684                ),
685            )
686        } else {
687            let branch = entry.worktree.branch();
688            let branch_first_line = branch.lines().next().unwrap_or(branch);
689            let positions: Vec<_> = entry
690                .positions
691                .iter()
692                .copied()
693                .filter(|&pos| pos < branch_first_line.len())
694                .collect();
695
696            (
697                HighlightedLabel::new(branch_first_line.to_owned(), positions)
698                    .truncate()
699                    .into_any_element(),
700                path,
701            )
702        };
703
704        Some(
705            ListItem::new(format!("worktree-menu-{ix}"))
706                .inset(true)
707                .spacing(ListItemSpacing::Sparse)
708                .toggle_state(selected)
709                .child(
710                    v_flex()
711                        .w_full()
712                        .child(
713                            h_flex()
714                                .gap_2()
715                                .justify_between()
716                                .overflow_x_hidden()
717                                .child(branch_name)
718                                .when(!entry.is_new, |this| {
719                                    this.child(
720                                        Label::new(sha)
721                                            .size(LabelSize::Small)
722                                            .color(Color::Muted)
723                                            .buffer_font(cx)
724                                            .into_element(),
725                                    )
726                                }),
727                        )
728                        .child(
729                            Label::new(sublabel)
730                                .size(LabelSize::Small)
731                                .color(Color::Muted)
732                                .truncate()
733                                .into_any_element(),
734                        ),
735                ),
736        )
737    }
738
739    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
740        Some("No worktrees found".into())
741    }
742
743    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
744        let focus_handle = self.focus_handle.clone();
745        let selected_entry = self.matches.get(self.selected_index);
746        let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
747
748        let footer_container = h_flex()
749            .w_full()
750            .p_1p5()
751            .gap_0p5()
752            .justify_end()
753            .border_t_1()
754            .border_color(cx.theme().colors().border_variant);
755
756        if is_creating {
757            let from_default_button = self.default_branch.as_ref().map(|default_branch| {
758                Button::new(
759                    "worktree-from-default",
760                    format!("Create from: {default_branch}"),
761                )
762                .key_binding(
763                    KeyBinding::for_action_in(&WorktreeFromDefault, &focus_handle, cx)
764                        .map(|kb| kb.size(rems_from_px(12.))),
765                )
766                .on_click(|_, window, cx| {
767                    window.dispatch_action(WorktreeFromDefault.boxed_clone(), cx)
768                })
769            });
770
771            let current_branch = self.base_branch(cx).unwrap_or("current branch");
772
773            Some(
774                footer_container
775                    .when_some(from_default_button, |this, button| this.child(button))
776                    .child(
777                        Button::new(
778                            "worktree-from-current",
779                            format!("Create from: {current_branch}"),
780                        )
781                        .key_binding(
782                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
783                                .map(|kb| kb.size(rems_from_px(12.))),
784                        )
785                        .on_click(|_, window, cx| {
786                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
787                        }),
788                    )
789                    .into_any(),
790            )
791        } else {
792            Some(
793                footer_container
794                    .child(
795                        Button::new("open-in-new-window", "Open in New Window")
796                            .key_binding(
797                                KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
798                                    .map(|kb| kb.size(rems_from_px(12.))),
799                            )
800                            .on_click(|_, window, cx| {
801                                window.dispatch_action(menu::Confirm.boxed_clone(), cx)
802                            }),
803                    )
804                    .child(
805                        Button::new("open-in-window", "Open")
806                            .key_binding(
807                                KeyBinding::for_action_in(
808                                    &menu::SecondaryConfirm,
809                                    &focus_handle,
810                                    cx,
811                                )
812                                .map(|kb| kb.size(rems_from_px(12.))),
813                            )
814                            .on_click(|_, window, cx| {
815                                window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
816                            }),
817                    )
818                    .into_any(),
819            )
820        }
821    }
822}