1use anyhow::Result;
2use gpui::PathPromptOptions;
3use gpui::{
4 AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
5 ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId,
6 actions, deferred, px,
7};
8use project::{DirectoryLister, DisableAiSettings, Project, ProjectGroupId, ProjectGroupKey};
9use remote::RemoteConnectionOptions;
10use settings::Settings;
11pub use settings::SidebarSide;
12use std::collections::HashSet;
13use std::future::Future;
14use std::path::Path;
15use std::path::PathBuf;
16use ui::prelude::*;
17use util::ResultExt;
18use util::path_list::PathList;
19use zed_actions::agents_sidebar::ToggleThreadSwitcher;
20
21use agent_settings::AgentSettings;
22use settings::SidebarDockPosition;
23use ui::{ContextMenu, right_click_menu};
24
25const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
26
27use crate::open_remote_project_with_existing_connection;
28use crate::{
29 CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, OpenMode,
30 Panel, Workspace, WorkspaceId, client_side_decorations,
31 persistence::model::MultiWorkspaceState,
32};
33
34actions!(
35 multi_workspace,
36 [
37 /// Toggles the workspace switcher sidebar.
38 ToggleWorkspaceSidebar,
39 /// Closes the workspace sidebar.
40 CloseWorkspaceSidebar,
41 /// Moves focus to or from the workspace sidebar without closing it.
42 FocusWorkspaceSidebar,
43 /// Activates the next project in the sidebar.
44 NextProject,
45 /// Activates the previous project in the sidebar.
46 PreviousProject,
47 /// Activates the next thread in sidebar order.
48 NextThread,
49 /// Activates the previous thread in sidebar order.
50 PreviousThread,
51 /// Expands the thread list for the current project to show more threads.
52 ShowMoreThreads,
53 /// Collapses the thread list for the current project to show fewer threads.
54 ShowFewerThreads,
55 /// Creates a new thread in the current workspace.
56 NewThread,
57 ]
58);
59
60#[derive(Default)]
61pub struct SidebarRenderState {
62 pub open: bool,
63 pub side: SidebarSide,
64}
65
66pub fn sidebar_side_context_menu(
67 id: impl Into<ElementId>,
68 cx: &App,
69) -> ui::RightClickMenu<ContextMenu> {
70 let current_position = AgentSettings::get_global(cx).sidebar_side;
71 right_click_menu(id).menu(move |window, cx| {
72 let fs = <dyn fs::Fs>::global(cx);
73 ContextMenu::build(window, cx, move |mut menu, _, _cx| {
74 let positions: [(SidebarDockPosition, &str); 2] = [
75 (SidebarDockPosition::Left, "Left"),
76 (SidebarDockPosition::Right, "Right"),
77 ];
78 for (position, label) in positions {
79 let fs = fs.clone();
80 menu = menu.toggleable_entry(
81 label,
82 position == current_position,
83 IconPosition::Start,
84 None,
85 move |_window, cx| {
86 settings::update_settings_file(fs.clone(), cx, move |settings, _cx| {
87 settings
88 .agent
89 .get_or_insert_default()
90 .set_sidebar_side(position);
91 });
92 },
93 );
94 }
95 menu
96 })
97 })
98}
99
100pub enum MultiWorkspaceEvent {
101 ActiveWorkspaceChanged,
102 WorkspaceAdded(Entity<Workspace>),
103 WorkspaceRemoved(EntityId),
104}
105
106pub enum SidebarEvent {
107 SerializeNeeded,
108}
109
110pub trait Sidebar: Focusable + Render + EventEmitter<SidebarEvent> + Sized {
111 fn width(&self, cx: &App) -> Pixels;
112 fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
113 fn has_notifications(&self, cx: &App) -> bool;
114 fn side(&self, _cx: &App) -> SidebarSide;
115
116 fn is_threads_list_view_active(&self) -> bool {
117 true
118 }
119 /// Makes focus reset back to the search editor upon toggling the sidebar from outside
120 fn prepare_for_focus(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
121 /// Opens or cycles the thread switcher popup.
122 fn toggle_thread_switcher(
123 &mut self,
124 _select_last: bool,
125 _window: &mut Window,
126 _cx: &mut Context<Self>,
127 ) {
128 }
129
130 /// Activates the next or previous project.
131 fn cycle_project(&mut self, _forward: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
132
133 /// Activates the next or previous thread in sidebar order.
134 fn cycle_thread(&mut self, _forward: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
135
136 /// Return an opaque JSON blob of sidebar-specific state to persist.
137 fn serialized_state(&self, _cx: &App) -> Option<String> {
138 None
139 }
140
141 /// Restore sidebar state from a previously-serialized blob.
142 fn restore_serialized_state(
143 &mut self,
144 _state: &str,
145 _window: &mut Window,
146 _cx: &mut Context<Self>,
147 ) {
148 }
149}
150
151pub trait SidebarHandle: 'static + Send + Sync {
152 fn width(&self, cx: &App) -> Pixels;
153 fn set_width(&self, width: Option<Pixels>, cx: &mut App);
154 fn focus_handle(&self, cx: &App) -> FocusHandle;
155 fn focus(&self, window: &mut Window, cx: &mut App);
156 fn prepare_for_focus(&self, window: &mut Window, cx: &mut App);
157 fn has_notifications(&self, cx: &App) -> bool;
158 fn to_any(&self) -> AnyView;
159 fn entity_id(&self) -> EntityId;
160 fn toggle_thread_switcher(&self, select_last: bool, window: &mut Window, cx: &mut App);
161 fn cycle_project(&self, forward: bool, window: &mut Window, cx: &mut App);
162 fn cycle_thread(&self, forward: bool, window: &mut Window, cx: &mut App);
163
164 fn is_threads_list_view_active(&self, cx: &App) -> bool;
165
166 fn side(&self, cx: &App) -> SidebarSide;
167 fn serialized_state(&self, cx: &App) -> Option<String>;
168 fn restore_serialized_state(&self, state: &str, window: &mut Window, cx: &mut App);
169}
170
171#[derive(Clone)]
172pub struct DraggedSidebar;
173
174impl Render for DraggedSidebar {
175 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
176 gpui::Empty
177 }
178}
179
180impl<T: Sidebar> SidebarHandle for Entity<T> {
181 fn width(&self, cx: &App) -> Pixels {
182 self.read(cx).width(cx)
183 }
184
185 fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
186 self.update(cx, |this, cx| this.set_width(width, cx))
187 }
188
189 fn focus_handle(&self, cx: &App) -> FocusHandle {
190 self.read(cx).focus_handle(cx)
191 }
192
193 fn focus(&self, window: &mut Window, cx: &mut App) {
194 let handle = self.read(cx).focus_handle(cx);
195 window.focus(&handle, cx);
196 }
197
198 fn prepare_for_focus(&self, window: &mut Window, cx: &mut App) {
199 self.update(cx, |this, cx| this.prepare_for_focus(window, cx));
200 }
201
202 fn has_notifications(&self, cx: &App) -> bool {
203 self.read(cx).has_notifications(cx)
204 }
205
206 fn to_any(&self) -> AnyView {
207 self.clone().into()
208 }
209
210 fn entity_id(&self) -> EntityId {
211 Entity::entity_id(self)
212 }
213
214 fn toggle_thread_switcher(&self, select_last: bool, window: &mut Window, cx: &mut App) {
215 let entity = self.clone();
216 window.defer(cx, move |window, cx| {
217 entity.update(cx, |this, cx| {
218 this.toggle_thread_switcher(select_last, window, cx);
219 });
220 });
221 }
222
223 fn cycle_project(&self, forward: bool, window: &mut Window, cx: &mut App) {
224 let entity = self.clone();
225 window.defer(cx, move |window, cx| {
226 entity.update(cx, |this, cx| {
227 this.cycle_project(forward, window, cx);
228 });
229 });
230 }
231
232 fn cycle_thread(&self, forward: bool, window: &mut Window, cx: &mut App) {
233 let entity = self.clone();
234 window.defer(cx, move |window, cx| {
235 entity.update(cx, |this, cx| {
236 this.cycle_thread(forward, window, cx);
237 });
238 });
239 }
240
241 fn is_threads_list_view_active(&self, cx: &App) -> bool {
242 self.read(cx).is_threads_list_view_active()
243 }
244
245 fn side(&self, cx: &App) -> SidebarSide {
246 self.read(cx).side(cx)
247 }
248
249 fn serialized_state(&self, cx: &App) -> Option<String> {
250 self.read(cx).serialized_state(cx)
251 }
252
253 fn restore_serialized_state(&self, state: &str, window: &mut Window, cx: &mut App) {
254 self.update(cx, |this, cx| {
255 this.restore_serialized_state(state, window, cx)
256 })
257 }
258}
259
260#[derive(Clone)]
261pub struct ProjectGroup {
262 pub id: ProjectGroupId,
263 pub key: ProjectGroupKey,
264 pub workspaces: Vec<Entity<Workspace>>,
265 pub expanded: bool,
266 pub visible_thread_count: Option<usize>,
267}
268
269pub struct MultiWorkspace {
270 window_id: WindowId,
271 project_groups: Vec<ProjectGroup>,
272 active_workspace: Entity<Workspace>,
273 sidebar: Option<Box<dyn SidebarHandle>>,
274 sidebar_open: bool,
275 sidebar_overlay: Option<AnyView>,
276 pending_removal_tasks: Vec<Task<()>>,
277 _serialize_task: Option<Task<()>>,
278 _subscriptions: Vec<Subscription>,
279 previous_focus_handle: Option<FocusHandle>,
280}
281
282impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
283
284impl MultiWorkspace {
285 pub fn sidebar_side(&self, cx: &App) -> SidebarSide {
286 self.sidebar
287 .as_ref()
288 .map_or(SidebarSide::Left, |s| s.side(cx))
289 }
290
291 pub fn sidebar_render_state(&self, cx: &App) -> SidebarRenderState {
292 SidebarRenderState {
293 open: self.sidebar_open() && self.multi_workspace_enabled(cx),
294 side: self.sidebar_side(cx),
295 }
296 }
297
298 pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
299 let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
300 if let Some(task) = this._serialize_task.take() {
301 task.detach();
302 }
303 for task in std::mem::take(&mut this.pending_removal_tasks) {
304 task.detach();
305 }
306 });
307 let quit_subscription = cx.on_app_quit(Self::app_will_quit);
308 let settings_subscription = cx.observe_global_in::<settings::SettingsStore>(window, {
309 let mut previous_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
310 move |this, window, cx| {
311 if DisableAiSettings::get_global(cx).disable_ai != previous_disable_ai {
312 this.collapse_to_single_workspace(window, cx);
313 previous_disable_ai = DisableAiSettings::get_global(cx).disable_ai;
314 }
315 }
316 });
317 Self::subscribe_to_workspace(&workspace, window, cx);
318 let weak_self = cx.weak_entity();
319 workspace.update(cx, |workspace, cx| {
320 workspace.set_multi_workspace(weak_self, cx);
321 });
322 Self {
323 window_id: window.window_handle().window_id(),
324 project_groups: Vec::new(),
325 active_workspace: workspace,
326 sidebar: None,
327 sidebar_open: false,
328 sidebar_overlay: None,
329 pending_removal_tasks: Vec::new(),
330 _serialize_task: None,
331 _subscriptions: vec![
332 release_subscription,
333 quit_subscription,
334 settings_subscription,
335 ],
336 previous_focus_handle: None,
337 }
338 }
339
340 pub fn register_sidebar<T: Sidebar>(&mut self, sidebar: Entity<T>, cx: &mut Context<Self>) {
341 self._subscriptions
342 .push(cx.observe(&sidebar, |_this, _, cx| {
343 cx.notify();
344 }));
345 self._subscriptions
346 .push(cx.subscribe(&sidebar, |this, _, event, cx| match event {
347 SidebarEvent::SerializeNeeded => {
348 this.serialize(cx);
349 }
350 }));
351 self.sidebar = Some(Box::new(sidebar));
352 }
353
354 pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
355 self.sidebar.as_deref()
356 }
357
358 pub fn set_sidebar_overlay(&mut self, overlay: Option<AnyView>, cx: &mut Context<Self>) {
359 self.sidebar_overlay = overlay;
360 cx.notify();
361 }
362
363 pub fn sidebar_open(&self) -> bool {
364 self.sidebar_open
365 }
366
367 pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
368 self.sidebar
369 .as_ref()
370 .map_or(false, |s| s.has_notifications(cx))
371 }
372
373 pub fn is_threads_list_view_active(&self, cx: &App) -> bool {
374 self.sidebar
375 .as_ref()
376 .map_or(false, |s| s.is_threads_list_view_active(cx))
377 }
378
379 pub fn multi_workspace_enabled(&self, cx: &App) -> bool {
380 !DisableAiSettings::get_global(cx).disable_ai
381 }
382
383 pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
384 if !self.multi_workspace_enabled(cx) {
385 return;
386 }
387
388 if self.sidebar_open() {
389 self.close_sidebar(window, cx);
390 } else {
391 self.previous_focus_handle = window.focused(cx);
392 self.open_sidebar(cx);
393 if let Some(sidebar) = &self.sidebar {
394 sidebar.prepare_for_focus(window, cx);
395 sidebar.focus(window, cx);
396 }
397 }
398 }
399
400 pub fn close_sidebar_action(&mut self, window: &mut Window, cx: &mut Context<Self>) {
401 if !self.multi_workspace_enabled(cx) {
402 return;
403 }
404
405 if self.sidebar_open() {
406 self.close_sidebar(window, cx);
407 }
408 }
409
410 pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
411 if !self.multi_workspace_enabled(cx) {
412 return;
413 }
414
415 if self.sidebar_open() {
416 let sidebar_is_focused = self
417 .sidebar
418 .as_ref()
419 .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
420
421 if sidebar_is_focused {
422 self.restore_previous_focus(false, window, cx);
423 } else {
424 self.previous_focus_handle = window.focused(cx);
425 if let Some(sidebar) = &self.sidebar {
426 sidebar.prepare_for_focus(window, cx);
427 sidebar.focus(window, cx);
428 }
429 }
430 } else {
431 self.previous_focus_handle = window.focused(cx);
432 self.open_sidebar(cx);
433 if let Some(sidebar) = &self.sidebar {
434 sidebar.prepare_for_focus(window, cx);
435 sidebar.focus(window, cx);
436 }
437 }
438 }
439
440 pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
441 self.sidebar_open = true;
442 self.retain_active_workspace(cx);
443 let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
444 for group in &self.project_groups {
445 for workspace in &group.workspaces {
446 workspace.update(cx, |workspace, _cx| {
447 workspace.set_sidebar_focus_handle(sidebar_focus_handle.clone());
448 });
449 }
450 }
451 self.serialize(cx);
452 cx.notify();
453 }
454
455 pub fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
456 self.sidebar_open = false;
457 for group in &self.project_groups {
458 for workspace in &group.workspaces {
459 workspace.update(cx, |workspace, _cx| {
460 workspace.set_sidebar_focus_handle(None);
461 });
462 }
463 }
464 let sidebar_has_focus = self
465 .sidebar
466 .as_ref()
467 .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
468 if sidebar_has_focus {
469 self.restore_previous_focus(true, window, cx);
470 } else {
471 self.previous_focus_handle.take();
472 }
473 self.serialize(cx);
474 cx.notify();
475 }
476
477 fn restore_previous_focus(&mut self, clear: bool, window: &mut Window, cx: &mut Context<Self>) {
478 let focus_handle = if clear {
479 self.previous_focus_handle.take()
480 } else {
481 self.previous_focus_handle.clone()
482 };
483
484 if let Some(previous_focus) = focus_handle {
485 previous_focus.focus(window, cx);
486 } else {
487 let pane = self.workspace().read(cx).active_pane().clone();
488 window.focus(&pane.read(cx).focus_handle(cx), cx);
489 }
490 }
491
492 pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
493 cx.spawn_in(window, async move |this, cx| {
494 let workspaces = this.update(cx, |multi_workspace, _cx| {
495 multi_workspace.workspaces().cloned().collect::<Vec<_>>()
496 })?;
497
498 for workspace in workspaces {
499 let should_continue = workspace
500 .update_in(cx, |workspace, window, cx| {
501 workspace.prepare_to_close(CloseIntent::CloseWindow, window, cx)
502 })?
503 .await?;
504 if !should_continue {
505 return anyhow::Ok(());
506 }
507 }
508
509 cx.update(|window, _cx| {
510 window.remove_window();
511 })?;
512
513 anyhow::Ok(())
514 })
515 .detach_and_log_err(cx);
516 }
517
518 fn subscribe_to_workspace(
519 workspace: &Entity<Workspace>,
520 window: &Window,
521 cx: &mut Context<Self>,
522 ) {
523 let project = workspace.read(cx).project().clone();
524 cx.subscribe_in(&project, window, {
525 let workspace = workspace.downgrade();
526 move |this, _project, event, _window, cx| match event {
527 project::Event::WorktreeAdded(_)
528 | project::Event::WorktreeRemoved(_)
529 | project::Event::WorktreeUpdatedRootRepoCommonDir(_) => {
530 if let Some(workspace) = workspace.upgrade() {
531 this.handle_workspace_key_change(&workspace, cx);
532 }
533 }
534 _ => {}
535 }
536 })
537 .detach();
538
539 cx.subscribe_in(workspace, window, |this, workspace, event, window, cx| {
540 if let WorkspaceEvent::Activate = event {
541 this.activate(workspace.clone(), window, cx);
542 }
543 })
544 .detach();
545 }
546
547 fn handle_workspace_key_change(
548 &mut self,
549 workspace: &Entity<Workspace>,
550 cx: &mut Context<Self>,
551 ) {
552 let new_key = workspace.read(cx).project_group_key(cx);
553
554 if new_key.path_list().paths().is_empty() {
555 return;
556 }
557
558 // Find which group this workspace is in
559 let group_idx = self
560 .project_groups
561 .iter()
562 .position(|g| g.workspaces.contains(workspace));
563
564 if let Some(idx) = group_idx {
565 let old_key = self.project_groups[idx].key.clone();
566 if old_key == new_key {
567 return;
568 }
569
570 let old_paths = old_key.path_list().paths().to_vec();
571 let new_paths = new_key.path_list().paths().to_vec();
572 let changed_root_paths = workspace.read(cx).root_paths(cx);
573 let workspace_id = workspace.entity_id();
574
575 // Remove true duplicates from the same group
576 let duplicates: Vec<Entity<Workspace>> = self.project_groups[idx]
577 .workspaces
578 .iter()
579 .filter(|ws| {
580 ws.entity_id() != workspace_id
581 && ws.read(cx).root_paths(cx) == changed_root_paths
582 })
583 .cloned()
584 .collect();
585
586 let active = self.active_workspace.clone();
587 if duplicates.contains(&active) {
588 // Active is a duplicate — remove the incoming workspace instead
589 self.project_groups[idx]
590 .workspaces
591 .retain(|w| w != workspace);
592 self.detach_workspace(workspace, cx);
593 } else {
594 for ws in &duplicates {
595 self.project_groups[idx].workspaces.retain(|w| w != ws);
596 self.detach_workspace(ws, cx);
597 }
598 }
599
600 // Propagate folder adds/removes to sibling workspaces in the same group
601 let siblings: Vec<Entity<Workspace>> = self.project_groups[idx]
602 .workspaces
603 .iter()
604 .filter(|ws| ws.entity_id() != workspace_id)
605 .cloned()
606 .collect();
607
608 for sibling in &siblings {
609 let project = sibling.read(cx).project().clone();
610
611 for added_path in new_paths.iter().filter(|p| !old_paths.contains(p)) {
612 project
613 .update(cx, |project, cx| {
614 project.find_or_create_worktree(added_path, true, cx)
615 })
616 .detach_and_log_err(cx);
617 }
618
619 for removed_path in old_paths.iter().filter(|p| !new_paths.contains(p)) {
620 project.update(cx, |project, cx| {
621 project.remove_worktree_for_main_worktree_path(removed_path, cx);
622 });
623 }
624 }
625
626 // Update the group's key in-place — the ID stays the same
627 self.project_groups[idx].key = new_key;
628 }
629
630 self.serialize(cx);
631 cx.notify();
632 }
633
634 pub fn project_group_key_for_workspace(
635 &self,
636 workspace: &Entity<Workspace>,
637 cx: &App,
638 ) -> ProjectGroupKey {
639 self.group_for_workspace(workspace)
640 .map(|g| g.key.clone())
641 .unwrap_or_else(|| workspace.read(cx).project_group_key(cx))
642 }
643
644 pub fn restore_project_groups(&mut self, groups: Vec<(ProjectGroupId, ProjectGroupKey)>) {
645 let mut restored: Vec<ProjectGroup> = Vec::new();
646 for (id, key) in groups {
647 if key.path_list().paths().is_empty() {
648 continue;
649 }
650 if restored.iter().any(|g| g.id == id) {
651 continue;
652 }
653 restored.push(ProjectGroup {
654 id,
655 key,
656 workspaces: Vec::new(),
657 expanded: true,
658 visible_thread_count: None,
659 });
660 }
661 for existing in &self.project_groups {
662 if !restored.iter().any(|g| g.id == existing.id) {
663 restored.push(existing.clone());
664 }
665 }
666 self.project_groups = restored;
667 }
668
669 pub fn project_group_keys(&self) -> impl Iterator<Item = &ProjectGroupKey> {
670 self.project_groups.iter().map(|g| &g.key)
671 }
672
673 pub fn project_groups(&self) -> &[ProjectGroup] {
674 &self.project_groups
675 }
676
677 pub fn group(&self, id: ProjectGroupId) -> Option<&ProjectGroup> {
678 self.project_groups.iter().find(|g| g.id == id)
679 }
680
681 pub fn group_mut(&mut self, id: ProjectGroupId) -> Option<&mut ProjectGroup> {
682 self.project_groups.iter_mut().find(|g| g.id == id)
683 }
684
685 pub fn group_for_workspace(&self, workspace: &Entity<Workspace>) -> Option<&ProjectGroup> {
686 self.project_groups
687 .iter()
688 .find(|g| g.workspaces.contains(workspace))
689 }
690
691 pub(crate) fn ensure_workspace_in_group(
692 &mut self,
693 workspace: Entity<Workspace>,
694 key: ProjectGroupKey,
695 cx: &mut Context<Self>,
696 ) {
697 if let Some(group) = self.project_groups.iter_mut().find(|g| g.key == key) {
698 if !group.workspaces.contains(&workspace) {
699 group.workspaces.push(workspace.clone());
700 cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
701 }
702 return;
703 }
704 let group = ProjectGroup {
705 id: ProjectGroupId::new(),
706 key,
707 expanded: true,
708 visible_thread_count: None,
709 workspaces: vec![workspace.clone()],
710 };
711 self.project_groups.insert(0, group);
712 cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
713 }
714
715 pub fn workspaces_for_project_group(&self, id: ProjectGroupId) -> Option<&[Entity<Workspace>]> {
716 self.group(id).map(|g| g.workspaces.as_slice())
717 }
718
719 pub fn remove_folder_from_project_group(
720 &mut self,
721 group_id: ProjectGroupId,
722 path: &Path,
723 cx: &mut Context<Self>,
724 ) {
725 let Some(group) = self.group_mut(group_id) else {
726 return;
727 };
728
729 let new_path_list = group.key.path_list().without_path(path);
730 if new_path_list.is_empty() {
731 return;
732 }
733
734 group.key = ProjectGroupKey::new(group.key.host(), new_path_list);
735 let workspaces: Vec<_> = group.workspaces.clone();
736
737 for workspace in workspaces {
738 let project = workspace.read(cx).project().clone();
739 project.update(cx, |project, cx| {
740 project.remove_worktree_for_main_worktree_path(path, cx);
741 });
742 }
743
744 self.serialize(cx);
745 cx.notify();
746 }
747
748 pub fn prompt_to_add_folders_to_project_group(
749 &mut self,
750 group_id: ProjectGroupId,
751 window: &mut Window,
752 cx: &mut Context<Self>,
753 ) {
754 let paths = self.workspace().update(cx, |workspace, cx| {
755 workspace.prompt_for_open_path(
756 PathPromptOptions {
757 files: false,
758 directories: true,
759 multiple: true,
760 prompt: None,
761 },
762 DirectoryLister::Project(workspace.project().clone()),
763 window,
764 cx,
765 )
766 });
767
768 cx.spawn_in(window, async move |this, cx| {
769 if let Some(new_paths) = paths.await.ok().flatten() {
770 if !new_paths.is_empty() {
771 this.update(cx, |multi_workspace, cx| {
772 multi_workspace.add_folders_to_project_group(group_id, new_paths, cx);
773 })?;
774 }
775 }
776 anyhow::Ok(())
777 })
778 .detach_and_log_err(cx);
779 }
780
781 pub fn add_folders_to_project_group(
782 &mut self,
783 group_id: ProjectGroupId,
784 new_paths: Vec<PathBuf>,
785 cx: &mut Context<Self>,
786 ) {
787 let Some(group) = self.group_mut(group_id) else {
788 return;
789 };
790
791 let mut all_paths: Vec<PathBuf> = group.key.path_list().paths().to_vec();
792 all_paths.extend(new_paths.iter().cloned());
793 let new_path_list = PathList::new(&all_paths);
794 group.key = ProjectGroupKey::new(group.key.host(), new_path_list);
795 let workspaces: Vec<_> = group.workspaces.clone();
796
797 for workspace in workspaces {
798 let project = workspace.read(cx).project().clone();
799 for path in &new_paths {
800 project
801 .update(cx, |project, cx| {
802 project.find_or_create_worktree(path, true, cx)
803 })
804 .detach_and_log_err(cx);
805 }
806 }
807
808 self.serialize(cx);
809 cx.notify();
810 }
811
812 pub fn remove_project_group(
813 &mut self,
814 group_id: ProjectGroupId,
815 window: &mut Window,
816 cx: &mut Context<Self>,
817 ) -> Task<Result<bool>> {
818 let pos = self.project_groups.iter().position(|g| g.id == group_id);
819 let workspaces: Vec<_> = pos
820 .map(|p| self.project_groups[p].workspaces.clone())
821 .unwrap_or_default();
822
823 // Compute the neighbor while the group is still in the list.
824 let neighbor_key = pos.and_then(|pos| {
825 self.project_groups
826 .get(pos + 1)
827 .or_else(|| pos.checked_sub(1).and_then(|i| self.project_groups.get(i)))
828 .map(|g| g.key.clone())
829 });
830
831 // Now remove the group.
832 self.project_groups.retain(|g| g.id != group_id);
833
834 self.remove(
835 workspaces,
836 move |this, window, cx| {
837 if let Some(neighbor_key) = neighbor_key {
838 return this.find_or_create_local_workspace(
839 neighbor_key.path_list().clone(),
840 window,
841 cx,
842 );
843 }
844
845 // No other project groups remain — create an empty workspace.
846 let app_state = this.workspace().read(cx).app_state().clone();
847 let project = Project::local(
848 app_state.client.clone(),
849 app_state.node_runtime.clone(),
850 app_state.user_store.clone(),
851 app_state.languages.clone(),
852 app_state.fs.clone(),
853 None,
854 project::LocalProjectFlags::default(),
855 cx,
856 );
857 let new_workspace =
858 cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
859 Task::ready(Ok(new_workspace))
860 },
861 window,
862 cx,
863 )
864 }
865
866 /// Finds an existing workspace whose root paths and host exactly match.
867 pub fn workspace_for_paths(
868 &self,
869 path_list: &PathList,
870 host: Option<&RemoteConnectionOptions>,
871 cx: &App,
872 ) -> Option<Entity<Workspace>> {
873 self.project_groups
874 .iter()
875 .flat_map(|g| &g.workspaces)
876 .find(|ws| {
877 let key = ws.read(cx).project_group_key(cx);
878 key.host().as_ref() == host
879 && PathList::new(&ws.read(cx).root_paths(cx)) == *path_list
880 })
881 .cloned()
882 }
883
884 /// Finds an existing workspace whose paths match, or creates a new one.
885 ///
886 /// For local projects (`host` is `None`), this delegates to
887 /// [`Self::find_or_create_local_workspace`]. For remote projects, it
888 /// tries an exact path match and, if no existing workspace is found,
889 /// calls `connect_remote` to establish a connection and creates a new
890 /// remote workspace.
891 ///
892 /// The `connect_remote` closure is responsible for any user-facing
893 /// connection UI (e.g. password prompts). It receives the connection
894 /// options and should return a [`Task`] that resolves to the
895 /// [`RemoteClient`] session, or `None` if the connection was
896 /// cancelled.
897 pub fn find_or_create_workspace(
898 &mut self,
899 paths: PathList,
900 host: Option<RemoteConnectionOptions>,
901 provisional_project_group_key: Option<ProjectGroupKey>,
902 connect_remote: impl FnOnce(
903 RemoteConnectionOptions,
904 &mut Window,
905 &mut Context<Self>,
906 ) -> Task<Result<Option<Entity<remote::RemoteClient>>>>
907 + 'static,
908 window: &mut Window,
909 cx: &mut Context<Self>,
910 ) -> Task<Result<Entity<Workspace>>> {
911 if let Some(workspace) = self.workspace_for_paths(&paths, host.as_ref(), cx) {
912 self.activate(workspace.clone(), window, cx);
913 return Task::ready(Ok(workspace));
914 }
915
916 let Some(connection_options) = host else {
917 return self.find_or_create_local_workspace(paths, window, cx);
918 };
919
920 let app_state = self.workspace().read(cx).app_state().clone();
921 let window_handle = window.window_handle().downcast::<MultiWorkspace>();
922 let connect_task = connect_remote(connection_options.clone(), window, cx);
923 let paths_vec = paths.paths().to_vec();
924
925 cx.spawn(async move |_this, cx| {
926 let session = connect_task
927 .await?
928 .ok_or_else(|| anyhow::anyhow!("Remote connection was cancelled"))?;
929
930 let new_project = cx.update(|cx| {
931 Project::remote(
932 session,
933 app_state.client.clone(),
934 app_state.node_runtime.clone(),
935 app_state.user_store.clone(),
936 app_state.languages.clone(),
937 app_state.fs.clone(),
938 true,
939 cx,
940 )
941 });
942
943 let window_handle =
944 window_handle.ok_or_else(|| anyhow::anyhow!("Window is not a MultiWorkspace"))?;
945
946 open_remote_project_with_existing_connection(
947 connection_options,
948 new_project,
949 paths_vec,
950 app_state,
951 window_handle,
952 provisional_project_group_key,
953 cx,
954 )
955 .await?;
956
957 window_handle.update(cx, |multi_workspace, window, cx| {
958 let workspace = multi_workspace.workspace().clone();
959 multi_workspace.add(workspace.clone(), window, cx);
960 workspace
961 })
962 })
963 }
964
965 /// Finds an existing workspace in this multi-workspace whose paths match,
966 /// or creates a new one (deserializing its saved state from the database).
967 /// Never searches other windows or matches workspaces with a superset of
968 /// the requested paths.
969 pub fn find_or_create_local_workspace(
970 &mut self,
971 path_list: PathList,
972 window: &mut Window,
973 cx: &mut Context<Self>,
974 ) -> Task<Result<Entity<Workspace>>> {
975 if let Some(workspace) = self.workspace_for_paths(&path_list, None, cx) {
976 self.activate(workspace.clone(), window, cx);
977 return Task::ready(Ok(workspace));
978 }
979
980 let paths = path_list.paths().to_vec();
981 let app_state = self.workspace().read(cx).app_state().clone();
982 let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
983
984 cx.spawn(async move |_this, cx| {
985 let result = cx
986 .update(|cx| {
987 Workspace::new_local(
988 paths,
989 app_state,
990 requesting_window,
991 None,
992 None,
993 OpenMode::Activate,
994 cx,
995 )
996 })
997 .await?;
998 Ok(result.workspace)
999 })
1000 }
1001
1002 pub fn workspace(&self) -> &Entity<Workspace> {
1003 &self.active_workspace
1004 }
1005
1006 pub fn workspaces(&self) -> impl Iterator<Item = &Entity<Workspace>> {
1007 let grouped = self.project_groups.iter().flat_map(|g| &g.workspaces);
1008 let active = std::iter::once(&self.active_workspace);
1009 let mut seen = HashSet::new();
1010 grouped
1011 .chain(active)
1012 .filter(move |ws| seen.insert(ws.entity_id()))
1013 }
1014
1015 /// Adds a workspace to this window as persistent without changing which
1016 /// workspace is active. Unlike `activate()`, this always inserts into the
1017 /// persistent list regardless of sidebar state — it's used for system-
1018 /// initiated additions like deserialization and worktree discovery.
1019 pub fn add(&mut self, workspace: Entity<Workspace>, window: &Window, cx: &mut Context<Self>) {
1020 if self.group_for_workspace(&workspace).is_some() {
1021 return;
1022 }
1023 let key = workspace.read(cx).project_group_key(cx);
1024 Self::subscribe_to_workspace(&workspace, window, cx);
1025 self.sync_sidebar_to_workspace(&workspace, cx);
1026 let weak_self = cx.weak_entity();
1027 workspace.update(cx, |ws, cx| ws.set_multi_workspace(weak_self, cx));
1028 self.ensure_workspace_in_group(workspace, key, cx);
1029 cx.notify();
1030 }
1031
1032 /// Ensures the workspace is in the multiworkspace and makes it the active one.
1033 pub fn activate(
1034 &mut self,
1035 workspace: Entity<Workspace>,
1036 window: &mut Window,
1037 cx: &mut Context<Self>,
1038 ) {
1039 if self.workspace() == &workspace {
1040 self.focus_active_workspace(window, cx);
1041 return;
1042 }
1043
1044 // If the workspace isn't in any group yet, subscribe and optionally group it
1045 if self.group_for_workspace(&workspace).is_none() {
1046 Self::subscribe_to_workspace(&workspace, window, cx);
1047 self.sync_sidebar_to_workspace(&workspace, cx);
1048 let weak_self = cx.weak_entity();
1049 workspace.update(cx, |ws, cx| ws.set_multi_workspace(weak_self, cx));
1050
1051 if self.sidebar_open {
1052 let key = workspace.read(cx).project_group_key(cx);
1053 self.ensure_workspace_in_group(workspace.clone(), key, cx);
1054 }
1055 }
1056
1057 self.active_workspace = workspace;
1058 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
1059 self.serialize(cx);
1060 self.focus_active_workspace(window, cx);
1061 cx.notify();
1062 }
1063
1064 /// Promotes the currently active workspace to persistent if it is
1065 /// transient, so it is retained across workspace switches even when
1066 /// the sidebar is closed. No-op if the workspace is already persistent.
1067 pub fn retain_active_workspace(&mut self, cx: &mut Context<Self>) {
1068 let workspace = self.active_workspace.clone();
1069 if self.group_for_workspace(&workspace).is_none() {
1070 let key = workspace.read(cx).project_group_key(cx);
1071 self.ensure_workspace_in_group(workspace, key, cx);
1072 self.serialize(cx);
1073 cx.notify();
1074 }
1075 }
1076
1077 /// Collapses to a single workspace, discarding all groups.
1078 /// Used when multi-workspace is disabled (e.g. disable_ai).
1079 fn collapse_to_single_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1080 if self.sidebar_open {
1081 self.close_sidebar(window, cx);
1082 }
1083 let active = self.active_workspace.clone();
1084 for group in std::mem::take(&mut self.project_groups) {
1085 for workspace in group.workspaces {
1086 if workspace != active {
1087 self.detach_workspace(&workspace, cx);
1088 }
1089 }
1090 }
1091 cx.notify();
1092 }
1093
1094 /// Detaches a workspace: clears session state, DB binding, cached
1095 /// group key, and emits `WorkspaceRemoved`. The DB row is preserved
1096 /// so the workspace still appears in the recent-projects list.
1097 fn detach_workspace(&mut self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
1098 // Remove workspace from its group
1099 for group in &mut self.project_groups {
1100 group.workspaces.retain(|w| w != workspace);
1101 }
1102 // Remove empty groups
1103 self.project_groups.retain(|g| !g.workspaces.is_empty());
1104 cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(workspace.entity_id()));
1105 workspace.update(cx, |workspace, _cx| {
1106 workspace.session_id.take();
1107 workspace._schedule_serialize_workspace.take();
1108 workspace._serialize_workspace_task.take();
1109 });
1110
1111 if let Some(workspace_id) = workspace.read(cx).database_id() {
1112 let db = crate::persistence::WorkspaceDb::global(cx);
1113 self.pending_removal_tasks.retain(|task| !task.is_ready());
1114 self.pending_removal_tasks
1115 .push(cx.background_spawn(async move {
1116 db.set_session_binding(workspace_id, None, None)
1117 .await
1118 .log_err();
1119 }));
1120 }
1121 }
1122
1123 fn sync_sidebar_to_workspace(&self, workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
1124 if self.sidebar_open() {
1125 let sidebar_focus_handle = self.sidebar.as_ref().map(|s| s.focus_handle(cx));
1126 workspace.update(cx, |workspace, _| {
1127 workspace.set_sidebar_focus_handle(sidebar_focus_handle);
1128 });
1129 }
1130 }
1131
1132 pub(crate) fn serialize(&mut self, cx: &mut Context<Self>) {
1133 self._serialize_task = Some(cx.spawn(async move |this, cx| {
1134 let Some((window_id, state)) = this
1135 .read_with(cx, |this, cx| {
1136 let state = MultiWorkspaceState {
1137 active_workspace_id: this.workspace().read(cx).database_id(),
1138 project_group_keys: this
1139 .project_groups
1140 .iter()
1141 .map(|g| {
1142 crate::persistence::model::SerializedProjectGroup::from_group(
1143 g.id, &g.key,
1144 )
1145 })
1146 .collect::<Vec<_>>(),
1147 sidebar_open: this.sidebar_open,
1148 sidebar_state: this.sidebar.as_ref().and_then(|s| s.serialized_state(cx)),
1149 };
1150 (this.window_id, state)
1151 })
1152 .ok()
1153 else {
1154 return;
1155 };
1156 let kvp = cx.update(|cx| db::kvp::KeyValueStore::global(cx));
1157 crate::persistence::write_multi_workspace_state(&kvp, window_id, state).await;
1158 }));
1159 }
1160
1161 /// Returns the in-flight serialization task (if any) so the caller can
1162 /// await it. Used by the quit handler to ensure pending DB writes
1163 /// complete before the process exits.
1164 pub fn flush_serialization(&mut self) -> Task<()> {
1165 self._serialize_task.take().unwrap_or(Task::ready(()))
1166 }
1167
1168 fn app_will_quit(&mut self, _cx: &mut Context<Self>) -> impl Future<Output = ()> + use<> {
1169 let mut tasks: Vec<Task<()>> = Vec::new();
1170 if let Some(task) = self._serialize_task.take() {
1171 tasks.push(task);
1172 }
1173 tasks.extend(std::mem::take(&mut self.pending_removal_tasks));
1174
1175 async move {
1176 futures::future::join_all(tasks).await;
1177 }
1178 }
1179
1180 pub fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
1181 // If a dock panel is zoomed, focus it instead of the center pane.
1182 // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
1183 // which closes the zoomed dock.
1184 let focus_handle = {
1185 let workspace = self.workspace().read(cx);
1186 let mut target = None;
1187 for dock in workspace.all_docks() {
1188 let dock = dock.read(cx);
1189 if dock.is_open() {
1190 if let Some(panel) = dock.active_panel() {
1191 if panel.is_zoomed(window, cx) {
1192 target = Some(panel.panel_focus_handle(cx));
1193 break;
1194 }
1195 }
1196 }
1197 }
1198 target.unwrap_or_else(|| {
1199 let pane = workspace.active_pane().clone();
1200 pane.read(cx).focus_handle(cx)
1201 })
1202 };
1203 window.focus(&focus_handle, cx);
1204 }
1205
1206 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
1207 self.workspace().read(cx).panel::<T>(cx)
1208 }
1209
1210 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
1211 self.workspace().read(cx).active_modal::<V>(cx)
1212 }
1213
1214 pub fn add_panel<T: Panel>(
1215 &mut self,
1216 panel: Entity<T>,
1217 window: &mut Window,
1218 cx: &mut Context<Self>,
1219 ) {
1220 self.workspace().update(cx, |workspace, cx| {
1221 workspace.add_panel(panel, window, cx);
1222 });
1223 }
1224
1225 pub fn focus_panel<T: Panel>(
1226 &mut self,
1227 window: &mut Window,
1228 cx: &mut Context<Self>,
1229 ) -> Option<Entity<T>> {
1230 self.workspace()
1231 .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
1232 }
1233
1234 // used in a test
1235 pub fn toggle_modal<V: ModalView, B>(
1236 &mut self,
1237 window: &mut Window,
1238 cx: &mut Context<Self>,
1239 build: B,
1240 ) where
1241 B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
1242 {
1243 self.workspace().update(cx, |workspace, cx| {
1244 workspace.toggle_modal(window, cx, build);
1245 });
1246 }
1247
1248 pub fn toggle_dock(
1249 &mut self,
1250 dock_side: DockPosition,
1251 window: &mut Window,
1252 cx: &mut Context<Self>,
1253 ) {
1254 self.workspace().update(cx, |workspace, cx| {
1255 workspace.toggle_dock(dock_side, window, cx);
1256 });
1257 }
1258
1259 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
1260 self.workspace().read(cx).active_item_as::<I>(cx)
1261 }
1262
1263 pub fn items_of_type<'a, T: Item>(
1264 &'a self,
1265 cx: &'a App,
1266 ) -> impl 'a + Iterator<Item = Entity<T>> {
1267 self.workspace().read(cx).items_of_type::<T>(cx)
1268 }
1269
1270 pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
1271 self.workspace().read(cx).database_id()
1272 }
1273
1274 pub fn take_pending_removal_tasks(&mut self) -> Vec<Task<()>> {
1275 let tasks: Vec<Task<()>> = std::mem::take(&mut self.pending_removal_tasks)
1276 .into_iter()
1277 .filter(|task| !task.is_ready())
1278 .collect();
1279 tasks
1280 }
1281
1282 #[cfg(any(test, feature = "test-support"))]
1283 pub fn assert_project_group_key_integrity(&self, cx: &App) -> anyhow::Result<()> {
1284 for group in &self.project_groups {
1285 for workspace in &group.workspaces {
1286 let live_key = workspace.read(cx).project_group_key(cx);
1287 anyhow::ensure!(
1288 group.key == live_key,
1289 "workspace {:?} has live key {:?} but group key {:?}",
1290 workspace.entity_id(),
1291 live_key,
1292 group.key,
1293 );
1294 }
1295 }
1296 Ok(())
1297 }
1298
1299 #[cfg(any(test, feature = "test-support"))]
1300 pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
1301 self.workspace().update(cx, |workspace, _cx| {
1302 workspace.set_random_database_id();
1303 });
1304 }
1305
1306 #[cfg(any(test, feature = "test-support"))]
1307 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
1308 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1309 Self::new(workspace, window, cx)
1310 }
1311
1312 #[cfg(any(test, feature = "test-support"))]
1313 pub fn test_add_workspace(
1314 &mut self,
1315 project: Entity<Project>,
1316 window: &mut Window,
1317 cx: &mut Context<Self>,
1318 ) -> Entity<Workspace> {
1319 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
1320 self.activate(workspace.clone(), window, cx);
1321 workspace
1322 }
1323
1324 #[cfg(any(test, feature = "test-support"))]
1325 pub fn create_test_workspace(
1326 &mut self,
1327 window: &mut Window,
1328 cx: &mut Context<Self>,
1329 ) -> Task<()> {
1330 let app_state = self.workspace().read(cx).app_state().clone();
1331 let project = Project::local(
1332 app_state.client.clone(),
1333 app_state.node_runtime.clone(),
1334 app_state.user_store.clone(),
1335 app_state.languages.clone(),
1336 app_state.fs.clone(),
1337 None,
1338 project::LocalProjectFlags::default(),
1339 cx,
1340 );
1341 let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
1342 self.activate(new_workspace.clone(), window, cx);
1343
1344 let weak_workspace = new_workspace.downgrade();
1345 let db = crate::persistence::WorkspaceDb::global(cx);
1346 cx.spawn_in(window, async move |this, cx| {
1347 let workspace_id = db.next_id().await.unwrap();
1348 let workspace = weak_workspace.upgrade().unwrap();
1349 let task: Task<()> = this
1350 .update_in(cx, |this, window, cx| {
1351 let session_id = workspace.read(cx).session_id();
1352 let window_id = window.window_handle().window_id().as_u64();
1353 workspace.update(cx, |workspace, _cx| {
1354 workspace.set_database_id(workspace_id);
1355 });
1356 this.serialize(cx);
1357 let db = db.clone();
1358 cx.background_spawn(async move {
1359 db.set_session_binding(workspace_id, session_id, Some(window_id))
1360 .await
1361 .log_err();
1362 })
1363 })
1364 .unwrap();
1365 task.await
1366 })
1367 }
1368
1369 /// Removes one or more workspaces from this multi-workspace.
1370 ///
1371 /// If the active workspace is among those being removed,
1372 /// `fallback_workspace` is called **synchronously before the removal
1373 /// begins** to produce a `Task` that resolves to the workspace that
1374 /// should become active. The fallback must not be one of the
1375 /// workspaces being removed.
1376 ///
1377 /// Returns `true` if any workspaces were actually removed.
1378 pub fn remove(
1379 &mut self,
1380 workspaces: impl IntoIterator<Item = Entity<Workspace>>,
1381 fallback_workspace: impl FnOnce(
1382 &mut Self,
1383 &mut Window,
1384 &mut Context<Self>,
1385 ) -> Task<Result<Entity<Workspace>>>,
1386 window: &mut Window,
1387 cx: &mut Context<Self>,
1388 ) -> Task<Result<bool>> {
1389 let workspaces: Vec<_> = workspaces.into_iter().collect();
1390
1391 if workspaces.is_empty() {
1392 return Task::ready(Ok(false));
1393 }
1394
1395 let removing_active = workspaces.iter().any(|ws| ws == self.workspace());
1396 let original_active = self.workspace().clone();
1397
1398 let fallback_task = removing_active.then(|| fallback_workspace(self, window, cx));
1399
1400 cx.spawn_in(window, async move |this, cx| {
1401 // Prompt each workspace for unsaved changes. If any workspace
1402 // has dirty buffers, save_all_internal will emit Activate to
1403 // bring it into view before showing the save dialog.
1404 for workspace in &workspaces {
1405 let should_continue = workspace
1406 .update_in(cx, |workspace, window, cx| {
1407 workspace.save_all_internal(crate::SaveIntent::Close, window, cx)
1408 })?
1409 .await?;
1410
1411 if !should_continue {
1412 return Ok(false);
1413 }
1414 }
1415
1416 // If we're removing the active workspace, await the
1417 // fallback and switch to it before tearing anything down.
1418 // Otherwise restore the original active workspace in case
1419 // prompting switched away from it.
1420 if let Some(fallback_task) = fallback_task {
1421 let new_active = fallback_task.await?;
1422
1423 this.update_in(cx, |this, window, cx| {
1424 assert!(
1425 !workspaces.contains(&new_active),
1426 "fallback workspace must not be one of the workspaces being removed"
1427 );
1428 this.activate(new_active, window, cx);
1429 })?;
1430 } else {
1431 this.update_in(cx, |this, window, cx| {
1432 if *this.workspace() != original_active {
1433 this.activate(original_active, window, cx);
1434 }
1435 })?;
1436 }
1437
1438 // Actually remove the workspaces.
1439 this.update_in(cx, |this, _, cx| {
1440 let mut removed_any = false;
1441
1442 for workspace in &workspaces {
1443 // detach_workspace already removes from groups
1444 let was_in_group = this.group_for_workspace(workspace).is_some();
1445 if was_in_group {
1446 this.detach_workspace(workspace, cx);
1447 removed_any = true;
1448 }
1449 }
1450
1451 if removed_any {
1452 this.serialize(cx);
1453 cx.notify();
1454 }
1455
1456 Ok(removed_any)
1457 })?
1458 })
1459 }
1460
1461 pub fn open_project(
1462 &mut self,
1463 paths: Vec<PathBuf>,
1464 open_mode: OpenMode,
1465 window: &mut Window,
1466 cx: &mut Context<Self>,
1467 ) -> Task<Result<Entity<Workspace>>> {
1468 if self.multi_workspace_enabled(cx) {
1469 self.find_or_create_local_workspace(PathList::new(&paths), window, cx)
1470 } else {
1471 let workspace = self.workspace().clone();
1472 cx.spawn_in(window, async move |_this, cx| {
1473 let should_continue = workspace
1474 .update_in(cx, |workspace, window, cx| {
1475 workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
1476 })?
1477 .await?;
1478 if should_continue {
1479 workspace
1480 .update_in(cx, |workspace, window, cx| {
1481 workspace.open_workspace_for_paths(open_mode, paths, window, cx)
1482 })?
1483 .await
1484 } else {
1485 Ok(workspace)
1486 }
1487 })
1488 }
1489 }
1490}
1491
1492impl Render for MultiWorkspace {
1493 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1494 let multi_workspace_enabled = self.multi_workspace_enabled(cx);
1495 let sidebar_side = self.sidebar_side(cx);
1496 let sidebar_on_right = sidebar_side == SidebarSide::Right;
1497
1498 let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open() {
1499 self.sidebar.as_ref().map(|sidebar_handle| {
1500 let weak = cx.weak_entity();
1501
1502 let sidebar_width = sidebar_handle.width(cx);
1503 let resize_handle = deferred(
1504 div()
1505 .id("sidebar-resize-handle")
1506 .absolute()
1507 .when(!sidebar_on_right, |el| {
1508 el.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
1509 })
1510 .when(sidebar_on_right, |el| {
1511 el.left(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
1512 })
1513 .top(px(0.))
1514 .h_full()
1515 .w(SIDEBAR_RESIZE_HANDLE_SIZE)
1516 .cursor_col_resize()
1517 .on_drag(DraggedSidebar, |dragged, _, _, cx| {
1518 cx.stop_propagation();
1519 cx.new(|_| dragged.clone())
1520 })
1521 .on_mouse_down(MouseButton::Left, |_, _, cx| {
1522 cx.stop_propagation();
1523 })
1524 .on_mouse_up(MouseButton::Left, move |event, _, cx| {
1525 if event.click_count == 2 {
1526 weak.update(cx, |this, cx| {
1527 if let Some(sidebar) = this.sidebar.as_mut() {
1528 sidebar.set_width(None, cx);
1529 }
1530 this.serialize(cx);
1531 })
1532 .ok();
1533 cx.stop_propagation();
1534 } else {
1535 weak.update(cx, |this, cx| {
1536 this.serialize(cx);
1537 })
1538 .ok();
1539 }
1540 })
1541 .occlude(),
1542 );
1543
1544 div()
1545 .id("sidebar-container")
1546 .relative()
1547 .h_full()
1548 .w(sidebar_width)
1549 .flex_shrink_0()
1550 .child(sidebar_handle.to_any())
1551 .child(resize_handle)
1552 .into_any_element()
1553 })
1554 } else {
1555 None
1556 };
1557
1558 let (left_sidebar, right_sidebar) = if sidebar_on_right {
1559 (None, sidebar)
1560 } else {
1561 (sidebar, None)
1562 };
1563
1564 let ui_font = theme_settings::setup_ui_font(window, cx);
1565 let text_color = cx.theme().colors().text;
1566
1567 let workspace = self.workspace().clone();
1568 let workspace_key_context = workspace.update(cx, |workspace, cx| workspace.key_context(cx));
1569 let root = workspace.update(cx, |workspace, cx| workspace.actions(h_flex(), window, cx));
1570
1571 client_side_decorations(
1572 root.key_context(workspace_key_context)
1573 .relative()
1574 .size_full()
1575 .font(ui_font)
1576 .text_color(text_color)
1577 .on_action(cx.listener(Self::close_window))
1578 .when(self.multi_workspace_enabled(cx), |this| {
1579 this.on_action(cx.listener(
1580 |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
1581 this.toggle_sidebar(window, cx);
1582 },
1583 ))
1584 .on_action(cx.listener(
1585 |this: &mut Self, _: &CloseWorkspaceSidebar, window, cx| {
1586 this.close_sidebar_action(window, cx);
1587 },
1588 ))
1589 .on_action(cx.listener(
1590 |this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
1591 this.focus_sidebar(window, cx);
1592 },
1593 ))
1594 .on_action(cx.listener(
1595 |this: &mut Self, action: &ToggleThreadSwitcher, window, cx| {
1596 if let Some(sidebar) = &this.sidebar {
1597 sidebar.toggle_thread_switcher(action.select_last, window, cx);
1598 }
1599 },
1600 ))
1601 .on_action(cx.listener(|this: &mut Self, _: &NextProject, window, cx| {
1602 if let Some(sidebar) = &this.sidebar {
1603 sidebar.cycle_project(true, window, cx);
1604 }
1605 }))
1606 .on_action(
1607 cx.listener(|this: &mut Self, _: &PreviousProject, window, cx| {
1608 if let Some(sidebar) = &this.sidebar {
1609 sidebar.cycle_project(false, window, cx);
1610 }
1611 }),
1612 )
1613 .on_action(cx.listener(|this: &mut Self, _: &NextThread, window, cx| {
1614 if let Some(sidebar) = &this.sidebar {
1615 sidebar.cycle_thread(true, window, cx);
1616 }
1617 }))
1618 .on_action(cx.listener(
1619 |this: &mut Self, _: &PreviousThread, window, cx| {
1620 if let Some(sidebar) = &this.sidebar {
1621 sidebar.cycle_thread(false, window, cx);
1622 }
1623 },
1624 ))
1625 })
1626 .when(
1627 self.sidebar_open() && self.multi_workspace_enabled(cx),
1628 |this| {
1629 this.on_drag_move(cx.listener(
1630 move |this: &mut Self,
1631 e: &DragMoveEvent<DraggedSidebar>,
1632 window,
1633 cx| {
1634 if let Some(sidebar) = &this.sidebar {
1635 let new_width = if sidebar_on_right {
1636 window.bounds().size.width - e.event.position.x
1637 } else {
1638 e.event.position.x
1639 };
1640 sidebar.set_width(Some(new_width), cx);
1641 }
1642 },
1643 ))
1644 },
1645 )
1646 .children(left_sidebar)
1647 .child(
1648 div()
1649 .flex()
1650 .flex_1()
1651 .size_full()
1652 .overflow_hidden()
1653 .child(self.workspace().clone()),
1654 )
1655 .children(right_sidebar)
1656 .child(self.workspace().read(cx).modal_layer.clone())
1657 .children(self.sidebar_overlay.as_ref().map(|view| {
1658 deferred(div().absolute().size_full().inset_0().occlude().child(
1659 v_flex().h(px(0.0)).top_20().items_center().child(
1660 h_flex().occlude().child(view.clone()).on_mouse_down(
1661 MouseButton::Left,
1662 |_, _, cx| {
1663 cx.stop_propagation();
1664 },
1665 ),
1666 ),
1667 ))
1668 .with_priority(2)
1669 })),
1670 window,
1671 cx,
1672 Tiling {
1673 left: !sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
1674 right: sidebar_on_right && multi_workspace_enabled && self.sidebar_open(),
1675 ..Tiling::default()
1676 },
1677 )
1678 }
1679}