worktree_picker.rs

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