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