project_dropdown.rs

  1use std::cell::RefCell;
  2use std::rc::Rc;
  3
  4use gpui::{
  5    Action, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
  6    WeakEntity, actions,
  7};
  8use menu;
  9use project::{Project, Worktree, git_store::Repository};
 10use settings::WorktreeId;
 11use ui::{ContextMenu, Tooltip, prelude::*};
 12use workspace::Workspace;
 13
 14actions!(project_dropdown, [RemoveSelectedFolder]);
 15
 16struct ProjectEntry {
 17    worktree_id: WorktreeId,
 18    name: SharedString,
 19    branch: Option<SharedString>,
 20    is_active: bool,
 21}
 22
 23pub struct ProjectDropdown {
 24    menu: Entity<ContextMenu>,
 25    workspace: WeakEntity<Workspace>,
 26    worktree_ids: Rc<RefCell<Vec<WorktreeId>>>,
 27    menu_shell: Rc<RefCell<Option<Entity<ContextMenu>>>>,
 28    _subscription: Subscription,
 29}
 30
 31impl ProjectDropdown {
 32    pub fn new(
 33        project: Entity<Project>,
 34        workspace: WeakEntity<Workspace>,
 35        initial_active_worktree_id: Option<WorktreeId>,
 36        window: &mut Window,
 37        cx: &mut Context<Self>,
 38    ) -> Self {
 39        let menu_shell: Rc<RefCell<Option<Entity<ContextMenu>>>> = Rc::new(RefCell::new(None));
 40        let worktree_ids: Rc<RefCell<Vec<WorktreeId>>> = Rc::new(RefCell::new(Vec::new()));
 41
 42        let menu = Self::build_menu(
 43            project,
 44            workspace.clone(),
 45            initial_active_worktree_id,
 46            menu_shell.clone(),
 47            worktree_ids.clone(),
 48            window,
 49            cx,
 50        );
 51
 52        *menu_shell.borrow_mut() = Some(menu.clone());
 53
 54        let _subscription = cx.subscribe(&menu, |_, _, _: &DismissEvent, cx| {
 55            cx.emit(DismissEvent);
 56        });
 57
 58        Self {
 59            menu,
 60            workspace,
 61            worktree_ids,
 62            menu_shell,
 63            _subscription,
 64        }
 65    }
 66
 67    fn build_menu(
 68        project: Entity<Project>,
 69        workspace: WeakEntity<Workspace>,
 70        initial_active_worktree_id: Option<WorktreeId>,
 71        menu_shell: Rc<RefCell<Option<Entity<ContextMenu>>>>,
 72        worktree_ids: Rc<RefCell<Vec<WorktreeId>>>,
 73        window: &mut Window,
 74        cx: &mut Context<Self>,
 75    ) -> Entity<ContextMenu> {
 76        ContextMenu::build_persistent(window, cx, move |menu, _window, cx| {
 77            let active_worktree_id = if menu_shell.borrow().is_some() {
 78                workspace
 79                    .upgrade()
 80                    .and_then(|ws| ws.read(cx).active_worktree_override())
 81                    .or(initial_active_worktree_id)
 82            } else {
 83                initial_active_worktree_id
 84            };
 85
 86            let entries = Self::get_project_entries(&project, active_worktree_id, cx);
 87
 88            // Update the worktree_ids list so we can map selected_index -> worktree_id.
 89            {
 90                let mut ids = worktree_ids.borrow_mut();
 91                ids.clear();
 92                for entry in &entries {
 93                    ids.push(entry.worktree_id);
 94                }
 95            }
 96
 97            let mut menu = menu.header("Open Folders");
 98
 99            for entry in entries {
100                let worktree_id = entry.worktree_id;
101                let name = entry.name.clone();
102                let branch = entry.branch.clone();
103                let is_active = entry.is_active;
104
105                let workspace_for_select = workspace.clone();
106                let workspace_for_remove = workspace.clone();
107                let menu_shell_for_remove = menu_shell.clone();
108
109                let menu_focus_handle = menu.focus_handle(cx);
110
111                menu = menu.custom_entry(
112                    move |_window, _cx| {
113                        let name = name.clone();
114                        let branch = branch.clone();
115                        let workspace_for_remove = workspace_for_remove.clone();
116                        let menu_shell = menu_shell_for_remove.clone();
117                        let menu_focus_handle = menu_focus_handle.clone();
118
119                        h_flex()
120                            .group(name.clone())
121                            .w_full()
122                            .justify_between()
123                            .child(
124                                h_flex()
125                                    .gap_1()
126                                    .child(
127                                        Label::new(name.clone())
128                                            .when(is_active, |label| label.color(Color::Accent)),
129                                    )
130                                    .when_some(branch, |this, branch| {
131                                        this.child(Label::new(branch).color(Color::Muted))
132                                    }),
133                            )
134                            .child(
135                                IconButton::new(
136                                    ("remove", worktree_id.to_usize()),
137                                    IconName::Close,
138                                )
139                                .visible_on_hover(name)
140                                .icon_size(IconSize::Small)
141                                .icon_color(Color::Muted)
142                                .tooltip(move |_, cx| {
143                                    Tooltip::for_action_in(
144                                        "Remove Folder",
145                                        &RemoveSelectedFolder,
146                                        &menu_focus_handle,
147                                        cx,
148                                    )
149                                })
150                                .on_click({
151                                    let workspace = workspace_for_remove;
152                                    move |_, window, cx| {
153                                        Self::handle_remove(
154                                            workspace.clone(),
155                                            worktree_id,
156                                            window,
157                                            cx,
158                                        );
159
160                                        if let Some(menu_entity) = menu_shell.borrow().clone() {
161                                            menu_entity.update(cx, |menu, cx| {
162                                                menu.rebuild(window, cx);
163                                            });
164                                        }
165                                    }
166                                }),
167                            )
168                            .into_any_element()
169                    },
170                    move |window, cx| {
171                        Self::handle_select(workspace_for_select.clone(), worktree_id, window, cx);
172                        window.dispatch_action(menu::Cancel.boxed_clone(), cx);
173                    },
174                );
175            }
176
177            menu.separator()
178                .action(
179                    "Add Folder to Workspace",
180                    workspace::AddFolderToProject.boxed_clone(),
181                )
182                .action(
183                    "Open Recent Projects",
184                    zed_actions::OpenRecent {
185                        create_new_window: false,
186                    }
187                    .boxed_clone(),
188                )
189        })
190    }
191
192    /// Get all projects sorted alphabetically with their branch info.
193    fn get_project_entries(
194        project: &Entity<Project>,
195        active_worktree_id: Option<WorktreeId>,
196        cx: &App,
197    ) -> Vec<ProjectEntry> {
198        let project = project.read(cx);
199        let git_store = project.git_store().read(cx);
200        let repositories: Vec<_> = git_store.repositories().values().cloned().collect();
201
202        let mut entries: Vec<ProjectEntry> = project
203            .visible_worktrees(cx)
204            .map(|worktree| {
205                let worktree_ref = worktree.read(cx);
206                let worktree_id = worktree_ref.id();
207                let name = SharedString::from(worktree_ref.root_name().as_unix_str().to_string());
208
209                let branch = Self::get_branch_for_worktree(worktree_ref, &repositories, cx);
210
211                let is_active = active_worktree_id == Some(worktree_id);
212
213                ProjectEntry {
214                    worktree_id,
215                    name,
216                    branch,
217                    is_active,
218                }
219            })
220            .collect();
221
222        entries.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
223        entries
224    }
225
226    fn get_branch_for_worktree(
227        worktree: &Worktree,
228        repositories: &[Entity<Repository>],
229        cx: &App,
230    ) -> Option<SharedString> {
231        let worktree_abs_path = worktree.abs_path();
232
233        for repo in repositories {
234            let repo = repo.read(cx);
235            if repo.work_directory_abs_path == worktree_abs_path
236                || worktree_abs_path.starts_with(&*repo.work_directory_abs_path)
237            {
238                if let Some(branch) = &repo.branch {
239                    return Some(SharedString::from(branch.name().to_string()));
240                }
241            }
242        }
243        None
244    }
245
246    fn handle_select(
247        workspace: WeakEntity<Workspace>,
248        worktree_id: WorktreeId,
249        _window: &mut Window,
250        cx: &mut App,
251    ) {
252        if let Some(workspace) = workspace.upgrade() {
253            workspace.update(cx, |workspace, cx| {
254                workspace.set_active_worktree_override(Some(worktree_id), cx);
255            });
256        }
257    }
258
259    fn handle_remove(
260        workspace: WeakEntity<Workspace>,
261        worktree_id: WorktreeId,
262        _window: &mut Window,
263        cx: &mut App,
264    ) {
265        if let Some(workspace) = workspace.upgrade() {
266            workspace.update(cx, |workspace, cx| {
267                let project = workspace.project().clone();
268
269                let current_active_id = workspace.active_worktree_override();
270                let is_removing_active = current_active_id == Some(worktree_id);
271
272                if is_removing_active {
273                    let worktrees: Vec<_> = project.read(cx).visible_worktrees(cx).collect();
274
275                    let mut sorted: Vec<_> = worktrees
276                        .iter()
277                        .map(|wt| {
278                            let wt = wt.read(cx);
279                            (wt.root_name().as_unix_str().to_string(), wt.id())
280                        })
281                        .collect();
282                    sorted.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
283
284                    if let Some(idx) = sorted.iter().position(|(_, id)| *id == worktree_id) {
285                        let new_active_id = if idx > 0 {
286                            Some(sorted[idx - 1].1)
287                        } else if sorted.len() > 1 {
288                            Some(sorted[1].1)
289                        } else {
290                            None
291                        };
292
293                        workspace.set_active_worktree_override(new_active_id, cx);
294                    }
295                }
296
297                project.update(cx, |project, cx| {
298                    project.remove_worktree(worktree_id, cx);
299                });
300            });
301        }
302    }
303
304    fn remove_selected_folder(
305        &mut self,
306        _: &RemoveSelectedFolder,
307        window: &mut Window,
308        cx: &mut Context<Self>,
309    ) {
310        let selected_index = self.menu.read(cx).selected_index();
311
312        if let Some(menu_index) = selected_index {
313            // Early return because the "Open Folders" header is index 0.
314            if menu_index == 0 {
315                return;
316            }
317
318            let entry_index = menu_index - 1;
319            let worktree_ids = self.worktree_ids.borrow();
320
321            if entry_index < worktree_ids.len() {
322                let worktree_id = worktree_ids[entry_index];
323                drop(worktree_ids);
324
325                Self::handle_remove(self.workspace.clone(), worktree_id, window, cx);
326
327                if let Some(menu_entity) = self.menu_shell.borrow().clone() {
328                    menu_entity.update(cx, |menu, cx| {
329                        menu.rebuild(window, cx);
330                    });
331                }
332            }
333        }
334    }
335}
336
337impl Render for ProjectDropdown {
338    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
339        div()
340            .key_context("MultiProjectDropdown")
341            .track_focus(&self.focus_handle(cx))
342            .on_action(cx.listener(Self::remove_selected_folder))
343            .child(self.menu.clone())
344    }
345}
346
347impl EventEmitter<DismissEvent> for ProjectDropdown {}
348
349impl Focusable for ProjectDropdown {
350    fn focus_handle(&self, cx: &App) -> FocusHandle {
351        self.menu.focus_handle(cx)
352    }
353}