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