multi_workspace.rs

  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}