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