workspace.rs

    1pub mod active_file_name;
    2pub mod dock;
    3pub mod history_manager;
    4pub mod invalid_item_view;
    5pub mod item;
    6mod modal_layer;
    7mod multi_workspace;
    8#[cfg(test)]
    9mod multi_workspace_tests;
   10pub mod notifications;
   11pub mod pane;
   12pub mod pane_group;
   13pub mod path_list {
   14    pub use util::path_list::{PathList, SerializedPathList};
   15}
   16mod persistence;
   17pub mod searchable;
   18mod security_modal;
   19pub mod shared_screen;
   20use db::smol::future::yield_now;
   21pub use shared_screen::SharedScreen;
   22pub mod focus_follows_mouse;
   23mod status_bar;
   24pub mod tasks;
   25mod theme_preview;
   26mod toast_layer;
   27mod toolbar;
   28pub mod welcome;
   29mod workspace_settings;
   30
   31pub use crate::notifications::NotificationFrame;
   32pub use dock::Panel;
   33pub use multi_workspace::{
   34    CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MoveProjectToNewWindow,
   35    MultiWorkspace, MultiWorkspaceEvent, NewThread, NextProject, NextThread, PreviousProject,
   36    PreviousThread, ProjectGroup, ProjectGroupKey, SerializedProjectGroupState, Sidebar,
   37    SidebarEvent, SidebarHandle, SidebarRenderState, SidebarSide, ToggleWorkspaceSidebar,
   38    sidebar_side_context_menu,
   39};
   40pub use path_list::{PathList, SerializedPathList};
   41pub use remote::{
   42    RemoteConnectionIdentity, remote_connection_identity, same_remote_connection_identity,
   43};
   44pub use toast_layer::{ToastAction, ToastLayer, ToastView};
   45
   46use anyhow::{Context as _, Result, anyhow};
   47use client::{
   48    ChannelId, Client, ErrorExt, ParticipantIndex, Status, TypedEnvelope, User, UserStore,
   49    proto::{self, ErrorCode, PanelId, PeerId},
   50};
   51use collections::{HashMap, HashSet, hash_map};
   52use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
   53use fs::Fs;
   54use futures::{
   55    Future, FutureExt, StreamExt,
   56    channel::{
   57        mpsc::{self, UnboundedReceiver, UnboundedSender},
   58        oneshot,
   59    },
   60    future::{Shared, try_join_all},
   61};
   62use gpui::{
   63    Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Axis, Bounds,
   64    Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
   65    Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton,
   66    PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription,
   67    SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId,
   68    WindowOptions, actions, canvas, point, relative, size, transparent_black,
   69};
   70pub use history_manager::*;
   71pub use item::{
   72    FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
   73    ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
   74};
   75use itertools::Itertools;
   76use language::{Buffer, LanguageRegistry, Rope, language_settings::all_language_settings};
   77pub use modal_layer::*;
   78use node_runtime::NodeRuntime;
   79use notifications::{
   80    DetachAndPromptErr, Notifications, dismiss_app_notification,
   81    simple_message_notification::MessageNotification,
   82};
   83pub use pane::*;
   84pub use pane_group::{
   85    ActivePaneDecorator, HANDLE_HITBOX_SIZE, Member, PaneAxis, PaneGroup, PaneRenderContext,
   86    SplitDirection,
   87};
   88use persistence::{SerializedWindowBounds, model::SerializedWorkspace};
   89pub use persistence::{
   90    WorkspaceDb, delete_unloaded_items,
   91    model::{
   92        DockData, DockStructure, ItemId, MultiWorkspaceState, SerializedMultiWorkspace,
   93        SerializedProjectGroup, SerializedWorkspaceLocation, SessionWorkspace,
   94    },
   95    read_serialized_multi_workspaces, resolve_worktree_workspaces,
   96};
   97use postage::stream::Stream;
   98use project::{
   99    DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
  100    WorktreeSettings,
  101    debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
  102    project_settings::ProjectSettings,
  103    toolchain_store::ToolchainStoreEvent,
  104    trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, TrustedWorktreesEvent},
  105};
  106use remote::{
  107    RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
  108    remote_client::ConnectionIdentifier,
  109};
  110use schemars::JsonSchema;
  111use serde::Deserialize;
  112use session::AppSession;
  113use settings::{
  114    CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file,
  115};
  116
  117use sqlez::{
  118    bindable::{Bind, Column, StaticColumnCount},
  119    statement::Statement,
  120};
  121use status_bar::StatusBar;
  122pub use status_bar::StatusItemView;
  123use std::{
  124    any::TypeId,
  125    borrow::Cow,
  126    cell::RefCell,
  127    cmp,
  128    collections::VecDeque,
  129    env,
  130    hash::Hash,
  131    path::{Path, PathBuf},
  132    process::ExitStatus,
  133    rc::Rc,
  134    sync::{
  135        Arc, LazyLock,
  136        atomic::{AtomicBool, AtomicUsize},
  137    },
  138    time::Duration,
  139};
  140use task::{DebugScenario, SharedTaskContext, SpawnInTerminal};
  141use theme::{ActiveTheme, SystemAppearance};
  142use theme_settings::ThemeSettings;
  143pub use toolbar::{
  144    PaneSearchBarCallbacks, Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
  145};
  146pub use ui;
  147use ui::{Window, prelude::*};
  148use util::{
  149    ResultExt, TryFutureExt,
  150    paths::{PathStyle, SanitizedPath},
  151    rel_path::RelPath,
  152    serde::default_true,
  153};
  154use uuid::Uuid;
  155pub use workspace_settings::{
  156    AutosaveSetting, BottomDockLayout, FocusFollowsMouse, RestoreOnStartupBehavior,
  157    StatusBarSettings, TabBarSettings, WorkspaceSettings,
  158};
  159use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode};
  160
  161use crate::{dock::PanelSizeState, item::ItemBufferKind, notifications::NotificationId};
  162use crate::{
  163    persistence::{
  164        SerializedAxis,
  165        model::{SerializedItem, SerializedPane, SerializedPaneGroup},
  166    },
  167    security_modal::SecurityModal,
  168};
  169
  170pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
  171
  172static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
  173    env::var("ZED_WINDOW_SIZE")
  174        .ok()
  175        .as_deref()
  176        .and_then(parse_pixel_size_env_var)
  177});
  178
  179static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
  180    env::var("ZED_WINDOW_POSITION")
  181        .ok()
  182        .as_deref()
  183        .and_then(parse_pixel_position_env_var)
  184});
  185
  186pub trait TerminalProvider {
  187    fn spawn(
  188        &self,
  189        task: SpawnInTerminal,
  190        window: &mut Window,
  191        cx: &mut App,
  192    ) -> Task<Option<Result<ExitStatus>>>;
  193}
  194
  195pub trait DebuggerProvider {
  196    // `active_buffer` is used to resolve build task's name against language-specific tasks.
  197    fn start_session(
  198        &self,
  199        definition: DebugScenario,
  200        task_context: SharedTaskContext,
  201        active_buffer: Option<Entity<Buffer>>,
  202        worktree_id: Option<WorktreeId>,
  203        window: &mut Window,
  204        cx: &mut App,
  205    );
  206
  207    fn spawn_task_or_modal(
  208        &self,
  209        workspace: &mut Workspace,
  210        action: &Spawn,
  211        window: &mut Window,
  212        cx: &mut Context<Workspace>,
  213    );
  214
  215    fn task_scheduled(&self, cx: &mut App);
  216    fn debug_scenario_scheduled(&self, cx: &mut App);
  217    fn debug_scenario_scheduled_last(&self, cx: &App) -> bool;
  218
  219    fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus>;
  220}
  221
  222/// Opens a file or directory.
  223#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
  224#[action(namespace = workspace)]
  225pub struct Open {
  226    /// When true, opens in a new window. When false, adds to the current
  227    /// window as a new workspace (multi-workspace).
  228    #[serde(default = "Open::default_create_new_window")]
  229    pub create_new_window: bool,
  230}
  231
  232impl Open {
  233    pub const DEFAULT: Self = Self {
  234        create_new_window: false,
  235    };
  236
  237    /// Used by `#[serde(default)]` on the `create_new_window` field so that
  238    /// the serde default and `Open::DEFAULT` stay in sync.
  239    fn default_create_new_window() -> bool {
  240        Self::DEFAULT.create_new_window
  241    }
  242}
  243
  244impl Default for Open {
  245    fn default() -> Self {
  246        Self::DEFAULT
  247    }
  248}
  249
  250actions!(
  251    workspace,
  252    [
  253        /// Activates the next pane in the workspace.
  254        ActivateNextPane,
  255        /// Activates the previous pane in the workspace.
  256        ActivatePreviousPane,
  257        /// Activates the last pane in the workspace.
  258        ActivateLastPane,
  259        /// Switches to the next window.
  260        ActivateNextWindow,
  261        /// Switches to the previous window.
  262        ActivatePreviousWindow,
  263        /// Adds a folder to the current project.
  264        AddFolderToProject,
  265        /// Clears all bookmarks in the project.
  266        ClearBookmarks,
  267        /// Clears all notifications.
  268        ClearAllNotifications,
  269        /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**.
  270        ClearNavigationHistory,
  271        /// Closes the active dock.
  272        CloseActiveDock,
  273        /// Closes all docks.
  274        CloseAllDocks,
  275        /// Toggles all docks.
  276        ToggleAllDocks,
  277        /// Closes the current window.
  278        CloseWindow,
  279        /// Closes the current project.
  280        CloseProject,
  281        /// Opens the feedback dialog.
  282        Feedback,
  283        /// Follows the next collaborator in the session.
  284        FollowNextCollaborator,
  285        /// Moves the focused panel to the next position.
  286        MoveFocusedPanelToNextPosition,
  287        /// Creates a new file.
  288        NewFile,
  289        /// Creates a new file in a vertical split.
  290        NewFileSplitVertical,
  291        /// Creates a new file in a horizontal split.
  292        NewFileSplitHorizontal,
  293        /// Opens a new search.
  294        NewSearch,
  295        /// Opens a new window.
  296        NewWindow,
  297        /// Opens multiple files.
  298        OpenFiles,
  299        /// Opens the current location in terminal.
  300        OpenInTerminal,
  301        /// Opens the component preview.
  302        OpenComponentPreview,
  303        /// Reloads the active item.
  304        ReloadActiveItem,
  305        /// Resets the active dock to its default size.
  306        ResetActiveDockSize,
  307        /// Resets all open docks to their default sizes.
  308        ResetOpenDocksSize,
  309        /// Reloads the application
  310        Reload,
  311        /// Formats and saves the current file, regardless of the format_on_save setting.
  312        FormatAndSave,
  313        /// Saves the current file with a new name.
  314        SaveAs,
  315        /// Saves without formatting.
  316        SaveWithoutFormat,
  317        /// Shuts down all debug adapters.
  318        ShutdownDebugAdapters,
  319        /// Suppresses the current notification.
  320        SuppressNotification,
  321        /// Toggles the bottom dock.
  322        ToggleBottomDock,
  323        /// Toggles centered layout mode.
  324        ToggleCenteredLayout,
  325        /// Toggles edit prediction feature globally for all files.
  326        ToggleEditPrediction,
  327        /// Toggles the left dock.
  328        ToggleLeftDock,
  329        /// Toggles the right dock.
  330        ToggleRightDock,
  331        /// Toggles zoom on the active pane.
  332        ToggleZoom,
  333        /// Toggles read-only mode for the active item (if supported by that item).
  334        ToggleReadOnlyFile,
  335        /// Zooms in on the active pane.
  336        ZoomIn,
  337        /// Zooms out of the active pane.
  338        ZoomOut,
  339        /// If any worktrees are in restricted mode, shows a modal with possible actions.
  340        /// If the modal is shown already, closes it without trusting any worktree.
  341        ToggleWorktreeSecurity,
  342        /// Clears all trusted worktrees, placing them in restricted mode on next open.
  343        /// Requires restart to take effect on already opened projects.
  344        ClearTrustedWorktrees,
  345        /// Stops following a collaborator.
  346        Unfollow,
  347        /// Restores the banner.
  348        RestoreBanner,
  349        /// Toggles expansion of the selected item.
  350        ToggleExpandItem,
  351    ]
  352);
  353
  354/// Activates a specific pane by its index.
  355#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
  356#[action(namespace = workspace)]
  357pub struct ActivatePane(pub usize);
  358
  359/// Moves an item to a specific pane by index.
  360#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
  361#[action(namespace = workspace)]
  362#[serde(deny_unknown_fields)]
  363pub struct MoveItemToPane {
  364    #[serde(default = "default_1")]
  365    pub destination: usize,
  366    #[serde(default = "default_true")]
  367    pub focus: bool,
  368    #[serde(default)]
  369    pub clone: bool,
  370}
  371
  372fn default_1() -> usize {
  373    1
  374}
  375
  376/// Moves an item to a pane in the specified direction.
  377#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
  378#[action(namespace = workspace)]
  379#[serde(deny_unknown_fields)]
  380pub struct MoveItemToPaneInDirection {
  381    #[serde(default = "default_right")]
  382    pub direction: SplitDirection,
  383    #[serde(default = "default_true")]
  384    pub focus: bool,
  385    #[serde(default)]
  386    pub clone: bool,
  387}
  388
  389/// Creates a new file in a split of the desired direction.
  390#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
  391#[action(namespace = workspace)]
  392#[serde(deny_unknown_fields)]
  393pub struct NewFileSplit(pub SplitDirection);
  394
  395fn default_right() -> SplitDirection {
  396    SplitDirection::Right
  397}
  398
  399/// Saves all open files in the workspace.
  400#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
  401#[action(namespace = workspace)]
  402#[serde(deny_unknown_fields)]
  403pub struct SaveAll {
  404    #[serde(default)]
  405    pub save_intent: Option<SaveIntent>,
  406}
  407
  408/// Saves the current file with the specified options.
  409#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
  410#[action(namespace = workspace)]
  411#[serde(deny_unknown_fields)]
  412pub struct Save {
  413    #[serde(default)]
  414    pub save_intent: Option<SaveIntent>,
  415}
  416
  417/// Moves Focus to the central panes in the workspace.
  418#[derive(Clone, Debug, PartialEq, Eq, Action)]
  419#[action(namespace = workspace)]
  420pub struct FocusCenterPane;
  421
  422///  Closes all items and panes in the workspace.
  423#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
  424#[action(namespace = workspace)]
  425#[serde(deny_unknown_fields)]
  426pub struct CloseAllItemsAndPanes {
  427    #[serde(default)]
  428    pub save_intent: Option<SaveIntent>,
  429}
  430
  431/// Closes all inactive tabs and panes in the workspace.
  432#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
  433#[action(namespace = workspace)]
  434#[serde(deny_unknown_fields)]
  435pub struct CloseInactiveTabsAndPanes {
  436    #[serde(default)]
  437    pub save_intent: Option<SaveIntent>,
  438}
  439
  440/// Closes the active item across all panes.
  441#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
  442#[action(namespace = workspace)]
  443#[serde(deny_unknown_fields)]
  444pub struct CloseItemInAllPanes {
  445    #[serde(default)]
  446    pub save_intent: Option<SaveIntent>,
  447    #[serde(default)]
  448    pub close_pinned: bool,
  449}
  450
  451/// Sends a sequence of keystrokes to the active element.
  452#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
  453#[action(namespace = workspace)]
  454pub struct SendKeystrokes(pub String);
  455
  456actions!(
  457    project_symbols,
  458    [
  459        /// Toggles the project symbols search.
  460        #[action(name = "Toggle")]
  461        ToggleProjectSymbols
  462    ]
  463);
  464
  465/// Toggles the file finder interface.
  466#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
  467#[action(namespace = file_finder, name = "Toggle")]
  468#[serde(deny_unknown_fields)]
  469pub struct ToggleFileFinder {
  470    #[serde(default)]
  471    pub separate_history: bool,
  472}
  473
  474/// Opens a new terminal in the center.
  475#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
  476#[action(namespace = workspace)]
  477#[serde(deny_unknown_fields)]
  478pub struct NewCenterTerminal {
  479    /// If true, creates a local terminal even in remote projects.
  480    #[serde(default)]
  481    pub local: bool,
  482}
  483
  484/// Opens a new terminal.
  485#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
  486#[action(namespace = workspace)]
  487#[serde(deny_unknown_fields)]
  488pub struct NewTerminal {
  489    /// If true, creates a local terminal even in remote projects.
  490    #[serde(default)]
  491    pub local: bool,
  492}
  493
  494/// Increases size of a currently focused dock by a given amount of pixels.
  495#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
  496#[action(namespace = workspace)]
  497#[serde(deny_unknown_fields)]
  498pub struct IncreaseActiveDockSize {
  499    /// For 0px parameter, uses UI font size value.
  500    #[serde(default)]
  501    pub px: u32,
  502}
  503
  504/// Decreases size of a currently focused dock by a given amount of pixels.
  505#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
  506#[action(namespace = workspace)]
  507#[serde(deny_unknown_fields)]
  508pub struct DecreaseActiveDockSize {
  509    /// For 0px parameter, uses UI font size value.
  510    #[serde(default)]
  511    pub px: u32,
  512}
  513
  514/// Increases size of all currently visible docks uniformly, by a given amount of pixels.
  515#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
  516#[action(namespace = workspace)]
  517#[serde(deny_unknown_fields)]
  518pub struct IncreaseOpenDocksSize {
  519    /// For 0px parameter, uses UI font size value.
  520    #[serde(default)]
  521    pub px: u32,
  522}
  523
  524/// Decreases size of all currently visible docks uniformly, by a given amount of pixels.
  525#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
  526#[action(namespace = workspace)]
  527#[serde(deny_unknown_fields)]
  528pub struct DecreaseOpenDocksSize {
  529    /// For 0px parameter, uses UI font size value.
  530    #[serde(default)]
  531    pub px: u32,
  532}
  533
  534actions!(
  535    workspace,
  536    [
  537        /// Activates the pane to the left.
  538        ActivatePaneLeft,
  539        /// Activates the pane to the right.
  540        ActivatePaneRight,
  541        /// Activates the pane above.
  542        ActivatePaneUp,
  543        /// Activates the pane below.
  544        ActivatePaneDown,
  545        /// Swaps the current pane with the one to the left.
  546        SwapPaneLeft,
  547        /// Swaps the current pane with the one to the right.
  548        SwapPaneRight,
  549        /// Swaps the current pane with the one above.
  550        SwapPaneUp,
  551        /// Swaps the current pane with the one below.
  552        SwapPaneDown,
  553        // Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane.
  554        SwapPaneAdjacent,
  555        /// Move the current pane to be at the far left.
  556        MovePaneLeft,
  557        /// Move the current pane to be at the far right.
  558        MovePaneRight,
  559        /// Move the current pane to be at the very top.
  560        MovePaneUp,
  561        /// Move the current pane to be at the very bottom.
  562        MovePaneDown,
  563    ]
  564);
  565
  566#[derive(PartialEq, Eq, Debug)]
  567pub enum CloseIntent {
  568    /// Quit the program entirely.
  569    Quit,
  570    /// Close a window.
  571    CloseWindow,
  572    /// Replace the workspace in an existing window.
  573    ReplaceWindow,
  574}
  575
  576#[derive(Clone)]
  577pub struct Toast {
  578    id: NotificationId,
  579    msg: Cow<'static, str>,
  580    autohide: bool,
  581    on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
  582}
  583
  584impl Toast {
  585    pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
  586        Toast {
  587            id,
  588            msg: msg.into(),
  589            on_click: None,
  590            autohide: false,
  591        }
  592    }
  593
  594    pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
  595    where
  596        M: Into<Cow<'static, str>>,
  597        F: Fn(&mut Window, &mut App) + 'static,
  598    {
  599        self.on_click = Some((message.into(), Arc::new(on_click)));
  600        self
  601    }
  602
  603    pub fn autohide(mut self) -> Self {
  604        self.autohide = true;
  605        self
  606    }
  607}
  608
  609impl PartialEq for Toast {
  610    fn eq(&self, other: &Self) -> bool {
  611        self.id == other.id
  612            && self.msg == other.msg
  613            && self.on_click.is_some() == other.on_click.is_some()
  614    }
  615}
  616
  617/// Opens a new terminal with the specified working directory.
  618#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)]
  619#[action(namespace = workspace)]
  620#[serde(deny_unknown_fields)]
  621pub struct OpenTerminal {
  622    pub working_directory: PathBuf,
  623    /// If true, creates a local terminal even in remote projects.
  624    #[serde(default)]
  625    pub local: bool,
  626}
  627
  628#[derive(
  629    Clone,
  630    Copy,
  631    Debug,
  632    Default,
  633    Hash,
  634    PartialEq,
  635    Eq,
  636    PartialOrd,
  637    Ord,
  638    serde::Serialize,
  639    serde::Deserialize,
  640)]
  641pub struct WorkspaceId(i64);
  642
  643impl WorkspaceId {
  644    pub fn from_i64(value: i64) -> Self {
  645        Self(value)
  646    }
  647}
  648
  649impl StaticColumnCount for WorkspaceId {}
  650impl Bind for WorkspaceId {
  651    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
  652        self.0.bind(statement, start_index)
  653    }
  654}
  655impl Column for WorkspaceId {
  656    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
  657        i64::column(statement, start_index)
  658            .map(|(i, next_index)| (Self(i), next_index))
  659            .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
  660    }
  661}
  662impl From<WorkspaceId> for i64 {
  663    fn from(val: WorkspaceId) -> Self {
  664        val.0
  665    }
  666}
  667
  668fn prompt_and_open_paths(
  669    app_state: Arc<AppState>,
  670    options: PathPromptOptions,
  671    create_new_window: bool,
  672    cx: &mut App,
  673) {
  674    if let Some(workspace_window) = local_workspace_windows(cx).into_iter().next() {
  675        workspace_window
  676            .update(cx, |multi_workspace, window, cx| {
  677                let workspace = multi_workspace.workspace().clone();
  678                workspace.update(cx, |workspace, cx| {
  679                    prompt_for_open_path_and_open(
  680                        workspace,
  681                        app_state,
  682                        options,
  683                        create_new_window,
  684                        window,
  685                        cx,
  686                    );
  687                });
  688            })
  689            .ok();
  690    } else {
  691        let task = Workspace::new_local(
  692            Vec::new(),
  693            app_state.clone(),
  694            None,
  695            None,
  696            None,
  697            OpenMode::Activate,
  698            cx,
  699        );
  700        cx.spawn(async move |cx| {
  701            let OpenResult { window, .. } = task.await?;
  702            window.update(cx, |multi_workspace, window, cx| {
  703                window.activate_window();
  704                let workspace = multi_workspace.workspace().clone();
  705                workspace.update(cx, |workspace, cx| {
  706                    prompt_for_open_path_and_open(
  707                        workspace,
  708                        app_state,
  709                        options,
  710                        create_new_window,
  711                        window,
  712                        cx,
  713                    );
  714                });
  715            })?;
  716            anyhow::Ok(())
  717        })
  718        .detach_and_log_err(cx);
  719    }
  720}
  721
  722pub fn prompt_for_open_path_and_open(
  723    workspace: &mut Workspace,
  724    app_state: Arc<AppState>,
  725    options: PathPromptOptions,
  726    create_new_window: bool,
  727    window: &mut Window,
  728    cx: &mut Context<Workspace>,
  729) {
  730    let paths = workspace.prompt_for_open_path(
  731        options,
  732        DirectoryLister::Local(workspace.project().clone(), app_state.fs.clone()),
  733        window,
  734        cx,
  735    );
  736    let multi_workspace_handle = window.window_handle().downcast::<MultiWorkspace>();
  737    cx.spawn_in(window, async move |this, cx| {
  738        let Some(paths) = paths.await.log_err().flatten() else {
  739            return;
  740        };
  741        if !create_new_window {
  742            if let Some(handle) = multi_workspace_handle {
  743                if let Some(task) = handle
  744                    .update(cx, |multi_workspace, window, cx| {
  745                        multi_workspace.open_project(paths, OpenMode::Activate, window, cx)
  746                    })
  747                    .log_err()
  748                {
  749                    task.await.log_err();
  750                }
  751                return;
  752            }
  753        }
  754        if let Some(task) = this
  755            .update_in(cx, |this, window, cx| {
  756                this.open_workspace_for_paths(OpenMode::NewWindow, paths, window, cx)
  757            })
  758            .log_err()
  759        {
  760            task.await.log_err();
  761        }
  762    })
  763    .detach();
  764}
  765
  766pub fn init(app_state: Arc<AppState>, cx: &mut App) {
  767    component::init();
  768    theme_preview::init(cx);
  769    toast_layer::init(cx);
  770    history_manager::init(app_state.fs.clone(), cx);
  771
  772    cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
  773        .on_action(|_: &Reload, cx| reload(cx))
  774        .on_action(|action: &Open, cx: &mut App| {
  775            let app_state = AppState::global(cx);
  776            prompt_and_open_paths(
  777                app_state,
  778                PathPromptOptions {
  779                    files: true,
  780                    directories: true,
  781                    multiple: true,
  782                    prompt: None,
  783                },
  784                action.create_new_window,
  785                cx,
  786            );
  787        })
  788        .on_action(|_: &OpenFiles, cx: &mut App| {
  789            let directories = cx.can_select_mixed_files_and_dirs();
  790            let app_state = AppState::global(cx);
  791            prompt_and_open_paths(
  792                app_state,
  793                PathPromptOptions {
  794                    files: true,
  795                    directories,
  796                    multiple: true,
  797                    prompt: None,
  798                },
  799                true,
  800                cx,
  801            );
  802        });
  803}
  804
  805type BuildProjectItemFn =
  806    fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
  807
  808type BuildProjectItemForPathFn =
  809    fn(
  810        &Entity<Project>,
  811        &ProjectPath,
  812        &mut Window,
  813        &mut App,
  814    ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
  815
  816#[derive(Clone, Default)]
  817struct ProjectItemRegistry {
  818    build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
  819    build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
  820}
  821
  822impl ProjectItemRegistry {
  823    fn register<T: ProjectItem>(&mut self) {
  824        self.build_project_item_fns_by_type.insert(
  825            TypeId::of::<T::Item>(),
  826            |item, project, pane, window, cx| {
  827                let item = item.downcast().unwrap();
  828                Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
  829                    as Box<dyn ItemHandle>
  830            },
  831        );
  832        self.build_project_item_for_path_fns
  833            .push(|project, project_path, window, cx| {
  834                let project_path = project_path.clone();
  835                let is_file = project
  836                    .read(cx)
  837                    .entry_for_path(&project_path, cx)
  838                    .is_some_and(|entry| entry.is_file());
  839                let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
  840                let is_local = project.read(cx).is_local();
  841                let project_item =
  842                    <T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
  843                let project = project.clone();
  844                Some(window.spawn(cx, async move |cx| {
  845                    match project_item.await.with_context(|| {
  846                        format!(
  847                            "opening project path {:?}",
  848                            entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
  849                        )
  850                    }) {
  851                        Ok(project_item) => {
  852                            let project_item = project_item;
  853                            let project_entry_id: Option<ProjectEntryId> =
  854                                project_item.read_with(cx, project::ProjectItem::entry_id);
  855                            let build_workspace_item = Box::new(
  856                                |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
  857                                    Box::new(cx.new(|cx| {
  858                                        T::for_project_item(
  859                                            project,
  860                                            Some(pane),
  861                                            project_item,
  862                                            window,
  863                                            cx,
  864                                        )
  865                                    })) as Box<dyn ItemHandle>
  866                                },
  867                            ) as Box<_>;
  868                            Ok((project_entry_id, build_workspace_item))
  869                        }
  870                        Err(e) => {
  871                            log::warn!("Failed to open a project item: {e:#}");
  872                            if e.error_code() == ErrorCode::Internal {
  873                                if let Some(abs_path) =
  874                                    entry_abs_path.as_deref().filter(|_| is_file)
  875                                {
  876                                    if let Some(broken_project_item_view) =
  877                                        cx.update(|window, cx| {
  878                                            T::for_broken_project_item(
  879                                                abs_path, is_local, &e, window, cx,
  880                                            )
  881                                        })?
  882                                    {
  883                                        let build_workspace_item = Box::new(
  884                                            move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
  885                                                cx.new(|_| broken_project_item_view).boxed_clone()
  886                                            },
  887                                        )
  888                                        as Box<_>;
  889                                        return Ok((None, build_workspace_item));
  890                                    }
  891                                }
  892                            }
  893                            Err(e)
  894                        }
  895                    }
  896                }))
  897            });
  898    }
  899
  900    fn open_path(
  901        &self,
  902        project: &Entity<Project>,
  903        path: &ProjectPath,
  904        window: &mut Window,
  905        cx: &mut App,
  906    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
  907        let Some(open_project_item) = self
  908            .build_project_item_for_path_fns
  909            .iter()
  910            .rev()
  911            .find_map(|open_project_item| open_project_item(project, path, window, cx))
  912        else {
  913            return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
  914        };
  915        open_project_item
  916    }
  917
  918    fn build_item<T: project::ProjectItem>(
  919        &self,
  920        item: Entity<T>,
  921        project: Entity<Project>,
  922        pane: Option<&Pane>,
  923        window: &mut Window,
  924        cx: &mut App,
  925    ) -> Option<Box<dyn ItemHandle>> {
  926        let build = self
  927            .build_project_item_fns_by_type
  928            .get(&TypeId::of::<T>())?;
  929        Some(build(item.into_any(), project, pane, window, cx))
  930    }
  931}
  932
  933type WorkspaceItemBuilder =
  934    Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
  935
  936impl Global for ProjectItemRegistry {}
  937
  938/// Registers a [ProjectItem] for the app. When opening a file, all the registered
  939/// items will get a chance to open the file, starting from the project item that
  940/// was added last.
  941pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
  942    cx.default_global::<ProjectItemRegistry>().register::<I>();
  943}
  944
  945#[derive(Default)]
  946pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
  947
  948struct FollowableViewDescriptor {
  949    from_state_proto: fn(
  950        Entity<Workspace>,
  951        ViewId,
  952        &mut Option<proto::view::Variant>,
  953        &mut Window,
  954        &mut App,
  955    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
  956    to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
  957}
  958
  959impl Global for FollowableViewRegistry {}
  960
  961impl FollowableViewRegistry {
  962    pub fn register<I: FollowableItem>(cx: &mut App) {
  963        cx.default_global::<Self>().0.insert(
  964            TypeId::of::<I>(),
  965            FollowableViewDescriptor {
  966                from_state_proto: |workspace, id, state, window, cx| {
  967                    I::from_state_proto(workspace, id, state, window, cx).map(|task| {
  968                        cx.foreground_executor()
  969                            .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
  970                    })
  971                },
  972                to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
  973            },
  974        );
  975    }
  976
  977    pub fn from_state_proto(
  978        workspace: Entity<Workspace>,
  979        view_id: ViewId,
  980        mut state: Option<proto::view::Variant>,
  981        window: &mut Window,
  982        cx: &mut App,
  983    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
  984        cx.update_default_global(|this: &mut Self, cx| {
  985            this.0.values().find_map(|descriptor| {
  986                (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
  987            })
  988        })
  989    }
  990
  991    pub fn to_followable_view(
  992        view: impl Into<AnyView>,
  993        cx: &App,
  994    ) -> Option<Box<dyn FollowableItemHandle>> {
  995        let this = cx.try_global::<Self>()?;
  996        let view = view.into();
  997        let descriptor = this.0.get(&view.entity_type())?;
  998        Some((descriptor.to_followable_view)(&view))
  999    }
 1000}
 1001
 1002#[derive(Copy, Clone)]
 1003struct SerializableItemDescriptor {
 1004    deserialize: fn(
 1005        Entity<Project>,
 1006        WeakEntity<Workspace>,
 1007        WorkspaceId,
 1008        ItemId,
 1009        &mut Window,
 1010        &mut Context<Pane>,
 1011    ) -> Task<Result<Box<dyn ItemHandle>>>,
 1012    cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
 1013    view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
 1014}
 1015
 1016#[derive(Default)]
 1017struct SerializableItemRegistry {
 1018    descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
 1019    descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
 1020}
 1021
 1022impl Global for SerializableItemRegistry {}
 1023
 1024impl SerializableItemRegistry {
 1025    fn deserialize(
 1026        item_kind: &str,
 1027        project: Entity<Project>,
 1028        workspace: WeakEntity<Workspace>,
 1029        workspace_id: WorkspaceId,
 1030        item_item: ItemId,
 1031        window: &mut Window,
 1032        cx: &mut Context<Pane>,
 1033    ) -> Task<Result<Box<dyn ItemHandle>>> {
 1034        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
 1035            return Task::ready(Err(anyhow!(
 1036                "cannot deserialize {}, descriptor not found",
 1037                item_kind
 1038            )));
 1039        };
 1040
 1041        (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
 1042    }
 1043
 1044    fn cleanup(
 1045        item_kind: &str,
 1046        workspace_id: WorkspaceId,
 1047        loaded_items: Vec<ItemId>,
 1048        window: &mut Window,
 1049        cx: &mut App,
 1050    ) -> Task<Result<()>> {
 1051        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
 1052            return Task::ready(Err(anyhow!(
 1053                "cannot cleanup {}, descriptor not found",
 1054                item_kind
 1055            )));
 1056        };
 1057
 1058        (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
 1059    }
 1060
 1061    fn view_to_serializable_item_handle(
 1062        view: AnyView,
 1063        cx: &App,
 1064    ) -> Option<Box<dyn SerializableItemHandle>> {
 1065        let this = cx.try_global::<Self>()?;
 1066        let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
 1067        Some((descriptor.view_to_serializable_item)(view))
 1068    }
 1069
 1070    fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
 1071        let this = cx.try_global::<Self>()?;
 1072        this.descriptors_by_kind.get(item_kind).copied()
 1073    }
 1074}
 1075
 1076pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
 1077    let serialized_item_kind = I::serialized_item_kind();
 1078
 1079    let registry = cx.default_global::<SerializableItemRegistry>();
 1080    let descriptor = SerializableItemDescriptor {
 1081        deserialize: |project, workspace, workspace_id, item_id, window, cx| {
 1082            let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
 1083            cx.foreground_executor()
 1084                .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
 1085        },
 1086        cleanup: |workspace_id, loaded_items, window, cx| {
 1087            I::cleanup(workspace_id, loaded_items, window, cx)
 1088        },
 1089        view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
 1090    };
 1091    registry
 1092        .descriptors_by_kind
 1093        .insert(Arc::from(serialized_item_kind), descriptor);
 1094    registry
 1095        .descriptors_by_type
 1096        .insert(TypeId::of::<I>(), descriptor);
 1097}
 1098
 1099pub struct AppState {
 1100    pub languages: Arc<LanguageRegistry>,
 1101    pub client: Arc<Client>,
 1102    pub user_store: Entity<UserStore>,
 1103    pub workspace_store: Entity<WorkspaceStore>,
 1104    pub fs: Arc<dyn fs::Fs>,
 1105    pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
 1106    pub node_runtime: NodeRuntime,
 1107    pub session: Entity<AppSession>,
 1108}
 1109
 1110struct GlobalAppState(Arc<AppState>);
 1111
 1112impl Global for GlobalAppState {}
 1113
 1114/// Tracks worktree creation progress for the workspace.
 1115/// Read by the title bar to show a loading indicator on the worktree button.
 1116#[derive(Default)]
 1117pub struct ActiveWorktreeCreation {
 1118    pub label: Option<SharedString>,
 1119    pub is_switch: bool,
 1120}
 1121
 1122/// Captured workspace state used when switching between worktrees.
 1123/// Stores the layout and open files so they can be restored in the new workspace.
 1124pub struct PreviousWorkspaceState {
 1125    pub dock_structure: DockStructure,
 1126    pub open_file_paths: Vec<PathBuf>,
 1127    pub active_file_path: Option<PathBuf>,
 1128    pub focused_dock: Option<DockPosition>,
 1129}
 1130
 1131pub struct WorkspaceStore {
 1132    workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity<Workspace>)>,
 1133    client: Arc<Client>,
 1134    _subscriptions: Vec<client::Subscription>,
 1135}
 1136
 1137#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
 1138pub enum CollaboratorId {
 1139    PeerId(PeerId),
 1140    Agent,
 1141}
 1142
 1143impl From<PeerId> for CollaboratorId {
 1144    fn from(peer_id: PeerId) -> Self {
 1145        CollaboratorId::PeerId(peer_id)
 1146    }
 1147}
 1148
 1149impl From<&PeerId> for CollaboratorId {
 1150    fn from(peer_id: &PeerId) -> Self {
 1151        CollaboratorId::PeerId(*peer_id)
 1152    }
 1153}
 1154
 1155#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
 1156struct Follower {
 1157    project_id: Option<u64>,
 1158    peer_id: PeerId,
 1159}
 1160
 1161impl AppState {
 1162    #[track_caller]
 1163    pub fn global(cx: &App) -> Arc<Self> {
 1164        cx.global::<GlobalAppState>().0.clone()
 1165    }
 1166    pub fn try_global(cx: &App) -> Option<Arc<Self>> {
 1167        cx.try_global::<GlobalAppState>()
 1168            .map(|state| state.0.clone())
 1169    }
 1170    pub fn set_global(state: Arc<AppState>, cx: &mut App) {
 1171        cx.set_global(GlobalAppState(state));
 1172    }
 1173
 1174    #[cfg(any(test, feature = "test-support"))]
 1175    pub fn test(cx: &mut App) -> Arc<Self> {
 1176        use fs::Fs;
 1177        use node_runtime::NodeRuntime;
 1178        use session::Session;
 1179        use settings::SettingsStore;
 1180
 1181        if !cx.has_global::<SettingsStore>() {
 1182            let settings_store = SettingsStore::test(cx);
 1183            cx.set_global(settings_store);
 1184        }
 1185
 1186        let fs = fs::FakeFs::new(cx.background_executor().clone());
 1187        <dyn Fs>::set_global(fs.clone(), cx);
 1188        let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
 1189        let clock = Arc::new(clock::FakeSystemClock::new());
 1190        let http_client = http_client::FakeHttpClient::with_404_response();
 1191        let client = Client::new(clock, http_client, cx);
 1192        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
 1193        let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
 1194        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
 1195
 1196        theme_settings::init(theme::LoadThemes::JustBase, cx);
 1197        client::init(&client, cx);
 1198
 1199        Arc::new(Self {
 1200            client,
 1201            fs,
 1202            languages,
 1203            user_store,
 1204            workspace_store,
 1205            node_runtime: NodeRuntime::unavailable(),
 1206            build_window_options: |_, _| Default::default(),
 1207            session,
 1208        })
 1209    }
 1210}
 1211
 1212struct DelayedDebouncedEditAction {
 1213    task: Option<Task<()>>,
 1214    cancel_channel: Option<oneshot::Sender<()>>,
 1215}
 1216
 1217impl DelayedDebouncedEditAction {
 1218    fn new() -> DelayedDebouncedEditAction {
 1219        DelayedDebouncedEditAction {
 1220            task: None,
 1221            cancel_channel: None,
 1222        }
 1223    }
 1224
 1225    fn fire_new<F>(
 1226        &mut self,
 1227        delay: Duration,
 1228        window: &mut Window,
 1229        cx: &mut Context<Workspace>,
 1230        func: F,
 1231    ) where
 1232        F: 'static
 1233            + Send
 1234            + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
 1235    {
 1236        if let Some(channel) = self.cancel_channel.take() {
 1237            _ = channel.send(());
 1238        }
 1239
 1240        let (sender, mut receiver) = oneshot::channel::<()>();
 1241        self.cancel_channel = Some(sender);
 1242
 1243        let previous_task = self.task.take();
 1244        self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
 1245            let mut timer = cx.background_executor().timer(delay).fuse();
 1246            if let Some(previous_task) = previous_task {
 1247                previous_task.await;
 1248            }
 1249
 1250            futures::select_biased! {
 1251                _ = receiver => return,
 1252                    _ = timer => {}
 1253            }
 1254
 1255            if let Some(result) = workspace
 1256                .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
 1257                .log_err()
 1258            {
 1259                result.await.log_err();
 1260            }
 1261        }));
 1262    }
 1263}
 1264
 1265pub enum Event {
 1266    PaneAdded(Entity<Pane>),
 1267    PaneRemoved,
 1268    ItemAdded {
 1269        item: Box<dyn ItemHandle>,
 1270    },
 1271    ActiveItemChanged,
 1272    ItemRemoved {
 1273        item_id: EntityId,
 1274    },
 1275    UserSavedItem {
 1276        pane: WeakEntity<Pane>,
 1277        item: Box<dyn WeakItemHandle>,
 1278        save_intent: SaveIntent,
 1279    },
 1280    ContactRequestedJoin(u64),
 1281    WorkspaceCreated(WeakEntity<Workspace>),
 1282    OpenBundledFile {
 1283        text: Cow<'static, str>,
 1284        title: &'static str,
 1285        language: &'static str,
 1286    },
 1287    ZoomChanged,
 1288    ModalOpened,
 1289    Activate,
 1290    PanelAdded(AnyView),
 1291    WorktreeCreationChanged,
 1292}
 1293
 1294#[derive(Debug, Clone)]
 1295pub enum OpenVisible {
 1296    All,
 1297    None,
 1298    OnlyFiles,
 1299    OnlyDirectories,
 1300}
 1301
 1302enum WorkspaceLocation {
 1303    // Valid local paths or SSH project to serialize
 1304    Location(SerializedWorkspaceLocation, PathList),
 1305    // No valid location found hence clear session id
 1306    DetachFromSession,
 1307    // No valid location found to serialize
 1308    None,
 1309}
 1310
 1311type PromptForNewPath = Box<
 1312    dyn Fn(
 1313        &mut Workspace,
 1314        DirectoryLister,
 1315        Option<String>,
 1316        &mut Window,
 1317        &mut Context<Workspace>,
 1318    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
 1319>;
 1320
 1321type PromptForOpenPath = Box<
 1322    dyn Fn(
 1323        &mut Workspace,
 1324        DirectoryLister,
 1325        &mut Window,
 1326        &mut Context<Workspace>,
 1327    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
 1328>;
 1329
 1330#[derive(Default)]
 1331struct DispatchingKeystrokes {
 1332    dispatched: HashSet<Vec<Keystroke>>,
 1333    queue: VecDeque<Keystroke>,
 1334    task: Option<Shared<Task<()>>>,
 1335}
 1336
 1337/// Collects everything project-related for a certain window opened.
 1338/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
 1339///
 1340/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
 1341/// The `Workspace` owns everybody's state and serves as a default, "global context",
 1342/// that can be used to register a global action to be triggered from any place in the window.
 1343pub struct Workspace {
 1344    weak_self: WeakEntity<Self>,
 1345    workspace_actions: Vec<Box<dyn Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div>>,
 1346    zoomed: Option<AnyWeakView>,
 1347    previous_dock_drag_coordinates: Option<Point<Pixels>>,
 1348    zoomed_position: Option<DockPosition>,
 1349    center: PaneGroup,
 1350    left_dock: Entity<Dock>,
 1351    bottom_dock: Entity<Dock>,
 1352    right_dock: Entity<Dock>,
 1353    panes: Vec<Entity<Pane>>,
 1354    panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
 1355    active_pane: Entity<Pane>,
 1356    last_active_center_pane: Option<WeakEntity<Pane>>,
 1357    last_active_view_id: Option<proto::ViewId>,
 1358    status_bar: Entity<StatusBar>,
 1359    pub(crate) modal_layer: Entity<ModalLayer>,
 1360    toast_layer: Entity<ToastLayer>,
 1361    titlebar_item: Option<AnyView>,
 1362    notifications: Notifications,
 1363    suppressed_notifications: HashSet<NotificationId>,
 1364    project: Entity<Project>,
 1365    follower_states: HashMap<CollaboratorId, FollowerState>,
 1366    last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
 1367    window_edited: bool,
 1368    last_window_title: Option<String>,
 1369    dirty_items: HashMap<EntityId, Subscription>,
 1370    active_call: Option<(GlobalAnyActiveCall, Vec<Subscription>)>,
 1371    leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
 1372    database_id: Option<WorkspaceId>,
 1373    app_state: Arc<AppState>,
 1374    dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
 1375    _subscriptions: Vec<Subscription>,
 1376    _apply_leader_updates: Task<Result<()>>,
 1377    _observe_current_user: Task<Result<()>>,
 1378    _schedule_serialize_workspace: Option<Task<()>>,
 1379    _serialize_workspace_task: Option<Task<()>>,
 1380    _schedule_serialize_ssh_paths: Option<Task<()>>,
 1381    pane_history_timestamp: Arc<AtomicUsize>,
 1382    bounds: Bounds<Pixels>,
 1383    pub centered_layout: bool,
 1384    bounds_save_task_queued: Option<Task<()>>,
 1385    on_prompt_for_new_path: Option<PromptForNewPath>,
 1386    on_prompt_for_open_path: Option<PromptForOpenPath>,
 1387    terminal_provider: Option<Box<dyn TerminalProvider>>,
 1388    debugger_provider: Option<Arc<dyn DebuggerProvider>>,
 1389    serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
 1390    _items_serializer: Task<Result<()>>,
 1391    session_id: Option<String>,
 1392    scheduled_tasks: Vec<Task<()>>,
 1393    last_open_dock_positions: Vec<DockPosition>,
 1394    removing: bool,
 1395    open_in_dev_container: bool,
 1396    _dev_container_task: Option<Task<Result<()>>>,
 1397    _panels_task: Option<Task<Result<()>>>,
 1398    sidebar_focus_handle: Option<FocusHandle>,
 1399    multi_workspace: Option<WeakEntity<MultiWorkspace>>,
 1400    active_worktree_creation: ActiveWorktreeCreation,
 1401    deferred_save_items: Vec<Box<dyn WeakItemHandle>>,
 1402}
 1403
 1404impl EventEmitter<Event> for Workspace {}
 1405
 1406#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 1407pub struct ViewId {
 1408    pub creator: CollaboratorId,
 1409    pub id: u64,
 1410}
 1411
 1412pub struct FollowerState {
 1413    center_pane: Entity<Pane>,
 1414    dock_pane: Option<Entity<Pane>>,
 1415    active_view_id: Option<ViewId>,
 1416    items_by_leader_view_id: HashMap<ViewId, FollowerView>,
 1417}
 1418
 1419struct FollowerView {
 1420    view: Box<dyn FollowableItemHandle>,
 1421    location: Option<proto::PanelId>,
 1422}
 1423
 1424#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
 1425pub enum OpenMode {
 1426    /// Open the workspace in a new window.
 1427    NewWindow,
 1428    /// Add to the window's multi workspace without activating it (used during deserialization).
 1429    Add,
 1430    /// Add to the window's multi workspace and activate it.
 1431    #[default]
 1432    Activate,
 1433}
 1434
 1435impl Workspace {
 1436    pub fn new(
 1437        workspace_id: Option<WorkspaceId>,
 1438        project: Entity<Project>,
 1439        app_state: Arc<AppState>,
 1440        window: &mut Window,
 1441        cx: &mut Context<Self>,
 1442    ) -> Self {
 1443        if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
 1444            cx.subscribe(&trusted_worktrees, |_, worktrees_store, e, cx| {
 1445                if let TrustedWorktreesEvent::Trusted(..) = e {
 1446                    // Do not persist auto trusted worktrees
 1447                    if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
 1448                        worktrees_store.update(cx, |worktrees_store, cx| {
 1449                            worktrees_store.schedule_serialization(
 1450                                cx,
 1451                                |new_trusted_worktrees, cx| {
 1452                                    let timeout =
 1453                                        cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
 1454                                    let db = WorkspaceDb::global(cx);
 1455                                    cx.background_spawn(async move {
 1456                                        timeout.await;
 1457                                        db.save_trusted_worktrees(new_trusted_worktrees)
 1458                                            .await
 1459                                            .log_err();
 1460                                    })
 1461                                },
 1462                            )
 1463                        });
 1464                    }
 1465                }
 1466            })
 1467            .detach();
 1468
 1469            cx.observe_global::<SettingsStore>(|_, cx| {
 1470                if ProjectSettings::get_global(cx).session.trust_all_worktrees {
 1471                    if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
 1472                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
 1473                            trusted_worktrees.auto_trust_all(cx);
 1474                        })
 1475                    }
 1476                }
 1477            })
 1478            .detach();
 1479        }
 1480
 1481        cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
 1482            match event {
 1483                project::Event::RemoteIdChanged(_) => {
 1484                    this.update_window_title(window, cx);
 1485                }
 1486
 1487                project::Event::CollaboratorLeft(peer_id) => {
 1488                    this.collaborator_left(*peer_id, window, cx);
 1489                }
 1490
 1491                &project::Event::WorktreeRemoved(_) => {
 1492                    this.update_window_title(window, cx);
 1493                    this.serialize_workspace(window, cx);
 1494                    this.update_history(cx);
 1495                }
 1496
 1497                &project::Event::WorktreeAdded(id) => {
 1498                    this.update_window_title(window, cx);
 1499                    if this
 1500                        .project()
 1501                        .read(cx)
 1502                        .worktree_for_id(id, cx)
 1503                        .is_some_and(|wt| wt.read(cx).is_visible())
 1504                    {
 1505                        this.serialize_workspace(window, cx);
 1506                        this.update_history(cx);
 1507                    }
 1508                }
 1509                project::Event::WorktreeUpdatedEntries(..) => {
 1510                    this.update_window_title(window, cx);
 1511                    this.serialize_workspace(window, cx);
 1512                }
 1513
 1514                project::Event::DisconnectedFromHost => {
 1515                    this.update_window_edited(window, cx);
 1516                    let leaders_to_unfollow =
 1517                        this.follower_states.keys().copied().collect::<Vec<_>>();
 1518                    for leader_id in leaders_to_unfollow {
 1519                        this.unfollow(leader_id, window, cx);
 1520                    }
 1521                }
 1522
 1523                project::Event::DisconnectedFromRemote {
 1524                    server_not_running: _,
 1525                } => {
 1526                    this.update_window_edited(window, cx);
 1527                }
 1528
 1529                project::Event::Closed => {
 1530                    window.remove_window();
 1531                }
 1532
 1533                project::Event::DeletedEntry(_, entry_id) => {
 1534                    for pane in this.panes.iter() {
 1535                        pane.update(cx, |pane, cx| {
 1536                            pane.handle_deleted_project_item(*entry_id, window, cx)
 1537                        });
 1538                    }
 1539                }
 1540
 1541                project::Event::Toast {
 1542                    notification_id,
 1543                    message,
 1544                    link,
 1545                } => this.show_notification(
 1546                    NotificationId::named(notification_id.clone()),
 1547                    cx,
 1548                    |cx| {
 1549                        let mut notification = MessageNotification::new(message.clone(), cx);
 1550                        if let Some(link) = link {
 1551                            notification = notification
 1552                                .more_info_message(link.label)
 1553                                .more_info_url(link.url);
 1554                        }
 1555
 1556                        cx.new(|_| notification)
 1557                    },
 1558                ),
 1559
 1560                project::Event::HideToast { notification_id } => {
 1561                    this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
 1562                }
 1563
 1564                project::Event::LanguageServerPrompt(request) => {
 1565                    struct LanguageServerPrompt;
 1566
 1567                    this.show_notification(
 1568                        NotificationId::composite::<LanguageServerPrompt>(request.id),
 1569                        cx,
 1570                        |cx| {
 1571                            cx.new(|cx| {
 1572                                notifications::LanguageServerPrompt::new(request.clone(), cx)
 1573                            })
 1574                        },
 1575                    );
 1576                }
 1577
 1578                project::Event::AgentLocationChanged => {
 1579                    this.handle_agent_location_changed(window, cx)
 1580                }
 1581
 1582                _ => {}
 1583            }
 1584            cx.notify()
 1585        })
 1586        .detach();
 1587
 1588        cx.subscribe_in(
 1589            &project.read(cx).breakpoint_store(),
 1590            window,
 1591            |workspace, _, event, window, cx| match event {
 1592                BreakpointStoreEvent::BreakpointsUpdated(_, _)
 1593                | BreakpointStoreEvent::BreakpointsCleared(_) => {
 1594                    workspace.serialize_workspace(window, cx);
 1595                }
 1596                BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
 1597            },
 1598        )
 1599        .detach();
 1600        if let Some(toolchain_store) = project.read(cx).toolchain_store() {
 1601            cx.subscribe_in(
 1602                &toolchain_store,
 1603                window,
 1604                |workspace, _, event, window, cx| match event {
 1605                    ToolchainStoreEvent::CustomToolchainsModified => {
 1606                        workspace.serialize_workspace(window, cx);
 1607                    }
 1608                    _ => {}
 1609                },
 1610            )
 1611            .detach();
 1612        }
 1613
 1614        cx.on_focus_lost(window, |this, window, cx| {
 1615            let focus_handle = this.focus_handle(cx);
 1616            window.focus(&focus_handle, cx);
 1617        })
 1618        .detach();
 1619
 1620        let weak_handle = cx.entity().downgrade();
 1621        let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
 1622
 1623        let center_pane = cx.new(|cx| {
 1624            let mut center_pane = Pane::new(
 1625                weak_handle.clone(),
 1626                project.clone(),
 1627                pane_history_timestamp.clone(),
 1628                None,
 1629                NewFile.boxed_clone(),
 1630                true,
 1631                window,
 1632                cx,
 1633            );
 1634            center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
 1635            center_pane.set_should_display_welcome_page(true);
 1636            center_pane
 1637        });
 1638        cx.subscribe_in(&center_pane, window, Self::handle_pane_event)
 1639            .detach();
 1640
 1641        window.focus(&center_pane.focus_handle(cx), cx);
 1642
 1643        cx.emit(Event::PaneAdded(center_pane.clone()));
 1644
 1645        let any_window_handle = window.window_handle();
 1646        app_state.workspace_store.update(cx, |store, _| {
 1647            store
 1648                .workspaces
 1649                .insert((any_window_handle, weak_handle.clone()));
 1650        });
 1651
 1652        let mut current_user = app_state.user_store.read(cx).watch_current_user();
 1653        let mut connection_status = app_state.client.status();
 1654        let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
 1655            current_user.next().await;
 1656            connection_status.next().await;
 1657            let mut stream =
 1658                Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
 1659
 1660            while stream.recv().await.is_some() {
 1661                this.update(cx, |_, cx| cx.notify())?;
 1662            }
 1663            anyhow::Ok(())
 1664        });
 1665
 1666        // All leader updates are enqueued and then processed in a single task, so
 1667        // that each asynchronous operation can be run in order.
 1668        let (leader_updates_tx, mut leader_updates_rx) =
 1669            mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
 1670        let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
 1671            while let Some((leader_id, update)) = leader_updates_rx.next().await {
 1672                Self::process_leader_update(&this, leader_id, update, cx)
 1673                    .await
 1674                    .log_err();
 1675            }
 1676
 1677            Ok(())
 1678        });
 1679
 1680        cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
 1681        let modal_layer = cx.new(|_| ModalLayer::new());
 1682        let toast_layer = cx.new(|_| ToastLayer::new());
 1683        cx.subscribe(
 1684            &modal_layer,
 1685            |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
 1686                cx.emit(Event::ModalOpened);
 1687            },
 1688        )
 1689        .detach();
 1690
 1691        let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
 1692        let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
 1693        let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
 1694        let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
 1695        let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
 1696        let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
 1697        let multi_workspace = window
 1698            .root::<MultiWorkspace>()
 1699            .flatten()
 1700            .map(|mw| mw.downgrade());
 1701        let status_bar = cx.new(|cx| {
 1702            let mut status_bar =
 1703                StatusBar::new(&center_pane.clone(), multi_workspace.clone(), window, cx);
 1704            status_bar.add_left_item(left_dock_buttons, window, cx);
 1705            status_bar.add_right_item(right_dock_buttons, window, cx);
 1706            status_bar.add_right_item(bottom_dock_buttons, window, cx);
 1707            status_bar
 1708        });
 1709
 1710        let session_id = app_state.session.read(cx).id().to_owned();
 1711
 1712        let mut active_call = None;
 1713        if let Some(call) = GlobalAnyActiveCall::try_global(cx).cloned() {
 1714            let subscriptions =
 1715                vec![
 1716                    call.0
 1717                        .subscribe(window, cx, Box::new(Self::on_active_call_event)),
 1718                ];
 1719            active_call = Some((call, subscriptions));
 1720        }
 1721
 1722        let (serializable_items_tx, serializable_items_rx) =
 1723            mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
 1724        let _items_serializer = cx.spawn_in(window, async move |this, cx| {
 1725            Self::serialize_items(&this, serializable_items_rx, cx).await
 1726        });
 1727
 1728        let subscriptions = vec![
 1729            cx.observe_window_activation(window, Self::on_window_activation_changed),
 1730            cx.observe_window_bounds(window, move |this, window, cx| {
 1731                if this.bounds_save_task_queued.is_some() {
 1732                    return;
 1733                }
 1734                this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
 1735                    cx.background_executor()
 1736                        .timer(Duration::from_millis(100))
 1737                        .await;
 1738                    this.update_in(cx, |this, window, cx| {
 1739                        this.save_window_bounds(window, cx).detach();
 1740                        this.bounds_save_task_queued.take();
 1741                    })
 1742                    .ok();
 1743                }));
 1744                cx.notify();
 1745            }),
 1746            cx.observe_window_appearance(window, |_, window, cx| {
 1747                let window_appearance = window.appearance();
 1748
 1749                *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
 1750
 1751                theme_settings::reload_theme(cx);
 1752                theme_settings::reload_icon_theme(cx);
 1753            }),
 1754            cx.on_release({
 1755                let weak_handle = weak_handle.clone();
 1756                move |this, cx| {
 1757                    this.app_state.workspace_store.update(cx, move |store, _| {
 1758                        store.workspaces.retain(|(_, weak)| weak != &weak_handle);
 1759                    })
 1760                }
 1761            }),
 1762        ];
 1763
 1764        cx.defer_in(window, move |this, window, cx| {
 1765            this.update_window_title(window, cx);
 1766            this.show_initial_notifications(cx);
 1767        });
 1768
 1769        let mut center = PaneGroup::new(center_pane.clone());
 1770        center.set_is_center(true);
 1771        center.mark_positions(cx);
 1772
 1773        Workspace {
 1774            weak_self: weak_handle.clone(),
 1775            zoomed: None,
 1776            zoomed_position: None,
 1777            previous_dock_drag_coordinates: None,
 1778            center,
 1779            panes: vec![center_pane.clone()],
 1780            panes_by_item: Default::default(),
 1781            active_pane: center_pane.clone(),
 1782            last_active_center_pane: Some(center_pane.downgrade()),
 1783            last_active_view_id: None,
 1784            status_bar,
 1785            modal_layer,
 1786            toast_layer,
 1787            titlebar_item: None,
 1788            notifications: Notifications::default(),
 1789            suppressed_notifications: HashSet::default(),
 1790            left_dock,
 1791            bottom_dock,
 1792            right_dock,
 1793            _panels_task: None,
 1794            project: project.clone(),
 1795            follower_states: Default::default(),
 1796            last_leaders_by_pane: Default::default(),
 1797            dispatching_keystrokes: Default::default(),
 1798            window_edited: false,
 1799            last_window_title: None,
 1800            dirty_items: Default::default(),
 1801            active_call,
 1802            database_id: workspace_id,
 1803            app_state,
 1804            _observe_current_user,
 1805            _apply_leader_updates,
 1806            _schedule_serialize_workspace: None,
 1807            _serialize_workspace_task: None,
 1808            _schedule_serialize_ssh_paths: None,
 1809            leader_updates_tx,
 1810            _subscriptions: subscriptions,
 1811            pane_history_timestamp,
 1812            workspace_actions: Default::default(),
 1813            // This data will be incorrect, but it will be overwritten by the time it needs to be used.
 1814            bounds: Default::default(),
 1815            centered_layout: false,
 1816            bounds_save_task_queued: None,
 1817            on_prompt_for_new_path: None,
 1818            on_prompt_for_open_path: None,
 1819            terminal_provider: None,
 1820            debugger_provider: None,
 1821            serializable_items_tx,
 1822            _items_serializer,
 1823            session_id: Some(session_id),
 1824
 1825            scheduled_tasks: Vec::new(),
 1826            last_open_dock_positions: Vec::new(),
 1827            removing: false,
 1828            sidebar_focus_handle: None,
 1829            multi_workspace,
 1830            active_worktree_creation: ActiveWorktreeCreation::default(),
 1831            open_in_dev_container: false,
 1832            _dev_container_task: None,
 1833            deferred_save_items: Vec::new(),
 1834        }
 1835    }
 1836
 1837    pub fn new_local(
 1838        abs_paths: Vec<PathBuf>,
 1839        app_state: Arc<AppState>,
 1840        requesting_window: Option<WindowHandle<MultiWorkspace>>,
 1841        env: Option<HashMap<String, String>>,
 1842        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
 1843        open_mode: OpenMode,
 1844        cx: &mut App,
 1845    ) -> Task<anyhow::Result<OpenResult>> {
 1846        let project_handle = Project::local(
 1847            app_state.client.clone(),
 1848            app_state.node_runtime.clone(),
 1849            app_state.user_store.clone(),
 1850            app_state.languages.clone(),
 1851            app_state.fs.clone(),
 1852            env,
 1853            Default::default(),
 1854            cx,
 1855        );
 1856
 1857        let db = WorkspaceDb::global(cx);
 1858        let kvp = db::kvp::KeyValueStore::global(cx);
 1859        cx.spawn(async move |cx| {
 1860            let mut paths_to_open = Vec::with_capacity(abs_paths.len());
 1861            for path in abs_paths.into_iter() {
 1862                if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
 1863                    paths_to_open.push(canonical)
 1864                } else {
 1865                    paths_to_open.push(path)
 1866                }
 1867            }
 1868
 1869            let serialized_workspace = db.workspace_for_roots(paths_to_open.as_slice());
 1870
 1871            if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
 1872                paths_to_open = paths.ordered_paths().cloned().collect();
 1873                if !paths.is_lexicographically_ordered() {
 1874                    project_handle.update(cx, |project, cx| {
 1875                        project.set_worktrees_reordered(true, cx);
 1876                    });
 1877                }
 1878            }
 1879
 1880            // Get project paths for all of the abs_paths
 1881            let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
 1882                Vec::with_capacity(paths_to_open.len());
 1883
 1884            for path in paths_to_open.into_iter() {
 1885                if let Some((_, project_entry)) = cx
 1886                    .update(|cx| {
 1887                        Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
 1888                    })
 1889                    .await
 1890                    .log_err()
 1891                {
 1892                    project_paths.push((path, Some(project_entry)));
 1893                } else {
 1894                    project_paths.push((path, None));
 1895                }
 1896            }
 1897
 1898            let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
 1899                serialized_workspace.id
 1900            } else {
 1901                db.next_id().await.unwrap_or_else(|_| Default::default())
 1902            };
 1903
 1904            let toolchains = db.toolchains(workspace_id).await?;
 1905
 1906            for (toolchain, worktree_path, path) in toolchains {
 1907                let toolchain_path = PathBuf::from(toolchain.path.clone().to_string());
 1908                let Some(worktree_id) = project_handle.read_with(cx, |this, cx| {
 1909                    this.find_worktree(&worktree_path, cx)
 1910                        .and_then(|(worktree, rel_path)| {
 1911                            if rel_path.is_empty() {
 1912                                Some(worktree.read(cx).id())
 1913                            } else {
 1914                                None
 1915                            }
 1916                        })
 1917                }) else {
 1918                    // We did not find a worktree with a given path, but that's whatever.
 1919                    continue;
 1920                };
 1921                if !app_state.fs.is_file(toolchain_path.as_path()).await {
 1922                    continue;
 1923                }
 1924
 1925                project_handle
 1926                    .update(cx, |this, cx| {
 1927                        this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
 1928                    })
 1929                    .await;
 1930            }
 1931            if let Some(workspace) = serialized_workspace.as_ref() {
 1932                project_handle.update(cx, |this, cx| {
 1933                    for (scope, toolchains) in &workspace.user_toolchains {
 1934                        for toolchain in toolchains {
 1935                            this.add_toolchain(toolchain.clone(), scope.clone(), cx);
 1936                        }
 1937                    }
 1938                });
 1939            }
 1940
 1941            let window_to_replace = match open_mode {
 1942                OpenMode::NewWindow => None,
 1943                _ => requesting_window,
 1944            };
 1945
 1946            let (window, workspace): (WindowHandle<MultiWorkspace>, Entity<Workspace>) =
 1947                if let Some(window) = window_to_replace {
 1948                    let centered_layout = serialized_workspace
 1949                        .as_ref()
 1950                        .map(|w| w.centered_layout)
 1951                        .unwrap_or(false);
 1952
 1953                    let workspace = window.update(cx, |multi_workspace, window, cx| {
 1954                        let workspace = cx.new(|cx| {
 1955                            let mut workspace = Workspace::new(
 1956                                Some(workspace_id),
 1957                                project_handle.clone(),
 1958                                app_state.clone(),
 1959                                window,
 1960                                cx,
 1961                            );
 1962
 1963                            workspace.centered_layout = centered_layout;
 1964
 1965                            // Call init callback to add items before window renders
 1966                            if let Some(init) = init {
 1967                                init(&mut workspace, window, cx);
 1968                            }
 1969
 1970                            workspace
 1971                        });
 1972                        match open_mode {
 1973                            OpenMode::Activate => {
 1974                                multi_workspace.activate(workspace.clone(), None, window, cx);
 1975                            }
 1976                            OpenMode::Add => {
 1977                                multi_workspace.add(workspace.clone(), &*window, cx);
 1978                            }
 1979                            OpenMode::NewWindow => {
 1980                                unreachable!()
 1981                            }
 1982                        }
 1983                        workspace
 1984                    })?;
 1985                    (window, workspace)
 1986                } else {
 1987                    let window_bounds_override = window_bounds_env_override();
 1988
 1989                    let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
 1990                        (Some(WindowBounds::Windowed(bounds)), None)
 1991                    } else if let Some(workspace) = serialized_workspace.as_ref()
 1992                        && let Some(display) = workspace.display
 1993                        && let Some(bounds) = workspace.window_bounds.as_ref()
 1994                    {
 1995                        // Reopening an existing workspace - restore its saved bounds
 1996                        (Some(bounds.0), Some(display))
 1997                    } else if let Some((display, bounds)) =
 1998                        persistence::read_default_window_bounds(&kvp)
 1999                    {
 2000                        // New or empty workspace - use the last known window bounds
 2001                        (Some(bounds), Some(display))
 2002                    } else {
 2003                        // New window - let GPUI's default_bounds() handle cascading
 2004                        (None, None)
 2005                    };
 2006
 2007                    // Use the serialized workspace to construct the new window
 2008                    let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
 2009                    options.window_bounds = window_bounds;
 2010                    let centered_layout = serialized_workspace
 2011                        .as_ref()
 2012                        .map(|w| w.centered_layout)
 2013                        .unwrap_or(false);
 2014                    let window = cx.open_window(options, {
 2015                        let app_state = app_state.clone();
 2016                        let project_handle = project_handle.clone();
 2017                        move |window, cx| {
 2018                            let workspace = cx.new(|cx| {
 2019                                let mut workspace = Workspace::new(
 2020                                    Some(workspace_id),
 2021                                    project_handle,
 2022                                    app_state,
 2023                                    window,
 2024                                    cx,
 2025                                );
 2026                                workspace.centered_layout = centered_layout;
 2027
 2028                                // Call init callback to add items before window renders
 2029                                if let Some(init) = init {
 2030                                    init(&mut workspace, window, cx);
 2031                                }
 2032
 2033                                workspace
 2034                            });
 2035                            cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
 2036                        }
 2037                    })?;
 2038                    let workspace =
 2039                        window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
 2040                            multi_workspace.workspace().clone()
 2041                        })?;
 2042                    (window, workspace)
 2043                };
 2044
 2045            notify_if_database_failed(window, cx);
 2046            // Check if this is an empty workspace (no paths to open)
 2047            // An empty workspace is one where project_paths is empty
 2048            let is_empty_workspace = project_paths.is_empty();
 2049            // Check if serialized workspace has paths before it's moved
 2050            let serialized_workspace_has_paths = serialized_workspace
 2051                .as_ref()
 2052                .map(|ws| !ws.paths.is_empty())
 2053                .unwrap_or(false);
 2054
 2055            let opened_items = window
 2056                .update(cx, |_, window, cx| {
 2057                    workspace.update(cx, |_workspace: &mut Workspace, cx| {
 2058                        open_items(serialized_workspace, project_paths, window, cx)
 2059                    })
 2060                })?
 2061                .await
 2062                .unwrap_or_default();
 2063
 2064            // Restore default dock state for empty workspaces
 2065            // Only restore if:
 2066            // 1. This is an empty workspace (no paths), AND
 2067            // 2. The serialized workspace either doesn't exist or has no paths
 2068            if is_empty_workspace && !serialized_workspace_has_paths {
 2069                if let Some(default_docks) = persistence::read_default_dock_state(&kvp) {
 2070                    window
 2071                        .update(cx, |_, window, cx| {
 2072                            workspace.update(cx, |workspace, cx| {
 2073                                for (dock, serialized_dock) in [
 2074                                    (&workspace.right_dock, &default_docks.right),
 2075                                    (&workspace.left_dock, &default_docks.left),
 2076                                    (&workspace.bottom_dock, &default_docks.bottom),
 2077                                ] {
 2078                                    dock.update(cx, |dock, cx| {
 2079                                        dock.serialized_dock = Some(serialized_dock.clone());
 2080                                        dock.restore_state(window, cx);
 2081                                    });
 2082                                }
 2083                                cx.notify();
 2084                            });
 2085                        })
 2086                        .log_err();
 2087                }
 2088            }
 2089
 2090            window
 2091                .update(cx, |_, _window, cx| {
 2092                    workspace.update(cx, |this: &mut Workspace, cx| {
 2093                        this.update_history(cx);
 2094                    });
 2095                })
 2096                .log_err();
 2097
 2098            if open_mode == OpenMode::NewWindow {
 2099                window
 2100                    .update(cx, |_, window, _cx| {
 2101                        window.activate_window();
 2102                    })
 2103                    .log_err();
 2104            }
 2105
 2106            Ok(OpenResult {
 2107                window,
 2108                workspace,
 2109                opened_items,
 2110            })
 2111        })
 2112    }
 2113
 2114    pub fn project_group_key(&self, cx: &App) -> ProjectGroupKey {
 2115        self.project.read(cx).project_group_key(cx)
 2116    }
 2117
 2118    pub fn weak_handle(&self) -> WeakEntity<Self> {
 2119        self.weak_self.clone()
 2120    }
 2121
 2122    pub fn left_dock(&self) -> &Entity<Dock> {
 2123        &self.left_dock
 2124    }
 2125
 2126    pub fn bottom_dock(&self) -> &Entity<Dock> {
 2127        &self.bottom_dock
 2128    }
 2129
 2130    pub fn set_bottom_dock_layout(
 2131        &mut self,
 2132        layout: BottomDockLayout,
 2133        window: &mut Window,
 2134        cx: &mut Context<Self>,
 2135    ) {
 2136        let fs = self.project().read(cx).fs();
 2137        settings::update_settings_file(fs.clone(), cx, move |content, _cx| {
 2138            content.workspace.bottom_dock_layout = Some(layout);
 2139        });
 2140
 2141        cx.notify();
 2142        self.serialize_workspace(window, cx);
 2143    }
 2144
 2145    pub fn right_dock(&self) -> &Entity<Dock> {
 2146        &self.right_dock
 2147    }
 2148
 2149    pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
 2150        [&self.left_dock, &self.bottom_dock, &self.right_dock]
 2151    }
 2152
 2153    pub fn capture_dock_state(&self, _window: &Window, cx: &App) -> DockStructure {
 2154        let left_dock = self.left_dock.read(cx);
 2155        let left_visible = left_dock.is_open();
 2156        let left_active_panel = left_dock
 2157            .active_panel()
 2158            .map(|panel| panel.persistent_name().to_string());
 2159        // `zoomed_position` is kept in sync with individual panel zoom state
 2160        // by the dock code in `Dock::new` and `Dock::add_panel`.
 2161        let left_dock_zoom = self.zoomed_position == Some(DockPosition::Left);
 2162
 2163        let right_dock = self.right_dock.read(cx);
 2164        let right_visible = right_dock.is_open();
 2165        let right_active_panel = right_dock
 2166            .active_panel()
 2167            .map(|panel| panel.persistent_name().to_string());
 2168        let right_dock_zoom = self.zoomed_position == Some(DockPosition::Right);
 2169
 2170        let bottom_dock = self.bottom_dock.read(cx);
 2171        let bottom_visible = bottom_dock.is_open();
 2172        let bottom_active_panel = bottom_dock
 2173            .active_panel()
 2174            .map(|panel| panel.persistent_name().to_string());
 2175        let bottom_dock_zoom = self.zoomed_position == Some(DockPosition::Bottom);
 2176
 2177        DockStructure {
 2178            left: DockData {
 2179                visible: left_visible,
 2180                active_panel: left_active_panel,
 2181                zoom: left_dock_zoom,
 2182            },
 2183            right: DockData {
 2184                visible: right_visible,
 2185                active_panel: right_active_panel,
 2186                zoom: right_dock_zoom,
 2187            },
 2188            bottom: DockData {
 2189                visible: bottom_visible,
 2190                active_panel: bottom_active_panel,
 2191                zoom: bottom_dock_zoom,
 2192            },
 2193        }
 2194    }
 2195
 2196    pub fn set_dock_structure(
 2197        &self,
 2198        docks: DockStructure,
 2199        window: &mut Window,
 2200        cx: &mut Context<Self>,
 2201    ) {
 2202        for (dock, data) in [
 2203            (&self.left_dock, docks.left),
 2204            (&self.bottom_dock, docks.bottom),
 2205            (&self.right_dock, docks.right),
 2206        ] {
 2207            dock.update(cx, |dock, cx| {
 2208                dock.serialized_dock = Some(data);
 2209                dock.restore_state(window, cx);
 2210            });
 2211        }
 2212    }
 2213
 2214    /// Returns which dock currently has focus, or `None` if focus is in the
 2215    /// center pane or elsewhere. Does NOT fall back to any global state.
 2216    pub fn focused_dock_position(&self, window: &Window, cx: &App) -> Option<DockPosition> {
 2217        [
 2218            (DockPosition::Left, &self.left_dock),
 2219            (DockPosition::Right, &self.right_dock),
 2220            (DockPosition::Bottom, &self.bottom_dock),
 2221        ]
 2222        .into_iter()
 2223        .find(|(_, dock)| {
 2224            dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
 2225        })
 2226        .map(|(position, _)| position)
 2227    }
 2228
 2229    pub fn active_worktree_creation(&self) -> &ActiveWorktreeCreation {
 2230        &self.active_worktree_creation
 2231    }
 2232
 2233    pub fn set_active_worktree_creation(
 2234        &mut self,
 2235        label: Option<SharedString>,
 2236        is_switch: bool,
 2237        cx: &mut Context<Self>,
 2238    ) {
 2239        self.active_worktree_creation.label = label;
 2240        self.active_worktree_creation.is_switch = is_switch;
 2241        cx.emit(Event::WorktreeCreationChanged);
 2242        cx.notify();
 2243    }
 2244
 2245    /// Captures the current workspace state for restoring after a worktree switch.
 2246    /// This includes dock layout, open file paths, and the active file path.
 2247    pub fn capture_state_for_worktree_switch(
 2248        &self,
 2249        window: &Window,
 2250        fallback_focused_dock: Option<DockPosition>,
 2251        cx: &App,
 2252    ) -> PreviousWorkspaceState {
 2253        let dock_structure = self.capture_dock_state(window, cx);
 2254        let open_file_paths = self.open_item_abs_paths(cx);
 2255        let active_file_path = self
 2256            .active_item(cx)
 2257            .and_then(|item| item.project_path(cx))
 2258            .and_then(|pp| self.project().read(cx).absolute_path(&pp, cx));
 2259
 2260        let focused_dock = self
 2261            .focused_dock_position(window, cx)
 2262            .or(fallback_focused_dock);
 2263
 2264        PreviousWorkspaceState {
 2265            dock_structure,
 2266            open_file_paths,
 2267            active_file_path,
 2268            focused_dock,
 2269        }
 2270    }
 2271
 2272    pub fn open_item_abs_paths(&self, cx: &App) -> Vec<PathBuf> {
 2273        self.items(cx)
 2274            .filter_map(|item| {
 2275                let project_path = item.project_path(cx)?;
 2276                self.project.read(cx).absolute_path(&project_path, cx)
 2277            })
 2278            .collect()
 2279    }
 2280
 2281    pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
 2282        match position {
 2283            DockPosition::Left => &self.left_dock,
 2284            DockPosition::Bottom => &self.bottom_dock,
 2285            DockPosition::Right => &self.right_dock,
 2286        }
 2287    }
 2288
 2289    pub fn agent_panel_position(&self, cx: &App) -> Option<DockPosition> {
 2290        self.all_docks().into_iter().find_map(|dock| {
 2291            let dock = dock.read(cx);
 2292            dock.has_agent_panel(cx).then_some(dock.position())
 2293        })
 2294    }
 2295
 2296    pub fn panel_size_state<T: Panel>(&self, cx: &App) -> Option<dock::PanelSizeState> {
 2297        self.all_docks().into_iter().find_map(|dock| {
 2298            let dock = dock.read(cx);
 2299            let panel = dock.panel::<T>()?;
 2300            dock.stored_panel_size_state(&panel)
 2301        })
 2302    }
 2303
 2304    pub fn persisted_panel_size_state(
 2305        &self,
 2306        panel_key: &'static str,
 2307        cx: &App,
 2308    ) -> Option<dock::PanelSizeState> {
 2309        dock::Dock::load_persisted_size_state(self, panel_key, cx)
 2310    }
 2311
 2312    pub fn persist_panel_size_state(
 2313        &self,
 2314        panel_key: &str,
 2315        size_state: dock::PanelSizeState,
 2316        cx: &mut App,
 2317    ) {
 2318        let Some(workspace_id) = self
 2319            .database_id()
 2320            .map(|id| i64::from(id).to_string())
 2321            .or(self.session_id())
 2322        else {
 2323            return;
 2324        };
 2325
 2326        let kvp = db::kvp::KeyValueStore::global(cx);
 2327        let panel_key = panel_key.to_string();
 2328        cx.background_spawn(async move {
 2329            let scope = kvp.scoped(dock::PANEL_SIZE_STATE_KEY);
 2330            scope
 2331                .write(
 2332                    format!("{workspace_id}:{panel_key}"),
 2333                    serde_json::to_string(&size_state)?,
 2334                )
 2335                .await
 2336        })
 2337        .detach_and_log_err(cx);
 2338    }
 2339
 2340    pub fn set_panel_size_state<T: Panel>(
 2341        &mut self,
 2342        size_state: dock::PanelSizeState,
 2343        window: &mut Window,
 2344        cx: &mut Context<Self>,
 2345    ) -> bool {
 2346        let Some(panel) = self.panel::<T>(cx) else {
 2347            return false;
 2348        };
 2349
 2350        let dock = self.dock_at_position(panel.position(window, cx));
 2351        let did_set = dock.update(cx, |dock, cx| {
 2352            dock.set_panel_size_state(&panel, size_state, cx)
 2353        });
 2354
 2355        if did_set {
 2356            self.persist_panel_size_state(T::panel_key(), size_state, cx);
 2357        }
 2358
 2359        did_set
 2360    }
 2361
 2362    pub fn toggle_dock_panel_flexible_size(
 2363        &self,
 2364        dock: &Entity<Dock>,
 2365        panel: &dyn PanelHandle,
 2366        window: &mut Window,
 2367        cx: &mut App,
 2368    ) {
 2369        let position = dock.read(cx).position();
 2370        let current_size = self.dock_size(&dock.read(cx), window, cx);
 2371        let current_flex =
 2372            current_size.and_then(|size| self.dock_flex_for_size(position, size, window, cx));
 2373        dock.update(cx, |dock, cx| {
 2374            dock.toggle_panel_flexible_size(panel, current_size, current_flex, window, cx);
 2375        });
 2376    }
 2377
 2378    fn dock_size(&self, dock: &Dock, window: &Window, cx: &App) -> Option<Pixels> {
 2379        let panel = dock.active_panel()?;
 2380        let size_state = dock
 2381            .stored_panel_size_state(panel.as_ref())
 2382            .unwrap_or_default();
 2383        let position = dock.position();
 2384
 2385        let use_flex = panel.has_flexible_size(window, cx);
 2386
 2387        if position.axis() == Axis::Horizontal
 2388            && use_flex
 2389            && let Some(flex) = size_state.flex.or_else(|| self.default_dock_flex(position))
 2390        {
 2391            let workspace_width = self.bounds.size.width;
 2392            if workspace_width <= Pixels::ZERO {
 2393                return None;
 2394            }
 2395            let flex = flex.max(0.001);
 2396            let center_column_count = self.center_full_height_column_count();
 2397            let opposite = self.opposite_dock_panel_and_size_state(position, window, cx);
 2398            if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) {
 2399                let total_flex = flex + center_column_count + opposite_flex;
 2400                return Some((flex / total_flex * workspace_width).max(RESIZE_HANDLE_SIZE));
 2401            } else {
 2402                let opposite_fixed = opposite
 2403                    .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx)))
 2404                    .unwrap_or_default();
 2405                let available = (workspace_width - opposite_fixed).max(RESIZE_HANDLE_SIZE);
 2406                return Some(
 2407                    (flex / (flex + center_column_count) * available).max(RESIZE_HANDLE_SIZE),
 2408                );
 2409            }
 2410        }
 2411
 2412        Some(
 2413            size_state
 2414                .size
 2415                .unwrap_or_else(|| panel.default_size(window, cx)),
 2416        )
 2417    }
 2418
 2419    pub fn dock_flex_for_size(
 2420        &self,
 2421        position: DockPosition,
 2422        size: Pixels,
 2423        window: &Window,
 2424        cx: &App,
 2425    ) -> Option<f32> {
 2426        if position.axis() != Axis::Horizontal {
 2427            return None;
 2428        }
 2429
 2430        let workspace_width = self.bounds.size.width;
 2431        if workspace_width <= Pixels::ZERO {
 2432            return None;
 2433        }
 2434
 2435        let center_column_count = self.center_full_height_column_count();
 2436        let opposite = self.opposite_dock_panel_and_size_state(position, window, cx);
 2437        if let Some(opposite_flex) = opposite.as_ref().and_then(|(_, s)| s.flex) {
 2438            let size = size.clamp(px(0.), workspace_width - px(1.));
 2439            Some((size * (center_column_count + opposite_flex) / (workspace_width - size)).max(0.0))
 2440        } else {
 2441            let opposite_width = opposite
 2442                .map(|(panel, s)| s.size.unwrap_or_else(|| panel.default_size(window, cx)))
 2443                .unwrap_or_default();
 2444            let available = (workspace_width - opposite_width).max(RESIZE_HANDLE_SIZE);
 2445            let remaining = (available - size).max(px(1.));
 2446            Some((size * center_column_count / remaining).max(0.0))
 2447        }
 2448    }
 2449
 2450    fn opposite_dock_panel_and_size_state(
 2451        &self,
 2452        position: DockPosition,
 2453        window: &Window,
 2454        cx: &App,
 2455    ) -> Option<(Arc<dyn PanelHandle>, PanelSizeState)> {
 2456        let opposite_position = match position {
 2457            DockPosition::Left => DockPosition::Right,
 2458            DockPosition::Right => DockPosition::Left,
 2459            DockPosition::Bottom => return None,
 2460        };
 2461
 2462        let opposite_dock = self.dock_at_position(opposite_position).read(cx);
 2463        let panel = opposite_dock.visible_panel()?;
 2464        let mut size_state = opposite_dock
 2465            .stored_panel_size_state(panel.as_ref())
 2466            .unwrap_or_default();
 2467        if size_state.flex.is_none() && panel.has_flexible_size(window, cx) {
 2468            size_state.flex = self.default_dock_flex(opposite_position);
 2469        }
 2470        Some((panel.clone(), size_state))
 2471    }
 2472
 2473    fn center_full_height_column_count(&self) -> f32 {
 2474        self.center.full_height_column_count().max(1) as f32
 2475    }
 2476
 2477    pub fn default_dock_flex(&self, position: DockPosition) -> Option<f32> {
 2478        if position.axis() != Axis::Horizontal {
 2479            return None;
 2480        }
 2481
 2482        Some(1.0)
 2483    }
 2484
 2485    pub fn is_edited(&self) -> bool {
 2486        self.window_edited
 2487    }
 2488
 2489    pub fn add_panel<T: Panel>(
 2490        &mut self,
 2491        panel: Entity<T>,
 2492        window: &mut Window,
 2493        cx: &mut Context<Self>,
 2494    ) {
 2495        let focus_handle = panel.panel_focus_handle(cx);
 2496        cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
 2497            .detach();
 2498
 2499        let dock_position = panel.position(window, cx);
 2500        let dock = self.dock_at_position(dock_position);
 2501        let any_panel = panel.to_any();
 2502        let persisted_size_state =
 2503            self.persisted_panel_size_state(T::panel_key(), cx)
 2504                .or_else(|| {
 2505                    load_legacy_panel_size(T::panel_key(), dock_position, self, cx).map(|size| {
 2506                        let state = dock::PanelSizeState {
 2507                            size: Some(size),
 2508                            flex: None,
 2509                        };
 2510                        self.persist_panel_size_state(T::panel_key(), state, cx);
 2511                        state
 2512                    })
 2513                });
 2514
 2515        dock.update(cx, |dock, cx| {
 2516            let index = dock.add_panel(panel.clone(), self.weak_self.clone(), window, cx);
 2517            if let Some(size_state) = persisted_size_state {
 2518                dock.set_panel_size_state(&panel, size_state, cx);
 2519            }
 2520            index
 2521        });
 2522
 2523        cx.emit(Event::PanelAdded(any_panel));
 2524    }
 2525
 2526    pub fn remove_panel<T: Panel>(
 2527        &mut self,
 2528        panel: &Entity<T>,
 2529        window: &mut Window,
 2530        cx: &mut Context<Self>,
 2531    ) {
 2532        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
 2533            dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
 2534        }
 2535    }
 2536
 2537    pub fn status_bar(&self) -> &Entity<StatusBar> {
 2538        &self.status_bar
 2539    }
 2540
 2541    pub fn set_sidebar_focus_handle(&mut self, handle: Option<FocusHandle>) {
 2542        self.sidebar_focus_handle = handle;
 2543    }
 2544
 2545    pub fn status_bar_visible(&self, cx: &App) -> bool {
 2546        StatusBarSettings::get_global(cx).show
 2547    }
 2548
 2549    pub fn multi_workspace(&self) -> Option<&WeakEntity<MultiWorkspace>> {
 2550        self.multi_workspace.as_ref()
 2551    }
 2552
 2553    pub fn set_multi_workspace(
 2554        &mut self,
 2555        multi_workspace: WeakEntity<MultiWorkspace>,
 2556        cx: &mut App,
 2557    ) {
 2558        self.status_bar.update(cx, |status_bar, cx| {
 2559            status_bar.set_multi_workspace(multi_workspace.clone(), cx);
 2560        });
 2561        self.multi_workspace = Some(multi_workspace);
 2562    }
 2563
 2564    pub fn app_state(&self) -> &Arc<AppState> {
 2565        &self.app_state
 2566    }
 2567
 2568    pub fn set_panels_task(&mut self, task: Task<Result<()>>) {
 2569        self._panels_task = Some(task);
 2570    }
 2571
 2572    pub fn take_panels_task(&mut self) -> Option<Task<Result<()>>> {
 2573        self._panels_task.take()
 2574    }
 2575
 2576    pub fn user_store(&self) -> &Entity<UserStore> {
 2577        &self.app_state.user_store
 2578    }
 2579
 2580    pub fn project(&self) -> &Entity<Project> {
 2581        &self.project
 2582    }
 2583
 2584    pub fn path_style(&self, cx: &App) -> PathStyle {
 2585        self.project.read(cx).path_style(cx)
 2586    }
 2587
 2588    pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
 2589        let mut history: HashMap<EntityId, usize> = HashMap::default();
 2590
 2591        for pane_handle in &self.panes {
 2592            let pane = pane_handle.read(cx);
 2593
 2594            for entry in pane.activation_history() {
 2595                history.insert(
 2596                    entry.entity_id,
 2597                    history
 2598                        .get(&entry.entity_id)
 2599                        .cloned()
 2600                        .unwrap_or(0)
 2601                        .max(entry.timestamp),
 2602                );
 2603            }
 2604        }
 2605
 2606        history
 2607    }
 2608
 2609    pub fn recent_active_item_by_type<T: 'static>(&self, cx: &App) -> Option<Entity<T>> {
 2610        let mut recent_item: Option<Entity<T>> = None;
 2611        let mut recent_timestamp = 0;
 2612        for pane_handle in &self.panes {
 2613            let pane = pane_handle.read(cx);
 2614            let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> =
 2615                pane.items().map(|item| (item.item_id(), item)).collect();
 2616            for entry in pane.activation_history() {
 2617                if entry.timestamp > recent_timestamp
 2618                    && let Some(&item) = item_map.get(&entry.entity_id)
 2619                    && let Some(typed_item) = item.act_as::<T>(cx)
 2620                {
 2621                    recent_timestamp = entry.timestamp;
 2622                    recent_item = Some(typed_item);
 2623                }
 2624            }
 2625        }
 2626        recent_item
 2627    }
 2628
 2629    pub fn recent_navigation_history_iter(
 2630        &self,
 2631        cx: &App,
 2632    ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> + use<> {
 2633        let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
 2634        let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
 2635
 2636        for pane in &self.panes {
 2637            let pane = pane.read(cx);
 2638
 2639            pane.nav_history()
 2640                .for_each_entry(cx, &mut |entry, (project_path, fs_path)| {
 2641                    if let Some(fs_path) = &fs_path {
 2642                        abs_paths_opened
 2643                            .entry(fs_path.clone())
 2644                            .or_default()
 2645                            .insert(project_path.clone());
 2646                    }
 2647                    let timestamp = entry.timestamp;
 2648                    match history.entry(project_path) {
 2649                        hash_map::Entry::Occupied(mut entry) => {
 2650                            let (_, old_timestamp) = entry.get();
 2651                            if &timestamp > old_timestamp {
 2652                                entry.insert((fs_path, timestamp));
 2653                            }
 2654                        }
 2655                        hash_map::Entry::Vacant(entry) => {
 2656                            entry.insert((fs_path, timestamp));
 2657                        }
 2658                    }
 2659                });
 2660
 2661            if let Some(item) = pane.active_item()
 2662                && let Some(project_path) = item.project_path(cx)
 2663            {
 2664                let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
 2665
 2666                if let Some(fs_path) = &fs_path {
 2667                    abs_paths_opened
 2668                        .entry(fs_path.clone())
 2669                        .or_default()
 2670                        .insert(project_path.clone());
 2671                }
 2672
 2673                history.insert(project_path, (fs_path, std::usize::MAX));
 2674            }
 2675        }
 2676
 2677        history
 2678            .into_iter()
 2679            .sorted_by_key(|(_, (_, order))| *order)
 2680            .map(|(project_path, (fs_path, _))| (project_path, fs_path))
 2681            .rev()
 2682            .filter(move |(history_path, abs_path)| {
 2683                let latest_project_path_opened = abs_path
 2684                    .as_ref()
 2685                    .and_then(|abs_path| abs_paths_opened.get(abs_path))
 2686                    .and_then(|project_paths| {
 2687                        project_paths
 2688                            .iter()
 2689                            .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
 2690                    });
 2691
 2692                latest_project_path_opened.is_none_or(|path| path == history_path)
 2693            })
 2694    }
 2695
 2696    pub fn recent_navigation_history(
 2697        &self,
 2698        limit: Option<usize>,
 2699        cx: &App,
 2700    ) -> Vec<(ProjectPath, Option<PathBuf>)> {
 2701        self.recent_navigation_history_iter(cx)
 2702            .take(limit.unwrap_or(usize::MAX))
 2703            .collect()
 2704    }
 2705
 2706    pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context<Workspace>) {
 2707        for pane in &self.panes {
 2708            pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx));
 2709        }
 2710    }
 2711
 2712    fn navigate_history(
 2713        &mut self,
 2714        pane: WeakEntity<Pane>,
 2715        mode: NavigationMode,
 2716        window: &mut Window,
 2717        cx: &mut Context<Workspace>,
 2718    ) -> Task<Result<()>> {
 2719        self.navigate_history_impl(
 2720            pane,
 2721            mode,
 2722            window,
 2723            &mut |history, cx| history.pop(mode, cx),
 2724            cx,
 2725        )
 2726    }
 2727
 2728    fn navigate_tag_history(
 2729        &mut self,
 2730        pane: WeakEntity<Pane>,
 2731        mode: TagNavigationMode,
 2732        window: &mut Window,
 2733        cx: &mut Context<Workspace>,
 2734    ) -> Task<Result<()>> {
 2735        self.navigate_history_impl(
 2736            pane,
 2737            NavigationMode::Normal,
 2738            window,
 2739            &mut |history, _cx| history.pop_tag(mode),
 2740            cx,
 2741        )
 2742    }
 2743
 2744    fn navigate_history_impl(
 2745        &mut self,
 2746        pane: WeakEntity<Pane>,
 2747        mode: NavigationMode,
 2748        window: &mut Window,
 2749        cb: &mut dyn FnMut(&mut NavHistory, &mut App) -> Option<NavigationEntry>,
 2750        cx: &mut Context<Workspace>,
 2751    ) -> Task<Result<()>> {
 2752        let to_load = if let Some(pane) = pane.upgrade() {
 2753            pane.update(cx, |pane, cx| {
 2754                window.focus(&pane.focus_handle(cx), cx);
 2755                loop {
 2756                    // Retrieve the weak item handle from the history.
 2757                    let entry = cb(pane.nav_history_mut(), cx)?;
 2758
 2759                    // If the item is still present in this pane, then activate it.
 2760                    if let Some(index) = entry
 2761                        .item
 2762                        .upgrade()
 2763                        .and_then(|v| pane.index_for_item(v.as_ref()))
 2764                    {
 2765                        let prev_active_item_index = pane.active_item_index();
 2766                        pane.nav_history_mut().set_mode(mode);
 2767                        pane.activate_item(index, true, true, window, cx);
 2768                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
 2769
 2770                        let mut navigated = prev_active_item_index != pane.active_item_index();
 2771                        if let Some(data) = entry.data {
 2772                            navigated |= pane.active_item()?.navigate(data, window, cx);
 2773                        }
 2774
 2775                        if navigated {
 2776                            break None;
 2777                        }
 2778                    } else {
 2779                        // If the item is no longer present in this pane, then retrieve its
 2780                        // path info in order to reopen it.
 2781                        break pane
 2782                            .nav_history()
 2783                            .path_for_item(entry.item.id())
 2784                            .map(|(project_path, abs_path)| (project_path, abs_path, entry));
 2785                    }
 2786                }
 2787            })
 2788        } else {
 2789            None
 2790        };
 2791
 2792        if let Some((project_path, abs_path, entry)) = to_load {
 2793            // If the item was no longer present, then load it again from its previous path, first try the local path
 2794            let open_by_project_path = self.load_path(project_path.clone(), window, cx);
 2795
 2796            cx.spawn_in(window, async move  |workspace, cx| {
 2797                let open_by_project_path = open_by_project_path.await;
 2798                let mut navigated = false;
 2799                match open_by_project_path
 2800                    .with_context(|| format!("Navigating to {project_path:?}"))
 2801                {
 2802                    Ok((project_entry_id, build_item)) => {
 2803                        let prev_active_item_id = pane.update(cx, |pane, _| {
 2804                            pane.nav_history_mut().set_mode(mode);
 2805                            pane.active_item().map(|p| p.item_id())
 2806                        })?;
 2807
 2808                        pane.update_in(cx, |pane, window, cx| {
 2809                            let item = pane.open_item(
 2810                                project_entry_id,
 2811                                project_path,
 2812                                true,
 2813                                entry.is_preview,
 2814                                true,
 2815                                None,
 2816                                window, cx,
 2817                                build_item,
 2818                            );
 2819                            navigated |= Some(item.item_id()) != prev_active_item_id;
 2820                            pane.nav_history_mut().set_mode(NavigationMode::Normal);
 2821                            if let Some(data) = entry.data {
 2822                                navigated |= item.navigate(data, window, cx);
 2823                            }
 2824                        })?;
 2825                    }
 2826                    Err(open_by_project_path_e) => {
 2827                        // Fall back to opening by abs path, in case an external file was opened and closed,
 2828                        // and its worktree is now dropped
 2829                        if let Some(abs_path) = abs_path {
 2830                            let prev_active_item_id = pane.update(cx, |pane, _| {
 2831                                pane.nav_history_mut().set_mode(mode);
 2832                                pane.active_item().map(|p| p.item_id())
 2833                            })?;
 2834                            let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
 2835                                workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
 2836                            })?;
 2837                            match open_by_abs_path
 2838                                .await
 2839                                .with_context(|| format!("Navigating to {abs_path:?}"))
 2840                            {
 2841                                Ok(item) => {
 2842                                    pane.update_in(cx, |pane, window, cx| {
 2843                                        navigated |= Some(item.item_id()) != prev_active_item_id;
 2844                                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
 2845                                        if let Some(data) = entry.data {
 2846                                            navigated |= item.navigate(data, window, cx);
 2847                                        }
 2848                                    })?;
 2849                                }
 2850                                Err(open_by_abs_path_e) => {
 2851                                    log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
 2852                                }
 2853                            }
 2854                        }
 2855                    }
 2856                }
 2857
 2858                if !navigated {
 2859                    workspace
 2860                        .update_in(cx, |workspace, window, cx| {
 2861                            Self::navigate_history(workspace, pane, mode, window, cx)
 2862                        })?
 2863                        .await?;
 2864                }
 2865
 2866                Ok(())
 2867            })
 2868        } else {
 2869            Task::ready(Ok(()))
 2870        }
 2871    }
 2872
 2873    pub fn go_back(
 2874        &mut self,
 2875        pane: WeakEntity<Pane>,
 2876        window: &mut Window,
 2877        cx: &mut Context<Workspace>,
 2878    ) -> Task<Result<()>> {
 2879        self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
 2880    }
 2881
 2882    pub fn go_forward(
 2883        &mut self,
 2884        pane: WeakEntity<Pane>,
 2885        window: &mut Window,
 2886        cx: &mut Context<Workspace>,
 2887    ) -> Task<Result<()>> {
 2888        self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
 2889    }
 2890
 2891    pub fn reopen_closed_item(
 2892        &mut self,
 2893        window: &mut Window,
 2894        cx: &mut Context<Workspace>,
 2895    ) -> Task<Result<()>> {
 2896        self.navigate_history(
 2897            self.active_pane().downgrade(),
 2898            NavigationMode::ReopeningClosedItem,
 2899            window,
 2900            cx,
 2901        )
 2902    }
 2903
 2904    pub fn client(&self) -> &Arc<Client> {
 2905        &self.app_state.client
 2906    }
 2907
 2908    pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
 2909        self.titlebar_item = Some(item);
 2910        cx.notify();
 2911    }
 2912
 2913    pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
 2914        self.on_prompt_for_new_path = Some(prompt)
 2915    }
 2916
 2917    pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
 2918        self.on_prompt_for_open_path = Some(prompt)
 2919    }
 2920
 2921    pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
 2922        self.terminal_provider = Some(Box::new(provider));
 2923    }
 2924
 2925    pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
 2926        self.debugger_provider = Some(Arc::new(provider));
 2927    }
 2928
 2929    pub fn set_open_in_dev_container(&mut self, value: bool) {
 2930        self.open_in_dev_container = value;
 2931    }
 2932
 2933    pub fn open_in_dev_container(&self) -> bool {
 2934        self.open_in_dev_container
 2935    }
 2936
 2937    pub fn set_dev_container_task(&mut self, task: Task<Result<()>>) {
 2938        self._dev_container_task = Some(task);
 2939    }
 2940
 2941    pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
 2942        self.debugger_provider.clone()
 2943    }
 2944
 2945    pub fn prompt_for_open_path(
 2946        &mut self,
 2947        path_prompt_options: PathPromptOptions,
 2948        lister: DirectoryLister,
 2949        window: &mut Window,
 2950        cx: &mut Context<Self>,
 2951    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
 2952        // TODO: If `on_prompt_for_open_path` is set, we should always use it
 2953        // rather than gating on `use_system_path_prompts`. This would let tests
 2954        // inject a mock without also having to disable the setting.
 2955        if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
 2956            let prompt = self.on_prompt_for_open_path.take().unwrap();
 2957            let rx = prompt(self, lister, window, cx);
 2958            self.on_prompt_for_open_path = Some(prompt);
 2959            rx
 2960        } else {
 2961            let (tx, rx) = oneshot::channel();
 2962            let abs_path = cx.prompt_for_paths(path_prompt_options);
 2963
 2964            cx.spawn_in(window, async move |workspace, cx| {
 2965                let Ok(result) = abs_path.await else {
 2966                    return Ok(());
 2967                };
 2968
 2969                match result {
 2970                    Ok(result) => {
 2971                        tx.send(result).ok();
 2972                    }
 2973                    Err(err) => {
 2974                        let rx = workspace.update_in(cx, |workspace, window, cx| {
 2975                            workspace.show_portal_error(err.to_string(), cx);
 2976                            let prompt = workspace.on_prompt_for_open_path.take().unwrap();
 2977                            let rx = prompt(workspace, lister, window, cx);
 2978                            workspace.on_prompt_for_open_path = Some(prompt);
 2979                            rx
 2980                        })?;
 2981                        if let Ok(path) = rx.await {
 2982                            tx.send(path).ok();
 2983                        }
 2984                    }
 2985                };
 2986                anyhow::Ok(())
 2987            })
 2988            .detach();
 2989
 2990            rx
 2991        }
 2992    }
 2993
 2994    pub fn prompt_for_new_path(
 2995        &mut self,
 2996        lister: DirectoryLister,
 2997        suggested_name: Option<String>,
 2998        window: &mut Window,
 2999        cx: &mut Context<Self>,
 3000    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
 3001        if self.project.read(cx).is_via_collab()
 3002            || self.project.read(cx).is_via_remote_server()
 3003            || !WorkspaceSettings::get_global(cx).use_system_path_prompts
 3004        {
 3005            let prompt = self.on_prompt_for_new_path.take().unwrap();
 3006            let rx = prompt(self, lister, suggested_name, window, cx);
 3007            self.on_prompt_for_new_path = Some(prompt);
 3008            return rx;
 3009        }
 3010
 3011        let (tx, rx) = oneshot::channel();
 3012        cx.spawn_in(window, async move |workspace, cx| {
 3013            let abs_path = workspace.update(cx, |workspace, cx| {
 3014                let relative_to = workspace
 3015                    .most_recent_active_path(cx)
 3016                    .and_then(|p| p.parent().map(|p| p.to_path_buf()))
 3017                    .or_else(|| {
 3018                        let project = workspace.project.read(cx);
 3019                        project.visible_worktrees(cx).find_map(|worktree| {
 3020                            Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
 3021                        })
 3022                    })
 3023                    .or_else(std::env::home_dir)
 3024                    .unwrap_or_else(|| PathBuf::from(""));
 3025                cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
 3026            })?;
 3027            let abs_path = match abs_path.await? {
 3028                Ok(path) => path,
 3029                Err(err) => {
 3030                    let rx = workspace.update_in(cx, |workspace, window, cx| {
 3031                        workspace.show_portal_error(err.to_string(), cx);
 3032
 3033                        let prompt = workspace.on_prompt_for_new_path.take().unwrap();
 3034                        let rx = prompt(workspace, lister, suggested_name, window, cx);
 3035                        workspace.on_prompt_for_new_path = Some(prompt);
 3036                        rx
 3037                    })?;
 3038                    if let Ok(path) = rx.await {
 3039                        tx.send(path).ok();
 3040                    }
 3041                    return anyhow::Ok(());
 3042                }
 3043            };
 3044
 3045            tx.send(abs_path.map(|path| vec![path])).ok();
 3046            anyhow::Ok(())
 3047        })
 3048        .detach();
 3049
 3050        rx
 3051    }
 3052
 3053    pub fn titlebar_item(&self) -> Option<AnyView> {
 3054        self.titlebar_item.clone()
 3055    }
 3056
 3057    /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
 3058    ///
 3059    /// If the given workspace has a local project, then it will be passed
 3060    /// to the callback. Otherwise, a new empty window will be created.
 3061    pub fn with_local_workspace<T, F>(
 3062        &mut self,
 3063        window: &mut Window,
 3064        cx: &mut Context<Self>,
 3065        callback: F,
 3066    ) -> Task<Result<T>>
 3067    where
 3068        T: 'static,
 3069        F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
 3070    {
 3071        if self.project.read(cx).is_local() {
 3072            Task::ready(Ok(callback(self, window, cx)))
 3073        } else {
 3074            let env = self.project.read(cx).cli_environment(cx);
 3075            let task = Self::new_local(
 3076                Vec::new(),
 3077                self.app_state.clone(),
 3078                None,
 3079                env,
 3080                None,
 3081                OpenMode::Activate,
 3082                cx,
 3083            );
 3084            cx.spawn_in(window, async move |_vh, cx| {
 3085                let OpenResult {
 3086                    window: multi_workspace_window,
 3087                    ..
 3088                } = task.await?;
 3089                multi_workspace_window.update(cx, |multi_workspace, window, cx| {
 3090                    let workspace = multi_workspace.workspace().clone();
 3091                    workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
 3092                })
 3093            })
 3094        }
 3095    }
 3096
 3097    /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
 3098    ///
 3099    /// If the given workspace has a local project, then it will be passed
 3100    /// to the callback. Otherwise, a new empty window will be created.
 3101    pub fn with_local_or_wsl_workspace<T, F>(
 3102        &mut self,
 3103        window: &mut Window,
 3104        cx: &mut Context<Self>,
 3105        callback: F,
 3106    ) -> Task<Result<T>>
 3107    where
 3108        T: 'static,
 3109        F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
 3110    {
 3111        let project = self.project.read(cx);
 3112        if project.is_local() || project.is_via_wsl_with_host_interop(cx) {
 3113            Task::ready(Ok(callback(self, window, cx)))
 3114        } else {
 3115            let env = self.project.read(cx).cli_environment(cx);
 3116            let task = Self::new_local(
 3117                Vec::new(),
 3118                self.app_state.clone(),
 3119                None,
 3120                env,
 3121                None,
 3122                OpenMode::Activate,
 3123                cx,
 3124            );
 3125            cx.spawn_in(window, async move |_vh, cx| {
 3126                let OpenResult {
 3127                    window: multi_workspace_window,
 3128                    ..
 3129                } = task.await?;
 3130                multi_workspace_window.update(cx, |multi_workspace, window, cx| {
 3131                    let workspace = multi_workspace.workspace().clone();
 3132                    workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
 3133                })
 3134            })
 3135        }
 3136    }
 3137
 3138    pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
 3139        self.project.read(cx).worktrees(cx)
 3140    }
 3141
 3142    pub fn visible_worktrees<'a>(
 3143        &self,
 3144        cx: &'a App,
 3145    ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
 3146        self.project.read(cx).visible_worktrees(cx)
 3147    }
 3148
 3149    pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
 3150        let futures = self
 3151            .worktrees(cx)
 3152            .filter_map(|worktree| worktree.read(cx).as_local())
 3153            .map(|worktree| worktree.scan_complete())
 3154            .collect::<Vec<_>>();
 3155        async move {
 3156            for future in futures {
 3157                future.await;
 3158            }
 3159        }
 3160    }
 3161
 3162    pub fn close_global(cx: &mut App) {
 3163        cx.defer(|cx| {
 3164            cx.windows().iter().find(|window| {
 3165                window
 3166                    .update(cx, |_, window, _| {
 3167                        if window.is_window_active() {
 3168                            //This can only get called when the window's project connection has been lost
 3169                            //so we don't need to prompt the user for anything and instead just close the window
 3170                            window.remove_window();
 3171                            true
 3172                        } else {
 3173                            false
 3174                        }
 3175                    })
 3176                    .unwrap_or(false)
 3177            });
 3178        });
 3179    }
 3180
 3181    pub fn move_focused_panel_to_next_position(
 3182        &mut self,
 3183        _: &MoveFocusedPanelToNextPosition,
 3184        window: &mut Window,
 3185        cx: &mut Context<Self>,
 3186    ) {
 3187        let docks = self.all_docks();
 3188        let active_dock = docks
 3189            .into_iter()
 3190            .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
 3191
 3192        if let Some(dock) = active_dock {
 3193            dock.update(cx, |dock, cx| {
 3194                let active_panel = dock
 3195                    .active_panel()
 3196                    .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
 3197
 3198                if let Some(panel) = active_panel {
 3199                    panel.move_to_next_position(window, cx);
 3200                }
 3201            })
 3202        }
 3203    }
 3204
 3205    pub fn prepare_to_close(
 3206        &mut self,
 3207        close_intent: CloseIntent,
 3208        window: &mut Window,
 3209        cx: &mut Context<Self>,
 3210    ) -> Task<Result<bool>> {
 3211        let active_call = self.active_global_call();
 3212
 3213        cx.spawn_in(window, async move |this, cx| {
 3214            this.update(cx, |this, _| {
 3215                if close_intent == CloseIntent::CloseWindow {
 3216                    this.removing = true;
 3217                }
 3218            })?;
 3219
 3220            let workspace_count = cx.update(|_window, cx| {
 3221                cx.windows()
 3222                    .iter()
 3223                    .filter(|window| window.downcast::<MultiWorkspace>().is_some())
 3224                    .count()
 3225            })?;
 3226
 3227            #[cfg(target_os = "macos")]
 3228            let save_last_workspace = false;
 3229
 3230            // On Linux and Windows, closing the last window should restore the last workspace.
 3231            #[cfg(not(target_os = "macos"))]
 3232            let save_last_workspace = {
 3233                let remaining_workspaces = cx.update(|_window, cx| {
 3234                    cx.windows()
 3235                        .iter()
 3236                        .filter_map(|window| window.downcast::<MultiWorkspace>())
 3237                        .filter_map(|multi_workspace| {
 3238                            multi_workspace
 3239                                .update(cx, |multi_workspace, _, cx| {
 3240                                    multi_workspace.workspace().read(cx).removing
 3241                                })
 3242                                .ok()
 3243                        })
 3244                        .filter(|removing| !removing)
 3245                        .count()
 3246                })?;
 3247
 3248                close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0
 3249            };
 3250
 3251            if let Some(active_call) = active_call
 3252                && workspace_count == 1
 3253                && cx
 3254                    .update(|_window, cx| active_call.0.is_in_room(cx))
 3255                    .unwrap_or(false)
 3256            {
 3257                if close_intent == CloseIntent::CloseWindow {
 3258                    this.update(cx, |_, cx| cx.emit(Event::Activate))?;
 3259                    let answer = cx.update(|window, cx| {
 3260                        window.prompt(
 3261                            PromptLevel::Warning,
 3262                            "Do you want to leave the current call?",
 3263                            None,
 3264                            &["Close window and hang up", "Cancel"],
 3265                            cx,
 3266                        )
 3267                    })?;
 3268
 3269                    if answer.await.log_err() == Some(1) {
 3270                        return anyhow::Ok(false);
 3271                    } else {
 3272                        if let Ok(task) = cx.update(|_window, cx| active_call.0.hang_up(cx)) {
 3273                            task.await.log_err();
 3274                        }
 3275                    }
 3276                }
 3277                if close_intent == CloseIntent::ReplaceWindow {
 3278                    _ = cx.update(|_window, cx| {
 3279                        let multi_workspace = cx
 3280                            .windows()
 3281                            .iter()
 3282                            .filter_map(|window| window.downcast::<MultiWorkspace>())
 3283                            .next()
 3284                            .unwrap();
 3285                        let project = multi_workspace
 3286                            .read(cx)?
 3287                            .workspace()
 3288                            .read(cx)
 3289                            .project
 3290                            .clone();
 3291                        if project.read(cx).is_shared() {
 3292                            active_call.0.unshare_project(project, cx)?;
 3293                        }
 3294                        Ok::<_, anyhow::Error>(())
 3295                    });
 3296                }
 3297            }
 3298
 3299            let save_result = this
 3300                .update_in(cx, |this, window, cx| {
 3301                    this.save_all_internal(SaveIntent::Close, window, cx)
 3302                })?
 3303                .await;
 3304
 3305            // If we're not quitting, but closing, we remove the workspace from
 3306            // the current session.
 3307            if close_intent != CloseIntent::Quit
 3308                && !save_last_workspace
 3309                && save_result.as_ref().is_ok_and(|&res| res)
 3310            {
 3311                this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
 3312                    .await;
 3313            }
 3314
 3315            save_result
 3316        })
 3317    }
 3318
 3319    fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
 3320        self.save_all_internal(
 3321            action.save_intent.unwrap_or(SaveIntent::SaveAll),
 3322            window,
 3323            cx,
 3324        )
 3325        .detach_and_log_err(cx);
 3326    }
 3327
 3328    fn send_keystrokes(
 3329        &mut self,
 3330        action: &SendKeystrokes,
 3331        window: &mut Window,
 3332        cx: &mut Context<Self>,
 3333    ) {
 3334        let keystrokes: Vec<Keystroke> = action
 3335            .0
 3336            .split(' ')
 3337            .flat_map(|k| Keystroke::parse(k).log_err())
 3338            .map(|k| {
 3339                cx.keyboard_mapper()
 3340                    .map_key_equivalent(k, false)
 3341                    .inner()
 3342                    .clone()
 3343            })
 3344            .collect();
 3345        let _ = self.send_keystrokes_impl(keystrokes, window, cx);
 3346    }
 3347
 3348    pub fn send_keystrokes_impl(
 3349        &mut self,
 3350        keystrokes: Vec<Keystroke>,
 3351        window: &mut Window,
 3352        cx: &mut Context<Self>,
 3353    ) -> Shared<Task<()>> {
 3354        let mut state = self.dispatching_keystrokes.borrow_mut();
 3355        if !state.dispatched.insert(keystrokes.clone()) {
 3356            cx.propagate();
 3357            return state.task.clone().unwrap();
 3358        }
 3359
 3360        state.queue.extend(keystrokes);
 3361
 3362        let keystrokes = self.dispatching_keystrokes.clone();
 3363        if state.task.is_none() {
 3364            state.task = Some(
 3365                window
 3366                    .spawn(cx, async move |cx| {
 3367                        // limit to 100 keystrokes to avoid infinite recursion.
 3368                        for _ in 0..100 {
 3369                            let keystroke = {
 3370                                let mut state = keystrokes.borrow_mut();
 3371                                let Some(keystroke) = state.queue.pop_front() else {
 3372                                    state.dispatched.clear();
 3373                                    state.task.take();
 3374                                    return;
 3375                                };
 3376                                keystroke
 3377                            };
 3378                            let focus_changed = cx
 3379                                .update(|window, cx| {
 3380                                    let focused = window.focused(cx);
 3381                                    window.dispatch_keystroke(keystroke.clone(), cx);
 3382                                    if window.focused(cx) != focused {
 3383                                        // dispatch_keystroke may cause the focus to change.
 3384                                        // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
 3385                                        // And we need that to happen before the next keystroke to keep vim mode happy...
 3386                                        // (Note that the tests always do this implicitly, so you must manually test with something like:
 3387                                        //   "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
 3388                                        // )
 3389                                        window.draw(cx).clear();
 3390                                        return true;
 3391                                    }
 3392                                    false
 3393                                })
 3394                                .unwrap_or(false);
 3395
 3396                            if focus_changed {
 3397                                yield_now().await;
 3398                            }
 3399                        }
 3400
 3401                        *keystrokes.borrow_mut() = Default::default();
 3402                        log::error!("over 100 keystrokes passed to send_keystrokes");
 3403                    })
 3404                    .shared(),
 3405            );
 3406        }
 3407        state.task.clone().unwrap()
 3408    }
 3409
 3410    /// Prompts the user to save or discard each dirty item, returning
 3411    /// `true` if they confirmed (saved/discarded everything) or `false`
 3412    /// if they cancelled. Used before removing worktree roots during
 3413    /// thread archival.
 3414    pub fn prompt_to_save_or_discard_dirty_items(
 3415        &mut self,
 3416        window: &mut Window,
 3417        cx: &mut Context<Self>,
 3418    ) -> Task<Result<bool>> {
 3419        self.save_all_internal(SaveIntent::Close, window, cx)
 3420    }
 3421
 3422    fn save_all_internal(
 3423        &mut self,
 3424        mut save_intent: SaveIntent,
 3425        window: &mut Window,
 3426        cx: &mut Context<Self>,
 3427    ) -> Task<Result<bool>> {
 3428        if self.project.read(cx).is_disconnected(cx) {
 3429            return Task::ready(Ok(true));
 3430        }
 3431        let dirty_items = self
 3432            .panes
 3433            .iter()
 3434            .flat_map(|pane| {
 3435                pane.read(cx).items().filter_map(|item| {
 3436                    if item.is_dirty(cx) {
 3437                        item.tab_content_text(0, cx);
 3438                        Some((pane.downgrade(), item.boxed_clone()))
 3439                    } else {
 3440                        None
 3441                    }
 3442                })
 3443            })
 3444            .collect::<Vec<_>>();
 3445
 3446        let project = self.project.clone();
 3447        cx.spawn_in(window, async move |workspace, cx| {
 3448            let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
 3449                let mut serialize_tasks = Vec::new();
 3450                let mut remaining_dirty_items = Vec::new();
 3451                workspace.update_in(cx, |workspace, window, cx| {
 3452                    for (pane, item) in dirty_items {
 3453                        if let Some(task) = item
 3454                            .to_serializable_item_handle(cx)
 3455                            .and_then(|handle| handle.serialize(workspace, true, window, cx))
 3456                        {
 3457                            serialize_tasks.push((pane, item, task));
 3458                        } else {
 3459                            remaining_dirty_items.push((pane, item));
 3460                        }
 3461                    }
 3462                })?;
 3463
 3464                for (pane, item, task) in serialize_tasks {
 3465                    if task.await.log_err().is_none() {
 3466                        remaining_dirty_items.push((pane, item));
 3467                    }
 3468                }
 3469
 3470                if !remaining_dirty_items.is_empty() {
 3471                    workspace.update(cx, |_, cx| cx.emit(Event::Activate))?;
 3472                }
 3473
 3474                if remaining_dirty_items.len() > 1 {
 3475                    let answer = workspace.update_in(cx, |_, window, cx| {
 3476                        cx.emit(Event::Activate);
 3477                        let detail = Pane::file_names_for_prompt(
 3478                            &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
 3479                            cx,
 3480                        );
 3481                        window.prompt(
 3482                            PromptLevel::Warning,
 3483                            "Do you want to save all changes in the following files?",
 3484                            Some(&detail),
 3485                            &["Save all", "Discard all", "Cancel"],
 3486                            cx,
 3487                        )
 3488                    })?;
 3489                    match answer.await.log_err() {
 3490                        Some(0) => save_intent = SaveIntent::SaveAll,
 3491                        Some(1) => save_intent = SaveIntent::Skip,
 3492                        Some(2) => return Ok(false),
 3493                        _ => {}
 3494                    }
 3495                }
 3496
 3497                remaining_dirty_items
 3498            } else {
 3499                dirty_items
 3500            };
 3501
 3502            for (pane, item) in dirty_items {
 3503                let (singleton, project_entry_ids) = cx.update(|_, cx| {
 3504                    (
 3505                        item.buffer_kind(cx) == ItemBufferKind::Singleton,
 3506                        item.project_entry_ids(cx),
 3507                    )
 3508                })?;
 3509                if (singleton || !project_entry_ids.is_empty())
 3510                    && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await?
 3511                {
 3512                    return Ok(false);
 3513                }
 3514            }
 3515            Ok(true)
 3516        })
 3517    }
 3518
 3519    pub fn open_workspace_for_paths(
 3520        &mut self,
 3521        // replace_current_window: bool,
 3522        mut open_mode: OpenMode,
 3523        paths: Vec<PathBuf>,
 3524        window: &mut Window,
 3525        cx: &mut Context<Self>,
 3526    ) -> Task<Result<Entity<Workspace>>> {
 3527        let requesting_window = window.window_handle().downcast::<MultiWorkspace>();
 3528        let is_remote = self.project.read(cx).is_via_collab();
 3529        let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
 3530        let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
 3531
 3532        let workspace_is_empty = !is_remote && !has_worktree && !has_dirty_items;
 3533        if workspace_is_empty {
 3534            open_mode = OpenMode::Activate;
 3535        }
 3536
 3537        let app_state = self.app_state.clone();
 3538
 3539        cx.spawn(async move |_, cx| {
 3540            let OpenResult { workspace, .. } = cx
 3541                .update(|cx| {
 3542                    open_paths(
 3543                        &paths,
 3544                        app_state,
 3545                        OpenOptions {
 3546                            requesting_window,
 3547                            open_mode,
 3548                            workspace_matching: if open_mode == OpenMode::NewWindow {
 3549                                WorkspaceMatching::None
 3550                            } else {
 3551                                WorkspaceMatching::default()
 3552                            },
 3553                            ..Default::default()
 3554                        },
 3555                        cx,
 3556                    )
 3557                })
 3558                .await?;
 3559            Ok(workspace)
 3560        })
 3561    }
 3562
 3563    #[allow(clippy::type_complexity)]
 3564    pub fn open_paths(
 3565        &mut self,
 3566        mut abs_paths: Vec<PathBuf>,
 3567        options: OpenOptions,
 3568        pane: Option<WeakEntity<Pane>>,
 3569        window: &mut Window,
 3570        cx: &mut Context<Self>,
 3571    ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> {
 3572        let fs = self.app_state.fs.clone();
 3573
 3574        let caller_ordered_abs_paths = abs_paths.clone();
 3575
 3576        // Sort the paths to ensure we add worktrees for parents before their children.
 3577        abs_paths.sort_unstable();
 3578        cx.spawn_in(window, async move |this, cx| {
 3579            let mut tasks = Vec::with_capacity(abs_paths.len());
 3580
 3581            for abs_path in &abs_paths {
 3582                let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
 3583                    OpenVisible::All => Some(true),
 3584                    OpenVisible::None => Some(false),
 3585                    OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
 3586                        Some(Some(metadata)) => Some(!metadata.is_dir),
 3587                        Some(None) => Some(true),
 3588                        None => None,
 3589                    },
 3590                    OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
 3591                        Some(Some(metadata)) => Some(metadata.is_dir),
 3592                        Some(None) => Some(false),
 3593                        None => None,
 3594                    },
 3595                };
 3596                let project_path = match visible {
 3597                    Some(visible) => match this
 3598                        .update(cx, |this, cx| {
 3599                            Workspace::project_path_for_path(
 3600                                this.project.clone(),
 3601                                abs_path,
 3602                                visible,
 3603                                cx,
 3604                            )
 3605                        })
 3606                        .log_err()
 3607                    {
 3608                        Some(project_path) => project_path.await.log_err(),
 3609                        None => None,
 3610                    },
 3611                    None => None,
 3612                };
 3613
 3614                let this = this.clone();
 3615                let abs_path: Arc<Path> = SanitizedPath::new(&abs_path).as_path().into();
 3616                let fs = fs.clone();
 3617                let pane = pane.clone();
 3618                let task = cx.spawn(async move |cx| {
 3619                    let (_worktree, project_path) = project_path?;
 3620                    if fs.is_dir(&abs_path).await {
 3621                        // Opening a directory should not race to update the active entry.
 3622                        // We'll select/reveal a deterministic final entry after all paths finish opening.
 3623                        None
 3624                    } else {
 3625                        Some(
 3626                            this.update_in(cx, |this, window, cx| {
 3627                                this.open_path(
 3628                                    project_path,
 3629                                    pane,
 3630                                    options.focus.unwrap_or(true),
 3631                                    window,
 3632                                    cx,
 3633                                )
 3634                            })
 3635                            .ok()?
 3636                            .await,
 3637                        )
 3638                    }
 3639                });
 3640                tasks.push(task);
 3641            }
 3642
 3643            let results = futures::future::join_all(tasks).await;
 3644
 3645            // Determine the winner using the fake/abstract FS metadata, not `Path::is_dir`.
 3646            let mut winner: Option<(PathBuf, bool)> = None;
 3647            for abs_path in caller_ordered_abs_paths.into_iter().rev() {
 3648                if let Some(Some(metadata)) = fs.metadata(&abs_path).await.log_err() {
 3649                    if !metadata.is_dir {
 3650                        winner = Some((abs_path, false));
 3651                        break;
 3652                    }
 3653                    if winner.is_none() {
 3654                        winner = Some((abs_path, true));
 3655                    }
 3656                } else if winner.is_none() {
 3657                    winner = Some((abs_path, false));
 3658                }
 3659            }
 3660
 3661            // Compute the winner entry id on the foreground thread and emit once, after all
 3662            // paths finish opening. This avoids races between concurrently-opening paths
 3663            // (directories in particular) and makes the resulting project panel selection
 3664            // deterministic.
 3665            if let Some((winner_abs_path, winner_is_dir)) = winner {
 3666                'emit_winner: {
 3667                    let winner_abs_path: Arc<Path> =
 3668                        SanitizedPath::new(&winner_abs_path).as_path().into();
 3669
 3670                    let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
 3671                        OpenVisible::All => true,
 3672                        OpenVisible::None => false,
 3673                        OpenVisible::OnlyFiles => !winner_is_dir,
 3674                        OpenVisible::OnlyDirectories => winner_is_dir,
 3675                    };
 3676
 3677                    let Some(worktree_task) = this
 3678                        .update(cx, |workspace, cx| {
 3679                            workspace.project.update(cx, |project, cx| {
 3680                                project.find_or_create_worktree(
 3681                                    winner_abs_path.as_ref(),
 3682                                    visible,
 3683                                    cx,
 3684                                )
 3685                            })
 3686                        })
 3687                        .ok()
 3688                    else {
 3689                        break 'emit_winner;
 3690                    };
 3691
 3692                    let Ok((worktree, _)) = worktree_task.await else {
 3693                        break 'emit_winner;
 3694                    };
 3695
 3696                    let Ok(Some(entry_id)) = this.update(cx, |_, cx| {
 3697                        let worktree = worktree.read(cx);
 3698                        let worktree_abs_path = worktree.abs_path();
 3699                        let entry = if winner_abs_path.as_ref() == worktree_abs_path.as_ref() {
 3700                            worktree.root_entry()
 3701                        } else {
 3702                            winner_abs_path
 3703                                .strip_prefix(worktree_abs_path.as_ref())
 3704                                .ok()
 3705                                .and_then(|relative_path| {
 3706                                    let relative_path =
 3707                                        RelPath::new(relative_path, PathStyle::local())
 3708                                            .log_err()?;
 3709                                    worktree.entry_for_path(&relative_path)
 3710                                })
 3711                        }?;
 3712                        Some(entry.id)
 3713                    }) else {
 3714                        break 'emit_winner;
 3715                    };
 3716
 3717                    this.update(cx, |workspace, cx| {
 3718                        workspace.project.update(cx, |_, cx| {
 3719                            cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
 3720                        });
 3721                    })
 3722                    .ok();
 3723                }
 3724            }
 3725
 3726            results
 3727        })
 3728    }
 3729
 3730    pub fn open_resolved_path(
 3731        &mut self,
 3732        path: ResolvedPath,
 3733        window: &mut Window,
 3734        cx: &mut Context<Self>,
 3735    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 3736        match path {
 3737            ResolvedPath::ProjectPath { project_path, .. } => {
 3738                self.open_path(project_path, None, true, window, cx)
 3739            }
 3740            ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
 3741                PathBuf::from(path),
 3742                OpenOptions {
 3743                    visible: Some(OpenVisible::None),
 3744                    ..Default::default()
 3745                },
 3746                window,
 3747                cx,
 3748            ),
 3749        }
 3750    }
 3751
 3752    pub fn absolute_path_of_worktree(
 3753        &self,
 3754        worktree_id: WorktreeId,
 3755        cx: &mut Context<Self>,
 3756    ) -> Option<PathBuf> {
 3757        self.project
 3758            .read(cx)
 3759            .worktree_for_id(worktree_id, cx)
 3760            // TODO: use `abs_path` or `root_dir`
 3761            .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
 3762    }
 3763
 3764    pub fn add_folder_to_project(
 3765        &mut self,
 3766        _: &AddFolderToProject,
 3767        window: &mut Window,
 3768        cx: &mut Context<Self>,
 3769    ) {
 3770        let project = self.project.read(cx);
 3771        if project.is_via_collab() {
 3772            self.show_error(
 3773                &anyhow!("You cannot add folders to someone else's project"),
 3774                cx,
 3775            );
 3776            return;
 3777        }
 3778        let paths = self.prompt_for_open_path(
 3779            PathPromptOptions {
 3780                files: false,
 3781                directories: true,
 3782                multiple: true,
 3783                prompt: None,
 3784            },
 3785            DirectoryLister::Project(self.project.clone()),
 3786            window,
 3787            cx,
 3788        );
 3789        cx.spawn_in(window, async move |this, cx| {
 3790            if let Some(paths) = paths.await.log_err().flatten() {
 3791                let results = this
 3792                    .update_in(cx, |this, window, cx| {
 3793                        this.open_paths(
 3794                            paths,
 3795                            OpenOptions {
 3796                                visible: Some(OpenVisible::All),
 3797                                ..Default::default()
 3798                            },
 3799                            None,
 3800                            window,
 3801                            cx,
 3802                        )
 3803                    })?
 3804                    .await;
 3805                for result in results.into_iter().flatten() {
 3806                    result.log_err();
 3807                }
 3808            }
 3809            anyhow::Ok(())
 3810        })
 3811        .detach_and_log_err(cx);
 3812    }
 3813
 3814    pub fn project_path_for_path(
 3815        project: Entity<Project>,
 3816        abs_path: &Path,
 3817        visible: bool,
 3818        cx: &mut App,
 3819    ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
 3820        let entry = project.update(cx, |project, cx| {
 3821            project.find_or_create_worktree(abs_path, visible, cx)
 3822        });
 3823        cx.spawn(async move |cx| {
 3824            let (worktree, path) = entry.await?;
 3825            let worktree_id = worktree.read_with(cx, |t, _| t.id());
 3826            Ok((worktree, ProjectPath { worktree_id, path }))
 3827        })
 3828    }
 3829
 3830    pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
 3831        self.panes.iter().flat_map(|pane| pane.read(cx).items())
 3832    }
 3833
 3834    pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
 3835        self.items_of_type(cx).max_by_key(|item| item.item_id())
 3836    }
 3837
 3838    pub fn items_of_type<'a, T: Item>(
 3839        &'a self,
 3840        cx: &'a App,
 3841    ) -> impl 'a + Iterator<Item = Entity<T>> {
 3842        self.panes
 3843            .iter()
 3844            .flat_map(|pane| pane.read(cx).items_of_type())
 3845    }
 3846
 3847    pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
 3848        self.active_pane().read(cx).active_item()
 3849    }
 3850
 3851    pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
 3852        let item = self.active_item(cx)?;
 3853        item.to_any_view().downcast::<I>().ok()
 3854    }
 3855
 3856    fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
 3857        self.active_item(cx).and_then(|item| item.project_path(cx))
 3858    }
 3859
 3860    pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
 3861        self.recent_navigation_history_iter(cx)
 3862            .filter_map(|(path, abs_path)| {
 3863                let worktree = self
 3864                    .project
 3865                    .read(cx)
 3866                    .worktree_for_id(path.worktree_id, cx)?;
 3867                if !worktree.read(cx).is_visible() {
 3868                    return None;
 3869                }
 3870                let settings_location = SettingsLocation {
 3871                    worktree_id: path.worktree_id,
 3872                    path: &path.path,
 3873                };
 3874                if WorktreeSettings::get(Some(settings_location), cx).is_path_read_only(&path.path)
 3875                {
 3876                    return None;
 3877                }
 3878                abs_path
 3879            })
 3880            .next()
 3881    }
 3882
 3883    pub fn save_active_item(
 3884        &mut self,
 3885        save_intent: SaveIntent,
 3886        window: &mut Window,
 3887        cx: &mut App,
 3888    ) -> Task<Result<()>> {
 3889        let project = self.project.clone();
 3890        let pane = self.active_pane();
 3891        let item = pane.read(cx).active_item();
 3892        let pane = pane.downgrade();
 3893
 3894        window.spawn(cx, async move |cx| {
 3895            if let Some(item) = item {
 3896                Pane::save_item(project, &pane, item.as_ref(), save_intent, cx)
 3897                    .await
 3898                    .map(|_| ())
 3899            } else {
 3900                Ok(())
 3901            }
 3902        })
 3903    }
 3904
 3905    pub fn close_inactive_items_and_panes(
 3906        &mut self,
 3907        action: &CloseInactiveTabsAndPanes,
 3908        window: &mut Window,
 3909        cx: &mut Context<Self>,
 3910    ) {
 3911        if let Some(task) = self.close_all_internal(
 3912            true,
 3913            action.save_intent.unwrap_or(SaveIntent::Close),
 3914            window,
 3915            cx,
 3916        ) {
 3917            task.detach_and_log_err(cx)
 3918        }
 3919    }
 3920
 3921    pub fn close_all_items_and_panes(
 3922        &mut self,
 3923        action: &CloseAllItemsAndPanes,
 3924        window: &mut Window,
 3925        cx: &mut Context<Self>,
 3926    ) {
 3927        if let Some(task) = self.close_all_internal(
 3928            false,
 3929            action.save_intent.unwrap_or(SaveIntent::Close),
 3930            window,
 3931            cx,
 3932        ) {
 3933            task.detach_and_log_err(cx)
 3934        }
 3935    }
 3936
 3937    /// Closes the active item across all panes.
 3938    pub fn close_item_in_all_panes(
 3939        &mut self,
 3940        action: &CloseItemInAllPanes,
 3941        window: &mut Window,
 3942        cx: &mut Context<Self>,
 3943    ) {
 3944        let Some(active_item) = self.active_pane().read(cx).active_item() else {
 3945            return;
 3946        };
 3947
 3948        let save_intent = action.save_intent.unwrap_or(SaveIntent::Close);
 3949        let close_pinned = action.close_pinned;
 3950
 3951        if let Some(project_path) = active_item.project_path(cx) {
 3952            self.close_items_with_project_path(
 3953                &project_path,
 3954                save_intent,
 3955                close_pinned,
 3956                window,
 3957                cx,
 3958            );
 3959        } else if close_pinned || !self.active_pane().read(cx).is_active_item_pinned() {
 3960            let item_id = active_item.item_id();
 3961            self.active_pane().update(cx, |pane, cx| {
 3962                pane.close_item_by_id(item_id, save_intent, window, cx)
 3963                    .detach_and_log_err(cx);
 3964            });
 3965        }
 3966    }
 3967
 3968    /// Closes all items with the given project path across all panes.
 3969    pub fn close_items_with_project_path(
 3970        &mut self,
 3971        project_path: &ProjectPath,
 3972        save_intent: SaveIntent,
 3973        close_pinned: bool,
 3974        window: &mut Window,
 3975        cx: &mut Context<Self>,
 3976    ) {
 3977        let panes = self.panes().to_vec();
 3978        for pane in panes {
 3979            pane.update(cx, |pane, cx| {
 3980                pane.close_items_for_project_path(
 3981                    project_path,
 3982                    save_intent,
 3983                    close_pinned,
 3984                    window,
 3985                    cx,
 3986                )
 3987                .detach_and_log_err(cx);
 3988            });
 3989        }
 3990    }
 3991
 3992    fn close_all_internal(
 3993        &mut self,
 3994        retain_active_pane: bool,
 3995        save_intent: SaveIntent,
 3996        window: &mut Window,
 3997        cx: &mut Context<Self>,
 3998    ) -> Option<Task<Result<()>>> {
 3999        let current_pane = self.active_pane();
 4000
 4001        let mut tasks = Vec::new();
 4002
 4003        if retain_active_pane {
 4004            let current_pane_close = current_pane.update(cx, |pane, cx| {
 4005                pane.close_other_items(
 4006                    &CloseOtherItems {
 4007                        save_intent: None,
 4008                        close_pinned: false,
 4009                    },
 4010                    None,
 4011                    window,
 4012                    cx,
 4013                )
 4014            });
 4015
 4016            tasks.push(current_pane_close);
 4017        }
 4018
 4019        for pane in self.panes() {
 4020            if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
 4021                continue;
 4022            }
 4023
 4024            let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| {
 4025                pane.close_all_items(
 4026                    &CloseAllItems {
 4027                        save_intent: Some(save_intent),
 4028                        close_pinned: false,
 4029                    },
 4030                    window,
 4031                    cx,
 4032                )
 4033            });
 4034
 4035            tasks.push(close_pane_items)
 4036        }
 4037
 4038        if tasks.is_empty() {
 4039            None
 4040        } else {
 4041            Some(cx.spawn_in(window, async move |_, _| {
 4042                for task in tasks {
 4043                    task.await?
 4044                }
 4045                Ok(())
 4046            }))
 4047        }
 4048    }
 4049
 4050    pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
 4051        self.dock_at_position(position).read(cx).is_open()
 4052    }
 4053
 4054    pub fn toggle_dock(
 4055        &mut self,
 4056        dock_side: DockPosition,
 4057        window: &mut Window,
 4058        cx: &mut Context<Self>,
 4059    ) {
 4060        let mut focus_center = false;
 4061        let mut reveal_dock = false;
 4062
 4063        let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
 4064        let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed;
 4065
 4066        if let Some(panel) = self.dock_at_position(dock_side).read(cx).active_panel() {
 4067            telemetry::event!(
 4068                "Panel Button Clicked",
 4069                name = panel.persistent_name(),
 4070                toggle_state = !was_visible
 4071            );
 4072        }
 4073        if was_visible {
 4074            self.save_open_dock_positions(cx);
 4075        }
 4076
 4077        let dock = self.dock_at_position(dock_side);
 4078        dock.update(cx, |dock, cx| {
 4079            dock.set_open(!was_visible, window, cx);
 4080
 4081            if dock.active_panel().is_none() {
 4082                let Some(panel_ix) = dock
 4083                    .first_enabled_panel_idx(cx)
 4084                    .log_with_level(log::Level::Info)
 4085                else {
 4086                    return;
 4087                };
 4088                dock.activate_panel(panel_ix, window, cx);
 4089            }
 4090
 4091            if let Some(active_panel) = dock.active_panel() {
 4092                if was_visible {
 4093                    if active_panel
 4094                        .panel_focus_handle(cx)
 4095                        .contains_focused(window, cx)
 4096                    {
 4097                        focus_center = true;
 4098                    }
 4099                } else {
 4100                    let focus_handle = &active_panel.panel_focus_handle(cx);
 4101                    window.focus(focus_handle, cx);
 4102                    reveal_dock = true;
 4103                }
 4104            }
 4105        });
 4106
 4107        if reveal_dock {
 4108            self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
 4109        }
 4110
 4111        if focus_center {
 4112            self.active_pane
 4113                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
 4114        }
 4115
 4116        cx.notify();
 4117        self.serialize_workspace(window, cx);
 4118    }
 4119
 4120    fn active_dock(&self, window: &Window, cx: &Context<Self>) -> Option<&Entity<Dock>> {
 4121        self.all_docks().into_iter().find(|&dock| {
 4122            dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
 4123        })
 4124    }
 4125
 4126    fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 4127        if let Some(dock) = self.active_dock(window, cx).cloned() {
 4128            self.save_open_dock_positions(cx);
 4129            dock.update(cx, |dock, cx| {
 4130                dock.set_open(false, window, cx);
 4131            });
 4132            return true;
 4133        }
 4134        false
 4135    }
 4136
 4137    pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 4138        self.save_open_dock_positions(cx);
 4139        for dock in self.all_docks() {
 4140            dock.update(cx, |dock, cx| {
 4141                dock.set_open(false, window, cx);
 4142            });
 4143        }
 4144
 4145        cx.focus_self(window);
 4146        cx.notify();
 4147        self.serialize_workspace(window, cx);
 4148    }
 4149
 4150    fn get_open_dock_positions(&self, cx: &Context<Self>) -> Vec<DockPosition> {
 4151        self.all_docks()
 4152            .into_iter()
 4153            .filter_map(|dock| {
 4154                let dock_ref = dock.read(cx);
 4155                if dock_ref.is_open() {
 4156                    Some(dock_ref.position())
 4157                } else {
 4158                    None
 4159                }
 4160            })
 4161            .collect()
 4162    }
 4163
 4164    /// Saves the positions of currently open docks.
 4165    ///
 4166    /// Updates `last_open_dock_positions` with positions of all currently open
 4167    /// docks, to later be restored by the 'Toggle All Docks' action.
 4168    fn save_open_dock_positions(&mut self, cx: &mut Context<Self>) {
 4169        let open_dock_positions = self.get_open_dock_positions(cx);
 4170        if !open_dock_positions.is_empty() {
 4171            self.last_open_dock_positions = open_dock_positions;
 4172        }
 4173    }
 4174
 4175    /// Toggles all docks between open and closed states.
 4176    ///
 4177    /// If any docks are open, closes all and remembers their positions. If all
 4178    /// docks are closed, restores the last remembered dock configuration.
 4179    fn toggle_all_docks(
 4180        &mut self,
 4181        _: &ToggleAllDocks,
 4182        window: &mut Window,
 4183        cx: &mut Context<Self>,
 4184    ) {
 4185        let open_dock_positions = self.get_open_dock_positions(cx);
 4186
 4187        if !open_dock_positions.is_empty() {
 4188            self.close_all_docks(window, cx);
 4189        } else if !self.last_open_dock_positions.is_empty() {
 4190            self.restore_last_open_docks(window, cx);
 4191        }
 4192    }
 4193
 4194    /// Reopens docks from the most recently remembered configuration.
 4195    ///
 4196    /// Opens all docks whose positions are stored in `last_open_dock_positions`
 4197    /// and clears the stored positions.
 4198    fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 4199        let positions_to_open = std::mem::take(&mut self.last_open_dock_positions);
 4200
 4201        for position in positions_to_open {
 4202            let dock = self.dock_at_position(position);
 4203            dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
 4204        }
 4205
 4206        cx.focus_self(window);
 4207        cx.notify();
 4208        self.serialize_workspace(window, cx);
 4209    }
 4210
 4211    /// Transfer focus to the panel of the given type.
 4212    pub fn focus_panel<T: Panel>(
 4213        &mut self,
 4214        window: &mut Window,
 4215        cx: &mut Context<Self>,
 4216    ) -> Option<Entity<T>> {
 4217        let panel = self.focus_or_unfocus_panel::<T>(window, cx, &mut |_, _, _| true)?;
 4218        panel.to_any().downcast().ok()
 4219    }
 4220
 4221    /// Focus the panel of the given type if it isn't already focused. If it is
 4222    /// already focused, then transfer focus back to the workspace center.
 4223    /// When the `close_panel_on_toggle` setting is enabled, also closes the
 4224    /// panel when transferring focus back to the center.
 4225    pub fn toggle_panel_focus<T: Panel>(
 4226        &mut self,
 4227        window: &mut Window,
 4228        cx: &mut Context<Self>,
 4229    ) -> bool {
 4230        let mut did_focus_panel = false;
 4231        self.focus_or_unfocus_panel::<T>(window, cx, &mut |panel, window, cx| {
 4232            did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
 4233            did_focus_panel
 4234        });
 4235
 4236        if !did_focus_panel && WorkspaceSettings::get_global(cx).close_panel_on_toggle {
 4237            self.close_panel::<T>(window, cx);
 4238        }
 4239
 4240        telemetry::event!(
 4241            "Panel Button Clicked",
 4242            name = T::persistent_name(),
 4243            toggle_state = did_focus_panel
 4244        );
 4245
 4246        did_focus_panel
 4247    }
 4248
 4249    pub fn focus_center_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 4250        if let Some(item) = self.active_item(cx) {
 4251            item.item_focus_handle(cx).focus(window, cx);
 4252        } else {
 4253            log::error!("Could not find a focus target when switching focus to the center panes",);
 4254        }
 4255    }
 4256
 4257    pub fn activate_panel_for_proto_id(
 4258        &mut self,
 4259        panel_id: PanelId,
 4260        window: &mut Window,
 4261        cx: &mut Context<Self>,
 4262    ) -> Option<Arc<dyn PanelHandle>> {
 4263        let mut panel = None;
 4264        for dock in self.all_docks() {
 4265            if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
 4266                panel = dock.update(cx, |dock, cx| {
 4267                    dock.activate_panel(panel_index, window, cx);
 4268                    dock.set_open(true, window, cx);
 4269                    dock.active_panel().cloned()
 4270                });
 4271                break;
 4272            }
 4273        }
 4274
 4275        if panel.is_some() {
 4276            cx.notify();
 4277            self.serialize_workspace(window, cx);
 4278        }
 4279
 4280        panel
 4281    }
 4282
 4283    /// Focus or unfocus the given panel type, depending on the given callback.
 4284    fn focus_or_unfocus_panel<T: Panel>(
 4285        &mut self,
 4286        window: &mut Window,
 4287        cx: &mut Context<Self>,
 4288        should_focus: &mut dyn FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
 4289    ) -> Option<Arc<dyn PanelHandle>> {
 4290        let mut result_panel = None;
 4291        let mut serialize = false;
 4292        for dock in self.all_docks() {
 4293            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
 4294                let mut focus_center = false;
 4295                let panel = dock.update(cx, |dock, cx| {
 4296                    dock.activate_panel(panel_index, window, cx);
 4297
 4298                    let panel = dock.active_panel().cloned();
 4299                    if let Some(panel) = panel.as_ref() {
 4300                        if should_focus(&**panel, window, cx) {
 4301                            dock.set_open(true, window, cx);
 4302                            panel.panel_focus_handle(cx).focus(window, cx);
 4303                        } else {
 4304                            focus_center = true;
 4305                        }
 4306                    }
 4307                    panel
 4308                });
 4309
 4310                if focus_center {
 4311                    self.active_pane
 4312                        .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
 4313                }
 4314
 4315                result_panel = panel;
 4316                serialize = true;
 4317                break;
 4318            }
 4319        }
 4320
 4321        if serialize {
 4322            self.serialize_workspace(window, cx);
 4323        }
 4324
 4325        cx.notify();
 4326        result_panel
 4327    }
 4328
 4329    /// Open the panel of the given type
 4330    pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 4331        for dock in self.all_docks() {
 4332            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
 4333                dock.update(cx, |dock, cx| {
 4334                    dock.activate_panel(panel_index, window, cx);
 4335                    dock.set_open(true, window, cx);
 4336                });
 4337            }
 4338        }
 4339    }
 4340
 4341    /// Open the panel of the given type, dismissing any zoomed items that
 4342    /// would obscure it (e.g. a zoomed terminal).
 4343    pub fn reveal_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 4344        let dock_position = self.all_docks().iter().find_map(|dock| {
 4345            let dock = dock.read(cx);
 4346            dock.panel_index_for_type::<T>().map(|_| dock.position())
 4347        });
 4348        self.dismiss_zoomed_items_to_reveal(dock_position, window, cx);
 4349        self.open_panel::<T>(window, cx);
 4350    }
 4351
 4352    pub fn close_panel<T: Panel>(&self, window: &mut Window, cx: &mut Context<Self>) {
 4353        for dock in self.all_docks().iter() {
 4354            dock.update(cx, |dock, cx| {
 4355                if dock.panel::<T>().is_some() {
 4356                    dock.set_open(false, window, cx)
 4357                }
 4358            })
 4359        }
 4360    }
 4361
 4362    pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
 4363        self.all_docks()
 4364            .iter()
 4365            .find_map(|dock| dock.read(cx).panel::<T>())
 4366    }
 4367
 4368    fn dismiss_zoomed_items_to_reveal(
 4369        &mut self,
 4370        dock_to_reveal: Option<DockPosition>,
 4371        window: &mut Window,
 4372        cx: &mut Context<Self>,
 4373    ) {
 4374        // If a center pane is zoomed, unzoom it.
 4375        for pane in &self.panes {
 4376            if pane != &self.active_pane || dock_to_reveal.is_some() {
 4377                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
 4378            }
 4379        }
 4380
 4381        // If another dock is zoomed, hide it.
 4382        let mut focus_center = false;
 4383        for dock in self.all_docks() {
 4384            dock.update(cx, |dock, cx| {
 4385                if Some(dock.position()) != dock_to_reveal
 4386                    && let Some(panel) = dock.active_panel()
 4387                    && panel.is_zoomed(window, cx)
 4388                {
 4389                    focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx);
 4390                    dock.set_open(false, window, cx);
 4391                }
 4392            });
 4393        }
 4394
 4395        if focus_center {
 4396            self.active_pane
 4397                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
 4398        }
 4399
 4400        if self.zoomed_position != dock_to_reveal {
 4401            self.zoomed = None;
 4402            self.zoomed_position = None;
 4403            cx.emit(Event::ZoomChanged);
 4404        }
 4405
 4406        cx.notify();
 4407    }
 4408
 4409    fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
 4410        let pane = cx.new(|cx| {
 4411            let mut pane = Pane::new(
 4412                self.weak_handle(),
 4413                self.project.clone(),
 4414                self.pane_history_timestamp.clone(),
 4415                None,
 4416                NewFile.boxed_clone(),
 4417                true,
 4418                window,
 4419                cx,
 4420            );
 4421            pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
 4422            pane
 4423        });
 4424        cx.subscribe_in(&pane, window, Self::handle_pane_event)
 4425            .detach();
 4426        self.panes.push(pane.clone());
 4427
 4428        window.focus(&pane.focus_handle(cx), cx);
 4429
 4430        cx.emit(Event::PaneAdded(pane.clone()));
 4431        pane
 4432    }
 4433
 4434    pub fn add_item_to_center(
 4435        &mut self,
 4436        item: Box<dyn ItemHandle>,
 4437        window: &mut Window,
 4438        cx: &mut Context<Self>,
 4439    ) -> bool {
 4440        if let Some(center_pane) = self.last_active_center_pane.clone() {
 4441            if let Some(center_pane) = center_pane.upgrade() {
 4442                center_pane.update(cx, |pane, cx| {
 4443                    pane.add_item(item, true, true, None, window, cx)
 4444                });
 4445                true
 4446            } else {
 4447                false
 4448            }
 4449        } else {
 4450            false
 4451        }
 4452    }
 4453
 4454    pub fn add_item_to_active_pane(
 4455        &mut self,
 4456        item: Box<dyn ItemHandle>,
 4457        destination_index: Option<usize>,
 4458        focus_item: bool,
 4459        window: &mut Window,
 4460        cx: &mut App,
 4461    ) {
 4462        self.add_item(
 4463            self.active_pane.clone(),
 4464            item,
 4465            destination_index,
 4466            false,
 4467            focus_item,
 4468            window,
 4469            cx,
 4470        )
 4471    }
 4472
 4473    pub fn add_item(
 4474        &mut self,
 4475        pane: Entity<Pane>,
 4476        item: Box<dyn ItemHandle>,
 4477        destination_index: Option<usize>,
 4478        activate_pane: bool,
 4479        focus_item: bool,
 4480        window: &mut Window,
 4481        cx: &mut App,
 4482    ) {
 4483        pane.update(cx, |pane, cx| {
 4484            pane.add_item(
 4485                item,
 4486                activate_pane,
 4487                focus_item,
 4488                destination_index,
 4489                window,
 4490                cx,
 4491            )
 4492        });
 4493    }
 4494
 4495    pub fn split_item(
 4496        &mut self,
 4497        split_direction: SplitDirection,
 4498        item: Box<dyn ItemHandle>,
 4499        window: &mut Window,
 4500        cx: &mut Context<Self>,
 4501    ) {
 4502        let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
 4503        self.add_item(new_pane, item, None, true, true, window, cx);
 4504    }
 4505
 4506    pub fn open_abs_path(
 4507        &mut self,
 4508        abs_path: PathBuf,
 4509        options: OpenOptions,
 4510        window: &mut Window,
 4511        cx: &mut Context<Self>,
 4512    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4513        cx.spawn_in(window, async move |workspace, cx| {
 4514            let open_paths_task_result = workspace
 4515                .update_in(cx, |workspace, window, cx| {
 4516                    workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
 4517                })
 4518                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
 4519                .await;
 4520            anyhow::ensure!(
 4521                open_paths_task_result.len() == 1,
 4522                "open abs path {abs_path:?} task returned incorrect number of results"
 4523            );
 4524            match open_paths_task_result
 4525                .into_iter()
 4526                .next()
 4527                .expect("ensured single task result")
 4528            {
 4529                Some(open_result) => {
 4530                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
 4531                }
 4532                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
 4533            }
 4534        })
 4535    }
 4536
 4537    pub fn split_abs_path(
 4538        &mut self,
 4539        abs_path: PathBuf,
 4540        visible: bool,
 4541        window: &mut Window,
 4542        cx: &mut Context<Self>,
 4543    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4544        let project_path_task =
 4545            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
 4546        cx.spawn_in(window, async move |this, cx| {
 4547            let (_, path) = project_path_task.await?;
 4548            this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
 4549                .await
 4550        })
 4551    }
 4552
 4553    pub fn open_path(
 4554        &mut self,
 4555        path: impl Into<ProjectPath>,
 4556        pane: Option<WeakEntity<Pane>>,
 4557        focus_item: bool,
 4558        window: &mut Window,
 4559        cx: &mut App,
 4560    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4561        self.open_path_preview(path, pane, focus_item, false, true, window, cx)
 4562    }
 4563
 4564    pub fn open_path_preview(
 4565        &mut self,
 4566        path: impl Into<ProjectPath>,
 4567        pane: Option<WeakEntity<Pane>>,
 4568        focus_item: bool,
 4569        allow_preview: bool,
 4570        activate: bool,
 4571        window: &mut Window,
 4572        cx: &mut App,
 4573    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4574        let pane = pane.unwrap_or_else(|| {
 4575            self.last_active_center_pane.clone().unwrap_or_else(|| {
 4576                self.panes
 4577                    .first()
 4578                    .expect("There must be an active pane")
 4579                    .downgrade()
 4580            })
 4581        });
 4582
 4583        let project_path = path.into();
 4584        let task = self.load_path(project_path.clone(), window, cx);
 4585        window.spawn(cx, async move |cx| {
 4586            let (project_entry_id, build_item) = task.await?;
 4587
 4588            pane.update_in(cx, |pane, window, cx| {
 4589                pane.open_item(
 4590                    project_entry_id,
 4591                    project_path,
 4592                    focus_item,
 4593                    allow_preview,
 4594                    activate,
 4595                    None,
 4596                    window,
 4597                    cx,
 4598                    build_item,
 4599                )
 4600            })
 4601        })
 4602    }
 4603
 4604    pub fn split_path(
 4605        &mut self,
 4606        path: impl Into<ProjectPath>,
 4607        window: &mut Window,
 4608        cx: &mut Context<Self>,
 4609    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4610        self.split_path_preview(path, false, None, window, cx)
 4611    }
 4612
 4613    pub fn split_path_preview(
 4614        &mut self,
 4615        path: impl Into<ProjectPath>,
 4616        allow_preview: bool,
 4617        split_direction: Option<SplitDirection>,
 4618        window: &mut Window,
 4619        cx: &mut Context<Self>,
 4620    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4621        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
 4622            self.panes
 4623                .first()
 4624                .expect("There must be an active pane")
 4625                .downgrade()
 4626        });
 4627
 4628        if let Member::Pane(center_pane) = &self.center.root
 4629            && center_pane.read(cx).items_len() == 0
 4630        {
 4631            return self.open_path(path, Some(pane), true, window, cx);
 4632        }
 4633
 4634        let project_path = path.into();
 4635        let task = self.load_path(project_path.clone(), window, cx);
 4636        cx.spawn_in(window, async move |this, cx| {
 4637            let (project_entry_id, build_item) = task.await?;
 4638            this.update_in(cx, move |this, window, cx| -> Option<_> {
 4639                let pane = pane.upgrade()?;
 4640                let new_pane = this.split_pane(
 4641                    pane,
 4642                    split_direction.unwrap_or(SplitDirection::Right),
 4643                    window,
 4644                    cx,
 4645                );
 4646                new_pane.update(cx, |new_pane, cx| {
 4647                    Some(new_pane.open_item(
 4648                        project_entry_id,
 4649                        project_path,
 4650                        true,
 4651                        allow_preview,
 4652                        true,
 4653                        None,
 4654                        window,
 4655                        cx,
 4656                        build_item,
 4657                    ))
 4658                })
 4659            })
 4660            .map(|option| option.context("pane was dropped"))?
 4661        })
 4662    }
 4663
 4664    fn load_path(
 4665        &mut self,
 4666        path: ProjectPath,
 4667        window: &mut Window,
 4668        cx: &mut App,
 4669    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
 4670        let registry = cx.default_global::<ProjectItemRegistry>().clone();
 4671        registry.open_path(self.project(), &path, window, cx)
 4672    }
 4673
 4674    pub fn find_project_item<T>(
 4675        &self,
 4676        pane: &Entity<Pane>,
 4677        project_item: &Entity<T::Item>,
 4678        cx: &App,
 4679    ) -> Option<Entity<T>>
 4680    where
 4681        T: ProjectItem,
 4682    {
 4683        use project::ProjectItem as _;
 4684        let project_item = project_item.read(cx);
 4685        let entry_id = project_item.entry_id(cx);
 4686        let project_path = project_item.project_path(cx);
 4687
 4688        let mut item = None;
 4689        if let Some(entry_id) = entry_id {
 4690            item = pane.read(cx).item_for_entry(entry_id, cx);
 4691        }
 4692        if item.is_none()
 4693            && let Some(project_path) = project_path
 4694        {
 4695            item = pane.read(cx).item_for_path(project_path, cx);
 4696        }
 4697
 4698        item.and_then(|item| item.downcast::<T>())
 4699    }
 4700
 4701    pub fn is_project_item_open<T>(
 4702        &self,
 4703        pane: &Entity<Pane>,
 4704        project_item: &Entity<T::Item>,
 4705        cx: &App,
 4706    ) -> bool
 4707    where
 4708        T: ProjectItem,
 4709    {
 4710        self.find_project_item::<T>(pane, project_item, cx)
 4711            .is_some()
 4712    }
 4713
 4714    pub fn open_project_item<T>(
 4715        &mut self,
 4716        pane: Entity<Pane>,
 4717        project_item: Entity<T::Item>,
 4718        activate_pane: bool,
 4719        focus_item: bool,
 4720        keep_old_preview: bool,
 4721        allow_new_preview: bool,
 4722        window: &mut Window,
 4723        cx: &mut Context<Self>,
 4724    ) -> Entity<T>
 4725    where
 4726        T: ProjectItem,
 4727    {
 4728        let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
 4729
 4730        if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
 4731            if !keep_old_preview
 4732                && let Some(old_id) = old_item_id
 4733                && old_id != item.item_id()
 4734            {
 4735                // switching to a different item, so unpreview old active item
 4736                pane.update(cx, |pane, _| {
 4737                    pane.unpreview_item_if_preview(old_id);
 4738                });
 4739            }
 4740
 4741            self.activate_item(&item, activate_pane, focus_item, window, cx);
 4742            if !allow_new_preview {
 4743                pane.update(cx, |pane, _| {
 4744                    pane.unpreview_item_if_preview(item.item_id());
 4745                });
 4746            }
 4747            return item;
 4748        }
 4749
 4750        let item = pane.update(cx, |pane, cx| {
 4751            cx.new(|cx| {
 4752                T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
 4753            })
 4754        });
 4755        let mut destination_index = None;
 4756        pane.update(cx, |pane, cx| {
 4757            if !keep_old_preview && let Some(old_id) = old_item_id {
 4758                pane.unpreview_item_if_preview(old_id);
 4759            }
 4760            if allow_new_preview {
 4761                destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
 4762            }
 4763        });
 4764
 4765        self.add_item(
 4766            pane,
 4767            Box::new(item.clone()),
 4768            destination_index,
 4769            activate_pane,
 4770            focus_item,
 4771            window,
 4772            cx,
 4773        );
 4774        item
 4775    }
 4776
 4777    pub fn open_shared_screen(
 4778        &mut self,
 4779        peer_id: PeerId,
 4780        window: &mut Window,
 4781        cx: &mut Context<Self>,
 4782    ) {
 4783        if let Some(shared_screen) =
 4784            self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
 4785        {
 4786            self.active_pane.update(cx, |pane, cx| {
 4787                pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
 4788            });
 4789        }
 4790    }
 4791
 4792    pub fn activate_item(
 4793        &mut self,
 4794        item: &dyn ItemHandle,
 4795        activate_pane: bool,
 4796        focus_item: bool,
 4797        window: &mut Window,
 4798        cx: &mut App,
 4799    ) -> bool {
 4800        let result = self.panes.iter().find_map(|pane| {
 4801            pane.read(cx)
 4802                .index_for_item(item)
 4803                .map(|ix| (pane.clone(), ix))
 4804        });
 4805        if let Some((pane, ix)) = result {
 4806            pane.update(cx, |pane, cx| {
 4807                pane.activate_item(ix, activate_pane, focus_item, window, cx)
 4808            });
 4809            true
 4810        } else {
 4811            false
 4812        }
 4813    }
 4814
 4815    fn activate_pane_at_index(
 4816        &mut self,
 4817        action: &ActivatePane,
 4818        window: &mut Window,
 4819        cx: &mut Context<Self>,
 4820    ) {
 4821        let panes = self.center.panes();
 4822        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
 4823            window.focus(&pane.focus_handle(cx), cx);
 4824        } else {
 4825            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
 4826                .detach();
 4827        }
 4828    }
 4829
 4830    fn move_item_to_pane_at_index(
 4831        &mut self,
 4832        action: &MoveItemToPane,
 4833        window: &mut Window,
 4834        cx: &mut Context<Self>,
 4835    ) {
 4836        let panes = self.center.panes();
 4837        let destination = match panes.get(action.destination) {
 4838            Some(&destination) => destination.clone(),
 4839            None => {
 4840                if !action.clone && self.active_pane.read(cx).items_len() < 2 {
 4841                    return;
 4842                }
 4843                let direction = SplitDirection::Right;
 4844                let split_off_pane = self
 4845                    .find_pane_in_direction(direction, cx)
 4846                    .unwrap_or_else(|| self.active_pane.clone());
 4847                let new_pane = self.add_pane(window, cx);
 4848                self.center.split(&split_off_pane, &new_pane, direction, cx);
 4849                new_pane
 4850            }
 4851        };
 4852
 4853        if action.clone {
 4854            if self
 4855                .active_pane
 4856                .read(cx)
 4857                .active_item()
 4858                .is_some_and(|item| item.can_split(cx))
 4859            {
 4860                clone_active_item(
 4861                    self.database_id(),
 4862                    &self.active_pane,
 4863                    &destination,
 4864                    action.focus,
 4865                    window,
 4866                    cx,
 4867                );
 4868                return;
 4869            }
 4870        }
 4871        move_active_item(
 4872            &self.active_pane,
 4873            &destination,
 4874            action.focus,
 4875            true,
 4876            window,
 4877            cx,
 4878        )
 4879    }
 4880
 4881    pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
 4882        let panes = self.center.panes();
 4883        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
 4884            let next_ix = (ix + 1) % panes.len();
 4885            let next_pane = panes[next_ix].clone();
 4886            window.focus(&next_pane.focus_handle(cx), cx);
 4887        }
 4888    }
 4889
 4890    pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
 4891        let panes = self.center.panes();
 4892        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
 4893            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
 4894            let prev_pane = panes[prev_ix].clone();
 4895            window.focus(&prev_pane.focus_handle(cx), cx);
 4896        }
 4897    }
 4898
 4899    pub fn activate_last_pane(&mut self, window: &mut Window, cx: &mut App) {
 4900        let last_pane = self.center.last_pane();
 4901        window.focus(&last_pane.focus_handle(cx), cx);
 4902    }
 4903
 4904    pub fn activate_pane_in_direction(
 4905        &mut self,
 4906        direction: SplitDirection,
 4907        window: &mut Window,
 4908        cx: &mut App,
 4909    ) {
 4910        use ActivateInDirectionTarget as Target;
 4911        enum Origin {
 4912            Sidebar,
 4913            LeftDock,
 4914            RightDock,
 4915            BottomDock,
 4916            Center,
 4917        }
 4918
 4919        let origin: Origin = if self
 4920            .sidebar_focus_handle
 4921            .as_ref()
 4922            .is_some_and(|h| h.contains_focused(window, cx))
 4923        {
 4924            Origin::Sidebar
 4925        } else {
 4926            [
 4927                (&self.left_dock, Origin::LeftDock),
 4928                (&self.right_dock, Origin::RightDock),
 4929                (&self.bottom_dock, Origin::BottomDock),
 4930            ]
 4931            .into_iter()
 4932            .find_map(|(dock, origin)| {
 4933                if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
 4934                    Some(origin)
 4935                } else {
 4936                    None
 4937                }
 4938            })
 4939            .unwrap_or(Origin::Center)
 4940        };
 4941
 4942        let get_last_active_pane = || {
 4943            let pane = self
 4944                .last_active_center_pane
 4945                .clone()
 4946                .unwrap_or_else(|| {
 4947                    self.panes
 4948                        .first()
 4949                        .expect("There must be an active pane")
 4950                        .downgrade()
 4951                })
 4952                .upgrade()?;
 4953            (pane.read(cx).items_len() != 0).then_some(pane)
 4954        };
 4955
 4956        let try_dock =
 4957            |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
 4958
 4959        let sidebar_target = self
 4960            .sidebar_focus_handle
 4961            .as_ref()
 4962            .map(|h| Target::Sidebar(h.clone()));
 4963
 4964        let sidebar_on_right = self
 4965            .multi_workspace
 4966            .as_ref()
 4967            .and_then(|mw| mw.upgrade())
 4968            .map_or(false, |mw| {
 4969                mw.read(cx).sidebar_side(cx) == SidebarSide::Right
 4970            });
 4971
 4972        let away_from_sidebar = if sidebar_on_right {
 4973            SplitDirection::Left
 4974        } else {
 4975            SplitDirection::Right
 4976        };
 4977
 4978        let (near_dock, far_dock) = if sidebar_on_right {
 4979            (&self.right_dock, &self.left_dock)
 4980        } else {
 4981            (&self.left_dock, &self.right_dock)
 4982        };
 4983
 4984        let target = match (origin, direction) {
 4985            (Origin::Sidebar, dir) if dir == away_from_sidebar => try_dock(near_dock)
 4986                .or_else(|| get_last_active_pane().map(Target::Pane))
 4987                .or_else(|| try_dock(&self.bottom_dock))
 4988                .or_else(|| try_dock(far_dock)),
 4989
 4990            (Origin::Sidebar, _) => None,
 4991
 4992            // We're in the center, so we first try to go to a different pane,
 4993            // otherwise try to go to a dock.
 4994            (Origin::Center, direction) => {
 4995                if let Some(pane) = self.find_pane_in_direction(direction, cx) {
 4996                    Some(Target::Pane(pane))
 4997                } else {
 4998                    match direction {
 4999                        SplitDirection::Up => None,
 5000                        SplitDirection::Down => try_dock(&self.bottom_dock),
 5001                        SplitDirection::Left => {
 5002                            let dock_target = try_dock(&self.left_dock);
 5003                            if sidebar_on_right {
 5004                                dock_target
 5005                            } else {
 5006                                dock_target.or(sidebar_target)
 5007                            }
 5008                        }
 5009                        SplitDirection::Right => {
 5010                            let dock_target = try_dock(&self.right_dock);
 5011                            if sidebar_on_right {
 5012                                dock_target.or(sidebar_target)
 5013                            } else {
 5014                                dock_target
 5015                            }
 5016                        }
 5017                    }
 5018                }
 5019            }
 5020
 5021            (Origin::LeftDock, SplitDirection::Right) => {
 5022                if let Some(last_active_pane) = get_last_active_pane() {
 5023                    Some(Target::Pane(last_active_pane))
 5024                } else {
 5025                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
 5026                }
 5027            }
 5028
 5029            (Origin::LeftDock, SplitDirection::Left) => {
 5030                if sidebar_on_right {
 5031                    None
 5032                } else {
 5033                    sidebar_target
 5034                }
 5035            }
 5036
 5037            (Origin::LeftDock, SplitDirection::Down)
 5038            | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
 5039
 5040            (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
 5041            (Origin::BottomDock, SplitDirection::Left) => {
 5042                let dock_target = try_dock(&self.left_dock);
 5043                if sidebar_on_right {
 5044                    dock_target
 5045                } else {
 5046                    dock_target.or(sidebar_target)
 5047                }
 5048            }
 5049            (Origin::BottomDock, SplitDirection::Right) => {
 5050                let dock_target = try_dock(&self.right_dock);
 5051                if sidebar_on_right {
 5052                    dock_target.or(sidebar_target)
 5053                } else {
 5054                    dock_target
 5055                }
 5056            }
 5057
 5058            (Origin::RightDock, SplitDirection::Left) => {
 5059                if let Some(last_active_pane) = get_last_active_pane() {
 5060                    Some(Target::Pane(last_active_pane))
 5061                } else {
 5062                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
 5063                }
 5064            }
 5065
 5066            (Origin::RightDock, SplitDirection::Right) => {
 5067                if sidebar_on_right {
 5068                    sidebar_target
 5069                } else {
 5070                    None
 5071                }
 5072            }
 5073
 5074            _ => None,
 5075        };
 5076
 5077        match target {
 5078            Some(ActivateInDirectionTarget::Pane(pane)) => {
 5079                let pane = pane.read(cx);
 5080                if let Some(item) = pane.active_item() {
 5081                    item.item_focus_handle(cx).focus(window, cx);
 5082                } else {
 5083                    log::error!(
 5084                        "Could not find a focus target when in switching focus in {direction} direction for a pane",
 5085                    );
 5086                }
 5087            }
 5088            Some(ActivateInDirectionTarget::Dock(dock)) => {
 5089                // Defer this to avoid a panic when the dock's active panel is already on the stack.
 5090                window.defer(cx, move |window, cx| {
 5091                    let dock = dock.read(cx);
 5092                    if let Some(panel) = dock.active_panel() {
 5093                        panel.panel_focus_handle(cx).focus(window, cx);
 5094                    } else {
 5095                        log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
 5096                    }
 5097                })
 5098            }
 5099            Some(ActivateInDirectionTarget::Sidebar(focus_handle)) => {
 5100                focus_handle.focus(window, cx);
 5101            }
 5102            None => {}
 5103        }
 5104    }
 5105
 5106    pub fn move_item_to_pane_in_direction(
 5107        &mut self,
 5108        action: &MoveItemToPaneInDirection,
 5109        window: &mut Window,
 5110        cx: &mut Context<Self>,
 5111    ) {
 5112        let destination = match self.find_pane_in_direction(action.direction, cx) {
 5113            Some(destination) => destination,
 5114            None => {
 5115                if !action.clone && self.active_pane.read(cx).items_len() < 2 {
 5116                    return;
 5117                }
 5118                let new_pane = self.add_pane(window, cx);
 5119                self.center
 5120                    .split(&self.active_pane, &new_pane, action.direction, cx);
 5121                new_pane
 5122            }
 5123        };
 5124
 5125        if action.clone {
 5126            if self
 5127                .active_pane
 5128                .read(cx)
 5129                .active_item()
 5130                .is_some_and(|item| item.can_split(cx))
 5131            {
 5132                clone_active_item(
 5133                    self.database_id(),
 5134                    &self.active_pane,
 5135                    &destination,
 5136                    action.focus,
 5137                    window,
 5138                    cx,
 5139                );
 5140                return;
 5141            }
 5142        }
 5143        move_active_item(
 5144            &self.active_pane,
 5145            &destination,
 5146            action.focus,
 5147            true,
 5148            window,
 5149            cx,
 5150        );
 5151    }
 5152
 5153    pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
 5154        self.center.bounding_box_for_pane(pane)
 5155    }
 5156
 5157    pub fn find_pane_in_direction(
 5158        &mut self,
 5159        direction: SplitDirection,
 5160        cx: &App,
 5161    ) -> Option<Entity<Pane>> {
 5162        self.center
 5163            .find_pane_in_direction(&self.active_pane, direction, cx)
 5164            .cloned()
 5165    }
 5166
 5167    pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
 5168        if let Some(to) = self.find_pane_in_direction(direction, cx) {
 5169            self.center.swap(&self.active_pane, &to, cx);
 5170            cx.notify();
 5171        }
 5172    }
 5173
 5174    pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
 5175        if self
 5176            .center
 5177            .move_to_border(&self.active_pane, direction, cx)
 5178            .unwrap()
 5179        {
 5180            cx.notify();
 5181        }
 5182    }
 5183
 5184    pub fn resize_pane(
 5185        &mut self,
 5186        axis: gpui::Axis,
 5187        amount: Pixels,
 5188        window: &mut Window,
 5189        cx: &mut Context<Self>,
 5190    ) {
 5191        let docks = self.all_docks();
 5192        let active_dock = docks
 5193            .into_iter()
 5194            .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
 5195
 5196        if let Some(dock_entity) = active_dock {
 5197            let dock = dock_entity.read(cx);
 5198            let Some(panel_size) = self.dock_size(&dock, window, cx) else {
 5199                return;
 5200            };
 5201            match dock.position() {
 5202                DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx),
 5203                DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx),
 5204                DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx),
 5205            }
 5206        } else {
 5207            self.center
 5208                .resize(&self.active_pane, axis, amount, &self.bounds, cx);
 5209        }
 5210        cx.notify();
 5211    }
 5212
 5213    pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
 5214        self.center.reset_pane_sizes(cx);
 5215        cx.notify();
 5216    }
 5217
 5218    fn handle_pane_focused(
 5219        &mut self,
 5220        pane: Entity<Pane>,
 5221        window: &mut Window,
 5222        cx: &mut Context<Self>,
 5223    ) {
 5224        self.flush_deferred_saves(window, cx);
 5225
 5226        // This is explicitly hoisted out of the following check for pane identity as
 5227        // terminal panel panes are not registered as a center panes.
 5228        self.status_bar.update(cx, |status_bar, cx| {
 5229            status_bar.set_active_pane(&pane, window, cx);
 5230        });
 5231        if self.active_pane != pane {
 5232            self.set_active_pane(&pane, window, cx);
 5233        }
 5234
 5235        if self.last_active_center_pane.is_none() {
 5236            self.last_active_center_pane = Some(pane.downgrade());
 5237        }
 5238
 5239        // If this pane is in a dock, preserve that dock when dismissing zoomed items.
 5240        // This prevents the dock from closing when focus events fire during window activation.
 5241        // We also preserve any dock whose active panel itself has focus — this covers
 5242        // panels like AgentPanel that don't implement `pane()` but can still be zoomed.
 5243        let dock_to_preserve = self.all_docks().iter().find_map(|dock| {
 5244            let dock_read = dock.read(cx);
 5245            if let Some(panel) = dock_read.active_panel() {
 5246                if panel.pane(cx).is_some_and(|dock_pane| dock_pane == pane)
 5247                    || panel.panel_focus_handle(cx).contains_focused(window, cx)
 5248                {
 5249                    return Some(dock_read.position());
 5250                }
 5251            }
 5252            None
 5253        });
 5254
 5255        self.dismiss_zoomed_items_to_reveal(dock_to_preserve, window, cx);
 5256        if pane.read(cx).is_zoomed() {
 5257            self.zoomed = Some(pane.downgrade().into());
 5258        } else {
 5259            self.zoomed = None;
 5260        }
 5261        self.zoomed_position = None;
 5262        cx.emit(Event::ZoomChanged);
 5263        self.update_active_view_for_followers(window, cx);
 5264        pane.update(cx, |pane, _| {
 5265            pane.track_alternate_file_items();
 5266        });
 5267
 5268        cx.notify();
 5269    }
 5270
 5271    fn set_active_pane(
 5272        &mut self,
 5273        pane: &Entity<Pane>,
 5274        window: &mut Window,
 5275        cx: &mut Context<Self>,
 5276    ) {
 5277        self.active_pane = pane.clone();
 5278        self.active_item_path_changed(true, window, cx);
 5279        self.last_active_center_pane = Some(pane.downgrade());
 5280    }
 5281
 5282    fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 5283        self.flush_deferred_saves(window, cx);
 5284        self.update_active_view_for_followers(window, cx);
 5285    }
 5286
 5287    fn flush_deferred_saves(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 5288        let deferred = std::mem::take(&mut self.deferred_save_items);
 5289        for weak_item in deferred {
 5290            let Some(item) = weak_item.upgrade() else {
 5291                continue;
 5292            };
 5293            // Skip if focus returned to this item
 5294            let focus_handle = item.item_focus_handle(cx);
 5295            if focus_handle.contains_focused(window, cx) {
 5296                continue;
 5297            }
 5298            Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
 5299                .detach_and_log_err(cx);
 5300        }
 5301    }
 5302
 5303    fn handle_pane_event(
 5304        &mut self,
 5305        pane: &Entity<Pane>,
 5306        event: &pane::Event,
 5307        window: &mut Window,
 5308        cx: &mut Context<Self>,
 5309    ) {
 5310        let mut serialize_workspace = true;
 5311        match event {
 5312            pane::Event::AddItem { item } => {
 5313                item.added_to_pane(self, pane.clone(), window, cx);
 5314                cx.emit(Event::ItemAdded {
 5315                    item: item.boxed_clone(),
 5316                });
 5317            }
 5318            pane::Event::Split { direction, mode } => {
 5319                match mode {
 5320                    SplitMode::ClonePane => {
 5321                        self.split_and_clone(pane.clone(), *direction, window, cx)
 5322                            .detach();
 5323                    }
 5324                    SplitMode::EmptyPane => {
 5325                        self.split_pane(pane.clone(), *direction, window, cx);
 5326                    }
 5327                    SplitMode::MovePane => {
 5328                        self.split_and_move(pane.clone(), *direction, window, cx);
 5329                    }
 5330                };
 5331            }
 5332            pane::Event::JoinIntoNext => {
 5333                self.join_pane_into_next(pane.clone(), window, cx);
 5334            }
 5335            pane::Event::JoinAll => {
 5336                self.join_all_panes(window, cx);
 5337            }
 5338            pane::Event::Remove { focus_on_pane } => {
 5339                self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
 5340            }
 5341            pane::Event::ActivateItem {
 5342                local,
 5343                focus_changed,
 5344            } => {
 5345                window.invalidate_character_coordinates();
 5346
 5347                pane.update(cx, |pane, _| {
 5348                    pane.track_alternate_file_items();
 5349                });
 5350                if *local {
 5351                    self.unfollow_in_pane(pane, window, cx);
 5352                }
 5353                serialize_workspace = *focus_changed || pane != self.active_pane();
 5354                if pane == self.active_pane() {
 5355                    self.active_item_path_changed(*focus_changed, window, cx);
 5356                    self.update_active_view_for_followers(window, cx);
 5357                } else if *local {
 5358                    self.set_active_pane(pane, window, cx);
 5359                }
 5360            }
 5361            pane::Event::UserSavedItem { item, save_intent } => {
 5362                cx.emit(Event::UserSavedItem {
 5363                    pane: pane.downgrade(),
 5364                    item: item.boxed_clone(),
 5365                    save_intent: *save_intent,
 5366                });
 5367                serialize_workspace = false;
 5368            }
 5369            pane::Event::ChangeItemTitle => {
 5370                if *pane == self.active_pane {
 5371                    self.active_item_path_changed(false, window, cx);
 5372                }
 5373                serialize_workspace = false;
 5374            }
 5375            pane::Event::RemovedItem { item } => {
 5376                cx.emit(Event::ActiveItemChanged);
 5377                self.update_window_edited(window, cx);
 5378                if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id())
 5379                    && entry.get().entity_id() == pane.entity_id()
 5380                {
 5381                    entry.remove();
 5382                }
 5383                cx.emit(Event::ItemRemoved {
 5384                    item_id: item.item_id(),
 5385                });
 5386            }
 5387            pane::Event::Focus => {
 5388                window.invalidate_character_coordinates();
 5389                self.handle_pane_focused(pane.clone(), window, cx);
 5390            }
 5391            pane::Event::ZoomIn => {
 5392                if *pane == self.active_pane {
 5393                    pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
 5394                    if pane.read(cx).has_focus(window, cx) {
 5395                        self.zoomed = Some(pane.downgrade().into());
 5396                        self.zoomed_position = None;
 5397                        cx.emit(Event::ZoomChanged);
 5398                    }
 5399                    cx.notify();
 5400                }
 5401            }
 5402            pane::Event::ZoomOut => {
 5403                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
 5404                if self.zoomed_position.is_none() {
 5405                    self.zoomed = None;
 5406                    cx.emit(Event::ZoomChanged);
 5407                }
 5408                cx.notify();
 5409            }
 5410            pane::Event::ItemPinned | pane::Event::ItemUnpinned => {}
 5411        }
 5412
 5413        if serialize_workspace {
 5414            self.serialize_workspace(window, cx);
 5415        }
 5416    }
 5417
 5418    pub fn unfollow_in_pane(
 5419        &mut self,
 5420        pane: &Entity<Pane>,
 5421        window: &mut Window,
 5422        cx: &mut Context<Workspace>,
 5423    ) -> Option<CollaboratorId> {
 5424        let leader_id = self.leader_for_pane(pane)?;
 5425        self.unfollow(leader_id, window, cx);
 5426        Some(leader_id)
 5427    }
 5428
 5429    pub fn split_pane(
 5430        &mut self,
 5431        pane_to_split: Entity<Pane>,
 5432        split_direction: SplitDirection,
 5433        window: &mut Window,
 5434        cx: &mut Context<Self>,
 5435    ) -> Entity<Pane> {
 5436        let new_pane = self.add_pane(window, cx);
 5437        self.center
 5438            .split(&pane_to_split, &new_pane, split_direction, cx);
 5439        cx.notify();
 5440        new_pane
 5441    }
 5442
 5443    pub fn split_and_move(
 5444        &mut self,
 5445        pane: Entity<Pane>,
 5446        direction: SplitDirection,
 5447        window: &mut Window,
 5448        cx: &mut Context<Self>,
 5449    ) {
 5450        let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else {
 5451            return;
 5452        };
 5453        let new_pane = self.add_pane(window, cx);
 5454        new_pane.update(cx, |pane, cx| {
 5455            pane.add_item(item, true, true, None, window, cx)
 5456        });
 5457        self.center.split(&pane, &new_pane, direction, cx);
 5458        cx.notify();
 5459    }
 5460
 5461    pub fn split_and_clone(
 5462        &mut self,
 5463        pane: Entity<Pane>,
 5464        direction: SplitDirection,
 5465        window: &mut Window,
 5466        cx: &mut Context<Self>,
 5467    ) -> Task<Option<Entity<Pane>>> {
 5468        let Some(item) = pane.read(cx).active_item() else {
 5469            return Task::ready(None);
 5470        };
 5471        if !item.can_split(cx) {
 5472            return Task::ready(None);
 5473        }
 5474        let task = item.clone_on_split(self.database_id(), window, cx);
 5475        cx.spawn_in(window, async move |this, cx| {
 5476            if let Some(clone) = task.await {
 5477                this.update_in(cx, |this, window, cx| {
 5478                    let new_pane = this.add_pane(window, cx);
 5479                    let nav_history = pane.read(cx).fork_nav_history();
 5480                    new_pane.update(cx, |pane, cx| {
 5481                        pane.set_nav_history(nav_history, cx);
 5482                        pane.add_item(clone, true, true, None, window, cx)
 5483                    });
 5484                    this.center.split(&pane, &new_pane, direction, cx);
 5485                    cx.notify();
 5486                    new_pane
 5487                })
 5488                .ok()
 5489            } else {
 5490                None
 5491            }
 5492        })
 5493    }
 5494
 5495    pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 5496        let active_item = self.active_pane.read(cx).active_item();
 5497        for pane in &self.panes {
 5498            join_pane_into_active(&self.active_pane, pane, window, cx);
 5499        }
 5500        if let Some(active_item) = active_item {
 5501            self.activate_item(active_item.as_ref(), true, true, window, cx);
 5502        }
 5503        cx.notify();
 5504    }
 5505
 5506    pub fn join_pane_into_next(
 5507        &mut self,
 5508        pane: Entity<Pane>,
 5509        window: &mut Window,
 5510        cx: &mut Context<Self>,
 5511    ) {
 5512        let next_pane = self
 5513            .find_pane_in_direction(SplitDirection::Right, cx)
 5514            .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
 5515            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
 5516            .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
 5517        let Some(next_pane) = next_pane else {
 5518            return;
 5519        };
 5520        move_all_items(&pane, &next_pane, window, cx);
 5521        cx.notify();
 5522    }
 5523
 5524    fn remove_pane(
 5525        &mut self,
 5526        pane: Entity<Pane>,
 5527        focus_on: Option<Entity<Pane>>,
 5528        window: &mut Window,
 5529        cx: &mut Context<Self>,
 5530    ) {
 5531        if self.center.remove(&pane, cx).unwrap() {
 5532            self.force_remove_pane(&pane, &focus_on, window, cx);
 5533            self.unfollow_in_pane(&pane, window, cx);
 5534            self.last_leaders_by_pane.remove(&pane.downgrade());
 5535            for removed_item in pane.read(cx).items() {
 5536                self.panes_by_item.remove(&removed_item.item_id());
 5537            }
 5538
 5539            cx.notify();
 5540        } else {
 5541            self.active_item_path_changed(true, window, cx);
 5542        }
 5543        cx.emit(Event::PaneRemoved);
 5544    }
 5545
 5546    pub fn panes_mut(&mut self) -> &mut [Entity<Pane>] {
 5547        &mut self.panes
 5548    }
 5549
 5550    pub fn panes(&self) -> &[Entity<Pane>] {
 5551        &self.panes
 5552    }
 5553
 5554    pub fn active_pane(&self) -> &Entity<Pane> {
 5555        &self.active_pane
 5556    }
 5557
 5558    pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
 5559        for dock in self.all_docks() {
 5560            if dock.focus_handle(cx).contains_focused(window, cx)
 5561                && let Some(pane) = dock
 5562                    .read(cx)
 5563                    .active_panel()
 5564                    .and_then(|panel| panel.pane(cx))
 5565            {
 5566                return pane;
 5567            }
 5568        }
 5569        self.active_pane().clone()
 5570    }
 5571
 5572    pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
 5573        self.find_pane_in_direction(SplitDirection::Right, cx)
 5574            .unwrap_or_else(|| {
 5575                self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
 5576            })
 5577    }
 5578
 5579    pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
 5580        self.pane_for_item_id(handle.item_id())
 5581    }
 5582
 5583    pub fn pane_for_item_id(&self, item_id: EntityId) -> Option<Entity<Pane>> {
 5584        let weak_pane = self.panes_by_item.get(&item_id)?;
 5585        weak_pane.upgrade()
 5586    }
 5587
 5588    pub fn pane_for_entity_id(&self, entity_id: EntityId) -> Option<Entity<Pane>> {
 5589        self.panes
 5590            .iter()
 5591            .find(|pane| pane.entity_id() == entity_id)
 5592            .cloned()
 5593    }
 5594
 5595    fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
 5596        self.follower_states.retain(|leader_id, state| {
 5597            if *leader_id == CollaboratorId::PeerId(peer_id) {
 5598                for item in state.items_by_leader_view_id.values() {
 5599                    item.view.set_leader_id(None, window, cx);
 5600                }
 5601                false
 5602            } else {
 5603                true
 5604            }
 5605        });
 5606        cx.notify();
 5607    }
 5608
 5609    pub fn start_following(
 5610        &mut self,
 5611        leader_id: impl Into<CollaboratorId>,
 5612        window: &mut Window,
 5613        cx: &mut Context<Self>,
 5614    ) -> Option<Task<Result<()>>> {
 5615        let leader_id = leader_id.into();
 5616        let pane = self.active_pane().clone();
 5617
 5618        self.last_leaders_by_pane
 5619            .insert(pane.downgrade(), leader_id);
 5620        self.unfollow(leader_id, window, cx);
 5621        self.unfollow_in_pane(&pane, window, cx);
 5622        self.follower_states.insert(
 5623            leader_id,
 5624            FollowerState {
 5625                center_pane: pane.clone(),
 5626                dock_pane: None,
 5627                active_view_id: None,
 5628                items_by_leader_view_id: Default::default(),
 5629            },
 5630        );
 5631        cx.notify();
 5632
 5633        match leader_id {
 5634            CollaboratorId::PeerId(leader_peer_id) => {
 5635                let room_id = self.active_call()?.room_id(cx)?;
 5636                let project_id = self.project.read(cx).remote_id();
 5637                let request = self.app_state.client.request(proto::Follow {
 5638                    room_id,
 5639                    project_id,
 5640                    leader_id: Some(leader_peer_id),
 5641                });
 5642
 5643                Some(cx.spawn_in(window, async move |this, cx| {
 5644                    let response = request.await?;
 5645                    this.update(cx, |this, _| {
 5646                        let state = this
 5647                            .follower_states
 5648                            .get_mut(&leader_id)
 5649                            .context("following interrupted")?;
 5650                        state.active_view_id = response
 5651                            .active_view
 5652                            .as_ref()
 5653                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
 5654                        anyhow::Ok(())
 5655                    })??;
 5656                    if let Some(view) = response.active_view {
 5657                        Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
 5658                    }
 5659                    this.update_in(cx, |this, window, cx| {
 5660                        this.leader_updated(leader_id, window, cx)
 5661                    })?;
 5662                    Ok(())
 5663                }))
 5664            }
 5665            CollaboratorId::Agent => {
 5666                self.leader_updated(leader_id, window, cx)?;
 5667                Some(Task::ready(Ok(())))
 5668            }
 5669        }
 5670    }
 5671
 5672    pub fn follow_next_collaborator(
 5673        &mut self,
 5674        _: &FollowNextCollaborator,
 5675        window: &mut Window,
 5676        cx: &mut Context<Self>,
 5677    ) {
 5678        let collaborators = self.project.read(cx).collaborators();
 5679        let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
 5680            let mut collaborators = collaborators.keys().copied();
 5681            for peer_id in collaborators.by_ref() {
 5682                if CollaboratorId::PeerId(peer_id) == leader_id {
 5683                    break;
 5684                }
 5685            }
 5686            collaborators.next().map(CollaboratorId::PeerId)
 5687        } else if let Some(last_leader_id) =
 5688            self.last_leaders_by_pane.get(&self.active_pane.downgrade())
 5689        {
 5690            match last_leader_id {
 5691                CollaboratorId::PeerId(peer_id) => {
 5692                    if collaborators.contains_key(peer_id) {
 5693                        Some(*last_leader_id)
 5694                    } else {
 5695                        None
 5696                    }
 5697                }
 5698                CollaboratorId::Agent => Some(CollaboratorId::Agent),
 5699            }
 5700        } else {
 5701            None
 5702        };
 5703
 5704        let pane = self.active_pane.clone();
 5705        let Some(leader_id) = next_leader_id.or_else(|| {
 5706            Some(CollaboratorId::PeerId(
 5707                collaborators.keys().copied().next()?,
 5708            ))
 5709        }) else {
 5710            return;
 5711        };
 5712        if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
 5713            return;
 5714        }
 5715        if let Some(task) = self.start_following(leader_id, window, cx) {
 5716            task.detach_and_log_err(cx)
 5717        }
 5718    }
 5719
 5720    pub fn follow(
 5721        &mut self,
 5722        leader_id: impl Into<CollaboratorId>,
 5723        window: &mut Window,
 5724        cx: &mut Context<Self>,
 5725    ) {
 5726        let leader_id = leader_id.into();
 5727
 5728        if let CollaboratorId::PeerId(peer_id) = leader_id {
 5729            let Some(active_call) = GlobalAnyActiveCall::try_global(cx) else {
 5730                return;
 5731            };
 5732            let Some(remote_participant) =
 5733                active_call.0.remote_participant_for_peer_id(peer_id, cx)
 5734            else {
 5735                return;
 5736            };
 5737
 5738            let project = self.project.read(cx);
 5739
 5740            let other_project_id = match remote_participant.location {
 5741                ParticipantLocation::External => None,
 5742                ParticipantLocation::UnsharedProject => None,
 5743                ParticipantLocation::SharedProject { project_id } => {
 5744                    if Some(project_id) == project.remote_id() {
 5745                        None
 5746                    } else {
 5747                        Some(project_id)
 5748                    }
 5749                }
 5750            };
 5751
 5752            // if they are active in another project, follow there.
 5753            if let Some(project_id) = other_project_id {
 5754                let app_state = self.app_state.clone();
 5755                crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
 5756                    .detach_and_prompt_err("Failed to join project", window, cx, |error, _, _| {
 5757                        Some(format!("{error:#}"))
 5758                    });
 5759            }
 5760        }
 5761
 5762        // if you're already following, find the right pane and focus it.
 5763        if let Some(follower_state) = self.follower_states.get(&leader_id) {
 5764            window.focus(&follower_state.pane().focus_handle(cx), cx);
 5765
 5766            return;
 5767        }
 5768
 5769        // Otherwise, follow.
 5770        if let Some(task) = self.start_following(leader_id, window, cx) {
 5771            task.detach_and_log_err(cx)
 5772        }
 5773    }
 5774
 5775    pub fn unfollow(
 5776        &mut self,
 5777        leader_id: impl Into<CollaboratorId>,
 5778        window: &mut Window,
 5779        cx: &mut Context<Self>,
 5780    ) -> Option<()> {
 5781        cx.notify();
 5782
 5783        let leader_id = leader_id.into();
 5784        let state = self.follower_states.remove(&leader_id)?;
 5785        for (_, item) in state.items_by_leader_view_id {
 5786            item.view.set_leader_id(None, window, cx);
 5787        }
 5788
 5789        if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
 5790            let project_id = self.project.read(cx).remote_id();
 5791            let room_id = self.active_call()?.room_id(cx)?;
 5792            self.app_state
 5793                .client
 5794                .send(proto::Unfollow {
 5795                    room_id,
 5796                    project_id,
 5797                    leader_id: Some(leader_peer_id),
 5798                })
 5799                .log_err();
 5800        }
 5801
 5802        Some(())
 5803    }
 5804
 5805    pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
 5806        self.follower_states.contains_key(&id.into())
 5807    }
 5808
 5809    fn active_item_path_changed(
 5810        &mut self,
 5811        focus_changed: bool,
 5812        window: &mut Window,
 5813        cx: &mut Context<Self>,
 5814    ) {
 5815        cx.emit(Event::ActiveItemChanged);
 5816        let active_entry = self.active_project_path(cx);
 5817        self.project.update(cx, |project, cx| {
 5818            project.set_active_path(active_entry.clone(), cx)
 5819        });
 5820
 5821        if focus_changed && let Some(project_path) = &active_entry {
 5822            let git_store_entity = self.project.read(cx).git_store().clone();
 5823            git_store_entity.update(cx, |git_store, cx| {
 5824                git_store.set_active_repo_for_path(project_path, cx);
 5825            });
 5826        }
 5827
 5828        self.update_window_title(window, cx);
 5829    }
 5830
 5831    fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
 5832        let project = self.project().read(cx);
 5833        let mut title = String::new();
 5834
 5835        for (i, worktree) in project.visible_worktrees(cx).enumerate() {
 5836            let name = worktree.read(cx).root_name_str();
 5837
 5838            if i > 0 {
 5839                title.push_str(", ");
 5840            }
 5841            title.push_str(name);
 5842        }
 5843
 5844        if title.is_empty() {
 5845            title = "empty project".to_string();
 5846        }
 5847
 5848        if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
 5849            let filename = path.path.file_name().or_else(|| {
 5850                Some(
 5851                    project
 5852                        .worktree_for_id(path.worktree_id, cx)?
 5853                        .read(cx)
 5854                        .root_name_str(),
 5855                )
 5856            });
 5857
 5858            if let Some(filename) = filename {
 5859                title.push_str("");
 5860                title.push_str(filename.as_ref());
 5861            }
 5862        }
 5863
 5864        if project.is_via_collab() {
 5865            title.push_str("");
 5866        } else if project.is_shared() {
 5867            title.push_str("");
 5868        }
 5869
 5870        if let Some(last_title) = self.last_window_title.as_ref()
 5871            && &title == last_title
 5872        {
 5873            return;
 5874        }
 5875        window.set_window_title(&title);
 5876        SystemWindowTabController::update_tab_title(
 5877            cx,
 5878            window.window_handle().window_id(),
 5879            SharedString::from(&title),
 5880        );
 5881        self.last_window_title = Some(title);
 5882    }
 5883
 5884    fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
 5885        let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
 5886        if is_edited != self.window_edited {
 5887            self.window_edited = is_edited;
 5888            window.set_window_edited(self.window_edited)
 5889        }
 5890    }
 5891
 5892    fn update_item_dirty_state(
 5893        &mut self,
 5894        item: &dyn ItemHandle,
 5895        window: &mut Window,
 5896        cx: &mut App,
 5897    ) {
 5898        let is_dirty = item.is_dirty(cx);
 5899        let item_id = item.item_id();
 5900        let was_dirty = self.dirty_items.contains_key(&item_id);
 5901        if is_dirty == was_dirty {
 5902            return;
 5903        }
 5904        if was_dirty {
 5905            self.dirty_items.remove(&item_id);
 5906            self.update_window_edited(window, cx);
 5907            return;
 5908        }
 5909
 5910        let workspace = self.weak_handle();
 5911        let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
 5912            return;
 5913        };
 5914        let on_release_callback = Box::new(move |cx: &mut App| {
 5915            window_handle
 5916                .update(cx, |_, window, cx| {
 5917                    workspace
 5918                        .update(cx, |workspace, cx| {
 5919                            workspace.dirty_items.remove(&item_id);
 5920                            workspace.update_window_edited(window, cx)
 5921                        })
 5922                        .ok();
 5923                })
 5924                .ok();
 5925        });
 5926
 5927        let s = item.on_release(cx, on_release_callback);
 5928        self.dirty_items.insert(item_id, s);
 5929        self.update_window_edited(window, cx);
 5930    }
 5931
 5932    fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
 5933        if self.notifications.is_empty() {
 5934            None
 5935        } else {
 5936            Some(
 5937                div()
 5938                    .absolute()
 5939                    .right_3()
 5940                    .bottom_3()
 5941                    .w_112()
 5942                    .h_full()
 5943                    .flex()
 5944                    .flex_col()
 5945                    .justify_end()
 5946                    .gap_2()
 5947                    .children(
 5948                        self.notifications
 5949                            .iter()
 5950                            .map(|(_, notification)| notification.clone().into_any()),
 5951                    ),
 5952            )
 5953        }
 5954    }
 5955
 5956    // RPC handlers
 5957
 5958    fn active_view_for_follower(
 5959        &self,
 5960        follower_project_id: Option<u64>,
 5961        window: &mut Window,
 5962        cx: &mut Context<Self>,
 5963    ) -> Option<proto::View> {
 5964        let (item, panel_id) = self.active_item_for_followers(window, cx);
 5965        let item = item?;
 5966        let leader_id = self
 5967            .pane_for(&*item)
 5968            .and_then(|pane| self.leader_for_pane(&pane));
 5969        let leader_peer_id = match leader_id {
 5970            Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
 5971            Some(CollaboratorId::Agent) | None => None,
 5972        };
 5973
 5974        let item_handle = item.to_followable_item_handle(cx)?;
 5975        let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
 5976        let variant = item_handle.to_state_proto(window, cx)?;
 5977
 5978        if item_handle.is_project_item(window, cx)
 5979            && (follower_project_id.is_none()
 5980                || follower_project_id != self.project.read(cx).remote_id())
 5981        {
 5982            return None;
 5983        }
 5984
 5985        Some(proto::View {
 5986            id: id.to_proto(),
 5987            leader_id: leader_peer_id,
 5988            variant: Some(variant),
 5989            panel_id: panel_id.map(|id| id as i32),
 5990        })
 5991    }
 5992
 5993    fn handle_follow(
 5994        &mut self,
 5995        follower_project_id: Option<u64>,
 5996        window: &mut Window,
 5997        cx: &mut Context<Self>,
 5998    ) -> proto::FollowResponse {
 5999        let active_view = self.active_view_for_follower(follower_project_id, window, cx);
 6000
 6001        cx.notify();
 6002        proto::FollowResponse {
 6003            views: active_view.iter().cloned().collect(),
 6004            active_view,
 6005        }
 6006    }
 6007
 6008    fn handle_update_followers(
 6009        &mut self,
 6010        leader_id: PeerId,
 6011        message: proto::UpdateFollowers,
 6012        _window: &mut Window,
 6013        _cx: &mut Context<Self>,
 6014    ) {
 6015        self.leader_updates_tx
 6016            .unbounded_send((leader_id, message))
 6017            .ok();
 6018    }
 6019
 6020    async fn process_leader_update(
 6021        this: &WeakEntity<Self>,
 6022        leader_id: PeerId,
 6023        update: proto::UpdateFollowers,
 6024        cx: &mut AsyncWindowContext,
 6025    ) -> Result<()> {
 6026        match update.variant.context("invalid update")? {
 6027            proto::update_followers::Variant::CreateView(view) => {
 6028                let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
 6029                let should_add_view = this.update(cx, |this, _| {
 6030                    if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
 6031                        anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
 6032                    } else {
 6033                        anyhow::Ok(false)
 6034                    }
 6035                })??;
 6036
 6037                if should_add_view {
 6038                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
 6039                }
 6040            }
 6041            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
 6042                let should_add_view = this.update(cx, |this, _| {
 6043                    if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
 6044                        state.active_view_id = update_active_view
 6045                            .view
 6046                            .as_ref()
 6047                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
 6048
 6049                        if state.active_view_id.is_some_and(|view_id| {
 6050                            !state.items_by_leader_view_id.contains_key(&view_id)
 6051                        }) {
 6052                            anyhow::Ok(true)
 6053                        } else {
 6054                            anyhow::Ok(false)
 6055                        }
 6056                    } else {
 6057                        anyhow::Ok(false)
 6058                    }
 6059                })??;
 6060
 6061                if should_add_view && let Some(view) = update_active_view.view {
 6062                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
 6063                }
 6064            }
 6065            proto::update_followers::Variant::UpdateView(update_view) => {
 6066                let variant = update_view.variant.context("missing update view variant")?;
 6067                let id = update_view.id.context("missing update view id")?;
 6068                let mut tasks = Vec::new();
 6069                this.update_in(cx, |this, window, cx| {
 6070                    let project = this.project.clone();
 6071                    if let Some(state) = this.follower_states.get(&leader_id.into()) {
 6072                        let view_id = ViewId::from_proto(id.clone())?;
 6073                        if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
 6074                            tasks.push(item.view.apply_update_proto(
 6075                                &project,
 6076                                variant.clone(),
 6077                                window,
 6078                                cx,
 6079                            ));
 6080                        }
 6081                    }
 6082                    anyhow::Ok(())
 6083                })??;
 6084                try_join_all(tasks).await.log_err();
 6085            }
 6086        }
 6087        this.update_in(cx, |this, window, cx| {
 6088            this.leader_updated(leader_id, window, cx)
 6089        })?;
 6090        Ok(())
 6091    }
 6092
 6093    async fn add_view_from_leader(
 6094        this: WeakEntity<Self>,
 6095        leader_id: PeerId,
 6096        view: &proto::View,
 6097        cx: &mut AsyncWindowContext,
 6098    ) -> Result<()> {
 6099        let this = this.upgrade().context("workspace dropped")?;
 6100
 6101        let Some(id) = view.id.clone() else {
 6102            anyhow::bail!("no id for view");
 6103        };
 6104        let id = ViewId::from_proto(id)?;
 6105        let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
 6106
 6107        let pane = this.update(cx, |this, _cx| {
 6108            let state = this
 6109                .follower_states
 6110                .get(&leader_id.into())
 6111                .context("stopped following")?;
 6112            anyhow::Ok(state.pane().clone())
 6113        })?;
 6114        let existing_item = pane.update_in(cx, |pane, window, cx| {
 6115            let client = this.read(cx).client().clone();
 6116            pane.items().find_map(|item| {
 6117                let item = item.to_followable_item_handle(cx)?;
 6118                if item.remote_id(&client, window, cx) == Some(id) {
 6119                    Some(item)
 6120                } else {
 6121                    None
 6122                }
 6123            })
 6124        })?;
 6125        let item = if let Some(existing_item) = existing_item {
 6126            existing_item
 6127        } else {
 6128            let variant = view.variant.clone();
 6129            anyhow::ensure!(variant.is_some(), "missing view variant");
 6130
 6131            let task = cx.update(|window, cx| {
 6132                FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
 6133            })?;
 6134
 6135            let Some(task) = task else {
 6136                anyhow::bail!(
 6137                    "failed to construct view from leader (maybe from a different version of zed?)"
 6138                );
 6139            };
 6140
 6141            let mut new_item = task.await?;
 6142            pane.update_in(cx, |pane, window, cx| {
 6143                let mut item_to_remove = None;
 6144                for (ix, item) in pane.items().enumerate() {
 6145                    if let Some(item) = item.to_followable_item_handle(cx) {
 6146                        match new_item.dedup(item.as_ref(), window, cx) {
 6147                            Some(item::Dedup::KeepExisting) => {
 6148                                new_item =
 6149                                    item.boxed_clone().to_followable_item_handle(cx).unwrap();
 6150                                break;
 6151                            }
 6152                            Some(item::Dedup::ReplaceExisting) => {
 6153                                item_to_remove = Some((ix, item.item_id()));
 6154                                break;
 6155                            }
 6156                            None => {}
 6157                        }
 6158                    }
 6159                }
 6160
 6161                if let Some((ix, id)) = item_to_remove {
 6162                    pane.remove_item(id, false, false, window, cx);
 6163                    pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
 6164                }
 6165            })?;
 6166
 6167            new_item
 6168        };
 6169
 6170        this.update_in(cx, |this, window, cx| {
 6171            let state = this.follower_states.get_mut(&leader_id.into())?;
 6172            item.set_leader_id(Some(leader_id.into()), window, cx);
 6173            state.items_by_leader_view_id.insert(
 6174                id,
 6175                FollowerView {
 6176                    view: item,
 6177                    location: panel_id,
 6178                },
 6179            );
 6180
 6181            Some(())
 6182        })
 6183        .context("no follower state")?;
 6184
 6185        Ok(())
 6186    }
 6187
 6188    fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 6189        let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
 6190            return;
 6191        };
 6192
 6193        if let Some(agent_location) = self.project.read(cx).agent_location() {
 6194            let buffer_entity_id = agent_location.buffer.entity_id();
 6195            let view_id = ViewId {
 6196                creator: CollaboratorId::Agent,
 6197                id: buffer_entity_id.as_u64(),
 6198            };
 6199            follower_state.active_view_id = Some(view_id);
 6200
 6201            let item = match follower_state.items_by_leader_view_id.entry(view_id) {
 6202                hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
 6203                hash_map::Entry::Vacant(entry) => {
 6204                    let existing_view =
 6205                        follower_state
 6206                            .center_pane
 6207                            .read(cx)
 6208                            .items()
 6209                            .find_map(|item| {
 6210                                let item = item.to_followable_item_handle(cx)?;
 6211                                if item.buffer_kind(cx) == ItemBufferKind::Singleton
 6212                                    && item.project_item_model_ids(cx).as_slice()
 6213                                        == [buffer_entity_id]
 6214                                {
 6215                                    Some(item)
 6216                                } else {
 6217                                    None
 6218                                }
 6219                            });
 6220                    let view = existing_view.or_else(|| {
 6221                        agent_location.buffer.upgrade().and_then(|buffer| {
 6222                            cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
 6223                                registry.build_item(buffer, self.project.clone(), None, window, cx)
 6224                            })?
 6225                            .to_followable_item_handle(cx)
 6226                        })
 6227                    });
 6228
 6229                    view.map(|view| {
 6230                        entry.insert(FollowerView {
 6231                            view,
 6232                            location: None,
 6233                        })
 6234                    })
 6235                }
 6236            };
 6237
 6238            if let Some(item) = item {
 6239                item.view
 6240                    .set_leader_id(Some(CollaboratorId::Agent), window, cx);
 6241                item.view
 6242                    .update_agent_location(agent_location.position, window, cx);
 6243            }
 6244        } else {
 6245            follower_state.active_view_id = None;
 6246        }
 6247
 6248        self.leader_updated(CollaboratorId::Agent, window, cx);
 6249    }
 6250
 6251    pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
 6252        let mut is_project_item = true;
 6253        let mut update = proto::UpdateActiveView::default();
 6254        if window.is_window_active() {
 6255            let (active_item, panel_id) = self.active_item_for_followers(window, cx);
 6256
 6257            if let Some(item) = active_item
 6258                && item.item_focus_handle(cx).contains_focused(window, cx)
 6259            {
 6260                let leader_id = self
 6261                    .pane_for(&*item)
 6262                    .and_then(|pane| self.leader_for_pane(&pane));
 6263                let leader_peer_id = match leader_id {
 6264                    Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
 6265                    Some(CollaboratorId::Agent) | None => None,
 6266                };
 6267
 6268                if let Some(item) = item.to_followable_item_handle(cx) {
 6269                    let id = item
 6270                        .remote_id(&self.app_state.client, window, cx)
 6271                        .map(|id| id.to_proto());
 6272
 6273                    if let Some(id) = id
 6274                        && let Some(variant) = item.to_state_proto(window, cx)
 6275                    {
 6276                        let view = Some(proto::View {
 6277                            id,
 6278                            leader_id: leader_peer_id,
 6279                            variant: Some(variant),
 6280                            panel_id: panel_id.map(|id| id as i32),
 6281                        });
 6282
 6283                        is_project_item = item.is_project_item(window, cx);
 6284                        update = proto::UpdateActiveView { view };
 6285                    };
 6286                }
 6287            }
 6288        }
 6289
 6290        let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
 6291        if active_view_id != self.last_active_view_id.as_ref() {
 6292            self.last_active_view_id = active_view_id.cloned();
 6293            self.update_followers(
 6294                is_project_item,
 6295                proto::update_followers::Variant::UpdateActiveView(update),
 6296                window,
 6297                cx,
 6298            );
 6299        }
 6300    }
 6301
 6302    fn active_item_for_followers(
 6303        &self,
 6304        window: &mut Window,
 6305        cx: &mut App,
 6306    ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
 6307        let mut active_item = None;
 6308        let mut panel_id = None;
 6309        for dock in self.all_docks() {
 6310            if dock.focus_handle(cx).contains_focused(window, cx)
 6311                && let Some(panel) = dock.read(cx).active_panel()
 6312                && let Some(pane) = panel.pane(cx)
 6313                && let Some(item) = pane.read(cx).active_item()
 6314            {
 6315                active_item = Some(item);
 6316                panel_id = panel.remote_id();
 6317                break;
 6318            }
 6319        }
 6320
 6321        if active_item.is_none() {
 6322            active_item = self.active_pane().read(cx).active_item();
 6323        }
 6324        (active_item, panel_id)
 6325    }
 6326
 6327    fn update_followers(
 6328        &self,
 6329        project_only: bool,
 6330        update: proto::update_followers::Variant,
 6331        _: &mut Window,
 6332        cx: &mut App,
 6333    ) -> Option<()> {
 6334        // If this update only applies to for followers in the current project,
 6335        // then skip it unless this project is shared. If it applies to all
 6336        // followers, regardless of project, then set `project_id` to none,
 6337        // indicating that it goes to all followers.
 6338        let project_id = if project_only {
 6339            Some(self.project.read(cx).remote_id()?)
 6340        } else {
 6341            None
 6342        };
 6343        self.app_state().workspace_store.update(cx, |store, cx| {
 6344            store.update_followers(project_id, update, cx)
 6345        })
 6346    }
 6347
 6348    pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
 6349        self.follower_states.iter().find_map(|(leader_id, state)| {
 6350            if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
 6351                Some(*leader_id)
 6352            } else {
 6353                None
 6354            }
 6355        })
 6356    }
 6357
 6358    fn leader_updated(
 6359        &mut self,
 6360        leader_id: impl Into<CollaboratorId>,
 6361        window: &mut Window,
 6362        cx: &mut Context<Self>,
 6363    ) -> Option<Box<dyn ItemHandle>> {
 6364        cx.notify();
 6365
 6366        let leader_id = leader_id.into();
 6367        let (panel_id, item) = match leader_id {
 6368            CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
 6369            CollaboratorId::Agent => (None, self.active_item_for_agent()?),
 6370        };
 6371
 6372        let state = self.follower_states.get(&leader_id)?;
 6373        let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
 6374        let pane;
 6375        if let Some(panel_id) = panel_id {
 6376            pane = self
 6377                .activate_panel_for_proto_id(panel_id, window, cx)?
 6378                .pane(cx)?;
 6379            let state = self.follower_states.get_mut(&leader_id)?;
 6380            state.dock_pane = Some(pane.clone());
 6381        } else {
 6382            pane = state.center_pane.clone();
 6383            let state = self.follower_states.get_mut(&leader_id)?;
 6384            if let Some(dock_pane) = state.dock_pane.take() {
 6385                transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
 6386            }
 6387        }
 6388
 6389        pane.update(cx, |pane, cx| {
 6390            let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
 6391            if let Some(index) = pane.index_for_item(item.as_ref()) {
 6392                pane.activate_item(index, false, false, window, cx);
 6393            } else {
 6394                pane.add_item(item.boxed_clone(), false, false, None, window, cx)
 6395            }
 6396
 6397            if focus_active_item {
 6398                pane.focus_active_item(window, cx)
 6399            }
 6400        });
 6401
 6402        Some(item)
 6403    }
 6404
 6405    fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
 6406        let state = self.follower_states.get(&CollaboratorId::Agent)?;
 6407        let active_view_id = state.active_view_id?;
 6408        Some(
 6409            state
 6410                .items_by_leader_view_id
 6411                .get(&active_view_id)?
 6412                .view
 6413                .boxed_clone(),
 6414        )
 6415    }
 6416
 6417    fn active_item_for_peer(
 6418        &self,
 6419        peer_id: PeerId,
 6420        window: &mut Window,
 6421        cx: &mut Context<Self>,
 6422    ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
 6423        let call = self.active_call()?;
 6424        let participant = call.remote_participant_for_peer_id(peer_id, cx)?;
 6425        let leader_in_this_app;
 6426        let leader_in_this_project;
 6427        match participant.location {
 6428            ParticipantLocation::SharedProject { project_id } => {
 6429                leader_in_this_app = true;
 6430                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
 6431            }
 6432            ParticipantLocation::UnsharedProject => {
 6433                leader_in_this_app = true;
 6434                leader_in_this_project = false;
 6435            }
 6436            ParticipantLocation::External => {
 6437                leader_in_this_app = false;
 6438                leader_in_this_project = false;
 6439            }
 6440        };
 6441        let state = self.follower_states.get(&peer_id.into())?;
 6442        let mut item_to_activate = None;
 6443        if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
 6444            if let Some(item) = state.items_by_leader_view_id.get(&active_view_id)
 6445                && (leader_in_this_project || !item.view.is_project_item(window, cx))
 6446            {
 6447                item_to_activate = Some((item.location, item.view.boxed_clone()));
 6448            }
 6449        } else if let Some(shared_screen) =
 6450            self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
 6451        {
 6452            item_to_activate = Some((None, Box::new(shared_screen)));
 6453        }
 6454        item_to_activate
 6455    }
 6456
 6457    fn shared_screen_for_peer(
 6458        &self,
 6459        peer_id: PeerId,
 6460        pane: &Entity<Pane>,
 6461        window: &mut Window,
 6462        cx: &mut App,
 6463    ) -> Option<Entity<SharedScreen>> {
 6464        self.active_call()?
 6465            .create_shared_screen(peer_id, pane, window, cx)
 6466    }
 6467
 6468    pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 6469        if window.is_window_active() {
 6470            self.update_active_view_for_followers(window, cx);
 6471
 6472            if let Some(database_id) = self.database_id {
 6473                let db = WorkspaceDb::global(cx);
 6474                cx.background_spawn(async move { db.update_timestamp(database_id).await })
 6475                    .detach();
 6476            }
 6477        } else {
 6478            // When window is deactivated, flush any deferred saves since focus has left the window
 6479            self.flush_deferred_saves(window, cx);
 6480            for pane in &self.panes {
 6481                pane.update(cx, |pane, cx| {
 6482                    if let Some(item) = pane.active_item() {
 6483                        item.workspace_deactivated(window, cx);
 6484                    }
 6485                    for item in pane.items() {
 6486                        if matches!(
 6487                            item.workspace_settings(cx).autosave,
 6488                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
 6489                        ) {
 6490                            Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
 6491                                .detach_and_log_err(cx);
 6492                        }
 6493                    }
 6494                });
 6495            }
 6496        }
 6497    }
 6498
 6499    pub fn active_call(&self) -> Option<&dyn AnyActiveCall> {
 6500        self.active_call.as_ref().map(|(call, _)| &*call.0)
 6501    }
 6502
 6503    pub fn active_global_call(&self) -> Option<GlobalAnyActiveCall> {
 6504        self.active_call.as_ref().map(|(call, _)| call.clone())
 6505    }
 6506
 6507    fn on_active_call_event(
 6508        &mut self,
 6509        event: &ActiveCallEvent,
 6510        window: &mut Window,
 6511        cx: &mut Context<Self>,
 6512    ) {
 6513        match event {
 6514            ActiveCallEvent::ParticipantLocationChanged { participant_id }
 6515            | ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => {
 6516                self.leader_updated(participant_id, window, cx);
 6517            }
 6518        }
 6519    }
 6520
 6521    pub fn database_id(&self) -> Option<WorkspaceId> {
 6522        self.database_id
 6523    }
 6524
 6525    #[cfg(any(test, feature = "test-support"))]
 6526    pub(crate) fn set_database_id(&mut self, id: WorkspaceId) {
 6527        self.database_id = Some(id);
 6528    }
 6529
 6530    pub fn session_id(&self) -> Option<String> {
 6531        self.session_id.clone()
 6532    }
 6533
 6534    fn save_window_bounds(&self, window: &mut Window, cx: &mut App) -> Task<()> {
 6535        let Some(display) = window.display(cx) else {
 6536            return Task::ready(());
 6537        };
 6538        let Ok(display_uuid) = display.uuid() else {
 6539            return Task::ready(());
 6540        };
 6541
 6542        let window_bounds = window.inner_window_bounds();
 6543        let database_id = self.database_id;
 6544        let has_paths = !self.root_paths(cx).is_empty();
 6545        let db = WorkspaceDb::global(cx);
 6546        let kvp = db::kvp::KeyValueStore::global(cx);
 6547
 6548        cx.background_executor().spawn(async move {
 6549            if !has_paths {
 6550                persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
 6551                    .await
 6552                    .log_err();
 6553            }
 6554            if let Some(database_id) = database_id {
 6555                db.set_window_open_status(
 6556                    database_id,
 6557                    SerializedWindowBounds(window_bounds),
 6558                    display_uuid,
 6559                )
 6560                .await
 6561                .log_err();
 6562            } else {
 6563                persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
 6564                    .await
 6565                    .log_err();
 6566            }
 6567        })
 6568    }
 6569
 6570    /// Bypass the 200ms serialization throttle and write workspace state to
 6571    /// the DB immediately. Returns a task the caller can await to ensure the
 6572    /// write completes. Used by the quit handler so the most recent state
 6573    /// isn't lost to a pending throttle timer when the process exits.
 6574    pub fn flush_serialization(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
 6575        self._schedule_serialize_workspace.take();
 6576        self._serialize_workspace_task.take();
 6577        self.bounds_save_task_queued.take();
 6578
 6579        let bounds_task = self.save_window_bounds(window, cx);
 6580        let serialize_task = self.serialize_workspace_internal(window, cx);
 6581        cx.spawn(async move |_| {
 6582            bounds_task.await;
 6583            serialize_task.await;
 6584        })
 6585    }
 6586
 6587    pub fn root_paths(&self, cx: &App) -> Vec<Arc<Path>> {
 6588        let project = self.project().read(cx);
 6589        project
 6590            .visible_worktrees(cx)
 6591            .map(|worktree| worktree.read(cx).abs_path())
 6592            .collect::<Vec<_>>()
 6593    }
 6594
 6595    fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
 6596        match member {
 6597            Member::Axis(PaneAxis { members, .. }) => {
 6598                for child in members.iter() {
 6599                    self.remove_panes(child.clone(), window, cx)
 6600                }
 6601            }
 6602            Member::Pane(pane) => {
 6603                self.force_remove_pane(&pane, &None, window, cx);
 6604            }
 6605        }
 6606    }
 6607
 6608    fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
 6609        self.session_id.take();
 6610        self.serialize_workspace_internal(window, cx)
 6611    }
 6612
 6613    fn force_remove_pane(
 6614        &mut self,
 6615        pane: &Entity<Pane>,
 6616        focus_on: &Option<Entity<Pane>>,
 6617        window: &mut Window,
 6618        cx: &mut Context<Workspace>,
 6619    ) {
 6620        self.panes.retain(|p| p != pane);
 6621        if let Some(focus_on) = focus_on {
 6622            focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
 6623        } else if self.active_pane() == pane {
 6624            let fallback_pane = self.panes.last().unwrap().clone();
 6625            if self.has_active_modal(window, cx) {
 6626                self.set_active_pane(&fallback_pane, window, cx);
 6627            } else {
 6628                fallback_pane.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
 6629            }
 6630        }
 6631        if self.last_active_center_pane == Some(pane.downgrade()) {
 6632            self.last_active_center_pane = None;
 6633        }
 6634        cx.notify();
 6635    }
 6636
 6637    fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 6638        if self._schedule_serialize_workspace.is_none() {
 6639            self._schedule_serialize_workspace =
 6640                Some(cx.spawn_in(window, async move |this, cx| {
 6641                    cx.background_executor()
 6642                        .timer(SERIALIZATION_THROTTLE_TIME)
 6643                        .await;
 6644                    this.update_in(cx, |this, window, cx| {
 6645                        this._serialize_workspace_task =
 6646                            Some(this.serialize_workspace_internal(window, cx));
 6647                        this._schedule_serialize_workspace.take();
 6648                    })
 6649                    .log_err();
 6650                }));
 6651        }
 6652    }
 6653
 6654    fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
 6655        let Some(database_id) = self.database_id() else {
 6656            return Task::ready(());
 6657        };
 6658
 6659        fn serialize_pane_handle(
 6660            pane_handle: &Entity<Pane>,
 6661            window: &mut Window,
 6662            cx: &mut App,
 6663        ) -> SerializedPane {
 6664            let (items, active, pinned_count) = {
 6665                let pane = pane_handle.read(cx);
 6666                let active_item_id = pane.active_item().map(|item| item.item_id());
 6667                (
 6668                    pane.items()
 6669                        .filter_map(|handle| {
 6670                            let handle = handle.to_serializable_item_handle(cx)?;
 6671
 6672                            Some(SerializedItem {
 6673                                kind: Arc::from(handle.serialized_item_kind()),
 6674                                item_id: handle.item_id().as_u64(),
 6675                                active: Some(handle.item_id()) == active_item_id,
 6676                                preview: pane.is_active_preview_item(handle.item_id()),
 6677                            })
 6678                        })
 6679                        .collect::<Vec<_>>(),
 6680                    pane.has_focus(window, cx),
 6681                    pane.pinned_count(),
 6682                )
 6683            };
 6684
 6685            SerializedPane::new(items, active, pinned_count)
 6686        }
 6687
 6688        fn build_serialized_pane_group(
 6689            pane_group: &Member,
 6690            window: &mut Window,
 6691            cx: &mut App,
 6692        ) -> SerializedPaneGroup {
 6693            match pane_group {
 6694                Member::Axis(PaneAxis {
 6695                    axis,
 6696                    members,
 6697                    flexes,
 6698                    bounding_boxes: _,
 6699                }) => SerializedPaneGroup::Group {
 6700                    axis: SerializedAxis(*axis),
 6701                    children: members
 6702                        .iter()
 6703                        .map(|member| build_serialized_pane_group(member, window, cx))
 6704                        .collect::<Vec<_>>(),
 6705                    flexes: Some(flexes.lock().clone()),
 6706                },
 6707                Member::Pane(pane_handle) => {
 6708                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
 6709                }
 6710            }
 6711        }
 6712
 6713        fn build_serialized_docks(
 6714            this: &Workspace,
 6715            window: &mut Window,
 6716            cx: &mut App,
 6717        ) -> DockStructure {
 6718            this.capture_dock_state(window, cx)
 6719        }
 6720
 6721        match self.workspace_location(cx) {
 6722            WorkspaceLocation::Location(location, paths) => {
 6723                let bookmarks = self.project.update(cx, |project, cx| {
 6724                    project
 6725                        .bookmark_store()
 6726                        .read(cx)
 6727                        .all_serialized_bookmarks(cx)
 6728                });
 6729
 6730                let breakpoints = self.project.update(cx, |project, cx| {
 6731                    project
 6732                        .breakpoint_store()
 6733                        .read(cx)
 6734                        .all_source_breakpoints(cx)
 6735                });
 6736                let user_toolchains = self
 6737                    .project
 6738                    .read(cx)
 6739                    .user_toolchains(cx)
 6740                    .unwrap_or_default();
 6741
 6742                let center_group = build_serialized_pane_group(&self.center.root, window, cx);
 6743                let docks = build_serialized_docks(self, window, cx);
 6744                let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
 6745
 6746                let serialized_workspace = SerializedWorkspace {
 6747                    id: database_id,
 6748                    location,
 6749                    paths,
 6750                    center_group,
 6751                    window_bounds,
 6752                    display: Default::default(),
 6753                    docks,
 6754                    centered_layout: self.centered_layout,
 6755                    session_id: self.session_id.clone(),
 6756                    bookmarks,
 6757                    breakpoints,
 6758                    window_id: Some(window.window_handle().window_id().as_u64()),
 6759                    user_toolchains,
 6760                };
 6761
 6762                let db = WorkspaceDb::global(cx);
 6763                window.spawn(cx, async move |_| {
 6764                    db.save_workspace(serialized_workspace).await;
 6765                })
 6766            }
 6767            WorkspaceLocation::DetachFromSession => {
 6768                let window_bounds = SerializedWindowBounds(window.window_bounds());
 6769                let display = window.display(cx).and_then(|d| d.uuid().ok());
 6770                // Save dock state for empty local workspaces
 6771                let docks = build_serialized_docks(self, window, cx);
 6772                let db = WorkspaceDb::global(cx);
 6773                let kvp = db::kvp::KeyValueStore::global(cx);
 6774                window.spawn(cx, async move |_| {
 6775                    db.set_window_open_status(
 6776                        database_id,
 6777                        window_bounds,
 6778                        display.unwrap_or_default(),
 6779                    )
 6780                    .await
 6781                    .log_err();
 6782                    db.set_session_id(database_id, None).await.log_err();
 6783                    persistence::write_default_dock_state(&kvp, docks)
 6784                        .await
 6785                        .log_err();
 6786                })
 6787            }
 6788            WorkspaceLocation::None => {
 6789                // Save dock state for empty non-local workspaces
 6790                let docks = build_serialized_docks(self, window, cx);
 6791                let kvp = db::kvp::KeyValueStore::global(cx);
 6792                window.spawn(cx, async move |_| {
 6793                    persistence::write_default_dock_state(&kvp, docks)
 6794                        .await
 6795                        .log_err();
 6796                })
 6797            }
 6798        }
 6799    }
 6800
 6801    fn has_any_items_open(&self, cx: &App) -> bool {
 6802        self.panes.iter().any(|pane| pane.read(cx).items_len() > 0)
 6803    }
 6804
 6805    fn workspace_location(&self, cx: &App) -> WorkspaceLocation {
 6806        let paths = PathList::new(&self.root_paths(cx));
 6807        if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
 6808            WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
 6809        } else if self.project.read(cx).is_local() {
 6810            if !paths.is_empty() || self.has_any_items_open(cx) {
 6811                WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
 6812            } else {
 6813                WorkspaceLocation::DetachFromSession
 6814            }
 6815        } else {
 6816            WorkspaceLocation::None
 6817        }
 6818    }
 6819
 6820    fn update_history(&self, cx: &mut App) {
 6821        let Some(id) = self.database_id() else {
 6822            return;
 6823        };
 6824        if !self.project.read(cx).is_local() {
 6825            return;
 6826        }
 6827        if let Some(manager) = HistoryManager::global(cx) {
 6828            let paths = PathList::new(&self.root_paths(cx));
 6829            manager.update(cx, |this, cx| {
 6830                this.update_history(id, HistoryManagerEntry::new(id, &paths), cx);
 6831            });
 6832        }
 6833    }
 6834
 6835    async fn serialize_items(
 6836        this: &WeakEntity<Self>,
 6837        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
 6838        cx: &mut AsyncWindowContext,
 6839    ) -> Result<()> {
 6840        const CHUNK_SIZE: usize = 200;
 6841
 6842        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
 6843
 6844        while let Some(items_received) = serializable_items.next().await {
 6845            let unique_items =
 6846                items_received
 6847                    .into_iter()
 6848                    .fold(HashMap::default(), |mut acc, item| {
 6849                        acc.entry(item.item_id()).or_insert(item);
 6850                        acc
 6851                    });
 6852
 6853            // We use into_iter() here so that the references to the items are moved into
 6854            // the tasks and not kept alive while we're sleeping.
 6855            for (_, item) in unique_items.into_iter() {
 6856                if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
 6857                    item.serialize(workspace, false, window, cx)
 6858                }) {
 6859                    cx.background_spawn(async move { task.await.log_err() })
 6860                        .detach();
 6861                }
 6862            }
 6863
 6864            cx.background_executor()
 6865                .timer(SERIALIZATION_THROTTLE_TIME)
 6866                .await;
 6867        }
 6868
 6869        Ok(())
 6870    }
 6871
 6872    pub(crate) fn enqueue_item_serialization(
 6873        &mut self,
 6874        item: Box<dyn SerializableItemHandle>,
 6875    ) -> Result<()> {
 6876        self.serializable_items_tx
 6877            .unbounded_send(item)
 6878            .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
 6879    }
 6880
 6881    pub(crate) fn load_workspace(
 6882        serialized_workspace: SerializedWorkspace,
 6883        paths_to_open: Vec<Option<ProjectPath>>,
 6884        window: &mut Window,
 6885        cx: &mut Context<Workspace>,
 6886    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
 6887        cx.spawn_in(window, async move |workspace, cx| {
 6888            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 6889
 6890            let mut center_group = None;
 6891            let mut center_items = None;
 6892
 6893            // Traverse the splits tree and add to things
 6894            if let Some((group, active_pane, items)) = serialized_workspace
 6895                .center_group
 6896                .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
 6897                .await
 6898            {
 6899                center_items = Some(items);
 6900                center_group = Some((group, active_pane))
 6901            }
 6902
 6903            let mut items_by_project_path = HashMap::default();
 6904            let mut item_ids_by_kind = HashMap::default();
 6905            let mut all_deserialized_items = Vec::default();
 6906            cx.update(|_, cx| {
 6907                for item in center_items.unwrap_or_default().into_iter().flatten() {
 6908                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
 6909                        item_ids_by_kind
 6910                            .entry(serializable_item_handle.serialized_item_kind())
 6911                            .or_insert(Vec::new())
 6912                            .push(item.item_id().as_u64() as ItemId);
 6913                    }
 6914
 6915                    if let Some(project_path) = item.project_path(cx) {
 6916                        items_by_project_path.insert(project_path, item.clone());
 6917                    }
 6918                    all_deserialized_items.push(item);
 6919                }
 6920            })?;
 6921
 6922            let opened_items = paths_to_open
 6923                .into_iter()
 6924                .map(|path_to_open| {
 6925                    path_to_open
 6926                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
 6927                })
 6928                .collect::<Vec<_>>();
 6929
 6930            // Remove old panes from workspace panes list
 6931            workspace.update_in(cx, |workspace, window, cx| {
 6932                if let Some((center_group, active_pane)) = center_group {
 6933                    workspace.remove_panes(workspace.center.root.clone(), window, cx);
 6934
 6935                    // Swap workspace center group
 6936                    workspace.center = PaneGroup::with_root(center_group);
 6937                    workspace.center.set_is_center(true);
 6938                    workspace.center.mark_positions(cx);
 6939
 6940                    if let Some(active_pane) = active_pane {
 6941                        workspace.set_active_pane(&active_pane, window, cx);
 6942                        cx.focus_self(window);
 6943                    } else {
 6944                        workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
 6945                    }
 6946                }
 6947
 6948                let docks = serialized_workspace.docks;
 6949
 6950                for (dock, serialized_dock) in [
 6951                    (&mut workspace.right_dock, docks.right),
 6952                    (&mut workspace.left_dock, docks.left),
 6953                    (&mut workspace.bottom_dock, docks.bottom),
 6954                ]
 6955                .iter_mut()
 6956                {
 6957                    dock.update(cx, |dock, cx| {
 6958                        dock.serialized_dock = Some(serialized_dock.clone());
 6959                        dock.restore_state(window, cx);
 6960                    });
 6961                }
 6962
 6963                cx.notify();
 6964            })?;
 6965
 6966            project
 6967                .update(cx, |project, cx| {
 6968                    project.bookmark_store().update(cx, |bookmark_store, cx| {
 6969                        bookmark_store.load_serialized_bookmarks(serialized_workspace.bookmarks, cx)
 6970                    })
 6971                })
 6972                .await
 6973                .log_err();
 6974
 6975            let _ = project
 6976                .update(cx, |project, cx| {
 6977                    project
 6978                        .breakpoint_store()
 6979                        .update(cx, |breakpoint_store, cx| {
 6980                            breakpoint_store
 6981                                .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
 6982                        })
 6983                })
 6984                .await;
 6985
 6986            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
 6987            // after loading the items, we might have different items and in order to avoid
 6988            // the database filling up, we delete items that haven't been loaded now.
 6989            //
 6990            // The items that have been loaded, have been saved after they've been added to the workspace.
 6991            let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
 6992                item_ids_by_kind
 6993                    .into_iter()
 6994                    .map(|(item_kind, loaded_items)| {
 6995                        SerializableItemRegistry::cleanup(
 6996                            item_kind,
 6997                            serialized_workspace.id,
 6998                            loaded_items,
 6999                            window,
 7000                            cx,
 7001                        )
 7002                        .log_err()
 7003                    })
 7004                    .collect::<Vec<_>>()
 7005            })?;
 7006
 7007            futures::future::join_all(clean_up_tasks).await;
 7008
 7009            workspace
 7010                .update_in(cx, |workspace, window, cx| {
 7011                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
 7012                    workspace.serialize_workspace_internal(window, cx).detach();
 7013
 7014                    // Ensure that we mark the window as edited if we did load dirty items
 7015                    workspace.update_window_edited(window, cx);
 7016                })
 7017                .ok();
 7018
 7019            Ok(opened_items)
 7020        })
 7021    }
 7022
 7023    pub fn key_context(&self, cx: &App) -> KeyContext {
 7024        let mut context = KeyContext::new_with_defaults();
 7025        context.add("Workspace");
 7026        context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
 7027        if let Some(status) = self
 7028            .debugger_provider
 7029            .as_ref()
 7030            .and_then(|provider| provider.active_thread_state(cx))
 7031        {
 7032            match status {
 7033                ThreadStatus::Running | ThreadStatus::Stepping => {
 7034                    context.add("debugger_running");
 7035                }
 7036                ThreadStatus::Stopped => context.add("debugger_stopped"),
 7037                ThreadStatus::Exited | ThreadStatus::Ended => {}
 7038            }
 7039        }
 7040
 7041        if self.left_dock.read(cx).is_open() {
 7042            if let Some(active_panel) = self.left_dock.read(cx).active_panel() {
 7043                context.set("left_dock", active_panel.panel_key());
 7044            }
 7045        }
 7046
 7047        if self.right_dock.read(cx).is_open() {
 7048            if let Some(active_panel) = self.right_dock.read(cx).active_panel() {
 7049                context.set("right_dock", active_panel.panel_key());
 7050            }
 7051        }
 7052
 7053        if self.bottom_dock.read(cx).is_open() {
 7054            if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() {
 7055                context.set("bottom_dock", active_panel.panel_key());
 7056            }
 7057        }
 7058
 7059        context
 7060    }
 7061
 7062    /// Multiworkspace uses this to add workspace action handling to itself
 7063    pub fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
 7064        self.add_workspace_actions_listeners(div, window, cx)
 7065            .on_action(cx.listener(
 7066                |_workspace, action_sequence: &settings::ActionSequence, window, cx| {
 7067                    for action in &action_sequence.0 {
 7068                        window.dispatch_action(action.boxed_clone(), cx);
 7069                    }
 7070                },
 7071            ))
 7072            .on_action(cx.listener(Self::close_inactive_items_and_panes))
 7073            .on_action(cx.listener(Self::close_all_items_and_panes))
 7074            .on_action(cx.listener(Self::close_item_in_all_panes))
 7075            .on_action(cx.listener(Self::save_all))
 7076            .on_action(cx.listener(Self::send_keystrokes))
 7077            .on_action(cx.listener(Self::add_folder_to_project))
 7078            .on_action(cx.listener(Self::follow_next_collaborator))
 7079            .on_action(cx.listener(Self::activate_pane_at_index))
 7080            .on_action(cx.listener(Self::move_item_to_pane_at_index))
 7081            .on_action(cx.listener(Self::move_focused_panel_to_next_position))
 7082            .on_action(cx.listener(Self::toggle_edit_predictions_all_files))
 7083            .on_action(cx.listener(Self::toggle_theme_mode))
 7084            .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
 7085                let pane = workspace.active_pane().clone();
 7086                workspace.unfollow_in_pane(&pane, window, cx);
 7087            }))
 7088            .on_action(cx.listener(|workspace, action: &Save, window, cx| {
 7089                workspace
 7090                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
 7091                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
 7092            }))
 7093            .on_action(cx.listener(|workspace, _: &FormatAndSave, window, cx| {
 7094                workspace
 7095                    .save_active_item(SaveIntent::FormatAndSave, window, cx)
 7096                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
 7097            }))
 7098            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
 7099                workspace
 7100                    .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
 7101                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
 7102            }))
 7103            .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
 7104                workspace
 7105                    .save_active_item(SaveIntent::SaveAs, window, cx)
 7106                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
 7107            }))
 7108            .on_action(
 7109                cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
 7110                    workspace.activate_previous_pane(window, cx)
 7111                }),
 7112            )
 7113            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
 7114                workspace.activate_next_pane(window, cx)
 7115            }))
 7116            .on_action(cx.listener(|workspace, _: &ActivateLastPane, window, cx| {
 7117                workspace.activate_last_pane(window, cx)
 7118            }))
 7119            .on_action(
 7120                cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
 7121                    workspace.activate_next_window(cx)
 7122                }),
 7123            )
 7124            .on_action(
 7125                cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
 7126                    workspace.activate_previous_window(cx)
 7127                }),
 7128            )
 7129            .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
 7130                workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
 7131            }))
 7132            .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
 7133                workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
 7134            }))
 7135            .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
 7136                workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
 7137            }))
 7138            .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
 7139                workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
 7140            }))
 7141            .on_action(cx.listener(
 7142                |workspace, action: &MoveItemToPaneInDirection, window, cx| {
 7143                    workspace.move_item_to_pane_in_direction(action, window, cx)
 7144                },
 7145            ))
 7146            .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
 7147                workspace.swap_pane_in_direction(SplitDirection::Left, cx)
 7148            }))
 7149            .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
 7150                workspace.swap_pane_in_direction(SplitDirection::Right, cx)
 7151            }))
 7152            .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
 7153                workspace.swap_pane_in_direction(SplitDirection::Up, cx)
 7154            }))
 7155            .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
 7156                workspace.swap_pane_in_direction(SplitDirection::Down, cx)
 7157            }))
 7158            .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| {
 7159                const DIRECTION_PRIORITY: [SplitDirection; 4] = [
 7160                    SplitDirection::Down,
 7161                    SplitDirection::Up,
 7162                    SplitDirection::Right,
 7163                    SplitDirection::Left,
 7164                ];
 7165                for dir in DIRECTION_PRIORITY {
 7166                    if workspace.find_pane_in_direction(dir, cx).is_some() {
 7167                        workspace.swap_pane_in_direction(dir, cx);
 7168                        workspace.activate_pane_in_direction(dir.opposite(), window, cx);
 7169                        break;
 7170                    }
 7171                }
 7172            }))
 7173            .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| {
 7174                workspace.move_pane_to_border(SplitDirection::Left, cx)
 7175            }))
 7176            .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| {
 7177                workspace.move_pane_to_border(SplitDirection::Right, cx)
 7178            }))
 7179            .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| {
 7180                workspace.move_pane_to_border(SplitDirection::Up, cx)
 7181            }))
 7182            .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| {
 7183                workspace.move_pane_to_border(SplitDirection::Down, cx)
 7184            }))
 7185            .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
 7186                this.toggle_dock(DockPosition::Left, window, cx);
 7187            }))
 7188            .on_action(cx.listener(
 7189                |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
 7190                    workspace.toggle_dock(DockPosition::Right, window, cx);
 7191                },
 7192            ))
 7193            .on_action(cx.listener(
 7194                |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
 7195                    workspace.toggle_dock(DockPosition::Bottom, window, cx);
 7196                },
 7197            ))
 7198            .on_action(cx.listener(
 7199                |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
 7200                    if !workspace.close_active_dock(window, cx) {
 7201                        cx.propagate();
 7202                    }
 7203                },
 7204            ))
 7205            .on_action(
 7206                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
 7207                    workspace.close_all_docks(window, cx);
 7208                }),
 7209            )
 7210            .on_action(cx.listener(Self::toggle_all_docks))
 7211            .on_action(cx.listener(
 7212                |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
 7213                    workspace.clear_all_notifications(cx);
 7214                },
 7215            ))
 7216            .on_action(cx.listener(
 7217                |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| {
 7218                    workspace.clear_navigation_history(window, cx);
 7219                },
 7220            ))
 7221            .on_action(cx.listener(
 7222                |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
 7223                    if let Some((notification_id, _)) = workspace.notifications.pop() {
 7224                        workspace.suppress_notification(&notification_id, cx);
 7225                    }
 7226                },
 7227            ))
 7228            .on_action(cx.listener(
 7229                |workspace: &mut Workspace, _: &ToggleWorktreeSecurity, window, cx| {
 7230                    workspace.show_worktree_trust_security_modal(true, window, cx);
 7231                },
 7232            ))
 7233            .on_action(
 7234                cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
 7235                    if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
 7236                        trusted_worktrees.update(cx, |trusted_worktrees, _| {
 7237                            trusted_worktrees.clear_trusted_paths()
 7238                        });
 7239                        let db = WorkspaceDb::global(cx);
 7240                        cx.spawn(async move |_, cx| {
 7241                            if db.clear_trusted_worktrees().await.log_err().is_some() {
 7242                                cx.update(|cx| reload(cx));
 7243                            }
 7244                        })
 7245                        .detach();
 7246                    }
 7247                }),
 7248            )
 7249            .on_action(cx.listener(
 7250                |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
 7251                    workspace.reopen_closed_item(window, cx).detach();
 7252                },
 7253            ))
 7254            .on_action(cx.listener(
 7255                |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| {
 7256                    for dock in workspace.all_docks() {
 7257                        if dock.focus_handle(cx).contains_focused(window, cx) {
 7258                            let panel = dock.read(cx).active_panel().cloned();
 7259                            if let Some(panel) = panel {
 7260                                dock.update(cx, |dock, cx| {
 7261                                    dock.set_panel_size_state(
 7262                                        panel.as_ref(),
 7263                                        dock::PanelSizeState::default(),
 7264                                        cx,
 7265                                    );
 7266                                });
 7267                            }
 7268                            return;
 7269                        }
 7270                    }
 7271                },
 7272            ))
 7273            .on_action(cx.listener(
 7274                |workspace: &mut Workspace, _: &ResetOpenDocksSize, _window, cx| {
 7275                    for dock in workspace.all_docks() {
 7276                        let panel = dock.read(cx).visible_panel().cloned();
 7277                        if let Some(panel) = panel {
 7278                            dock.update(cx, |dock, cx| {
 7279                                dock.set_panel_size_state(
 7280                                    panel.as_ref(),
 7281                                    dock::PanelSizeState::default(),
 7282                                    cx,
 7283                                );
 7284                            });
 7285                        }
 7286                    }
 7287                },
 7288            ))
 7289            .on_action(cx.listener(
 7290                |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| {
 7291                    adjust_active_dock_size_by_px(
 7292                        px_with_ui_font_fallback(act.px, cx),
 7293                        workspace,
 7294                        window,
 7295                        cx,
 7296                    );
 7297                },
 7298            ))
 7299            .on_action(cx.listener(
 7300                |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| {
 7301                    adjust_active_dock_size_by_px(
 7302                        px_with_ui_font_fallback(act.px, cx) * -1.,
 7303                        workspace,
 7304                        window,
 7305                        cx,
 7306                    );
 7307                },
 7308            ))
 7309            .on_action(cx.listener(
 7310                |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| {
 7311                    adjust_open_docks_size_by_px(
 7312                        px_with_ui_font_fallback(act.px, cx),
 7313                        workspace,
 7314                        window,
 7315                        cx,
 7316                    );
 7317                },
 7318            ))
 7319            .on_action(cx.listener(
 7320                |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| {
 7321                    adjust_open_docks_size_by_px(
 7322                        px_with_ui_font_fallback(act.px, cx) * -1.,
 7323                        workspace,
 7324                        window,
 7325                        cx,
 7326                    );
 7327                },
 7328            ))
 7329            .on_action(cx.listener(Workspace::toggle_centered_layout))
 7330            .on_action(cx.listener(
 7331                |workspace: &mut Workspace, action: &pane::ActivateNextItem, window, cx| {
 7332                    if let Some(active_dock) = workspace.active_dock(window, cx) {
 7333                        let dock = active_dock.read(cx);
 7334                        if let Some(active_panel) = dock.active_panel() {
 7335                            if active_panel.pane(cx).is_none() {
 7336                                let mut recent_pane: Option<Entity<Pane>> = None;
 7337                                let mut recent_timestamp = 0;
 7338                                for pane_handle in workspace.panes() {
 7339                                    let pane = pane_handle.read(cx);
 7340                                    for entry in pane.activation_history() {
 7341                                        if entry.timestamp > recent_timestamp {
 7342                                            recent_timestamp = entry.timestamp;
 7343                                            recent_pane = Some(pane_handle.clone());
 7344                                        }
 7345                                    }
 7346                                }
 7347
 7348                                if let Some(pane) = recent_pane {
 7349                                    let wrap_around = action.wrap_around;
 7350                                    pane.update(cx, |pane, cx| {
 7351                                        let current_index = pane.active_item_index();
 7352                                        let items_len = pane.items_len();
 7353                                        if items_len > 0 {
 7354                                            let next_index = if current_index + 1 < items_len {
 7355                                                current_index + 1
 7356                                            } else if wrap_around {
 7357                                                0
 7358                                            } else {
 7359                                                return;
 7360                                            };
 7361                                            pane.activate_item(
 7362                                                next_index, false, false, window, cx,
 7363                                            );
 7364                                        }
 7365                                    });
 7366                                    return;
 7367                                }
 7368                            }
 7369                        }
 7370                    }
 7371                    cx.propagate();
 7372                },
 7373            ))
 7374            .on_action(cx.listener(
 7375                |workspace: &mut Workspace, action: &pane::ActivatePreviousItem, window, cx| {
 7376                    if let Some(active_dock) = workspace.active_dock(window, cx) {
 7377                        let dock = active_dock.read(cx);
 7378                        if let Some(active_panel) = dock.active_panel() {
 7379                            if active_panel.pane(cx).is_none() {
 7380                                let mut recent_pane: Option<Entity<Pane>> = None;
 7381                                let mut recent_timestamp = 0;
 7382                                for pane_handle in workspace.panes() {
 7383                                    let pane = pane_handle.read(cx);
 7384                                    for entry in pane.activation_history() {
 7385                                        if entry.timestamp > recent_timestamp {
 7386                                            recent_timestamp = entry.timestamp;
 7387                                            recent_pane = Some(pane_handle.clone());
 7388                                        }
 7389                                    }
 7390                                }
 7391
 7392                                if let Some(pane) = recent_pane {
 7393                                    let wrap_around = action.wrap_around;
 7394                                    pane.update(cx, |pane, cx| {
 7395                                        let current_index = pane.active_item_index();
 7396                                        let items_len = pane.items_len();
 7397                                        if items_len > 0 {
 7398                                            let prev_index = if current_index > 0 {
 7399                                                current_index - 1
 7400                                            } else if wrap_around {
 7401                                                items_len.saturating_sub(1)
 7402                                            } else {
 7403                                                return;
 7404                                            };
 7405                                            pane.activate_item(
 7406                                                prev_index, false, false, window, cx,
 7407                                            );
 7408                                        }
 7409                                    });
 7410                                    return;
 7411                                }
 7412                            }
 7413                        }
 7414                    }
 7415                    cx.propagate();
 7416                },
 7417            ))
 7418            .on_action(cx.listener(
 7419                |workspace: &mut Workspace, action: &pane::CloseActiveItem, window, cx| {
 7420                    if let Some(active_dock) = workspace.active_dock(window, cx) {
 7421                        let dock = active_dock.read(cx);
 7422                        if let Some(active_panel) = dock.active_panel() {
 7423                            if active_panel.pane(cx).is_none() {
 7424                                let active_pane = workspace.active_pane().clone();
 7425                                active_pane.update(cx, |pane, cx| {
 7426                                    pane.close_active_item(action, window, cx)
 7427                                        .detach_and_log_err(cx);
 7428                                });
 7429                                return;
 7430                            }
 7431                        }
 7432                    }
 7433                    cx.propagate();
 7434                },
 7435            ))
 7436            .on_action(
 7437                cx.listener(|workspace, _: &ToggleReadOnlyFile, window, cx| {
 7438                    let pane = workspace.active_pane().clone();
 7439                    if let Some(item) = pane.read(cx).active_item() {
 7440                        item.toggle_read_only(window, cx);
 7441                    }
 7442                }),
 7443            )
 7444            .on_action(cx.listener(|workspace, _: &FocusCenterPane, window, cx| {
 7445                workspace.focus_center_pane(window, cx);
 7446            }))
 7447            .on_action(cx.listener(Workspace::clear_bookmarks))
 7448            .on_action(cx.listener(Workspace::cancel))
 7449    }
 7450
 7451    #[cfg(any(test, feature = "test-support"))]
 7452    pub fn set_random_database_id(&mut self) {
 7453        self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64));
 7454    }
 7455
 7456    #[cfg(any(test, feature = "test-support"))]
 7457    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
 7458        use node_runtime::NodeRuntime;
 7459        use session::Session;
 7460
 7461        let client = project.read(cx).client();
 7462        let user_store = project.read(cx).user_store();
 7463        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
 7464        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
 7465        window.activate_window();
 7466        let app_state = Arc::new(AppState {
 7467            languages: project.read(cx).languages().clone(),
 7468            workspace_store,
 7469            client,
 7470            user_store,
 7471            fs: project.read(cx).fs().clone(),
 7472            build_window_options: |_, _| Default::default(),
 7473            node_runtime: NodeRuntime::unavailable(),
 7474            session,
 7475        });
 7476        let workspace = Self::new(Default::default(), project, app_state, window, cx);
 7477        workspace
 7478            .active_pane
 7479            .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
 7480        workspace
 7481    }
 7482
 7483    pub fn register_action<A: Action>(
 7484        &mut self,
 7485        callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
 7486    ) -> &mut Self {
 7487        let callback = Arc::new(callback);
 7488
 7489        self.workspace_actions.push(Box::new(move |div, _, _, cx| {
 7490            let callback = callback.clone();
 7491            div.on_action(cx.listener(move |workspace, event, window, cx| {
 7492                (callback)(workspace, event, window, cx)
 7493            }))
 7494        }));
 7495        self
 7496    }
 7497    pub fn register_action_renderer(
 7498        &mut self,
 7499        callback: impl Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div + 'static,
 7500    ) -> &mut Self {
 7501        self.workspace_actions.push(Box::new(callback));
 7502        self
 7503    }
 7504
 7505    fn add_workspace_actions_listeners(
 7506        &self,
 7507        mut div: Div,
 7508        window: &mut Window,
 7509        cx: &mut Context<Self>,
 7510    ) -> Div {
 7511        for action in self.workspace_actions.iter() {
 7512            div = (action)(div, self, window, cx)
 7513        }
 7514        div
 7515    }
 7516
 7517    pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
 7518        self.modal_layer.read(cx).has_active_modal()
 7519    }
 7520
 7521    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
 7522        self.modal_layer.read(cx).active_modal()
 7523    }
 7524
 7525    /// Toggles a modal of type `V`. If a modal of the same type is currently active,
 7526    /// it will be hidden. If a different modal is active, it will be replaced with the new one.
 7527    /// If no modal is active, the new modal will be shown.
 7528    ///
 7529    /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
 7530    /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
 7531    /// will not be shown.
 7532    pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
 7533    where
 7534        B: FnOnce(&mut Window, &mut Context<V>) -> V,
 7535    {
 7536        self.modal_layer.update(cx, |modal_layer, cx| {
 7537            modal_layer.toggle_modal(window, cx, build)
 7538        })
 7539    }
 7540
 7541    pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool {
 7542        self.modal_layer
 7543            .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx))
 7544    }
 7545
 7546    pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
 7547        self.toast_layer
 7548            .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
 7549    }
 7550
 7551    pub fn toggle_centered_layout(
 7552        &mut self,
 7553        _: &ToggleCenteredLayout,
 7554        _: &mut Window,
 7555        cx: &mut Context<Self>,
 7556    ) {
 7557        self.centered_layout = !self.centered_layout;
 7558        if let Some(database_id) = self.database_id() {
 7559            let db = WorkspaceDb::global(cx);
 7560            let centered_layout = self.centered_layout;
 7561            cx.background_spawn(async move {
 7562                db.set_centered_layout(database_id, centered_layout).await
 7563            })
 7564            .detach_and_log_err(cx);
 7565        }
 7566        cx.notify();
 7567    }
 7568
 7569    pub fn clear_bookmarks(&mut self, _: &ClearBookmarks, _: &mut Window, cx: &mut Context<Self>) {
 7570        self.project()
 7571            .read(cx)
 7572            .bookmark_store()
 7573            .update(cx, |bookmark_store, cx| {
 7574                bookmark_store.clear_bookmarks(cx);
 7575            });
 7576    }
 7577
 7578    fn adjust_padding(padding: Option<f32>) -> f32 {
 7579        padding
 7580            .unwrap_or(CenteredPaddingSettings::default().0)
 7581            .clamp(
 7582                CenteredPaddingSettings::MIN_PADDING,
 7583                CenteredPaddingSettings::MAX_PADDING,
 7584            )
 7585    }
 7586
 7587    fn render_dock(
 7588        &self,
 7589        position: DockPosition,
 7590        dock: &Entity<Dock>,
 7591        window: &mut Window,
 7592        cx: &mut App,
 7593    ) -> Option<Div> {
 7594        if self.zoomed_position == Some(position) {
 7595            return None;
 7596        }
 7597
 7598        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
 7599            let pane = panel.pane(cx)?;
 7600            let follower_states = &self.follower_states;
 7601            leader_border_for_pane(follower_states, &pane, window, cx)
 7602        });
 7603
 7604        let mut container = div()
 7605            .flex()
 7606            .overflow_hidden()
 7607            .flex_none()
 7608            .child(dock.clone())
 7609            .children(leader_border);
 7610
 7611        // Apply sizing only when the dock is open. When closed the dock is still
 7612        // included in the element tree so its focus handle remains mounted — without
 7613        // this, toggle_panel_focus cannot focus the panel when the dock is closed.
 7614        let dock = dock.read(cx);
 7615        if let Some(panel) = dock.visible_panel() {
 7616            let size_state = dock.stored_panel_size_state(panel.as_ref());
 7617            let min_size = panel.min_size(window, cx);
 7618            if position.axis() == Axis::Horizontal {
 7619                let use_flexible = panel.has_flexible_size(window, cx);
 7620                let flex_grow = if use_flexible {
 7621                    size_state
 7622                        .and_then(|state| state.flex)
 7623                        .or_else(|| self.default_dock_flex(position))
 7624                } else {
 7625                    None
 7626                };
 7627                if let Some(grow) = flex_grow {
 7628                    let grow = (grow / self.center_full_height_column_count()).max(0.001);
 7629                    let style = container.style();
 7630                    style.flex_grow = Some(grow);
 7631                    style.flex_shrink = Some(1.0);
 7632                    style.flex_basis = Some(relative(0.).into());
 7633                } else {
 7634                    let size = size_state
 7635                        .and_then(|state| state.size)
 7636                        .unwrap_or_else(|| panel.default_size(window, cx));
 7637                    container = container.w(size);
 7638                }
 7639                if let Some(min) = min_size {
 7640                    container = container.min_w(min);
 7641                }
 7642            } else {
 7643                let size = size_state
 7644                    .and_then(|state| state.size)
 7645                    .unwrap_or_else(|| panel.default_size(window, cx));
 7646                container = container.h(size);
 7647            }
 7648        }
 7649
 7650        Some(container)
 7651    }
 7652
 7653    pub fn for_window(window: &Window, cx: &App) -> Option<Entity<Workspace>> {
 7654        window
 7655            .root::<MultiWorkspace>()
 7656            .flatten()
 7657            .map(|multi_workspace| multi_workspace.read(cx).workspace().clone())
 7658    }
 7659
 7660    pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
 7661        self.zoomed.as_ref()
 7662    }
 7663
 7664    pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
 7665        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
 7666            return;
 7667        };
 7668        let windows = cx.windows();
 7669        let next_window =
 7670            SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else(
 7671                || {
 7672                    windows
 7673                        .iter()
 7674                        .cycle()
 7675                        .skip_while(|window| window.window_id() != current_window_id)
 7676                        .nth(1)
 7677                },
 7678            );
 7679
 7680        if let Some(window) = next_window {
 7681            window
 7682                .update(cx, |_, window, _| window.activate_window())
 7683                .ok();
 7684        }
 7685    }
 7686
 7687    pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
 7688        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
 7689            return;
 7690        };
 7691        let windows = cx.windows();
 7692        let prev_window =
 7693            SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else(
 7694                || {
 7695                    windows
 7696                        .iter()
 7697                        .rev()
 7698                        .cycle()
 7699                        .skip_while(|window| window.window_id() != current_window_id)
 7700                        .nth(1)
 7701                },
 7702            );
 7703
 7704        if let Some(window) = prev_window {
 7705            window
 7706                .update(cx, |_, window, _| window.activate_window())
 7707                .ok();
 7708        }
 7709    }
 7710
 7711    pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 7712        if cx.stop_active_drag(window) {
 7713        } else if let Some((notification_id, _)) = self.notifications.pop() {
 7714            dismiss_app_notification(&notification_id, cx);
 7715        } else {
 7716            cx.propagate();
 7717        }
 7718    }
 7719
 7720    fn resize_dock(
 7721        &mut self,
 7722        dock_pos: DockPosition,
 7723        new_size: Pixels,
 7724        window: &mut Window,
 7725        cx: &mut Context<Self>,
 7726    ) {
 7727        match dock_pos {
 7728            DockPosition::Left => self.resize_left_dock(new_size, window, cx),
 7729            DockPosition::Right => self.resize_right_dock(new_size, window, cx),
 7730            DockPosition::Bottom => self.resize_bottom_dock(new_size, window, cx),
 7731        }
 7732    }
 7733
 7734    fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
 7735        let workspace_width = self.bounds.size.width;
 7736        let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
 7737
 7738        self.right_dock.read_with(cx, |right_dock, cx| {
 7739            let right_dock_size = right_dock
 7740                .stored_active_panel_size(window, cx)
 7741                .unwrap_or(Pixels::ZERO);
 7742            if right_dock_size + size > workspace_width {
 7743                size = workspace_width - right_dock_size
 7744            }
 7745        });
 7746
 7747        let flex_grow = self.dock_flex_for_size(DockPosition::Left, size, window, cx);
 7748        self.left_dock.update(cx, |left_dock, cx| {
 7749            if WorkspaceSettings::get_global(cx)
 7750                .resize_all_panels_in_dock
 7751                .contains(&DockPosition::Left)
 7752            {
 7753                left_dock.resize_all_panels(Some(size), flex_grow, window, cx);
 7754            } else {
 7755                left_dock.resize_active_panel(Some(size), flex_grow, window, cx);
 7756            }
 7757        });
 7758    }
 7759
 7760    fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
 7761        let workspace_width = self.bounds.size.width;
 7762        let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
 7763        self.left_dock.read_with(cx, |left_dock, cx| {
 7764            let left_dock_size = left_dock
 7765                .stored_active_panel_size(window, cx)
 7766                .unwrap_or(Pixels::ZERO);
 7767            if left_dock_size + size > workspace_width {
 7768                size = workspace_width - left_dock_size
 7769            }
 7770        });
 7771        let flex_grow = self.dock_flex_for_size(DockPosition::Right, size, window, cx);
 7772        self.right_dock.update(cx, |right_dock, cx| {
 7773            if WorkspaceSettings::get_global(cx)
 7774                .resize_all_panels_in_dock
 7775                .contains(&DockPosition::Right)
 7776            {
 7777                right_dock.resize_all_panels(Some(size), flex_grow, window, cx);
 7778            } else {
 7779                right_dock.resize_active_panel(Some(size), flex_grow, window, cx);
 7780            }
 7781        });
 7782    }
 7783
 7784    fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
 7785        let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top());
 7786        self.bottom_dock.update(cx, |bottom_dock, cx| {
 7787            if WorkspaceSettings::get_global(cx)
 7788                .resize_all_panels_in_dock
 7789                .contains(&DockPosition::Bottom)
 7790            {
 7791                bottom_dock.resize_all_panels(Some(size), None, window, cx);
 7792            } else {
 7793                bottom_dock.resize_active_panel(Some(size), None, window, cx);
 7794            }
 7795        });
 7796    }
 7797
 7798    fn toggle_edit_predictions_all_files(
 7799        &mut self,
 7800        _: &ToggleEditPrediction,
 7801        _window: &mut Window,
 7802        cx: &mut Context<Self>,
 7803    ) {
 7804        let fs = self.project().read(cx).fs().clone();
 7805        let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx);
 7806        update_settings_file(fs, cx, move |file, _| {
 7807            file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
 7808        });
 7809    }
 7810
 7811    fn toggle_theme_mode(&mut self, _: &ToggleMode, _window: &mut Window, cx: &mut Context<Self>) {
 7812        let current_mode = ThemeSettings::get_global(cx).theme.mode();
 7813        let next_mode = match current_mode {
 7814            Some(theme_settings::ThemeAppearanceMode::Light) => {
 7815                theme_settings::ThemeAppearanceMode::Dark
 7816            }
 7817            Some(theme_settings::ThemeAppearanceMode::Dark) => {
 7818                theme_settings::ThemeAppearanceMode::Light
 7819            }
 7820            Some(theme_settings::ThemeAppearanceMode::System) | None => {
 7821                match cx.theme().appearance() {
 7822                    theme::Appearance::Light => theme_settings::ThemeAppearanceMode::Dark,
 7823                    theme::Appearance::Dark => theme_settings::ThemeAppearanceMode::Light,
 7824                }
 7825            }
 7826        };
 7827
 7828        let fs = self.project().read(cx).fs().clone();
 7829        settings::update_settings_file(fs, cx, move |settings, _cx| {
 7830            theme_settings::set_mode(settings, next_mode);
 7831        });
 7832    }
 7833
 7834    pub fn show_worktree_trust_security_modal(
 7835        &mut self,
 7836        toggle: bool,
 7837        window: &mut Window,
 7838        cx: &mut Context<Self>,
 7839    ) {
 7840        if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
 7841            if toggle {
 7842                security_modal.update(cx, |security_modal, cx| {
 7843                    security_modal.dismiss(cx);
 7844                })
 7845            } else {
 7846                security_modal.update(cx, |security_modal, cx| {
 7847                    security_modal.refresh_restricted_paths(cx);
 7848                });
 7849            }
 7850        } else {
 7851            let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
 7852                .map(|trusted_worktrees| {
 7853                    trusted_worktrees
 7854                        .read(cx)
 7855                        .has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
 7856                })
 7857                .unwrap_or(false);
 7858            if has_restricted_worktrees {
 7859                let project = self.project().read(cx);
 7860                let remote_host = project
 7861                    .remote_connection_options(cx)
 7862                    .map(RemoteHostLocation::from);
 7863                let worktree_store = project.worktree_store().downgrade();
 7864                self.toggle_modal(window, cx, |_, cx| {
 7865                    SecurityModal::new(worktree_store, remote_host, cx)
 7866                });
 7867            }
 7868        }
 7869    }
 7870}
 7871
 7872pub trait AnyActiveCall {
 7873    fn entity(&self) -> AnyEntity;
 7874    fn is_in_room(&self, _: &App) -> bool;
 7875    fn room_id(&self, _: &App) -> Option<u64>;
 7876    fn channel_id(&self, _: &App) -> Option<ChannelId>;
 7877    fn hang_up(&self, _: &mut App) -> Task<Result<()>>;
 7878    fn unshare_project(&self, _: Entity<Project>, _: &mut App) -> Result<()>;
 7879    fn remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option<RemoteCollaborator>;
 7880    fn is_sharing_project(&self, _: &App) -> bool;
 7881    fn has_remote_participants(&self, _: &App) -> bool;
 7882    fn local_participant_is_guest(&self, _: &App) -> bool;
 7883    fn client(&self, _: &App) -> Arc<Client>;
 7884    fn share_on_join(&self, _: &App) -> bool;
 7885    fn join_channel(&self, _: ChannelId, _: &mut App) -> Task<Result<bool>>;
 7886    fn room_update_completed(&self, _: &mut App) -> Task<()>;
 7887    fn most_active_project(&self, _: &App) -> Option<(u64, u64)>;
 7888    fn share_project(&self, _: Entity<Project>, _: &mut App) -> Task<Result<u64>>;
 7889    fn join_project(
 7890        &self,
 7891        _: u64,
 7892        _: Arc<LanguageRegistry>,
 7893        _: Arc<dyn Fs>,
 7894        _: &mut App,
 7895    ) -> Task<Result<Entity<Project>>>;
 7896    fn peer_id_for_user_in_room(&self, _: u64, _: &App) -> Option<PeerId>;
 7897    fn subscribe(
 7898        &self,
 7899        _: &mut Window,
 7900        _: &mut Context<Workspace>,
 7901        _: Box<dyn Fn(&mut Workspace, &ActiveCallEvent, &mut Window, &mut Context<Workspace>)>,
 7902    ) -> Subscription;
 7903    fn create_shared_screen(
 7904        &self,
 7905        _: PeerId,
 7906        _: &Entity<Pane>,
 7907        _: &mut Window,
 7908        _: &mut App,
 7909    ) -> Option<Entity<SharedScreen>>;
 7910}
 7911
 7912#[derive(Clone)]
 7913pub struct GlobalAnyActiveCall(pub Arc<dyn AnyActiveCall>);
 7914impl Global for GlobalAnyActiveCall {}
 7915
 7916impl GlobalAnyActiveCall {
 7917    pub(crate) fn try_global(cx: &App) -> Option<&Self> {
 7918        cx.try_global()
 7919    }
 7920
 7921    pub(crate) fn global(cx: &App) -> &Self {
 7922        cx.global()
 7923    }
 7924}
 7925
 7926/// Workspace-local view of a remote participant's location.
 7927#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 7928pub enum ParticipantLocation {
 7929    SharedProject { project_id: u64 },
 7930    UnsharedProject,
 7931    External,
 7932}
 7933
 7934impl ParticipantLocation {
 7935    pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
 7936        match location
 7937            .and_then(|l| l.variant)
 7938            .context("participant location was not provided")?
 7939        {
 7940            proto::participant_location::Variant::SharedProject(project) => {
 7941                Ok(Self::SharedProject {
 7942                    project_id: project.id,
 7943                })
 7944            }
 7945            proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject),
 7946            proto::participant_location::Variant::External(_) => Ok(Self::External),
 7947        }
 7948    }
 7949}
 7950/// Workspace-local view of a remote collaborator's state.
 7951/// This is the subset of `call::RemoteParticipant` that workspace needs.
 7952#[derive(Clone)]
 7953pub struct RemoteCollaborator {
 7954    pub user: Arc<User>,
 7955    pub peer_id: PeerId,
 7956    pub location: ParticipantLocation,
 7957    pub participant_index: ParticipantIndex,
 7958}
 7959
 7960pub enum ActiveCallEvent {
 7961    ParticipantLocationChanged { participant_id: PeerId },
 7962    RemoteVideoTracksChanged { participant_id: PeerId },
 7963}
 7964
 7965fn leader_border_for_pane(
 7966    follower_states: &HashMap<CollaboratorId, FollowerState>,
 7967    pane: &Entity<Pane>,
 7968    _: &Window,
 7969    cx: &App,
 7970) -> Option<Div> {
 7971    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
 7972        if state.pane() == pane {
 7973            Some((*leader_id, state))
 7974        } else {
 7975            None
 7976        }
 7977    })?;
 7978
 7979    let mut leader_color = match leader_id {
 7980        CollaboratorId::PeerId(leader_peer_id) => {
 7981            let leader = GlobalAnyActiveCall::try_global(cx)?
 7982                .0
 7983                .remote_participant_for_peer_id(leader_peer_id, cx)?;
 7984
 7985            cx.theme()
 7986                .players()
 7987                .color_for_participant(leader.participant_index.0)
 7988                .cursor
 7989        }
 7990        CollaboratorId::Agent => cx.theme().players().agent().cursor,
 7991    };
 7992    leader_color.fade_out(0.3);
 7993    Some(
 7994        div()
 7995            .absolute()
 7996            .size_full()
 7997            .left_0()
 7998            .top_0()
 7999            .border_2()
 8000            .border_color(leader_color),
 8001    )
 8002}
 8003
 8004fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
 8005    ZED_WINDOW_POSITION
 8006        .zip(*ZED_WINDOW_SIZE)
 8007        .map(|(position, size)| Bounds {
 8008            origin: position,
 8009            size,
 8010        })
 8011}
 8012
 8013fn open_items(
 8014    serialized_workspace: Option<SerializedWorkspace>,
 8015    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
 8016    window: &mut Window,
 8017    cx: &mut Context<Workspace>,
 8018) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
 8019    let restored_items = serialized_workspace.map(|serialized_workspace| {
 8020        Workspace::load_workspace(
 8021            serialized_workspace,
 8022            project_paths_to_open
 8023                .iter()
 8024                .map(|(_, project_path)| project_path)
 8025                .cloned()
 8026                .collect(),
 8027            window,
 8028            cx,
 8029        )
 8030    });
 8031
 8032    cx.spawn_in(window, async move |workspace, cx| {
 8033        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
 8034
 8035        if let Some(restored_items) = restored_items {
 8036            let restored_items = restored_items.await?;
 8037
 8038            let restored_project_paths = restored_items
 8039                .iter()
 8040                .filter_map(|item| {
 8041                    cx.update(|_, cx| item.as_ref()?.project_path(cx))
 8042                        .ok()
 8043                        .flatten()
 8044                })
 8045                .collect::<HashSet<_>>();
 8046
 8047            for restored_item in restored_items {
 8048                opened_items.push(restored_item.map(Ok));
 8049            }
 8050
 8051            project_paths_to_open
 8052                .iter_mut()
 8053                .for_each(|(_, project_path)| {
 8054                    if let Some(project_path_to_open) = project_path
 8055                        && restored_project_paths.contains(project_path_to_open)
 8056                    {
 8057                        *project_path = None;
 8058                    }
 8059                });
 8060        } else {
 8061            for _ in 0..project_paths_to_open.len() {
 8062                opened_items.push(None);
 8063            }
 8064        }
 8065        assert!(opened_items.len() == project_paths_to_open.len());
 8066
 8067        let tasks =
 8068            project_paths_to_open
 8069                .into_iter()
 8070                .enumerate()
 8071                .map(|(ix, (abs_path, project_path))| {
 8072                    let workspace = workspace.clone();
 8073                    cx.spawn(async move |cx| {
 8074                        let file_project_path = project_path?;
 8075                        let abs_path_task = workspace.update(cx, |workspace, cx| {
 8076                            workspace.project().update(cx, |project, cx| {
 8077                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
 8078                            })
 8079                        });
 8080
 8081                        // We only want to open file paths here. If one of the items
 8082                        // here is a directory, it was already opened further above
 8083                        // with a `find_or_create_worktree`.
 8084                        if let Ok(task) = abs_path_task
 8085                            && task.await.is_none_or(|p| p.is_file())
 8086                        {
 8087                            return Some((
 8088                                ix,
 8089                                workspace
 8090                                    .update_in(cx, |workspace, window, cx| {
 8091                                        workspace.open_path(
 8092                                            file_project_path,
 8093                                            None,
 8094                                            true,
 8095                                            window,
 8096                                            cx,
 8097                                        )
 8098                                    })
 8099                                    .log_err()?
 8100                                    .await,
 8101                            ));
 8102                        }
 8103                        None
 8104                    })
 8105                });
 8106
 8107        let tasks = tasks.collect::<Vec<_>>();
 8108
 8109        let tasks = futures::future::join_all(tasks);
 8110        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
 8111            opened_items[ix] = Some(path_open_result);
 8112        }
 8113
 8114        Ok(opened_items)
 8115    })
 8116}
 8117
 8118#[derive(Clone)]
 8119enum ActivateInDirectionTarget {
 8120    Pane(Entity<Pane>),
 8121    Dock(Entity<Dock>),
 8122    Sidebar(FocusHandle),
 8123}
 8124
 8125fn notify_if_database_failed(window: WindowHandle<MultiWorkspace>, cx: &mut AsyncApp) {
 8126    window
 8127        .update(cx, |multi_workspace, _, cx| {
 8128            let workspace = multi_workspace.workspace().clone();
 8129            workspace.update(cx, |workspace, cx| {
 8130                if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
 8131                    struct DatabaseFailedNotification;
 8132
 8133                    workspace.show_notification(
 8134                        NotificationId::unique::<DatabaseFailedNotification>(),
 8135                        cx,
 8136                        |cx| {
 8137                            cx.new(|cx| {
 8138                                MessageNotification::new("Failed to load the database file.", cx)
 8139                                    .primary_message("File an Issue")
 8140                                    .primary_icon(IconName::Plus)
 8141                                    .primary_on_click(|window, cx| {
 8142                                        window.dispatch_action(Box::new(FileBugReport), cx)
 8143                                    })
 8144                            })
 8145                        },
 8146                    );
 8147                }
 8148            });
 8149        })
 8150        .log_err();
 8151}
 8152
 8153fn px_with_ui_font_fallback(val: u32, cx: &Context<Workspace>) -> Pixels {
 8154    if val == 0 {
 8155        ThemeSettings::get_global(cx).ui_font_size(cx)
 8156    } else {
 8157        px(val as f32)
 8158    }
 8159}
 8160
 8161fn adjust_active_dock_size_by_px(
 8162    px: Pixels,
 8163    workspace: &mut Workspace,
 8164    window: &mut Window,
 8165    cx: &mut Context<Workspace>,
 8166) {
 8167    let Some(active_dock) = workspace
 8168        .all_docks()
 8169        .into_iter()
 8170        .find(|dock| dock.focus_handle(cx).contains_focused(window, cx))
 8171    else {
 8172        return;
 8173    };
 8174    let dock = active_dock.read(cx);
 8175    let Some(panel_size) = workspace.dock_size(&dock, window, cx) else {
 8176        return;
 8177    };
 8178    workspace.resize_dock(dock.position(), panel_size + px, window, cx);
 8179}
 8180
 8181fn adjust_open_docks_size_by_px(
 8182    px: Pixels,
 8183    workspace: &mut Workspace,
 8184    window: &mut Window,
 8185    cx: &mut Context<Workspace>,
 8186) {
 8187    let docks = workspace
 8188        .all_docks()
 8189        .into_iter()
 8190        .filter_map(|dock_entity| {
 8191            let dock = dock_entity.read(cx);
 8192            if dock.is_open() {
 8193                let dock_pos = dock.position();
 8194                let panel_size = workspace.dock_size(&dock, window, cx)?;
 8195                Some((dock_pos, panel_size + px))
 8196            } else {
 8197                None
 8198            }
 8199        })
 8200        .collect::<Vec<_>>();
 8201
 8202    for (position, new_size) in docks {
 8203        workspace.resize_dock(position, new_size, window, cx);
 8204    }
 8205}
 8206
 8207impl Focusable for Workspace {
 8208    fn focus_handle(&self, cx: &App) -> FocusHandle {
 8209        self.active_pane.focus_handle(cx)
 8210    }
 8211}
 8212
 8213#[derive(Clone)]
 8214struct DraggedDock(DockPosition);
 8215
 8216impl Render for DraggedDock {
 8217    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 8218        gpui::Empty
 8219    }
 8220}
 8221
 8222impl Render for Workspace {
 8223    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 8224        static FIRST_PAINT: AtomicBool = AtomicBool::new(true);
 8225        if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) {
 8226            log::info!("Rendered first frame");
 8227        }
 8228
 8229        let centered_layout = self.centered_layout
 8230            && self.center.panes().len() == 1
 8231            && self.active_item(cx).is_some();
 8232        let render_padding = |size| {
 8233            (size > 0.0).then(|| {
 8234                div()
 8235                    .h_full()
 8236                    .w(relative(size))
 8237                    .bg(cx.theme().colors().editor_background)
 8238                    .border_color(cx.theme().colors().pane_group_border)
 8239            })
 8240        };
 8241        let paddings = if centered_layout {
 8242            let settings = WorkspaceSettings::get_global(cx).centered_layout;
 8243            (
 8244                render_padding(Self::adjust_padding(
 8245                    settings.left_padding.map(|padding| padding.0),
 8246                )),
 8247                render_padding(Self::adjust_padding(
 8248                    settings.right_padding.map(|padding| padding.0),
 8249                )),
 8250            )
 8251        } else {
 8252            (None, None)
 8253        };
 8254        let ui_font = theme_settings::setup_ui_font(window, cx);
 8255
 8256        let theme = cx.theme().clone();
 8257        let colors = theme.colors();
 8258        let notification_entities = self
 8259            .notifications
 8260            .iter()
 8261            .map(|(_, notification)| notification.entity_id())
 8262            .collect::<Vec<_>>();
 8263        let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
 8264
 8265        let pane_render_context = PaneRenderContext {
 8266            follower_states: &self.follower_states,
 8267            active_call: self.active_call(),
 8268            active_pane: &self.active_pane,
 8269            app_state: &self.app_state,
 8270            project: &self.project,
 8271            workspace: &self.weak_self,
 8272        };
 8273
 8274        div()
 8275            .relative()
 8276            .size_full()
 8277            .flex()
 8278            .flex_col()
 8279            .font(ui_font)
 8280            .gap_0()
 8281            .justify_start()
 8282            .items_start()
 8283            .text_color(colors.text)
 8284            .overflow_hidden()
 8285            .children(self.titlebar_item.clone())
 8286            .on_modifiers_changed(move |_, _, cx| {
 8287                for &id in &notification_entities {
 8288                    cx.notify(id);
 8289                }
 8290            })
 8291            .child(
 8292                div()
 8293                    .size_full()
 8294                    .relative()
 8295                    .flex_1()
 8296                    .flex()
 8297                    .flex_col()
 8298                    .child(
 8299                        div()
 8300                            .id("workspace")
 8301                            .bg(colors.background)
 8302                            .relative()
 8303                            .flex_1()
 8304                            .w_full()
 8305                            .flex()
 8306                            .flex_col()
 8307                            .overflow_hidden()
 8308                            .border_t_1()
 8309                            .border_b_1()
 8310                            .border_color(colors.border)
 8311                            .child({
 8312                                let this = cx.entity();
 8313                                canvas(
 8314                                    move |bounds, window, cx| {
 8315                                        this.update(cx, |this, cx| {
 8316                                            let bounds_changed = this.bounds != bounds;
 8317                                            this.bounds = bounds;
 8318
 8319                                            if bounds_changed {
 8320                                                this.left_dock.update(cx, |dock, cx| {
 8321                                                    dock.clamp_panel_size(
 8322                                                        bounds.size.width,
 8323                                                        window,
 8324                                                        cx,
 8325                                                    )
 8326                                                });
 8327
 8328                                                this.right_dock.update(cx, |dock, cx| {
 8329                                                    dock.clamp_panel_size(
 8330                                                        bounds.size.width,
 8331                                                        window,
 8332                                                        cx,
 8333                                                    )
 8334                                                });
 8335
 8336                                                this.bottom_dock.update(cx, |dock, cx| {
 8337                                                    dock.clamp_panel_size(
 8338                                                        bounds.size.height,
 8339                                                        window,
 8340                                                        cx,
 8341                                                    )
 8342                                                });
 8343                                            }
 8344                                        })
 8345                                    },
 8346                                    |_, _, _, _| {},
 8347                                )
 8348                                .absolute()
 8349                                .size_full()
 8350                            })
 8351                            .when(self.zoomed.is_none(), |this| {
 8352                                this.on_drag_move(cx.listener(
 8353                                    move |workspace, e: &DragMoveEvent<DraggedDock>, window, cx| {
 8354                                        if workspace.previous_dock_drag_coordinates
 8355                                            != Some(e.event.position)
 8356                                        {
 8357                                            workspace.previous_dock_drag_coordinates =
 8358                                                Some(e.event.position);
 8359
 8360                                            match e.drag(cx).0 {
 8361                                                DockPosition::Left => {
 8362                                                    workspace.resize_left_dock(
 8363                                                        e.event.position.x
 8364                                                            - workspace.bounds.left(),
 8365                                                        window,
 8366                                                        cx,
 8367                                                    );
 8368                                                }
 8369                                                DockPosition::Right => {
 8370                                                    workspace.resize_right_dock(
 8371                                                        workspace.bounds.right()
 8372                                                            - e.event.position.x,
 8373                                                        window,
 8374                                                        cx,
 8375                                                    );
 8376                                                }
 8377                                                DockPosition::Bottom => {
 8378                                                    workspace.resize_bottom_dock(
 8379                                                        workspace.bounds.bottom()
 8380                                                            - e.event.position.y,
 8381                                                        window,
 8382                                                        cx,
 8383                                                    );
 8384                                                }
 8385                                            };
 8386                                            workspace.serialize_workspace(window, cx);
 8387                                        }
 8388                                    },
 8389                                ))
 8390                            })
 8391                            .child({
 8392                                match bottom_dock_layout {
 8393                                    BottomDockLayout::Full => div()
 8394                                        .flex()
 8395                                        .flex_col()
 8396                                        .h_full()
 8397                                        .child(
 8398                                            div()
 8399                                                .flex()
 8400                                                .flex_row()
 8401                                                .flex_1()
 8402                                                .overflow_hidden()
 8403                                                .children(self.render_dock(
 8404                                                    DockPosition::Left,
 8405                                                    &self.left_dock,
 8406                                                    window,
 8407                                                    cx,
 8408                                                ))
 8409                                                .child(
 8410                                                    div()
 8411                                                        .flex()
 8412                                                        .flex_col()
 8413                                                        .flex_1()
 8414                                                        .overflow_hidden()
 8415                                                        .child(
 8416                                                            h_flex()
 8417                                                                .flex_1()
 8418                                                                .when_some(paddings.0, |this, p| {
 8419                                                                    this.child(p.border_r_1())
 8420                                                                })
 8421                                                                .child(self.center.render(
 8422                                                                    self.zoomed.as_ref(),
 8423                                                                    &pane_render_context,
 8424                                                                    window,
 8425                                                                    cx,
 8426                                                                ))
 8427                                                                .when_some(
 8428                                                                    paddings.1,
 8429                                                                    |this, p| {
 8430                                                                        this.child(p.border_l_1())
 8431                                                                    },
 8432                                                                ),
 8433                                                        ),
 8434                                                )
 8435                                                .children(self.render_dock(
 8436                                                    DockPosition::Right,
 8437                                                    &self.right_dock,
 8438                                                    window,
 8439                                                    cx,
 8440                                                )),
 8441                                        )
 8442                                        .child(div().w_full().children(self.render_dock(
 8443                                            DockPosition::Bottom,
 8444                                            &self.bottom_dock,
 8445                                            window,
 8446                                            cx,
 8447                                        ))),
 8448
 8449                                    BottomDockLayout::LeftAligned => div()
 8450                                        .flex()
 8451                                        .flex_row()
 8452                                        .h_full()
 8453                                        .child(
 8454                                            div()
 8455                                                .flex()
 8456                                                .flex_col()
 8457                                                .flex_1()
 8458                                                .h_full()
 8459                                                .child(
 8460                                                    div()
 8461                                                        .flex()
 8462                                                        .flex_row()
 8463                                                        .flex_1()
 8464                                                        .children(self.render_dock(
 8465                                                            DockPosition::Left,
 8466                                                            &self.left_dock,
 8467                                                            window,
 8468                                                            cx,
 8469                                                        ))
 8470                                                        .child(
 8471                                                            div()
 8472                                                                .flex()
 8473                                                                .flex_col()
 8474                                                                .flex_1()
 8475                                                                .overflow_hidden()
 8476                                                                .child(
 8477                                                                    h_flex()
 8478                                                                        .flex_1()
 8479                                                                        .when_some(
 8480                                                                            paddings.0,
 8481                                                                            |this, p| {
 8482                                                                                this.child(
 8483                                                                                    p.border_r_1(),
 8484                                                                                )
 8485                                                                            },
 8486                                                                        )
 8487                                                                        .child(self.center.render(
 8488                                                                            self.zoomed.as_ref(),
 8489                                                                            &pane_render_context,
 8490                                                                            window,
 8491                                                                            cx,
 8492                                                                        ))
 8493                                                                        .when_some(
 8494                                                                            paddings.1,
 8495                                                                            |this, p| {
 8496                                                                                this.child(
 8497                                                                                    p.border_l_1(),
 8498                                                                                )
 8499                                                                            },
 8500                                                                        ),
 8501                                                                ),
 8502                                                        ),
 8503                                                )
 8504                                                .child(div().w_full().children(self.render_dock(
 8505                                                    DockPosition::Bottom,
 8506                                                    &self.bottom_dock,
 8507                                                    window,
 8508                                                    cx,
 8509                                                ))),
 8510                                        )
 8511                                        .children(self.render_dock(
 8512                                            DockPosition::Right,
 8513                                            &self.right_dock,
 8514                                            window,
 8515                                            cx,
 8516                                        )),
 8517                                    BottomDockLayout::RightAligned => div()
 8518                                        .flex()
 8519                                        .flex_row()
 8520                                        .h_full()
 8521                                        .children(self.render_dock(
 8522                                            DockPosition::Left,
 8523                                            &self.left_dock,
 8524                                            window,
 8525                                            cx,
 8526                                        ))
 8527                                        .child(
 8528                                            div()
 8529                                                .flex()
 8530                                                .flex_col()
 8531                                                .flex_1()
 8532                                                .h_full()
 8533                                                .child(
 8534                                                    div()
 8535                                                        .flex()
 8536                                                        .flex_row()
 8537                                                        .flex_1()
 8538                                                        .child(
 8539                                                            div()
 8540                                                                .flex()
 8541                                                                .flex_col()
 8542                                                                .flex_1()
 8543                                                                .overflow_hidden()
 8544                                                                .child(
 8545                                                                    h_flex()
 8546                                                                        .flex_1()
 8547                                                                        .when_some(
 8548                                                                            paddings.0,
 8549                                                                            |this, p| {
 8550                                                                                this.child(
 8551                                                                                    p.border_r_1(),
 8552                                                                                )
 8553                                                                            },
 8554                                                                        )
 8555                                                                        .child(self.center.render(
 8556                                                                            self.zoomed.as_ref(),
 8557                                                                            &pane_render_context,
 8558                                                                            window,
 8559                                                                            cx,
 8560                                                                        ))
 8561                                                                        .when_some(
 8562                                                                            paddings.1,
 8563                                                                            |this, p| {
 8564                                                                                this.child(
 8565                                                                                    p.border_l_1(),
 8566                                                                                )
 8567                                                                            },
 8568                                                                        ),
 8569                                                                ),
 8570                                                        )
 8571                                                        .children(self.render_dock(
 8572                                                            DockPosition::Right,
 8573                                                            &self.right_dock,
 8574                                                            window,
 8575                                                            cx,
 8576                                                        )),
 8577                                                )
 8578                                                .child(div().w_full().children(self.render_dock(
 8579                                                    DockPosition::Bottom,
 8580                                                    &self.bottom_dock,
 8581                                                    window,
 8582                                                    cx,
 8583                                                ))),
 8584                                        ),
 8585                                    BottomDockLayout::Contained => div()
 8586                                        .flex()
 8587                                        .flex_row()
 8588                                        .h_full()
 8589                                        .children(self.render_dock(
 8590                                            DockPosition::Left,
 8591                                            &self.left_dock,
 8592                                            window,
 8593                                            cx,
 8594                                        ))
 8595                                        .child(
 8596                                            div()
 8597                                                .flex()
 8598                                                .flex_col()
 8599                                                .flex_1()
 8600                                                .overflow_hidden()
 8601                                                .child(
 8602                                                    h_flex()
 8603                                                        .flex_1()
 8604                                                        .when_some(paddings.0, |this, p| {
 8605                                                            this.child(p.border_r_1())
 8606                                                        })
 8607                                                        .child(self.center.render(
 8608                                                            self.zoomed.as_ref(),
 8609                                                            &pane_render_context,
 8610                                                            window,
 8611                                                            cx,
 8612                                                        ))
 8613                                                        .when_some(paddings.1, |this, p| {
 8614                                                            this.child(p.border_l_1())
 8615                                                        }),
 8616                                                )
 8617                                                .children(self.render_dock(
 8618                                                    DockPosition::Bottom,
 8619                                                    &self.bottom_dock,
 8620                                                    window,
 8621                                                    cx,
 8622                                                )),
 8623                                        )
 8624                                        .children(self.render_dock(
 8625                                            DockPosition::Right,
 8626                                            &self.right_dock,
 8627                                            window,
 8628                                            cx,
 8629                                        )),
 8630                                }
 8631                            })
 8632                            .children(self.zoomed.as_ref().and_then(|view| {
 8633                                let zoomed_view = view.upgrade()?;
 8634                                let div = div()
 8635                                    .occlude()
 8636                                    .absolute()
 8637                                    .overflow_hidden()
 8638                                    .border_color(colors.border)
 8639                                    .bg(colors.background)
 8640                                    .child(zoomed_view)
 8641                                    .inset_0()
 8642                                    .shadow_lg();
 8643
 8644                                if !WorkspaceSettings::get_global(cx).zoomed_padding {
 8645                                    return Some(div);
 8646                                }
 8647
 8648                                Some(match self.zoomed_position {
 8649                                    Some(DockPosition::Left) => div.right_2().border_r_1(),
 8650                                    Some(DockPosition::Right) => div.left_2().border_l_1(),
 8651                                    Some(DockPosition::Bottom) => div.top_2().border_t_1(),
 8652                                    None => div.top_2().bottom_2().left_2().right_2().border_1(),
 8653                                })
 8654                            }))
 8655                            .children(self.render_notifications(window, cx)),
 8656                    )
 8657                    .when(self.status_bar_visible(cx), |parent| {
 8658                        parent.child(self.status_bar.clone())
 8659                    })
 8660                    .child(self.toast_layer.clone()),
 8661            )
 8662    }
 8663}
 8664
 8665impl WorkspaceStore {
 8666    pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
 8667        Self {
 8668            workspaces: Default::default(),
 8669            _subscriptions: vec![
 8670                client.add_request_handler(cx.weak_entity(), Self::handle_follow),
 8671                client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
 8672            ],
 8673            client,
 8674        }
 8675    }
 8676
 8677    pub fn update_followers(
 8678        &self,
 8679        project_id: Option<u64>,
 8680        update: proto::update_followers::Variant,
 8681        cx: &App,
 8682    ) -> Option<()> {
 8683        let active_call = GlobalAnyActiveCall::try_global(cx)?;
 8684        let room_id = active_call.0.room_id(cx)?;
 8685        self.client
 8686            .send(proto::UpdateFollowers {
 8687                room_id,
 8688                project_id,
 8689                variant: Some(update),
 8690            })
 8691            .log_err()
 8692    }
 8693
 8694    pub async fn handle_follow(
 8695        this: Entity<Self>,
 8696        envelope: TypedEnvelope<proto::Follow>,
 8697        mut cx: AsyncApp,
 8698    ) -> Result<proto::FollowResponse> {
 8699        this.update(&mut cx, |this, cx| {
 8700            let follower = Follower {
 8701                project_id: envelope.payload.project_id,
 8702                peer_id: envelope.original_sender_id()?,
 8703            };
 8704
 8705            let mut response = proto::FollowResponse::default();
 8706
 8707            this.workspaces.retain(|(window_handle, weak_workspace)| {
 8708                let Some(workspace) = weak_workspace.upgrade() else {
 8709                    return false;
 8710                };
 8711                window_handle
 8712                    .update(cx, |_, window, cx| {
 8713                        workspace.update(cx, |workspace, cx| {
 8714                            let handler_response =
 8715                                workspace.handle_follow(follower.project_id, window, cx);
 8716                            if let Some(active_view) = handler_response.active_view
 8717                                && workspace.project.read(cx).remote_id() == follower.project_id
 8718                            {
 8719                                response.active_view = Some(active_view)
 8720                            }
 8721                        });
 8722                    })
 8723                    .is_ok()
 8724            });
 8725
 8726            Ok(response)
 8727        })
 8728    }
 8729
 8730    async fn handle_update_followers(
 8731        this: Entity<Self>,
 8732        envelope: TypedEnvelope<proto::UpdateFollowers>,
 8733        mut cx: AsyncApp,
 8734    ) -> Result<()> {
 8735        let leader_id = envelope.original_sender_id()?;
 8736        let update = envelope.payload;
 8737
 8738        this.update(&mut cx, |this, cx| {
 8739            this.workspaces.retain(|(window_handle, weak_workspace)| {
 8740                let Some(workspace) = weak_workspace.upgrade() else {
 8741                    return false;
 8742                };
 8743                window_handle
 8744                    .update(cx, |_, window, cx| {
 8745                        workspace.update(cx, |workspace, cx| {
 8746                            let project_id = workspace.project.read(cx).remote_id();
 8747                            if update.project_id != project_id && update.project_id.is_some() {
 8748                                return;
 8749                            }
 8750                            workspace.handle_update_followers(
 8751                                leader_id,
 8752                                update.clone(),
 8753                                window,
 8754                                cx,
 8755                            );
 8756                        });
 8757                    })
 8758                    .is_ok()
 8759            });
 8760            Ok(())
 8761        })
 8762    }
 8763
 8764    pub fn workspaces(&self) -> impl Iterator<Item = &WeakEntity<Workspace>> {
 8765        self.workspaces.iter().map(|(_, weak)| weak)
 8766    }
 8767
 8768    pub fn workspaces_with_windows(
 8769        &self,
 8770    ) -> impl Iterator<Item = (gpui::AnyWindowHandle, &WeakEntity<Workspace>)> {
 8771        self.workspaces.iter().map(|(window, weak)| (*window, weak))
 8772    }
 8773}
 8774
 8775impl ViewId {
 8776    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
 8777        Ok(Self {
 8778            creator: message
 8779                .creator
 8780                .map(CollaboratorId::PeerId)
 8781                .context("creator is missing")?,
 8782            id: message.id,
 8783        })
 8784    }
 8785
 8786    pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
 8787        if let CollaboratorId::PeerId(peer_id) = self.creator {
 8788            Some(proto::ViewId {
 8789                creator: Some(peer_id),
 8790                id: self.id,
 8791            })
 8792        } else {
 8793            None
 8794        }
 8795    }
 8796}
 8797
 8798impl FollowerState {
 8799    fn pane(&self) -> &Entity<Pane> {
 8800        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
 8801    }
 8802}
 8803
 8804pub trait WorkspaceHandle {
 8805    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
 8806}
 8807
 8808impl WorkspaceHandle for Entity<Workspace> {
 8809    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
 8810        self.read(cx)
 8811            .worktrees(cx)
 8812            .flat_map(|worktree| {
 8813                let worktree_id = worktree.read(cx).id();
 8814                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
 8815                    worktree_id,
 8816                    path: f.path.clone(),
 8817                })
 8818            })
 8819            .collect::<Vec<_>>()
 8820    }
 8821}
 8822
 8823pub async fn last_opened_workspace_location(
 8824    db: &WorkspaceDb,
 8825    fs: &dyn fs::Fs,
 8826) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
 8827    db.last_workspace(fs)
 8828        .await
 8829        .log_err()
 8830        .flatten()
 8831        .map(|(id, location, paths, _timestamp)| (id, location, paths))
 8832}
 8833
 8834pub async fn last_session_workspace_locations(
 8835    db: &WorkspaceDb,
 8836    last_session_id: &str,
 8837    last_session_window_stack: Option<Vec<WindowId>>,
 8838    fs: &dyn fs::Fs,
 8839) -> Option<Vec<SessionWorkspace>> {
 8840    db.last_session_workspace_locations(last_session_id, last_session_window_stack, fs)
 8841        .await
 8842        .log_err()
 8843}
 8844
 8845pub async fn restore_multiworkspace(
 8846    multi_workspace: SerializedMultiWorkspace,
 8847    app_state: Arc<AppState>,
 8848    cx: &mut AsyncApp,
 8849) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
 8850    let SerializedMultiWorkspace {
 8851        active_workspace,
 8852        state,
 8853    } = multi_workspace;
 8854
 8855    let workspace_result = if active_workspace.paths.is_empty() {
 8856        cx.update(|cx| {
 8857            open_workspace_by_id(active_workspace.workspace_id, app_state.clone(), None, cx)
 8858        })
 8859        .await
 8860    } else {
 8861        cx.update(|cx| {
 8862            Workspace::new_local(
 8863                active_workspace.paths.paths().to_vec(),
 8864                app_state.clone(),
 8865                None,
 8866                None,
 8867                None,
 8868                OpenMode::Activate,
 8869                cx,
 8870            )
 8871        })
 8872        .await
 8873        .map(|result| result.window)
 8874    };
 8875
 8876    let window_handle = match workspace_result {
 8877        Ok(handle) => handle,
 8878        Err(err) => {
 8879            log::error!("Failed to restore active workspace: {err:#}");
 8880
 8881            let mut fallback_handle = None;
 8882            for key in &state.project_groups {
 8883                let key: ProjectGroupKey = key.clone().into();
 8884                let paths = key.path_list().paths().to_vec();
 8885                match cx
 8886                    .update(|cx| {
 8887                        Workspace::new_local(
 8888                            paths,
 8889                            app_state.clone(),
 8890                            None,
 8891                            None,
 8892                            None,
 8893                            OpenMode::Activate,
 8894                            cx,
 8895                        )
 8896                    })
 8897                    .await
 8898                {
 8899                    Ok(OpenResult { window, .. }) => {
 8900                        fallback_handle = Some(window);
 8901                        break;
 8902                    }
 8903                    Err(fallback_err) => {
 8904                        log::error!("Fallback project group also failed: {fallback_err:#}");
 8905                    }
 8906                }
 8907            }
 8908
 8909            fallback_handle.ok_or(err)?
 8910        }
 8911    };
 8912
 8913    apply_restored_multiworkspace_state(window_handle, &state, app_state.fs.clone(), cx).await;
 8914
 8915    window_handle
 8916        .update(cx, |_, window, _cx| {
 8917            window.activate_window();
 8918        })
 8919        .ok();
 8920
 8921    Ok(window_handle)
 8922}
 8923
 8924pub async fn apply_restored_multiworkspace_state(
 8925    window_handle: WindowHandle<MultiWorkspace>,
 8926    state: &MultiWorkspaceState,
 8927    fs: Arc<dyn fs::Fs>,
 8928    cx: &mut AsyncApp,
 8929) {
 8930    let MultiWorkspaceState {
 8931        sidebar_open,
 8932        project_groups,
 8933        sidebar_state,
 8934        ..
 8935    } = state;
 8936
 8937    if !project_groups.is_empty() {
 8938        // Resolve linked worktree paths to their main repo paths so
 8939        // stale keys from previous sessions get normalized and deduped.
 8940        let mut resolved_groups: Vec<SerializedProjectGroupState> = Vec::new();
 8941        for serialized in project_groups.iter().cloned() {
 8942            let SerializedProjectGroupState { key, expanded } = serialized.into_restored_state();
 8943            if key.path_list().paths().is_empty() {
 8944                continue;
 8945            }
 8946            let mut resolved_paths = Vec::new();
 8947            for path in key.path_list().paths() {
 8948                if key.host().is_none()
 8949                    && let Some(common_dir) =
 8950                        project::discover_root_repo_common_dir(path, fs.as_ref()).await
 8951                {
 8952                    let main_path = common_dir.parent().unwrap_or(&common_dir);
 8953                    resolved_paths.push(main_path.to_path_buf());
 8954                } else {
 8955                    resolved_paths.push(path.to_path_buf());
 8956                }
 8957            }
 8958            let resolved = ProjectGroupKey::new(key.host(), PathList::new(&resolved_paths));
 8959            if !resolved_groups.iter().any(|g| g.key == resolved) {
 8960                resolved_groups.push(SerializedProjectGroupState {
 8961                    key: resolved,
 8962                    expanded,
 8963                });
 8964            }
 8965        }
 8966
 8967        window_handle
 8968            .update(cx, |multi_workspace, _window, cx| {
 8969                multi_workspace.restore_project_groups(resolved_groups, cx);
 8970            })
 8971            .ok();
 8972    }
 8973
 8974    if *sidebar_open {
 8975        window_handle
 8976            .update(cx, |multi_workspace, _, cx| {
 8977                multi_workspace.restore_open_sidebar(cx);
 8978            })
 8979            .ok();
 8980    }
 8981
 8982    if let Some(sidebar_state) = sidebar_state {
 8983        window_handle
 8984            .update(cx, |multi_workspace, window, cx| {
 8985                if let Some(sidebar) = multi_workspace.sidebar() {
 8986                    sidebar.restore_serialized_state(sidebar_state, window, cx);
 8987                }
 8988                multi_workspace.serialize(cx);
 8989            })
 8990            .ok();
 8991    }
 8992}
 8993
 8994actions!(
 8995    collab,
 8996    [
 8997        /// Opens the channel notes for the current call.
 8998        ///
 8999        /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected
 9000        /// channel in the collab panel.
 9001        ///
 9002        /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
 9003        /// can be copied via "Copy link to section" in the context menu of the channel notes
 9004        /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
 9005        OpenChannelNotes,
 9006        /// Mutes your microphone.
 9007        Mute,
 9008        /// Deafens yourself (mute both microphone and speakers).
 9009        Deafen,
 9010        /// Leaves the current call.
 9011        LeaveCall,
 9012        /// Shares the current project with collaborators.
 9013        ShareProject,
 9014        /// Shares your screen with collaborators.
 9015        ScreenShare,
 9016        /// Copies the current room name and session id for debugging purposes.
 9017        CopyRoomId,
 9018    ]
 9019);
 9020
 9021/// Opens the channel notes for a specific channel by its ID.
 9022#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
 9023#[action(namespace = collab)]
 9024#[serde(deny_unknown_fields)]
 9025pub struct OpenChannelNotesById {
 9026    pub channel_id: u64,
 9027}
 9028
 9029actions!(
 9030    zed,
 9031    [
 9032        /// Opens the Zed log file.
 9033        OpenLog,
 9034        /// Reveals the Zed log file in the system file manager.
 9035        RevealLogInFileManager
 9036    ]
 9037);
 9038
 9039async fn join_channel_internal(
 9040    channel_id: ChannelId,
 9041    app_state: &Arc<AppState>,
 9042    requesting_window: Option<WindowHandle<MultiWorkspace>>,
 9043    requesting_workspace: Option<WeakEntity<Workspace>>,
 9044    active_call: &dyn AnyActiveCall,
 9045    cx: &mut AsyncApp,
 9046) -> Result<bool> {
 9047    let (should_prompt, already_in_channel) = cx.update(|cx| {
 9048        if !active_call.is_in_room(cx) {
 9049            return (false, false);
 9050        }
 9051
 9052        let already_in_channel = active_call.channel_id(cx) == Some(channel_id);
 9053        let should_prompt = active_call.is_sharing_project(cx)
 9054            && active_call.has_remote_participants(cx)
 9055            && !already_in_channel;
 9056        (should_prompt, already_in_channel)
 9057    });
 9058
 9059    if already_in_channel {
 9060        let task = cx.update(|cx| {
 9061            if let Some((project, host)) = active_call.most_active_project(cx) {
 9062                Some(join_in_room_project(project, host, app_state.clone(), cx))
 9063            } else {
 9064                None
 9065            }
 9066        });
 9067        if let Some(task) = task {
 9068            task.await?;
 9069        }
 9070        return anyhow::Ok(true);
 9071    }
 9072
 9073    if should_prompt {
 9074        if let Some(multi_workspace) = requesting_window {
 9075            let answer = multi_workspace
 9076                .update(cx, |_, window, cx| {
 9077                    window.prompt(
 9078                        PromptLevel::Warning,
 9079                        "Do you want to switch channels?",
 9080                        Some("Leaving this call will unshare your current project."),
 9081                        &["Yes, Join Channel", "Cancel"],
 9082                        cx,
 9083                    )
 9084                })?
 9085                .await;
 9086
 9087            if answer == Ok(1) {
 9088                return Ok(false);
 9089            }
 9090        } else {
 9091            return Ok(false);
 9092        }
 9093    }
 9094
 9095    let client = cx.update(|cx| active_call.client(cx));
 9096
 9097    let mut client_status = client.status();
 9098
 9099    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
 9100    'outer: loop {
 9101        let Some(status) = client_status.recv().await else {
 9102            anyhow::bail!("error connecting");
 9103        };
 9104
 9105        match status {
 9106            Status::Connecting
 9107            | Status::Authenticating
 9108            | Status::Authenticated
 9109            | Status::Reconnecting
 9110            | Status::Reauthenticating
 9111            | Status::Reauthenticated => continue,
 9112            Status::Connected { .. } => break 'outer,
 9113            Status::SignedOut | Status::AuthenticationError => {
 9114                return Err(ErrorCode::SignedOut.into());
 9115            }
 9116            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
 9117            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
 9118                return Err(ErrorCode::Disconnected.into());
 9119            }
 9120        }
 9121    }
 9122
 9123    let joined = cx
 9124        .update(|cx| active_call.join_channel(channel_id, cx))
 9125        .await?;
 9126
 9127    if !joined {
 9128        return anyhow::Ok(true);
 9129    }
 9130
 9131    cx.update(|cx| active_call.room_update_completed(cx)).await;
 9132
 9133    let task = cx.update(|cx| {
 9134        if let Some((project, host)) = active_call.most_active_project(cx) {
 9135            return Some(join_in_room_project(project, host, app_state.clone(), cx));
 9136        }
 9137
 9138        // If you are the first to join a channel, see if you should share your project.
 9139        if !active_call.has_remote_participants(cx)
 9140            && !active_call.local_participant_is_guest(cx)
 9141            && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade())
 9142        {
 9143            let project = workspace.update(cx, |workspace, cx| {
 9144                let project = workspace.project.read(cx);
 9145
 9146                if !active_call.share_on_join(cx) {
 9147                    return None;
 9148                }
 9149
 9150                if (project.is_local() || project.is_via_remote_server())
 9151                    && project.visible_worktrees(cx).any(|tree| {
 9152                        tree.read(cx)
 9153                            .root_entry()
 9154                            .is_some_and(|entry| entry.is_dir())
 9155                    })
 9156                {
 9157                    Some(workspace.project.clone())
 9158                } else {
 9159                    None
 9160                }
 9161            });
 9162            if let Some(project) = project {
 9163                let share_task = active_call.share_project(project, cx);
 9164                return Some(cx.spawn(async move |_cx| -> Result<()> {
 9165                    share_task.await?;
 9166                    Ok(())
 9167                }));
 9168            }
 9169        }
 9170
 9171        None
 9172    });
 9173    if let Some(task) = task {
 9174        task.await?;
 9175        return anyhow::Ok(true);
 9176    }
 9177    anyhow::Ok(false)
 9178}
 9179
 9180pub fn join_channel(
 9181    channel_id: ChannelId,
 9182    app_state: Arc<AppState>,
 9183    requesting_window: Option<WindowHandle<MultiWorkspace>>,
 9184    requesting_workspace: Option<WeakEntity<Workspace>>,
 9185    cx: &mut App,
 9186) -> Task<Result<()>> {
 9187    let active_call = GlobalAnyActiveCall::global(cx).clone();
 9188    cx.spawn(async move |cx| {
 9189        let result = join_channel_internal(
 9190            channel_id,
 9191            &app_state,
 9192            requesting_window,
 9193            requesting_workspace,
 9194            &*active_call.0,
 9195            cx,
 9196        )
 9197        .await;
 9198
 9199        // join channel succeeded, and opened a window
 9200        if matches!(result, Ok(true)) {
 9201            return anyhow::Ok(());
 9202        }
 9203
 9204        // find an existing workspace to focus and show call controls
 9205        let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
 9206        if active_window.is_none() {
 9207            // no open workspaces, make one to show the error in (blergh)
 9208            let OpenResult {
 9209                window: window_handle,
 9210                ..
 9211            } = cx
 9212                .update(|cx| {
 9213                    Workspace::new_local(
 9214                        vec![],
 9215                        app_state.clone(),
 9216                        requesting_window,
 9217                        None,
 9218                        None,
 9219                        OpenMode::Activate,
 9220                        cx,
 9221                    )
 9222                })
 9223                .await?;
 9224
 9225            window_handle
 9226                .update(cx, |_, window, _cx| {
 9227                    window.activate_window();
 9228                })
 9229                .ok();
 9230
 9231            if result.is_ok() {
 9232                cx.update(|cx| {
 9233                    cx.dispatch_action(&OpenChannelNotes);
 9234                });
 9235            }
 9236
 9237            active_window = Some(window_handle);
 9238        }
 9239
 9240        if let Err(err) = result {
 9241            log::error!("failed to join channel: {}", err);
 9242            if let Some(active_window) = active_window {
 9243                active_window
 9244                    .update(cx, |_, window, cx| {
 9245                        let detail: SharedString = match err.error_code() {
 9246                            ErrorCode::SignedOut => "Please sign in to continue.".into(),
 9247                            ErrorCode::UpgradeRequired => concat!(
 9248                                "Your are running an unsupported version of Zed. ",
 9249                                "Please update to continue."
 9250                            )
 9251                            .into(),
 9252                            ErrorCode::NoSuchChannel => concat!(
 9253                                "No matching channel was found. ",
 9254                                "Please check the link and try again."
 9255                            )
 9256                            .into(),
 9257                            ErrorCode::Forbidden => concat!(
 9258                                "This channel is private, and you do not have access. ",
 9259                                "Please ask someone to add you and try again."
 9260                            )
 9261                            .into(),
 9262                            ErrorCode::Disconnected => {
 9263                                "Please check your internet connection and try again.".into()
 9264                            }
 9265                            _ => format!("{}\n\nPlease try again.", err).into(),
 9266                        };
 9267                        window.prompt(
 9268                            PromptLevel::Critical,
 9269                            "Failed to join channel",
 9270                            Some(&detail),
 9271                            &["Ok"],
 9272                            cx,
 9273                        )
 9274                    })?
 9275                    .await
 9276                    .ok();
 9277            }
 9278        }
 9279
 9280        // return ok, we showed the error to the user.
 9281        anyhow::Ok(())
 9282    })
 9283}
 9284
 9285pub async fn get_any_active_multi_workspace(
 9286    app_state: Arc<AppState>,
 9287    mut cx: AsyncApp,
 9288) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
 9289    // find an existing workspace to focus and show call controls
 9290    let active_window = activate_any_workspace_window(&mut cx);
 9291    if active_window.is_none() {
 9292        cx.update(|cx| {
 9293            Workspace::new_local(
 9294                vec![],
 9295                app_state.clone(),
 9296                None,
 9297                None,
 9298                None,
 9299                OpenMode::Activate,
 9300                cx,
 9301            )
 9302        })
 9303        .await?;
 9304    }
 9305    activate_any_workspace_window(&mut cx).context("could not open zed")
 9306}
 9307
 9308fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
 9309    cx.update(|cx| {
 9310        if let Some(workspace_window) = cx
 9311            .active_window()
 9312            .and_then(|window| window.downcast::<MultiWorkspace>())
 9313        {
 9314            return Some(workspace_window);
 9315        }
 9316
 9317        for window in cx.windows() {
 9318            if let Some(workspace_window) = window.downcast::<MultiWorkspace>() {
 9319                workspace_window
 9320                    .update(cx, |_, window, _| window.activate_window())
 9321                    .ok();
 9322                return Some(workspace_window);
 9323            }
 9324        }
 9325        None
 9326    })
 9327}
 9328
 9329pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
 9330    workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
 9331}
 9332
 9333pub fn workspace_windows_for_location(
 9334    serialized_location: &SerializedWorkspaceLocation,
 9335    cx: &App,
 9336) -> Vec<WindowHandle<MultiWorkspace>> {
 9337    cx.windows()
 9338        .into_iter()
 9339        .filter_map(|window| window.downcast::<MultiWorkspace>())
 9340        .filter(|multi_workspace| {
 9341            let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
 9342                (RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
 9343                    (&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
 9344                }
 9345                (RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
 9346                    // The WSL username is not consistently populated in the workspace location, so ignore it for now.
 9347                    a.distro_name == b.distro_name
 9348                }
 9349                (RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
 9350                    a.container_id == b.container_id
 9351                }
 9352                #[cfg(any(test, feature = "test-support"))]
 9353                (RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
 9354                    a.id == b.id
 9355                }
 9356                _ => false,
 9357            };
 9358
 9359            multi_workspace.read(cx).is_ok_and(|multi_workspace| {
 9360                multi_workspace.workspaces().any(|workspace| {
 9361                    match workspace.read(cx).workspace_location(cx) {
 9362                        WorkspaceLocation::Location(location, _) => {
 9363                            match (&location, serialized_location) {
 9364                                (
 9365                                    SerializedWorkspaceLocation::Local,
 9366                                    SerializedWorkspaceLocation::Local,
 9367                                ) => true,
 9368                                (
 9369                                    SerializedWorkspaceLocation::Remote(a),
 9370                                    SerializedWorkspaceLocation::Remote(b),
 9371                                ) => same_host(a, b),
 9372                                _ => false,
 9373                            }
 9374                        }
 9375                        _ => false,
 9376                    }
 9377                })
 9378            })
 9379        })
 9380        .collect()
 9381}
 9382
 9383pub async fn find_existing_workspace(
 9384    abs_paths: &[PathBuf],
 9385    open_options: &OpenOptions,
 9386    location: &SerializedWorkspaceLocation,
 9387    cx: &mut AsyncApp,
 9388) -> (
 9389    Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)>,
 9390    OpenVisible,
 9391) {
 9392    let mut existing: Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> = None;
 9393    let mut open_visible = OpenVisible::All;
 9394    let mut best_match = None;
 9395
 9396    if open_options.workspace_matching != WorkspaceMatching::None {
 9397        cx.update(|cx| {
 9398            for window in workspace_windows_for_location(location, cx) {
 9399                if let Ok(multi_workspace) = window.read(cx) {
 9400                    for workspace in multi_workspace.workspaces() {
 9401                        let project = workspace.read(cx).project.read(cx);
 9402                        let m = project.visibility_for_paths(
 9403                            abs_paths,
 9404                            open_options.workspace_matching != WorkspaceMatching::MatchSubdirectory,
 9405                            cx,
 9406                        );
 9407                        if m > best_match {
 9408                            existing = Some((window, workspace.clone()));
 9409                            best_match = m;
 9410                        } else if best_match.is_none()
 9411                            && open_options.workspace_matching
 9412                                == WorkspaceMatching::MatchSubdirectory
 9413                        {
 9414                            existing = Some((window, workspace.clone()))
 9415                        }
 9416                    }
 9417                }
 9418            }
 9419        });
 9420
 9421        let all_paths_are_files = existing
 9422            .as_ref()
 9423            .and_then(|(_, target_workspace)| {
 9424                cx.update(|cx| {
 9425                    let workspace = target_workspace.read(cx);
 9426                    let project = workspace.project.read(cx);
 9427                    let path_style = workspace.path_style(cx);
 9428                    Some(!abs_paths.iter().any(|path| {
 9429                        let path = util::paths::SanitizedPath::new(path);
 9430                        project.worktrees(cx).any(|worktree| {
 9431                            let worktree = worktree.read(cx);
 9432                            let abs_path = worktree.abs_path();
 9433                            path_style
 9434                                .strip_prefix(path.as_ref(), abs_path.as_ref())
 9435                                .and_then(|rel| worktree.entry_for_path(&rel))
 9436                                .is_some_and(|e| e.is_dir())
 9437                        })
 9438                    }))
 9439                })
 9440            })
 9441            .unwrap_or(false);
 9442
 9443        if open_options.wait && existing.is_some() && all_paths_are_files {
 9444            cx.update(|cx| {
 9445                let windows = workspace_windows_for_location(location, cx);
 9446                let window = cx
 9447                    .active_window()
 9448                    .and_then(|window| window.downcast::<MultiWorkspace>())
 9449                    .filter(|window| windows.contains(window))
 9450                    .or_else(|| windows.into_iter().next());
 9451                if let Some(window) = window {
 9452                    if let Ok(multi_workspace) = window.read(cx) {
 9453                        let active_workspace = multi_workspace.workspace().clone();
 9454                        existing = Some((window, active_workspace));
 9455                        open_visible = OpenVisible::None;
 9456                    }
 9457                }
 9458            });
 9459        }
 9460    }
 9461    (existing, open_visible)
 9462}
 9463
 9464/// Controls whether to reuse an existing workspace whose worktrees contain the
 9465/// given paths, and how broadly to match.
 9466#[derive(Clone, Debug, Default, PartialEq, Eq)]
 9467pub enum WorkspaceMatching {
 9468    /// Always open a new workspace. No matching against existing worktrees.
 9469    None,
 9470    /// Match paths against existing worktree roots and files within them.
 9471    #[default]
 9472    MatchExact,
 9473    /// Match paths against existing worktrees including subdirectories, and
 9474    /// fall back to any existing window if no worktree matched.
 9475    ///
 9476    /// For example, `zed -a foo/bar` will activate the `bar` workspace if it
 9477    /// exists, otherwise it will open a new window with `foo/bar` as the root.
 9478    MatchSubdirectory,
 9479}
 9480
 9481#[derive(Clone)]
 9482pub struct OpenOptions {
 9483    pub visible: Option<OpenVisible>,
 9484    pub focus: Option<bool>,
 9485    pub workspace_matching: WorkspaceMatching,
 9486    /// Whether to add unmatched directories to the existing window's sidebar
 9487    /// rather than opening a new window. Defaults to true, matching the default
 9488    /// `cli_default_open_behavior` setting.
 9489    pub add_dirs_to_sidebar: bool,
 9490    pub wait: bool,
 9491    pub requesting_window: Option<WindowHandle<MultiWorkspace>>,
 9492    pub open_mode: OpenMode,
 9493    pub env: Option<HashMap<String, String>>,
 9494    pub open_in_dev_container: bool,
 9495}
 9496
 9497impl Default for OpenOptions {
 9498    fn default() -> Self {
 9499        Self {
 9500            visible: None,
 9501            focus: None,
 9502            workspace_matching: WorkspaceMatching::default(),
 9503            add_dirs_to_sidebar: true,
 9504            wait: false,
 9505            requesting_window: None,
 9506            open_mode: OpenMode::default(),
 9507            env: None,
 9508            open_in_dev_container: false,
 9509        }
 9510    }
 9511}
 9512
 9513impl OpenOptions {
 9514    fn should_reuse_existing_window(&self) -> bool {
 9515        self.workspace_matching != WorkspaceMatching::None && self.open_mode != OpenMode::NewWindow
 9516    }
 9517}
 9518
 9519/// The result of opening a workspace via [`open_paths`], [`Workspace::new_local`],
 9520/// or [`Workspace::open_workspace_for_paths`].
 9521pub struct OpenResult {
 9522    pub window: WindowHandle<MultiWorkspace>,
 9523    pub workspace: Entity<Workspace>,
 9524    pub opened_items: Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
 9525}
 9526
 9527/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
 9528pub fn open_workspace_by_id(
 9529    workspace_id: WorkspaceId,
 9530    app_state: Arc<AppState>,
 9531    requesting_window: Option<WindowHandle<MultiWorkspace>>,
 9532    cx: &mut App,
 9533) -> Task<anyhow::Result<WindowHandle<MultiWorkspace>>> {
 9534    let project_handle = Project::local(
 9535        app_state.client.clone(),
 9536        app_state.node_runtime.clone(),
 9537        app_state.user_store.clone(),
 9538        app_state.languages.clone(),
 9539        app_state.fs.clone(),
 9540        None,
 9541        project::LocalProjectFlags {
 9542            init_worktree_trust: true,
 9543            ..project::LocalProjectFlags::default()
 9544        },
 9545        cx,
 9546    );
 9547
 9548    let db = WorkspaceDb::global(cx);
 9549    let kvp = db::kvp::KeyValueStore::global(cx);
 9550    cx.spawn(async move |cx| {
 9551        let serialized_workspace = db
 9552            .workspace_for_id(workspace_id)
 9553            .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
 9554
 9555        let centered_layout = serialized_workspace.centered_layout;
 9556
 9557        let (window, workspace) = if let Some(window) = requesting_window {
 9558            let workspace = window.update(cx, |multi_workspace, window, cx| {
 9559                let workspace = cx.new(|cx| {
 9560                    let mut workspace = Workspace::new(
 9561                        Some(workspace_id),
 9562                        project_handle.clone(),
 9563                        app_state.clone(),
 9564                        window,
 9565                        cx,
 9566                    );
 9567                    workspace.centered_layout = centered_layout;
 9568                    workspace
 9569                });
 9570                multi_workspace.add(workspace.clone(), &*window, cx);
 9571                workspace
 9572            })?;
 9573            (window, workspace)
 9574        } else {
 9575            let window_bounds_override = window_bounds_env_override();
 9576
 9577            let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
 9578                (Some(WindowBounds::Windowed(bounds)), None)
 9579            } else if let Some(display) = serialized_workspace.display
 9580                && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
 9581            {
 9582                (Some(bounds.0), Some(display))
 9583            } else if let Some((display, bounds)) = persistence::read_default_window_bounds(&kvp) {
 9584                (Some(bounds), Some(display))
 9585            } else {
 9586                (None, None)
 9587            };
 9588
 9589            let options = cx.update(|cx| {
 9590                let mut options = (app_state.build_window_options)(display, cx);
 9591                options.window_bounds = window_bounds;
 9592                options
 9593            });
 9594
 9595            let window = cx.open_window(options, {
 9596                let app_state = app_state.clone();
 9597                let project_handle = project_handle.clone();
 9598                move |window, cx| {
 9599                    let workspace = cx.new(|cx| {
 9600                        let mut workspace = Workspace::new(
 9601                            Some(workspace_id),
 9602                            project_handle,
 9603                            app_state,
 9604                            window,
 9605                            cx,
 9606                        );
 9607                        workspace.centered_layout = centered_layout;
 9608                        workspace
 9609                    });
 9610                    cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
 9611                }
 9612            })?;
 9613
 9614            let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
 9615                multi_workspace.workspace().clone()
 9616            })?;
 9617
 9618            (window, workspace)
 9619        };
 9620
 9621        notify_if_database_failed(window, cx);
 9622
 9623        // Restore items from the serialized workspace
 9624        window
 9625            .update(cx, |_, window, cx| {
 9626                workspace.update(cx, |_workspace, cx| {
 9627                    open_items(Some(serialized_workspace), vec![], window, cx)
 9628                })
 9629            })?
 9630            .await?;
 9631
 9632        window.update(cx, |_, window, cx| {
 9633            workspace.update(cx, |workspace, cx| {
 9634                workspace.serialize_workspace(window, cx);
 9635            });
 9636        })?;
 9637
 9638        Ok(window)
 9639    })
 9640}
 9641
 9642#[allow(clippy::type_complexity)]
 9643pub fn open_paths(
 9644    abs_paths: &[PathBuf],
 9645    app_state: Arc<AppState>,
 9646    mut open_options: OpenOptions,
 9647    cx: &mut App,
 9648) -> Task<anyhow::Result<OpenResult>> {
 9649    let abs_paths = abs_paths.to_vec();
 9650    #[cfg(target_os = "windows")]
 9651    let wsl_path = abs_paths
 9652        .iter()
 9653        .find_map(|p| util::paths::WslPath::from_path(p));
 9654
 9655    cx.spawn(async move |cx| {
 9656        let (mut existing, mut open_visible) = find_existing_workspace(
 9657            &abs_paths,
 9658            &open_options,
 9659            &SerializedWorkspaceLocation::Local,
 9660            cx,
 9661        )
 9662        .await;
 9663
 9664        // Fallback: if no workspace contains the paths and all paths are files,
 9665        // prefer an existing local workspace window (active window first).
 9666        if open_options.should_reuse_existing_window() && existing.is_none() {
 9667            let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
 9668            let all_metadatas = futures::future::join_all(all_paths)
 9669                .await
 9670                .into_iter()
 9671                .filter_map(|result| result.ok().flatten());
 9672
 9673            if all_metadatas.into_iter().all(|file| !file.is_dir) {
 9674                cx.update(|cx| {
 9675                    let windows = workspace_windows_for_location(
 9676                        &SerializedWorkspaceLocation::Local,
 9677                        cx,
 9678                    );
 9679                    let window = cx
 9680                        .active_window()
 9681                        .and_then(|window| window.downcast::<MultiWorkspace>())
 9682                        .filter(|window| windows.contains(window))
 9683                        .or_else(|| windows.into_iter().next());
 9684                    if let Some(window) = window {
 9685                        if let Ok(multi_workspace) = window.read(cx) {
 9686                            let active_workspace = multi_workspace.workspace().clone();
 9687                            existing = Some((window, active_workspace));
 9688                            open_visible = OpenVisible::None;
 9689                        }
 9690                    }
 9691                });
 9692            }
 9693        }
 9694
 9695        // Fallback for directories: when no flag is specified and no existing
 9696        // workspace matched, check the user's setting to decide whether to add
 9697        // the directory as a new workspace in the active window's MultiWorkspace
 9698        // or open a new window.
 9699        // Skip when requesting_window is already set: the caller (e.g.
 9700        // open_workspace_for_paths reusing an empty window) already chose the
 9701        // target window, so we must not open the sidebar as a side-effect.
 9702        if open_options.should_reuse_existing_window()
 9703            && existing.is_none()
 9704            && open_options.requesting_window.is_none()
 9705        {
 9706            let use_existing_window = open_options.add_dirs_to_sidebar;
 9707
 9708            if use_existing_window {
 9709                let target_window = cx.update(|cx| {
 9710                    let windows = workspace_windows_for_location(
 9711                        &SerializedWorkspaceLocation::Local,
 9712                        cx,
 9713                    );
 9714                    let window = cx
 9715                        .active_window()
 9716                        .and_then(|window| window.downcast::<MultiWorkspace>())
 9717                        .filter(|window| windows.contains(window))
 9718                        .or_else(|| windows.into_iter().next());
 9719                    window.filter(|window| {
 9720                        window
 9721                            .read(cx)
 9722                            .is_ok_and(|mw| mw.multi_workspace_enabled(cx))
 9723                    })
 9724                });
 9725
 9726                if let Some(window) = target_window {
 9727                    open_options.requesting_window = Some(window);
 9728                    window
 9729                        .update(cx, |multi_workspace, _, cx| {
 9730                            multi_workspace.open_sidebar(cx);
 9731                        })
 9732                        .log_err();
 9733                }
 9734            }
 9735        }
 9736
 9737        let open_in_dev_container = open_options.open_in_dev_container;
 9738
 9739        let result = if let Some((existing, target_workspace)) = existing {
 9740            let open_task = existing
 9741                .update(cx, |multi_workspace, window, cx| {
 9742                    window.activate_window();
 9743                    multi_workspace.activate(target_workspace.clone(), None, window, cx);
 9744                    target_workspace.update(cx, |workspace, cx| {
 9745                        if open_in_dev_container {
 9746                            workspace.set_open_in_dev_container(true);
 9747                        }
 9748                        workspace.open_paths(
 9749                            abs_paths,
 9750                            OpenOptions {
 9751                                visible: Some(open_visible),
 9752                                ..Default::default()
 9753                            },
 9754                            None,
 9755                            window,
 9756                            cx,
 9757                        )
 9758                    })
 9759                })?
 9760                .await;
 9761
 9762            _ = existing.update(cx, |multi_workspace, _, cx| {
 9763                let workspace = multi_workspace.workspace().clone();
 9764                workspace.update(cx, |workspace, cx| {
 9765                    for item in open_task.iter().flatten() {
 9766                        if let Err(e) = item {
 9767                            workspace.show_error(&e, cx);
 9768                        }
 9769                    }
 9770                });
 9771            });
 9772
 9773            Ok(OpenResult { window: existing, workspace: target_workspace, opened_items: open_task })
 9774        } else {
 9775            let init = if open_in_dev_container {
 9776                Some(Box::new(|workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>| {
 9777                    workspace.set_open_in_dev_container(true);
 9778                }) as Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>)
 9779            } else {
 9780                None
 9781            };
 9782            let result = cx
 9783                .update(move |cx| {
 9784                    Workspace::new_local(
 9785                        abs_paths,
 9786                        app_state.clone(),
 9787                        open_options.requesting_window,
 9788                        open_options.env,
 9789                        init,
 9790                        open_options.open_mode,
 9791                        cx,
 9792                    )
 9793                })
 9794                .await;
 9795
 9796            if let Ok(ref result) = result {
 9797                result.window
 9798                    .update(cx, |_, window, _cx| {
 9799                        window.activate_window();
 9800                    })
 9801                    .log_err();
 9802            }
 9803
 9804            result
 9805        };
 9806
 9807        #[cfg(target_os = "windows")]
 9808        if let Some(util::paths::WslPath{distro, path}) = wsl_path
 9809            && let Ok(ref result) = result
 9810        {
 9811            result.window
 9812                .update(cx, move |multi_workspace, _window, cx| {
 9813                    struct OpenInWsl;
 9814                    let workspace = multi_workspace.workspace().clone();
 9815                    workspace.update(cx, |workspace, cx| {
 9816                        workspace.show_notification(NotificationId::unique::<OpenInWsl>(), cx, move |cx| {
 9817                            let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy());
 9818                            let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote");
 9819                            cx.new(move |cx| {
 9820                                MessageNotification::new(msg, cx)
 9821                                    .primary_message("Open in WSL")
 9822                                    .primary_icon(IconName::FolderOpen)
 9823                                    .primary_on_click(move |window, cx| {
 9824                                        window.dispatch_action(Box::new(remote::OpenWslPath {
 9825                                                distro: remote::WslConnectionOptions {
 9826                                                        distro_name: distro.clone(),
 9827                                                    user: None,
 9828                                                },
 9829                                                paths: vec![path.clone().into()],
 9830                                            }), cx)
 9831                                    })
 9832                            })
 9833                        });
 9834                    });
 9835                })
 9836                .unwrap();
 9837        };
 9838        result
 9839    })
 9840}
 9841
 9842pub fn open_new(
 9843    open_options: OpenOptions,
 9844    app_state: Arc<AppState>,
 9845    cx: &mut App,
 9846    init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
 9847) -> Task<anyhow::Result<()>> {
 9848    let addition = open_options.open_mode;
 9849    let task = Workspace::new_local(
 9850        Vec::new(),
 9851        app_state,
 9852        open_options.requesting_window,
 9853        open_options.env,
 9854        Some(Box::new(init)),
 9855        addition,
 9856        cx,
 9857    );
 9858    cx.spawn(async move |cx| {
 9859        let OpenResult { window, .. } = task.await?;
 9860        window
 9861            .update(cx, |_, window, _cx| {
 9862                window.activate_window();
 9863            })
 9864            .ok();
 9865        Ok(())
 9866    })
 9867}
 9868
 9869pub fn create_and_open_local_file(
 9870    path: &'static Path,
 9871    window: &mut Window,
 9872    cx: &mut Context<Workspace>,
 9873    default_content: impl 'static + Send + FnOnce() -> Rope,
 9874) -> Task<Result<Box<dyn ItemHandle>>> {
 9875    cx.spawn_in(window, async move |workspace, cx| {
 9876        let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
 9877        if !fs.is_file(path).await {
 9878            fs.create_file(path, Default::default()).await?;
 9879            fs.save(path, &default_content(), Default::default())
 9880                .await?;
 9881        }
 9882
 9883        workspace
 9884            .update_in(cx, |workspace, window, cx| {
 9885                workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
 9886                    let path = workspace
 9887                        .project
 9888                        .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
 9889                    cx.spawn_in(window, async move |workspace, cx| {
 9890                        let path = path.await?;
 9891
 9892                        let path = fs.canonicalize(&path).await.unwrap_or(path);
 9893
 9894                        let mut items = workspace
 9895                            .update_in(cx, |workspace, window, cx| {
 9896                                workspace.open_paths(
 9897                                    vec![path.to_path_buf()],
 9898                                    OpenOptions {
 9899                                        visible: Some(OpenVisible::None),
 9900                                        ..Default::default()
 9901                                    },
 9902                                    None,
 9903                                    window,
 9904                                    cx,
 9905                                )
 9906                            })?
 9907                            .await;
 9908                        let item = items.pop().flatten();
 9909                        item.with_context(|| format!("path {path:?} is not a file"))?
 9910                    })
 9911                })
 9912            })?
 9913            .await?
 9914            .await
 9915    })
 9916}
 9917
 9918pub fn open_remote_project_with_new_connection(
 9919    window: WindowHandle<MultiWorkspace>,
 9920    remote_connection: Arc<dyn RemoteConnection>,
 9921    cancel_rx: oneshot::Receiver<()>,
 9922    delegate: Arc<dyn RemoteClientDelegate>,
 9923    app_state: Arc<AppState>,
 9924    paths: Vec<PathBuf>,
 9925    cx: &mut App,
 9926) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
 9927    cx.spawn(async move |cx| {
 9928        let (workspace_id, serialized_workspace) =
 9929            deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx)
 9930                .await?;
 9931
 9932        let session = match cx
 9933            .update(|cx| {
 9934                remote::RemoteClient::new(
 9935                    ConnectionIdentifier::Workspace(workspace_id.0),
 9936                    remote_connection,
 9937                    cancel_rx,
 9938                    delegate,
 9939                    cx,
 9940                )
 9941            })
 9942            .await?
 9943        {
 9944            Some(result) => result,
 9945            None => return Ok(Vec::new()),
 9946        };
 9947
 9948        let project = cx.update(|cx| {
 9949            project::Project::remote(
 9950                session,
 9951                app_state.client.clone(),
 9952                app_state.node_runtime.clone(),
 9953                app_state.user_store.clone(),
 9954                app_state.languages.clone(),
 9955                app_state.fs.clone(),
 9956                true,
 9957                cx,
 9958            )
 9959        });
 9960
 9961        open_remote_project_inner(
 9962            project,
 9963            paths,
 9964            workspace_id,
 9965            serialized_workspace,
 9966            app_state,
 9967            window,
 9968            None,
 9969            None,
 9970            cx,
 9971        )
 9972        .await
 9973    })
 9974}
 9975
 9976pub fn open_remote_project_with_existing_connection(
 9977    connection_options: RemoteConnectionOptions,
 9978    project: Entity<Project>,
 9979    paths: Vec<PathBuf>,
 9980    app_state: Arc<AppState>,
 9981    window: WindowHandle<MultiWorkspace>,
 9982    provisional_project_group_key: Option<ProjectGroupKey>,
 9983    source_workspace: Option<WeakEntity<Workspace>>,
 9984    cx: &mut AsyncApp,
 9985) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
 9986    cx.spawn(async move |cx| {
 9987        let (workspace_id, serialized_workspace) =
 9988            deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
 9989
 9990        open_remote_project_inner(
 9991            project,
 9992            paths,
 9993            workspace_id,
 9994            serialized_workspace,
 9995            app_state,
 9996            window,
 9997            provisional_project_group_key,
 9998            source_workspace,
 9999            cx,
10000        )
10001        .await
10002    })
10003}
10004
10005async fn open_remote_project_inner(
10006    project: Entity<Project>,
10007    paths: Vec<PathBuf>,
10008    workspace_id: WorkspaceId,
10009    serialized_workspace: Option<SerializedWorkspace>,
10010    app_state: Arc<AppState>,
10011    window: WindowHandle<MultiWorkspace>,
10012    provisional_project_group_key: Option<ProjectGroupKey>,
10013    source_workspace: Option<WeakEntity<Workspace>>,
10014    cx: &mut AsyncApp,
10015) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
10016    let db = cx.update(|cx| WorkspaceDb::global(cx));
10017    let toolchains = db.toolchains(workspace_id).await?;
10018    for (toolchain, worktree_path, path) in toolchains {
10019        project
10020            .update(cx, |this, cx| {
10021                let Some(worktree_id) =
10022                    this.find_worktree(&worktree_path, cx)
10023                        .and_then(|(worktree, rel_path)| {
10024                            if rel_path.is_empty() {
10025                                Some(worktree.read(cx).id())
10026                            } else {
10027                                None
10028                            }
10029                        })
10030                else {
10031                    return Task::ready(None);
10032                };
10033
10034                this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
10035            })
10036            .await;
10037    }
10038    let mut project_paths_to_open = vec![];
10039    let mut project_path_errors = vec![];
10040
10041    for path in paths {
10042        let result = cx
10043            .update(|cx| {
10044                Workspace::project_path_for_path(project.clone(), path.as_path(), true, cx)
10045            })
10046            .await;
10047        match result {
10048            Ok((_, project_path)) => {
10049                project_paths_to_open.push((path, Some(project_path)));
10050            }
10051            Err(error) => {
10052                project_path_errors.push(error);
10053            }
10054        };
10055    }
10056
10057    if project_paths_to_open.is_empty() {
10058        return Err(project_path_errors.pop().context("no paths given")?);
10059    }
10060
10061    let workspace = window.update(cx, |multi_workspace, window, cx| {
10062        telemetry::event!("SSH Project Opened");
10063
10064        let new_workspace = cx.new(|cx| {
10065            let mut workspace =
10066                Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
10067            workspace.update_history(cx);
10068
10069            if let Some(ref serialized) = serialized_workspace {
10070                workspace.centered_layout = serialized.centered_layout;
10071            }
10072
10073            workspace
10074        });
10075
10076        if let Some(project_group_key) = provisional_project_group_key.clone() {
10077            multi_workspace.activate_provisional_workspace(
10078                new_workspace.clone(),
10079                project_group_key,
10080                window,
10081                cx,
10082            );
10083        } else {
10084            multi_workspace.activate(new_workspace.clone(), source_workspace, window, cx);
10085        }
10086        new_workspace
10087    })?;
10088
10089    let items = window
10090        .update(cx, |_, window, cx| {
10091            window.activate_window();
10092            workspace.update(cx, |_workspace, cx| {
10093                open_items(serialized_workspace, project_paths_to_open, window, cx)
10094            })
10095        })?
10096        .await?;
10097
10098    workspace.update(cx, |workspace, cx| {
10099        for error in project_path_errors {
10100            if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
10101                if let Some(path) = error.error_tag("path") {
10102                    workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
10103                }
10104            } else {
10105                workspace.show_error(&error, cx)
10106            }
10107        }
10108    });
10109
10110    Ok(items.into_iter().map(|item| item?.ok()).collect())
10111}
10112
10113fn deserialize_remote_project(
10114    connection_options: RemoteConnectionOptions,
10115    paths: Vec<PathBuf>,
10116    cx: &AsyncApp,
10117) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
10118    let db = cx.update(|cx| WorkspaceDb::global(cx));
10119    cx.background_spawn(async move {
10120        let remote_connection_id = db
10121            .get_or_create_remote_connection(connection_options)
10122            .await?;
10123
10124        let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
10125
10126        let workspace_id = if let Some(workspace_id) =
10127            serialized_workspace.as_ref().map(|workspace| workspace.id)
10128        {
10129            workspace_id
10130        } else {
10131            db.next_id().await?
10132        };
10133
10134        Ok((workspace_id, serialized_workspace))
10135    })
10136}
10137
10138pub fn join_in_room_project(
10139    project_id: u64,
10140    follow_user_id: u64,
10141    app_state: Arc<AppState>,
10142    cx: &mut App,
10143) -> Task<Result<()>> {
10144    let windows = cx.windows();
10145    cx.spawn(async move |cx| {
10146        let existing_window_and_workspace: Option<(
10147            WindowHandle<MultiWorkspace>,
10148            Entity<Workspace>,
10149        )> = windows.into_iter().find_map(|window_handle| {
10150            window_handle
10151                .downcast::<MultiWorkspace>()
10152                .and_then(|window_handle| {
10153                    window_handle
10154                        .update(cx, |multi_workspace, _window, cx| {
10155                            multi_workspace
10156                                .workspaces()
10157                                .find(|workspace| {
10158                                    workspace.read(cx).project().read(cx).remote_id()
10159                                        == Some(project_id)
10160                                })
10161                                .map(|workspace| (window_handle, workspace.clone()))
10162                        })
10163                        .unwrap_or(None)
10164                })
10165        });
10166
10167        let multi_workspace_window = if let Some((existing_window, target_workspace)) =
10168            existing_window_and_workspace
10169        {
10170            existing_window
10171                .update(cx, |multi_workspace, window, cx| {
10172                    multi_workspace.activate(target_workspace, None, window, cx);
10173                })
10174                .ok();
10175            existing_window
10176        } else {
10177            let active_call = cx.update(|cx| GlobalAnyActiveCall::global(cx).clone());
10178            let project = cx
10179                .update(|cx| {
10180                    active_call.0.join_project(
10181                        project_id,
10182                        app_state.languages.clone(),
10183                        app_state.fs.clone(),
10184                        cx,
10185                    )
10186                })
10187                .await?;
10188
10189            let window_bounds_override = window_bounds_env_override();
10190            cx.update(|cx| {
10191                let mut options = (app_state.build_window_options)(None, cx);
10192                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
10193                cx.open_window(options, |window, cx| {
10194                    let workspace = cx.new(|cx| {
10195                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
10196                    });
10197                    cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
10198                })
10199            })?
10200        };
10201
10202        multi_workspace_window.update(cx, |multi_workspace, window, cx| {
10203            cx.activate(true);
10204            window.activate_window();
10205
10206            // We set the active workspace above, so this is the correct workspace.
10207            let workspace = multi_workspace.workspace().clone();
10208            workspace.update(cx, |workspace, cx| {
10209                let follow_peer_id = GlobalAnyActiveCall::try_global(cx)
10210                    .and_then(|call| call.0.peer_id_for_user_in_room(follow_user_id, cx))
10211                    .or_else(|| {
10212                        // If we couldn't follow the given user, follow the host instead.
10213                        let collaborator = workspace
10214                            .project()
10215                            .read(cx)
10216                            .collaborators()
10217                            .values()
10218                            .find(|collaborator| collaborator.is_host)?;
10219                        Some(collaborator.peer_id)
10220                    });
10221
10222                if let Some(follow_peer_id) = follow_peer_id {
10223                    workspace.follow(follow_peer_id, window, cx);
10224                }
10225            });
10226        })?;
10227
10228        anyhow::Ok(())
10229    })
10230}
10231
10232pub fn reload(cx: &mut App) {
10233    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
10234    let mut workspace_windows = cx
10235        .windows()
10236        .into_iter()
10237        .filter_map(|window| window.downcast::<MultiWorkspace>())
10238        .collect::<Vec<_>>();
10239
10240    // If multiple windows have unsaved changes, and need a save prompt,
10241    // prompt in the active window before switching to a different window.
10242    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
10243
10244    let mut prompt = None;
10245    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
10246        prompt = window
10247            .update(cx, |_, window, cx| {
10248                window.prompt(
10249                    PromptLevel::Info,
10250                    "Are you sure you want to restart?",
10251                    None,
10252                    &["Restart", "Cancel"],
10253                    cx,
10254                )
10255            })
10256            .ok();
10257    }
10258
10259    cx.spawn(async move |cx| {
10260        if let Some(prompt) = prompt {
10261            let answer = prompt.await?;
10262            if answer != 0 {
10263                return anyhow::Ok(());
10264            }
10265        }
10266
10267        // If the user cancels any save prompt, then keep the app open.
10268        for window in workspace_windows {
10269            if let Ok(should_close) = window.update(cx, |multi_workspace, window, cx| {
10270                let workspace = multi_workspace.workspace().clone();
10271                workspace.update(cx, |workspace, cx| {
10272                    workspace.prepare_to_close(CloseIntent::Quit, window, cx)
10273                })
10274            }) && !should_close.await?
10275            {
10276                return anyhow::Ok(());
10277            }
10278        }
10279        cx.update(|cx| cx.restart());
10280        anyhow::Ok(())
10281    })
10282    .detach_and_log_err(cx);
10283}
10284
10285fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
10286    let mut parts = value.split(',');
10287    let x: usize = parts.next()?.parse().ok()?;
10288    let y: usize = parts.next()?.parse().ok()?;
10289    Some(point(px(x as f32), px(y as f32)))
10290}
10291
10292fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
10293    let mut parts = value.split(',');
10294    let width: usize = parts.next()?.parse().ok()?;
10295    let height: usize = parts.next()?.parse().ok()?;
10296    Some(size(px(width as f32), px(height as f32)))
10297}
10298
10299/// Add client-side decorations (rounded corners, shadows, resize handling) when
10300/// appropriate.
10301///
10302/// The `border_radius_tiling` parameter allows overriding which corners get
10303/// rounded, independently of the actual window tiling state. This is used
10304/// specifically for the workspace switcher sidebar: when the sidebar is open,
10305/// we want square corners on the left (so the sidebar appears flush with the
10306/// window edge) but we still need the shadow padding for proper visual
10307/// appearance. Unlike actual window tiling, this only affects border radius -
10308/// not padding or shadows.
10309pub fn client_side_decorations(
10310    element: impl IntoElement,
10311    window: &mut Window,
10312    cx: &mut App,
10313    border_radius_tiling: Tiling,
10314) -> Stateful<Div> {
10315    const BORDER_SIZE: Pixels = px(1.0);
10316    let decorations = window.window_decorations();
10317    let tiling = match decorations {
10318        Decorations::Server => Tiling::default(),
10319        Decorations::Client { tiling } => tiling,
10320    };
10321
10322    match decorations {
10323        Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW),
10324        Decorations::Server => window.set_client_inset(px(0.0)),
10325    }
10326
10327    struct GlobalResizeEdge(ResizeEdge);
10328    impl Global for GlobalResizeEdge {}
10329
10330    div()
10331        .id("window-backdrop")
10332        .bg(transparent_black())
10333        .map(|div| match decorations {
10334            Decorations::Server => div,
10335            Decorations::Client { .. } => div
10336                .when(
10337                    !(tiling.top
10338                        || tiling.right
10339                        || border_radius_tiling.top
10340                        || border_radius_tiling.right),
10341                    |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10342                )
10343                .when(
10344                    !(tiling.top
10345                        || tiling.left
10346                        || border_radius_tiling.top
10347                        || border_radius_tiling.left),
10348                    |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10349                )
10350                .when(
10351                    !(tiling.bottom
10352                        || tiling.right
10353                        || border_radius_tiling.bottom
10354                        || border_radius_tiling.right),
10355                    |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10356                )
10357                .when(
10358                    !(tiling.bottom
10359                        || tiling.left
10360                        || border_radius_tiling.bottom
10361                        || border_radius_tiling.left),
10362                    |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10363                )
10364                .when(!tiling.top, |div| {
10365                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
10366                })
10367                .when(!tiling.bottom, |div| {
10368                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
10369                })
10370                .when(!tiling.left, |div| {
10371                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
10372                })
10373                .when(!tiling.right, |div| {
10374                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
10375                })
10376                .on_mouse_move(move |e, window, cx| {
10377                    let size = window.window_bounds().get_bounds().size;
10378                    let pos = e.position;
10379
10380                    let new_edge =
10381                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
10382
10383                    let edge = cx.try_global::<GlobalResizeEdge>();
10384                    if new_edge != edge.map(|edge| edge.0) {
10385                        window
10386                            .window_handle()
10387                            .update(cx, |workspace, _, cx| {
10388                                cx.notify(workspace.entity_id());
10389                            })
10390                            .ok();
10391                    }
10392                })
10393                .on_mouse_down(MouseButton::Left, move |e, window, _| {
10394                    let size = window.window_bounds().get_bounds().size;
10395                    let pos = e.position;
10396
10397                    let edge = match resize_edge(
10398                        pos,
10399                        theme::CLIENT_SIDE_DECORATION_SHADOW,
10400                        size,
10401                        tiling,
10402                    ) {
10403                        Some(value) => value,
10404                        None => return,
10405                    };
10406
10407                    window.start_window_resize(edge);
10408                }),
10409        })
10410        .size_full()
10411        .child(
10412            div()
10413                .cursor(CursorStyle::Arrow)
10414                .map(|div| match decorations {
10415                    Decorations::Server => div,
10416                    Decorations::Client { .. } => div
10417                        .border_color(cx.theme().colors().border)
10418                        .when(
10419                            !(tiling.top
10420                                || tiling.right
10421                                || border_radius_tiling.top
10422                                || border_radius_tiling.right),
10423                            |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10424                        )
10425                        .when(
10426                            !(tiling.top
10427                                || tiling.left
10428                                || border_radius_tiling.top
10429                                || border_radius_tiling.left),
10430                            |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10431                        )
10432                        .when(
10433                            !(tiling.bottom
10434                                || tiling.right
10435                                || border_radius_tiling.bottom
10436                                || border_radius_tiling.right),
10437                            |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10438                        )
10439                        .when(
10440                            !(tiling.bottom
10441                                || tiling.left
10442                                || border_radius_tiling.bottom
10443                                || border_radius_tiling.left),
10444                            |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
10445                        )
10446                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
10447                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
10448                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
10449                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
10450                        .when(!tiling.is_tiled(), |div| {
10451                            div.shadow(vec![gpui::BoxShadow {
10452                                color: Hsla {
10453                                    h: 0.,
10454                                    s: 0.,
10455                                    l: 0.,
10456                                    a: 0.4,
10457                                },
10458                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
10459                                spread_radius: px(0.),
10460                                offset: point(px(0.0), px(0.0)),
10461                            }])
10462                        }),
10463                })
10464                .on_mouse_move(|_e, _, cx| {
10465                    cx.stop_propagation();
10466                })
10467                .size_full()
10468                .child(element),
10469        )
10470        .map(|div| match decorations {
10471            Decorations::Server => div,
10472            Decorations::Client { tiling, .. } => div.child(
10473                canvas(
10474                    |_bounds, window, _| {
10475                        window.insert_hitbox(
10476                            Bounds::new(
10477                                point(px(0.0), px(0.0)),
10478                                window.window_bounds().get_bounds().size,
10479                            ),
10480                            HitboxBehavior::Normal,
10481                        )
10482                    },
10483                    move |_bounds, hitbox, window, cx| {
10484                        let mouse = window.mouse_position();
10485                        let size = window.window_bounds().get_bounds().size;
10486                        let Some(edge) =
10487                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
10488                        else {
10489                            return;
10490                        };
10491                        cx.set_global(GlobalResizeEdge(edge));
10492                        window.set_cursor_style(
10493                            match edge {
10494                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
10495                                ResizeEdge::Left | ResizeEdge::Right => {
10496                                    CursorStyle::ResizeLeftRight
10497                                }
10498                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
10499                                    CursorStyle::ResizeUpLeftDownRight
10500                                }
10501                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
10502                                    CursorStyle::ResizeUpRightDownLeft
10503                                }
10504                            },
10505                            &hitbox,
10506                        );
10507                    },
10508                )
10509                .size_full()
10510                .absolute(),
10511            ),
10512        })
10513}
10514
10515fn resize_edge(
10516    pos: Point<Pixels>,
10517    shadow_size: Pixels,
10518    window_size: Size<Pixels>,
10519    tiling: Tiling,
10520) -> Option<ResizeEdge> {
10521    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
10522    if bounds.contains(&pos) {
10523        return None;
10524    }
10525
10526    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
10527    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
10528    if !tiling.top && top_left_bounds.contains(&pos) {
10529        return Some(ResizeEdge::TopLeft);
10530    }
10531
10532    let top_right_bounds = Bounds::new(
10533        Point::new(window_size.width - corner_size.width, px(0.)),
10534        corner_size,
10535    );
10536    if !tiling.top && top_right_bounds.contains(&pos) {
10537        return Some(ResizeEdge::TopRight);
10538    }
10539
10540    let bottom_left_bounds = Bounds::new(
10541        Point::new(px(0.), window_size.height - corner_size.height),
10542        corner_size,
10543    );
10544    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
10545        return Some(ResizeEdge::BottomLeft);
10546    }
10547
10548    let bottom_right_bounds = Bounds::new(
10549        Point::new(
10550            window_size.width - corner_size.width,
10551            window_size.height - corner_size.height,
10552        ),
10553        corner_size,
10554    );
10555    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
10556        return Some(ResizeEdge::BottomRight);
10557    }
10558
10559    if !tiling.top && pos.y < shadow_size {
10560        Some(ResizeEdge::Top)
10561    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
10562        Some(ResizeEdge::Bottom)
10563    } else if !tiling.left && pos.x < shadow_size {
10564        Some(ResizeEdge::Left)
10565    } else if !tiling.right && pos.x > window_size.width - shadow_size {
10566        Some(ResizeEdge::Right)
10567    } else {
10568        None
10569    }
10570}
10571
10572fn join_pane_into_active(
10573    active_pane: &Entity<Pane>,
10574    pane: &Entity<Pane>,
10575    window: &mut Window,
10576    cx: &mut App,
10577) {
10578    if pane == active_pane {
10579    } else if pane.read(cx).items_len() == 0 {
10580        pane.update(cx, |_, cx| {
10581            cx.emit(pane::Event::Remove {
10582                focus_on_pane: None,
10583            });
10584        })
10585    } else {
10586        move_all_items(pane, active_pane, window, cx);
10587    }
10588}
10589
10590fn move_all_items(
10591    from_pane: &Entity<Pane>,
10592    to_pane: &Entity<Pane>,
10593    window: &mut Window,
10594    cx: &mut App,
10595) {
10596    let destination_is_different = from_pane != to_pane;
10597    let mut moved_items = 0;
10598    for (item_ix, item_handle) in from_pane
10599        .read(cx)
10600        .items()
10601        .enumerate()
10602        .map(|(ix, item)| (ix, item.clone()))
10603        .collect::<Vec<_>>()
10604    {
10605        let ix = item_ix - moved_items;
10606        if destination_is_different {
10607            // Close item from previous pane
10608            from_pane.update(cx, |source, cx| {
10609                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
10610            });
10611            moved_items += 1;
10612        }
10613
10614        // This automatically removes duplicate items in the pane
10615        to_pane.update(cx, |destination, cx| {
10616            destination.add_item(item_handle, true, true, None, window, cx);
10617            window.focus(&destination.focus_handle(cx), cx)
10618        });
10619    }
10620}
10621
10622pub fn move_item(
10623    source: &Entity<Pane>,
10624    destination: &Entity<Pane>,
10625    item_id_to_move: EntityId,
10626    destination_index: usize,
10627    activate: bool,
10628    window: &mut Window,
10629    cx: &mut App,
10630) {
10631    let Some((item_ix, item_handle)) = source
10632        .read(cx)
10633        .items()
10634        .enumerate()
10635        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
10636        .map(|(ix, item)| (ix, item.clone()))
10637    else {
10638        // Tab was closed during drag
10639        return;
10640    };
10641
10642    if source != destination {
10643        // Close item from previous pane
10644        source.update(cx, |source, cx| {
10645            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
10646        });
10647    }
10648
10649    // This automatically removes duplicate items in the pane
10650    destination.update(cx, |destination, cx| {
10651        destination.add_item_inner(
10652            item_handle,
10653            activate,
10654            activate,
10655            activate,
10656            Some(destination_index),
10657            window,
10658            cx,
10659        );
10660        if activate {
10661            window.focus(&destination.focus_handle(cx), cx)
10662        }
10663    });
10664}
10665
10666pub fn move_active_item(
10667    source: &Entity<Pane>,
10668    destination: &Entity<Pane>,
10669    focus_destination: bool,
10670    close_if_empty: bool,
10671    window: &mut Window,
10672    cx: &mut App,
10673) {
10674    if source == destination {
10675        return;
10676    }
10677    let Some(active_item) = source.read(cx).active_item() else {
10678        return;
10679    };
10680    source.update(cx, |source_pane, cx| {
10681        let item_id = active_item.item_id();
10682        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
10683        destination.update(cx, |target_pane, cx| {
10684            target_pane.add_item(
10685                active_item,
10686                focus_destination,
10687                focus_destination,
10688                Some(target_pane.items_len()),
10689                window,
10690                cx,
10691            );
10692        });
10693    });
10694}
10695
10696pub fn clone_active_item(
10697    workspace_id: Option<WorkspaceId>,
10698    source: &Entity<Pane>,
10699    destination: &Entity<Pane>,
10700    focus_destination: bool,
10701    window: &mut Window,
10702    cx: &mut App,
10703) {
10704    if source == destination {
10705        return;
10706    }
10707    let Some(active_item) = source.read(cx).active_item() else {
10708        return;
10709    };
10710    if !active_item.can_split(cx) {
10711        return;
10712    }
10713    let destination = destination.downgrade();
10714    let task = active_item.clone_on_split(workspace_id, window, cx);
10715    window
10716        .spawn(cx, async move |cx| {
10717            let Some(clone) = task.await else {
10718                return;
10719            };
10720            destination
10721                .update_in(cx, |target_pane, window, cx| {
10722                    target_pane.add_item(
10723                        clone,
10724                        focus_destination,
10725                        focus_destination,
10726                        Some(target_pane.items_len()),
10727                        window,
10728                        cx,
10729                    );
10730                })
10731                .log_err();
10732        })
10733        .detach();
10734}
10735
10736#[derive(Debug)]
10737pub struct WorkspacePosition {
10738    pub window_bounds: Option<WindowBounds>,
10739    pub display: Option<Uuid>,
10740    pub centered_layout: bool,
10741}
10742
10743pub fn remote_workspace_position_from_db(
10744    connection_options: RemoteConnectionOptions,
10745    paths_to_open: &[PathBuf],
10746    cx: &App,
10747) -> Task<Result<WorkspacePosition>> {
10748    let paths = paths_to_open.to_vec();
10749    let db = WorkspaceDb::global(cx);
10750    let kvp = db::kvp::KeyValueStore::global(cx);
10751
10752    cx.background_spawn(async move {
10753        let remote_connection_id = db
10754            .get_or_create_remote_connection(connection_options)
10755            .await
10756            .context("fetching serialized ssh project")?;
10757        let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
10758
10759        let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
10760            (Some(WindowBounds::Windowed(bounds)), None)
10761        } else {
10762            let restorable_bounds = serialized_workspace
10763                .as_ref()
10764                .and_then(|workspace| {
10765                    Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
10766                })
10767                .or_else(|| persistence::read_default_window_bounds(&kvp));
10768
10769            if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
10770                (Some(serialized_bounds), Some(serialized_display))
10771            } else {
10772                (None, None)
10773            }
10774        };
10775
10776        let centered_layout = serialized_workspace
10777            .as_ref()
10778            .map(|w| w.centered_layout)
10779            .unwrap_or(false);
10780
10781        Ok(WorkspacePosition {
10782            window_bounds,
10783            display,
10784            centered_layout,
10785        })
10786    })
10787}
10788
10789pub fn with_active_or_new_workspace(
10790    cx: &mut App,
10791    f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
10792) {
10793    match cx
10794        .active_window()
10795        .and_then(|w| w.downcast::<MultiWorkspace>())
10796    {
10797        Some(multi_workspace) => {
10798            cx.defer(move |cx| {
10799                multi_workspace
10800                    .update(cx, |multi_workspace, window, cx| {
10801                        let workspace = multi_workspace.workspace().clone();
10802                        workspace.update(cx, |workspace, cx| f(workspace, window, cx));
10803                    })
10804                    .log_err();
10805            });
10806        }
10807        None => {
10808            let app_state = AppState::global(cx);
10809            open_new(
10810                OpenOptions::default(),
10811                app_state,
10812                cx,
10813                move |workspace, window, cx| f(workspace, window, cx),
10814            )
10815            .detach_and_log_err(cx);
10816        }
10817    }
10818}
10819
10820/// Reads a panel's pixel size from its legacy KVP format and deletes the legacy
10821/// key. This migration path only runs once per panel per workspace.
10822fn load_legacy_panel_size(
10823    panel_key: &str,
10824    dock_position: DockPosition,
10825    workspace: &Workspace,
10826    cx: &mut App,
10827) -> Option<Pixels> {
10828    #[derive(Deserialize)]
10829    struct LegacyPanelState {
10830        #[serde(default)]
10831        width: Option<Pixels>,
10832        #[serde(default)]
10833        height: Option<Pixels>,
10834    }
10835
10836    let workspace_id = workspace
10837        .database_id()
10838        .map(|id| i64::from(id).to_string())
10839        .or_else(|| workspace.session_id())?;
10840
10841    let legacy_key = match panel_key {
10842        "ProjectPanel" => {
10843            format!("{}-{:?}", "ProjectPanel", workspace_id)
10844        }
10845        "OutlinePanel" => {
10846            format!("{}-{:?}", "OutlinePanel", workspace_id)
10847        }
10848        "GitPanel" => {
10849            format!("{}-{:?}", "GitPanel", workspace_id)
10850        }
10851        "TerminalPanel" => {
10852            format!("{:?}-{:?}", "TerminalPanel", workspace_id)
10853        }
10854        _ => return None,
10855    };
10856
10857    let kvp = db::kvp::KeyValueStore::global(cx);
10858    let json = kvp.read_kvp(&legacy_key).log_err().flatten()?;
10859    let state = serde_json::from_str::<LegacyPanelState>(&json).log_err()?;
10860    let size = match dock_position {
10861        DockPosition::Bottom => state.height,
10862        DockPosition::Left | DockPosition::Right => state.width,
10863    }?;
10864
10865    cx.background_spawn(async move { kvp.delete_kvp(legacy_key).await })
10866        .detach_and_log_err(cx);
10867
10868    Some(size)
10869}
10870
10871#[cfg(test)]
10872mod tests {
10873    use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
10874
10875    use super::*;
10876    use crate::{
10877        dock::{PanelEvent, test::TestPanel},
10878        item::{
10879            ItemBufferKind, ItemEvent,
10880            test::{TestItem, TestProjectItem},
10881        },
10882    };
10883    use fs::FakeFs;
10884    use gpui::{
10885        DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
10886        UpdateGlobal, VisualTestContext, px,
10887    };
10888    use project::{Project, ProjectEntryId};
10889    use serde_json::json;
10890    use settings::SettingsStore;
10891    use util::path;
10892    use util::rel_path::rel_path;
10893
10894    #[gpui::test]
10895    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
10896        init_test(cx);
10897
10898        let fs = FakeFs::new(cx.executor());
10899        let project = Project::test(fs, [], cx).await;
10900        let (workspace, cx) =
10901            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10902
10903        // Adding an item with no ambiguity renders the tab without detail.
10904        let item1 = cx.new(|cx| {
10905            let mut item = TestItem::new(cx);
10906            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
10907            item
10908        });
10909        workspace.update_in(cx, |workspace, window, cx| {
10910            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10911        });
10912        item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
10913
10914        // Adding an item that creates ambiguity increases the level of detail on
10915        // both tabs.
10916        let item2 = cx.new_window_entity(|_window, cx| {
10917            let mut item = TestItem::new(cx);
10918            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10919            item
10920        });
10921        workspace.update_in(cx, |workspace, window, cx| {
10922            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10923        });
10924        item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10925        item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10926
10927        // Adding an item that creates ambiguity increases the level of detail only
10928        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
10929        // we stop at the highest detail available.
10930        let item3 = cx.new(|cx| {
10931            let mut item = TestItem::new(cx);
10932            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10933            item
10934        });
10935        workspace.update_in(cx, |workspace, window, cx| {
10936            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10937        });
10938        item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10939        item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10940        item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10941    }
10942
10943    #[gpui::test]
10944    async fn test_tracking_active_path(cx: &mut TestAppContext) {
10945        init_test(cx);
10946
10947        let fs = FakeFs::new(cx.executor());
10948        fs.insert_tree(
10949            "/root1",
10950            json!({
10951                "one.txt": "",
10952                "two.txt": "",
10953            }),
10954        )
10955        .await;
10956        fs.insert_tree(
10957            "/root2",
10958            json!({
10959                "three.txt": "",
10960            }),
10961        )
10962        .await;
10963
10964        let project = Project::test(fs, ["root1".as_ref()], cx).await;
10965        let (workspace, cx) =
10966            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10967        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10968        let worktree_id = project.update(cx, |project, cx| {
10969            project.worktrees(cx).next().unwrap().read(cx).id()
10970        });
10971
10972        let item1 = cx.new(|cx| {
10973            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
10974        });
10975        let item2 = cx.new(|cx| {
10976            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
10977        });
10978
10979        // Add an item to an empty pane
10980        workspace.update_in(cx, |workspace, window, cx| {
10981            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
10982        });
10983        project.update(cx, |project, cx| {
10984            assert_eq!(
10985                project.active_entry(),
10986                project
10987                    .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10988                    .map(|e| e.id)
10989            );
10990        });
10991        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10992
10993        // Add a second item to a non-empty pane
10994        workspace.update_in(cx, |workspace, window, cx| {
10995            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
10996        });
10997        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
10998        project.update(cx, |project, cx| {
10999            assert_eq!(
11000                project.active_entry(),
11001                project
11002                    .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
11003                    .map(|e| e.id)
11004            );
11005        });
11006
11007        // Close the active item
11008        pane.update_in(cx, |pane, window, cx| {
11009            pane.close_active_item(&Default::default(), window, cx)
11010        })
11011        .await
11012        .unwrap();
11013        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
11014        project.update(cx, |project, cx| {
11015            assert_eq!(
11016                project.active_entry(),
11017                project
11018                    .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
11019                    .map(|e| e.id)
11020            );
11021        });
11022
11023        // Add a project folder
11024        project
11025            .update(cx, |project, cx| {
11026                project.find_or_create_worktree("root2", true, cx)
11027            })
11028            .await
11029            .unwrap();
11030        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
11031
11032        // Remove a project folder
11033        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
11034        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
11035    }
11036
11037    #[gpui::test]
11038    async fn test_close_window(cx: &mut TestAppContext) {
11039        init_test(cx);
11040
11041        let fs = FakeFs::new(cx.executor());
11042        fs.insert_tree("/root", json!({ "one": "" })).await;
11043
11044        let project = Project::test(fs, ["root".as_ref()], cx).await;
11045        let (workspace, cx) =
11046            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
11047
11048        // When there are no dirty items, there's nothing to do.
11049        let item1 = cx.new(TestItem::new);
11050        workspace.update_in(cx, |w, window, cx| {
11051            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
11052        });
11053        let task = workspace.update_in(cx, |w, window, cx| {
11054            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
11055        });
11056        assert!(task.await.unwrap());
11057
11058        // When there are dirty untitled items, prompt to save each one. If the user
11059        // cancels any prompt, then abort.
11060        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
11061        let item3 = cx.new(|cx| {
11062            TestItem::new(cx)
11063                .with_dirty(true)
11064                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11065        });
11066        workspace.update_in(cx, |w, window, cx| {
11067            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11068            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
11069        });
11070        let task = workspace.update_in(cx, |w, window, cx| {
11071            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
11072        });
11073        cx.executor().run_until_parked();
11074        cx.simulate_prompt_answer("Cancel"); // cancel save all
11075        cx.executor().run_until_parked();
11076        assert!(!cx.has_pending_prompt());
11077        assert!(!task.await.unwrap());
11078    }
11079
11080    #[gpui::test]
11081    async fn test_multi_workspace_close_window_multiple_workspaces_cancel(cx: &mut TestAppContext) {
11082        init_test(cx);
11083
11084        let fs = FakeFs::new(cx.executor());
11085        fs.insert_tree("/root", json!({ "one": "" })).await;
11086
11087        let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
11088        let project_b = Project::test(fs, ["root".as_ref()], cx).await;
11089        let multi_workspace_handle =
11090            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
11091        cx.run_until_parked();
11092
11093        multi_workspace_handle
11094            .update(cx, |mw, _window, cx| {
11095                mw.open_sidebar(cx);
11096            })
11097            .unwrap();
11098
11099        let workspace_a = multi_workspace_handle
11100            .read_with(cx, |mw, _| mw.workspace().clone())
11101            .unwrap();
11102
11103        let workspace_b = multi_workspace_handle
11104            .update(cx, |mw, window, cx| {
11105                mw.test_add_workspace(project_b, window, cx)
11106            })
11107            .unwrap();
11108
11109        // Activate workspace A
11110        multi_workspace_handle
11111            .update(cx, |mw, window, cx| {
11112                mw.activate(workspace_a.clone(), None, window, cx);
11113            })
11114            .unwrap();
11115
11116        let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
11117
11118        // Workspace A has a clean item
11119        let item_a = cx.new(TestItem::new);
11120        workspace_a.update_in(cx, |w, window, cx| {
11121            w.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
11122        });
11123
11124        // Workspace B has a dirty item
11125        let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
11126        workspace_b.update_in(cx, |w, window, cx| {
11127            w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
11128        });
11129
11130        // Verify workspace A is active
11131        multi_workspace_handle
11132            .read_with(cx, |mw, _| {
11133                assert_eq!(mw.workspace(), &workspace_a);
11134            })
11135            .unwrap();
11136
11137        // Dispatch CloseWindow — workspace A will pass, workspace B will prompt
11138        multi_workspace_handle
11139            .update(cx, |mw, window, cx| {
11140                mw.close_window(&CloseWindow, window, cx);
11141            })
11142            .unwrap();
11143        cx.run_until_parked();
11144
11145        // Workspace B should now be active since it has dirty items that need attention
11146        multi_workspace_handle
11147            .read_with(cx, |mw, _| {
11148                assert_eq!(
11149                    mw.workspace(),
11150                    &workspace_b,
11151                    "workspace B should be activated when it prompts"
11152                );
11153            })
11154            .unwrap();
11155
11156        // User cancels the save prompt from workspace B
11157        cx.simulate_prompt_answer("Cancel");
11158        cx.run_until_parked();
11159
11160        // Window should still exist because workspace B's close was cancelled
11161        assert!(
11162            multi_workspace_handle.update(cx, |_, _, _| ()).is_ok(),
11163            "window should still exist after cancelling one workspace's close"
11164        );
11165    }
11166
11167    #[gpui::test]
11168    async fn test_remove_workspace_prompts_for_unsaved_changes(cx: &mut TestAppContext) {
11169        init_test(cx);
11170
11171        let fs = FakeFs::new(cx.executor());
11172        fs.insert_tree("/root", json!({ "one": "" })).await;
11173
11174        let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
11175        let project_b = Project::test(fs.clone(), ["root".as_ref()], cx).await;
11176        let multi_workspace_handle =
11177            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
11178        cx.run_until_parked();
11179
11180        multi_workspace_handle
11181            .update(cx, |mw, _window, cx| mw.open_sidebar(cx))
11182            .unwrap();
11183
11184        let workspace_a = multi_workspace_handle
11185            .read_with(cx, |mw, _| mw.workspace().clone())
11186            .unwrap();
11187
11188        let workspace_b = multi_workspace_handle
11189            .update(cx, |mw, window, cx| {
11190                mw.test_add_workspace(project_b, window, cx)
11191            })
11192            .unwrap();
11193
11194        // Activate workspace A.
11195        multi_workspace_handle
11196            .update(cx, |mw, window, cx| {
11197                mw.activate(workspace_a.clone(), None, window, cx);
11198            })
11199            .unwrap();
11200
11201        let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
11202
11203        // Workspace B has a dirty item.
11204        let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
11205        workspace_b.update_in(cx, |w, window, cx| {
11206            w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
11207        });
11208
11209        // Try to remove workspace B. It should prompt because of the dirty item.
11210        let remove_task = multi_workspace_handle
11211            .update(cx, |mw, window, cx| {
11212                mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
11213            })
11214            .unwrap();
11215        cx.run_until_parked();
11216
11217        // The prompt should have activated workspace B.
11218        multi_workspace_handle
11219            .read_with(cx, |mw, _| {
11220                assert_eq!(
11221                    mw.workspace(),
11222                    &workspace_b,
11223                    "workspace B should be active while prompting"
11224                );
11225            })
11226            .unwrap();
11227
11228        // Cancel the prompt — user stays on workspace B.
11229        cx.simulate_prompt_answer("Cancel");
11230        cx.run_until_parked();
11231        let removed = remove_task.await.unwrap();
11232        assert!(!removed, "removal should have been cancelled");
11233
11234        multi_workspace_handle
11235            .read_with(cx, |mw, _cx| {
11236                assert_eq!(
11237                    mw.workspace(),
11238                    &workspace_b,
11239                    "user should stay on workspace B after cancelling"
11240                );
11241                assert_eq!(mw.workspaces().count(), 2, "both workspaces should remain");
11242            })
11243            .unwrap();
11244
11245        // Try again. This time accept the prompt.
11246        let remove_task = multi_workspace_handle
11247            .update(cx, |mw, window, cx| {
11248                // First switch back to A.
11249                mw.activate(workspace_a.clone(), None, window, cx);
11250                mw.remove([workspace_b.clone()], |_, _, _| unreachable!(), window, cx)
11251            })
11252            .unwrap();
11253        cx.run_until_parked();
11254
11255        // Accept the save prompt.
11256        cx.simulate_prompt_answer("Don't Save");
11257        cx.run_until_parked();
11258        let removed = remove_task.await.unwrap();
11259        assert!(removed, "removal should have succeeded");
11260
11261        // Should be back on workspace A, and B should be gone.
11262        multi_workspace_handle
11263            .read_with(cx, |mw, _cx| {
11264                assert_eq!(
11265                    mw.workspace(),
11266                    &workspace_a,
11267                    "should be back on workspace A after removing B"
11268                );
11269                assert_eq!(mw.workspaces().count(), 1, "only workspace A should remain");
11270            })
11271            .unwrap();
11272    }
11273
11274    #[gpui::test]
11275    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
11276        init_test(cx);
11277
11278        // Register TestItem as a serializable item
11279        cx.update(|cx| {
11280            register_serializable_item::<TestItem>(cx);
11281        });
11282
11283        let fs = FakeFs::new(cx.executor());
11284        fs.insert_tree("/root", json!({ "one": "" })).await;
11285
11286        let project = Project::test(fs, ["root".as_ref()], cx).await;
11287        let (workspace, cx) =
11288            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
11289
11290        // When there are dirty untitled items, but they can serialize, then there is no prompt.
11291        let item1 = cx.new(|cx| {
11292            TestItem::new(cx)
11293                .with_dirty(true)
11294                .with_serialize(|| Some(Task::ready(Ok(()))))
11295        });
11296        let item2 = cx.new(|cx| {
11297            TestItem::new(cx)
11298                .with_dirty(true)
11299                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11300                .with_serialize(|| Some(Task::ready(Ok(()))))
11301        });
11302        workspace.update_in(cx, |w, window, cx| {
11303            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
11304            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11305        });
11306        let task = workspace.update_in(cx, |w, window, cx| {
11307            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
11308        });
11309        assert!(task.await.unwrap());
11310    }
11311
11312    #[gpui::test]
11313    async fn test_close_window_with_failing_serialization(cx: &mut TestAppContext) {
11314        init_test(cx);
11315
11316        cx.update(|cx| {
11317            register_serializable_item::<TestItem>(cx);
11318        });
11319
11320        let fs = FakeFs::new(cx.executor());
11321        let project = Project::test(fs, None, cx).await;
11322        let (workspace, cx) =
11323            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
11324
11325        let item = cx.new(|cx| {
11326            TestItem::new(cx).with_dirty(true).with_serialize(|| {
11327                Some(Task::ready(Err(anyhow::anyhow!(
11328                    "FOREIGN KEY constraint failed"
11329                ))))
11330            })
11331        });
11332        workspace.update_in(cx, |w, window, cx| {
11333            w.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11334        });
11335
11336        let task = workspace.update_in(cx, |w, window, cx| {
11337            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
11338        });
11339        cx.executor().run_until_parked();
11340
11341        // The failing serialization must not short-circuit the close; a
11342        // save/discard prompt must be shown for the dirty scratch item.
11343        assert!(
11344            cx.has_pending_prompt(),
11345            "a save/discard prompt should be shown for the dirty scratch item \
11346             when its serialization fails"
11347        );
11348        cx.simulate_prompt_answer("Don't Save");
11349        cx.executor().run_until_parked();
11350
11351        // Preparing to close succeeds, even though serialization failed.
11352        assert!(task.await.unwrap());
11353    }
11354
11355    #[gpui::test]
11356    async fn test_close_pane_items(cx: &mut TestAppContext) {
11357        init_test(cx);
11358
11359        let fs = FakeFs::new(cx.executor());
11360
11361        let project = Project::test(fs, None, cx).await;
11362        let (workspace, cx) =
11363            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11364
11365        let item1 = cx.new(|cx| {
11366            TestItem::new(cx)
11367                .with_dirty(true)
11368                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
11369        });
11370        let item2 = cx.new(|cx| {
11371            TestItem::new(cx)
11372                .with_dirty(true)
11373                .with_conflict(true)
11374                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
11375        });
11376        let item3 = cx.new(|cx| {
11377            TestItem::new(cx)
11378                .with_dirty(true)
11379                .with_conflict(true)
11380                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
11381        });
11382        let item4 = cx.new(|cx| {
11383            TestItem::new(cx).with_dirty(true).with_project_items(&[{
11384                let project_item = TestProjectItem::new_untitled(cx);
11385                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11386                project_item
11387            }])
11388        });
11389        let pane = workspace.update_in(cx, |workspace, window, cx| {
11390            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
11391            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
11392            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
11393            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
11394            workspace.active_pane().clone()
11395        });
11396
11397        let close_items = pane.update_in(cx, |pane, window, cx| {
11398            pane.activate_item(1, true, true, window, cx);
11399            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
11400            let item1_id = item1.item_id();
11401            let item3_id = item3.item_id();
11402            let item4_id = item4.item_id();
11403            pane.close_items(window, cx, SaveIntent::Close, &move |id| {
11404                [item1_id, item3_id, item4_id].contains(&id)
11405            })
11406        });
11407        cx.executor().run_until_parked();
11408
11409        assert!(cx.has_pending_prompt());
11410        cx.simulate_prompt_answer("Save all");
11411
11412        cx.executor().run_until_parked();
11413
11414        // Item 1 is saved. There's a prompt to save item 3.
11415        pane.update(cx, |pane, cx| {
11416            assert_eq!(item1.read(cx).save_count, 1);
11417            assert_eq!(item1.read(cx).save_as_count, 0);
11418            assert_eq!(item1.read(cx).reload_count, 0);
11419            assert_eq!(pane.items_len(), 3);
11420            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
11421        });
11422        assert!(cx.has_pending_prompt());
11423
11424        // Cancel saving item 3.
11425        cx.simulate_prompt_answer("Discard");
11426        cx.executor().run_until_parked();
11427
11428        // Item 3 is reloaded. There's a prompt to save item 4.
11429        pane.update(cx, |pane, cx| {
11430            assert_eq!(item3.read(cx).save_count, 0);
11431            assert_eq!(item3.read(cx).save_as_count, 0);
11432            assert_eq!(item3.read(cx).reload_count, 1);
11433            assert_eq!(pane.items_len(), 2);
11434            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
11435        });
11436
11437        // There's a prompt for a path for item 4.
11438        cx.simulate_new_path_selection(|_| Some(Default::default()));
11439        close_items.await.unwrap();
11440
11441        // The requested items are closed.
11442        pane.update(cx, |pane, cx| {
11443            assert_eq!(item4.read(cx).save_count, 0);
11444            assert_eq!(item4.read(cx).save_as_count, 1);
11445            assert_eq!(item4.read(cx).reload_count, 0);
11446            assert_eq!(pane.items_len(), 1);
11447            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
11448        });
11449    }
11450
11451    #[gpui::test]
11452    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
11453        init_test(cx);
11454
11455        let fs = FakeFs::new(cx.executor());
11456        let project = Project::test(fs, [], cx).await;
11457        let (workspace, cx) =
11458            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11459
11460        // Create several workspace items with single project entries, and two
11461        // workspace items with multiple project entries.
11462        let single_entry_items = (0..=4)
11463            .map(|project_entry_id| {
11464                cx.new(|cx| {
11465                    TestItem::new(cx)
11466                        .with_dirty(true)
11467                        .with_project_items(&[dirty_project_item(
11468                            project_entry_id,
11469                            &format!("{project_entry_id}.txt"),
11470                            cx,
11471                        )])
11472                })
11473            })
11474            .collect::<Vec<_>>();
11475        let item_2_3 = cx.new(|cx| {
11476            TestItem::new(cx)
11477                .with_dirty(true)
11478                .with_buffer_kind(ItemBufferKind::Multibuffer)
11479                .with_project_items(&[
11480                    single_entry_items[2].read(cx).project_items[0].clone(),
11481                    single_entry_items[3].read(cx).project_items[0].clone(),
11482                ])
11483        });
11484        let item_3_4 = cx.new(|cx| {
11485            TestItem::new(cx)
11486                .with_dirty(true)
11487                .with_buffer_kind(ItemBufferKind::Multibuffer)
11488                .with_project_items(&[
11489                    single_entry_items[3].read(cx).project_items[0].clone(),
11490                    single_entry_items[4].read(cx).project_items[0].clone(),
11491                ])
11492        });
11493
11494        // Create two panes that contain the following project entries:
11495        //   left pane:
11496        //     multi-entry items:   (2, 3)
11497        //     single-entry items:  0, 2, 3, 4
11498        //   right pane:
11499        //     single-entry items:  4, 1
11500        //     multi-entry items:   (3, 4)
11501        let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
11502            let left_pane = workspace.active_pane().clone();
11503            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
11504            workspace.add_item_to_active_pane(
11505                single_entry_items[0].boxed_clone(),
11506                None,
11507                true,
11508                window,
11509                cx,
11510            );
11511            workspace.add_item_to_active_pane(
11512                single_entry_items[2].boxed_clone(),
11513                None,
11514                true,
11515                window,
11516                cx,
11517            );
11518            workspace.add_item_to_active_pane(
11519                single_entry_items[3].boxed_clone(),
11520                None,
11521                true,
11522                window,
11523                cx,
11524            );
11525            workspace.add_item_to_active_pane(
11526                single_entry_items[4].boxed_clone(),
11527                None,
11528                true,
11529                window,
11530                cx,
11531            );
11532
11533            let right_pane =
11534                workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
11535
11536            let boxed_clone = single_entry_items[1].boxed_clone();
11537            let right_pane = window.spawn(cx, async move |cx| {
11538                right_pane.await.inspect(|right_pane| {
11539                    right_pane
11540                        .update_in(cx, |pane, window, cx| {
11541                            pane.add_item(boxed_clone, true, true, None, window, cx);
11542                            pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
11543                        })
11544                        .unwrap();
11545                })
11546            });
11547
11548            (left_pane, right_pane)
11549        });
11550        let right_pane = right_pane.await.unwrap();
11551        cx.focus(&right_pane);
11552
11553        let close = right_pane.update_in(cx, |pane, window, cx| {
11554            pane.close_all_items(&CloseAllItems::default(), window, cx)
11555                .unwrap()
11556        });
11557        cx.executor().run_until_parked();
11558
11559        let msg = cx.pending_prompt().unwrap().0;
11560        assert!(msg.contains("1.txt"));
11561        assert!(!msg.contains("2.txt"));
11562        assert!(!msg.contains("3.txt"));
11563        assert!(!msg.contains("4.txt"));
11564
11565        // With best-effort close, cancelling item 1 keeps it open but items 4
11566        // and (3,4) still close since their entries exist in left pane.
11567        cx.simulate_prompt_answer("Cancel");
11568        close.await;
11569
11570        right_pane.read_with(cx, |pane, _| {
11571            assert_eq!(pane.items_len(), 1);
11572        });
11573
11574        // Remove item 3 from left pane, making (2,3) the only item with entry 3.
11575        left_pane
11576            .update_in(cx, |left_pane, window, cx| {
11577                left_pane.close_item_by_id(
11578                    single_entry_items[3].entity_id(),
11579                    SaveIntent::Skip,
11580                    window,
11581                    cx,
11582                )
11583            })
11584            .await
11585            .unwrap();
11586
11587        let close = left_pane.update_in(cx, |pane, window, cx| {
11588            pane.close_all_items(&CloseAllItems::default(), window, cx)
11589                .unwrap()
11590        });
11591        cx.executor().run_until_parked();
11592
11593        let details = cx.pending_prompt().unwrap().1;
11594        assert!(details.contains("0.txt"));
11595        assert!(details.contains("3.txt"));
11596        assert!(details.contains("4.txt"));
11597        // Ideally 2.txt wouldn't appear since entry 2 still exists in item 2.
11598        // But we can only save whole items, so saving (2,3) for entry 3 includes 2.
11599        // assert!(!details.contains("2.txt"));
11600
11601        cx.simulate_prompt_answer("Save all");
11602        cx.executor().run_until_parked();
11603        close.await;
11604
11605        left_pane.read_with(cx, |pane, _| {
11606            assert_eq!(pane.items_len(), 0);
11607        });
11608    }
11609
11610    #[gpui::test]
11611    async fn test_autosave(cx: &mut gpui::TestAppContext) {
11612        init_test(cx);
11613
11614        let fs = FakeFs::new(cx.executor());
11615        let project = Project::test(fs, [], cx).await;
11616        let (workspace, cx) =
11617            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11618        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11619
11620        let item = cx.new(|cx| {
11621            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11622        });
11623        let item_id = item.entity_id();
11624        workspace.update_in(cx, |workspace, window, cx| {
11625            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11626        });
11627
11628        // Autosave on window change.
11629        item.update(cx, |item, cx| {
11630            SettingsStore::update_global(cx, |settings, cx| {
11631                settings.update_user_settings(cx, |settings| {
11632                    settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
11633                })
11634            });
11635            item.is_dirty = true;
11636        });
11637
11638        // Deactivating the window saves the file.
11639        cx.deactivate_window();
11640        item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11641
11642        // Re-activating the window doesn't save the file.
11643        cx.update(|window, _| window.activate_window());
11644        cx.executor().run_until_parked();
11645        item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11646
11647        // Autosave on focus change.
11648        item.update_in(cx, |item, window, cx| {
11649            cx.focus_self(window);
11650            SettingsStore::update_global(cx, |settings, cx| {
11651                settings.update_user_settings(cx, |settings| {
11652                    settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11653                })
11654            });
11655            item.is_dirty = true;
11656        });
11657        // Focus leaving the item (via window deactivation) saves the file.
11658        // Deferred autosaves are flushed when focus lands elsewhere (pane, panel)
11659        // or when the window is deactivated.
11660        cx.deactivate_window();
11661        cx.executor().run_until_parked();
11662        item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
11663        cx.update(|window, _| window.activate_window());
11664
11665        // Deactivating the window still saves the file.
11666        item.update_in(cx, |item, window, cx| {
11667            cx.focus_self(window);
11668            item.is_dirty = true;
11669        });
11670        cx.deactivate_window();
11671        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
11672
11673        // Autosave after delay.
11674        item.update(cx, |item, cx| {
11675            SettingsStore::update_global(cx, |settings, cx| {
11676                settings.update_user_settings(cx, |settings| {
11677                    settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
11678                        milliseconds: 500.into(),
11679                    });
11680                })
11681            });
11682            item.is_dirty = true;
11683            cx.emit(ItemEvent::Edit);
11684        });
11685
11686        // Delay hasn't fully expired, so the file is still dirty and unsaved.
11687        cx.executor().advance_clock(Duration::from_millis(250));
11688        item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
11689
11690        // After delay expires, the file is saved.
11691        cx.executor().advance_clock(Duration::from_millis(250));
11692        item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11693
11694        // Autosave after delay, should save earlier than delay if tab is closed
11695        item.update(cx, |item, cx| {
11696            item.is_dirty = true;
11697            cx.emit(ItemEvent::Edit);
11698        });
11699        cx.executor().advance_clock(Duration::from_millis(250));
11700        item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
11701
11702        // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out.
11703        pane.update_in(cx, |pane, window, cx| {
11704            pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11705        })
11706        .await
11707        .unwrap();
11708        assert!(!cx.has_pending_prompt());
11709        item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11710
11711        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11712        workspace.update_in(cx, |workspace, window, cx| {
11713            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11714        });
11715        item.update_in(cx, |item, _window, cx| {
11716            item.is_dirty = true;
11717            for project_item in &mut item.project_items {
11718                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11719            }
11720        });
11721        cx.run_until_parked();
11722        item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
11723
11724        // Autosave on focus change, ensuring closing the tab counts as such.
11725        item.update(cx, |item, cx| {
11726            SettingsStore::update_global(cx, |settings, cx| {
11727                settings.update_user_settings(cx, |settings| {
11728                    settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11729                })
11730            });
11731            item.is_dirty = true;
11732            for project_item in &mut item.project_items {
11733                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
11734            }
11735        });
11736
11737        pane.update_in(cx, |pane, window, cx| {
11738            pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11739        })
11740        .await
11741        .unwrap();
11742        assert!(!cx.has_pending_prompt());
11743        item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11744
11745        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
11746        workspace.update_in(cx, |workspace, window, cx| {
11747            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11748        });
11749        item.update_in(cx, |item, window, cx| {
11750            item.project_items[0].update(cx, |item, _| {
11751                item.entry_id = None;
11752            });
11753            item.is_dirty = true;
11754            window.blur();
11755        });
11756        cx.run_until_parked();
11757        item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11758
11759        // Ensure autosave is prevented for deleted files also when closing the buffer.
11760        let _close_items = pane.update_in(cx, |pane, window, cx| {
11761            pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
11762        });
11763        cx.run_until_parked();
11764        assert!(cx.has_pending_prompt());
11765        item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
11766    }
11767
11768    #[gpui::test]
11769    async fn test_autosave_on_focus_change_in_multibuffer(cx: &mut gpui::TestAppContext) {
11770        init_test(cx);
11771
11772        let fs = FakeFs::new(cx.executor());
11773        let project = Project::test(fs, [], cx).await;
11774        let (workspace, cx) =
11775            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11776
11777        // Create a multibuffer-like item with two child focus handles,
11778        // simulating individual buffer editors within a multibuffer.
11779        let item = cx.new(|cx| {
11780            TestItem::new(cx)
11781                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11782                .with_child_focus_handles(2, cx)
11783        });
11784        workspace.update_in(cx, |workspace, window, cx| {
11785            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11786        });
11787
11788        // Set autosave to OnFocusChange and focus the first child handle,
11789        // simulating the user's cursor being inside one of the multibuffer's excerpts.
11790        item.update_in(cx, |item, window, cx| {
11791            SettingsStore::update_global(cx, |settings, cx| {
11792                settings.update_user_settings(cx, |settings| {
11793                    settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11794                })
11795            });
11796            item.is_dirty = true;
11797            window.focus(&item.child_focus_handles[0], cx);
11798        });
11799        cx.executor().run_until_parked();
11800        item.read_with(cx, |item, _| assert_eq!(item.save_count, 0));
11801
11802        // Moving focus from one child to another within the same item should
11803        // NOT trigger autosave — focus is still within the item's focus hierarchy.
11804        item.update_in(cx, |item, window, cx| {
11805            window.focus(&item.child_focus_handles[1], cx);
11806        });
11807        cx.executor().run_until_parked();
11808        item.read_with(cx, |item, _| {
11809            assert_eq!(
11810                item.save_count, 0,
11811                "Switching focus between children within the same item should not autosave"
11812            );
11813        });
11814
11815        // Focus leaving the item saves the file. This is the core regression scenario:
11816        // with `on_blur`, this would NOT trigger because `on_blur` only fires when
11817        // the item's own focus handle is the leaf that lost focus. In a multibuffer,
11818        // the leaf is always a child focus handle, so `on_blur` never detected
11819        // focus leaving the item.
11820        //
11821        // With deferred saves, the save happens when focus lands on a pane/panel or
11822        // the window deactivates.
11823        cx.deactivate_window();
11824        cx.executor().run_until_parked();
11825        item.read_with(cx, |item, _| {
11826            assert_eq!(
11827                item.save_count, 1,
11828                "Window deactivation should trigger autosave when focus was on a child of the item"
11829            );
11830        });
11831        cx.update(|window, _| window.activate_window());
11832
11833        // Deactivating the window should also trigger autosave when a child of
11834        // the multibuffer item currently owns focus.
11835        item.update_in(cx, |item, window, cx| {
11836            item.is_dirty = true;
11837            window.focus(&item.child_focus_handles[0], cx);
11838        });
11839        cx.executor().run_until_parked();
11840        item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
11841
11842        cx.deactivate_window();
11843        item.read_with(cx, |item, _| {
11844            assert_eq!(
11845                item.save_count, 2,
11846                "Deactivating window should trigger autosave when focus was on a child"
11847            );
11848        });
11849    }
11850
11851    #[gpui::test]
11852    async fn test_autosave_deferred_for_modals(cx: &mut gpui::TestAppContext) {
11853        init_test(cx);
11854
11855        let fs = FakeFs::new(cx.executor());
11856        let project = Project::test(fs, [], cx).await;
11857        let (workspace, cx) =
11858            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11859
11860        let item = cx.new(|cx| {
11861            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11862        });
11863
11864        workspace.update_in(cx, |workspace, window, cx| {
11865            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
11866        });
11867
11868        item.update_in(cx, |item, window, cx| {
11869            SettingsStore::update_global(cx, |settings, cx| {
11870                settings.update_user_settings(cx, |settings| {
11871                    settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11872                })
11873            });
11874            item.is_dirty = true;
11875            cx.focus_self(window);
11876        });
11877        cx.executor().run_until_parked();
11878
11879        // Opening a modal moves focus away from the item, but autosave should be
11880        // deferred until focus lands on a pane or panel (not saved immediately).
11881        workspace.update_in(cx, |workspace, window, cx| {
11882            workspace.toggle_modal(window, cx, TestModal::new);
11883        });
11884        cx.executor().run_until_parked();
11885        item.read_with(cx, |item, _| {
11886            assert_eq!(
11887                item.save_count, 0,
11888                "Opening a modal should NOT immediately trigger autosave"
11889            );
11890        });
11891
11892        // If focus returns to the same item (modal dismissed), the deferred save
11893        // should be skipped.
11894        workspace.update_in(cx, |workspace, window, cx| {
11895            workspace.modal_layer.update(cx, |modal, cx| {
11896                modal.hide_modal(window, cx);
11897            });
11898        });
11899        cx.executor().run_until_parked();
11900        item.read_with(cx, |item, _| {
11901            assert_eq!(
11902                item.save_count, 0,
11903                "Returning focus to the same item should skip deferred save"
11904            );
11905        });
11906
11907        // Open modal again with a dirty item.
11908        item.update_in(cx, |item, window, cx| {
11909            item.is_dirty = true;
11910            cx.focus_self(window);
11911        });
11912        workspace.update_in(cx, |workspace, window, cx| {
11913            workspace.toggle_modal(window, cx, TestModal::new);
11914        });
11915        cx.executor().run_until_parked();
11916        item.read_with(cx, |item, _| {
11917            assert_eq!(item.save_count, 0, "Modal open should not trigger save");
11918        });
11919
11920        // Window deactivation should flush deferred saves.
11921        cx.deactivate_window();
11922        cx.executor().run_until_parked();
11923        item.read_with(cx, |item, _| {
11924            assert_eq!(
11925                item.save_count, 1,
11926                "Window deactivation should flush deferred saves"
11927            );
11928        });
11929    }
11930
11931    #[gpui::test]
11932    async fn test_autosave_deferred_until_pane_focus(cx: &mut gpui::TestAppContext) {
11933        init_test(cx);
11934
11935        let fs = FakeFs::new(cx.executor());
11936        let project = Project::test(fs, [], cx).await;
11937        let (workspace, cx) =
11938            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11939
11940        let item1 = cx.new(|cx| {
11941            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11942        });
11943        let item2 = cx.new(|cx| {
11944            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
11945        });
11946
11947        let pane = workspace.update_in(cx, |workspace, window, cx| {
11948            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, false, window, cx);
11949            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, false, window, cx);
11950            workspace.active_pane().clone()
11951        });
11952        // Ensure added_to_pane is called for both items (sets up focus handlers)
11953        cx.executor().run_until_parked();
11954
11955        // Activate item1 (at index 0) and focus it.
11956        pane.update_in(cx, |pane, window, cx| {
11957            pane.activate_item(0, true, true, window, cx);
11958        });
11959        cx.executor().run_until_parked();
11960
11961        // Set up OnFocusChange autosave and make item1 dirty.
11962        item1.update(cx, |item, cx| {
11963            SettingsStore::update_global(cx, |settings, cx| {
11964                settings.update_user_settings(cx, |settings| {
11965                    settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
11966                })
11967            });
11968            item.is_dirty = true;
11969        });
11970        cx.executor().run_until_parked();
11971
11972        // Activate item2 via the pane - this should trigger autosave of item1.
11973        pane.update_in(cx, |pane, window, cx| {
11974            pane.activate_item(1, true, true, window, cx);
11975        });
11976        cx.executor().run_until_parked();
11977
11978        item1.read_with(cx, |item, _| {
11979            assert_eq!(
11980                item.save_count, 1,
11981                "Switching to another item should trigger deferred save of the previous item"
11982            );
11983        });
11984    }
11985
11986    #[gpui::test]
11987    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
11988        init_test(cx);
11989
11990        let fs = FakeFs::new(cx.executor());
11991
11992        let project = Project::test(fs, [], cx).await;
11993        let (workspace, cx) =
11994            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11995
11996        let item = cx.new(|cx| {
11997            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
11998        });
11999        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12000        let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
12001        let toolbar_notify_count = Rc::new(RefCell::new(0));
12002
12003        workspace.update_in(cx, |workspace, window, cx| {
12004            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
12005            let toolbar_notification_count = toolbar_notify_count.clone();
12006            cx.observe_in(&toolbar, window, move |_, _, _, _| {
12007                *toolbar_notification_count.borrow_mut() += 1
12008            })
12009            .detach();
12010        });
12011
12012        pane.read_with(cx, |pane, _| {
12013            assert!(!pane.can_navigate_backward());
12014            assert!(!pane.can_navigate_forward());
12015        });
12016
12017        item.update_in(cx, |item, _, cx| {
12018            item.set_state("one".to_string(), cx);
12019        });
12020
12021        // Toolbar must be notified to re-render the navigation buttons
12022        assert_eq!(*toolbar_notify_count.borrow(), 1);
12023
12024        pane.read_with(cx, |pane, _| {
12025            assert!(pane.can_navigate_backward());
12026            assert!(!pane.can_navigate_forward());
12027        });
12028
12029        workspace
12030            .update_in(cx, |workspace, window, cx| {
12031                workspace.go_back(pane.downgrade(), window, cx)
12032            })
12033            .await
12034            .unwrap();
12035
12036        assert_eq!(*toolbar_notify_count.borrow(), 2);
12037        pane.read_with(cx, |pane, _| {
12038            assert!(!pane.can_navigate_backward());
12039            assert!(pane.can_navigate_forward());
12040        });
12041    }
12042
12043    /// Tests that the navigation history deduplicates entries for the same item.
12044    ///
12045    /// When navigating back and forth between items (e.g., A -> B -> A -> B -> A -> B -> C),
12046    /// the navigation history deduplicates by keeping only the most recent visit to each item,
12047    /// resulting in [A, B, C] instead of [A, B, A, B, A, B, C]. This ensures that Go Back (Ctrl-O)
12048    /// navigates through unique items efficiently: C -> B -> A, rather than bouncing between
12049    /// repeated entries: C -> B -> A -> B -> A -> B -> A.
12050    ///
12051    /// This behavior prevents the navigation history from growing unnecessarily large and provides
12052    /// a better user experience by eliminating redundant navigation steps when jumping between files.
12053    #[gpui::test]
12054    async fn test_navigation_history_deduplication(cx: &mut gpui::TestAppContext) {
12055        init_test(cx);
12056
12057        let fs = FakeFs::new(cx.executor());
12058        let project = Project::test(fs, [], cx).await;
12059        let (workspace, cx) =
12060            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12061
12062        let item_a = cx.new(|cx| {
12063            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "a.txt", cx)])
12064        });
12065        let item_b = cx.new(|cx| {
12066            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "b.txt", cx)])
12067        });
12068        let item_c = cx.new(|cx| {
12069            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "c.txt", cx)])
12070        });
12071
12072        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12073
12074        workspace.update_in(cx, |workspace, window, cx| {
12075            workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx);
12076            workspace.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx);
12077            workspace.add_item_to_active_pane(Box::new(item_c.clone()), None, true, window, cx);
12078        });
12079
12080        workspace.update_in(cx, |workspace, window, cx| {
12081            workspace.activate_item(&item_a, false, false, window, cx);
12082        });
12083        cx.run_until_parked();
12084
12085        workspace.update_in(cx, |workspace, window, cx| {
12086            workspace.activate_item(&item_b, false, false, window, cx);
12087        });
12088        cx.run_until_parked();
12089
12090        workspace.update_in(cx, |workspace, window, cx| {
12091            workspace.activate_item(&item_a, false, false, window, cx);
12092        });
12093        cx.run_until_parked();
12094
12095        workspace.update_in(cx, |workspace, window, cx| {
12096            workspace.activate_item(&item_b, false, false, window, cx);
12097        });
12098        cx.run_until_parked();
12099
12100        workspace.update_in(cx, |workspace, window, cx| {
12101            workspace.activate_item(&item_a, false, false, window, cx);
12102        });
12103        cx.run_until_parked();
12104
12105        workspace.update_in(cx, |workspace, window, cx| {
12106            workspace.activate_item(&item_b, false, false, window, cx);
12107        });
12108        cx.run_until_parked();
12109
12110        workspace.update_in(cx, |workspace, window, cx| {
12111            workspace.activate_item(&item_c, false, false, window, cx);
12112        });
12113        cx.run_until_parked();
12114
12115        let backward_count = pane.read_with(cx, |pane, cx| {
12116            let mut count = 0;
12117            pane.nav_history().for_each_entry(cx, &mut |_, _| {
12118                count += 1;
12119            });
12120            count
12121        });
12122        assert!(
12123            backward_count <= 4,
12124            "Should have at most 4 entries, got {}",
12125            backward_count
12126        );
12127
12128        workspace
12129            .update_in(cx, |workspace, window, cx| {
12130                workspace.go_back(pane.downgrade(), window, cx)
12131            })
12132            .await
12133            .unwrap();
12134
12135        let active_item = workspace.read_with(cx, |workspace, cx| {
12136            workspace.active_item(cx).unwrap().item_id()
12137        });
12138        assert_eq!(
12139            active_item,
12140            item_b.entity_id(),
12141            "After first go_back, should be at item B"
12142        );
12143
12144        workspace
12145            .update_in(cx, |workspace, window, cx| {
12146                workspace.go_back(pane.downgrade(), window, cx)
12147            })
12148            .await
12149            .unwrap();
12150
12151        let active_item = workspace.read_with(cx, |workspace, cx| {
12152            workspace.active_item(cx).unwrap().item_id()
12153        });
12154        assert_eq!(
12155            active_item,
12156            item_a.entity_id(),
12157            "After second go_back, should be at item A"
12158        );
12159
12160        pane.read_with(cx, |pane, _| {
12161            assert!(pane.can_navigate_forward(), "Should be able to go forward");
12162        });
12163    }
12164
12165    #[gpui::test]
12166    async fn test_activate_last_pane(cx: &mut gpui::TestAppContext) {
12167        init_test(cx);
12168        let fs = FakeFs::new(cx.executor());
12169        let project = Project::test(fs, [], cx).await;
12170        let (multi_workspace, cx) =
12171            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12172        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12173
12174        workspace.update_in(cx, |workspace, window, cx| {
12175            let first_item = cx.new(|cx| {
12176                TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
12177            });
12178            workspace.add_item_to_active_pane(Box::new(first_item), None, true, window, cx);
12179            workspace.split_pane(
12180                workspace.active_pane().clone(),
12181                SplitDirection::Right,
12182                window,
12183                cx,
12184            );
12185            workspace.split_pane(
12186                workspace.active_pane().clone(),
12187                SplitDirection::Right,
12188                window,
12189                cx,
12190            );
12191        });
12192
12193        let (first_pane_id, target_last_pane_id) = workspace.update(cx, |workspace, _cx| {
12194            let panes = workspace.center.panes();
12195            assert!(panes.len() >= 2);
12196            (
12197                panes.first().expect("at least one pane").entity_id(),
12198                panes.last().expect("at least one pane").entity_id(),
12199            )
12200        });
12201
12202        workspace.update_in(cx, |workspace, window, cx| {
12203            workspace.activate_pane_at_index(&ActivatePane(0), window, cx);
12204        });
12205        workspace.update(cx, |workspace, _| {
12206            assert_eq!(workspace.active_pane().entity_id(), first_pane_id);
12207            assert_ne!(workspace.active_pane().entity_id(), target_last_pane_id);
12208        });
12209
12210        cx.dispatch_action(ActivateLastPane);
12211
12212        workspace.update(cx, |workspace, _| {
12213            assert_eq!(workspace.active_pane().entity_id(), target_last_pane_id);
12214        });
12215    }
12216
12217    #[gpui::test]
12218    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
12219        init_test(cx);
12220        let fs = FakeFs::new(cx.executor());
12221
12222        let project = Project::test(fs, [], cx).await;
12223        let (workspace, cx) =
12224            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12225
12226        let panel = workspace.update_in(cx, |workspace, window, cx| {
12227            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12228            workspace.add_panel(panel.clone(), window, cx);
12229
12230            workspace
12231                .right_dock()
12232                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
12233
12234            panel
12235        });
12236
12237        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12238        pane.update_in(cx, |pane, window, cx| {
12239            let item = cx.new(TestItem::new);
12240            pane.add_item(Box::new(item), true, true, None, window, cx);
12241        });
12242
12243        // Transfer focus from center to panel
12244        workspace.update_in(cx, |workspace, window, cx| {
12245            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12246        });
12247
12248        workspace.update_in(cx, |workspace, window, cx| {
12249            assert!(workspace.right_dock().read(cx).is_open());
12250            assert!(!panel.is_zoomed(window, cx));
12251            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12252        });
12253
12254        // Transfer focus from panel to center
12255        workspace.update_in(cx, |workspace, window, cx| {
12256            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12257        });
12258
12259        workspace.update_in(cx, |workspace, window, cx| {
12260            assert!(workspace.right_dock().read(cx).is_open());
12261            assert!(!panel.is_zoomed(window, cx));
12262            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12263            assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
12264        });
12265
12266        // Close the dock
12267        workspace.update_in(cx, |workspace, window, cx| {
12268            workspace.toggle_dock(DockPosition::Right, window, cx);
12269        });
12270
12271        workspace.update_in(cx, |workspace, window, cx| {
12272            assert!(!workspace.right_dock().read(cx).is_open());
12273            assert!(!panel.is_zoomed(window, cx));
12274            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12275            assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
12276        });
12277
12278        // Open the dock
12279        workspace.update_in(cx, |workspace, window, cx| {
12280            workspace.toggle_dock(DockPosition::Right, window, cx);
12281        });
12282
12283        workspace.update_in(cx, |workspace, window, cx| {
12284            assert!(workspace.right_dock().read(cx).is_open());
12285            assert!(!panel.is_zoomed(window, cx));
12286            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12287        });
12288
12289        // Focus and zoom panel
12290        panel.update_in(cx, |panel, window, cx| {
12291            cx.focus_self(window);
12292            panel.set_zoomed(true, window, cx)
12293        });
12294
12295        workspace.update_in(cx, |workspace, window, cx| {
12296            assert!(workspace.right_dock().read(cx).is_open());
12297            assert!(panel.is_zoomed(window, cx));
12298            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12299        });
12300
12301        // Transfer focus to the center closes the dock
12302        workspace.update_in(cx, |workspace, window, cx| {
12303            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12304        });
12305
12306        workspace.update_in(cx, |workspace, window, cx| {
12307            assert!(!workspace.right_dock().read(cx).is_open());
12308            assert!(panel.is_zoomed(window, cx));
12309            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12310        });
12311
12312        // Transferring focus back to the panel keeps it zoomed
12313        workspace.update_in(cx, |workspace, window, cx| {
12314            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12315        });
12316
12317        workspace.update_in(cx, |workspace, window, cx| {
12318            assert!(workspace.right_dock().read(cx).is_open());
12319            assert!(panel.is_zoomed(window, cx));
12320            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12321        });
12322
12323        // Close the dock while it is zoomed
12324        workspace.update_in(cx, |workspace, window, cx| {
12325            workspace.toggle_dock(DockPosition::Right, window, cx)
12326        });
12327
12328        workspace.update_in(cx, |workspace, window, cx| {
12329            assert!(!workspace.right_dock().read(cx).is_open());
12330            assert!(panel.is_zoomed(window, cx));
12331            assert!(workspace.zoomed.is_none());
12332            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12333        });
12334
12335        // Opening the dock, when it's zoomed, retains focus
12336        workspace.update_in(cx, |workspace, window, cx| {
12337            workspace.toggle_dock(DockPosition::Right, window, cx)
12338        });
12339
12340        workspace.update_in(cx, |workspace, window, cx| {
12341            assert!(workspace.right_dock().read(cx).is_open());
12342            assert!(panel.is_zoomed(window, cx));
12343            assert!(workspace.zoomed.is_some());
12344            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12345        });
12346
12347        // Unzoom and close the panel, zoom the active pane.
12348        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
12349        workspace.update_in(cx, |workspace, window, cx| {
12350            workspace.toggle_dock(DockPosition::Right, window, cx)
12351        });
12352        pane.update_in(cx, |pane, window, cx| {
12353            pane.toggle_zoom(&Default::default(), window, cx)
12354        });
12355
12356        // Opening a dock unzooms the pane.
12357        workspace.update_in(cx, |workspace, window, cx| {
12358            workspace.toggle_dock(DockPosition::Right, window, cx)
12359        });
12360        workspace.update_in(cx, |workspace, window, cx| {
12361            let pane = pane.read(cx);
12362            assert!(!pane.is_zoomed());
12363            assert!(!pane.focus_handle(cx).is_focused(window));
12364            assert!(workspace.right_dock().read(cx).is_open());
12365            assert!(workspace.zoomed.is_none());
12366        });
12367    }
12368
12369    #[gpui::test]
12370    async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) {
12371        init_test(cx);
12372        let fs = FakeFs::new(cx.executor());
12373
12374        let project = Project::test(fs, [], cx).await;
12375        let (workspace, cx) =
12376            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12377
12378        let panel = workspace.update_in(cx, |workspace, window, cx| {
12379            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12380            workspace.add_panel(panel.clone(), window, cx);
12381            panel
12382        });
12383
12384        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12385        pane.update_in(cx, |pane, window, cx| {
12386            let item = cx.new(TestItem::new);
12387            pane.add_item(Box::new(item), true, true, None, window, cx);
12388        });
12389
12390        // Enable close_panel_on_toggle
12391        cx.update_global(|store: &mut SettingsStore, cx| {
12392            store.update_user_settings(cx, |settings| {
12393                settings.workspace.close_panel_on_toggle = Some(true);
12394            });
12395        });
12396
12397        // Panel starts closed. Toggling should open and focus it.
12398        workspace.update_in(cx, |workspace, window, cx| {
12399            assert!(!workspace.right_dock().read(cx).is_open());
12400            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12401        });
12402
12403        workspace.update_in(cx, |workspace, window, cx| {
12404            assert!(
12405                workspace.right_dock().read(cx).is_open(),
12406                "Dock should be open after toggling from center"
12407            );
12408            assert!(
12409                panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12410                "Panel should be focused after toggling from center"
12411            );
12412        });
12413
12414        // Panel is open and focused. Toggling should close the panel and
12415        // return focus to the center.
12416        workspace.update_in(cx, |workspace, window, cx| {
12417            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12418        });
12419
12420        workspace.update_in(cx, |workspace, window, cx| {
12421            assert!(
12422                !workspace.right_dock().read(cx).is_open(),
12423                "Dock should be closed after toggling from focused panel"
12424            );
12425            assert!(
12426                !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12427                "Panel should not be focused after toggling from focused panel"
12428            );
12429        });
12430
12431        // Open the dock and focus something else so the panel is open but not
12432        // focused. Toggling should focus the panel (not close it).
12433        workspace.update_in(cx, |workspace, window, cx| {
12434            workspace
12435                .right_dock()
12436                .update(cx, |dock, cx| dock.set_open(true, window, cx));
12437            window.focus(&pane.read(cx).focus_handle(cx), cx);
12438        });
12439
12440        workspace.update_in(cx, |workspace, window, cx| {
12441            assert!(workspace.right_dock().read(cx).is_open());
12442            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
12443            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12444        });
12445
12446        workspace.update_in(cx, |workspace, window, cx| {
12447            assert!(
12448                workspace.right_dock().read(cx).is_open(),
12449                "Dock should remain open when toggling focuses an open-but-unfocused panel"
12450            );
12451            assert!(
12452                panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12453                "Panel should be focused after toggling an open-but-unfocused panel"
12454            );
12455        });
12456
12457        // Now disable the setting and verify the original behavior: toggling
12458        // from a focused panel moves focus to center but leaves the dock open.
12459        cx.update_global(|store: &mut SettingsStore, cx| {
12460            store.update_user_settings(cx, |settings| {
12461                settings.workspace.close_panel_on_toggle = Some(false);
12462            });
12463        });
12464
12465        workspace.update_in(cx, |workspace, window, cx| {
12466            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12467        });
12468
12469        workspace.update_in(cx, |workspace, window, cx| {
12470            assert!(
12471                workspace.right_dock().read(cx).is_open(),
12472                "Dock should remain open when setting is disabled"
12473            );
12474            assert!(
12475                !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
12476                "Panel should not be focused after toggling with setting disabled"
12477            );
12478        });
12479    }
12480
12481    #[gpui::test]
12482    async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
12483        init_test(cx);
12484        let fs = FakeFs::new(cx.executor());
12485
12486        let project = Project::test(fs, [], cx).await;
12487        let (workspace, cx) =
12488            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12489
12490        let pane = workspace.update_in(cx, |workspace, _window, _cx| {
12491            workspace.active_pane().clone()
12492        });
12493
12494        // Add an item to the pane so it can be zoomed
12495        workspace.update_in(cx, |workspace, window, cx| {
12496            let item = cx.new(TestItem::new);
12497            workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
12498        });
12499
12500        // Initially not zoomed
12501        workspace.update_in(cx, |workspace, _window, cx| {
12502            assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
12503            assert!(
12504                workspace.zoomed.is_none(),
12505                "Workspace should track no zoomed pane"
12506            );
12507            assert!(pane.read(cx).items_len() > 0, "Pane should have items");
12508        });
12509
12510        // Zoom In
12511        pane.update_in(cx, |pane, window, cx| {
12512            pane.zoom_in(&crate::ZoomIn, window, cx);
12513        });
12514
12515        workspace.update_in(cx, |workspace, window, cx| {
12516            assert!(
12517                pane.read(cx).is_zoomed(),
12518                "Pane should be zoomed after ZoomIn"
12519            );
12520            assert!(
12521                workspace.zoomed.is_some(),
12522                "Workspace should track the zoomed pane"
12523            );
12524            assert!(
12525                pane.read(cx).focus_handle(cx).contains_focused(window, cx),
12526                "ZoomIn should focus the pane"
12527            );
12528        });
12529
12530        // Zoom In again is a no-op
12531        pane.update_in(cx, |pane, window, cx| {
12532            pane.zoom_in(&crate::ZoomIn, window, cx);
12533        });
12534
12535        workspace.update_in(cx, |workspace, window, cx| {
12536            assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
12537            assert!(
12538                workspace.zoomed.is_some(),
12539                "Workspace still tracks zoomed pane"
12540            );
12541            assert!(
12542                pane.read(cx).focus_handle(cx).contains_focused(window, cx),
12543                "Pane remains focused after repeated ZoomIn"
12544            );
12545        });
12546
12547        // Zoom Out
12548        pane.update_in(cx, |pane, window, cx| {
12549            pane.zoom_out(&crate::ZoomOut, window, cx);
12550        });
12551
12552        workspace.update_in(cx, |workspace, _window, cx| {
12553            assert!(
12554                !pane.read(cx).is_zoomed(),
12555                "Pane should unzoom after ZoomOut"
12556            );
12557            assert!(
12558                workspace.zoomed.is_none(),
12559                "Workspace clears zoom tracking after ZoomOut"
12560            );
12561        });
12562
12563        // Zoom Out again is a no-op
12564        pane.update_in(cx, |pane, window, cx| {
12565            pane.zoom_out(&crate::ZoomOut, window, cx);
12566        });
12567
12568        workspace.update_in(cx, |workspace, _window, cx| {
12569            assert!(
12570                !pane.read(cx).is_zoomed(),
12571                "Second ZoomOut keeps pane unzoomed"
12572            );
12573            assert!(
12574                workspace.zoomed.is_none(),
12575                "Workspace remains without zoomed pane"
12576            );
12577        });
12578    }
12579
12580    #[gpui::test]
12581    async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
12582        init_test(cx);
12583        let fs = FakeFs::new(cx.executor());
12584
12585        let project = Project::test(fs, [], cx).await;
12586        let (workspace, cx) =
12587            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12588        workspace.update_in(cx, |workspace, window, cx| {
12589            // Open two docks
12590            let left_dock = workspace.dock_at_position(DockPosition::Left);
12591            let right_dock = workspace.dock_at_position(DockPosition::Right);
12592
12593            left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12594            right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12595
12596            assert!(left_dock.read(cx).is_open());
12597            assert!(right_dock.read(cx).is_open());
12598        });
12599
12600        workspace.update_in(cx, |workspace, window, cx| {
12601            // Toggle all docks - should close both
12602            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12603
12604            let left_dock = workspace.dock_at_position(DockPosition::Left);
12605            let right_dock = workspace.dock_at_position(DockPosition::Right);
12606            assert!(!left_dock.read(cx).is_open());
12607            assert!(!right_dock.read(cx).is_open());
12608        });
12609
12610        workspace.update_in(cx, |workspace, window, cx| {
12611            // Toggle again - should reopen both
12612            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12613
12614            let left_dock = workspace.dock_at_position(DockPosition::Left);
12615            let right_dock = workspace.dock_at_position(DockPosition::Right);
12616            assert!(left_dock.read(cx).is_open());
12617            assert!(right_dock.read(cx).is_open());
12618        });
12619    }
12620
12621    #[gpui::test]
12622    async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
12623        init_test(cx);
12624        let fs = FakeFs::new(cx.executor());
12625
12626        let project = Project::test(fs, [], cx).await;
12627        let (workspace, cx) =
12628            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12629        workspace.update_in(cx, |workspace, window, cx| {
12630            // Open two docks
12631            let left_dock = workspace.dock_at_position(DockPosition::Left);
12632            let right_dock = workspace.dock_at_position(DockPosition::Right);
12633
12634            left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12635            right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
12636
12637            assert!(left_dock.read(cx).is_open());
12638            assert!(right_dock.read(cx).is_open());
12639        });
12640
12641        workspace.update_in(cx, |workspace, window, cx| {
12642            // Close them manually
12643            workspace.toggle_dock(DockPosition::Left, window, cx);
12644            workspace.toggle_dock(DockPosition::Right, window, cx);
12645
12646            let left_dock = workspace.dock_at_position(DockPosition::Left);
12647            let right_dock = workspace.dock_at_position(DockPosition::Right);
12648            assert!(!left_dock.read(cx).is_open());
12649            assert!(!right_dock.read(cx).is_open());
12650        });
12651
12652        workspace.update_in(cx, |workspace, window, cx| {
12653            // Toggle all docks - only last closed (right dock) should reopen
12654            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12655
12656            let left_dock = workspace.dock_at_position(DockPosition::Left);
12657            let right_dock = workspace.dock_at_position(DockPosition::Right);
12658            assert!(!left_dock.read(cx).is_open());
12659            assert!(right_dock.read(cx).is_open());
12660        });
12661    }
12662
12663    #[gpui::test]
12664    async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
12665        init_test(cx);
12666        let fs = FakeFs::new(cx.executor());
12667        let project = Project::test(fs, [], cx).await;
12668        let (multi_workspace, cx) =
12669            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12670        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12671
12672        // Open two docks (left and right) with one panel each
12673        let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
12674            let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
12675            workspace.add_panel(left_panel.clone(), window, cx);
12676
12677            let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
12678            workspace.add_panel(right_panel.clone(), window, cx);
12679
12680            workspace.toggle_dock(DockPosition::Left, window, cx);
12681            workspace.toggle_dock(DockPosition::Right, window, cx);
12682
12683            // Verify initial state
12684            assert!(
12685                workspace.left_dock().read(cx).is_open(),
12686                "Left dock should be open"
12687            );
12688            assert_eq!(
12689                workspace
12690                    .left_dock()
12691                    .read(cx)
12692                    .visible_panel()
12693                    .unwrap()
12694                    .panel_id(),
12695                left_panel.panel_id(),
12696                "Left panel should be visible in left dock"
12697            );
12698            assert!(
12699                workspace.right_dock().read(cx).is_open(),
12700                "Right dock should be open"
12701            );
12702            assert_eq!(
12703                workspace
12704                    .right_dock()
12705                    .read(cx)
12706                    .visible_panel()
12707                    .unwrap()
12708                    .panel_id(),
12709                right_panel.panel_id(),
12710                "Right panel should be visible in right dock"
12711            );
12712            assert!(
12713                !workspace.bottom_dock().read(cx).is_open(),
12714                "Bottom dock should be closed"
12715            );
12716
12717            (left_panel, right_panel)
12718        });
12719
12720        // Focus the left panel and move it to the next position (bottom dock)
12721        workspace.update_in(cx, |workspace, window, cx| {
12722            workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
12723            assert!(
12724                left_panel.read(cx).focus_handle(cx).is_focused(window),
12725                "Left panel should be focused"
12726            );
12727        });
12728
12729        cx.dispatch_action(MoveFocusedPanelToNextPosition);
12730
12731        // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
12732        workspace.update(cx, |workspace, cx| {
12733            assert!(
12734                !workspace.left_dock().read(cx).is_open(),
12735                "Left dock should be closed"
12736            );
12737            assert!(
12738                workspace.bottom_dock().read(cx).is_open(),
12739                "Bottom dock should now be open"
12740            );
12741            assert_eq!(
12742                left_panel.read(cx).position,
12743                DockPosition::Bottom,
12744                "Left panel should now be in the bottom dock"
12745            );
12746            assert_eq!(
12747                workspace
12748                    .bottom_dock()
12749                    .read(cx)
12750                    .visible_panel()
12751                    .unwrap()
12752                    .panel_id(),
12753                left_panel.panel_id(),
12754                "Left panel should be the visible panel in the bottom dock"
12755            );
12756        });
12757
12758        // Toggle all docks off
12759        workspace.update_in(cx, |workspace, window, cx| {
12760            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12761            assert!(
12762                !workspace.left_dock().read(cx).is_open(),
12763                "Left dock should be closed"
12764            );
12765            assert!(
12766                !workspace.right_dock().read(cx).is_open(),
12767                "Right dock should be closed"
12768            );
12769            assert!(
12770                !workspace.bottom_dock().read(cx).is_open(),
12771                "Bottom dock should be closed"
12772            );
12773        });
12774
12775        // Toggle all docks back on and verify positions are restored
12776        workspace.update_in(cx, |workspace, window, cx| {
12777            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
12778            assert!(
12779                !workspace.left_dock().read(cx).is_open(),
12780                "Left dock should remain closed"
12781            );
12782            assert!(
12783                workspace.right_dock().read(cx).is_open(),
12784                "Right dock should remain open"
12785            );
12786            assert!(
12787                workspace.bottom_dock().read(cx).is_open(),
12788                "Bottom dock should remain open"
12789            );
12790            assert_eq!(
12791                left_panel.read(cx).position,
12792                DockPosition::Bottom,
12793                "Left panel should remain in the bottom dock"
12794            );
12795            assert_eq!(
12796                right_panel.read(cx).position,
12797                DockPosition::Right,
12798                "Right panel should remain in the right dock"
12799            );
12800            assert_eq!(
12801                workspace
12802                    .bottom_dock()
12803                    .read(cx)
12804                    .visible_panel()
12805                    .unwrap()
12806                    .panel_id(),
12807                left_panel.panel_id(),
12808                "Left panel should be the visible panel in the right dock"
12809            );
12810        });
12811    }
12812
12813    #[gpui::test]
12814    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
12815        init_test(cx);
12816
12817        let fs = FakeFs::new(cx.executor());
12818
12819        let project = Project::test(fs, None, cx).await;
12820        let (workspace, cx) =
12821            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12822
12823        // Let's arrange the panes like this:
12824        //
12825        // +-----------------------+
12826        // |         top           |
12827        // +------+--------+-------+
12828        // | left | center | right |
12829        // +------+--------+-------+
12830        // |        bottom         |
12831        // +-----------------------+
12832
12833        let top_item = cx.new(|cx| {
12834            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
12835        });
12836        let bottom_item = cx.new(|cx| {
12837            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
12838        });
12839        let left_item = cx.new(|cx| {
12840            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
12841        });
12842        let right_item = cx.new(|cx| {
12843            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
12844        });
12845        let center_item = cx.new(|cx| {
12846            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
12847        });
12848
12849        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12850            let top_pane_id = workspace.active_pane().entity_id();
12851            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
12852            workspace.split_pane(
12853                workspace.active_pane().clone(),
12854                SplitDirection::Down,
12855                window,
12856                cx,
12857            );
12858            top_pane_id
12859        });
12860        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12861            let bottom_pane_id = workspace.active_pane().entity_id();
12862            workspace.add_item_to_active_pane(
12863                Box::new(bottom_item.clone()),
12864                None,
12865                false,
12866                window,
12867                cx,
12868            );
12869            workspace.split_pane(
12870                workspace.active_pane().clone(),
12871                SplitDirection::Up,
12872                window,
12873                cx,
12874            );
12875            bottom_pane_id
12876        });
12877        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12878            let left_pane_id = workspace.active_pane().entity_id();
12879            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
12880            workspace.split_pane(
12881                workspace.active_pane().clone(),
12882                SplitDirection::Right,
12883                window,
12884                cx,
12885            );
12886            left_pane_id
12887        });
12888        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12889            let right_pane_id = workspace.active_pane().entity_id();
12890            workspace.add_item_to_active_pane(
12891                Box::new(right_item.clone()),
12892                None,
12893                false,
12894                window,
12895                cx,
12896            );
12897            workspace.split_pane(
12898                workspace.active_pane().clone(),
12899                SplitDirection::Left,
12900                window,
12901                cx,
12902            );
12903            right_pane_id
12904        });
12905        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
12906            let center_pane_id = workspace.active_pane().entity_id();
12907            workspace.add_item_to_active_pane(
12908                Box::new(center_item.clone()),
12909                None,
12910                false,
12911                window,
12912                cx,
12913            );
12914            center_pane_id
12915        });
12916        cx.executor().run_until_parked();
12917
12918        workspace.update_in(cx, |workspace, window, cx| {
12919            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
12920
12921            // Join into next from center pane into right
12922            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12923        });
12924
12925        workspace.update_in(cx, |workspace, window, cx| {
12926            let active_pane = workspace.active_pane();
12927            assert_eq!(right_pane_id, active_pane.entity_id());
12928            assert_eq!(2, active_pane.read(cx).items_len());
12929            let item_ids_in_pane =
12930                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12931            assert!(item_ids_in_pane.contains(&center_item.item_id()));
12932            assert!(item_ids_in_pane.contains(&right_item.item_id()));
12933
12934            // Join into next from right pane into bottom
12935            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12936        });
12937
12938        workspace.update_in(cx, |workspace, window, cx| {
12939            let active_pane = workspace.active_pane();
12940            assert_eq!(bottom_pane_id, active_pane.entity_id());
12941            assert_eq!(3, active_pane.read(cx).items_len());
12942            let item_ids_in_pane =
12943                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12944            assert!(item_ids_in_pane.contains(&center_item.item_id()));
12945            assert!(item_ids_in_pane.contains(&right_item.item_id()));
12946            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12947
12948            // Join into next from bottom pane into left
12949            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12950        });
12951
12952        workspace.update_in(cx, |workspace, window, cx| {
12953            let active_pane = workspace.active_pane();
12954            assert_eq!(left_pane_id, active_pane.entity_id());
12955            assert_eq!(4, active_pane.read(cx).items_len());
12956            let item_ids_in_pane =
12957                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12958            assert!(item_ids_in_pane.contains(&center_item.item_id()));
12959            assert!(item_ids_in_pane.contains(&right_item.item_id()));
12960            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12961            assert!(item_ids_in_pane.contains(&left_item.item_id()));
12962
12963            // Join into next from left pane into top
12964            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
12965        });
12966
12967        workspace.update_in(cx, |workspace, window, cx| {
12968            let active_pane = workspace.active_pane();
12969            assert_eq!(top_pane_id, active_pane.entity_id());
12970            assert_eq!(5, active_pane.read(cx).items_len());
12971            let item_ids_in_pane =
12972                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
12973            assert!(item_ids_in_pane.contains(&center_item.item_id()));
12974            assert!(item_ids_in_pane.contains(&right_item.item_id()));
12975            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
12976            assert!(item_ids_in_pane.contains(&left_item.item_id()));
12977            assert!(item_ids_in_pane.contains(&top_item.item_id()));
12978
12979            // Single pane left: no-op
12980            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
12981        });
12982
12983        workspace.update(cx, |workspace, _cx| {
12984            let active_pane = workspace.active_pane();
12985            assert_eq!(top_pane_id, active_pane.entity_id());
12986        });
12987    }
12988
12989    fn add_an_item_to_active_pane(
12990        cx: &mut VisualTestContext,
12991        workspace: &Entity<Workspace>,
12992        item_id: u64,
12993    ) -> Entity<TestItem> {
12994        let item = cx.new(|cx| {
12995            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
12996                item_id,
12997                "item{item_id}.txt",
12998                cx,
12999            )])
13000        });
13001        workspace.update_in(cx, |workspace, window, cx| {
13002            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
13003        });
13004        item
13005    }
13006
13007    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
13008        workspace.update_in(cx, |workspace, window, cx| {
13009            workspace.split_pane(
13010                workspace.active_pane().clone(),
13011                SplitDirection::Right,
13012                window,
13013                cx,
13014            )
13015        })
13016    }
13017
13018    #[gpui::test]
13019    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
13020        init_test(cx);
13021        let fs = FakeFs::new(cx.executor());
13022        let project = Project::test(fs, None, cx).await;
13023        let (workspace, cx) =
13024            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13025
13026        add_an_item_to_active_pane(cx, &workspace, 1);
13027        split_pane(cx, &workspace);
13028        add_an_item_to_active_pane(cx, &workspace, 2);
13029        split_pane(cx, &workspace); // empty pane
13030        split_pane(cx, &workspace);
13031        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
13032
13033        cx.executor().run_until_parked();
13034
13035        workspace.update(cx, |workspace, cx| {
13036            let num_panes = workspace.panes().len();
13037            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
13038            let active_item = workspace
13039                .active_pane()
13040                .read(cx)
13041                .active_item()
13042                .expect("item is in focus");
13043
13044            assert_eq!(num_panes, 4);
13045            assert_eq!(num_items_in_current_pane, 1);
13046            assert_eq!(active_item.item_id(), last_item.item_id());
13047        });
13048
13049        workspace.update_in(cx, |workspace, window, cx| {
13050            workspace.join_all_panes(window, cx);
13051        });
13052
13053        workspace.update(cx, |workspace, cx| {
13054            let num_panes = workspace.panes().len();
13055            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
13056            let active_item = workspace
13057                .active_pane()
13058                .read(cx)
13059                .active_item()
13060                .expect("item is in focus");
13061
13062            assert_eq!(num_panes, 1);
13063            assert_eq!(num_items_in_current_pane, 3);
13064            assert_eq!(active_item.item_id(), last_item.item_id());
13065        });
13066    }
13067
13068    #[gpui::test]
13069    async fn test_flexible_dock_sizing(cx: &mut gpui::TestAppContext) {
13070        init_test(cx);
13071        let fs = FakeFs::new(cx.executor());
13072
13073        let project = Project::test(fs, [], cx).await;
13074        let (multi_workspace, cx) =
13075            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13076        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13077
13078        workspace.update(cx, |workspace, _cx| {
13079            workspace.set_random_database_id();
13080        });
13081
13082        workspace.update_in(cx, |workspace, window, cx| {
13083            let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
13084            workspace.add_panel(panel.clone(), window, cx);
13085            workspace.toggle_dock(DockPosition::Right, window, cx);
13086
13087            let right_dock = workspace.right_dock().clone();
13088            right_dock.update(cx, |dock, cx| {
13089                dock.set_panel_size_state(
13090                    &panel,
13091                    dock::PanelSizeState {
13092                        size: None,
13093                        flex: Some(1.0),
13094                    },
13095                    cx,
13096                );
13097            });
13098        });
13099
13100        workspace.update_in(cx, |workspace, window, cx| {
13101            let item = cx.new(|cx| {
13102                TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
13103            });
13104            workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
13105            workspace.bounds.size.width = px(1920.);
13106
13107            let dock = workspace.right_dock().read(cx);
13108            let initial_width = workspace
13109                .dock_size(&dock, window, cx)
13110                .expect("flexible dock should have an initial width");
13111
13112            assert_eq!(initial_width, px(960.));
13113        });
13114
13115        workspace.update_in(cx, |workspace, window, cx| {
13116            workspace.split_pane(
13117                workspace.active_pane().clone(),
13118                SplitDirection::Right,
13119                window,
13120                cx,
13121            );
13122
13123            let center_column_count = workspace.center.full_height_column_count();
13124            assert_eq!(center_column_count, 2);
13125
13126            let dock = workspace.right_dock().read(cx);
13127            assert_eq!(workspace.dock_size(&dock, window, cx).unwrap(), px(640.));
13128
13129            workspace.bounds.size.width = px(2400.);
13130
13131            let dock = workspace.right_dock().read(cx);
13132            assert_eq!(workspace.dock_size(&dock, window, cx).unwrap(), px(800.));
13133        });
13134    }
13135
13136    #[gpui::test]
13137    async fn test_panel_size_state_persistence(cx: &mut gpui::TestAppContext) {
13138        init_test(cx);
13139        let fs = FakeFs::new(cx.executor());
13140
13141        // Fixed-width panel: pixel size is persisted to KVP and restored on re-add.
13142        {
13143            let project = Project::test(fs.clone(), [], cx).await;
13144            let (multi_workspace, cx) =
13145                cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13146            let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13147
13148            workspace.update(cx, |workspace, _cx| {
13149                workspace.set_random_database_id();
13150                workspace.bounds.size.width = px(800.);
13151            });
13152
13153            let panel = workspace.update_in(cx, |workspace, window, cx| {
13154                let panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
13155                workspace.add_panel(panel.clone(), window, cx);
13156                workspace.toggle_dock(DockPosition::Left, window, cx);
13157                panel
13158            });
13159
13160            workspace.update_in(cx, |workspace, window, cx| {
13161                workspace.resize_left_dock(px(350.), window, cx);
13162            });
13163
13164            cx.run_until_parked();
13165
13166            let persisted = workspace.read_with(cx, |workspace, cx| {
13167                workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
13168            });
13169            assert_eq!(
13170                persisted.and_then(|s| s.size),
13171                Some(px(350.)),
13172                "fixed-width panel size should be persisted to KVP"
13173            );
13174
13175            // Remove the panel and re-add a fresh instance with the same key.
13176            // The new instance should have its size state restored from KVP.
13177            workspace.update_in(cx, |workspace, window, cx| {
13178                workspace.remove_panel(&panel, window, cx);
13179            });
13180
13181            workspace.update_in(cx, |workspace, window, cx| {
13182                let new_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
13183                workspace.add_panel(new_panel, window, cx);
13184
13185                let left_dock = workspace.left_dock().read(cx);
13186                let size_state = left_dock
13187                    .panel::<TestPanel>()
13188                    .and_then(|p| left_dock.stored_panel_size_state(&p));
13189                assert_eq!(
13190                    size_state.and_then(|s| s.size),
13191                    Some(px(350.)),
13192                    "re-added fixed-width panel should restore persisted size from KVP"
13193                );
13194            });
13195        }
13196
13197        // Flexible panel: both pixel size and ratio are persisted and restored.
13198        {
13199            let project = Project::test(fs.clone(), [], cx).await;
13200            let (multi_workspace, cx) =
13201                cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13202            let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13203
13204            workspace.update(cx, |workspace, _cx| {
13205                workspace.set_random_database_id();
13206                workspace.bounds.size.width = px(800.);
13207            });
13208
13209            let panel = workspace.update_in(cx, |workspace, window, cx| {
13210                let item = cx.new(|cx| {
13211                    TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
13212                });
13213                workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
13214
13215                let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
13216                workspace.add_panel(panel.clone(), window, cx);
13217                workspace.toggle_dock(DockPosition::Right, window, cx);
13218                panel
13219            });
13220
13221            workspace.update_in(cx, |workspace, window, cx| {
13222                workspace.resize_right_dock(px(300.), window, cx);
13223            });
13224
13225            cx.run_until_parked();
13226
13227            let persisted = workspace
13228                .read_with(cx, |workspace, cx| {
13229                    workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
13230                })
13231                .expect("flexible panel state should be persisted to KVP");
13232            assert_eq!(
13233                persisted.size, None,
13234                "flexible panel should not persist a redundant pixel size"
13235            );
13236            let original_ratio = persisted.flex.expect("panel's flex should be persisted");
13237
13238            // Remove the panel and re-add: both size and ratio should be restored.
13239            workspace.update_in(cx, |workspace, window, cx| {
13240                workspace.remove_panel(&panel, window, cx);
13241            });
13242
13243            workspace.update_in(cx, |workspace, window, cx| {
13244                let new_panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
13245                workspace.add_panel(new_panel, window, cx);
13246
13247                let right_dock = workspace.right_dock().read(cx);
13248                let size_state = right_dock
13249                    .panel::<TestPanel>()
13250                    .and_then(|p| right_dock.stored_panel_size_state(&p))
13251                    .expect("re-added flexible panel should have restored size state from KVP");
13252                assert_eq!(
13253                    size_state.size, None,
13254                    "re-added flexible panel should not have a persisted pixel size"
13255                );
13256                assert_eq!(
13257                    size_state.flex,
13258                    Some(original_ratio),
13259                    "re-added flexible panel should restore persisted flex"
13260                );
13261            });
13262        }
13263    }
13264
13265    #[gpui::test]
13266    async fn test_flexible_panel_left_dock_sizing(cx: &mut gpui::TestAppContext) {
13267        init_test(cx);
13268        let fs = FakeFs::new(cx.executor());
13269
13270        let project = Project::test(fs, [], cx).await;
13271        let (multi_workspace, cx) =
13272            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13273        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13274
13275        workspace.update(cx, |workspace, _cx| {
13276            workspace.bounds.size.width = px(900.);
13277        });
13278
13279        // Step 1: Add a tab to the center pane then open a flexible panel in the left
13280        // dock. With one full-width center pane the default ratio is 0.5, so the panel
13281        // and the center pane each take half the workspace width.
13282        workspace.update_in(cx, |workspace, window, cx| {
13283            let item = cx.new(|cx| {
13284                TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
13285            });
13286            workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
13287
13288            let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Left, 100, cx));
13289            workspace.add_panel(panel, window, cx);
13290            workspace.toggle_dock(DockPosition::Left, window, cx);
13291
13292            let left_dock = workspace.left_dock().read(cx);
13293            let left_width = workspace
13294                .dock_size(&left_dock, window, cx)
13295                .expect("left dock should have an active panel");
13296
13297            assert_eq!(
13298                left_width,
13299                workspace.bounds.size.width / 2.,
13300                "flexible left panel should split evenly with the center pane"
13301            );
13302        });
13303
13304        // Step 2: Split the center pane left/right. The flexible panel is treated as one
13305        // average center column, so with two center columns it should take one third of
13306        // the workspace width.
13307        workspace.update_in(cx, |workspace, window, cx| {
13308            workspace.split_pane(
13309                workspace.active_pane().clone(),
13310                SplitDirection::Right,
13311                window,
13312                cx,
13313            );
13314
13315            let left_dock = workspace.left_dock().read(cx);
13316            let left_width = workspace
13317                .dock_size(&left_dock, window, cx)
13318                .expect("left dock should still have an active panel after horizontal split");
13319
13320            assert_eq!(
13321                left_width,
13322                workspace.bounds.size.width / 3.,
13323                "flexible left panel width should match the average center column width"
13324            );
13325        });
13326
13327        // Step 3: Split the active center pane vertically (top/bottom). Vertical splits do
13328        // not change the number of center columns, so the flexible panel width stays the same.
13329        workspace.update_in(cx, |workspace, window, cx| {
13330            workspace.split_pane(
13331                workspace.active_pane().clone(),
13332                SplitDirection::Down,
13333                window,
13334                cx,
13335            );
13336
13337            let left_dock = workspace.left_dock().read(cx);
13338            let left_width = workspace
13339                .dock_size(&left_dock, window, cx)
13340                .expect("left dock should still have an active panel after vertical split");
13341
13342            assert_eq!(
13343                left_width,
13344                workspace.bounds.size.width / 3.,
13345                "flexible left panel width should still match the average center column width"
13346            );
13347        });
13348
13349        // Step 4: Open a fixed-width panel in the right dock. The right dock's default
13350        // size reduces the available width, so the flexible left panel keeps matching one
13351        // average center column within the remaining space.
13352        workspace.update_in(cx, |workspace, window, cx| {
13353            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 200, cx));
13354            workspace.add_panel(panel, window, cx);
13355            workspace.toggle_dock(DockPosition::Right, window, cx);
13356
13357            let right_dock = workspace.right_dock().read(cx);
13358            let right_width = workspace
13359                .dock_size(&right_dock, window, cx)
13360                .expect("right dock should have an active panel");
13361
13362            let left_dock = workspace.left_dock().read(cx);
13363            let left_width = workspace
13364                .dock_size(&left_dock, window, cx)
13365                .expect("left dock should still have an active panel");
13366
13367            let available_width = workspace.bounds.size.width - right_width;
13368            assert_eq!(
13369                left_width,
13370                available_width / 3.,
13371                "flexible left panel should keep matching one average center column"
13372            );
13373        });
13374
13375        // Step 5: Toggle the right dock's panel to flexible. Now both docks use
13376        // column-equivalent flex sizing and the workspace width is divided among
13377        // left-flex, two center columns, and right-flex.
13378        workspace.update_in(cx, |workspace, window, cx| {
13379            let right_dock = workspace.right_dock().clone();
13380            let right_panel = right_dock
13381                .read(cx)
13382                .visible_panel()
13383                .expect("right dock should have a visible panel")
13384                .clone();
13385            workspace.toggle_dock_panel_flexible_size(
13386                &right_dock,
13387                right_panel.as_ref(),
13388                window,
13389                cx,
13390            );
13391
13392            let right_dock = right_dock.read(cx);
13393            let right_panel = right_dock
13394                .visible_panel()
13395                .expect("right dock should still have a visible panel");
13396            assert!(
13397                right_panel.has_flexible_size(window, cx),
13398                "right panel should now be flexible"
13399            );
13400
13401            let right_size_state = right_dock
13402                .stored_panel_size_state(right_panel.as_ref())
13403                .expect("right panel should have a stored size state after toggling");
13404            let right_flex = right_size_state
13405                .flex
13406                .expect("right panel should have a flex value after toggling");
13407
13408            let left_dock = workspace.left_dock().read(cx);
13409            let left_width = workspace
13410                .dock_size(&left_dock, window, cx)
13411                .expect("left dock should still have an active panel");
13412            let right_width = workspace
13413                .dock_size(&right_dock, window, cx)
13414                .expect("right dock should still have an active panel");
13415
13416            let left_flex = workspace
13417                .default_dock_flex(DockPosition::Left)
13418                .expect("left dock should have a default flex");
13419            let center_column_count = workspace.center.full_height_column_count() as f32;
13420
13421            let total_flex = left_flex + center_column_count + right_flex;
13422            let expected_left = left_flex / total_flex * workspace.bounds.size.width;
13423            let expected_right = right_flex / total_flex * workspace.bounds.size.width;
13424            assert_eq!(
13425                left_width, expected_left,
13426                "flexible left panel should share workspace width via flex ratios"
13427            );
13428            assert_eq!(
13429                right_width, expected_right,
13430                "flexible right panel should share workspace width via flex ratios"
13431            );
13432        });
13433    }
13434
13435    struct TestModal(FocusHandle);
13436
13437    impl TestModal {
13438        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
13439            Self(cx.focus_handle())
13440        }
13441    }
13442
13443    impl EventEmitter<DismissEvent> for TestModal {}
13444
13445    impl Focusable for TestModal {
13446        fn focus_handle(&self, _cx: &App) -> FocusHandle {
13447            self.0.clone()
13448        }
13449    }
13450
13451    impl ModalView for TestModal {}
13452
13453    impl Render for TestModal {
13454        fn render(
13455            &mut self,
13456            _window: &mut Window,
13457            _cx: &mut Context<TestModal>,
13458        ) -> impl IntoElement {
13459            div().track_focus(&self.0)
13460        }
13461    }
13462
13463    #[gpui::test]
13464    async fn test_panels(cx: &mut gpui::TestAppContext) {
13465        init_test(cx);
13466        let fs = FakeFs::new(cx.executor());
13467
13468        let project = Project::test(fs, [], cx).await;
13469        let (multi_workspace, cx) =
13470            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13471        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13472
13473        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
13474            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
13475            workspace.add_panel(panel_1.clone(), window, cx);
13476            workspace.toggle_dock(DockPosition::Left, window, cx);
13477            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
13478            workspace.add_panel(panel_2.clone(), window, cx);
13479            workspace.toggle_dock(DockPosition::Right, window, cx);
13480
13481            let left_dock = workspace.left_dock();
13482            assert_eq!(
13483                left_dock.read(cx).visible_panel().unwrap().panel_id(),
13484                panel_1.panel_id()
13485            );
13486            assert_eq!(
13487                workspace.dock_size(&left_dock.read(cx), window, cx),
13488                Some(px(300.))
13489            );
13490
13491            workspace.resize_left_dock(px(1337.), window, cx);
13492            assert_eq!(
13493                workspace
13494                    .right_dock()
13495                    .read(cx)
13496                    .visible_panel()
13497                    .unwrap()
13498                    .panel_id(),
13499                panel_2.panel_id(),
13500            );
13501
13502            (panel_1, panel_2)
13503        });
13504
13505        // Move panel_1 to the right
13506        panel_1.update_in(cx, |panel_1, window, cx| {
13507            panel_1.set_position(DockPosition::Right, window, cx)
13508        });
13509
13510        workspace.update_in(cx, |workspace, window, cx| {
13511            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
13512            // Since it was the only panel on the left, the left dock should now be closed.
13513            assert!(!workspace.left_dock().read(cx).is_open());
13514            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
13515            let right_dock = workspace.right_dock();
13516            assert_eq!(
13517                right_dock.read(cx).visible_panel().unwrap().panel_id(),
13518                panel_1.panel_id()
13519            );
13520            assert_eq!(
13521                right_dock
13522                    .read(cx)
13523                    .active_panel_size()
13524                    .unwrap()
13525                    .size
13526                    .unwrap(),
13527                px(1337.)
13528            );
13529
13530            // Now we move panel_2 to the left
13531            panel_2.set_position(DockPosition::Left, window, cx);
13532        });
13533
13534        workspace.update(cx, |workspace, cx| {
13535            // Since panel_2 was not visible on the right, we don't open the left dock.
13536            assert!(!workspace.left_dock().read(cx).is_open());
13537            // And the right dock is unaffected in its displaying of panel_1
13538            assert!(workspace.right_dock().read(cx).is_open());
13539            assert_eq!(
13540                workspace
13541                    .right_dock()
13542                    .read(cx)
13543                    .visible_panel()
13544                    .unwrap()
13545                    .panel_id(),
13546                panel_1.panel_id(),
13547            );
13548        });
13549
13550        // Move panel_1 back to the left
13551        panel_1.update_in(cx, |panel_1, window, cx| {
13552            panel_1.set_position(DockPosition::Left, window, cx)
13553        });
13554
13555        workspace.update_in(cx, |workspace, window, cx| {
13556            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
13557            let left_dock = workspace.left_dock();
13558            assert!(left_dock.read(cx).is_open());
13559            assert_eq!(
13560                left_dock.read(cx).visible_panel().unwrap().panel_id(),
13561                panel_1.panel_id()
13562            );
13563            assert_eq!(
13564                workspace.dock_size(&left_dock.read(cx), window, cx),
13565                Some(px(1337.))
13566            );
13567            // And the right dock should be closed as it no longer has any panels.
13568            assert!(!workspace.right_dock().read(cx).is_open());
13569
13570            // Now we move panel_1 to the bottom
13571            panel_1.set_position(DockPosition::Bottom, window, cx);
13572        });
13573
13574        workspace.update_in(cx, |workspace, window, cx| {
13575            // Since panel_1 was visible on the left, we close the left dock.
13576            assert!(!workspace.left_dock().read(cx).is_open());
13577            // The bottom dock is sized based on the panel's default size,
13578            // since the panel orientation changed from vertical to horizontal.
13579            let bottom_dock = workspace.bottom_dock();
13580            assert_eq!(
13581                workspace.dock_size(&bottom_dock.read(cx), window, cx),
13582                Some(px(300.))
13583            );
13584            // Close bottom dock and move panel_1 back to the left.
13585            bottom_dock.update(cx, |bottom_dock, cx| {
13586                bottom_dock.set_open(false, window, cx)
13587            });
13588            panel_1.set_position(DockPosition::Left, window, cx);
13589        });
13590
13591        // Emit activated event on panel 1
13592        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
13593
13594        // Now the left dock is open and panel_1 is active and focused.
13595        workspace.update_in(cx, |workspace, window, cx| {
13596            let left_dock = workspace.left_dock();
13597            assert!(left_dock.read(cx).is_open());
13598            assert_eq!(
13599                left_dock.read(cx).visible_panel().unwrap().panel_id(),
13600                panel_1.panel_id(),
13601            );
13602            assert!(panel_1.focus_handle(cx).is_focused(window));
13603        });
13604
13605        // Emit closed event on panel 2, which is not active
13606        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
13607
13608        // Wo don't close the left dock, because panel_2 wasn't the active panel
13609        workspace.update(cx, |workspace, cx| {
13610            let left_dock = workspace.left_dock();
13611            assert!(left_dock.read(cx).is_open());
13612            assert_eq!(
13613                left_dock.read(cx).visible_panel().unwrap().panel_id(),
13614                panel_1.panel_id(),
13615            );
13616        });
13617
13618        // Emitting a ZoomIn event shows the panel as zoomed.
13619        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
13620        workspace.read_with(cx, |workspace, _| {
13621            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13622            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
13623        });
13624
13625        // Move panel to another dock while it is zoomed
13626        panel_1.update_in(cx, |panel, window, cx| {
13627            panel.set_position(DockPosition::Right, window, cx)
13628        });
13629        workspace.read_with(cx, |workspace, _| {
13630            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13631
13632            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13633        });
13634
13635        // This is a helper for getting a:
13636        // - valid focus on an element,
13637        // - that isn't a part of the panes and panels system of the Workspace,
13638        // - and doesn't trigger the 'on_focus_lost' API.
13639        let focus_other_view = {
13640            let workspace = workspace.clone();
13641            move |cx: &mut VisualTestContext| {
13642                workspace.update_in(cx, |workspace, window, cx| {
13643                    if workspace.active_modal::<TestModal>(cx).is_some() {
13644                        workspace.toggle_modal(window, cx, TestModal::new);
13645                        workspace.toggle_modal(window, cx, TestModal::new);
13646                    } else {
13647                        workspace.toggle_modal(window, cx, TestModal::new);
13648                    }
13649                })
13650            }
13651        };
13652
13653        // If focus is transferred to another view that's not a panel or another pane, we still show
13654        // the panel as zoomed.
13655        focus_other_view(cx);
13656        workspace.read_with(cx, |workspace, _| {
13657            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13658            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13659        });
13660
13661        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
13662        workspace.update_in(cx, |_workspace, window, cx| {
13663            cx.focus_self(window);
13664        });
13665        workspace.read_with(cx, |workspace, _| {
13666            assert_eq!(workspace.zoomed, None);
13667            assert_eq!(workspace.zoomed_position, None);
13668        });
13669
13670        // If focus is transferred again to another view that's not a panel or a pane, we won't
13671        // show the panel as zoomed because it wasn't zoomed before.
13672        focus_other_view(cx);
13673        workspace.read_with(cx, |workspace, _| {
13674            assert_eq!(workspace.zoomed, None);
13675            assert_eq!(workspace.zoomed_position, None);
13676        });
13677
13678        // When the panel is activated, it is zoomed again.
13679        cx.dispatch_action(ToggleRightDock);
13680        workspace.read_with(cx, |workspace, _| {
13681            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
13682            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
13683        });
13684
13685        // Emitting a ZoomOut event unzooms the panel.
13686        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
13687        workspace.read_with(cx, |workspace, _| {
13688            assert_eq!(workspace.zoomed, None);
13689            assert_eq!(workspace.zoomed_position, None);
13690        });
13691
13692        // Emit closed event on panel 1, which is active
13693        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
13694
13695        // Now the left dock is closed, because panel_1 was the active panel
13696        workspace.update(cx, |workspace, cx| {
13697            let right_dock = workspace.right_dock();
13698            assert!(!right_dock.read(cx).is_open());
13699        });
13700    }
13701
13702    #[gpui::test]
13703    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
13704        init_test(cx);
13705
13706        let fs = FakeFs::new(cx.background_executor.clone());
13707        let project = Project::test(fs, [], cx).await;
13708        let (workspace, cx) =
13709            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13710        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13711
13712        let dirty_regular_buffer = cx.new(|cx| {
13713            TestItem::new(cx)
13714                .with_dirty(true)
13715                .with_label("1.txt")
13716                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13717        });
13718        let dirty_regular_buffer_2 = cx.new(|cx| {
13719            TestItem::new(cx)
13720                .with_dirty(true)
13721                .with_label("2.txt")
13722                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13723        });
13724        let dirty_multi_buffer_with_both = cx.new(|cx| {
13725            TestItem::new(cx)
13726                .with_dirty(true)
13727                .with_buffer_kind(ItemBufferKind::Multibuffer)
13728                .with_label("Fake Project Search")
13729                .with_project_items(&[
13730                    dirty_regular_buffer.read(cx).project_items[0].clone(),
13731                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13732                ])
13733        });
13734        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13735        workspace.update_in(cx, |workspace, window, cx| {
13736            workspace.add_item(
13737                pane.clone(),
13738                Box::new(dirty_regular_buffer.clone()),
13739                None,
13740                false,
13741                false,
13742                window,
13743                cx,
13744            );
13745            workspace.add_item(
13746                pane.clone(),
13747                Box::new(dirty_regular_buffer_2.clone()),
13748                None,
13749                false,
13750                false,
13751                window,
13752                cx,
13753            );
13754            workspace.add_item(
13755                pane.clone(),
13756                Box::new(dirty_multi_buffer_with_both.clone()),
13757                None,
13758                false,
13759                false,
13760                window,
13761                cx,
13762            );
13763        });
13764
13765        pane.update_in(cx, |pane, window, cx| {
13766            pane.activate_item(2, true, true, window, cx);
13767            assert_eq!(
13768                pane.active_item().unwrap().item_id(),
13769                multi_buffer_with_both_files_id,
13770                "Should select the multi buffer in the pane"
13771            );
13772        });
13773        let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13774            pane.close_other_items(
13775                &CloseOtherItems {
13776                    save_intent: Some(SaveIntent::Save),
13777                    close_pinned: true,
13778                },
13779                None,
13780                window,
13781                cx,
13782            )
13783        });
13784        cx.background_executor.run_until_parked();
13785        assert!(!cx.has_pending_prompt());
13786        close_all_but_multi_buffer_task
13787            .await
13788            .expect("Closing all buffers but the multi buffer failed");
13789        pane.update(cx, |pane, cx| {
13790            assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
13791            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
13792            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
13793            assert_eq!(pane.items_len(), 1);
13794            assert_eq!(
13795                pane.active_item().unwrap().item_id(),
13796                multi_buffer_with_both_files_id,
13797                "Should have only the multi buffer left in the pane"
13798            );
13799            assert!(
13800                dirty_multi_buffer_with_both.read(cx).is_dirty,
13801                "The multi buffer containing the unsaved buffer should still be dirty"
13802            );
13803        });
13804
13805        dirty_regular_buffer.update(cx, |buffer, cx| {
13806            buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
13807        });
13808
13809        let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13810            pane.close_active_item(
13811                &CloseActiveItem {
13812                    save_intent: Some(SaveIntent::Close),
13813                    close_pinned: false,
13814                },
13815                window,
13816                cx,
13817            )
13818        });
13819        cx.background_executor.run_until_parked();
13820        assert!(
13821            cx.has_pending_prompt(),
13822            "Dirty multi buffer should prompt a save dialog"
13823        );
13824        cx.simulate_prompt_answer("Save");
13825        cx.background_executor.run_until_parked();
13826        close_multi_buffer_task
13827            .await
13828            .expect("Closing the multi buffer failed");
13829        pane.update(cx, |pane, cx| {
13830            assert_eq!(
13831                dirty_multi_buffer_with_both.read(cx).save_count,
13832                1,
13833                "Multi buffer item should get be saved"
13834            );
13835            // Test impl does not save inner items, so we do not assert them
13836            assert_eq!(
13837                pane.items_len(),
13838                0,
13839                "No more items should be left in the pane"
13840            );
13841            assert!(pane.active_item().is_none());
13842        });
13843    }
13844
13845    #[gpui::test]
13846    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
13847        cx: &mut TestAppContext,
13848    ) {
13849        init_test(cx);
13850
13851        let fs = FakeFs::new(cx.background_executor.clone());
13852        let project = Project::test(fs, [], cx).await;
13853        let (workspace, cx) =
13854            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13855        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13856
13857        let dirty_regular_buffer = cx.new(|cx| {
13858            TestItem::new(cx)
13859                .with_dirty(true)
13860                .with_label("1.txt")
13861                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
13862        });
13863        let dirty_regular_buffer_2 = cx.new(|cx| {
13864            TestItem::new(cx)
13865                .with_dirty(true)
13866                .with_label("2.txt")
13867                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
13868        });
13869        let clear_regular_buffer = cx.new(|cx| {
13870            TestItem::new(cx)
13871                .with_label("3.txt")
13872                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
13873        });
13874
13875        let dirty_multi_buffer_with_both = cx.new(|cx| {
13876            TestItem::new(cx)
13877                .with_dirty(true)
13878                .with_buffer_kind(ItemBufferKind::Multibuffer)
13879                .with_label("Fake Project Search")
13880                .with_project_items(&[
13881                    dirty_regular_buffer.read(cx).project_items[0].clone(),
13882                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
13883                    clear_regular_buffer.read(cx).project_items[0].clone(),
13884                ])
13885        });
13886        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
13887        workspace.update_in(cx, |workspace, window, cx| {
13888            workspace.add_item(
13889                pane.clone(),
13890                Box::new(dirty_regular_buffer.clone()),
13891                None,
13892                false,
13893                false,
13894                window,
13895                cx,
13896            );
13897            workspace.add_item(
13898                pane.clone(),
13899                Box::new(dirty_multi_buffer_with_both.clone()),
13900                None,
13901                false,
13902                false,
13903                window,
13904                cx,
13905            );
13906        });
13907
13908        pane.update_in(cx, |pane, window, cx| {
13909            pane.activate_item(1, true, true, window, cx);
13910            assert_eq!(
13911                pane.active_item().unwrap().item_id(),
13912                multi_buffer_with_both_files_id,
13913                "Should select the multi buffer in the pane"
13914            );
13915        });
13916        let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
13917            pane.close_active_item(
13918                &CloseActiveItem {
13919                    save_intent: None,
13920                    close_pinned: false,
13921                },
13922                window,
13923                cx,
13924            )
13925        });
13926        cx.background_executor.run_until_parked();
13927        assert!(
13928            cx.has_pending_prompt(),
13929            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
13930        );
13931    }
13932
13933    /// Tests that when `close_on_file_delete` is enabled, files are automatically
13934    /// closed when they are deleted from disk.
13935    #[gpui::test]
13936    async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
13937        init_test(cx);
13938
13939        // Enable the close_on_disk_deletion setting
13940        cx.update_global(|store: &mut SettingsStore, cx| {
13941            store.update_user_settings(cx, |settings| {
13942                settings.workspace.close_on_file_delete = Some(true);
13943            });
13944        });
13945
13946        let fs = FakeFs::new(cx.background_executor.clone());
13947        let project = Project::test(fs, [], cx).await;
13948        let (workspace, cx) =
13949            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13950        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13951
13952        // Create a test item that simulates a file
13953        let item = cx.new(|cx| {
13954            TestItem::new(cx)
13955                .with_label("test.txt")
13956                .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13957        });
13958
13959        // Add item to workspace
13960        workspace.update_in(cx, |workspace, window, cx| {
13961            workspace.add_item(
13962                pane.clone(),
13963                Box::new(item.clone()),
13964                None,
13965                false,
13966                false,
13967                window,
13968                cx,
13969            );
13970        });
13971
13972        // Verify the item is in the pane
13973        pane.read_with(cx, |pane, _| {
13974            assert_eq!(pane.items().count(), 1);
13975        });
13976
13977        // Simulate file deletion by setting the item's deleted state
13978        item.update(cx, |item, _| {
13979            item.set_has_deleted_file(true);
13980        });
13981
13982        // Emit UpdateTab event to trigger the close behavior
13983        cx.run_until_parked();
13984        item.update(cx, |_, cx| {
13985            cx.emit(ItemEvent::UpdateTab);
13986        });
13987
13988        // Allow the close operation to complete
13989        cx.run_until_parked();
13990
13991        // Verify the item was automatically closed
13992        pane.read_with(cx, |pane, _| {
13993            assert_eq!(
13994                pane.items().count(),
13995                0,
13996                "Item should be automatically closed when file is deleted"
13997            );
13998        });
13999    }
14000
14001    /// Tests that when `close_on_file_delete` is disabled (default), files remain
14002    /// open with a strikethrough when they are deleted from disk.
14003    #[gpui::test]
14004    async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
14005        init_test(cx);
14006
14007        // Ensure close_on_disk_deletion is disabled (default)
14008        cx.update_global(|store: &mut SettingsStore, cx| {
14009            store.update_user_settings(cx, |settings| {
14010                settings.workspace.close_on_file_delete = Some(false);
14011            });
14012        });
14013
14014        let fs = FakeFs::new(cx.background_executor.clone());
14015        let project = Project::test(fs, [], cx).await;
14016        let (workspace, cx) =
14017            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14018        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14019
14020        // Create a test item that simulates a file
14021        let item = cx.new(|cx| {
14022            TestItem::new(cx)
14023                .with_label("test.txt")
14024                .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14025        });
14026
14027        // Add item to workspace
14028        workspace.update_in(cx, |workspace, window, cx| {
14029            workspace.add_item(
14030                pane.clone(),
14031                Box::new(item.clone()),
14032                None,
14033                false,
14034                false,
14035                window,
14036                cx,
14037            );
14038        });
14039
14040        // Verify the item is in the pane
14041        pane.read_with(cx, |pane, _| {
14042            assert_eq!(pane.items().count(), 1);
14043        });
14044
14045        // Simulate file deletion
14046        item.update(cx, |item, _| {
14047            item.set_has_deleted_file(true);
14048        });
14049
14050        // Emit UpdateTab event
14051        cx.run_until_parked();
14052        item.update(cx, |_, cx| {
14053            cx.emit(ItemEvent::UpdateTab);
14054        });
14055
14056        // Allow any potential close operation to complete
14057        cx.run_until_parked();
14058
14059        // Verify the item remains open (with strikethrough)
14060        pane.read_with(cx, |pane, _| {
14061            assert_eq!(
14062                pane.items().count(),
14063                1,
14064                "Item should remain open when close_on_disk_deletion is disabled"
14065            );
14066        });
14067
14068        // Verify the item shows as deleted
14069        item.read_with(cx, |item, _| {
14070            assert!(
14071                item.has_deleted_file,
14072                "Item should be marked as having deleted file"
14073            );
14074        });
14075    }
14076
14077    /// Tests that dirty files are not automatically closed when deleted from disk,
14078    /// even when `close_on_file_delete` is enabled. This ensures users don't lose
14079    /// unsaved changes without being prompted.
14080    #[gpui::test]
14081    async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
14082        init_test(cx);
14083
14084        // Enable the close_on_file_delete setting
14085        cx.update_global(|store: &mut SettingsStore, cx| {
14086            store.update_user_settings(cx, |settings| {
14087                settings.workspace.close_on_file_delete = Some(true);
14088            });
14089        });
14090
14091        let fs = FakeFs::new(cx.background_executor.clone());
14092        let project = Project::test(fs, [], cx).await;
14093        let (workspace, cx) =
14094            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14095        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14096
14097        // Create a dirty test item
14098        let item = cx.new(|cx| {
14099            TestItem::new(cx)
14100                .with_dirty(true)
14101                .with_label("test.txt")
14102                .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14103        });
14104
14105        // Add item to workspace
14106        workspace.update_in(cx, |workspace, window, cx| {
14107            workspace.add_item(
14108                pane.clone(),
14109                Box::new(item.clone()),
14110                None,
14111                false,
14112                false,
14113                window,
14114                cx,
14115            );
14116        });
14117
14118        // Simulate file deletion
14119        item.update(cx, |item, _| {
14120            item.set_has_deleted_file(true);
14121        });
14122
14123        // Emit UpdateTab event to trigger the close behavior
14124        cx.run_until_parked();
14125        item.update(cx, |_, cx| {
14126            cx.emit(ItemEvent::UpdateTab);
14127        });
14128
14129        // Allow any potential close operation to complete
14130        cx.run_until_parked();
14131
14132        // Verify the item remains open (dirty files are not auto-closed)
14133        pane.read_with(cx, |pane, _| {
14134            assert_eq!(
14135                pane.items().count(),
14136                1,
14137                "Dirty items should not be automatically closed even when file is deleted"
14138            );
14139        });
14140
14141        // Verify the item is marked as deleted and still dirty
14142        item.read_with(cx, |item, _| {
14143            assert!(
14144                item.has_deleted_file,
14145                "Item should be marked as having deleted file"
14146            );
14147            assert!(item.is_dirty, "Item should still be dirty");
14148        });
14149    }
14150
14151    /// Tests that navigation history is cleaned up when files are auto-closed
14152    /// due to deletion from disk.
14153    #[gpui::test]
14154    async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
14155        init_test(cx);
14156
14157        // Enable the close_on_file_delete setting
14158        cx.update_global(|store: &mut SettingsStore, cx| {
14159            store.update_user_settings(cx, |settings| {
14160                settings.workspace.close_on_file_delete = Some(true);
14161            });
14162        });
14163
14164        let fs = FakeFs::new(cx.background_executor.clone());
14165        let project = Project::test(fs, [], cx).await;
14166        let (workspace, cx) =
14167            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14168        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14169
14170        // Create test items
14171        let item1 = cx.new(|cx| {
14172            TestItem::new(cx)
14173                .with_label("test1.txt")
14174                .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
14175        });
14176        let item1_id = item1.item_id();
14177
14178        let item2 = cx.new(|cx| {
14179            TestItem::new(cx)
14180                .with_label("test2.txt")
14181                .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
14182        });
14183
14184        // Add items to workspace
14185        workspace.update_in(cx, |workspace, window, cx| {
14186            workspace.add_item(
14187                pane.clone(),
14188                Box::new(item1.clone()),
14189                None,
14190                false,
14191                false,
14192                window,
14193                cx,
14194            );
14195            workspace.add_item(
14196                pane.clone(),
14197                Box::new(item2.clone()),
14198                None,
14199                false,
14200                false,
14201                window,
14202                cx,
14203            );
14204        });
14205
14206        // Activate item1 to ensure it gets navigation entries
14207        pane.update_in(cx, |pane, window, cx| {
14208            pane.activate_item(0, true, true, window, cx);
14209        });
14210
14211        // Switch to item2 and back to create navigation history
14212        pane.update_in(cx, |pane, window, cx| {
14213            pane.activate_item(1, true, true, window, cx);
14214        });
14215        cx.run_until_parked();
14216
14217        pane.update_in(cx, |pane, window, cx| {
14218            pane.activate_item(0, true, true, window, cx);
14219        });
14220        cx.run_until_parked();
14221
14222        // Simulate file deletion for item1
14223        item1.update(cx, |item, _| {
14224            item.set_has_deleted_file(true);
14225        });
14226
14227        // Emit UpdateTab event to trigger the close behavior
14228        item1.update(cx, |_, cx| {
14229            cx.emit(ItemEvent::UpdateTab);
14230        });
14231        cx.run_until_parked();
14232
14233        // Verify item1 was closed
14234        pane.read_with(cx, |pane, _| {
14235            assert_eq!(
14236                pane.items().count(),
14237                1,
14238                "Should have 1 item remaining after auto-close"
14239            );
14240        });
14241
14242        // Check navigation history after close
14243        let has_item = pane.read_with(cx, |pane, cx| {
14244            let mut has_item = false;
14245            pane.nav_history().for_each_entry(cx, &mut |entry, _| {
14246                if entry.item.id() == item1_id {
14247                    has_item = true;
14248                }
14249            });
14250            has_item
14251        });
14252
14253        assert!(
14254            !has_item,
14255            "Navigation history should not contain closed item entries"
14256        );
14257    }
14258
14259    #[gpui::test]
14260    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
14261        cx: &mut TestAppContext,
14262    ) {
14263        init_test(cx);
14264
14265        let fs = FakeFs::new(cx.background_executor.clone());
14266        let project = Project::test(fs, [], cx).await;
14267        let (workspace, cx) =
14268            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
14269        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14270
14271        let dirty_regular_buffer = cx.new(|cx| {
14272            TestItem::new(cx)
14273                .with_dirty(true)
14274                .with_label("1.txt")
14275                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
14276        });
14277        let dirty_regular_buffer_2 = cx.new(|cx| {
14278            TestItem::new(cx)
14279                .with_dirty(true)
14280                .with_label("2.txt")
14281                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
14282        });
14283        let clear_regular_buffer = cx.new(|cx| {
14284            TestItem::new(cx)
14285                .with_label("3.txt")
14286                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
14287        });
14288
14289        let dirty_multi_buffer = cx.new(|cx| {
14290            TestItem::new(cx)
14291                .with_dirty(true)
14292                .with_buffer_kind(ItemBufferKind::Multibuffer)
14293                .with_label("Fake Project Search")
14294                .with_project_items(&[
14295                    dirty_regular_buffer.read(cx).project_items[0].clone(),
14296                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
14297                    clear_regular_buffer.read(cx).project_items[0].clone(),
14298                ])
14299        });
14300        workspace.update_in(cx, |workspace, window, cx| {
14301            workspace.add_item(
14302                pane.clone(),
14303                Box::new(dirty_regular_buffer.clone()),
14304                None,
14305                false,
14306                false,
14307                window,
14308                cx,
14309            );
14310            workspace.add_item(
14311                pane.clone(),
14312                Box::new(dirty_regular_buffer_2.clone()),
14313                None,
14314                false,
14315                false,
14316                window,
14317                cx,
14318            );
14319            workspace.add_item(
14320                pane.clone(),
14321                Box::new(dirty_multi_buffer.clone()),
14322                None,
14323                false,
14324                false,
14325                window,
14326                cx,
14327            );
14328        });
14329
14330        pane.update_in(cx, |pane, window, cx| {
14331            pane.activate_item(2, true, true, window, cx);
14332            assert_eq!(
14333                pane.active_item().unwrap().item_id(),
14334                dirty_multi_buffer.item_id(),
14335                "Should select the multi buffer in the pane"
14336            );
14337        });
14338        let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
14339            pane.close_active_item(
14340                &CloseActiveItem {
14341                    save_intent: None,
14342                    close_pinned: false,
14343                },
14344                window,
14345                cx,
14346            )
14347        });
14348        cx.background_executor.run_until_parked();
14349        assert!(
14350            !cx.has_pending_prompt(),
14351            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
14352        );
14353        close_multi_buffer_task
14354            .await
14355            .expect("Closing multi buffer failed");
14356        pane.update(cx, |pane, cx| {
14357            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
14358            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
14359            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
14360            assert_eq!(
14361                pane.items()
14362                    .map(|item| item.item_id())
14363                    .sorted()
14364                    .collect::<Vec<_>>(),
14365                vec![
14366                    dirty_regular_buffer.item_id(),
14367                    dirty_regular_buffer_2.item_id(),
14368                ],
14369                "Should have no multi buffer left in the pane"
14370            );
14371            assert!(dirty_regular_buffer.read(cx).is_dirty);
14372            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
14373        });
14374    }
14375
14376    #[gpui::test]
14377    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
14378        init_test(cx);
14379        let fs = FakeFs::new(cx.executor());
14380        let project = Project::test(fs, [], cx).await;
14381        let (multi_workspace, cx) =
14382            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
14383        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
14384
14385        // Add a new panel to the right dock, opening the dock and setting the
14386        // focus to the new panel.
14387        let panel = workspace.update_in(cx, |workspace, window, cx| {
14388            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
14389            workspace.add_panel(panel.clone(), window, cx);
14390
14391            workspace
14392                .right_dock()
14393                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
14394
14395            workspace.toggle_panel_focus::<TestPanel>(window, cx);
14396
14397            panel
14398        });
14399
14400        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
14401        // panel to the next valid position which, in this case, is the left
14402        // dock.
14403        cx.dispatch_action(MoveFocusedPanelToNextPosition);
14404        workspace.update(cx, |workspace, cx| {
14405            assert!(workspace.left_dock().read(cx).is_open());
14406            assert_eq!(panel.read(cx).position, DockPosition::Left);
14407        });
14408
14409        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
14410        // panel to the next valid position which, in this case, is the bottom
14411        // dock.
14412        cx.dispatch_action(MoveFocusedPanelToNextPosition);
14413        workspace.update(cx, |workspace, cx| {
14414            assert!(workspace.bottom_dock().read(cx).is_open());
14415            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
14416        });
14417
14418        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
14419        // around moving the panel to its initial position, the right dock.
14420        cx.dispatch_action(MoveFocusedPanelToNextPosition);
14421        workspace.update(cx, |workspace, cx| {
14422            assert!(workspace.right_dock().read(cx).is_open());
14423            assert_eq!(panel.read(cx).position, DockPosition::Right);
14424        });
14425
14426        // Remove focus from the panel, ensuring that, if the panel is not
14427        // focused, the `MoveFocusedPanelToNextPosition` action does not update
14428        // the panel's position, so the panel is still in the right dock.
14429        workspace.update_in(cx, |workspace, window, cx| {
14430            workspace.toggle_panel_focus::<TestPanel>(window, cx);
14431        });
14432
14433        cx.dispatch_action(MoveFocusedPanelToNextPosition);
14434        workspace.update(cx, |workspace, cx| {
14435            assert!(workspace.right_dock().read(cx).is_open());
14436            assert_eq!(panel.read(cx).position, DockPosition::Right);
14437        });
14438    }
14439
14440    #[gpui::test]
14441    async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
14442        init_test(cx);
14443
14444        let fs = FakeFs::new(cx.executor());
14445        let project = Project::test(fs, [], cx).await;
14446        let (workspace, cx) =
14447            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14448
14449        let item_1 = cx.new(|cx| {
14450            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
14451        });
14452        workspace.update_in(cx, |workspace, window, cx| {
14453            workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
14454            workspace.move_item_to_pane_in_direction(
14455                &MoveItemToPaneInDirection {
14456                    direction: SplitDirection::Right,
14457                    focus: true,
14458                    clone: false,
14459                },
14460                window,
14461                cx,
14462            );
14463            workspace.move_item_to_pane_at_index(
14464                &MoveItemToPane {
14465                    destination: 3,
14466                    focus: true,
14467                    clone: false,
14468                },
14469                window,
14470                cx,
14471            );
14472
14473            assert_eq!(workspace.panes.len(), 1, "No new panes were created");
14474            assert_eq!(
14475                pane_items_paths(&workspace.active_pane, cx),
14476                vec!["first.txt".to_string()],
14477                "Single item was not moved anywhere"
14478            );
14479        });
14480
14481        let item_2 = cx.new(|cx| {
14482            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
14483        });
14484        workspace.update_in(cx, |workspace, window, cx| {
14485            workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
14486            assert_eq!(
14487                pane_items_paths(&workspace.panes[0], cx),
14488                vec!["first.txt".to_string(), "second.txt".to_string()],
14489            );
14490            workspace.move_item_to_pane_in_direction(
14491                &MoveItemToPaneInDirection {
14492                    direction: SplitDirection::Right,
14493                    focus: true,
14494                    clone: false,
14495                },
14496                window,
14497                cx,
14498            );
14499
14500            assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
14501            assert_eq!(
14502                pane_items_paths(&workspace.panes[0], cx),
14503                vec!["first.txt".to_string()],
14504                "After moving, one item should be left in the original pane"
14505            );
14506            assert_eq!(
14507                pane_items_paths(&workspace.panes[1], cx),
14508                vec!["second.txt".to_string()],
14509                "New item should have been moved to the new pane"
14510            );
14511        });
14512
14513        let item_3 = cx.new(|cx| {
14514            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
14515        });
14516        workspace.update_in(cx, |workspace, window, cx| {
14517            let original_pane = workspace.panes[0].clone();
14518            workspace.set_active_pane(&original_pane, window, cx);
14519            workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
14520            assert_eq!(workspace.panes.len(), 2, "No new panes were created");
14521            assert_eq!(
14522                pane_items_paths(&workspace.active_pane, cx),
14523                vec!["first.txt".to_string(), "third.txt".to_string()],
14524                "New pane should be ready to move one item out"
14525            );
14526
14527            workspace.move_item_to_pane_at_index(
14528                &MoveItemToPane {
14529                    destination: 3,
14530                    focus: true,
14531                    clone: false,
14532                },
14533                window,
14534                cx,
14535            );
14536            assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
14537            assert_eq!(
14538                pane_items_paths(&workspace.active_pane, cx),
14539                vec!["first.txt".to_string()],
14540                "After moving, one item should be left in the original pane"
14541            );
14542            assert_eq!(
14543                pane_items_paths(&workspace.panes[1], cx),
14544                vec!["second.txt".to_string()],
14545                "Previously created pane should be unchanged"
14546            );
14547            assert_eq!(
14548                pane_items_paths(&workspace.panes[2], cx),
14549                vec!["third.txt".to_string()],
14550                "New item should have been moved to the new pane"
14551            );
14552        });
14553    }
14554
14555    #[gpui::test]
14556    async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
14557        init_test(cx);
14558
14559        let fs = FakeFs::new(cx.executor());
14560        let project = Project::test(fs, [], cx).await;
14561        let (workspace, cx) =
14562            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14563
14564        let item_1 = cx.new(|cx| {
14565            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
14566        });
14567        workspace.update_in(cx, |workspace, window, cx| {
14568            workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
14569            workspace.move_item_to_pane_in_direction(
14570                &MoveItemToPaneInDirection {
14571                    direction: SplitDirection::Right,
14572                    focus: true,
14573                    clone: true,
14574                },
14575                window,
14576                cx,
14577            );
14578        });
14579        cx.run_until_parked();
14580        workspace.update_in(cx, |workspace, window, cx| {
14581            workspace.move_item_to_pane_at_index(
14582                &MoveItemToPane {
14583                    destination: 3,
14584                    focus: true,
14585                    clone: true,
14586                },
14587                window,
14588                cx,
14589            );
14590        });
14591        cx.run_until_parked();
14592
14593        workspace.update(cx, |workspace, cx| {
14594            assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
14595            for pane in workspace.panes() {
14596                assert_eq!(
14597                    pane_items_paths(pane, cx),
14598                    vec!["first.txt".to_string()],
14599                    "Single item exists in all panes"
14600                );
14601            }
14602        });
14603
14604        // verify that the active pane has been updated after waiting for the
14605        // pane focus event to fire and resolve
14606        workspace.read_with(cx, |workspace, _app| {
14607            assert_eq!(
14608                workspace.active_pane(),
14609                &workspace.panes[2],
14610                "The third pane should be the active one: {:?}",
14611                workspace.panes
14612            );
14613        })
14614    }
14615
14616    #[gpui::test]
14617    async fn test_close_item_in_all_panes(cx: &mut TestAppContext) {
14618        init_test(cx);
14619
14620        let fs = FakeFs::new(cx.executor());
14621        fs.insert_tree("/root", json!({ "test.txt": "" })).await;
14622
14623        let project = Project::test(fs, ["root".as_ref()], cx).await;
14624        let (workspace, cx) =
14625            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14626
14627        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
14628        // Add item to pane A with project path
14629        let item_a = cx.new(|cx| {
14630            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14631        });
14632        workspace.update_in(cx, |workspace, window, cx| {
14633            workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
14634        });
14635
14636        // Split to create pane B
14637        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
14638            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
14639        });
14640
14641        // Add item with SAME project path to pane B, and pin it
14642        let item_b = cx.new(|cx| {
14643            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14644        });
14645        pane_b.update_in(cx, |pane, window, cx| {
14646            pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
14647            pane.set_pinned_count(1);
14648        });
14649
14650        assert_eq!(pane_a.read_with(cx, |pane, _| pane.items_len()), 1);
14651        assert_eq!(pane_b.read_with(cx, |pane, _| pane.items_len()), 1);
14652
14653        // close_pinned: false should only close the unpinned copy
14654        workspace.update_in(cx, |workspace, window, cx| {
14655            workspace.close_item_in_all_panes(
14656                &CloseItemInAllPanes {
14657                    save_intent: Some(SaveIntent::Close),
14658                    close_pinned: false,
14659                },
14660                window,
14661                cx,
14662            )
14663        });
14664        cx.executor().run_until_parked();
14665
14666        let item_count_a = pane_a.read_with(cx, |pane, _| pane.items_len());
14667        let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
14668        assert_eq!(item_count_a, 0, "Unpinned item in pane A should be closed");
14669        assert_eq!(item_count_b, 1, "Pinned item in pane B should remain");
14670
14671        // Split again, seeing as closing the previous item also closed its
14672        // pane, so only pane remains, which does not allow us to properly test
14673        // that both items close when `close_pinned: true`.
14674        let pane_c = workspace.update_in(cx, |workspace, window, cx| {
14675            workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
14676        });
14677
14678        // Add an item with the same project path to pane C so that
14679        // close_item_in_all_panes can determine what to close across all panes
14680        // (it reads the active item from the active pane, and split_pane
14681        // creates an empty pane).
14682        let item_c = cx.new(|cx| {
14683            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
14684        });
14685        pane_c.update_in(cx, |pane, window, cx| {
14686            pane.add_item(Box::new(item_c.clone()), true, true, None, window, cx);
14687        });
14688
14689        // close_pinned: true should close the pinned copy too
14690        workspace.update_in(cx, |workspace, window, cx| {
14691            let panes_count = workspace.panes().len();
14692            assert_eq!(panes_count, 2, "Workspace should have two panes (B and C)");
14693
14694            workspace.close_item_in_all_panes(
14695                &CloseItemInAllPanes {
14696                    save_intent: Some(SaveIntent::Close),
14697                    close_pinned: true,
14698                },
14699                window,
14700                cx,
14701            )
14702        });
14703        cx.executor().run_until_parked();
14704
14705        let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
14706        let item_count_c = pane_c.read_with(cx, |pane, _| pane.items_len());
14707        assert_eq!(item_count_b, 0, "Pinned item in pane B should be closed");
14708        assert_eq!(item_count_c, 0, "Unpinned item in pane C should be closed");
14709    }
14710
14711    mod register_project_item_tests {
14712
14713        use super::*;
14714
14715        // View
14716        struct TestPngItemView {
14717            focus_handle: FocusHandle,
14718        }
14719        // Model
14720        struct TestPngItem {}
14721
14722        impl project::ProjectItem for TestPngItem {
14723            fn try_open(
14724                _project: &Entity<Project>,
14725                path: &ProjectPath,
14726                cx: &mut App,
14727            ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
14728                if path.path.extension().unwrap() == "png" {
14729                    Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
14730                } else {
14731                    None
14732                }
14733            }
14734
14735            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
14736                None
14737            }
14738
14739            fn project_path(&self, _: &App) -> Option<ProjectPath> {
14740                None
14741            }
14742
14743            fn is_dirty(&self) -> bool {
14744                false
14745            }
14746        }
14747
14748        impl Item for TestPngItemView {
14749            type Event = ();
14750            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14751                "".into()
14752            }
14753        }
14754        impl EventEmitter<()> for TestPngItemView {}
14755        impl Focusable for TestPngItemView {
14756            fn focus_handle(&self, _cx: &App) -> FocusHandle {
14757                self.focus_handle.clone()
14758            }
14759        }
14760
14761        impl Render for TestPngItemView {
14762            fn render(
14763                &mut self,
14764                _window: &mut Window,
14765                _cx: &mut Context<Self>,
14766            ) -> impl IntoElement {
14767                Empty
14768            }
14769        }
14770
14771        impl ProjectItem for TestPngItemView {
14772            type Item = TestPngItem;
14773
14774            fn for_project_item(
14775                _project: Entity<Project>,
14776                _pane: Option<&Pane>,
14777                _item: Entity<Self::Item>,
14778                _: &mut Window,
14779                cx: &mut Context<Self>,
14780            ) -> Self
14781            where
14782                Self: Sized,
14783            {
14784                Self {
14785                    focus_handle: cx.focus_handle(),
14786                }
14787            }
14788        }
14789
14790        // View
14791        struct TestIpynbItemView {
14792            focus_handle: FocusHandle,
14793        }
14794        // Model
14795        struct TestIpynbItem {}
14796
14797        impl project::ProjectItem for TestIpynbItem {
14798            fn try_open(
14799                _project: &Entity<Project>,
14800                path: &ProjectPath,
14801                cx: &mut App,
14802            ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
14803                if path.path.extension().unwrap() == "ipynb" {
14804                    Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
14805                } else {
14806                    None
14807                }
14808            }
14809
14810            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
14811                None
14812            }
14813
14814            fn project_path(&self, _: &App) -> Option<ProjectPath> {
14815                None
14816            }
14817
14818            fn is_dirty(&self) -> bool {
14819                false
14820            }
14821        }
14822
14823        impl Item for TestIpynbItemView {
14824            type Event = ();
14825            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14826                "".into()
14827            }
14828        }
14829        impl EventEmitter<()> for TestIpynbItemView {}
14830        impl Focusable for TestIpynbItemView {
14831            fn focus_handle(&self, _cx: &App) -> FocusHandle {
14832                self.focus_handle.clone()
14833            }
14834        }
14835
14836        impl Render for TestIpynbItemView {
14837            fn render(
14838                &mut self,
14839                _window: &mut Window,
14840                _cx: &mut Context<Self>,
14841            ) -> impl IntoElement {
14842                Empty
14843            }
14844        }
14845
14846        impl ProjectItem for TestIpynbItemView {
14847            type Item = TestIpynbItem;
14848
14849            fn for_project_item(
14850                _project: Entity<Project>,
14851                _pane: Option<&Pane>,
14852                _item: Entity<Self::Item>,
14853                _: &mut Window,
14854                cx: &mut Context<Self>,
14855            ) -> Self
14856            where
14857                Self: Sized,
14858            {
14859                Self {
14860                    focus_handle: cx.focus_handle(),
14861                }
14862            }
14863        }
14864
14865        struct TestAlternatePngItemView {
14866            focus_handle: FocusHandle,
14867        }
14868
14869        impl Item for TestAlternatePngItemView {
14870            type Event = ();
14871            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
14872                "".into()
14873            }
14874        }
14875
14876        impl EventEmitter<()> for TestAlternatePngItemView {}
14877        impl Focusable for TestAlternatePngItemView {
14878            fn focus_handle(&self, _cx: &App) -> FocusHandle {
14879                self.focus_handle.clone()
14880            }
14881        }
14882
14883        impl Render for TestAlternatePngItemView {
14884            fn render(
14885                &mut self,
14886                _window: &mut Window,
14887                _cx: &mut Context<Self>,
14888            ) -> impl IntoElement {
14889                Empty
14890            }
14891        }
14892
14893        impl ProjectItem for TestAlternatePngItemView {
14894            type Item = TestPngItem;
14895
14896            fn for_project_item(
14897                _project: Entity<Project>,
14898                _pane: Option<&Pane>,
14899                _item: Entity<Self::Item>,
14900                _: &mut Window,
14901                cx: &mut Context<Self>,
14902            ) -> Self
14903            where
14904                Self: Sized,
14905            {
14906                Self {
14907                    focus_handle: cx.focus_handle(),
14908                }
14909            }
14910        }
14911
14912        #[gpui::test]
14913        async fn test_register_project_item(cx: &mut TestAppContext) {
14914            init_test(cx);
14915
14916            cx.update(|cx| {
14917                register_project_item::<TestPngItemView>(cx);
14918                register_project_item::<TestIpynbItemView>(cx);
14919            });
14920
14921            let fs = FakeFs::new(cx.executor());
14922            fs.insert_tree(
14923                "/root1",
14924                json!({
14925                    "one.png": "BINARYDATAHERE",
14926                    "two.ipynb": "{ totally a notebook }",
14927                    "three.txt": "editing text, sure why not?"
14928                }),
14929            )
14930            .await;
14931
14932            let project = Project::test(fs, ["root1".as_ref()], cx).await;
14933            let (workspace, cx) =
14934                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14935
14936            let worktree_id = project.update(cx, |project, cx| {
14937                project.worktrees(cx).next().unwrap().read(cx).id()
14938            });
14939
14940            let handle = workspace
14941                .update_in(cx, |workspace, window, cx| {
14942                    let project_path = (worktree_id, rel_path("one.png"));
14943                    workspace.open_path(project_path, None, true, window, cx)
14944                })
14945                .await
14946                .unwrap();
14947
14948            // Now we can check if the handle we got back errored or not
14949            assert_eq!(
14950                handle.to_any_view().entity_type(),
14951                TypeId::of::<TestPngItemView>()
14952            );
14953
14954            let handle = workspace
14955                .update_in(cx, |workspace, window, cx| {
14956                    let project_path = (worktree_id, rel_path("two.ipynb"));
14957                    workspace.open_path(project_path, None, true, window, cx)
14958                })
14959                .await
14960                .unwrap();
14961
14962            assert_eq!(
14963                handle.to_any_view().entity_type(),
14964                TypeId::of::<TestIpynbItemView>()
14965            );
14966
14967            let handle = workspace
14968                .update_in(cx, |workspace, window, cx| {
14969                    let project_path = (worktree_id, rel_path("three.txt"));
14970                    workspace.open_path(project_path, None, true, window, cx)
14971                })
14972                .await;
14973            assert!(handle.is_err());
14974        }
14975
14976        #[gpui::test]
14977        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
14978            init_test(cx);
14979
14980            cx.update(|cx| {
14981                register_project_item::<TestPngItemView>(cx);
14982                register_project_item::<TestAlternatePngItemView>(cx);
14983            });
14984
14985            let fs = FakeFs::new(cx.executor());
14986            fs.insert_tree(
14987                "/root1",
14988                json!({
14989                    "one.png": "BINARYDATAHERE",
14990                    "two.ipynb": "{ totally a notebook }",
14991                    "three.txt": "editing text, sure why not?"
14992                }),
14993            )
14994            .await;
14995            let project = Project::test(fs, ["root1".as_ref()], cx).await;
14996            let (workspace, cx) =
14997                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
14998            let worktree_id = project.update(cx, |project, cx| {
14999                project.worktrees(cx).next().unwrap().read(cx).id()
15000            });
15001
15002            let handle = workspace
15003                .update_in(cx, |workspace, window, cx| {
15004                    let project_path = (worktree_id, rel_path("one.png"));
15005                    workspace.open_path(project_path, None, true, window, cx)
15006                })
15007                .await
15008                .unwrap();
15009
15010            // This _must_ be the second item registered
15011            assert_eq!(
15012                handle.to_any_view().entity_type(),
15013                TypeId::of::<TestAlternatePngItemView>()
15014            );
15015
15016            let handle = workspace
15017                .update_in(cx, |workspace, window, cx| {
15018                    let project_path = (worktree_id, rel_path("three.txt"));
15019                    workspace.open_path(project_path, None, true, window, cx)
15020                })
15021                .await;
15022            assert!(handle.is_err());
15023        }
15024    }
15025
15026    #[gpui::test]
15027    async fn test_status_bar_visibility(cx: &mut TestAppContext) {
15028        init_test(cx);
15029
15030        let fs = FakeFs::new(cx.executor());
15031        let project = Project::test(fs, [], cx).await;
15032        let (workspace, _cx) =
15033            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
15034
15035        // Test with status bar shown (default)
15036        workspace.read_with(cx, |workspace, cx| {
15037            let visible = workspace.status_bar_visible(cx);
15038            assert!(visible, "Status bar should be visible by default");
15039        });
15040
15041        // Test with status bar hidden
15042        cx.update_global(|store: &mut SettingsStore, cx| {
15043            store.update_user_settings(cx, |settings| {
15044                settings.status_bar.get_or_insert_default().show = Some(false);
15045            });
15046        });
15047
15048        workspace.read_with(cx, |workspace, cx| {
15049            let visible = workspace.status_bar_visible(cx);
15050            assert!(!visible, "Status bar should be hidden when show is false");
15051        });
15052
15053        // Test with status bar shown explicitly
15054        cx.update_global(|store: &mut SettingsStore, cx| {
15055            store.update_user_settings(cx, |settings| {
15056                settings.status_bar.get_or_insert_default().show = Some(true);
15057            });
15058        });
15059
15060        workspace.read_with(cx, |workspace, cx| {
15061            let visible = workspace.status_bar_visible(cx);
15062            assert!(visible, "Status bar should be visible when show is true");
15063        });
15064    }
15065
15066    #[gpui::test]
15067    async fn test_pane_close_active_item(cx: &mut TestAppContext) {
15068        init_test(cx);
15069
15070        let fs = FakeFs::new(cx.executor());
15071        let project = Project::test(fs, [], cx).await;
15072        let (multi_workspace, cx) =
15073            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
15074        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
15075        let panel = workspace.update_in(cx, |workspace, window, cx| {
15076            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
15077            workspace.add_panel(panel.clone(), window, cx);
15078
15079            workspace
15080                .right_dock()
15081                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
15082
15083            panel
15084        });
15085
15086        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
15087        let item_a = cx.new(TestItem::new);
15088        let item_b = cx.new(TestItem::new);
15089        let item_a_id = item_a.entity_id();
15090        let item_b_id = item_b.entity_id();
15091
15092        pane.update_in(cx, |pane, window, cx| {
15093            pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
15094            pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
15095        });
15096
15097        pane.read_with(cx, |pane, _| {
15098            assert_eq!(pane.items_len(), 2);
15099            assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
15100        });
15101
15102        workspace.update_in(cx, |workspace, window, cx| {
15103            workspace.toggle_panel_focus::<TestPanel>(window, cx);
15104        });
15105
15106        workspace.update_in(cx, |_, window, cx| {
15107            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
15108        });
15109
15110        // Assert that the `pane::CloseActiveItem` action is handled at the
15111        // workspace level when one of the dock panels is focused and, in that
15112        // case, the center pane's active item is closed but the focus is not
15113        // moved.
15114        cx.dispatch_action(pane::CloseActiveItem::default());
15115        cx.run_until_parked();
15116
15117        pane.read_with(cx, |pane, _| {
15118            assert_eq!(pane.items_len(), 1);
15119            assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
15120        });
15121
15122        workspace.update_in(cx, |workspace, window, cx| {
15123            assert!(workspace.right_dock().read(cx).is_open());
15124            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
15125        });
15126    }
15127
15128    #[gpui::test]
15129    async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
15130        init_test(cx);
15131        let fs = FakeFs::new(cx.executor());
15132
15133        let project_a = Project::test(fs.clone(), [], cx).await;
15134        let project_b = Project::test(fs, [], cx).await;
15135
15136        let multi_workspace_handle =
15137            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
15138        cx.run_until_parked();
15139
15140        multi_workspace_handle
15141            .update(cx, |mw, _window, cx| {
15142                mw.open_sidebar(cx);
15143            })
15144            .unwrap();
15145
15146        let workspace_a = multi_workspace_handle
15147            .read_with(cx, |mw, _| mw.workspace().clone())
15148            .unwrap();
15149
15150        let _workspace_b = multi_workspace_handle
15151            .update(cx, |mw, window, cx| {
15152                mw.test_add_workspace(project_b, window, cx)
15153            })
15154            .unwrap();
15155
15156        // Switch to workspace A
15157        multi_workspace_handle
15158            .update(cx, |mw, window, cx| {
15159                let workspace = mw.workspaces().next().unwrap().clone();
15160                mw.activate(workspace, None, window, cx);
15161            })
15162            .unwrap();
15163
15164        let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
15165
15166        // Add a panel to workspace A's right dock and open the dock
15167        let panel = workspace_a.update_in(cx, |workspace, window, cx| {
15168            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
15169            workspace.add_panel(panel.clone(), window, cx);
15170            workspace
15171                .right_dock()
15172                .update(cx, |dock, cx| dock.set_open(true, window, cx));
15173            panel
15174        });
15175
15176        // Focus the panel through the workspace (matching existing test pattern)
15177        workspace_a.update_in(cx, |workspace, window, cx| {
15178            workspace.toggle_panel_focus::<TestPanel>(window, cx);
15179        });
15180
15181        // Zoom the panel
15182        panel.update_in(cx, |panel, window, cx| {
15183            panel.set_zoomed(true, window, cx);
15184        });
15185
15186        // Verify the panel is zoomed and the dock is open
15187        workspace_a.update_in(cx, |workspace, window, cx| {
15188            assert!(
15189                workspace.right_dock().read(cx).is_open(),
15190                "dock should be open before switch"
15191            );
15192            assert!(
15193                panel.is_zoomed(window, cx),
15194                "panel should be zoomed before switch"
15195            );
15196            assert!(
15197                panel.read(cx).focus_handle(cx).contains_focused(window, cx),
15198                "panel should be focused before switch"
15199            );
15200        });
15201
15202        // Switch to workspace B
15203        multi_workspace_handle
15204            .update(cx, |mw, window, cx| {
15205                let workspace = mw.workspaces().nth(1).unwrap().clone();
15206                mw.activate(workspace, None, window, cx);
15207            })
15208            .unwrap();
15209        cx.run_until_parked();
15210
15211        // Switch back to workspace A
15212        multi_workspace_handle
15213            .update(cx, |mw, window, cx| {
15214                let workspace = mw.workspaces().next().unwrap().clone();
15215                mw.activate(workspace, None, window, cx);
15216            })
15217            .unwrap();
15218        cx.run_until_parked();
15219
15220        // Verify the panel is still zoomed and the dock is still open
15221        workspace_a.update_in(cx, |workspace, window, cx| {
15222            assert!(
15223                workspace.right_dock().read(cx).is_open(),
15224                "dock should still be open after switching back"
15225            );
15226            assert!(
15227                panel.is_zoomed(window, cx),
15228                "panel should still be zoomed after switching back"
15229            );
15230        });
15231    }
15232
15233    fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
15234        pane.read(cx)
15235            .items()
15236            .flat_map(|item| {
15237                item.project_paths(cx)
15238                    .into_iter()
15239                    .map(|path| path.path.display(PathStyle::local()).into_owned())
15240            })
15241            .collect()
15242    }
15243
15244    pub fn init_test(cx: &mut TestAppContext) {
15245        cx.update(|cx| {
15246            let settings_store = SettingsStore::test(cx);
15247            cx.set_global(settings_store);
15248            cx.set_global(db::AppDatabase::test_new());
15249            theme_settings::init(theme::LoadThemes::JustBase, cx);
15250        });
15251    }
15252
15253    #[gpui::test]
15254    async fn test_toggle_theme_mode_persists_and_updates_active_theme(cx: &mut TestAppContext) {
15255        use settings::{ThemeName, ThemeSelection};
15256        use theme::SystemAppearance;
15257        use zed_actions::theme::ToggleMode;
15258
15259        init_test(cx);
15260
15261        let fs = FakeFs::new(cx.executor());
15262        let settings_fs: Arc<dyn fs::Fs> = fs.clone();
15263
15264        fs.insert_tree(path!("/root"), json!({ "file.rs": "fn main() {}\n" }))
15265            .await;
15266
15267        // Build a test project and workspace view so the test can invoke
15268        // the workspace action handler the same way the UI would.
15269        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
15270        let (workspace, cx) =
15271            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
15272
15273        // Seed the settings file with a plain static light theme so the
15274        // first toggle always starts from a known persisted state.
15275        workspace.update_in(cx, |_workspace, _window, cx| {
15276            *SystemAppearance::global_mut(cx) = SystemAppearance(theme::Appearance::Light);
15277            settings::update_settings_file(settings_fs.clone(), cx, |settings, _cx| {
15278                settings.theme.theme = Some(ThemeSelection::Static(ThemeName("One Light".into())));
15279            });
15280        });
15281        cx.executor().advance_clock(Duration::from_millis(200));
15282        cx.run_until_parked();
15283
15284        // Confirm the initial persisted settings contain the static theme
15285        // we just wrote before any toggling happens.
15286        let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
15287        assert!(settings_text.contains(r#""theme": "One Light""#));
15288
15289        // Toggle once. This should migrate the persisted theme settings
15290        // into light/dark slots and enable system mode.
15291        workspace.update_in(cx, |workspace, window, cx| {
15292            workspace.toggle_theme_mode(&ToggleMode, window, cx);
15293        });
15294        cx.executor().advance_clock(Duration::from_millis(200));
15295        cx.run_until_parked();
15296
15297        // 1. Static -> Dynamic
15298        // this assertion checks theme changed from static to dynamic.
15299        let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
15300        let parsed: serde_json::Value = settings::parse_json_with_comments(&settings_text).unwrap();
15301        assert_eq!(
15302            parsed["theme"],
15303            serde_json::json!({
15304                "mode": "system",
15305                "light": "One Light",
15306                "dark": "One Dark"
15307            })
15308        );
15309
15310        // 2. Toggle again, suppose it will change the mode to light
15311        workspace.update_in(cx, |workspace, window, cx| {
15312            workspace.toggle_theme_mode(&ToggleMode, window, cx);
15313        });
15314        cx.executor().advance_clock(Duration::from_millis(200));
15315        cx.run_until_parked();
15316
15317        let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
15318        assert!(settings_text.contains(r#""mode": "light""#));
15319    }
15320
15321    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
15322        let item = TestProjectItem::new(id, path, cx);
15323        item.update(cx, |item, _| {
15324            item.is_dirty = true;
15325        });
15326        item
15327    }
15328
15329    #[gpui::test]
15330    async fn test_zoomed_panel_without_pane_preserved_on_center_focus(
15331        cx: &mut gpui::TestAppContext,
15332    ) {
15333        init_test(cx);
15334        let fs = FakeFs::new(cx.executor());
15335
15336        let project = Project::test(fs, [], cx).await;
15337        let (workspace, cx) =
15338            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
15339
15340        let panel = workspace.update_in(cx, |workspace, window, cx| {
15341            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
15342            workspace.add_panel(panel.clone(), window, cx);
15343            workspace
15344                .right_dock()
15345                .update(cx, |dock, cx| dock.set_open(true, window, cx));
15346            panel
15347        });
15348
15349        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
15350        pane.update_in(cx, |pane, window, cx| {
15351            let item = cx.new(TestItem::new);
15352            pane.add_item(Box::new(item), true, true, None, window, cx);
15353        });
15354
15355        // Transfer focus to the panel, then zoom it. Using toggle_panel_focus
15356        // mirrors the real-world flow and avoids side effects from directly
15357        // focusing the panel while the center pane is active.
15358        workspace.update_in(cx, |workspace, window, cx| {
15359            workspace.toggle_panel_focus::<TestPanel>(window, cx);
15360        });
15361
15362        panel.update_in(cx, |panel, window, cx| {
15363            panel.set_zoomed(true, window, cx);
15364        });
15365
15366        workspace.update_in(cx, |workspace, window, cx| {
15367            assert!(workspace.right_dock().read(cx).is_open());
15368            assert!(panel.is_zoomed(window, cx));
15369            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
15370        });
15371
15372        // Simulate a spurious pane::Event::Focus on the center pane while the
15373        // panel still has focus. This mirrors what happens during macOS window
15374        // activation: the center pane fires a focus event even though actual
15375        // focus remains on the dock panel.
15376        pane.update_in(cx, |_, _, cx| {
15377            cx.emit(pane::Event::Focus);
15378        });
15379
15380        // The dock must remain open because the panel had focus at the time the
15381        // event was processed. Before the fix, dock_to_preserve was None for
15382        // panels that don't implement pane(), causing the dock to close.
15383        workspace.update_in(cx, |workspace, window, cx| {
15384            assert!(
15385                workspace.right_dock().read(cx).is_open(),
15386                "Dock should stay open when its zoomed panel (without pane()) still has focus"
15387            );
15388            assert!(panel.is_zoomed(window, cx));
15389        });
15390    }
15391
15392    #[gpui::test]
15393    async fn test_panels_stay_open_after_position_change_and_settings_update(
15394        cx: &mut gpui::TestAppContext,
15395    ) {
15396        init_test(cx);
15397        let fs = FakeFs::new(cx.executor());
15398        let project = Project::test(fs, [], cx).await;
15399        let (workspace, cx) =
15400            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
15401
15402        // Add two panels to the left dock and open it.
15403        let (panel_a, panel_b) = workspace.update_in(cx, |workspace, window, cx| {
15404            let panel_a = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
15405            let panel_b = cx.new(|cx| TestPanel::new(DockPosition::Left, 101, cx));
15406            workspace.add_panel(panel_a.clone(), window, cx);
15407            workspace.add_panel(panel_b.clone(), window, cx);
15408            workspace.left_dock().update(cx, |dock, cx| {
15409                dock.set_open(true, window, cx);
15410                dock.activate_panel(0, window, cx);
15411            });
15412            (panel_a, panel_b)
15413        });
15414
15415        workspace.update_in(cx, |workspace, _, cx| {
15416            assert!(workspace.left_dock().read(cx).is_open());
15417        });
15418
15419        // Simulate a feature flag changing default dock positions: both panels
15420        // move from Left to Right.
15421        workspace.update_in(cx, |_workspace, _window, cx| {
15422            panel_a.update(cx, |p, _cx| p.position = DockPosition::Right);
15423            panel_b.update(cx, |p, _cx| p.position = DockPosition::Right);
15424            cx.update_global::<SettingsStore, _>(|_, _| {});
15425        });
15426
15427        // Both panels should now be in the right dock.
15428        workspace.update_in(cx, |workspace, _, cx| {
15429            let right_dock = workspace.right_dock().read(cx);
15430            assert_eq!(right_dock.panels_len(), 2);
15431        });
15432
15433        // Open the right dock and activate panel_b (simulating the user
15434        // opening the panel after it moved).
15435        workspace.update_in(cx, |workspace, window, cx| {
15436            workspace.right_dock().update(cx, |dock, cx| {
15437                dock.set_open(true, window, cx);
15438                dock.activate_panel(1, window, cx);
15439            });
15440        });
15441
15442        // Now trigger another SettingsStore change
15443        workspace.update_in(cx, |_workspace, _window, cx| {
15444            cx.update_global::<SettingsStore, _>(|_, _| {});
15445        });
15446
15447        workspace.update_in(cx, |workspace, _, cx| {
15448            assert!(
15449                workspace.right_dock().read(cx).is_open(),
15450                "Right dock should still be open after a settings change"
15451            );
15452            assert_eq!(
15453                workspace.right_dock().read(cx).panels_len(),
15454                2,
15455                "Both panels should still be in the right dock"
15456            );
15457        });
15458    }
15459
15460    #[gpui::test]
15461    async fn test_most_recent_active_path_skips_read_only_paths(cx: &mut TestAppContext) {
15462        init_test(cx);
15463
15464        let fs = FakeFs::new(cx.executor());
15465        fs.insert_tree(
15466            path!("/project"),
15467            json!({
15468                "src": { "main.py": "" },
15469                ".venv": { "lib": { "dep.py": "" } },
15470            }),
15471        )
15472        .await;
15473
15474        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
15475        let (workspace, cx) =
15476            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
15477        let worktree_id = project.update(cx, |project, cx| {
15478            project.worktrees(cx).next().unwrap().read(cx).id()
15479        });
15480
15481        // Configure .venv as read-only
15482        workspace.update_in(cx, |_workspace, _window, cx| {
15483            cx.update_global::<SettingsStore, _>(|store, cx| {
15484                store
15485                    .set_user_settings(r#"{"read_only_files": ["**/.venv/**"]}"#, cx)
15486                    .ok();
15487            });
15488        });
15489
15490        let item_dep = cx.new(|cx| {
15491            TestItem::new(cx).with_project_items(&[TestProjectItem::new_in_worktree(
15492                1001,
15493                ".venv/lib/dep.py",
15494                worktree_id,
15495                cx,
15496            )])
15497        });
15498
15499        // dep.py is active but matches read_only_files → should be skipped
15500        workspace.update_in(cx, |workspace, window, cx| {
15501            workspace.add_item_to_active_pane(Box::new(item_dep.clone()), None, true, window, cx);
15502        });
15503        let path = workspace.read_with(cx, |workspace, cx| workspace.most_recent_active_path(cx));
15504        assert_eq!(path, None);
15505    }
15506}