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