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