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