1use anyhow::Result;
2use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
3use gpui::{
4 App, Context, Entity, EntityId, EventEmitter, Focusable, ManagedView, Pixels, Render,
5 Subscription, Task, Tiling, Window, WindowId, actions, px,
6};
7use project::{DisableAiSettings, Project};
8use settings::Settings;
9use std::future::Future;
10use std::path::PathBuf;
11use ui::prelude::*;
12use util::ResultExt;
13
14pub const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
15
16use crate::{
17 CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, Panel, Toast,
18 Workspace, WorkspaceId, client_side_decorations, notifications::NotificationId,
19 persistence::model::MultiWorkspaceId,
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}
43
44#[derive(Clone)]
45pub struct DraggedSidebar;
46
47impl Render for DraggedSidebar {
48 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
49 gpui::Empty
50 }
51}
52
53pub struct MultiWorkspace {
54 window_id: WindowId,
55 workspaces: Vec<Entity<Workspace>>,
56 database_id: Option<MultiWorkspaceId>,
57 active_workspace_index: usize,
58 pending_removal_tasks: Vec<Task<()>>,
59 _serialize_task: Option<Task<()>>,
60 _create_task: Option<Task<()>>,
61 _subscriptions: Vec<Subscription>,
62}
63
64impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
65
66pub fn multi_workspace_enabled(cx: &App) -> bool {
67 cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
68}
69
70impl MultiWorkspace {
71 pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
72 let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
73 if let Some(task) = this._serialize_task.take() {
74 task.detach();
75 }
76 if let Some(task) = this._create_task.take() {
77 task.detach();
78 }
79 for task in std::mem::take(&mut this.pending_removal_tasks) {
80 task.detach();
81 }
82 });
83 let quit_subscription = cx.on_app_quit(Self::app_will_quit);
84 Self::subscribe_to_workspace(&workspace, cx);
85 Self {
86 window_id: window.window_handle().window_id(),
87 database_id: None,
88 workspaces: vec![workspace],
89 active_workspace_index: 0,
90 pending_removal_tasks: Vec::new(),
91 _serialize_task: None,
92 _create_task: None,
93 _subscriptions: vec![release_subscription, quit_subscription],
94 }
95 }
96
97 pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
98 cx.spawn_in(window, async move |this, cx| {
99 let workspaces = this.update(cx, |multi_workspace, _cx| {
100 multi_workspace.workspaces().to_vec()
101 })?;
102
103 for workspace in workspaces {
104 let should_continue = workspace
105 .update_in(cx, |workspace, window, cx| {
106 workspace.prepare_to_close(CloseIntent::CloseWindow, window, cx)
107 })?
108 .await?;
109 if !should_continue {
110 return anyhow::Ok(());
111 }
112 }
113
114 cx.update(|window, _cx| {
115 window.remove_window();
116 })?;
117
118 anyhow::Ok(())
119 })
120 .detach_and_log_err(cx);
121 }
122
123 fn subscribe_to_workspace(workspace: &Entity<Workspace>, cx: &mut Context<Self>) {
124 cx.subscribe(workspace, |this, workspace, event, cx| {
125 if let WorkspaceEvent::Activate = event {
126 this.activate(workspace, cx);
127 }
128 })
129 .detach();
130 }
131
132 pub fn workspace(&self) -> &Entity<Workspace> {
133 &self.workspaces[self.active_workspace_index]
134 }
135
136 pub fn workspaces(&self) -> &[Entity<Workspace>] {
137 &self.workspaces
138 }
139
140 pub fn active_workspace_index(&self) -> usize {
141 self.active_workspace_index
142 }
143
144 pub fn activate(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) {
145 if !multi_workspace_enabled(cx) {
146 self.workspaces[0] = workspace;
147 self.active_workspace_index = 0;
148 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
149 cx.notify();
150 return;
151 }
152
153 let old_index = self.active_workspace_index;
154 let new_index = self.set_active_workspace(workspace, cx);
155 if old_index != new_index {
156 self.serialize(cx);
157 }
158 }
159
160 fn set_active_workspace(
161 &mut self,
162 workspace: Entity<Workspace>,
163 cx: &mut Context<Self>,
164 ) -> usize {
165 let index = self.add_workspace(workspace, cx);
166 let changed = self.active_workspace_index != index;
167 self.active_workspace_index = index;
168 if changed {
169 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
170 }
171 cx.notify();
172 index
173 }
174
175 /// Adds a workspace to this window without changing which workspace is active.
176 /// Returns the index of the workspace (existing or newly inserted).
177 pub fn add_workspace(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) -> usize {
178 if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
179 index
180 } else {
181 Self::subscribe_to_workspace(&workspace, cx);
182 self.workspaces.push(workspace.clone());
183 cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
184 cx.notify();
185 self.workspaces.len() - 1
186 }
187 }
188
189 pub fn database_id(&self) -> Option<MultiWorkspaceId> {
190 self.database_id
191 }
192
193 pub fn set_database_id(&mut self, id: Option<MultiWorkspaceId>) {
194 self.database_id = id;
195 }
196
197 pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
198 debug_assert!(
199 index < self.workspaces.len(),
200 "workspace index out of bounds"
201 );
202 let changed = self.active_workspace_index != index;
203 self.active_workspace_index = index;
204 self.serialize(cx);
205 self.focus_active_workspace(window, cx);
206 if changed {
207 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
208 }
209 cx.notify();
210 }
211
212 pub fn activate_next_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
213 if self.workspaces.len() > 1 {
214 let next_index = (self.active_workspace_index + 1) % self.workspaces.len();
215 self.activate_index(next_index, window, cx);
216 }
217 }
218
219 pub fn activate_previous_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
220 if self.workspaces.len() > 1 {
221 let prev_index = if self.active_workspace_index == 0 {
222 self.workspaces.len() - 1
223 } else {
224 self.active_workspace_index - 1
225 };
226 self.activate_index(prev_index, window, cx);
227 }
228 }
229
230 fn serialize(&mut self, cx: &mut App) {
231 let window_id = self.window_id;
232 let state = crate::persistence::model::MultiWorkspaceState {
233 active_workspace_id: self.workspace().read(cx).database_id(),
234 };
235 self._serialize_task = Some(cx.background_spawn(async move {
236 crate::persistence::write_multi_workspace_state(window_id, state).await;
237 }));
238 }
239
240 /// Returns the in-flight serialization task (if any) so the caller can
241 /// await it. Used by the quit handler to ensure pending DB writes
242 /// complete before the process exits.
243 pub fn flush_serialization(&mut self) -> Task<()> {
244 self._serialize_task.take().unwrap_or(Task::ready(()))
245 }
246
247 fn app_will_quit(&mut self, _cx: &mut Context<Self>) -> impl Future<Output = ()> + use<> {
248 let mut tasks: Vec<Task<()>> = Vec::new();
249 if let Some(task) = self._serialize_task.take() {
250 tasks.push(task);
251 }
252 if let Some(task) = self._create_task.take() {
253 tasks.push(task);
254 }
255 tasks.extend(std::mem::take(&mut self.pending_removal_tasks));
256
257 async move {
258 futures::future::join_all(tasks).await;
259 }
260 }
261
262 pub fn focus_active_workspace(&self, window: &mut Window, cx: &mut App) {
263 // If a dock panel is zoomed, focus it instead of the center pane.
264 // Otherwise, focusing the center pane triggers dismiss_zoomed_items_to_reveal
265 // which closes the zoomed dock.
266 let focus_handle = {
267 let workspace = self.workspace().read(cx);
268 let mut target = None;
269 for dock in workspace.all_docks() {
270 let dock = dock.read(cx);
271 if dock.is_open() {
272 if let Some(panel) = dock.active_panel() {
273 if panel.is_zoomed(window, cx) {
274 target = Some(panel.panel_focus_handle(cx));
275 break;
276 }
277 }
278 }
279 }
280 target.unwrap_or_else(|| {
281 let pane = workspace.active_pane().clone();
282 pane.read(cx).focus_handle(cx)
283 })
284 };
285 window.focus(&focus_handle, cx);
286 }
287
288 pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
289 self.workspace().read(cx).panel::<T>(cx)
290 }
291
292 pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
293 self.workspace().read(cx).active_modal::<V>(cx)
294 }
295
296 pub fn add_panel<T: Panel>(
297 &mut self,
298 panel: Entity<T>,
299 window: &mut Window,
300 cx: &mut Context<Self>,
301 ) {
302 self.workspace().update(cx, |workspace, cx| {
303 workspace.add_panel(panel, window, cx);
304 });
305 }
306
307 pub fn focus_panel<T: Panel>(
308 &mut self,
309 window: &mut Window,
310 cx: &mut Context<Self>,
311 ) -> Option<Entity<T>> {
312 self.workspace()
313 .update(cx, |workspace, cx| workspace.focus_panel::<T>(window, cx))
314 }
315
316 // used in a test
317 pub fn toggle_modal<V: ModalView, B>(
318 &mut self,
319 window: &mut Window,
320 cx: &mut Context<Self>,
321 build: B,
322 ) where
323 B: FnOnce(&mut Window, &mut gpui::Context<V>) -> V,
324 {
325 self.workspace().update(cx, |workspace, cx| {
326 workspace.toggle_modal(window, cx, build);
327 });
328 }
329
330 pub fn toggle_dock(
331 &mut self,
332 dock_side: DockPosition,
333 window: &mut Window,
334 cx: &mut Context<Self>,
335 ) {
336 self.workspace().update(cx, |workspace, cx| {
337 workspace.toggle_dock(dock_side, window, cx);
338 });
339 }
340
341 pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
342 self.workspace().read(cx).active_item_as::<I>(cx)
343 }
344
345 pub fn items_of_type<'a, T: Item>(
346 &'a self,
347 cx: &'a App,
348 ) -> impl 'a + Iterator<Item = Entity<T>> {
349 self.workspace().read(cx).items_of_type::<T>(cx)
350 }
351
352 pub fn active_workspace_database_id(&self, cx: &App) -> Option<WorkspaceId> {
353 self.workspace().read(cx).database_id()
354 }
355
356 pub fn take_pending_removal_tasks(&mut self) -> Vec<Task<()>> {
357 let mut tasks: Vec<Task<()>> = std::mem::take(&mut self.pending_removal_tasks)
358 .into_iter()
359 .filter(|task| !task.is_ready())
360 .collect();
361 if let Some(task) = self._create_task.take() {
362 if !task.is_ready() {
363 tasks.push(task);
364 }
365 }
366 tasks
367 }
368
369 #[cfg(any(test, feature = "test-support"))]
370 pub fn set_random_database_id(&mut self, cx: &mut Context<Self>) {
371 self.workspace().update(cx, |workspace, _cx| {
372 workspace.set_random_database_id();
373 });
374 }
375
376 #[cfg(any(test, feature = "test-support"))]
377 pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
378 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
379 Self::new(workspace, window, cx)
380 }
381
382 #[cfg(any(test, feature = "test-support"))]
383 pub fn test_add_workspace(
384 &mut self,
385 project: Entity<Project>,
386 window: &mut Window,
387 cx: &mut Context<Self>,
388 ) -> Entity<Workspace> {
389 let workspace = cx.new(|cx| Workspace::test_new(project, window, cx));
390 self.activate(workspace.clone(), cx);
391 workspace
392 }
393
394 pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
395 if !multi_workspace_enabled(cx) {
396 return;
397 }
398 let app_state = self.workspace().read(cx).app_state().clone();
399 let project = Project::local(
400 app_state.client.clone(),
401 app_state.node_runtime.clone(),
402 app_state.user_store.clone(),
403 app_state.languages.clone(),
404 app_state.fs.clone(),
405 None,
406 project::LocalProjectFlags::default(),
407 cx,
408 );
409 let new_workspace = cx.new(|cx| Workspace::new(None, project, app_state, window, cx));
410 self.set_active_workspace(new_workspace.clone(), cx);
411 self.focus_active_workspace(window, cx);
412
413 let weak_workspace = new_workspace.downgrade();
414 self._create_task = Some(cx.spawn_in(window, async move |this, cx| {
415 let result = crate::persistence::DB.next_id().await;
416 this.update_in(cx, |this, window, cx| match result {
417 Ok(workspace_id) => {
418 if let Some(workspace) = weak_workspace.upgrade() {
419 let session_id = workspace.read(cx).session_id();
420 let window_id = window.window_handle().window_id().as_u64();
421 workspace.update(cx, |workspace, _cx| {
422 workspace.set_database_id(workspace_id);
423 });
424 cx.background_spawn(async move {
425 crate::persistence::DB
426 .set_session_binding(workspace_id, session_id, Some(window_id))
427 .await
428 .log_err();
429 })
430 .detach();
431 } else {
432 cx.background_spawn(async move {
433 crate::persistence::DB
434 .delete_workspace_by_id(workspace_id)
435 .await
436 .log_err();
437 })
438 .detach();
439 }
440 this.serialize(cx);
441 }
442 Err(error) => {
443 log::error!("Failed to create workspace: {error:#}");
444 if let Some(index) = weak_workspace
445 .upgrade()
446 .and_then(|w| this.workspaces.iter().position(|ws| *ws == w))
447 {
448 this.remove_workspace(index, window, cx);
449 }
450 this.workspace().update(cx, |workspace, cx| {
451 let id = NotificationId::unique::<MultiWorkspace>();
452 workspace.show_toast(
453 Toast::new(id, format!("Failed to create workspace: {error}")),
454 cx,
455 );
456 });
457 }
458 })
459 .log_err();
460 }));
461 }
462
463 pub fn remove_workspace(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
464 if self.workspaces.len() <= 1 || index >= self.workspaces.len() {
465 return;
466 }
467
468 let removed_workspace = self.workspaces.remove(index);
469
470 if self.active_workspace_index >= self.workspaces.len() {
471 self.active_workspace_index = self.workspaces.len() - 1;
472 } else if self.active_workspace_index > index {
473 self.active_workspace_index -= 1;
474 }
475
476 if let Some(workspace_id) = removed_workspace.read(cx).database_id() {
477 self.pending_removal_tasks.retain(|task| !task.is_ready());
478 self.pending_removal_tasks
479 .push(cx.background_spawn(async move {
480 crate::persistence::DB
481 .delete_workspace_by_id(workspace_id)
482 .await
483 .log_err();
484 }));
485 }
486
487 self.serialize(cx);
488 self.focus_active_workspace(window, cx);
489 cx.emit(MultiWorkspaceEvent::WorkspaceRemoved(
490 removed_workspace.entity_id(),
491 ));
492 cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
493 cx.notify();
494 }
495
496 pub fn open_project(
497 &mut self,
498 paths: Vec<PathBuf>,
499 window: &mut Window,
500 cx: &mut Context<Self>,
501 ) -> Task<Result<()>> {
502 let workspace = self.workspace().clone();
503
504 if multi_workspace_enabled(cx) {
505 workspace.update(cx, |workspace, cx| {
506 workspace.open_workspace_for_paths(true, paths, window, cx)
507 })
508 } else {
509 cx.spawn_in(window, async move |_this, cx| {
510 let should_continue = workspace
511 .update_in(cx, |workspace, window, cx| {
512 workspace.prepare_to_close(crate::CloseIntent::ReplaceWindow, window, cx)
513 })?
514 .await?;
515 if should_continue {
516 workspace
517 .update_in(cx, |workspace, window, cx| {
518 workspace.open_workspace_for_paths(true, paths, window, cx)
519 })?
520 .await
521 } else {
522 Ok(())
523 }
524 })
525 }
526 }
527}
528
529impl Render for MultiWorkspace {
530 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
531 let ui_font = theme::setup_ui_font(window, cx);
532 let text_color = cx.theme().colors().text;
533
534 let workspace = self.workspace().clone();
535 let workspace_key_context = workspace.update(cx, |workspace, cx| workspace.key_context(cx));
536 let root = workspace.update(cx, |workspace, cx| workspace.actions(h_flex(), window, cx));
537
538 client_side_decorations(
539 root.key_context(workspace_key_context)
540 .relative()
541 .size_full()
542 .font(ui_font)
543 .text_color(text_color)
544 .on_action(cx.listener(Self::close_window))
545 .on_action(
546 cx.listener(|this: &mut Self, _: &NewWorkspaceInWindow, window, cx| {
547 this.create_workspace(window, cx);
548 }),
549 )
550 .on_action(
551 cx.listener(|this: &mut Self, _: &NextWorkspaceInWindow, window, cx| {
552 this.activate_next_workspace(window, cx);
553 }),
554 )
555 .on_action(cx.listener(
556 |this: &mut Self, _: &PreviousWorkspaceInWindow, window, cx| {
557 this.activate_previous_workspace(window, cx);
558 },
559 ))
560 .child(
561 div()
562 .flex()
563 .flex_1()
564 .size_full()
565 .overflow_hidden()
566 .child(self.workspace().clone()),
567 )
568 .child(self.workspace().read(cx).modal_layer.clone()),
569 window,
570 cx,
571 Tiling {
572 left: false,
573 ..Tiling::default()
574 },
575 )
576 }
577}