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