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