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, actions,
6 deferred, px,
7};
8use project::Project;
9use std::path::PathBuf;
10use ui::prelude::*;
11
12const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
13
14use crate::{
15 DockPosition, Item, ModalView, Panel, Workspace, WorkspaceId, client_side_decorations,
16};
17
18actions!(
19 multi_workspace,
20 [
21 /// Creates a new workspace within the current window.
22 NewWorkspaceInWindow,
23 /// Switches to the next workspace within the current window.
24 NextWorkspaceInWindow,
25 /// Switches to the previous workspace within the current window.
26 PreviousWorkspaceInWindow,
27 /// Toggles the workspace switcher sidebar.
28 ToggleWorkspaceSidebar,
29 /// Moves focus to or from the workspace sidebar without closing it.
30 FocusWorkspaceSidebar,
31 ]
32);
33
34pub enum SidebarEvent {
35 Open,
36 Close,
37}
38
39pub trait Sidebar: EventEmitter<SidebarEvent> + Focusable + Render + Sized {
40 fn width(&self, cx: &App) -> Pixels;
41 fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
42 fn has_notifications(&self, cx: &App) -> bool;
43}
44
45pub trait SidebarHandle: 'static + Send + Sync {
46 fn width(&self, cx: &App) -> Pixels;
47 fn set_width(&self, width: Option<Pixels>, cx: &mut App);
48 fn focus_handle(&self, cx: &App) -> FocusHandle;
49 fn focus(&self, window: &mut Window, cx: &mut App);
50 fn has_notifications(&self, cx: &App) -> bool;
51 fn to_any(&self) -> AnyView;
52 fn entity_id(&self) -> EntityId;
53}
54
55#[derive(Clone)]
56pub struct DraggedSidebar;
57
58impl Render for DraggedSidebar {
59 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
60 gpui::Empty
61 }
62}
63
64impl<T: Sidebar> SidebarHandle for Entity<T> {
65 fn width(&self, cx: &App) -> Pixels {
66 self.read(cx).width(cx)
67 }
68
69 fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
70 self.update(cx, |this, cx| this.set_width(width, cx))
71 }
72
73 fn focus_handle(&self, cx: &App) -> FocusHandle {
74 self.read(cx).focus_handle(cx)
75 }
76
77 fn focus(&self, window: &mut Window, cx: &mut App) {
78 let handle = self.read(cx).focus_handle(cx);
79 window.focus(&handle, cx);
80 }
81
82 fn has_notifications(&self, cx: &App) -> bool {
83 self.read(cx).has_notifications(cx)
84 }
85
86 fn to_any(&self) -> AnyView {
87 self.clone().into()
88 }
89
90 fn entity_id(&self) -> EntityId {
91 Entity::entity_id(self)
92 }
93}
94
95pub struct MultiWorkspace {
96 workspaces: Vec<Entity<Workspace>>,
97 active_workspace_index: usize,
98 sidebar: Option<Box<dyn SidebarHandle>>,
99 sidebar_open: bool,
100 _sidebar_subscription: Option<Subscription>,
101}
102
103impl MultiWorkspace {
104 pub fn new(workspace: Entity<Workspace>, _cx: &mut Context<Self>) -> Self {
105 Self {
106 workspaces: vec![workspace],
107 active_workspace_index: 0,
108 sidebar: None,
109 sidebar_open: false,
110 _sidebar_subscription: None,
111 }
112 }
113
114 pub fn register_sidebar<T: Sidebar>(
115 &mut self,
116 sidebar: Entity<T>,
117 window: &mut Window,
118 cx: &mut Context<Self>,
119 ) {
120 let subscription =
121 cx.subscribe_in(&sidebar, window, |this, _, event, window, cx| match event {
122 SidebarEvent::Open => this.toggle_sidebar(window, cx),
123 SidebarEvent::Close => {
124 this.close_sidebar(window, cx);
125 }
126 });
127 self.sidebar = Some(Box::new(sidebar));
128 self._sidebar_subscription = Some(subscription);
129 }
130
131 pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
132 self.sidebar.as_deref()
133 }
134
135 pub fn sidebar_open(&self) -> bool {
136 self.sidebar_open && self.sidebar.is_some()
137 }
138
139 pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
140 self.sidebar
141 .as_ref()
142 .map_or(false, |s| s.has_notifications(cx))
143 }
144
145 pub(crate) fn multi_workspace_enabled(&self, cx: &App) -> bool {
146 cx.has_flag::<AgentV2FeatureFlag>()
147 }
148
149 pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
150 if !self.multi_workspace_enabled(cx) {
151 return;
152 }
153
154 if self.sidebar_open {
155 self.close_sidebar(window, cx);
156 } else {
157 self.open_sidebar(window, cx);
158 if let Some(sidebar) = &self.sidebar {
159 sidebar.focus(window, cx);
160 }
161 }
162 }
163
164 pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
165 if !self.multi_workspace_enabled(cx) {
166 return;
167 }
168
169 if self.sidebar_open {
170 let sidebar_is_focused = self
171 .sidebar
172 .as_ref()
173 .is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
174
175 if sidebar_is_focused {
176 let pane = self.workspace().read(cx).active_pane().clone();
177 let pane_focus = pane.read(cx).focus_handle(cx);
178 window.focus(&pane_focus, cx);
179 } else if let Some(sidebar) = &self.sidebar {
180 sidebar.focus(window, cx);
181 }
182 } else {
183 self.open_sidebar(window, cx);
184 if let Some(sidebar) = &self.sidebar {
185 sidebar.focus(window, cx);
186 }
187 }
188 }
189
190 pub fn open_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
191 self.sidebar_open = true;
192 for workspace in &self.workspaces {
193 workspace.update(cx, |workspace, cx| {
194 workspace.set_workspace_sidebar_open(true, cx);
195 });
196 }
197 self.serialize(window, cx);
198 cx.notify();
199 }
200
201 fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
202 self.sidebar_open = false;
203 for workspace in &self.workspaces {
204 workspace.update(cx, |workspace, cx| {
205 workspace.set_workspace_sidebar_open(false, cx);
206 });
207 }
208 let pane = self.workspace().read(cx).active_pane().clone();
209 let pane_focus = pane.read(cx).focus_handle(cx);
210 window.focus(&pane_focus, cx);
211 self.serialize(window, cx);
212 cx.notify();
213 }
214
215 pub fn is_sidebar_open(&self) -> bool {
216 self.sidebar_open
217 }
218
219 pub fn workspace(&self) -> &Entity<Workspace> {
220 &self.workspaces[self.active_workspace_index]
221 }
222
223 pub fn workspaces(&self) -> &[Entity<Workspace>] {
224 &self.workspaces
225 }
226
227 pub fn active_workspace_index(&self) -> usize {
228 self.active_workspace_index
229 }
230
231 pub fn activate(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) {
232 if !self.multi_workspace_enabled(cx) {
233 self.workspaces[0] = workspace;
234 self.active_workspace_index = 0;
235 cx.notify();
236 return;
237 }
238
239 let index = self.add_workspace(workspace, cx);
240 if self.active_workspace_index != index {
241 self.active_workspace_index = index;
242 cx.notify();
243 }
244 }
245
246 /// Adds a workspace to this window without changing which workspace is active.
247 /// Returns the index of the workspace (existing or newly inserted).
248 pub fn add_workspace(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
249 if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
250 index
251 } else {
252 if self.sidebar_open {
253 workspace.update(cx, |workspace, cx| {
254 workspace.set_workspace_sidebar_open(true, cx);
255 });
256 }
257 self.workspaces.push(workspace);
258 cx.notify();
259 self.workspaces.len() - 1
260 }
261 }
262
263 pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
264 debug_assert!(
265 index < self.workspaces.len(),
266 "workspace index out of bounds"
267 );
268 self.active_workspace_index = index;
269 self.serialize(window, cx);
270 self.focus_active_workspace(window, cx);
271 cx.notify();
272 }
273
274 pub fn activate_next_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
275 if self.workspaces.len() > 1 {
276 let next_index = (self.active_workspace_index + 1) % self.workspaces.len();
277 self.activate_index(next_index, window, cx);
278 }
279 }
280
281 pub fn activate_previous_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
282 if self.workspaces.len() > 1 {
283 let prev_index = if self.active_workspace_index == 0 {
284 self.workspaces.len() - 1
285 } else {
286 self.active_workspace_index - 1
287 };
288 self.activate_index(prev_index, window, cx);
289 }
290 }
291
292 fn serialize(&self, window: &mut Window, cx: &mut App) {
293 let window_id = window.window_handle().window_id();
294 let state = crate::persistence::model::MultiWorkspaceState {
295 active_workspace_id: self.workspace().read(cx).database_id(),
296 sidebar_open: self.sidebar_open,
297 };
298 cx.background_spawn(async move {
299 crate::persistence::write_multi_workspace_state(window_id, state).await;
300 })
301 .detach();
302 }
303
304 fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
305 // If a dock panel is zoomed, focus it instead of the center pane.
306 // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
307 // which closes the zoomed dock.
308 let focus_handle = {
309 let workspace = self.workspace().read(cx);
310 let mut target = None;
311 for dock in workspace.all_docks() {
312 let dock = dock.read(cx);
313 if dock.is_open() {
314 if let Some(panel) = dock.active_panel() {
315 if panel.is_zoomed(window, cx) {
316 target = Some(panel.panel_focus_handle(cx));
317 break;
318 }
319 }
320 }
321 }
322 target.unwrap_or_else(|| {
323 let pane = workspace.active_pane().clone();
324 pane.read(cx).focus_handle(cx)
325 })
326 };
327 window.focus(&focus_handle, cx);
328 }
329
330 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
331 self.workspace().read(cx).panel::<T>(cx)
332 }
333
334 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
335 self.workspace().read(cx).active_modal::<V>(cx)
336 }
337
338 pub fn add_panel<T: Panel>(
339 &mut self,
340 panel: Entity<T>,
341 window: &mut Window,
342 cx: &mut Context<Self>,
343 ) {
344 self.workspace().update(cx, |workspace, cx| {
345 workspace.add_panel(panel, window, cx);
346 });
347 }
348
349 pub fn focus_panel<T: Panel>(
350 &mut self,
351 window: &mut Window,
352 cx: &mut Context<Self>,
353 ) -> Option<Entity<T>> {
354 self.workspace()
355 .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
356 }
357
358 pub fn toggle_modal<V: ModalView, B>(
359 &mut self,
360 window: &mut Window,
361 cx: &mut Context<Self>,
362 build: B,
363 ) where
364 B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
365 {
366 self.workspace().update(cx, |workspace, cx| {
367 workspace.toggle_modal(window, cx, build);
368 });
369 }
370
371 pub fn toggle_dock(
372 &mut self,
373 dock_side: DockPosition,
374 window: &mut Window,
375 cx: &mut Context<Self>,
376 ) {
377 self.workspace().update(cx, |workspace, cx| {
378 workspace.toggle_dock(dock_side, window, cx);
379 });
380 }
381
382 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
383 self.workspace().read(cx).active_item_as::<I>(cx)
384 }
385
386 pub fn items_of_type<'a, T: Item>(
387 &'a self,
388 cx: &'a App,
389 ) -> impl 'a + Iterator<Item = Entity<T>> {
390 self.workspace().read(cx).items_of_type::<T>(cx)
391 }
392
393 pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
394 self.workspace().read(cx).database_id()
395 }
396
397 #[cfg(any(test, feature = "test-support"))]
398 pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
399 self.workspace().update(cx, |workspace, _cx| {
400 workspace.set_random_database_id();
401 });
402 }
403
404 #[cfg(any(test, feature = "test-support"))]
405 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
406 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
407 Self::new(workspace, cx)
408 }
409
410 #[cfg(any(test, feature = "test-support"))]
411 pub fn test_add_workspace(
412 &mut self,
413 project: Entity<Project>,
414 window: &mut Window,
415 cx: &mut Context<Self>,
416 ) -> Entity<Workspace> {
417 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
418 self.activate(workspace.clone(), cx);
419 workspace
420 }
421
422 pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
423 if !self.multi_workspace_enabled(cx) {
424 return;
425 }
426 let app_state = self.workspace().read(cx).app_state().clone();
427 let project = Project::local(
428 app_state.client.clone(),
429 app_state.node_runtime.clone(),
430 app_state.user_store.clone(),
431 app_state.languages.clone(),
432 app_state.fs.clone(),
433 None,
434 project::LocalProjectFlags::default(),
435 cx,
436 );
437 let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
438 self.activate(new_workspace, cx);
439 self.focus_active_workspace(window, cx);
440 }
441
442 pub fn remove_workspace(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
443 if self.workspaces.len() <= 1 || index >= self.workspaces.len() {
444 return;
445 }
446
447 self.workspaces.remove(index);
448
449 if self.active_workspace_index >= self.workspaces.len() {
450 self.active_workspace_index = self.workspaces.len() - 1;
451 } else if self.active_workspace_index > index {
452 self.active_workspace_index -= 1;
453 }
454
455 self.focus_active_workspace(window, cx);
456 cx.notify();
457 }
458
459 pub fn open_project(
460 &mut self,
461 paths: Vec<PathBuf>,
462 window: &mut Window,
463 cx: &mut Context<Self>,
464 ) -> Task<Result<()>> {
465 let workspace = self.workspace().clone();
466
467 if self.multi_workspace_enabled(cx) {
468 workspace.update(cx, |workspace, cx| {
469 workspace.open_workspace_for_paths(true, paths, window, cx)
470 })
471 } else {
472 cx.spawn_in(window, async move |_this, cx| {
473 let should_continue = workspace
474 .update_in(cx, |workspace, window, cx| {
475 workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
476 })?
477 .await?;
478 if should_continue {
479 workspace
480 .update_in(cx, |workspace, window, cx| {
481 workspace.open_workspace_for_paths(true, paths, window, cx)
482 })?
483 .await
484 } else {
485 Ok(())
486 }
487 })
488 }
489 }
490}
491
492impl Render for MultiWorkspace {
493 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
494 let multi_workspace_enabled = self.multi_workspace_enabled(cx);
495
496 let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open {
497 self.sidebar.as_ref().map(|sidebar_handle| {
498 let weak = cx.weak_entity();
499
500 let sidebar_width = sidebar_handle.width(cx);
501 let resize_handle = deferred(
502 div()
503 .id("sidebar-resize-handle")
504 .absolute()
505 .right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
506 .top(px(0.))
507 .h_full()
508 .w(SIDEBAR_RESIZE_HANDLE_SIZE)
509 .cursor_col_resize()
510 .on_drag(DraggedSidebar, |dragged, _, _, cx| {
511 cx.stop_propagation();
512 cx.new(|_| dragged.clone())
513 })
514 .on_mouse_down(MouseButton::Left, |_, _, cx| {
515 cx.stop_propagation();
516 })
517 .on_mouse_up(MouseButton::Left, move |event, _, cx| {
518 if event.click_count == 2 {
519 weak.update(cx, |this, cx| {
520 if let Some(sidebar) = this.sidebar.as_mut() {
521 sidebar.set_width(None, cx);
522 }
523 })
524 .ok();
525 cx.stop_propagation();
526 }
527 })
528 .occlude(),
529 );
530
531 div()
532 .id("sidebar-container")
533 .relative()
534 .h_full()
535 .w(sidebar_width)
536 .flex_shrink_0()
537 .child(sidebar_handle.to_any())
538 .child(resize_handle)
539 .into_any_element()
540 })
541 } else {
542 None
543 };
544
545 client_side_decorations(
546 h_flex()
547 .key_context("Workspace")
548 .size_full()
549 .on_action(
550 cx.listener(|this: &mut Self, _: &NewWorkspaceInWindow, window, cx| {
551 this.create_workspace(window, cx);
552 }),
553 )
554 .on_action(
555 cx.listener(|this: &mut Self, _: &NextWorkspaceInWindow, window, cx| {
556 this.activate_next_workspace(window, cx);
557 }),
558 )
559 .on_action(cx.listener(
560 |this: &mut Self, _: &PreviousWorkspaceInWindow, window, cx| {
561 this.activate_previous_workspace(window, cx);
562 },
563 ))
564 .on_action(cx.listener(
565 |this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
566 this.toggle_sidebar(window, cx);
567 },
568 ))
569 .on_action(
570 cx.listener(|this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
571 this.focus_sidebar(window, cx);
572 }),
573 )
574 .when(
575 self.sidebar_open() && self.multi_workspace_enabled(cx),
576 |this| {
577 this.on_drag_move(cx.listener(
578 |this: &mut Self, e: &DragMoveEvent<DraggedSidebar>, _window, cx| {
579 if let Some(sidebar) = &this.sidebar {
580 let new_width = e.event.position.x;
581 sidebar.set_width(Some(new_width), cx);
582 }
583 },
584 ))
585 .children(sidebar)
586 },
587 )
588 .child(
589 div()
590 .flex()
591 .flex_1()
592 .size_full()
593 .overflow_hidden()
594 .child(self.workspace().clone()),
595 ),
596 window,
597 cx,
598 Tiling {
599 left: multi_workspace_enabled && self.sidebar_open,
600 ..Tiling::default()
601 },
602 )
603 }
604}