1use anyhow::Result;
2use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
3use gpui::{
4 AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
5 ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId,
6 actions, deferred, px,
7};
8use project::{DisableAiSettings, Project};
9use settings::Settings;
10use std::future::Future;
11use std::path::PathBuf;
12use ui::prelude::*;
13use util::ResultExt;
14
15const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
16
17use crate::{
18 CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, Panel, Toast,
19 Workspace, WorkspaceId, client_side_decorations, notifications::NotificationId,
20};
21
22actions!(
23 multi_workspace,
24 [
25 /// Creates a new workspace within the current window.
26 NewWorkspaceInWindow,
27 /// Switches to the next workspace within the current window.
28 NextWorkspaceInWindow,
29 /// Switches to the previous workspace within the current window.
30 PreviousWorkspaceInWindow,
31 /// Toggles the workspace switcher sidebar.
32 ToggleWorkspaceSidebar,
33 /// Moves focus to or from the workspace sidebar without closing it.
34 FocusWorkspaceSidebar,
35 ]
36);
37
38pub enum MultiWorkspaceEvent {
39 ActiveWorkspaceChanged,
40 WorkspaceAdded(Entity<Workspace>),
41 WorkspaceRemoved(EntityId),
42 SingletonModeChanged,
43}
44
45pub enum SidebarEvent {
46 Open,
47 Close,
48}
49
50pub trait Sidebar: EventEmitter<SidebarEvent> + Focusable + Render + Sized {
51 fn width(&self, cx: &App) -> Pixels;
52 fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
53 fn has_notifications(&self, cx: &App) -> bool;
54}
55
56pub trait SidebarHandle: 'static + Send + Sync {
57 fn width(&self, cx: &App) -> Pixels;
58 fn set_width(&self, width: Option<Pixels>, cx: &mut App);
59 fn focus_handle(&self, cx: &App) -> FocusHandle;
60 fn focus(&self, window: &mut Window, cx: &mut App);
61 fn has_notifications(&self, cx: &App) -> bool;
62 fn to_any(&self) -> AnyView;
63 fn entity_id(&self) -> EntityId;
64}
65
66#[derive(Clone)]
67pub struct DraggedSidebar;
68
69impl Render for DraggedSidebar {
70 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
71 gpui::Empty
72 }
73}
74
75impl<T: Sidebar> SidebarHandle for Entity<T> {
76 fn width(&self, cx: &App) -> Pixels {
77 self.read(cx).width(cx)
78 }
79
80 fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
81 self.update(cx, |this, cx| this.set_width(width, cx))
82 }
83
84 fn focus_handle(&self, cx: &App) -> FocusHandle {
85 self.read(cx).focus_handle(cx)
86 }
87
88 fn focus(&self, window: &mut Window, cx: &mut App) {
89 let handle = self.read(cx).focus_handle(cx);
90 window.focus(&handle, cx);
91 }
92
93 fn has_notifications(&self, cx: &App) -> bool {
94 self.read(cx).has_notifications(cx)
95 }
96
97 fn to_any(&self) -> AnyView {
98 self.clone().into()
99 }
100
101 fn entity_id(&self) -> EntityId {
102 Entity::entity_id(self)
103 }
104}
105
106pub struct MultiWorkspace {
107 window_id: WindowId,
108 workspaces: Vec<Entity<Workspace>>,
109 active_workspace_index: usize,
110 sidebar: Option<Box<dyn SidebarHandle>>,
111 sidebar_open: bool,
112 is_singleton: bool,
113 _sidebar_subscription: Option<Subscription>,
114 pending_removal_tasks: Vec<Task<()>>,
115 _serialize_task: Option<Task<()>>,
116 _create_task: Option<Task<()>>,
117 _subscriptions: Vec<Subscription>,
118}
119
120impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
121
122impl MultiWorkspace {
123 pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
124 let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
125 if let Some(task) = this._serialize_task.take() {
126 task.detach();
127 }
128 if let Some(task) = this._create_task.take() {
129 task.detach();
130 }
131 for task in std::mem::take(&mut this.pending_removal_tasks) {
132 task.detach();
133 }
134 });
135 let quit_subscription = cx.on_app_quit(Self::app_will_quit);
136 let settings_subscription =
137 cx.observe_global_in::<settings::SettingsStore>(window, |this, window, cx| {
138 if DisableAiSettings::get_global(cx).disable_ai && this.sidebar_open {
139 this.close_sidebar(window, cx);
140 }
141 });
142 Self::subscribe_to_workspace(&workspace, cx);
143 Self {
144 window_id: window.window_handle().window_id(),
145 workspaces: vec![workspace],
146 active_workspace_index: 0,
147 sidebar: None,
148 sidebar_open: false,
149 is_singleton: true,
150 _sidebar_subscription: None,
151 pending_removal_tasks: Vec::new(),
152 _serialize_task: None,
153 _create_task: None,
154 _subscriptions: vec![
155 release_subscription,
156 quit_subscription,
157 settings_subscription,
158 ],
159 }
160 }
161
162 pub fn register_sidebar<T: Sidebar>(
163 &mut self,
164 sidebar: Entity<T>,
165 window: &mut Window,
166 cx: &mut Context<Self>,
167 ) {
168 let subscription =
169 cx.subscribe_in(&sidebar, window, |this, _, event, window, cx| match event {
170 SidebarEvent::Open => this.toggle_sidebar(window, cx),
171 SidebarEvent::Close => {
172 this.close_sidebar(window, cx);
173 }
174 });
175 self.sidebar = Some(Box::new(sidebar));
176 self._sidebar_subscription = Some(subscription);
177 }
178
179 pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
180 self.sidebar.as_deref()
181 }
182
183 pub fn sidebar_open(&self) -> bool {
184 self.sidebar_open && self.sidebar.is_some()
185 }
186
187 pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
188 self.sidebar
189 .as_ref()
190 .map_or(false, |s| s.has_notifications(cx))
191 }
192
193 pub fn multi_workspace_enabled(&self, cx: &App) -> bool {
194 cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
195 }
196
197 pub fn is_singleton(&self) -> bool {
198 self.is_singleton
199 }
200
201 pub fn set_singleton(
202 &mut self,
203 is_singleton: bool,
204 window: &mut Window,
205 cx: &mut Context<Self>,
206 ) {
207 if self.is_singleton == is_singleton {
208 return;
209 }
210 self.is_singleton = is_singleton;
211 if is_singleton && self.sidebar_open {
212 self.close_sidebar(window, cx);
213 }
214 cx.emit(MultiWorkspaceEvent::SingletonModeChanged);
215 cx.notify();
216 }
217
218 pub fn set_left_dock_expanded_mode(&mut self, agent_mode: bool, cx: &mut Context<Self>) {
219 self.workspace().update(cx, |workspace, cx| {
220 workspace.set_left_dock_expanded_mode(agent_mode, cx);
221 });
222 cx.notify();
223 }
224
225 pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
226 if !self.multi_workspace_enabled(cx) || self.is_singleton {
227 return;
228 }
229
230 if self.sidebar_open {
231 self.close_sidebar(window, cx);
232 } else {
233 self.open_sidebar(cx);
234 if let Some(sidebar) = &self.sidebar {
235 sidebar.focus(window, cx);
236 }
237 }
238 }
239
240 pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
241 if !self.multi_workspace_enabled(cx) || self.is_singleton {
242 return;
243 }
244
245 if self.sidebar_open {
246 let sidebar_is_focused = self
247 .sidebar
248 .as_ref()
249 .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
250
251 if sidebar_is_focused {
252 let pane = self.workspace().read(cx).active_pane().clone();
253 let pane_focus = pane.read(cx).focus_handle(cx);
254 window.focus(&pane_focus, cx);
255 } else if let Some(sidebar) = &self.sidebar {
256 sidebar.focus(window, cx);
257 }
258 } else {
259 self.open_sidebar(cx);
260 if let Some(sidebar) = &self.sidebar {
261 sidebar.focus(window, cx);
262 }
263 }
264 }
265
266 pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
267 if self.is_singleton {
268 return;
269 }
270 self.sidebar_open = true;
271 for workspace in &self.workspaces {
272 workspace.update(cx, |workspace, cx| {
273 workspace.set_workspace_sidebar_open(true, cx);
274 });
275 }
276 self.serialize(cx);
277 cx.notify();
278 }
279
280 fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
281 self.sidebar_open = false;
282 for workspace in &self.workspaces {
283 workspace.update(cx, |workspace, cx| {
284 workspace.set_workspace_sidebar_open(false, cx);
285 });
286 }
287 let pane = self.workspace().read(cx).active_pane().clone();
288 let pane_focus = pane.read(cx).focus_handle(cx);
289 window.focus(&pane_focus, cx);
290 self.serialize(cx);
291 cx.notify();
292 }
293
294 pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
295 cx.spawn_in(window, async move |this, cx| {
296 let workspaces = this.update(cx, |multi_workspace, _cx| {
297 multi_workspace.workspaces().to_vec()
298 })?;
299
300 for workspace in workspaces {
301 let should_continue = workspace
302 .update_in(cx, |workspace, window, cx| {
303 workspace.prepare_to_close(CloseIntent::CloseWindow, window, cx)
304 })?
305 .await?;
306 if !should_continue {
307 return anyhow::Ok(());
308 }
309 }
310
311 cx.update(|window, _cx| {
312 window.remove_window();
313 })?;
314
315 anyhow::Ok(())
316 })
317 .detach_and_log_err(cx);
318 }
319
320 fn subscribe_to_workspace(workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
321 cx.subscribe(workspace, |this, workspace, event, cx| {
322 if let WorkspaceEvent::Activate = event {
323 this.activate(workspace, cx);
324 }
325 })
326 .detach();
327 }
328
329 pub fn is_sidebar_open(&self) -> bool {
330 self.sidebar_open
331 }
332
333 pub fn workspace(&self) -> &Entity<Workspace> {
334 &self.workspaces[self.active_workspace_index]
335 }
336
337 pub fn workspaces(&self) -> &[Entity<Workspace>] {
338 &self.workspaces
339 }
340
341 pub fn active_workspace_index(&self) -> usize {
342 self.active_workspace_index
343 }
344
345 pub fn activate(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) {
346 if !self.multi_workspace_enabled(cx) {
347 self.workspaces[0] = workspace;
348 self.active_workspace_index = 0;
349 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
350 cx.notify();
351 return;
352 }
353
354 let old_index = self.active_workspace_index;
355 let new_index = self.set_active_workspace(workspace, cx);
356 if old_index != new_index {
357 self.serialize(cx);
358 }
359 }
360
361 fn set_active_workspace(
362 &mut self,
363 workspace: Entity<Workspace>,
364 cx: &mut Context<Self>,
365 ) -> usize {
366 let index = self.add_workspace(workspace, cx);
367 let changed = self.active_workspace_index != index;
368 self.active_workspace_index = index;
369 if changed {
370 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
371 }
372 cx.notify();
373 index
374 }
375
376 /// Adds a workspace to this window without changing which workspace is active.
377 /// Returns the index of the workspace (existing or newly inserted).
378 pub fn add_workspace(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
379 if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
380 index
381 } else {
382 if self.sidebar_open {
383 workspace.update(cx, |workspace, cx| {
384 workspace.set_workspace_sidebar_open(true, cx);
385 });
386 }
387 Self::subscribe_to_workspace(&workspace, cx);
388 self.workspaces.push(workspace.clone());
389 cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
390 cx.notify();
391 self.workspaces.len() - 1
392 }
393 }
394
395 pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
396 debug_assert!(
397 index < self.workspaces.len(),
398 "workspace index out of bounds"
399 );
400 let changed = self.active_workspace_index != index;
401 self.active_workspace_index = index;
402 self.serialize(cx);
403 self.focus_active_workspace(window, cx);
404 if changed {
405 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
406 }
407 cx.notify();
408 }
409
410 pub fn activate_next_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
411 if self.workspaces.len() > 1 {
412 let next_index = (self.active_workspace_index + 1) % self.workspaces.len();
413 self.activate_index(next_index, window, cx);
414 }
415 }
416
417 pub fn activate_previous_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
418 if self.workspaces.len() > 1 {
419 let prev_index = if self.active_workspace_index == 0 {
420 self.workspaces.len() - 1
421 } else {
422 self.active_workspace_index - 1
423 };
424 self.activate_index(prev_index, window, cx);
425 }
426 }
427
428 fn serialize(&mut self, cx: &mut App) {
429 let window_id = self.window_id;
430 let state = crate::persistence::model::MultiWorkspaceState {
431 active_workspace_id: self.workspace().read(cx).database_id(),
432 sidebar_open: self.sidebar_open,
433 };
434 self._serialize_task = Some(cx.background_spawn(async move {
435 crate::persistence::write_multi_workspace_state(window_id, state).await;
436 }));
437 }
438
439 /// Returns the in-flight serialization task (if any) so the caller can
440 /// await it. Used by the quit handler to ensure pending DB writes
441 /// complete before the process exits.
442 pub fn flush_serialization(&mut self) -> Task<()> {
443 self._serialize_task.take().unwrap_or(Task::ready(()))
444 }
445
446 fn app_will_quit(&mut self, _cx: &mut Context<Self>) -> impl Future<Output = ()> + use<> {
447 let mut tasks: Vec<Task<()>> = Vec::new();
448 if let Some(task) = self._serialize_task.take() {
449 tasks.push(task);
450 }
451 if let Some(task) = self._create_task.take() {
452 tasks.push(task);
453 }
454 tasks.extend(std::mem::take(&mut self.pending_removal_tasks));
455
456 async move {
457 futures::future::join_all(tasks).await;
458 }
459 }
460
461 pub fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
462 // If a dock panel is zoomed, focus it instead of the center pane.
463 // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
464 // which closes the zoomed dock.
465 let focus_handle = {
466 let workspace = self.workspace().read(cx);
467 let mut target = None;
468 for dock in workspace.all_docks() {
469 let dock = dock.read(cx);
470 if dock.is_open() {
471 if let Some(panel) = dock.active_panel() {
472 if panel.is_zoomed(window, cx) {
473 target = Some(panel.panel_focus_handle(cx));
474 break;
475 }
476 }
477 }
478 }
479 target.unwrap_or_else(|| {
480 let pane = workspace.active_pane().clone();
481 pane.read(cx).focus_handle(cx)
482 })
483 };
484 window.focus(&focus_handle, cx);
485 }
486
487 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
488 self.workspace().read(cx).panel::<T>(cx)
489 }
490
491 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
492 self.workspace().read(cx).active_modal::<V>(cx)
493 }
494
495 pub fn add_panel<T: Panel>(
496 &mut self,
497 panel: Entity<T>,
498 position: DockPosition,
499 window: &mut Window,
500 cx: &mut Context<Self>,
501 ) {
502 self.workspace().update(cx, |workspace, cx| {
503 workspace.add_panel(panel, position, window, cx);
504 });
505 }
506
507 pub fn focus_panel<T: Panel>(
508 &mut self,
509 window: &mut Window,
510 cx: &mut Context<Self>,
511 ) -> Option<Entity<T>> {
512 self.workspace()
513 .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
514 }
515
516 // used in a test
517 pub fn toggle_modal<V: ModalView, B>(
518 &mut self,
519 window: &mut Window,
520 cx: &mut Context<Self>,
521 build: B,
522 ) where
523 B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
524 {
525 self.workspace().update(cx, |workspace, cx| {
526 workspace.toggle_modal(window, cx, build);
527 });
528 }
529
530 pub fn toggle_dock(
531 &mut self,
532 dock_side: DockPosition,
533 window: &mut Window,
534 cx: &mut Context<Self>,
535 ) {
536 self.workspace().update(cx, |workspace, cx| {
537 workspace.toggle_dock(dock_side, window, cx);
538 });
539 }
540
541 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
542 self.workspace().read(cx).active_item_as::<I>(cx)
543 }
544
545 pub fn items_of_type<'a, T: Item>(
546 &'a self,
547 cx: &'a App,
548 ) -> impl 'a + Iterator<Item = Entity<T>> {
549 self.workspace().read(cx).items_of_type::<T>(cx)
550 }
551
552 pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
553 self.workspace().read(cx).database_id()
554 }
555
556 pub fn take_pending_removal_tasks(&mut self) -> Vec<Task<()>> {
557 let mut tasks: Vec<Task<()>> = std::mem::take(&mut self.pending_removal_tasks)
558 .into_iter()
559 .filter(|task| !task.is_ready())
560 .collect();
561 if let Some(task) = self._create_task.take() {
562 if !task.is_ready() {
563 tasks.push(task);
564 }
565 }
566 tasks
567 }
568
569 #[cfg(any(test, feature = "test-support"))]
570 pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
571 self.workspace().update(cx, |workspace, _cx| {
572 workspace.set_random_database_id();
573 });
574 }
575
576 #[cfg(any(test, feature = "test-support"))]
577 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
578 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
579 Self::new(workspace, window, cx)
580 }
581
582 #[cfg(any(test, feature = "test-support"))]
583 pub fn test_add_workspace(
584 &mut self,
585 project: Entity<Project>,
586 window: &mut Window,
587 cx: &mut Context<Self>,
588 ) -> Entity<Workspace> {
589 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
590 self.activate(workspace.clone(), cx);
591 workspace
592 }
593
594 pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
595 if !self.multi_workspace_enabled(cx) {
596 return;
597 }
598 let app_state = self.workspace().read(cx).app_state().clone();
599 let project = Project::local(
600 app_state.client.clone(),
601 app_state.node_runtime.clone(),
602 app_state.user_store.clone(),
603 app_state.languages.clone(),
604 app_state.fs.clone(),
605 None,
606 project::LocalProjectFlags::default(),
607 cx,
608 );
609 let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
610 self.set_active_workspace(new_workspace.clone(), cx);
611 self.focus_active_workspace(window, cx);
612
613 let weak_workspace = new_workspace.downgrade();
614 self._create_task = Some(cx.spawn_in(window, async move |this, cx| {
615 let result = crate::persistence::DB.next_id().await;
616 this.update_in(cx, |this, window, cx| match result {
617 Ok(workspace_id) => {
618 if let Some(workspace) = weak_workspace.upgrade() {
619 let session_id = workspace.read(cx).session_id();
620 let window_id = window.window_handle().window_id().as_u64();
621 workspace.update(cx, |workspace, _cx| {
622 workspace.set_database_id(workspace_id);
623 });
624 cx.background_spawn(async move {
625 crate::persistence::DB
626 .set_session_binding(workspace_id, session_id, Some(window_id))
627 .await
628 .log_err();
629 })
630 .detach();
631 } else {
632 cx.background_spawn(async move {
633 crate::persistence::DB
634 .delete_workspace_by_id(workspace_id)
635 .await
636 .log_err();
637 })
638 .detach();
639 }
640 this.serialize(cx);
641 }
642 Err(error) => {
643 log::error!("Failed to create workspace: {error:#}");
644 if let Some(index) = weak_workspace
645 .upgrade()
646 .and_then(|w| this.workspaces.iter().position(|ws| *ws == w))
647 {
648 this.remove_workspace(index, window, cx);
649 }
650 this.workspace().update(cx, |workspace, cx| {
651 let id = NotificationId::unique::<MultiWorkspace>();
652 workspace.show_toast(
653 Toast::new(id, format!("Failed to create workspace: {error}")),
654 cx,
655 );
656 });
657 }
658 })
659 .log_err();
660 }));
661 }
662
663 pub fn remove_workspace(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
664 if self.workspaces.len() <= 1 || index >= self.workspaces.len() {
665 return;
666 }
667
668 let removed_workspace = self.workspaces.remove(index);
669
670 if self.active_workspace_index >= self.workspaces.len() {
671 self.active_workspace_index = self.workspaces.len() - 1;
672 } else if self.active_workspace_index > index {
673 self.active_workspace_index -= 1;
674 }
675
676 if let Some(workspace_id) = removed_workspace.read(cx).database_id() {
677 self.pending_removal_tasks.retain(|task| !task.is_ready());
678 self.pending_removal_tasks
679 .push(cx.background_spawn(async move {
680 crate::persistence::DB
681 .delete_workspace_by_id(workspace_id)
682 .await
683 .log_err();
684 }));
685 }
686
687 self.serialize(cx);
688 self.focus_active_workspace(window, cx);
689 cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(
690 removed_workspace.entity_id(),
691 ));
692 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
693 cx.notify();
694 }
695
696 pub fn open_project(
697 &mut self,
698 paths: Vec<PathBuf>,
699 window: &mut Window,
700 cx: &mut Context<Self>,
701 ) -> Task<Result<()>> {
702 let workspace = self.workspace().clone();
703
704 if self.multi_workspace_enabled(cx) {
705 workspace.update(cx, |workspace, cx| {
706 workspace.open_workspace_for_paths(true, paths, window, cx)
707 })
708 } else {
709 cx.spawn_in(window, async move |_this, cx| {
710 let should_continue = workspace
711 .update_in(cx, |workspace, window, cx| {
712 workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
713 })?
714 .await?;
715 if should_continue {
716 workspace
717 .update_in(cx, |workspace, window, cx| {
718 workspace.open_workspace_for_paths(true, paths, window, cx)
719 })?
720 .await
721 } else {
722 Ok(())
723 }
724 })
725 }
726 }
727}
728
729impl Render for MultiWorkspace {
730 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
731 let multi_workspace_enabled = self.multi_workspace_enabled(cx);
732
733 let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open {
734 self.sidebar.as_ref().map(|sidebar_handle| {
735 let weak = cx.weak_entity();
736
737 let sidebar_width = sidebar_handle.width(cx);
738 let resize_handle = deferred(
739 div()
740 .id("sidebar-resize-handle")
741 .absolute()
742 .right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
743 .top(px(0.))
744 .h_full()
745 .w(SIDEBAR_RESIZE_HANDLE_SIZE)
746 .cursor_col_resize()
747 .on_drag(DraggedSidebar, |dragged, _, _, cx| {
748 cx.stop_propagation();
749 cx.new(|_| dragged.clone())
750 })
751 .on_mouse_down(MouseButton::Left, |_, _, cx| {
752 cx.stop_propagation();
753 })
754 .on_mouse_up(MouseButton::Left, move |event, _, cx| {
755 if event.click_count == 2 {
756 weak.update(cx, |this, cx| {
757 if let Some(sidebar) = this.sidebar.as_mut() {
758 sidebar.set_width(None, cx);
759 }
760 })
761 .ok();
762 cx.stop_propagation();
763 }
764 })
765 .occlude(),
766 );
767
768 div()
769 .id("sidebar-container")
770 .relative()
771 .h_full()
772 .w(sidebar_width)
773 .flex_shrink_0()
774 .child(sidebar_handle.to_any())
775 .child(resize_handle)
776 .into_any_element()
777 })
778 } else {
779 None
780 };
781
782 let ui_font = theme::setup_ui_font(window, cx);
783 let text_color = cx.theme().colors().text;
784
785 let workspace = self.workspace().clone();
786 let workspace_key_context = workspace.update(cx, |workspace, cx| workspace.key_context(cx));
787 let root = workspace.update(cx, |workspace, cx| workspace.actions(h_flex(), window, cx));
788
789 client_side_decorations(
790 root.key_context(workspace_key_context)
791 .relative()
792 .size_full()
793 .font(ui_font)
794 .text_color(text_color)
795 .on_action(cx.listener(Self::close_window))
796 .on_action(
797 cx.listener(|this: &mut Self, _: &NewWorkspaceInWindow, window, cx| {
798 this.create_workspace(window, cx);
799 }),
800 )
801 .on_action(
802 cx.listener(|this: &mut Self, _: &NextWorkspaceInWindow, window, cx| {
803 this.activate_next_workspace(window, cx);
804 }),
805 )
806 .on_action(cx.listener(
807 |this: &mut Self, _: &PreviousWorkspaceInWindow, window, cx| {
808 this.activate_previous_workspace(window, cx);
809 },
810 ))
811 .when(self.multi_workspace_enabled(cx), |this| {
812 this.on_action(cx.listener(
813 |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
814 this.toggle_sidebar(window, cx);
815 },
816 ))
817 .on_action(cx.listener(
818 |this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
819 this.focus_sidebar(window, cx);
820 },
821 ))
822 })
823 .when(
824 self.sidebar_open() && self.multi_workspace_enabled(cx),
825 |this| {
826 this.on_drag_move(cx.listener(
827 |this: &mut Self, e: &DragMoveEvent<DraggedSidebar>, _window, cx| {
828 if let Some(sidebar) = &this.sidebar {
829 let new_width = e.event.position.x;
830 sidebar.set_width(Some(new_width), cx);
831 }
832 },
833 ))
834 .children(sidebar)
835 },
836 )
837 .child(
838 div()
839 .flex()
840 .flex_1()
841 .size_full()
842 .overflow_hidden()
843 .child(self.workspace().clone()),
844 )
845 .child(self.workspace().read(cx).modal_layer.clone()),
846 window,
847 cx,
848 Tiling {
849 left: multi_workspace_enabled && self.sidebar_open,
850 ..Tiling::default()
851 },
852 )
853 }
854}
855
856#[cfg(test)]
857mod tests {
858 use super::*;
859 use fs::FakeFs;
860 use gpui::TestAppContext;
861 use settings::SettingsStore;
862
863 fn init_test(cx: &mut TestAppContext) {
864 cx.update(|cx| {
865 let settings_store = SettingsStore::test(cx);
866 cx.set_global(settings_store);
867 theme::init(theme::LoadThemes::JustBase, cx);
868 DisableAiSettings::register(cx);
869 cx.update_flags(false, vec!["agent-v2".into()]);
870 });
871 }
872
873 #[gpui::test]
874 async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) {
875 init_test(cx);
876 let fs = FakeFs::new(cx.executor());
877 let project = Project::test(fs, [], cx).await;
878
879 let (multi_workspace, cx) =
880 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
881
882 multi_workspace.read_with(cx, |mw, cx| {
883 assert!(mw.multi_workspace_enabled(cx));
884 });
885
886 multi_workspace.update_in(cx, |mw, _window, cx| {
887 mw.open_sidebar(cx);
888 assert!(mw.is_sidebar_open());
889 });
890
891 cx.update(|_window, cx| {
892 DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx);
893 });
894 cx.run_until_parked();
895
896 multi_workspace.read_with(cx, |mw, cx| {
897 assert!(
898 !mw.is_sidebar_open(),
899 "Sidebar should be closed when disable_ai is true"
900 );
901 assert!(
902 !mw.multi_workspace_enabled(cx),
903 "Multi-workspace should be disabled when disable_ai is true"
904 );
905 });
906
907 multi_workspace.update_in(cx, |mw, window, cx| {
908 mw.toggle_sidebar(window, cx);
909 });
910 multi_workspace.read_with(cx, |mw, _cx| {
911 assert!(
912 !mw.is_sidebar_open(),
913 "Sidebar should remain closed when toggled with disable_ai true"
914 );
915 });
916
917 cx.update(|_window, cx| {
918 DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
919 });
920 cx.run_until_parked();
921
922 multi_workspace.read_with(cx, |mw, cx| {
923 assert!(
924 mw.multi_workspace_enabled(cx),
925 "Multi-workspace should be enabled after re-enabling AI"
926 );
927 assert!(
928 !mw.is_sidebar_open(),
929 "Sidebar should still be closed after re-enabling AI (not auto-opened)"
930 );
931 });
932
933 multi_workspace.update_in(cx, |mw, window, cx| {
934 mw.toggle_sidebar(window, cx);
935 });
936 multi_workspace.read_with(cx, |mw, _cx| {
937 assert!(
938 mw.is_sidebar_open(),
939 "Sidebar should open when toggled after re-enabling AI"
940 );
941 });
942 }
943}