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}