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