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 cx,
652 )
653 .await?;
654
655 Ok(())
656}
657
658impl PickerDelegate for WorktreeListDelegate {
659 type ListItem = ListItem;
660
661 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
662 "Select worktree…".into()
663 }
664
665 fn editor_position(&self) -> PickerEditorPosition {
666 PickerEditorPosition::Start
667 }
668
669 fn match_count(&self) -> usize {
670 self.matches.len()
671 }
672
673 fn selected_index(&self) -> usize {
674 self.selected_index
675 }
676
677 fn set_selected_index(
678 &mut self,
679 ix: usize,
680 _window: &mut Window,
681 _: &mut Context<Picker<Self>>,
682 ) {
683 self.selected_index = ix;
684 }
685
686 fn update_matches(
687 &mut self,
688 query: String,
689 window: &mut Window,
690 cx: &mut Context<Picker<Self>>,
691 ) -> Task<()> {
692 let Some(all_worktrees) = self.all_worktrees.clone() else {
693 return Task::ready(());
694 };
695
696 cx.spawn_in(window, async move |picker, cx| {
697 let mut matches: Vec<WorktreeEntry> = if query.is_empty() {
698 all_worktrees
699 .into_iter()
700 .map(|worktree| WorktreeEntry {
701 worktree,
702 positions: Vec::new(),
703 is_new: false,
704 })
705 .collect()
706 } else {
707 let candidates = all_worktrees
708 .iter()
709 .enumerate()
710 .map(|(ix, worktree)| {
711 let name = if worktree.is_main {
712 MAIN_WORKTREE_DISPLAY_NAME
713 } else {
714 worktree.display_name()
715 };
716 StringMatchCandidate::new(ix, name)
717 })
718 .collect::<Vec<StringMatchCandidate>>();
719 fuzzy::match_strings(
720 &candidates,
721 &query,
722 true,
723 true,
724 10000,
725 &Default::default(),
726 cx.background_executor().clone(),
727 )
728 .await
729 .into_iter()
730 .map(|candidate| WorktreeEntry {
731 worktree: all_worktrees[candidate.candidate_id].clone(),
732 positions: candidate.positions,
733 is_new: false,
734 })
735 .collect()
736 };
737 picker
738 .update(cx, |picker, _| {
739 if !query.is_empty()
740 && !matches.first().is_some_and(|entry| {
741 let name = if entry.worktree.is_main {
742 MAIN_WORKTREE_DISPLAY_NAME
743 } else {
744 entry.worktree.display_name()
745 };
746 name == query
747 })
748 {
749 let query = query.replace(' ', "-");
750 matches.push(WorktreeEntry {
751 worktree: GitWorktree {
752 path: Default::default(),
753 ref_name: Some(format!("refs/heads/{query}").into()),
754 sha: Default::default(),
755 is_main: false,
756 },
757 positions: Vec::new(),
758 is_new: true,
759 })
760 }
761 let delegate = &mut picker.delegate;
762 delegate.matches = matches;
763 if delegate.matches.is_empty() {
764 delegate.selected_index = 0;
765 } else {
766 delegate.selected_index =
767 core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
768 }
769 delegate.last_query = query;
770 })
771 .log_err();
772 })
773 }
774
775 fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
776 let Some(entry) = self.matches.get(self.selected_index()) else {
777 return;
778 };
779 if entry.is_new {
780 self.create_worktree(&entry.worktree.display_name(), secondary, None, window, cx);
781 } else {
782 self.open_worktree(&entry.worktree.path, !secondary, window, cx);
783 }
784
785 cx.emit(DismissEvent);
786 }
787
788 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
789 cx.emit(DismissEvent);
790 }
791
792 fn render_match(
793 &self,
794 ix: usize,
795 selected: bool,
796 _window: &mut Window,
797 cx: &mut Context<Picker<Self>>,
798 ) -> Option<Self::ListItem> {
799 let entry = &self.matches.get(ix)?;
800 let path = entry.worktree.path.compact().to_string_lossy().to_string();
801 let sha = entry
802 .worktree
803 .sha
804 .clone()
805 .chars()
806 .take(7)
807 .collect::<String>();
808
809 let (branch_name, sublabel) = if entry.is_new {
810 (
811 Label::new(format!(
812 "Create Worktree: \"{}\"…",
813 entry.worktree.display_name()
814 ))
815 .truncate()
816 .into_any_element(),
817 format!(
818 "based off {}",
819 self.base_branch(cx).unwrap_or("the current branch")
820 ),
821 )
822 } else {
823 let display_name = if entry.worktree.is_main {
824 MAIN_WORKTREE_DISPLAY_NAME
825 } else {
826 entry.worktree.display_name()
827 };
828 let first_line = display_name.lines().next().unwrap_or(display_name);
829 let positions: Vec<_> = entry
830 .positions
831 .iter()
832 .copied()
833 .filter(|&pos| pos < first_line.len())
834 .collect();
835
836 (
837 HighlightedLabel::new(first_line.to_owned(), positions)
838 .truncate()
839 .into_any_element(),
840 path,
841 )
842 };
843
844 let focus_handle = self.focus_handle.clone();
845
846 let can_delete = entry.can_delete(self.forbidden_deletion_path.as_ref());
847
848 let delete_button = |entry_ix: usize| {
849 IconButton::new(("delete-worktree", entry_ix), IconName::Trash)
850 .icon_size(IconSize::Small)
851 .tooltip(move |_, cx| {
852 Tooltip::for_action_in("Delete Worktree", &DeleteWorktree, &focus_handle, cx)
853 })
854 .on_click(cx.listener(move |this, _, window, cx| {
855 this.delegate.delete_at(entry_ix, window, cx);
856 }))
857 };
858
859 let is_current = !entry.is_new
860 && self
861 .current_worktree_path
862 .as_ref()
863 .is_some_and(|current| *current == entry.worktree.path);
864
865 let entry_icon = if entry.is_new {
866 IconName::Plus
867 } else if is_current {
868 IconName::Check
869 } else {
870 IconName::GitWorktree
871 };
872
873 Some(
874 ListItem::new(format!("worktree-menu-{ix}"))
875 .inset(true)
876 .spacing(ListItemSpacing::Sparse)
877 .toggle_state(selected)
878 .child(
879 h_flex()
880 .w_full()
881 .gap_2p5()
882 .child(
883 Icon::new(entry_icon)
884 .color(if is_current {
885 Color::Accent
886 } else {
887 Color::Muted
888 })
889 .size(IconSize::Small),
890 )
891 .child(v_flex().w_full().child(branch_name).map(|this| {
892 if entry.is_new {
893 this.child(
894 Label::new(sublabel)
895 .size(LabelSize::Small)
896 .color(Color::Muted)
897 .truncate(),
898 )
899 } else {
900 this.child(
901 h_flex()
902 .w_full()
903 .min_w_0()
904 .gap_1p5()
905 .child(
906 Label::new(sha)
907 .size(LabelSize::Small)
908 .color(Color::Muted),
909 )
910 .child(
911 Label::new("•")
912 .alpha(0.5)
913 .color(Color::Muted)
914 .size(LabelSize::Small),
915 )
916 .child(
917 Label::new(sublabel)
918 .truncate_start()
919 .color(Color::Muted)
920 .size(LabelSize::Small)
921 .flex_1(),
922 )
923 .into_any_element(),
924 )
925 }
926 })),
927 )
928 .when(!entry.is_new, |this| {
929 let focus_handle = self.focus_handle.clone();
930 let open_in_new_window_button =
931 IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
932 .icon_size(IconSize::Small)
933 .tooltip(move |_, cx| {
934 Tooltip::for_action_in(
935 "Open in New Window",
936 &menu::SecondaryConfirm,
937 &focus_handle,
938 cx,
939 )
940 })
941 .on_click(|_, window, cx| {
942 window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx);
943 });
944
945 this.end_slot(
946 h_flex()
947 .gap_0p5()
948 .child(open_in_new_window_button)
949 .when(can_delete, |this| this.child(delete_button(ix))),
950 )
951 .show_end_slot_on_hover()
952 }),
953 )
954 }
955
956 fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
957 Some("No worktrees found".into())
958 }
959
960 fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
961 let focus_handle = self.focus_handle.clone();
962 let selected_entry = self.matches.get(self.selected_index);
963 let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
964 let can_delete = selected_entry
965 .is_some_and(|entry| entry.can_delete(self.forbidden_deletion_path.as_ref()));
966
967 let footer_container = h_flex()
968 .w_full()
969 .p_1p5()
970 .gap_0p5()
971 .justify_end()
972 .border_t_1()
973 .border_color(cx.theme().colors().border_variant);
974
975 if is_creating {
976 let from_default_button = self.default_branch.as_ref().map(|default_branch| {
977 Button::new(
978 "worktree-from-default",
979 format!("Create from: {default_branch}"),
980 )
981 .key_binding(
982 KeyBinding::for_action_in(&WorktreeFromDefault, &focus_handle, cx)
983 .map(|kb| kb.size(rems_from_px(12.))),
984 )
985 .on_click(|_, window, cx| {
986 window.dispatch_action(WorktreeFromDefault.boxed_clone(), cx)
987 })
988 });
989
990 let current_branch = self.base_branch(cx).unwrap_or("current branch");
991
992 Some(
993 footer_container
994 .when_some(from_default_button, |this, button| this.child(button))
995 .child(
996 Button::new(
997 "worktree-from-current",
998 format!("Create from: {current_branch}"),
999 )
1000 .key_binding(
1001 KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
1002 .map(|kb| kb.size(rems_from_px(12.))),
1003 )
1004 .on_click(|_, window, cx| {
1005 window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1006 }),
1007 )
1008 .into_any(),
1009 )
1010 } else {
1011 Some(
1012 footer_container
1013 .when(can_delete, |this| {
1014 this.child(
1015 Button::new("delete-worktree", "Delete")
1016 .key_binding(
1017 KeyBinding::for_action_in(&DeleteWorktree, &focus_handle, cx)
1018 .map(|kb| kb.size(rems_from_px(12.))),
1019 )
1020 .on_click(|_, window, cx| {
1021 window.dispatch_action(DeleteWorktree.boxed_clone(), cx)
1022 }),
1023 )
1024 })
1025 .child(
1026 Button::new("open-in-new-window", "Open in New Window")
1027 .key_binding(
1028 KeyBinding::for_action_in(
1029 &menu::SecondaryConfirm,
1030 &focus_handle,
1031 cx,
1032 )
1033 .map(|kb| kb.size(rems_from_px(12.))),
1034 )
1035 .on_click(|_, window, cx| {
1036 window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
1037 }),
1038 )
1039 .child(
1040 Button::new("open-in-window", "Open")
1041 .key_binding(
1042 KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
1043 .map(|kb| kb.size(rems_from_px(12.))),
1044 )
1045 .on_click(|_, window, cx| {
1046 window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1047 }),
1048 )
1049 .into_any(),
1050 )
1051 }
1052 }
1053}