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 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, RepositoryEvent},
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, Tooltip, prelude::*};
22use util::{ResultExt, debug_panic, paths::PathExt};
23use workspace::{
24 ModalView, MultiWorkspace, OpenMode, Workspace, notifications::DetachAndPromptErr,
25};
26
27use crate::git_panel::show_error_toast;
28
29actions!(
30 git,
31 [
32 WorktreeFromDefault,
33 WorktreeFromDefaultOnWindow,
34 DeleteWorktree
35 ]
36);
37
38pub fn open(
39 workspace: &mut Workspace,
40 _: &zed_actions::git::Worktree,
41 window: &mut Window,
42 cx: &mut Context<Workspace>,
43) {
44 let repository = workspace.project().read(cx).active_repository(cx);
45 let workspace_handle = workspace.weak_handle();
46 workspace.toggle_modal(window, cx, |window, cx| {
47 WorktreeList::new(repository, workspace_handle, rems(34.), window, cx)
48 })
49}
50
51pub fn create_embedded(
52 repository: Option<Entity<Repository>>,
53 workspace: WeakEntity<Workspace>,
54 width: Rems,
55 window: &mut Window,
56 cx: &mut Context<WorktreeList>,
57) -> WorktreeList {
58 WorktreeList::new_embedded(repository, workspace, width, window, cx)
59}
60
61pub struct WorktreeList {
62 width: Rems,
63 pub picker: Entity<Picker<WorktreeListDelegate>>,
64 picker_focus_handle: FocusHandle,
65 _subscriptions: Vec<Subscription>,
66 embedded: bool,
67}
68
69impl WorktreeList {
70 fn new(
71 repository: Option<Entity<Repository>>,
72 workspace: WeakEntity<Workspace>,
73 width: Rems,
74 window: &mut Window,
75 cx: &mut Context<Self>,
76 ) -> Self {
77 let mut this = Self::new_inner(repository, workspace, width, false, window, cx);
78 this._subscriptions
79 .push(cx.subscribe(&this.picker, |_, _, _, cx| {
80 cx.emit(DismissEvent);
81 }));
82 this
83 }
84
85 fn new_inner(
86 repository: Option<Entity<Repository>>,
87 workspace: WeakEntity<Workspace>,
88 width: Rems,
89 embedded: bool,
90 window: &mut Window,
91 cx: &mut Context<Self>,
92 ) -> Self {
93 let all_worktrees_request = repository
94 .clone()
95 .map(|repository| repository.update(cx, |repository, _| repository.worktrees()));
96
97 let default_branch_request = repository.clone().map(|repository| {
98 repository.update(cx, |repository, _| repository.default_branch(false))
99 });
100
101 cx.spawn_in(window, async move |this, cx| {
102 let all_worktrees: Vec<_> = all_worktrees_request
103 .context("No active repository")?
104 .await??
105 .into_iter()
106 .filter(|worktree| !worktree.is_bare) // hide bare repositories
107 .collect();
108
109 let default_branch = default_branch_request
110 .context("No active repository")?
111 .await
112 .map(Result::ok)
113 .ok()
114 .flatten()
115 .flatten();
116
117 this.update_in(cx, |this, window, cx| {
118 this.picker.update(cx, |picker, cx| {
119 picker.delegate.all_worktrees = Some(all_worktrees);
120 picker.delegate.default_branch = default_branch;
121 picker.delegate.refresh_forbidden_deletion_path(cx);
122 picker.refresh(window, cx);
123 })
124 })?;
125
126 anyhow::Ok(())
127 })
128 .detach_and_log_err(cx);
129
130 let delegate = WorktreeListDelegate::new(workspace, repository.clone(), window, cx);
131 let picker = cx.new(|cx| {
132 Picker::uniform_list(delegate, window, cx)
133 .show_scrollbar(true)
134 .modal(!embedded)
135 });
136 let picker_focus_handle = picker.focus_handle(cx);
137 picker.update(cx, |picker, _| {
138 picker.delegate.focus_handle = picker_focus_handle.clone();
139 });
140
141 let mut subscriptions = Vec::new();
142 if let Some(repo) = &repository {
143 let picker_entity = picker.clone();
144 subscriptions.push(cx.subscribe(
145 repo,
146 move |_this, repo, event: &RepositoryEvent, cx| {
147 if matches!(event, RepositoryEvent::GitWorktreeListChanged) {
148 let worktrees_request = repo.update(cx, |repo, _| repo.worktrees());
149 let picker = picker_entity.clone();
150 cx.spawn(async move |_, cx| {
151 let all_worktrees: Vec<_> = worktrees_request
152 .await??
153 .into_iter()
154 .filter(|worktree| !worktree.is_bare)
155 .collect();
156 picker.update(cx, |picker, cx| {
157 picker.delegate.all_worktrees = Some(all_worktrees);
158 picker.delegate.refresh_forbidden_deletion_path(cx);
159 });
160 anyhow::Ok(())
161 })
162 .detach_and_log_err(cx);
163 }
164 },
165 ));
166 }
167
168 Self {
169 picker,
170 picker_focus_handle,
171 width,
172 _subscriptions: subscriptions,
173 embedded,
174 }
175 }
176
177 fn new_embedded(
178 repository: Option<Entity<Repository>>,
179 workspace: WeakEntity<Workspace>,
180 width: Rems,
181 window: &mut Window,
182 cx: &mut Context<Self>,
183 ) -> Self {
184 let mut this = Self::new_inner(repository, workspace, width, true, window, cx);
185 this._subscriptions
186 .push(cx.subscribe(&this.picker, |_, _, _, cx| {
187 cx.emit(DismissEvent);
188 }));
189 this
190 }
191
192 pub fn handle_modifiers_changed(
193 &mut self,
194 ev: &ModifiersChangedEvent,
195 _: &mut Window,
196 cx: &mut Context<Self>,
197 ) {
198 self.picker
199 .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)
200 }
201
202 pub fn handle_new_worktree(
203 &mut self,
204 replace_current_window: bool,
205 window: &mut Window,
206 cx: &mut Context<Self>,
207 ) {
208 self.picker.update(cx, |picker, cx| {
209 let ix = picker.delegate.selected_index();
210 let Some(entry) = picker.delegate.matches.get(ix) else {
211 return;
212 };
213 let Some(default_branch) = picker.delegate.default_branch.clone() else {
214 return;
215 };
216 if !entry.is_new {
217 return;
218 }
219 picker.delegate.create_worktree(
220 entry.worktree.display_name(),
221 replace_current_window,
222 Some(default_branch.into()),
223 window,
224 cx,
225 );
226 })
227 }
228
229 pub fn handle_delete(
230 &mut self,
231 _: &DeleteWorktree,
232 window: &mut Window,
233 cx: &mut Context<Self>,
234 ) {
235 self.picker.update(cx, |picker, cx| {
236 picker
237 .delegate
238 .delete_at(picker.delegate.selected_index, window, cx)
239 })
240 }
241}
242impl ModalView for WorktreeList {}
243impl EventEmitter<DismissEvent> for WorktreeList {}
244
245impl Focusable for WorktreeList {
246 fn focus_handle(&self, _: &App) -> FocusHandle {
247 self.picker_focus_handle.clone()
248 }
249}
250
251impl Render for WorktreeList {
252 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
253 v_flex()
254 .key_context("GitWorktreeSelector")
255 .w(self.width)
256 .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
257 .on_action(cx.listener(|this, _: &WorktreeFromDefault, w, cx| {
258 this.handle_new_worktree(false, w, cx)
259 }))
260 .on_action(cx.listener(|this, _: &WorktreeFromDefaultOnWindow, w, cx| {
261 this.handle_new_worktree(true, w, cx)
262 }))
263 .on_action(cx.listener(|this, _: &DeleteWorktree, window, cx| {
264 this.handle_delete(&DeleteWorktree, window, cx)
265 }))
266 .child(self.picker.clone())
267 .when(!self.embedded, |el| {
268 el.on_mouse_down_out({
269 cx.listener(move |this, _, window, cx| {
270 this.picker.update(cx, |this, cx| {
271 this.cancel(&Default::default(), window, cx);
272 })
273 })
274 })
275 })
276 }
277}
278
279#[derive(Debug, Clone)]
280struct WorktreeEntry {
281 worktree: GitWorktree,
282 positions: Vec<usize>,
283 is_new: bool,
284}
285
286impl WorktreeEntry {
287 fn can_delete(&self, forbidden_deletion_path: Option<&PathBuf>) -> bool {
288 !self.is_new
289 && !self.worktree.is_main
290 && forbidden_deletion_path != Some(&self.worktree.path)
291 }
292}
293
294pub struct WorktreeListDelegate {
295 matches: Vec<WorktreeEntry>,
296 all_worktrees: Option<Vec<GitWorktree>>,
297 workspace: WeakEntity<Workspace>,
298 repo: Option<Entity<Repository>>,
299 selected_index: usize,
300 last_query: String,
301 modifiers: Modifiers,
302 focus_handle: FocusHandle,
303 default_branch: Option<SharedString>,
304 forbidden_deletion_path: Option<PathBuf>,
305 current_worktree_path: Option<PathBuf>,
306}
307
308impl WorktreeListDelegate {
309 fn new(
310 workspace: WeakEntity<Workspace>,
311 repo: Option<Entity<Repository>>,
312 _window: &mut Window,
313 cx: &mut Context<WorktreeList>,
314 ) -> Self {
315 let current_worktree_path = repo
316 .as_ref()
317 .map(|r| r.read(cx).work_directory_abs_path.to_path_buf());
318
319 Self {
320 matches: vec![],
321 all_worktrees: None,
322 workspace,
323 selected_index: 0,
324 repo,
325 last_query: Default::default(),
326 modifiers: Default::default(),
327 focus_handle: cx.focus_handle(),
328 default_branch: None,
329 forbidden_deletion_path: None,
330 current_worktree_path,
331 }
332 }
333
334 fn create_worktree(
335 &self,
336 worktree_branch: &str,
337 replace_current_window: bool,
338 commit: Option<String>,
339 window: &mut Window,
340 cx: &mut Context<Picker<Self>>,
341 ) {
342 let Some(repo) = self.repo.clone() else {
343 return;
344 };
345
346 let branch = worktree_branch.to_string();
347 let workspace = self.workspace.clone();
348 cx.spawn_in(window, async move |_, cx| {
349 let (receiver, new_worktree_path) = repo.update(cx, |repo, cx| {
350 let worktree_directory_setting = ProjectSettings::get_global(cx)
351 .git
352 .worktree_directory
353 .clone();
354 let new_worktree_path =
355 repo.path_for_new_linked_worktree(&branch, &worktree_directory_setting)?;
356 let receiver = repo.create_worktree(
357 git::repository::CreateWorktreeTarget::NewBranch {
358 branch_name: branch.clone(),
359 base_sha: commit,
360 },
361 new_worktree_path.clone(),
362 );
363 anyhow::Ok((receiver, new_worktree_path))
364 })?;
365 receiver.await??;
366
367 workspace.update(cx, |workspace, cx| {
368 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
369 let repo_path = &repo.read(cx).snapshot().work_directory_abs_path;
370 let project = workspace.project();
371 if let Some((parent_worktree, _)) =
372 project.read(cx).find_worktree(repo_path, cx)
373 {
374 let worktree_store = project.read(cx).worktree_store();
375 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
376 if trusted_worktrees.can_trust(
377 &worktree_store,
378 parent_worktree.read(cx).id(),
379 cx,
380 ) {
381 trusted_worktrees.trust(
382 &worktree_store,
383 HashSet::from_iter([PathTrust::AbsPath(
384 new_worktree_path.clone(),
385 )]),
386 cx,
387 );
388 }
389 });
390 }
391 }
392 })?;
393
394 let (connection_options, app_state, is_local) =
395 workspace.update(cx, |workspace, cx| {
396 let project = workspace.project().clone();
397 let connection_options = project.read(cx).remote_connection_options(cx);
398 let app_state = workspace.app_state().clone();
399 let is_local = project.read(cx).is_local();
400 (connection_options, app_state, is_local)
401 })?;
402
403 if is_local {
404 workspace
405 .update_in(cx, |workspace, window, cx| {
406 workspace.open_workspace_for_paths(
407 OpenMode::Activate,
408 vec![new_worktree_path],
409 window,
410 cx,
411 )
412 })?
413 .await?;
414 } else if let Some(connection_options) = connection_options {
415 open_remote_worktree(
416 connection_options,
417 vec![new_worktree_path],
418 app_state,
419 workspace.clone(),
420 replace_current_window,
421 cx,
422 )
423 .await?;
424 }
425
426 anyhow::Ok(())
427 })
428 .detach_and_prompt_err("Failed to create worktree", window, cx, |e, _, _| {
429 let msg = e.to_string();
430 if msg.contains("git.worktree_directory") {
431 Some(format!("Invalid git.worktree_directory setting: {}", e))
432 } else {
433 Some(msg)
434 }
435 });
436 }
437
438 fn open_worktree(
439 &self,
440 worktree_path: &PathBuf,
441 replace_current_window: bool,
442 window: &mut Window,
443 cx: &mut Context<Picker<Self>>,
444 ) {
445 let workspace = self.workspace.clone();
446 let path = worktree_path.clone();
447
448 let Some((connection_options, app_state, is_local)) = workspace
449 .update(cx, |workspace, cx| {
450 let project = workspace.project().clone();
451 let connection_options = project.read(cx).remote_connection_options(cx);
452 let app_state = workspace.app_state().clone();
453 let is_local = project.read(cx).is_local();
454 (connection_options, app_state, is_local)
455 })
456 .log_err()
457 else {
458 return;
459 };
460 let open_mode = if replace_current_window {
461 OpenMode::Activate
462 } else {
463 OpenMode::NewWindow
464 };
465
466 if is_local {
467 let open_task = workspace.update(cx, |workspace, cx| {
468 workspace.open_workspace_for_paths(open_mode, vec![path], window, cx)
469 });
470 cx.spawn(async move |_, _| {
471 open_task?.await?;
472 anyhow::Ok(())
473 })
474 .detach_and_prompt_err(
475 "Failed to open worktree",
476 window,
477 cx,
478 |e, _, _| Some(e.to_string()),
479 );
480 } else if let Some(connection_options) = connection_options {
481 cx.spawn_in(window, async move |_, cx| {
482 open_remote_worktree(
483 connection_options,
484 vec![path],
485 app_state,
486 workspace,
487 replace_current_window,
488 cx,
489 )
490 .await
491 })
492 .detach_and_prompt_err(
493 "Failed to open worktree",
494 window,
495 cx,
496 |e, _, _| Some(e.to_string()),
497 );
498 }
499
500 cx.emit(DismissEvent);
501 }
502
503 fn base_branch<'a>(&'a self, cx: &'a mut Context<Picker<Self>>) -> Option<&'a str> {
504 self.repo
505 .as_ref()
506 .and_then(|repo| repo.read(cx).branch.as_ref().map(|b| b.name()))
507 }
508
509 fn delete_at(&self, idx: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
510 let Some(entry) = self.matches.get(idx).cloned() else {
511 return;
512 };
513 if !entry.can_delete(self.forbidden_deletion_path.as_ref()) {
514 return;
515 }
516 let Some(repo) = self.repo.clone() else {
517 return;
518 };
519 let workspace = self.workspace.clone();
520 let path = entry.worktree.path;
521
522 cx.spawn_in(window, async move |picker, cx| {
523 let result = repo
524 .update(cx, |repo, _| repo.remove_worktree(path.clone(), false))
525 .await?;
526
527 if let Err(e) = result {
528 log::error!("Failed to remove worktree: {}", e);
529 if let Some(workspace) = workspace.upgrade() {
530 cx.update(|_window, cx| {
531 show_error_toast(
532 workspace,
533 format!("worktree remove {}", path.display()),
534 e,
535 cx,
536 )
537 })?;
538 }
539 return Ok(());
540 }
541
542 picker.update_in(cx, |picker, _, cx| {
543 picker.delegate.matches.retain(|e| e.worktree.path != path);
544 if let Some(all_worktrees) = &mut picker.delegate.all_worktrees {
545 all_worktrees.retain(|w| w.path != path);
546 }
547 picker.delegate.refresh_forbidden_deletion_path(cx);
548 if picker.delegate.matches.is_empty() {
549 picker.delegate.selected_index = 0;
550 } else if picker.delegate.selected_index >= picker.delegate.matches.len() {
551 picker.delegate.selected_index = picker.delegate.matches.len() - 1;
552 }
553 cx.notify();
554 })?;
555
556 anyhow::Ok(())
557 })
558 .detach();
559 }
560
561 fn refresh_forbidden_deletion_path(&mut self, cx: &App) {
562 let Some(workspace) = self.workspace.upgrade() else {
563 debug_panic!("Workspace should always be available or else the picker would be closed");
564 self.forbidden_deletion_path = None;
565 return;
566 };
567
568 let visible_worktree_paths = workspace.read_with(cx, |workspace, cx| {
569 workspace
570 .project()
571 .read(cx)
572 .visible_worktrees(cx)
573 .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
574 .collect::<Vec<_>>()
575 });
576
577 self.forbidden_deletion_path = if visible_worktree_paths.len() == 1 {
578 visible_worktree_paths.into_iter().next()
579 } else {
580 None
581 };
582 }
583}
584
585async fn open_remote_worktree(
586 connection_options: RemoteConnectionOptions,
587 paths: Vec<PathBuf>,
588 app_state: Arc<workspace::AppState>,
589 workspace: WeakEntity<Workspace>,
590 replace_current_window: bool,
591 cx: &mut AsyncWindowContext,
592) -> anyhow::Result<()> {
593 let workspace_window = cx
594 .window_handle()
595 .downcast::<MultiWorkspace>()
596 .ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?;
597
598 let connect_task = workspace.update_in(cx, |workspace, window, cx| {
599 workspace.toggle_modal(window, cx, |window, cx| {
600 RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
601 });
602
603 let prompt = workspace
604 .active_modal::<RemoteConnectionModal>(cx)
605 .expect("Modal just created")
606 .read(cx)
607 .prompt
608 .clone();
609
610 connect(
611 ConnectionIdentifier::setup(),
612 connection_options.clone(),
613 prompt,
614 window,
615 cx,
616 )
617 .prompt_err("Failed to connect", window, cx, |_, _, _| None)
618 })?;
619
620 let session = connect_task.await;
621
622 workspace
623 .update_in(cx, |workspace, _window, cx| {
624 if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
625 prompt.update(cx, |prompt, cx| prompt.finished(cx))
626 }
627 })
628 .ok();
629
630 let Some(Some(session)) = session else {
631 return Ok(());
632 };
633
634 let new_project: Entity<project::Project> = cx.update(|_, cx| {
635 project::Project::remote(
636 session,
637 app_state.client.clone(),
638 app_state.node_runtime.clone(),
639 app_state.user_store.clone(),
640 app_state.languages.clone(),
641 app_state.fs.clone(),
642 true,
643 cx,
644 )
645 })?;
646
647 let window_to_use = if replace_current_window {
648 workspace_window
649 } else {
650 let workspace_position = cx
651 .update(|_, cx| {
652 workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
653 })?
654 .await
655 .context("fetching workspace position from db")?;
656
657 let mut options =
658 cx.update(|_, cx| (app_state.build_window_options)(workspace_position.display, cx))?;
659 options.window_bounds = workspace_position.window_bounds;
660
661 cx.open_window(options, |window, cx| {
662 let workspace = cx.new(|cx| {
663 let mut workspace =
664 Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
665 workspace.centered_layout = workspace_position.centered_layout;
666 workspace
667 });
668 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
669 })?
670 };
671
672 workspace::open_remote_project_with_existing_connection(
673 connection_options,
674 new_project,
675 paths,
676 app_state,
677 window_to_use,
678 None,
679 cx,
680 )
681 .await?;
682
683 Ok(())
684}
685
686impl PickerDelegate for WorktreeListDelegate {
687 type ListItem = ListItem;
688
689 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
690 "Select worktree…".into()
691 }
692
693 fn editor_position(&self) -> PickerEditorPosition {
694 PickerEditorPosition::Start
695 }
696
697 fn match_count(&self) -> usize {
698 self.matches.len()
699 }
700
701 fn selected_index(&self) -> usize {
702 self.selected_index
703 }
704
705 fn set_selected_index(
706 &mut self,
707 ix: usize,
708 _window: &mut Window,
709 _: &mut Context<Picker<Self>>,
710 ) {
711 self.selected_index = ix;
712 }
713
714 fn update_matches(
715 &mut self,
716 query: String,
717 window: &mut Window,
718 cx: &mut Context<Picker<Self>>,
719 ) -> Task<()> {
720 let Some(all_worktrees) = self.all_worktrees.clone() else {
721 return Task::ready(());
722 };
723
724 cx.spawn_in(window, async move |picker, cx| {
725 let main_worktree_path = all_worktrees
726 .iter()
727 .find(|wt| wt.is_main)
728 .map(|wt| wt.path.clone());
729
730 let mut matches: Vec<WorktreeEntry> = if query.is_empty() {
731 all_worktrees
732 .into_iter()
733 .map(|worktree| WorktreeEntry {
734 worktree,
735 positions: Vec::new(),
736 is_new: false,
737 })
738 .collect()
739 } else {
740 let candidates = all_worktrees
741 .iter()
742 .enumerate()
743 .map(|(ix, worktree)| {
744 StringMatchCandidate::new(
745 ix,
746 &worktree.directory_name(main_worktree_path.as_deref()),
747 )
748 })
749 .collect::<Vec<StringMatchCandidate>>();
750 fuzzy::match_strings(
751 &candidates,
752 &query,
753 true,
754 true,
755 10000,
756 &Default::default(),
757 cx.background_executor().clone(),
758 )
759 .await
760 .into_iter()
761 .map(|candidate| WorktreeEntry {
762 worktree: all_worktrees[candidate.candidate_id].clone(),
763 positions: candidate.positions,
764 is_new: false,
765 })
766 .collect()
767 };
768 picker
769 .update(cx, |picker, _| {
770 if !query.is_empty()
771 && !matches.first().is_some_and(|entry| {
772 entry.worktree.directory_name(main_worktree_path.as_deref()) == query
773 })
774 {
775 let query = query.replace(' ', "-");
776 matches.push(WorktreeEntry {
777 worktree: GitWorktree {
778 path: Default::default(),
779 ref_name: Some(format!("refs/heads/{query}").into()),
780 sha: Default::default(),
781 is_main: false,
782 is_bare: false,
783 },
784 positions: Vec::new(),
785 is_new: true,
786 })
787 }
788 let delegate = &mut picker.delegate;
789 delegate.matches = matches;
790 if delegate.matches.is_empty() {
791 delegate.selected_index = 0;
792 } else {
793 delegate.selected_index =
794 core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
795 }
796 delegate.last_query = query;
797 })
798 .log_err();
799 })
800 }
801
802 fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
803 let Some(entry) = self.matches.get(self.selected_index()) else {
804 return;
805 };
806 if entry.is_new {
807 self.create_worktree(&entry.worktree.display_name(), secondary, None, window, cx);
808 } else {
809 self.open_worktree(&entry.worktree.path, !secondary, window, cx);
810 }
811
812 cx.emit(DismissEvent);
813 }
814
815 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
816 cx.emit(DismissEvent);
817 }
818
819 fn render_match(
820 &self,
821 ix: usize,
822 selected: bool,
823 _window: &mut Window,
824 cx: &mut Context<Picker<Self>>,
825 ) -> Option<Self::ListItem> {
826 let entry = &self.matches.get(ix)?;
827 let path = entry.worktree.path.compact().to_string_lossy().to_string();
828 let sha = entry
829 .worktree
830 .sha
831 .clone()
832 .chars()
833 .take(7)
834 .collect::<String>();
835
836 let (branch_name, sublabel) = if entry.is_new {
837 (
838 Label::new(format!(
839 "Create Worktree: \"{}\"…",
840 entry.worktree.display_name()
841 ))
842 .truncate()
843 .into_any_element(),
844 format!(
845 "based off {}",
846 self.base_branch(cx).unwrap_or("the current branch")
847 ),
848 )
849 } else {
850 let main_worktree_path = self
851 .all_worktrees
852 .as_ref()
853 .and_then(|wts| wts.iter().find(|wt| wt.is_main))
854 .map(|wt| wt.path.as_path());
855 let display_name = entry.worktree.directory_name(main_worktree_path);
856 let first_line = display_name.lines().next().unwrap_or(&display_name);
857 let positions: Vec<_> = entry
858 .positions
859 .iter()
860 .copied()
861 .filter(|&pos| pos < first_line.len())
862 .collect();
863
864 (
865 HighlightedLabel::new(first_line.to_owned(), positions)
866 .truncate()
867 .into_any_element(),
868 path,
869 )
870 };
871
872 let focus_handle = self.focus_handle.clone();
873
874 let can_delete = entry.can_delete(self.forbidden_deletion_path.as_ref());
875
876 let delete_button = |entry_ix: usize| {
877 IconButton::new(("delete-worktree", entry_ix), IconName::Trash)
878 .icon_size(IconSize::Small)
879 .tooltip(move |_, cx| {
880 Tooltip::for_action_in("Delete Worktree", &DeleteWorktree, &focus_handle, cx)
881 })
882 .on_click(cx.listener(move |this, _, window, cx| {
883 this.delegate.delete_at(entry_ix, window, cx);
884 }))
885 };
886
887 let is_current = !entry.is_new
888 && self
889 .current_worktree_path
890 .as_ref()
891 .is_some_and(|current| *current == entry.worktree.path);
892
893 let entry_icon = if entry.is_new {
894 IconName::Plus
895 } else if is_current {
896 IconName::Check
897 } else {
898 IconName::GitWorktree
899 };
900
901 Some(
902 ListItem::new(format!("worktree-menu-{ix}"))
903 .inset(true)
904 .spacing(ListItemSpacing::Sparse)
905 .toggle_state(selected)
906 .child(
907 h_flex()
908 .w_full()
909 .gap_2p5()
910 .child(
911 Icon::new(entry_icon)
912 .color(if is_current {
913 Color::Accent
914 } else {
915 Color::Muted
916 })
917 .size(IconSize::Small),
918 )
919 .child(v_flex().w_full().min_w_0().child(branch_name).map(|this| {
920 if entry.is_new {
921 this.child(
922 Label::new(sublabel)
923 .size(LabelSize::Small)
924 .color(Color::Muted)
925 .truncate(),
926 )
927 } else {
928 this.child(
929 h_flex()
930 .w_full()
931 .min_w_0()
932 .gap_1p5()
933 .when_some(
934 entry.worktree.branch_name().map(|b| b.to_string()),
935 |this, branch| {
936 this.child(
937 Label::new(branch)
938 .size(LabelSize::Small)
939 .color(Color::Muted),
940 )
941 .child(
942 Label::new("•")
943 .alpha(0.5)
944 .color(Color::Muted)
945 .size(LabelSize::Small),
946 )
947 },
948 )
949 .child(
950 Label::new(sha)
951 .size(LabelSize::Small)
952 .color(Color::Muted),
953 )
954 .child(
955 Label::new("•")
956 .alpha(0.5)
957 .color(Color::Muted)
958 .size(LabelSize::Small),
959 )
960 .child(
961 Label::new(sublabel)
962 .truncate_start()
963 .color(Color::Muted)
964 .size(LabelSize::Small)
965 .flex_1(),
966 )
967 .into_any_element(),
968 )
969 }
970 })),
971 )
972 .when(!entry.is_new && !is_current, |this| {
973 let focus_handle = self.focus_handle.clone();
974 let open_in_new_window_button =
975 IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
976 .icon_size(IconSize::Small)
977 .tooltip(move |_, cx| {
978 Tooltip::for_action_in(
979 "Open in New Window",
980 &menu::SecondaryConfirm,
981 &focus_handle,
982 cx,
983 )
984 })
985 .on_click(|_, window, cx| {
986 window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx);
987 });
988
989 this.end_slot(
990 h_flex()
991 .gap_0p5()
992 .child(open_in_new_window_button)
993 .when(can_delete, |this| this.child(delete_button(ix))),
994 )
995 .show_end_slot_on_hover()
996 }),
997 )
998 }
999
1000 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
1001 Some("No worktrees found".into())
1002 }
1003
1004 fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
1005 let focus_handle = self.focus_handle.clone();
1006 let selected_entry = self.matches.get(self.selected_index);
1007 let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
1008 let can_delete = selected_entry
1009 .is_some_and(|entry| entry.can_delete(self.forbidden_deletion_path.as_ref()));
1010 let is_current = selected_entry.is_some_and(|entry| {
1011 !entry.is_new
1012 && self
1013 .current_worktree_path
1014 .as_ref()
1015 .is_some_and(|current| *current == entry.worktree.path)
1016 });
1017
1018 let footer_container = h_flex()
1019 .w_full()
1020 .p_1p5()
1021 .gap_0p5()
1022 .justify_end()
1023 .border_t_1()
1024 .border_color(cx.theme().colors().border_variant);
1025
1026 if is_creating {
1027 let from_default_button = self.default_branch.as_ref().map(|default_branch| {
1028 Button::new(
1029 "worktree-from-default",
1030 format!("Create from: {default_branch}"),
1031 )
1032 .key_binding(
1033 KeyBinding::for_action_in(&WorktreeFromDefault, &focus_handle, cx)
1034 .map(|kb| kb.size(rems_from_px(12.))),
1035 )
1036 .on_click(|_, window, cx| {
1037 window.dispatch_action(WorktreeFromDefault.boxed_clone(), cx)
1038 })
1039 });
1040
1041 let current_branch = self.base_branch(cx).unwrap_or("current branch");
1042
1043 Some(
1044 footer_container
1045 .when_some(from_default_button, |this, button| this.child(button))
1046 .child(
1047 Button::new(
1048 "worktree-from-current",
1049 format!("Create from: {current_branch}"),
1050 )
1051 .key_binding(
1052 KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
1053 .map(|kb| kb.size(rems_from_px(12.))),
1054 )
1055 .on_click(|_, window, cx| {
1056 window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1057 }),
1058 )
1059 .into_any(),
1060 )
1061 } else {
1062 Some(
1063 footer_container
1064 .when(can_delete, |this| {
1065 this.child(
1066 Button::new("delete-worktree", "Delete")
1067 .key_binding(
1068 KeyBinding::for_action_in(&DeleteWorktree, &focus_handle, cx)
1069 .map(|kb| kb.size(rems_from_px(12.))),
1070 )
1071 .on_click(|_, window, cx| {
1072 window.dispatch_action(DeleteWorktree.boxed_clone(), cx)
1073 }),
1074 )
1075 })
1076 .when(!is_current, |this| {
1077 this.child(
1078 Button::new("open-in-new-window", "Open in New Window")
1079 .key_binding(
1080 KeyBinding::for_action_in(
1081 &menu::SecondaryConfirm,
1082 &focus_handle,
1083 cx,
1084 )
1085 .map(|kb| kb.size(rems_from_px(12.))),
1086 )
1087 .on_click(|_, window, cx| {
1088 window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
1089 }),
1090 )
1091 })
1092 .child(
1093 Button::new("open-in-window", "Open")
1094 .key_binding(
1095 KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
1096 .map(|kb| kb.size(rems_from_px(12.))),
1097 )
1098 .on_click(|_, window, cx| {
1099 window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1100 }),
1101 )
1102 .into_any(),
1103 )
1104 }
1105 }
1106}