workspace.rs

    1pub mod dock;
    2pub mod history_manager;
    3pub mod invalid_item_view;
    4pub mod item;
    5mod modal_layer;
    6mod multi_workspace;
    7pub mod notifications;
    8pub mod pane;
    9pub mod pane_group;
   10pub mod path_list {
   11    pub use util::path_list::{PathList, SerializedPathList};
   12}
   13mod persistence;
   14pub mod searchable;
   15mod security_modal;
   16pub mod shared_screen;
   17use db::smol::future::yield_now;
   18pub use shared_screen::SharedScreen;
   19mod status_bar;
   20pub mod tasks;
   21mod theme_preview;
   22mod toast_layer;
   23mod toolbar;
   24pub mod welcome;
   25mod workspace_settings;
   26
   27pub use crate::notifications::NotificationFrame;
   28pub use dock::Panel;
   29pub use multi_workspace::{
   30    CloseWorkspaceSidebar, DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace,
   31    MultiWorkspaceEvent, Sidebar, SidebarHandle, ToggleWorkspaceSidebar,
   32};
   33pub use path_list::{PathList, SerializedPathList};
   34pub use toast_layer::{ToastAction, ToastLayer, ToastView};
   35
   36use anyhow::{Context as _, Result, anyhow};
   37use client::{
   38    ChannelId, Client, ErrorExt, ParticipantIndex, Status, TypedEnvelope, User, UserStore,
   39    proto::{self, ErrorCode, PanelId, PeerId},
   40};
   41use collections::{HashMap, HashSet, hash_map};
   42use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
   43use fs::Fs;
   44use futures::{
   45    Future, FutureExt, StreamExt,
   46    channel::{
   47        mpsc::{self, UnboundedReceiver, UnboundedSender},
   48        oneshot,
   49    },
   50    future::{Shared, try_join_all},
   51};
   52use gpui::{
   53    Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
   54    CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
   55    Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton,
   56    PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription,
   57    SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId,
   58    WindowOptions, actions, canvas, point, relative, size, transparent_black,
   59};
   60pub use history_manager::*;
   61pub use item::{
   62    FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
   63    ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
   64};
   65use itertools::Itertools;
   66use language::{Buffer, LanguageRegistry, Rope, language_settings::all_language_settings};
   67pub use modal_layer::*;
   68use node_runtime::NodeRuntime;
   69use notifications::{
   70    DetachAndPromptErr, Notifications, dismiss_app_notification,
   71    simple_message_notification::MessageNotification,
   72};
   73pub use pane::*;
   74pub use pane_group::{
   75    ActivePaneDecorator, HANDLE_HITBOX_SIZE, Member, PaneAxis, PaneGroup, PaneRenderContext,
   76    SplitDirection,
   77};
   78use persistence::{SerializedWindowBounds, model::SerializedWorkspace};
   79pub use persistence::{
   80    WorkspaceDb, delete_unloaded_items,
   81    model::{
   82        DockStructure, ItemId, SerializedMultiWorkspace, SerializedWorkspaceLocation,
   83        SessionWorkspace,
   84    },
   85    read_serialized_multi_workspaces, resolve_worktree_workspaces,
   86};
   87use postage::stream::Stream;
   88use project::{
   89    DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
   90    WorktreeSettings,
   91    debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
   92    project_settings::ProjectSettings,
   93    toolchain_store::ToolchainStoreEvent,
   94    trusted_worktrees::{RemoteHostLocation, TrustedWorktrees, TrustedWorktreesEvent},
   95};
   96use remote::{
   97    RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions,
   98    remote_client::ConnectionIdentifier,
   99};
  100use schemars::JsonSchema;
  101use serde::Deserialize;
  102use session::AppSession;
  103use settings::{
  104    CenteredPaddingSettings, Settings, SettingsLocation, SettingsStore, update_settings_file,
  105};
  106
  107use sqlez::{
  108    bindable::{Bind, Column, StaticColumnCount},
  109    statement::Statement,
  110};
  111use status_bar::StatusBar;
  112pub use status_bar::StatusItemView;
  113use std::{
  114    any::TypeId,
  115    borrow::Cow,
  116    cell::RefCell,
  117    cmp,
  118    collections::VecDeque,
  119    env,
  120    hash::Hash,
  121    path::{Path, PathBuf},
  122    process::ExitStatus,
  123    rc::Rc,
  124    sync::{
  125        Arc, LazyLock, Weak,
  126        atomic::{AtomicBool, AtomicUsize},
  127    },
  128    time::Duration,
  129};
  130use task::{DebugScenario, SharedTaskContext, SpawnInTerminal};
  131use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeSettings};
  132pub use toolbar::{
  133    PaneSearchBarCallbacks, Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
  134};
  135pub use ui;
  136use ui::{Window, prelude::*};
  137use util::{
  138    ResultExt, TryFutureExt,
  139    paths::{PathStyle, SanitizedPath},
  140    rel_path::RelPath,
  141    serde::default_true,
  142};
  143use uuid::Uuid;
  144pub use workspace_settings::{
  145    AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, StatusBarSettings, TabBarSettings,
  146    WorkspaceSettings,
  147};
  148use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode};
  149
  150use crate::{item::ItemBufferKind, notifications::NotificationId};
  151use crate::{
  152    persistence::{
  153        SerializedAxis,
  154        model::{DockData, SerializedItem, SerializedPane, SerializedPaneGroup},
  155    },
  156    security_modal::SecurityModal,
  157};
  158
  159pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
  160
  161static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
  162    env::var("ZED_WINDOW_SIZE")
  163        .ok()
  164        .as_deref()
  165        .and_then(parse_pixel_size_env_var)
  166});
  167
  168static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
  169    env::var("ZED_WINDOW_POSITION")
  170        .ok()
  171        .as_deref()
  172        .and_then(parse_pixel_position_env_var)
  173});
  174
  175pub trait TerminalProvider {
  176    fn spawn(
  177        &self,
  178        task: SpawnInTerminal,
  179        window: &mut Window,
  180        cx: &mut App,
  181    ) -> Task<Option<Result<ExitStatus>>>;
  182}
  183
  184pub trait DebuggerProvider {
  185    // `active_buffer` is used to resolve build task's name against language-specific tasks.
  186    fn start_session(
  187        &self,
  188        definition: DebugScenario,
  189        task_context: SharedTaskContext,
  190        active_buffer: Option<Entity<Buffer>>,
  191        worktree_id: Option<WorktreeId>,
  192        window: &mut Window,
  193        cx: &mut App,
  194    );
  195
  196    fn spawn_task_or_modal(
  197        &self,
  198        workspace: &mut Workspace,
  199        action: &Spawn,
  200        window: &mut Window,
  201        cx: &mut Context<Workspace>,
  202    );
  203
  204    fn task_scheduled(&self, cx: &mut App);
  205    fn debug_scenario_scheduled(&self, cx: &mut App);
  206    fn debug_scenario_scheduled_last(&self, cx: &App) -> bool;
  207
  208    fn active_thread_state(&self, cx: &App) -> Option<ThreadStatus>;
  209}
  210
  211/// Opens a file or directory.
  212#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
  213#[action(namespace = workspace)]
  214pub struct Open {
  215    /// When true, opens in a new window. When false, adds to the current
  216    /// window as a new workspace (multi-workspace).
  217    #[serde(default = "Open::default_create_new_window")]
  218    pub create_new_window: bool,
  219}
  220
  221impl Open {
  222    pub const DEFAULT: Self = Self {
  223        create_new_window: true,
  224    };
  225
  226    /// Used by `#[serde(default)]` on the `create_new_window` field so that
  227    /// the serde default and `Open::DEFAULT` stay in sync.
  228    fn default_create_new_window() -> bool {
  229        Self::DEFAULT.create_new_window
  230    }
  231}
  232
  233impl Default for Open {
  234    fn default() -> Self {
  235        Self::DEFAULT
  236    }
  237}
  238
  239actions!(
  240    workspace,
  241    [
  242        /// Activates the next pane in the workspace.
  243        ActivateNextPane,
  244        /// Activates the previous pane in the workspace.
  245        ActivatePreviousPane,
  246        /// Activates the last pane in the workspace.
  247        ActivateLastPane,
  248        /// Switches to the next window.
  249        ActivateNextWindow,
  250        /// Switches to the previous window.
  251        ActivatePreviousWindow,
  252        /// Adds a folder to the current project.
  253        AddFolderToProject,
  254        /// Clears all notifications.
  255        ClearAllNotifications,
  256        /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**.
  257        ClearNavigationHistory,
  258        /// Closes the active dock.
  259        CloseActiveDock,
  260        /// Closes all docks.
  261        CloseAllDocks,
  262        /// Toggles all docks.
  263        ToggleAllDocks,
  264        /// Closes the current window.
  265        CloseWindow,
  266        /// Closes the current project.
  267        CloseProject,
  268        /// Opens the feedback dialog.
  269        Feedback,
  270        /// Follows the next collaborator in the session.
  271        FollowNextCollaborator,
  272        /// Moves the focused panel to the next position.
  273        MoveFocusedPanelToNextPosition,
  274        /// Creates a new file.
  275        NewFile,
  276        /// Creates a new file in a vertical split.
  277        NewFileSplitVertical,
  278        /// Creates a new file in a horizontal split.
  279        NewFileSplitHorizontal,
  280        /// Opens a new search.
  281        NewSearch,
  282        /// Opens a new window.
  283        NewWindow,
  284        /// Opens multiple files.
  285        OpenFiles,
  286        /// Opens the current location in terminal.
  287        OpenInTerminal,
  288        /// Opens the component preview.
  289        OpenComponentPreview,
  290        /// Reloads the active item.
  291        ReloadActiveItem,
  292        /// Resets the active dock to its default size.
  293        ResetActiveDockSize,
  294        /// Resets all open docks to their default sizes.
  295        ResetOpenDocksSize,
  296        /// Reloads the application
  297        Reload,
  298        /// Saves the current file with a new name.
  299        SaveAs,
  300        /// Saves without formatting.
  301        SaveWithoutFormat,
  302        /// Shuts down all debug adapters.
  303        ShutdownDebugAdapters,
  304        /// Suppresses the current notification.
  305        SuppressNotification,
  306        /// Toggles the bottom dock.
  307        ToggleBottomDock,
  308        /// Toggles centered layout mode.
  309        ToggleCenteredLayout,
  310        /// Toggles edit prediction feature globally for all files.
  311        ToggleEditPrediction,
  312        /// Toggles the left dock.
  313        ToggleLeftDock,
  314        /// Toggles the right dock.
  315        ToggleRightDock,
  316        /// Toggles zoom on the active pane.
  317        ToggleZoom,
  318        /// Toggles read-only mode for the active item (if supported by that item).
  319        ToggleReadOnlyFile,
  320        /// Zooms in on the active pane.
  321        ZoomIn,
  322        /// Zooms out of the active pane.
  323        ZoomOut,
  324        /// If any worktrees are in restricted mode, shows a modal with possible actions.
  325        /// If the modal is shown already, closes it without trusting any worktree.
  326        ToggleWorktreeSecurity,
  327        /// Clears all trusted worktrees, placing them in restricted mode on next open.
  328        /// Requires restart to take effect on already opened projects.
  329        ClearTrustedWorktrees,
  330        /// Stops following a collaborator.
  331        Unfollow,
  332        /// Restores the banner.
  333        RestoreBanner,
  334        /// Toggles expansion of the selected item.
  335        ToggleExpandItem,
  336    ]
  337);
  338
  339/// Activates a specific pane by its index.
  340#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
  341#[action(namespace = workspace)]
  342pub struct ActivatePane(pub usize);
  343
  344/// Moves an item to a specific pane by index.
  345#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
  346#[action(namespace = workspace)]
  347#[serde(deny_unknown_fields)]
  348pub struct MoveItemToPane {
  349    #[serde(default = "default_1")]
  350    pub destination: usize,
  351    #[serde(default = "default_true")]
  352    pub focus: bool,
  353    #[serde(default)]
  354    pub clone: bool,
  355}
  356
  357fn default_1() -> usize {
  358    1
  359}
  360
  361/// Moves an item to a pane in the specified direction.
  362#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
  363#[action(namespace = workspace)]
  364#[serde(deny_unknown_fields)]
  365pub struct MoveItemToPaneInDirection {
  366    #[serde(default = "default_right")]
  367    pub direction: SplitDirection,
  368    #[serde(default = "default_true")]
  369    pub focus: bool,
  370    #[serde(default)]
  371    pub clone: bool,
  372}
  373
  374/// Creates a new file in a split of the desired direction.
  375#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
  376#[action(namespace = workspace)]
  377#[serde(deny_unknown_fields)]
  378pub struct NewFileSplit(pub SplitDirection);
  379
  380fn default_right() -> SplitDirection {
  381    SplitDirection::Right
  382}
  383
  384/// Saves all open files in the workspace.
  385#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
  386#[action(namespace = workspace)]
  387#[serde(deny_unknown_fields)]
  388pub struct SaveAll {
  389    #[serde(default)]
  390    pub save_intent: Option<SaveIntent>,
  391}
  392
  393/// Saves the current file with the specified options.
  394#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
  395#[action(namespace = workspace)]
  396#[serde(deny_unknown_fields)]
  397pub struct Save {
  398    #[serde(default)]
  399    pub save_intent: Option<SaveIntent>,
  400}
  401
  402/// Moves Focus to the central panes in the workspace.
  403#[derive(Clone, Debug, PartialEq, Eq, Action)]
  404#[action(namespace = workspace)]
  405pub struct FocusCenterPane;
  406
  407///  Closes all items and panes in the workspace.
  408#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
  409#[action(namespace = workspace)]
  410#[serde(deny_unknown_fields)]
  411pub struct CloseAllItemsAndPanes {
  412    #[serde(default)]
  413    pub save_intent: Option<SaveIntent>,
  414}
  415
  416/// Closes all inactive tabs and panes in the workspace.
  417#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
  418#[action(namespace = workspace)]
  419#[serde(deny_unknown_fields)]
  420pub struct CloseInactiveTabsAndPanes {
  421    #[serde(default)]
  422    pub save_intent: Option<SaveIntent>,
  423}
  424
  425/// Closes the active item across all panes.
  426#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
  427#[action(namespace = workspace)]
  428#[serde(deny_unknown_fields)]
  429pub struct CloseItemInAllPanes {
  430    #[serde(default)]
  431    pub save_intent: Option<SaveIntent>,
  432    #[serde(default)]
  433    pub close_pinned: bool,
  434}
  435
  436/// Sends a sequence of keystrokes to the active element.
  437#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
  438#[action(namespace = workspace)]
  439pub struct SendKeystrokes(pub String);
  440
  441actions!(
  442    project_symbols,
  443    [
  444        /// Toggles the project symbols search.
  445        #[action(name = "Toggle")]
  446        ToggleProjectSymbols
  447    ]
  448);
  449
  450/// Toggles the file finder interface.
  451#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
  452#[action(namespace = file_finder, name = "Toggle")]
  453#[serde(deny_unknown_fields)]
  454pub struct ToggleFileFinder {
  455    #[serde(default)]
  456    pub separate_history: bool,
  457}
  458
  459/// Opens a new terminal in the center.
  460#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
  461#[action(namespace = workspace)]
  462#[serde(deny_unknown_fields)]
  463pub struct NewCenterTerminal {
  464    /// If true, creates a local terminal even in remote projects.
  465    #[serde(default)]
  466    pub local: bool,
  467}
  468
  469/// Opens a new terminal.
  470#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
  471#[action(namespace = workspace)]
  472#[serde(deny_unknown_fields)]
  473pub struct NewTerminal {
  474    /// If true, creates a local terminal even in remote projects.
  475    #[serde(default)]
  476    pub local: bool,
  477}
  478
  479/// Increases size of a currently focused dock by a given amount of pixels.
  480#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
  481#[action(namespace = workspace)]
  482#[serde(deny_unknown_fields)]
  483pub struct IncreaseActiveDockSize {
  484    /// For 0px parameter, uses UI font size value.
  485    #[serde(default)]
  486    pub px: u32,
  487}
  488
  489/// Decreases size of a currently focused dock by a given amount of pixels.
  490#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
  491#[action(namespace = workspace)]
  492#[serde(deny_unknown_fields)]
  493pub struct DecreaseActiveDockSize {
  494    /// For 0px parameter, uses UI font size value.
  495    #[serde(default)]
  496    pub px: u32,
  497}
  498
  499/// Increases size of all currently visible docks uniformly, by a given amount of pixels.
  500#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
  501#[action(namespace = workspace)]
  502#[serde(deny_unknown_fields)]
  503pub struct IncreaseOpenDocksSize {
  504    /// For 0px parameter, uses UI font size value.
  505    #[serde(default)]
  506    pub px: u32,
  507}
  508
  509/// Decreases size of all currently visible docks uniformly, by a given amount of pixels.
  510#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
  511#[action(namespace = workspace)]
  512#[serde(deny_unknown_fields)]
  513pub struct DecreaseOpenDocksSize {
  514    /// For 0px parameter, uses UI font size value.
  515    #[serde(default)]
  516    pub px: u32,
  517}
  518
  519actions!(
  520    workspace,
  521    [
  522        /// Activates the pane to the left.
  523        ActivatePaneLeft,
  524        /// Activates the pane to the right.
  525        ActivatePaneRight,
  526        /// Activates the pane above.
  527        ActivatePaneUp,
  528        /// Activates the pane below.
  529        ActivatePaneDown,
  530        /// Swaps the current pane with the one to the left.
  531        SwapPaneLeft,
  532        /// Swaps the current pane with the one to the right.
  533        SwapPaneRight,
  534        /// Swaps the current pane with the one above.
  535        SwapPaneUp,
  536        /// Swaps the current pane with the one below.
  537        SwapPaneDown,
  538        // Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane.
  539        SwapPaneAdjacent,
  540        /// Move the current pane to be at the far left.
  541        MovePaneLeft,
  542        /// Move the current pane to be at the far right.
  543        MovePaneRight,
  544        /// Move the current pane to be at the very top.
  545        MovePaneUp,
  546        /// Move the current pane to be at the very bottom.
  547        MovePaneDown,
  548    ]
  549);
  550
  551#[derive(PartialEq, Eq, Debug)]
  552pub enum CloseIntent {
  553    /// Quit the program entirely.
  554    Quit,
  555    /// Close a window.
  556    CloseWindow,
  557    /// Replace the workspace in an existing window.
  558    ReplaceWindow,
  559}
  560
  561#[derive(Clone)]
  562pub struct Toast {
  563    id: NotificationId,
  564    msg: Cow<'static, str>,
  565    autohide: bool,
  566    on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
  567}
  568
  569impl Toast {
  570    pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
  571        Toast {
  572            id,
  573            msg: msg.into(),
  574            on_click: None,
  575            autohide: false,
  576        }
  577    }
  578
  579    pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
  580    where
  581        M: Into<Cow<'static, str>>,
  582        F: Fn(&mut Window, &mut App) + 'static,
  583    {
  584        self.on_click = Some((message.into(), Arc::new(on_click)));
  585        self
  586    }
  587
  588    pub fn autohide(mut self) -> Self {
  589        self.autohide = true;
  590        self
  591    }
  592}
  593
  594impl PartialEq for Toast {
  595    fn eq(&self, other: &Self) -> bool {
  596        self.id == other.id
  597            && self.msg == other.msg
  598            && self.on_click.is_some() == other.on_click.is_some()
  599    }
  600}
  601
  602/// Opens a new terminal with the specified working directory.
  603#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)]
  604#[action(namespace = workspace)]
  605#[serde(deny_unknown_fields)]
  606pub struct OpenTerminal {
  607    pub working_directory: PathBuf,
  608    /// If true, creates a local terminal even in remote projects.
  609    #[serde(default)]
  610    pub local: bool,
  611}
  612
  613#[derive(
  614    Clone,
  615    Copy,
  616    Debug,
  617    Default,
  618    Hash,
  619    PartialEq,
  620    Eq,
  621    PartialOrd,
  622    Ord,
  623    serde::Serialize,
  624    serde::Deserialize,
  625)]
  626pub struct WorkspaceId(i64);
  627
  628impl WorkspaceId {
  629    pub fn from_i64(value: i64) -> Self {
  630        Self(value)
  631    }
  632}
  633
  634impl StaticColumnCount for WorkspaceId {}
  635impl Bind for WorkspaceId {
  636    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
  637        self.0.bind(statement, start_index)
  638    }
  639}
  640impl Column for WorkspaceId {
  641    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
  642        i64::column(statement, start_index)
  643            .map(|(i, next_index)| (Self(i), next_index))
  644            .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
  645    }
  646}
  647impl From<WorkspaceId> for i64 {
  648    fn from(val: WorkspaceId) -> Self {
  649        val.0
  650    }
  651}
  652
  653fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, cx: &mut App) {
  654    if let Some(workspace_window) = local_workspace_windows(cx).into_iter().next() {
  655        workspace_window
  656            .update(cx, |multi_workspace, window, cx| {
  657                let workspace = multi_workspace.workspace().clone();
  658                workspace.update(cx, |workspace, cx| {
  659                    prompt_for_open_path_and_open(workspace, app_state, options, true, window, cx);
  660                });
  661            })
  662            .ok();
  663    } else {
  664        let task = Workspace::new_local(Vec::new(), app_state.clone(), None, None, None, true, cx);
  665        cx.spawn(async move |cx| {
  666            let OpenResult { window, .. } = task.await?;
  667            window.update(cx, |multi_workspace, window, cx| {
  668                window.activate_window();
  669                let workspace = multi_workspace.workspace().clone();
  670                workspace.update(cx, |workspace, cx| {
  671                    prompt_for_open_path_and_open(workspace, app_state, options, true, window, cx);
  672                });
  673            })?;
  674            anyhow::Ok(())
  675        })
  676        .detach_and_log_err(cx);
  677    }
  678}
  679
  680pub fn prompt_for_open_path_and_open(
  681    workspace: &mut Workspace,
  682    app_state: Arc<AppState>,
  683    options: PathPromptOptions,
  684    create_new_window: bool,
  685    window: &mut Window,
  686    cx: &mut Context<Workspace>,
  687) {
  688    let paths = workspace.prompt_for_open_path(
  689        options,
  690        DirectoryLister::Local(workspace.project().clone(), app_state.fs.clone()),
  691        window,
  692        cx,
  693    );
  694    let multi_workspace_handle = window.window_handle().downcast::<MultiWorkspace>();
  695    cx.spawn_in(window, async move |this, cx| {
  696        let Some(paths) = paths.await.log_err().flatten() else {
  697            return;
  698        };
  699        if !create_new_window {
  700            if let Some(handle) = multi_workspace_handle {
  701                if let Some(task) = handle
  702                    .update(cx, |multi_workspace, window, cx| {
  703                        multi_workspace.open_project(paths, window, cx)
  704                    })
  705                    .log_err()
  706                {
  707                    task.await.log_err();
  708                }
  709                return;
  710            }
  711        }
  712        if let Some(task) = this
  713            .update_in(cx, |this, window, cx| {
  714                this.open_workspace_for_paths(false, paths, window, cx)
  715            })
  716            .log_err()
  717        {
  718            task.await.log_err();
  719        }
  720    })
  721    .detach();
  722}
  723
  724pub fn init(app_state: Arc<AppState>, cx: &mut App) {
  725    component::init();
  726    theme_preview::init(cx);
  727    toast_layer::init(cx);
  728    history_manager::init(app_state.fs.clone(), cx);
  729
  730    cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
  731        .on_action(|_: &Reload, cx| reload(cx))
  732        .on_action({
  733            let app_state = Arc::downgrade(&app_state);
  734            move |_: &Open, cx: &mut App| {
  735                if let Some(app_state) = app_state.upgrade() {
  736                    prompt_and_open_paths(
  737                        app_state,
  738                        PathPromptOptions {
  739                            files: true,
  740                            directories: true,
  741                            multiple: true,
  742                            prompt: None,
  743                        },
  744                        cx,
  745                    );
  746                }
  747            }
  748        })
  749        .on_action({
  750            let app_state = Arc::downgrade(&app_state);
  751            move |_: &OpenFiles, cx: &mut App| {
  752                let directories = cx.can_select_mixed_files_and_dirs();
  753                if let Some(app_state) = app_state.upgrade() {
  754                    prompt_and_open_paths(
  755                        app_state,
  756                        PathPromptOptions {
  757                            files: true,
  758                            directories,
  759                            multiple: true,
  760                            prompt: None,
  761                        },
  762                        cx,
  763                    );
  764                }
  765            }
  766        });
  767}
  768
  769type BuildProjectItemFn =
  770    fn(AnyEntity, Entity<Project>, Option<&Pane>, &mut Window, &mut App) -> Box<dyn ItemHandle>;
  771
  772type BuildProjectItemForPathFn =
  773    fn(
  774        &Entity<Project>,
  775        &ProjectPath,
  776        &mut Window,
  777        &mut App,
  778    ) -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
  779
  780#[derive(Clone, Default)]
  781struct ProjectItemRegistry {
  782    build_project_item_fns_by_type: HashMap<TypeId, BuildProjectItemFn>,
  783    build_project_item_for_path_fns: Vec<BuildProjectItemForPathFn>,
  784}
  785
  786impl ProjectItemRegistry {
  787    fn register<T: ProjectItem>(&mut self) {
  788        self.build_project_item_fns_by_type.insert(
  789            TypeId::of::<T::Item>(),
  790            |item, project, pane, window, cx| {
  791                let item = item.downcast().unwrap();
  792                Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
  793                    as Box<dyn ItemHandle>
  794            },
  795        );
  796        self.build_project_item_for_path_fns
  797            .push(|project, project_path, window, cx| {
  798                let project_path = project_path.clone();
  799                let is_file = project
  800                    .read(cx)
  801                    .entry_for_path(&project_path, cx)
  802                    .is_some_and(|entry| entry.is_file());
  803                let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
  804                let is_local = project.read(cx).is_local();
  805                let project_item =
  806                    <T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
  807                let project = project.clone();
  808                Some(window.spawn(cx, async move |cx| {
  809                    match project_item.await.with_context(|| {
  810                        format!(
  811                            "opening project path {:?}",
  812                            entry_abs_path.as_deref().unwrap_or(&project_path.path.as_std_path())
  813                        )
  814                    }) {
  815                        Ok(project_item) => {
  816                            let project_item = project_item;
  817                            let project_entry_id: Option<ProjectEntryId> =
  818                                project_item.read_with(cx, project::ProjectItem::entry_id);
  819                            let build_workspace_item = Box::new(
  820                                |pane: &mut Pane, window: &mut Window, cx: &mut Context<Pane>| {
  821                                    Box::new(cx.new(|cx| {
  822                                        T::for_project_item(
  823                                            project,
  824                                            Some(pane),
  825                                            project_item,
  826                                            window,
  827                                            cx,
  828                                        )
  829                                    })) as Box<dyn ItemHandle>
  830                                },
  831                            ) as Box<_>;
  832                            Ok((project_entry_id, build_workspace_item))
  833                        }
  834                        Err(e) => {
  835                            log::warn!("Failed to open a project item: {e:#}");
  836                            if e.error_code() == ErrorCode::Internal {
  837                                if let Some(abs_path) =
  838                                    entry_abs_path.as_deref().filter(|_| is_file)
  839                                {
  840                                    if let Some(broken_project_item_view) =
  841                                        cx.update(|window, cx| {
  842                                            T::for_broken_project_item(
  843                                                abs_path, is_local, &e, window, cx,
  844                                            )
  845                                        })?
  846                                    {
  847                                        let build_workspace_item = Box::new(
  848                                            move |_: &mut Pane, _: &mut Window, cx: &mut Context<Pane>| {
  849                                                cx.new(|_| broken_project_item_view).boxed_clone()
  850                                            },
  851                                        )
  852                                        as Box<_>;
  853                                        return Ok((None, build_workspace_item));
  854                                    }
  855                                }
  856                            }
  857                            Err(e)
  858                        }
  859                    }
  860                }))
  861            });
  862    }
  863
  864    fn open_path(
  865        &self,
  866        project: &Entity<Project>,
  867        path: &ProjectPath,
  868        window: &mut Window,
  869        cx: &mut App,
  870    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
  871        let Some(open_project_item) = self
  872            .build_project_item_for_path_fns
  873            .iter()
  874            .rev()
  875            .find_map(|open_project_item| open_project_item(project, path, window, cx))
  876        else {
  877            return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
  878        };
  879        open_project_item
  880    }
  881
  882    fn build_item<T: project::ProjectItem>(
  883        &self,
  884        item: Entity<T>,
  885        project: Entity<Project>,
  886        pane: Option<&Pane>,
  887        window: &mut Window,
  888        cx: &mut App,
  889    ) -> Option<Box<dyn ItemHandle>> {
  890        let build = self
  891            .build_project_item_fns_by_type
  892            .get(&TypeId::of::<T>())?;
  893        Some(build(item.into_any(), project, pane, window, cx))
  894    }
  895}
  896
  897type WorkspaceItemBuilder =
  898    Box<dyn FnOnce(&mut Pane, &mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
  899
  900impl Global for ProjectItemRegistry {}
  901
  902/// Registers a [ProjectItem] for the app. When opening a file, all the registered
  903/// items will get a chance to open the file, starting from the project item that
  904/// was added last.
  905pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
  906    cx.default_global::<ProjectItemRegistry>().register::<I>();
  907}
  908
  909#[derive(Default)]
  910pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
  911
  912struct FollowableViewDescriptor {
  913    from_state_proto: fn(
  914        Entity<Workspace>,
  915        ViewId,
  916        &mut Option<proto::view::Variant>,
  917        &mut Window,
  918        &mut App,
  919    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
  920    to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
  921}
  922
  923impl Global for FollowableViewRegistry {}
  924
  925impl FollowableViewRegistry {
  926    pub fn register<I: FollowableItem>(cx: &mut App) {
  927        cx.default_global::<Self>().0.insert(
  928            TypeId::of::<I>(),
  929            FollowableViewDescriptor {
  930                from_state_proto: |workspace, id, state, window, cx| {
  931                    I::from_state_proto(workspace, id, state, window, cx).map(|task| {
  932                        cx.foreground_executor()
  933                            .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
  934                    })
  935                },
  936                to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
  937            },
  938        );
  939    }
  940
  941    pub fn from_state_proto(
  942        workspace: Entity<Workspace>,
  943        view_id: ViewId,
  944        mut state: Option<proto::view::Variant>,
  945        window: &mut Window,
  946        cx: &mut App,
  947    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
  948        cx.update_default_global(|this: &mut Self, cx| {
  949            this.0.values().find_map(|descriptor| {
  950                (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
  951            })
  952        })
  953    }
  954
  955    pub fn to_followable_view(
  956        view: impl Into<AnyView>,
  957        cx: &App,
  958    ) -> Option<Box<dyn FollowableItemHandle>> {
  959        let this = cx.try_global::<Self>()?;
  960        let view = view.into();
  961        let descriptor = this.0.get(&view.entity_type())?;
  962        Some((descriptor.to_followable_view)(&view))
  963    }
  964}
  965
  966#[derive(Copy, Clone)]
  967struct SerializableItemDescriptor {
  968    deserialize: fn(
  969        Entity<Project>,
  970        WeakEntity<Workspace>,
  971        WorkspaceId,
  972        ItemId,
  973        &mut Window,
  974        &mut Context<Pane>,
  975    ) -> Task<Result<Box<dyn ItemHandle>>>,
  976    cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
  977    view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
  978}
  979
  980#[derive(Default)]
  981struct SerializableItemRegistry {
  982    descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
  983    descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
  984}
  985
  986impl Global for SerializableItemRegistry {}
  987
  988impl SerializableItemRegistry {
  989    fn deserialize(
  990        item_kind: &str,
  991        project: Entity<Project>,
  992        workspace: WeakEntity<Workspace>,
  993        workspace_id: WorkspaceId,
  994        item_item: ItemId,
  995        window: &mut Window,
  996        cx: &mut Context<Pane>,
  997    ) -> Task<Result<Box<dyn ItemHandle>>> {
  998        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
  999            return Task::ready(Err(anyhow!(
 1000                "cannot deserialize {}, descriptor not found",
 1001                item_kind
 1002            )));
 1003        };
 1004
 1005        (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
 1006    }
 1007
 1008    fn cleanup(
 1009        item_kind: &str,
 1010        workspace_id: WorkspaceId,
 1011        loaded_items: Vec<ItemId>,
 1012        window: &mut Window,
 1013        cx: &mut App,
 1014    ) -> Task<Result<()>> {
 1015        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
 1016            return Task::ready(Err(anyhow!(
 1017                "cannot cleanup {}, descriptor not found",
 1018                item_kind
 1019            )));
 1020        };
 1021
 1022        (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
 1023    }
 1024
 1025    fn view_to_serializable_item_handle(
 1026        view: AnyView,
 1027        cx: &App,
 1028    ) -> Option<Box<dyn SerializableItemHandle>> {
 1029        let this = cx.try_global::<Self>()?;
 1030        let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
 1031        Some((descriptor.view_to_serializable_item)(view))
 1032    }
 1033
 1034    fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
 1035        let this = cx.try_global::<Self>()?;
 1036        this.descriptors_by_kind.get(item_kind).copied()
 1037    }
 1038}
 1039
 1040pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
 1041    let serialized_item_kind = I::serialized_item_kind();
 1042
 1043    let registry = cx.default_global::<SerializableItemRegistry>();
 1044    let descriptor = SerializableItemDescriptor {
 1045        deserialize: |project, workspace, workspace_id, item_id, window, cx| {
 1046            let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
 1047            cx.foreground_executor()
 1048                .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
 1049        },
 1050        cleanup: |workspace_id, loaded_items, window, cx| {
 1051            I::cleanup(workspace_id, loaded_items, window, cx)
 1052        },
 1053        view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
 1054    };
 1055    registry
 1056        .descriptors_by_kind
 1057        .insert(Arc::from(serialized_item_kind), descriptor);
 1058    registry
 1059        .descriptors_by_type
 1060        .insert(TypeId::of::<I>(), descriptor);
 1061}
 1062
 1063pub struct AppState {
 1064    pub languages: Arc<LanguageRegistry>,
 1065    pub client: Arc<Client>,
 1066    pub user_store: Entity<UserStore>,
 1067    pub workspace_store: Entity<WorkspaceStore>,
 1068    pub fs: Arc<dyn fs::Fs>,
 1069    pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
 1070    pub node_runtime: NodeRuntime,
 1071    pub session: Entity<AppSession>,
 1072}
 1073
 1074struct GlobalAppState(Weak<AppState>);
 1075
 1076impl Global for GlobalAppState {}
 1077
 1078pub struct WorkspaceStore {
 1079    workspaces: HashSet<(gpui::AnyWindowHandle, WeakEntity<Workspace>)>,
 1080    client: Arc<Client>,
 1081    _subscriptions: Vec<client::Subscription>,
 1082}
 1083
 1084#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
 1085pub enum CollaboratorId {
 1086    PeerId(PeerId),
 1087    Agent,
 1088}
 1089
 1090impl From<PeerId> for CollaboratorId {
 1091    fn from(peer_id: PeerId) -> Self {
 1092        CollaboratorId::PeerId(peer_id)
 1093    }
 1094}
 1095
 1096impl From<&PeerId> for CollaboratorId {
 1097    fn from(peer_id: &PeerId) -> Self {
 1098        CollaboratorId::PeerId(*peer_id)
 1099    }
 1100}
 1101
 1102#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
 1103struct Follower {
 1104    project_id: Option<u64>,
 1105    peer_id: PeerId,
 1106}
 1107
 1108impl AppState {
 1109    #[track_caller]
 1110    pub fn global(cx: &App) -> Weak<Self> {
 1111        cx.global::<GlobalAppState>().0.clone()
 1112    }
 1113    pub fn try_global(cx: &App) -> Option<Weak<Self>> {
 1114        cx.try_global::<GlobalAppState>()
 1115            .map(|state| state.0.clone())
 1116    }
 1117    pub fn set_global(state: Weak<AppState>, cx: &mut App) {
 1118        cx.set_global(GlobalAppState(state));
 1119    }
 1120
 1121    #[cfg(any(test, feature = "test-support"))]
 1122    pub fn test(cx: &mut App) -> Arc<Self> {
 1123        use fs::Fs;
 1124        use node_runtime::NodeRuntime;
 1125        use session::Session;
 1126        use settings::SettingsStore;
 1127
 1128        if !cx.has_global::<SettingsStore>() {
 1129            let settings_store = SettingsStore::test(cx);
 1130            cx.set_global(settings_store);
 1131        }
 1132
 1133        let fs = fs::FakeFs::new(cx.background_executor().clone());
 1134        <dyn Fs>::set_global(fs.clone(), cx);
 1135        let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
 1136        let clock = Arc::new(clock::FakeSystemClock::new());
 1137        let http_client = http_client::FakeHttpClient::with_404_response();
 1138        let client = Client::new(clock, http_client, cx);
 1139        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
 1140        let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
 1141        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
 1142
 1143        theme::init(theme::LoadThemes::JustBase, cx);
 1144        client::init(&client, cx);
 1145
 1146        Arc::new(Self {
 1147            client,
 1148            fs,
 1149            languages,
 1150            user_store,
 1151            workspace_store,
 1152            node_runtime: NodeRuntime::unavailable(),
 1153            build_window_options: |_, _| Default::default(),
 1154            session,
 1155        })
 1156    }
 1157}
 1158
 1159struct DelayedDebouncedEditAction {
 1160    task: Option<Task<()>>,
 1161    cancel_channel: Option<oneshot::Sender<()>>,
 1162}
 1163
 1164impl DelayedDebouncedEditAction {
 1165    fn new() -> DelayedDebouncedEditAction {
 1166        DelayedDebouncedEditAction {
 1167            task: None,
 1168            cancel_channel: None,
 1169        }
 1170    }
 1171
 1172    fn fire_new<F>(
 1173        &mut self,
 1174        delay: Duration,
 1175        window: &mut Window,
 1176        cx: &mut Context<Workspace>,
 1177        func: F,
 1178    ) where
 1179        F: 'static
 1180            + Send
 1181            + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
 1182    {
 1183        if let Some(channel) = self.cancel_channel.take() {
 1184            _ = channel.send(());
 1185        }
 1186
 1187        let (sender, mut receiver) = oneshot::channel::<()>();
 1188        self.cancel_channel = Some(sender);
 1189
 1190        let previous_task = self.task.take();
 1191        self.task = Some(cx.spawn_in(window, async move |workspace, cx| {
 1192            let mut timer = cx.background_executor().timer(delay).fuse();
 1193            if let Some(previous_task) = previous_task {
 1194                previous_task.await;
 1195            }
 1196
 1197            futures::select_biased! {
 1198                _ = receiver => return,
 1199                    _ = timer => {}
 1200            }
 1201
 1202            if let Some(result) = workspace
 1203                .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx))
 1204                .log_err()
 1205            {
 1206                result.await.log_err();
 1207            }
 1208        }));
 1209    }
 1210}
 1211
 1212pub enum Event {
 1213    PaneAdded(Entity<Pane>),
 1214    PaneRemoved,
 1215    ItemAdded {
 1216        item: Box<dyn ItemHandle>,
 1217    },
 1218    ActiveItemChanged,
 1219    ItemRemoved {
 1220        item_id: EntityId,
 1221    },
 1222    UserSavedItem {
 1223        pane: WeakEntity<Pane>,
 1224        item: Box<dyn WeakItemHandle>,
 1225        save_intent: SaveIntent,
 1226    },
 1227    ContactRequestedJoin(u64),
 1228    WorkspaceCreated(WeakEntity<Workspace>),
 1229    OpenBundledFile {
 1230        text: Cow<'static, str>,
 1231        title: &'static str,
 1232        language: &'static str,
 1233    },
 1234    ZoomChanged,
 1235    ModalOpened,
 1236    Activate,
 1237    PanelAdded(AnyView),
 1238}
 1239
 1240#[derive(Debug, Clone)]
 1241pub enum OpenVisible {
 1242    All,
 1243    None,
 1244    OnlyFiles,
 1245    OnlyDirectories,
 1246}
 1247
 1248enum WorkspaceLocation {
 1249    // Valid local paths or SSH project to serialize
 1250    Location(SerializedWorkspaceLocation, PathList),
 1251    // No valid location found hence clear session id
 1252    DetachFromSession,
 1253    // No valid location found to serialize
 1254    None,
 1255}
 1256
 1257type PromptForNewPath = Box<
 1258    dyn Fn(
 1259        &mut Workspace,
 1260        DirectoryLister,
 1261        Option<String>,
 1262        &mut Window,
 1263        &mut Context<Workspace>,
 1264    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
 1265>;
 1266
 1267type PromptForOpenPath = Box<
 1268    dyn Fn(
 1269        &mut Workspace,
 1270        DirectoryLister,
 1271        &mut Window,
 1272        &mut Context<Workspace>,
 1273    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
 1274>;
 1275
 1276#[derive(Default)]
 1277struct DispatchingKeystrokes {
 1278    dispatched: HashSet<Vec<Keystroke>>,
 1279    queue: VecDeque<Keystroke>,
 1280    task: Option<Shared<Task<()>>>,
 1281}
 1282
 1283/// Collects everything project-related for a certain window opened.
 1284/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
 1285///
 1286/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
 1287/// The `Workspace` owns everybody's state and serves as a default, "global context",
 1288/// that can be used to register a global action to be triggered from any place in the window.
 1289pub struct Workspace {
 1290    weak_self: WeakEntity<Self>,
 1291    workspace_actions: Vec<Box<dyn Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div>>,
 1292    zoomed: Option<AnyWeakView>,
 1293    previous_dock_drag_coordinates: Option<Point<Pixels>>,
 1294    zoomed_position: Option<DockPosition>,
 1295    center: PaneGroup,
 1296    left_dock: Entity<Dock>,
 1297    bottom_dock: Entity<Dock>,
 1298    right_dock: Entity<Dock>,
 1299    panes: Vec<Entity<Pane>>,
 1300    active_worktree_override: Option<WorktreeId>,
 1301    panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
 1302    active_pane: Entity<Pane>,
 1303    last_active_center_pane: Option<WeakEntity<Pane>>,
 1304    last_active_view_id: Option<proto::ViewId>,
 1305    status_bar: Entity<StatusBar>,
 1306    pub(crate) modal_layer: Entity<ModalLayer>,
 1307    toast_layer: Entity<ToastLayer>,
 1308    titlebar_item: Option<AnyView>,
 1309    notifications: Notifications,
 1310    suppressed_notifications: HashSet<NotificationId>,
 1311    project: Entity<Project>,
 1312    follower_states: HashMap<CollaboratorId, FollowerState>,
 1313    last_leaders_by_pane: HashMap<WeakEntity<Pane>, CollaboratorId>,
 1314    window_edited: bool,
 1315    last_window_title: Option<String>,
 1316    dirty_items: HashMap<EntityId, Subscription>,
 1317    active_call: Option<(GlobalAnyActiveCall, Vec<Subscription>)>,
 1318    leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
 1319    database_id: Option<WorkspaceId>,
 1320    app_state: Arc<AppState>,
 1321    dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
 1322    _subscriptions: Vec<Subscription>,
 1323    _apply_leader_updates: Task<Result<()>>,
 1324    _observe_current_user: Task<Result<()>>,
 1325    _schedule_serialize_workspace: Option<Task<()>>,
 1326    _serialize_workspace_task: Option<Task<()>>,
 1327    _schedule_serialize_ssh_paths: Option<Task<()>>,
 1328    pane_history_timestamp: Arc<AtomicUsize>,
 1329    bounds: Bounds<Pixels>,
 1330    pub centered_layout: bool,
 1331    bounds_save_task_queued: Option<Task<()>>,
 1332    on_prompt_for_new_path: Option<PromptForNewPath>,
 1333    on_prompt_for_open_path: Option<PromptForOpenPath>,
 1334    terminal_provider: Option<Box<dyn TerminalProvider>>,
 1335    debugger_provider: Option<Arc<dyn DebuggerProvider>>,
 1336    serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
 1337    _items_serializer: Task<Result<()>>,
 1338    session_id: Option<String>,
 1339    scheduled_tasks: Vec<Task<()>>,
 1340    last_open_dock_positions: Vec<DockPosition>,
 1341    removing: bool,
 1342    _panels_task: Option<Task<Result<()>>>,
 1343    sidebar_focus_handle: Option<FocusHandle>,
 1344}
 1345
 1346impl EventEmitter<Event> for Workspace {}
 1347
 1348#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 1349pub struct ViewId {
 1350    pub creator: CollaboratorId,
 1351    pub id: u64,
 1352}
 1353
 1354pub struct FollowerState {
 1355    center_pane: Entity<Pane>,
 1356    dock_pane: Option<Entity<Pane>>,
 1357    active_view_id: Option<ViewId>,
 1358    items_by_leader_view_id: HashMap<ViewId, FollowerView>,
 1359}
 1360
 1361struct FollowerView {
 1362    view: Box<dyn FollowableItemHandle>,
 1363    location: Option<proto::PanelId>,
 1364}
 1365
 1366impl Workspace {
 1367    pub fn new(
 1368        workspace_id: Option<WorkspaceId>,
 1369        project: Entity<Project>,
 1370        app_state: Arc<AppState>,
 1371        window: &mut Window,
 1372        cx: &mut Context<Self>,
 1373    ) -> Self {
 1374        if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
 1375            cx.subscribe(&trusted_worktrees, |_, worktrees_store, e, cx| {
 1376                if let TrustedWorktreesEvent::Trusted(..) = e {
 1377                    // Do not persist auto trusted worktrees
 1378                    if !ProjectSettings::get_global(cx).session.trust_all_worktrees {
 1379                        worktrees_store.update(cx, |worktrees_store, cx| {
 1380                            worktrees_store.schedule_serialization(
 1381                                cx,
 1382                                |new_trusted_worktrees, cx| {
 1383                                    let timeout =
 1384                                        cx.background_executor().timer(SERIALIZATION_THROTTLE_TIME);
 1385                                    let db = WorkspaceDb::global(cx);
 1386                                    cx.background_spawn(async move {
 1387                                        timeout.await;
 1388                                        db.save_trusted_worktrees(new_trusted_worktrees)
 1389                                            .await
 1390                                            .log_err();
 1391                                    })
 1392                                },
 1393                            )
 1394                        });
 1395                    }
 1396                }
 1397            })
 1398            .detach();
 1399
 1400            cx.observe_global::<SettingsStore>(|_, cx| {
 1401                if ProjectSettings::get_global(cx).session.trust_all_worktrees {
 1402                    if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
 1403                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
 1404                            trusted_worktrees.auto_trust_all(cx);
 1405                        })
 1406                    }
 1407                }
 1408            })
 1409            .detach();
 1410        }
 1411
 1412        cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
 1413            match event {
 1414                project::Event::RemoteIdChanged(_) => {
 1415                    this.update_window_title(window, cx);
 1416                }
 1417
 1418                project::Event::CollaboratorLeft(peer_id) => {
 1419                    this.collaborator_left(*peer_id, window, cx);
 1420                }
 1421
 1422                &project::Event::WorktreeRemoved(_) => {
 1423                    this.update_window_title(window, cx);
 1424                    this.serialize_workspace(window, cx);
 1425                    this.update_history(cx);
 1426                }
 1427
 1428                &project::Event::WorktreeAdded(id) => {
 1429                    this.update_window_title(window, cx);
 1430                    if this
 1431                        .project()
 1432                        .read(cx)
 1433                        .worktree_for_id(id, cx)
 1434                        .is_some_and(|wt| wt.read(cx).is_visible())
 1435                    {
 1436                        this.serialize_workspace(window, cx);
 1437                        this.update_history(cx);
 1438                    }
 1439                }
 1440                project::Event::WorktreeUpdatedEntries(..) => {
 1441                    this.update_window_title(window, cx);
 1442                    this.serialize_workspace(window, cx);
 1443                }
 1444
 1445                project::Event::DisconnectedFromHost => {
 1446                    this.update_window_edited(window, cx);
 1447                    let leaders_to_unfollow =
 1448                        this.follower_states.keys().copied().collect::<Vec<_>>();
 1449                    for leader_id in leaders_to_unfollow {
 1450                        this.unfollow(leader_id, window, cx);
 1451                    }
 1452                }
 1453
 1454                project::Event::DisconnectedFromRemote {
 1455                    server_not_running: _,
 1456                } => {
 1457                    this.update_window_edited(window, cx);
 1458                }
 1459
 1460                project::Event::Closed => {
 1461                    window.remove_window();
 1462                }
 1463
 1464                project::Event::DeletedEntry(_, entry_id) => {
 1465                    for pane in this.panes.iter() {
 1466                        pane.update(cx, |pane, cx| {
 1467                            pane.handle_deleted_project_item(*entry_id, window, cx)
 1468                        });
 1469                    }
 1470                }
 1471
 1472                project::Event::Toast {
 1473                    notification_id,
 1474                    message,
 1475                    link,
 1476                } => this.show_notification(
 1477                    NotificationId::named(notification_id.clone()),
 1478                    cx,
 1479                    |cx| {
 1480                        let mut notification = MessageNotification::new(message.clone(), cx);
 1481                        if let Some(link) = link {
 1482                            notification = notification
 1483                                .more_info_message(link.label)
 1484                                .more_info_url(link.url);
 1485                        }
 1486
 1487                        cx.new(|_| notification)
 1488                    },
 1489                ),
 1490
 1491                project::Event::HideToast { notification_id } => {
 1492                    this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
 1493                }
 1494
 1495                project::Event::LanguageServerPrompt(request) => {
 1496                    struct LanguageServerPrompt;
 1497
 1498                    this.show_notification(
 1499                        NotificationId::composite::<LanguageServerPrompt>(request.id),
 1500                        cx,
 1501                        |cx| {
 1502                            cx.new(|cx| {
 1503                                notifications::LanguageServerPrompt::new(request.clone(), cx)
 1504                            })
 1505                        },
 1506                    );
 1507                }
 1508
 1509                project::Event::AgentLocationChanged => {
 1510                    this.handle_agent_location_changed(window, cx)
 1511                }
 1512
 1513                _ => {}
 1514            }
 1515            cx.notify()
 1516        })
 1517        .detach();
 1518
 1519        cx.subscribe_in(
 1520            &project.read(cx).breakpoint_store(),
 1521            window,
 1522            |workspace, _, event, window, cx| match event {
 1523                BreakpointStoreEvent::BreakpointsUpdated(_, _)
 1524                | BreakpointStoreEvent::BreakpointsCleared(_) => {
 1525                    workspace.serialize_workspace(window, cx);
 1526                }
 1527                BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
 1528            },
 1529        )
 1530        .detach();
 1531        if let Some(toolchain_store) = project.read(cx).toolchain_store() {
 1532            cx.subscribe_in(
 1533                &toolchain_store,
 1534                window,
 1535                |workspace, _, event, window, cx| match event {
 1536                    ToolchainStoreEvent::CustomToolchainsModified => {
 1537                        workspace.serialize_workspace(window, cx);
 1538                    }
 1539                    _ => {}
 1540                },
 1541            )
 1542            .detach();
 1543        }
 1544
 1545        cx.on_focus_lost(window, |this, window, cx| {
 1546            let focus_handle = this.focus_handle(cx);
 1547            window.focus(&focus_handle, cx);
 1548        })
 1549        .detach();
 1550
 1551        let weak_handle = cx.entity().downgrade();
 1552        let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
 1553
 1554        let center_pane = cx.new(|cx| {
 1555            let mut center_pane = Pane::new(
 1556                weak_handle.clone(),
 1557                project.clone(),
 1558                pane_history_timestamp.clone(),
 1559                None,
 1560                NewFile.boxed_clone(),
 1561                true,
 1562                window,
 1563                cx,
 1564            );
 1565            center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
 1566            center_pane.set_should_display_welcome_page(true);
 1567            center_pane
 1568        });
 1569        cx.subscribe_in(&center_pane, window, Self::handle_pane_event)
 1570            .detach();
 1571
 1572        window.focus(&center_pane.focus_handle(cx), cx);
 1573
 1574        cx.emit(Event::PaneAdded(center_pane.clone()));
 1575
 1576        let any_window_handle = window.window_handle();
 1577        app_state.workspace_store.update(cx, |store, _| {
 1578            store
 1579                .workspaces
 1580                .insert((any_window_handle, weak_handle.clone()));
 1581        });
 1582
 1583        let mut current_user = app_state.user_store.read(cx).watch_current_user();
 1584        let mut connection_status = app_state.client.status();
 1585        let _observe_current_user = cx.spawn_in(window, async move |this, cx| {
 1586            current_user.next().await;
 1587            connection_status.next().await;
 1588            let mut stream =
 1589                Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
 1590
 1591            while stream.recv().await.is_some() {
 1592                this.update(cx, |_, cx| cx.notify())?;
 1593            }
 1594            anyhow::Ok(())
 1595        });
 1596
 1597        // All leader updates are enqueued and then processed in a single task, so
 1598        // that each asynchronous operation can be run in order.
 1599        let (leader_updates_tx, mut leader_updates_rx) =
 1600            mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
 1601        let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| {
 1602            while let Some((leader_id, update)) = leader_updates_rx.next().await {
 1603                Self::process_leader_update(&this, leader_id, update, cx)
 1604                    .await
 1605                    .log_err();
 1606            }
 1607
 1608            Ok(())
 1609        });
 1610
 1611        cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
 1612        let modal_layer = cx.new(|_| ModalLayer::new());
 1613        let toast_layer = cx.new(|_| ToastLayer::new());
 1614        cx.subscribe(
 1615            &modal_layer,
 1616            |_, _, _: &modal_layer::ModalOpenedEvent, cx| {
 1617                cx.emit(Event::ModalOpened);
 1618            },
 1619        )
 1620        .detach();
 1621
 1622        let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
 1623        let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
 1624        let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
 1625        let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
 1626        let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
 1627        let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
 1628        let status_bar = cx.new(|cx| {
 1629            let mut status_bar = StatusBar::new(&center_pane.clone(), window, cx);
 1630            status_bar.add_left_item(left_dock_buttons, window, cx);
 1631            status_bar.add_right_item(right_dock_buttons, window, cx);
 1632            status_bar.add_right_item(bottom_dock_buttons, window, cx);
 1633            status_bar
 1634        });
 1635
 1636        let session_id = app_state.session.read(cx).id().to_owned();
 1637
 1638        let mut active_call = None;
 1639        if let Some(call) = GlobalAnyActiveCall::try_global(cx).cloned() {
 1640            let subscriptions =
 1641                vec![
 1642                    call.0
 1643                        .subscribe(window, cx, Box::new(Self::on_active_call_event)),
 1644                ];
 1645            active_call = Some((call, subscriptions));
 1646        }
 1647
 1648        let (serializable_items_tx, serializable_items_rx) =
 1649            mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
 1650        let _items_serializer = cx.spawn_in(window, async move |this, cx| {
 1651            Self::serialize_items(&this, serializable_items_rx, cx).await
 1652        });
 1653
 1654        let subscriptions = vec![
 1655            cx.observe_window_activation(window, Self::on_window_activation_changed),
 1656            cx.observe_window_bounds(window, move |this, window, cx| {
 1657                if this.bounds_save_task_queued.is_some() {
 1658                    return;
 1659                }
 1660                this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| {
 1661                    cx.background_executor()
 1662                        .timer(Duration::from_millis(100))
 1663                        .await;
 1664                    this.update_in(cx, |this, window, cx| {
 1665                        this.save_window_bounds(window, cx).detach();
 1666                        this.bounds_save_task_queued.take();
 1667                    })
 1668                    .ok();
 1669                }));
 1670                cx.notify();
 1671            }),
 1672            cx.observe_window_appearance(window, |_, window, cx| {
 1673                let window_appearance = window.appearance();
 1674
 1675                *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
 1676
 1677                GlobalTheme::reload_theme(cx);
 1678                GlobalTheme::reload_icon_theme(cx);
 1679            }),
 1680            cx.on_release({
 1681                let weak_handle = weak_handle.clone();
 1682                move |this, cx| {
 1683                    this.app_state.workspace_store.update(cx, move |store, _| {
 1684                        store.workspaces.retain(|(_, weak)| weak != &weak_handle);
 1685                    })
 1686                }
 1687            }),
 1688        ];
 1689
 1690        cx.defer_in(window, move |this, window, cx| {
 1691            this.update_window_title(window, cx);
 1692            this.show_initial_notifications(cx);
 1693        });
 1694
 1695        let mut center = PaneGroup::new(center_pane.clone());
 1696        center.set_is_center(true);
 1697        center.mark_positions(cx);
 1698
 1699        Workspace {
 1700            weak_self: weak_handle.clone(),
 1701            zoomed: None,
 1702            zoomed_position: None,
 1703            previous_dock_drag_coordinates: None,
 1704            center,
 1705            panes: vec![center_pane.clone()],
 1706            panes_by_item: Default::default(),
 1707            active_pane: center_pane.clone(),
 1708            last_active_center_pane: Some(center_pane.downgrade()),
 1709            last_active_view_id: None,
 1710            status_bar,
 1711            modal_layer,
 1712            toast_layer,
 1713            titlebar_item: None,
 1714            active_worktree_override: None,
 1715            notifications: Notifications::default(),
 1716            suppressed_notifications: HashSet::default(),
 1717            left_dock,
 1718            bottom_dock,
 1719            right_dock,
 1720            _panels_task: None,
 1721            project: project.clone(),
 1722            follower_states: Default::default(),
 1723            last_leaders_by_pane: Default::default(),
 1724            dispatching_keystrokes: Default::default(),
 1725            window_edited: false,
 1726            last_window_title: None,
 1727            dirty_items: Default::default(),
 1728            active_call,
 1729            database_id: workspace_id,
 1730            app_state,
 1731            _observe_current_user,
 1732            _apply_leader_updates,
 1733            _schedule_serialize_workspace: None,
 1734            _serialize_workspace_task: None,
 1735            _schedule_serialize_ssh_paths: None,
 1736            leader_updates_tx,
 1737            _subscriptions: subscriptions,
 1738            pane_history_timestamp,
 1739            workspace_actions: Default::default(),
 1740            // This data will be incorrect, but it will be overwritten by the time it needs to be used.
 1741            bounds: Default::default(),
 1742            centered_layout: false,
 1743            bounds_save_task_queued: None,
 1744            on_prompt_for_new_path: None,
 1745            on_prompt_for_open_path: None,
 1746            terminal_provider: None,
 1747            debugger_provider: None,
 1748            serializable_items_tx,
 1749            _items_serializer,
 1750            session_id: Some(session_id),
 1751
 1752            scheduled_tasks: Vec::new(),
 1753            last_open_dock_positions: Vec::new(),
 1754            removing: false,
 1755            sidebar_focus_handle: None,
 1756        }
 1757    }
 1758
 1759    pub fn new_local(
 1760        abs_paths: Vec<PathBuf>,
 1761        app_state: Arc<AppState>,
 1762        requesting_window: Option<WindowHandle<MultiWorkspace>>,
 1763        env: Option<HashMap<String, String>>,
 1764        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
 1765        activate: bool,
 1766        cx: &mut App,
 1767    ) -> Task<anyhow::Result<OpenResult>> {
 1768        let project_handle = Project::local(
 1769            app_state.client.clone(),
 1770            app_state.node_runtime.clone(),
 1771            app_state.user_store.clone(),
 1772            app_state.languages.clone(),
 1773            app_state.fs.clone(),
 1774            env,
 1775            Default::default(),
 1776            cx,
 1777        );
 1778
 1779        let db = WorkspaceDb::global(cx);
 1780        let kvp = db::kvp::KeyValueStore::global(cx);
 1781        cx.spawn(async move |cx| {
 1782            let mut paths_to_open = Vec::with_capacity(abs_paths.len());
 1783            for path in abs_paths.into_iter() {
 1784                if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
 1785                    paths_to_open.push(canonical)
 1786                } else {
 1787                    paths_to_open.push(path)
 1788                }
 1789            }
 1790
 1791            let serialized_workspace = db.workspace_for_roots(paths_to_open.as_slice());
 1792
 1793            if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
 1794                paths_to_open = paths.ordered_paths().cloned().collect();
 1795                if !paths.is_lexicographically_ordered() {
 1796                    project_handle.update(cx, |project, cx| {
 1797                        project.set_worktrees_reordered(true, cx);
 1798                    });
 1799                }
 1800            }
 1801
 1802            // Get project paths for all of the abs_paths
 1803            let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
 1804                Vec::with_capacity(paths_to_open.len());
 1805
 1806            for path in paths_to_open.into_iter() {
 1807                if let Some((_, project_entry)) = cx
 1808                    .update(|cx| {
 1809                        Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
 1810                    })
 1811                    .await
 1812                    .log_err()
 1813                {
 1814                    project_paths.push((path, Some(project_entry)));
 1815                } else {
 1816                    project_paths.push((path, None));
 1817                }
 1818            }
 1819
 1820            let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
 1821                serialized_workspace.id
 1822            } else {
 1823                db.next_id().await.unwrap_or_else(|_| Default::default())
 1824            };
 1825
 1826            let toolchains = db.toolchains(workspace_id).await?;
 1827
 1828            for (toolchain, worktree_path, path) in toolchains {
 1829                let toolchain_path = PathBuf::from(toolchain.path.clone().to_string());
 1830                let Some(worktree_id) = project_handle.read_with(cx, |this, cx| {
 1831                    this.find_worktree(&worktree_path, cx)
 1832                        .and_then(|(worktree, rel_path)| {
 1833                            if rel_path.is_empty() {
 1834                                Some(worktree.read(cx).id())
 1835                            } else {
 1836                                None
 1837                            }
 1838                        })
 1839                }) else {
 1840                    // We did not find a worktree with a given path, but that's whatever.
 1841                    continue;
 1842                };
 1843                if !app_state.fs.is_file(toolchain_path.as_path()).await {
 1844                    continue;
 1845                }
 1846
 1847                project_handle
 1848                    .update(cx, |this, cx| {
 1849                        this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
 1850                    })
 1851                    .await;
 1852            }
 1853            if let Some(workspace) = serialized_workspace.as_ref() {
 1854                project_handle.update(cx, |this, cx| {
 1855                    for (scope, toolchains) in &workspace.user_toolchains {
 1856                        for toolchain in toolchains {
 1857                            this.add_toolchain(toolchain.clone(), scope.clone(), cx);
 1858                        }
 1859                    }
 1860                });
 1861            }
 1862
 1863            let (window, workspace): (WindowHandle<MultiWorkspace>, Entity<Workspace>) =
 1864                if let Some(window) = requesting_window {
 1865                    let centered_layout = serialized_workspace
 1866                        .as_ref()
 1867                        .map(|w| w.centered_layout)
 1868                        .unwrap_or(false);
 1869
 1870                    let workspace = window.update(cx, |multi_workspace, window, cx| {
 1871                        let workspace = cx.new(|cx| {
 1872                            let mut workspace = Workspace::new(
 1873                                Some(workspace_id),
 1874                                project_handle.clone(),
 1875                                app_state.clone(),
 1876                                window,
 1877                                cx,
 1878                            );
 1879
 1880                            workspace.centered_layout = centered_layout;
 1881
 1882                            // Call init callback to add items before window renders
 1883                            if let Some(init) = init {
 1884                                init(&mut workspace, window, cx);
 1885                            }
 1886
 1887                            workspace
 1888                        });
 1889                        if activate {
 1890                            multi_workspace.activate(workspace.clone(), cx);
 1891                        } else {
 1892                            multi_workspace.add_workspace(workspace.clone(), cx);
 1893                        }
 1894                        workspace
 1895                    })?;
 1896                    (window, workspace)
 1897                } else {
 1898                    let window_bounds_override = window_bounds_env_override();
 1899
 1900                    let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
 1901                        (Some(WindowBounds::Windowed(bounds)), None)
 1902                    } else if let Some(workspace) = serialized_workspace.as_ref()
 1903                        && let Some(display) = workspace.display
 1904                        && let Some(bounds) = workspace.window_bounds.as_ref()
 1905                    {
 1906                        // Reopening an existing workspace - restore its saved bounds
 1907                        (Some(bounds.0), Some(display))
 1908                    } else if let Some((display, bounds)) =
 1909                        persistence::read_default_window_bounds(&kvp)
 1910                    {
 1911                        // New or empty workspace - use the last known window bounds
 1912                        (Some(bounds), Some(display))
 1913                    } else {
 1914                        // New window - let GPUI's default_bounds() handle cascading
 1915                        (None, None)
 1916                    };
 1917
 1918                    // Use the serialized workspace to construct the new window
 1919                    let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx));
 1920                    options.window_bounds = window_bounds;
 1921                    let centered_layout = serialized_workspace
 1922                        .as_ref()
 1923                        .map(|w| w.centered_layout)
 1924                        .unwrap_or(false);
 1925                    let window = cx.open_window(options, {
 1926                        let app_state = app_state.clone();
 1927                        let project_handle = project_handle.clone();
 1928                        move |window, cx| {
 1929                            let workspace = cx.new(|cx| {
 1930                                let mut workspace = Workspace::new(
 1931                                    Some(workspace_id),
 1932                                    project_handle,
 1933                                    app_state,
 1934                                    window,
 1935                                    cx,
 1936                                );
 1937                                workspace.centered_layout = centered_layout;
 1938
 1939                                // Call init callback to add items before window renders
 1940                                if let Some(init) = init {
 1941                                    init(&mut workspace, window, cx);
 1942                                }
 1943
 1944                                workspace
 1945                            });
 1946                            cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
 1947                        }
 1948                    })?;
 1949                    let workspace =
 1950                        window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
 1951                            multi_workspace.workspace().clone()
 1952                        })?;
 1953                    (window, workspace)
 1954                };
 1955
 1956            notify_if_database_failed(window, cx);
 1957            // Check if this is an empty workspace (no paths to open)
 1958            // An empty workspace is one where project_paths is empty
 1959            let is_empty_workspace = project_paths.is_empty();
 1960            // Check if serialized workspace has paths before it's moved
 1961            let serialized_workspace_has_paths = serialized_workspace
 1962                .as_ref()
 1963                .map(|ws| !ws.paths.is_empty())
 1964                .unwrap_or(false);
 1965
 1966            let opened_items = window
 1967                .update(cx, |_, window, cx| {
 1968                    workspace.update(cx, |_workspace: &mut Workspace, cx| {
 1969                        open_items(serialized_workspace, project_paths, window, cx)
 1970                    })
 1971                })?
 1972                .await
 1973                .unwrap_or_default();
 1974
 1975            // Restore default dock state for empty workspaces
 1976            // Only restore if:
 1977            // 1. This is an empty workspace (no paths), AND
 1978            // 2. The serialized workspace either doesn't exist or has no paths
 1979            if is_empty_workspace && !serialized_workspace_has_paths {
 1980                if let Some(default_docks) = persistence::read_default_dock_state(&kvp) {
 1981                    window
 1982                        .update(cx, |_, window, cx| {
 1983                            workspace.update(cx, |workspace, cx| {
 1984                                for (dock, serialized_dock) in [
 1985                                    (&workspace.right_dock, &default_docks.right),
 1986                                    (&workspace.left_dock, &default_docks.left),
 1987                                    (&workspace.bottom_dock, &default_docks.bottom),
 1988                                ] {
 1989                                    dock.update(cx, |dock, cx| {
 1990                                        dock.serialized_dock = Some(serialized_dock.clone());
 1991                                        dock.restore_state(window, cx);
 1992                                    });
 1993                                }
 1994                                cx.notify();
 1995                            });
 1996                        })
 1997                        .log_err();
 1998                }
 1999            }
 2000
 2001            window
 2002                .update(cx, |_, _window, cx| {
 2003                    workspace.update(cx, |this: &mut Workspace, cx| {
 2004                        this.update_history(cx);
 2005                    });
 2006                })
 2007                .log_err();
 2008            Ok(OpenResult {
 2009                window,
 2010                workspace,
 2011                opened_items,
 2012            })
 2013        })
 2014    }
 2015
 2016    pub fn weak_handle(&self) -> WeakEntity<Self> {
 2017        self.weak_self.clone()
 2018    }
 2019
 2020    pub fn left_dock(&self) -> &Entity<Dock> {
 2021        &self.left_dock
 2022    }
 2023
 2024    pub fn bottom_dock(&self) -> &Entity<Dock> {
 2025        &self.bottom_dock
 2026    }
 2027
 2028    pub fn set_bottom_dock_layout(
 2029        &mut self,
 2030        layout: BottomDockLayout,
 2031        window: &mut Window,
 2032        cx: &mut Context<Self>,
 2033    ) {
 2034        let fs = self.project().read(cx).fs();
 2035        settings::update_settings_file(fs.clone(), cx, move |content, _cx| {
 2036            content.workspace.bottom_dock_layout = Some(layout);
 2037        });
 2038
 2039        cx.notify();
 2040        self.serialize_workspace(window, cx);
 2041    }
 2042
 2043    pub fn right_dock(&self) -> &Entity<Dock> {
 2044        &self.right_dock
 2045    }
 2046
 2047    pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
 2048        [&self.left_dock, &self.bottom_dock, &self.right_dock]
 2049    }
 2050
 2051    pub fn capture_dock_state(&self, _window: &Window, cx: &App) -> DockStructure {
 2052        let left_dock = self.left_dock.read(cx);
 2053        let left_visible = left_dock.is_open();
 2054        let left_active_panel = left_dock
 2055            .active_panel()
 2056            .map(|panel| panel.persistent_name().to_string());
 2057        // `zoomed_position` is kept in sync with individual panel zoom state
 2058        // by the dock code in `Dock::new` and `Dock::add_panel`.
 2059        let left_dock_zoom = self.zoomed_position == Some(DockPosition::Left);
 2060
 2061        let right_dock = self.right_dock.read(cx);
 2062        let right_visible = right_dock.is_open();
 2063        let right_active_panel = right_dock
 2064            .active_panel()
 2065            .map(|panel| panel.persistent_name().to_string());
 2066        let right_dock_zoom = self.zoomed_position == Some(DockPosition::Right);
 2067
 2068        let bottom_dock = self.bottom_dock.read(cx);
 2069        let bottom_visible = bottom_dock.is_open();
 2070        let bottom_active_panel = bottom_dock
 2071            .active_panel()
 2072            .map(|panel| panel.persistent_name().to_string());
 2073        let bottom_dock_zoom = self.zoomed_position == Some(DockPosition::Bottom);
 2074
 2075        DockStructure {
 2076            left: DockData {
 2077                visible: left_visible,
 2078                active_panel: left_active_panel,
 2079                zoom: left_dock_zoom,
 2080            },
 2081            right: DockData {
 2082                visible: right_visible,
 2083                active_panel: right_active_panel,
 2084                zoom: right_dock_zoom,
 2085            },
 2086            bottom: DockData {
 2087                visible: bottom_visible,
 2088                active_panel: bottom_active_panel,
 2089                zoom: bottom_dock_zoom,
 2090            },
 2091        }
 2092    }
 2093
 2094    pub fn set_dock_structure(
 2095        &self,
 2096        docks: DockStructure,
 2097        window: &mut Window,
 2098        cx: &mut Context<Self>,
 2099    ) {
 2100        for (dock, data) in [
 2101            (&self.left_dock, docks.left),
 2102            (&self.bottom_dock, docks.bottom),
 2103            (&self.right_dock, docks.right),
 2104        ] {
 2105            dock.update(cx, |dock, cx| {
 2106                dock.serialized_dock = Some(data);
 2107                dock.restore_state(window, cx);
 2108            });
 2109        }
 2110    }
 2111
 2112    pub fn open_item_abs_paths(&self, cx: &App) -> Vec<PathBuf> {
 2113        self.items(cx)
 2114            .filter_map(|item| {
 2115                let project_path = item.project_path(cx)?;
 2116                self.project.read(cx).absolute_path(&project_path, cx)
 2117            })
 2118            .collect()
 2119    }
 2120
 2121    pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
 2122        match position {
 2123            DockPosition::Left => &self.left_dock,
 2124            DockPosition::Bottom => &self.bottom_dock,
 2125            DockPosition::Right => &self.right_dock,
 2126        }
 2127    }
 2128
 2129    pub fn is_edited(&self) -> bool {
 2130        self.window_edited
 2131    }
 2132
 2133    pub fn add_panel<T: Panel>(
 2134        &mut self,
 2135        panel: Entity<T>,
 2136        window: &mut Window,
 2137        cx: &mut Context<Self>,
 2138    ) {
 2139        let focus_handle = panel.panel_focus_handle(cx);
 2140        cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
 2141            .detach();
 2142
 2143        let dock_position = panel.position(window, cx);
 2144        let dock = self.dock_at_position(dock_position);
 2145        let any_panel = panel.to_any();
 2146
 2147        dock.update(cx, |dock, cx| {
 2148            dock.add_panel(panel, self.weak_self.clone(), window, cx)
 2149        });
 2150
 2151        cx.emit(Event::PanelAdded(any_panel));
 2152    }
 2153
 2154    pub fn remove_panel<T: Panel>(
 2155        &mut self,
 2156        panel: &Entity<T>,
 2157        window: &mut Window,
 2158        cx: &mut Context<Self>,
 2159    ) {
 2160        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
 2161            dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
 2162        }
 2163    }
 2164
 2165    pub fn status_bar(&self) -> &Entity<StatusBar> {
 2166        &self.status_bar
 2167    }
 2168
 2169    pub fn set_workspace_sidebar_open(
 2170        &self,
 2171        open: bool,
 2172        has_notifications: bool,
 2173        show_toggle: bool,
 2174        cx: &mut App,
 2175    ) {
 2176        self.status_bar.update(cx, |status_bar, cx| {
 2177            status_bar.set_workspace_sidebar_open(open, cx);
 2178            status_bar.set_sidebar_has_notifications(has_notifications, cx);
 2179            status_bar.set_show_sidebar_toggle(show_toggle, cx);
 2180        });
 2181    }
 2182
 2183    pub fn set_sidebar_focus_handle(&mut self, handle: Option<FocusHandle>) {
 2184        self.sidebar_focus_handle = handle;
 2185    }
 2186
 2187    pub fn status_bar_visible(&self, cx: &App) -> bool {
 2188        StatusBarSettings::get_global(cx).show
 2189    }
 2190
 2191    pub fn app_state(&self) -> &Arc<AppState> {
 2192        &self.app_state
 2193    }
 2194
 2195    pub fn set_panels_task(&mut self, task: Task<Result<()>>) {
 2196        self._panels_task = Some(task);
 2197    }
 2198
 2199    pub fn take_panels_task(&mut self) -> Option<Task<Result<()>>> {
 2200        self._panels_task.take()
 2201    }
 2202
 2203    pub fn user_store(&self) -> &Entity<UserStore> {
 2204        &self.app_state.user_store
 2205    }
 2206
 2207    pub fn project(&self) -> &Entity<Project> {
 2208        &self.project
 2209    }
 2210
 2211    pub fn path_style(&self, cx: &App) -> PathStyle {
 2212        self.project.read(cx).path_style(cx)
 2213    }
 2214
 2215    pub fn recently_activated_items(&self, cx: &App) -> HashMap<EntityId, usize> {
 2216        let mut history: HashMap<EntityId, usize> = HashMap::default();
 2217
 2218        for pane_handle in &self.panes {
 2219            let pane = pane_handle.read(cx);
 2220
 2221            for entry in pane.activation_history() {
 2222                history.insert(
 2223                    entry.entity_id,
 2224                    history
 2225                        .get(&entry.entity_id)
 2226                        .cloned()
 2227                        .unwrap_or(0)
 2228                        .max(entry.timestamp),
 2229                );
 2230            }
 2231        }
 2232
 2233        history
 2234    }
 2235
 2236    pub fn recent_active_item_by_type<T: 'static>(&self, cx: &App) -> Option<Entity<T>> {
 2237        let mut recent_item: Option<Entity<T>> = None;
 2238        let mut recent_timestamp = 0;
 2239        for pane_handle in &self.panes {
 2240            let pane = pane_handle.read(cx);
 2241            let item_map: HashMap<EntityId, &Box<dyn ItemHandle>> =
 2242                pane.items().map(|item| (item.item_id(), item)).collect();
 2243            for entry in pane.activation_history() {
 2244                if entry.timestamp > recent_timestamp
 2245                    && let Some(&item) = item_map.get(&entry.entity_id)
 2246                    && let Some(typed_item) = item.act_as::<T>(cx)
 2247                {
 2248                    recent_timestamp = entry.timestamp;
 2249                    recent_item = Some(typed_item);
 2250                }
 2251            }
 2252        }
 2253        recent_item
 2254    }
 2255
 2256    pub fn recent_navigation_history_iter(
 2257        &self,
 2258        cx: &App,
 2259    ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> + use<> {
 2260        let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
 2261        let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
 2262
 2263        for pane in &self.panes {
 2264            let pane = pane.read(cx);
 2265
 2266            pane.nav_history()
 2267                .for_each_entry(cx, &mut |entry, (project_path, fs_path)| {
 2268                    if let Some(fs_path) = &fs_path {
 2269                        abs_paths_opened
 2270                            .entry(fs_path.clone())
 2271                            .or_default()
 2272                            .insert(project_path.clone());
 2273                    }
 2274                    let timestamp = entry.timestamp;
 2275                    match history.entry(project_path) {
 2276                        hash_map::Entry::Occupied(mut entry) => {
 2277                            let (_, old_timestamp) = entry.get();
 2278                            if &timestamp > old_timestamp {
 2279                                entry.insert((fs_path, timestamp));
 2280                            }
 2281                        }
 2282                        hash_map::Entry::Vacant(entry) => {
 2283                            entry.insert((fs_path, timestamp));
 2284                        }
 2285                    }
 2286                });
 2287
 2288            if let Some(item) = pane.active_item()
 2289                && let Some(project_path) = item.project_path(cx)
 2290            {
 2291                let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
 2292
 2293                if let Some(fs_path) = &fs_path {
 2294                    abs_paths_opened
 2295                        .entry(fs_path.clone())
 2296                        .or_default()
 2297                        .insert(project_path.clone());
 2298                }
 2299
 2300                history.insert(project_path, (fs_path, std::usize::MAX));
 2301            }
 2302        }
 2303
 2304        history
 2305            .into_iter()
 2306            .sorted_by_key(|(_, (_, order))| *order)
 2307            .map(|(project_path, (fs_path, _))| (project_path, fs_path))
 2308            .rev()
 2309            .filter(move |(history_path, abs_path)| {
 2310                let latest_project_path_opened = abs_path
 2311                    .as_ref()
 2312                    .and_then(|abs_path| abs_paths_opened.get(abs_path))
 2313                    .and_then(|project_paths| {
 2314                        project_paths
 2315                            .iter()
 2316                            .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
 2317                    });
 2318
 2319                latest_project_path_opened.is_none_or(|path| path == history_path)
 2320            })
 2321    }
 2322
 2323    pub fn recent_navigation_history(
 2324        &self,
 2325        limit: Option<usize>,
 2326        cx: &App,
 2327    ) -> Vec<(ProjectPath, Option<PathBuf>)> {
 2328        self.recent_navigation_history_iter(cx)
 2329            .take(limit.unwrap_or(usize::MAX))
 2330            .collect()
 2331    }
 2332
 2333    pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context<Workspace>) {
 2334        for pane in &self.panes {
 2335            pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx));
 2336        }
 2337    }
 2338
 2339    fn navigate_history(
 2340        &mut self,
 2341        pane: WeakEntity<Pane>,
 2342        mode: NavigationMode,
 2343        window: &mut Window,
 2344        cx: &mut Context<Workspace>,
 2345    ) -> Task<Result<()>> {
 2346        self.navigate_history_impl(
 2347            pane,
 2348            mode,
 2349            window,
 2350            &mut |history, cx| history.pop(mode, cx),
 2351            cx,
 2352        )
 2353    }
 2354
 2355    fn navigate_tag_history(
 2356        &mut self,
 2357        pane: WeakEntity<Pane>,
 2358        mode: TagNavigationMode,
 2359        window: &mut Window,
 2360        cx: &mut Context<Workspace>,
 2361    ) -> Task<Result<()>> {
 2362        self.navigate_history_impl(
 2363            pane,
 2364            NavigationMode::Normal,
 2365            window,
 2366            &mut |history, _cx| history.pop_tag(mode),
 2367            cx,
 2368        )
 2369    }
 2370
 2371    fn navigate_history_impl(
 2372        &mut self,
 2373        pane: WeakEntity<Pane>,
 2374        mode: NavigationMode,
 2375        window: &mut Window,
 2376        cb: &mut dyn FnMut(&mut NavHistory, &mut App) -> Option<NavigationEntry>,
 2377        cx: &mut Context<Workspace>,
 2378    ) -> Task<Result<()>> {
 2379        let to_load = if let Some(pane) = pane.upgrade() {
 2380            pane.update(cx, |pane, cx| {
 2381                window.focus(&pane.focus_handle(cx), cx);
 2382                loop {
 2383                    // Retrieve the weak item handle from the history.
 2384                    let entry = cb(pane.nav_history_mut(), cx)?;
 2385
 2386                    // If the item is still present in this pane, then activate it.
 2387                    if let Some(index) = entry
 2388                        .item
 2389                        .upgrade()
 2390                        .and_then(|v| pane.index_for_item(v.as_ref()))
 2391                    {
 2392                        let prev_active_item_index = pane.active_item_index();
 2393                        pane.nav_history_mut().set_mode(mode);
 2394                        pane.activate_item(index, true, true, window, cx);
 2395                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
 2396
 2397                        let mut navigated = prev_active_item_index != pane.active_item_index();
 2398                        if let Some(data) = entry.data {
 2399                            navigated |= pane.active_item()?.navigate(data, window, cx);
 2400                        }
 2401
 2402                        if navigated {
 2403                            break None;
 2404                        }
 2405                    } else {
 2406                        // If the item is no longer present in this pane, then retrieve its
 2407                        // path info in order to reopen it.
 2408                        break pane
 2409                            .nav_history()
 2410                            .path_for_item(entry.item.id())
 2411                            .map(|(project_path, abs_path)| (project_path, abs_path, entry));
 2412                    }
 2413                }
 2414            })
 2415        } else {
 2416            None
 2417        };
 2418
 2419        if let Some((project_path, abs_path, entry)) = to_load {
 2420            // If the item was no longer present, then load it again from its previous path, first try the local path
 2421            let open_by_project_path = self.load_path(project_path.clone(), window, cx);
 2422
 2423            cx.spawn_in(window, async move  |workspace, cx| {
 2424                let open_by_project_path = open_by_project_path.await;
 2425                let mut navigated = false;
 2426                match open_by_project_path
 2427                    .with_context(|| format!("Navigating to {project_path:?}"))
 2428                {
 2429                    Ok((project_entry_id, build_item)) => {
 2430                        let prev_active_item_id = pane.update(cx, |pane, _| {
 2431                            pane.nav_history_mut().set_mode(mode);
 2432                            pane.active_item().map(|p| p.item_id())
 2433                        })?;
 2434
 2435                        pane.update_in(cx, |pane, window, cx| {
 2436                            let item = pane.open_item(
 2437                                project_entry_id,
 2438                                project_path,
 2439                                true,
 2440                                entry.is_preview,
 2441                                true,
 2442                                None,
 2443                                window, cx,
 2444                                build_item,
 2445                            );
 2446                            navigated |= Some(item.item_id()) != prev_active_item_id;
 2447                            pane.nav_history_mut().set_mode(NavigationMode::Normal);
 2448                            if let Some(data) = entry.data {
 2449                                navigated |= item.navigate(data, window, cx);
 2450                            }
 2451                        })?;
 2452                    }
 2453                    Err(open_by_project_path_e) => {
 2454                        // Fall back to opening by abs path, in case an external file was opened and closed,
 2455                        // and its worktree is now dropped
 2456                        if let Some(abs_path) = abs_path {
 2457                            let prev_active_item_id = pane.update(cx, |pane, _| {
 2458                                pane.nav_history_mut().set_mode(mode);
 2459                                pane.active_item().map(|p| p.item_id())
 2460                            })?;
 2461                            let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| {
 2462                                workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
 2463                            })?;
 2464                            match open_by_abs_path
 2465                                .await
 2466                                .with_context(|| format!("Navigating to {abs_path:?}"))
 2467                            {
 2468                                Ok(item) => {
 2469                                    pane.update_in(cx, |pane, window, cx| {
 2470                                        navigated |= Some(item.item_id()) != prev_active_item_id;
 2471                                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
 2472                                        if let Some(data) = entry.data {
 2473                                            navigated |= item.navigate(data, window, cx);
 2474                                        }
 2475                                    })?;
 2476                                }
 2477                                Err(open_by_abs_path_e) => {
 2478                                    log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
 2479                                }
 2480                            }
 2481                        }
 2482                    }
 2483                }
 2484
 2485                if !navigated {
 2486                    workspace
 2487                        .update_in(cx, |workspace, window, cx| {
 2488                            Self::navigate_history(workspace, pane, mode, window, cx)
 2489                        })?
 2490                        .await?;
 2491                }
 2492
 2493                Ok(())
 2494            })
 2495        } else {
 2496            Task::ready(Ok(()))
 2497        }
 2498    }
 2499
 2500    pub fn go_back(
 2501        &mut self,
 2502        pane: WeakEntity<Pane>,
 2503        window: &mut Window,
 2504        cx: &mut Context<Workspace>,
 2505    ) -> Task<Result<()>> {
 2506        self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
 2507    }
 2508
 2509    pub fn go_forward(
 2510        &mut self,
 2511        pane: WeakEntity<Pane>,
 2512        window: &mut Window,
 2513        cx: &mut Context<Workspace>,
 2514    ) -> Task<Result<()>> {
 2515        self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
 2516    }
 2517
 2518    pub fn reopen_closed_item(
 2519        &mut self,
 2520        window: &mut Window,
 2521        cx: &mut Context<Workspace>,
 2522    ) -> Task<Result<()>> {
 2523        self.navigate_history(
 2524            self.active_pane().downgrade(),
 2525            NavigationMode::ReopeningClosedItem,
 2526            window,
 2527            cx,
 2528        )
 2529    }
 2530
 2531    pub fn client(&self) -> &Arc<Client> {
 2532        &self.app_state.client
 2533    }
 2534
 2535    pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
 2536        self.titlebar_item = Some(item);
 2537        cx.notify();
 2538    }
 2539
 2540    pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
 2541        self.on_prompt_for_new_path = Some(prompt)
 2542    }
 2543
 2544    pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
 2545        self.on_prompt_for_open_path = Some(prompt)
 2546    }
 2547
 2548    pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
 2549        self.terminal_provider = Some(Box::new(provider));
 2550    }
 2551
 2552    pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
 2553        self.debugger_provider = Some(Arc::new(provider));
 2554    }
 2555
 2556    pub fn debugger_provider(&self) -> Option<Arc<dyn DebuggerProvider>> {
 2557        self.debugger_provider.clone()
 2558    }
 2559
 2560    pub fn prompt_for_open_path(
 2561        &mut self,
 2562        path_prompt_options: PathPromptOptions,
 2563        lister: DirectoryLister,
 2564        window: &mut Window,
 2565        cx: &mut Context<Self>,
 2566    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
 2567        if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
 2568            let prompt = self.on_prompt_for_open_path.take().unwrap();
 2569            let rx = prompt(self, lister, window, cx);
 2570            self.on_prompt_for_open_path = Some(prompt);
 2571            rx
 2572        } else {
 2573            let (tx, rx) = oneshot::channel();
 2574            let abs_path = cx.prompt_for_paths(path_prompt_options);
 2575
 2576            cx.spawn_in(window, async move |workspace, cx| {
 2577                let Ok(result) = abs_path.await else {
 2578                    return Ok(());
 2579                };
 2580
 2581                match result {
 2582                    Ok(result) => {
 2583                        tx.send(result).ok();
 2584                    }
 2585                    Err(err) => {
 2586                        let rx = workspace.update_in(cx, |workspace, window, cx| {
 2587                            workspace.show_portal_error(err.to_string(), cx);
 2588                            let prompt = workspace.on_prompt_for_open_path.take().unwrap();
 2589                            let rx = prompt(workspace, lister, window, cx);
 2590                            workspace.on_prompt_for_open_path = Some(prompt);
 2591                            rx
 2592                        })?;
 2593                        if let Ok(path) = rx.await {
 2594                            tx.send(path).ok();
 2595                        }
 2596                    }
 2597                };
 2598                anyhow::Ok(())
 2599            })
 2600            .detach();
 2601
 2602            rx
 2603        }
 2604    }
 2605
 2606    pub fn prompt_for_new_path(
 2607        &mut self,
 2608        lister: DirectoryLister,
 2609        suggested_name: Option<String>,
 2610        window: &mut Window,
 2611        cx: &mut Context<Self>,
 2612    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
 2613        if self.project.read(cx).is_via_collab()
 2614            || self.project.read(cx).is_via_remote_server()
 2615            || !WorkspaceSettings::get_global(cx).use_system_path_prompts
 2616        {
 2617            let prompt = self.on_prompt_for_new_path.take().unwrap();
 2618            let rx = prompt(self, lister, suggested_name, window, cx);
 2619            self.on_prompt_for_new_path = Some(prompt);
 2620            return rx;
 2621        }
 2622
 2623        let (tx, rx) = oneshot::channel();
 2624        cx.spawn_in(window, async move |workspace, cx| {
 2625            let abs_path = workspace.update(cx, |workspace, cx| {
 2626                let relative_to = workspace
 2627                    .most_recent_active_path(cx)
 2628                    .and_then(|p| p.parent().map(|p| p.to_path_buf()))
 2629                    .or_else(|| {
 2630                        let project = workspace.project.read(cx);
 2631                        project.visible_worktrees(cx).find_map(|worktree| {
 2632                            Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
 2633                        })
 2634                    })
 2635                    .or_else(std::env::home_dir)
 2636                    .unwrap_or_else(|| PathBuf::from(""));
 2637                cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
 2638            })?;
 2639            let abs_path = match abs_path.await? {
 2640                Ok(path) => path,
 2641                Err(err) => {
 2642                    let rx = workspace.update_in(cx, |workspace, window, cx| {
 2643                        workspace.show_portal_error(err.to_string(), cx);
 2644
 2645                        let prompt = workspace.on_prompt_for_new_path.take().unwrap();
 2646                        let rx = prompt(workspace, lister, suggested_name, window, cx);
 2647                        workspace.on_prompt_for_new_path = Some(prompt);
 2648                        rx
 2649                    })?;
 2650                    if let Ok(path) = rx.await {
 2651                        tx.send(path).ok();
 2652                    }
 2653                    return anyhow::Ok(());
 2654                }
 2655            };
 2656
 2657            tx.send(abs_path.map(|path| vec![path])).ok();
 2658            anyhow::Ok(())
 2659        })
 2660        .detach();
 2661
 2662        rx
 2663    }
 2664
 2665    pub fn titlebar_item(&self) -> Option<AnyView> {
 2666        self.titlebar_item.clone()
 2667    }
 2668
 2669    /// Returns the worktree override set by the user (e.g., via the project dropdown).
 2670    /// When set, git-related operations should use this worktree instead of deriving
 2671    /// the active worktree from the focused file.
 2672    pub fn active_worktree_override(&self) -> Option<WorktreeId> {
 2673        self.active_worktree_override
 2674    }
 2675
 2676    pub fn set_active_worktree_override(
 2677        &mut self,
 2678        worktree_id: Option<WorktreeId>,
 2679        cx: &mut Context<Self>,
 2680    ) {
 2681        self.active_worktree_override = worktree_id;
 2682        cx.notify();
 2683    }
 2684
 2685    pub fn clear_active_worktree_override(&mut self, cx: &mut Context<Self>) {
 2686        self.active_worktree_override = None;
 2687        cx.notify();
 2688    }
 2689
 2690    /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
 2691    ///
 2692    /// If the given workspace has a local project, then it will be passed
 2693    /// to the callback. Otherwise, a new empty window will be created.
 2694    pub fn with_local_workspace<T, F>(
 2695        &mut self,
 2696        window: &mut Window,
 2697        cx: &mut Context<Self>,
 2698        callback: F,
 2699    ) -> Task<Result<T>>
 2700    where
 2701        T: 'static,
 2702        F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
 2703    {
 2704        if self.project.read(cx).is_local() {
 2705            Task::ready(Ok(callback(self, window, cx)))
 2706        } else {
 2707            let env = self.project.read(cx).cli_environment(cx);
 2708            let task = Self::new_local(
 2709                Vec::new(),
 2710                self.app_state.clone(),
 2711                None,
 2712                env,
 2713                None,
 2714                true,
 2715                cx,
 2716            );
 2717            cx.spawn_in(window, async move |_vh, cx| {
 2718                let OpenResult {
 2719                    window: multi_workspace_window,
 2720                    ..
 2721                } = task.await?;
 2722                multi_workspace_window.update(cx, |multi_workspace, window, cx| {
 2723                    let workspace = multi_workspace.workspace().clone();
 2724                    workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
 2725                })
 2726            })
 2727        }
 2728    }
 2729
 2730    /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
 2731    ///
 2732    /// If the given workspace has a local project, then it will be passed
 2733    /// to the callback. Otherwise, a new empty window will be created.
 2734    pub fn with_local_or_wsl_workspace<T, F>(
 2735        &mut self,
 2736        window: &mut Window,
 2737        cx: &mut Context<Self>,
 2738        callback: F,
 2739    ) -> Task<Result<T>>
 2740    where
 2741        T: 'static,
 2742        F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
 2743    {
 2744        let project = self.project.read(cx);
 2745        if project.is_local() || project.is_via_wsl_with_host_interop(cx) {
 2746            Task::ready(Ok(callback(self, window, cx)))
 2747        } else {
 2748            let env = self.project.read(cx).cli_environment(cx);
 2749            let task = Self::new_local(
 2750                Vec::new(),
 2751                self.app_state.clone(),
 2752                None,
 2753                env,
 2754                None,
 2755                true,
 2756                cx,
 2757            );
 2758            cx.spawn_in(window, async move |_vh, cx| {
 2759                let OpenResult {
 2760                    window: multi_workspace_window,
 2761                    ..
 2762                } = task.await?;
 2763                multi_workspace_window.update(cx, |multi_workspace, window, cx| {
 2764                    let workspace = multi_workspace.workspace().clone();
 2765                    workspace.update(cx, |workspace, cx| callback(workspace, window, cx))
 2766                })
 2767            })
 2768        }
 2769    }
 2770
 2771    pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
 2772        self.project.read(cx).worktrees(cx)
 2773    }
 2774
 2775    pub fn visible_worktrees<'a>(
 2776        &self,
 2777        cx: &'a App,
 2778    ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
 2779        self.project.read(cx).visible_worktrees(cx)
 2780    }
 2781
 2782    #[cfg(any(test, feature = "test-support"))]
 2783    pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static + use<> {
 2784        let futures = self
 2785            .worktrees(cx)
 2786            .filter_map(|worktree| worktree.read(cx).as_local())
 2787            .map(|worktree| worktree.scan_complete())
 2788            .collect::<Vec<_>>();
 2789        async move {
 2790            for future in futures {
 2791                future.await;
 2792            }
 2793        }
 2794    }
 2795
 2796    pub fn close_global(cx: &mut App) {
 2797        cx.defer(|cx| {
 2798            cx.windows().iter().find(|window| {
 2799                window
 2800                    .update(cx, |_, window, _| {
 2801                        if window.is_window_active() {
 2802                            //This can only get called when the window's project connection has been lost
 2803                            //so we don't need to prompt the user for anything and instead just close the window
 2804                            window.remove_window();
 2805                            true
 2806                        } else {
 2807                            false
 2808                        }
 2809                    })
 2810                    .unwrap_or(false)
 2811            });
 2812        });
 2813    }
 2814
 2815    pub fn move_focused_panel_to_next_position(
 2816        &mut self,
 2817        _: &MoveFocusedPanelToNextPosition,
 2818        window: &mut Window,
 2819        cx: &mut Context<Self>,
 2820    ) {
 2821        let docks = self.all_docks();
 2822        let active_dock = docks
 2823            .into_iter()
 2824            .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
 2825
 2826        if let Some(dock) = active_dock {
 2827            dock.update(cx, |dock, cx| {
 2828                let active_panel = dock
 2829                    .active_panel()
 2830                    .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
 2831
 2832                if let Some(panel) = active_panel {
 2833                    panel.move_to_next_position(window, cx);
 2834                }
 2835            })
 2836        }
 2837    }
 2838
 2839    pub fn prepare_to_close(
 2840        &mut self,
 2841        close_intent: CloseIntent,
 2842        window: &mut Window,
 2843        cx: &mut Context<Self>,
 2844    ) -> Task<Result<bool>> {
 2845        let active_call = self.active_global_call();
 2846
 2847        cx.spawn_in(window, async move |this, cx| {
 2848            this.update(cx, |this, _| {
 2849                if close_intent == CloseIntent::CloseWindow {
 2850                    this.removing = true;
 2851                }
 2852            })?;
 2853
 2854            let workspace_count = cx.update(|_window, cx| {
 2855                cx.windows()
 2856                    .iter()
 2857                    .filter(|window| window.downcast::<MultiWorkspace>().is_some())
 2858                    .count()
 2859            })?;
 2860
 2861            #[cfg(target_os = "macos")]
 2862            let save_last_workspace = false;
 2863
 2864            // On Linux and Windows, closing the last window should restore the last workspace.
 2865            #[cfg(not(target_os = "macos"))]
 2866            let save_last_workspace = {
 2867                let remaining_workspaces = cx.update(|_window, cx| {
 2868                    cx.windows()
 2869                        .iter()
 2870                        .filter_map(|window| window.downcast::<MultiWorkspace>())
 2871                        .filter_map(|multi_workspace| {
 2872                            multi_workspace
 2873                                .update(cx, |multi_workspace, _, cx| {
 2874                                    multi_workspace.workspace().read(cx).removing
 2875                                })
 2876                                .ok()
 2877                        })
 2878                        .filter(|removing| !removing)
 2879                        .count()
 2880                })?;
 2881
 2882                close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0
 2883            };
 2884
 2885            if let Some(active_call) = active_call
 2886                && workspace_count == 1
 2887                && cx
 2888                    .update(|_window, cx| active_call.0.is_in_room(cx))
 2889                    .unwrap_or(false)
 2890            {
 2891                if close_intent == CloseIntent::CloseWindow {
 2892                    this.update(cx, |_, cx| cx.emit(Event::Activate))?;
 2893                    let answer = cx.update(|window, cx| {
 2894                        window.prompt(
 2895                            PromptLevel::Warning,
 2896                            "Do you want to leave the current call?",
 2897                            None,
 2898                            &["Close window and hang up", "Cancel"],
 2899                            cx,
 2900                        )
 2901                    })?;
 2902
 2903                    if answer.await.log_err() == Some(1) {
 2904                        return anyhow::Ok(false);
 2905                    } else {
 2906                        if let Ok(task) = cx.update(|_window, cx| active_call.0.hang_up(cx)) {
 2907                            task.await.log_err();
 2908                        }
 2909                    }
 2910                }
 2911                if close_intent == CloseIntent::ReplaceWindow {
 2912                    _ = cx.update(|_window, cx| {
 2913                        let multi_workspace = cx
 2914                            .windows()
 2915                            .iter()
 2916                            .filter_map(|window| window.downcast::<MultiWorkspace>())
 2917                            .next()
 2918                            .unwrap();
 2919                        let project = multi_workspace
 2920                            .read(cx)?
 2921                            .workspace()
 2922                            .read(cx)
 2923                            .project
 2924                            .clone();
 2925                        if project.read(cx).is_shared() {
 2926                            active_call.0.unshare_project(project, cx)?;
 2927                        }
 2928                        Ok::<_, anyhow::Error>(())
 2929                    });
 2930                }
 2931            }
 2932
 2933            let save_result = this
 2934                .update_in(cx, |this, window, cx| {
 2935                    this.save_all_internal(SaveIntent::Close, window, cx)
 2936                })?
 2937                .await;
 2938
 2939            // If we're not quitting, but closing, we remove the workspace from
 2940            // the current session.
 2941            if close_intent != CloseIntent::Quit
 2942                && !save_last_workspace
 2943                && save_result.as_ref().is_ok_and(|&res| res)
 2944            {
 2945                this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))?
 2946                    .await;
 2947            }
 2948
 2949            save_result
 2950        })
 2951    }
 2952
 2953    fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
 2954        self.save_all_internal(
 2955            action.save_intent.unwrap_or(SaveIntent::SaveAll),
 2956            window,
 2957            cx,
 2958        )
 2959        .detach_and_log_err(cx);
 2960    }
 2961
 2962    fn send_keystrokes(
 2963        &mut self,
 2964        action: &SendKeystrokes,
 2965        window: &mut Window,
 2966        cx: &mut Context<Self>,
 2967    ) {
 2968        let keystrokes: Vec<Keystroke> = action
 2969            .0
 2970            .split(' ')
 2971            .flat_map(|k| Keystroke::parse(k).log_err())
 2972            .map(|k| {
 2973                cx.keyboard_mapper()
 2974                    .map_key_equivalent(k, false)
 2975                    .inner()
 2976                    .clone()
 2977            })
 2978            .collect();
 2979        let _ = self.send_keystrokes_impl(keystrokes, window, cx);
 2980    }
 2981
 2982    pub fn send_keystrokes_impl(
 2983        &mut self,
 2984        keystrokes: Vec<Keystroke>,
 2985        window: &mut Window,
 2986        cx: &mut Context<Self>,
 2987    ) -> Shared<Task<()>> {
 2988        let mut state = self.dispatching_keystrokes.borrow_mut();
 2989        if !state.dispatched.insert(keystrokes.clone()) {
 2990            cx.propagate();
 2991            return state.task.clone().unwrap();
 2992        }
 2993
 2994        state.queue.extend(keystrokes);
 2995
 2996        let keystrokes = self.dispatching_keystrokes.clone();
 2997        if state.task.is_none() {
 2998            state.task = Some(
 2999                window
 3000                    .spawn(cx, async move |cx| {
 3001                        // limit to 100 keystrokes to avoid infinite recursion.
 3002                        for _ in 0..100 {
 3003                            let keystroke = {
 3004                                let mut state = keystrokes.borrow_mut();
 3005                                let Some(keystroke) = state.queue.pop_front() else {
 3006                                    state.dispatched.clear();
 3007                                    state.task.take();
 3008                                    return;
 3009                                };
 3010                                keystroke
 3011                            };
 3012                            cx.update(|window, cx| {
 3013                                let focused = window.focused(cx);
 3014                                window.dispatch_keystroke(keystroke.clone(), cx);
 3015                                if window.focused(cx) != focused {
 3016                                    // dispatch_keystroke may cause the focus to change.
 3017                                    // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
 3018                                    // And we need that to happen before the next keystroke to keep vim mode happy...
 3019                                    // (Note that the tests always do this implicitly, so you must manually test with something like:
 3020                                    //   "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
 3021                                    // )
 3022                                    window.draw(cx).clear();
 3023                                }
 3024                            })
 3025                            .ok();
 3026
 3027                            // Yield between synthetic keystrokes so deferred focus and
 3028                            // other effects can settle before dispatching the next key.
 3029                            yield_now().await;
 3030                        }
 3031
 3032                        *keystrokes.borrow_mut() = Default::default();
 3033                        log::error!("over 100 keystrokes passed to send_keystrokes");
 3034                    })
 3035                    .shared(),
 3036            );
 3037        }
 3038        state.task.clone().unwrap()
 3039    }
 3040
 3041    fn save_all_internal(
 3042        &mut self,
 3043        mut save_intent: SaveIntent,
 3044        window: &mut Window,
 3045        cx: &mut Context<Self>,
 3046    ) -> Task<Result<bool>> {
 3047        if self.project.read(cx).is_disconnected(cx) {
 3048            return Task::ready(Ok(true));
 3049        }
 3050        let dirty_items = self
 3051            .panes
 3052            .iter()
 3053            .flat_map(|pane| {
 3054                pane.read(cx).items().filter_map(|item| {
 3055                    if item.is_dirty(cx) {
 3056                        item.tab_content_text(0, cx);
 3057                        Some((pane.downgrade(), item.boxed_clone()))
 3058                    } else {
 3059                        None
 3060                    }
 3061                })
 3062            })
 3063            .collect::<Vec<_>>();
 3064
 3065        let project = self.project.clone();
 3066        cx.spawn_in(window, async move |workspace, cx| {
 3067            let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
 3068                let (serialize_tasks, remaining_dirty_items) =
 3069                    workspace.update_in(cx, |workspace, window, cx| {
 3070                        let mut remaining_dirty_items = Vec::new();
 3071                        let mut serialize_tasks = Vec::new();
 3072                        for (pane, item) in dirty_items {
 3073                            if let Some(task) = item
 3074                                .to_serializable_item_handle(cx)
 3075                                .and_then(|handle| handle.serialize(workspace, true, window, cx))
 3076                            {
 3077                                serialize_tasks.push(task);
 3078                            } else {
 3079                                remaining_dirty_items.push((pane, item));
 3080                            }
 3081                        }
 3082                        (serialize_tasks, remaining_dirty_items)
 3083                    })?;
 3084
 3085                futures::future::try_join_all(serialize_tasks).await?;
 3086
 3087                if !remaining_dirty_items.is_empty() {
 3088                    workspace.update(cx, |_, cx| cx.emit(Event::Activate))?;
 3089                }
 3090
 3091                if remaining_dirty_items.len() > 1 {
 3092                    let answer = workspace.update_in(cx, |_, window, cx| {
 3093                        let detail = Pane::file_names_for_prompt(
 3094                            &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
 3095                            cx,
 3096                        );
 3097                        window.prompt(
 3098                            PromptLevel::Warning,
 3099                            "Do you want to save all changes in the following files?",
 3100                            Some(&detail),
 3101                            &["Save all", "Discard all", "Cancel"],
 3102                            cx,
 3103                        )
 3104                    })?;
 3105                    match answer.await.log_err() {
 3106                        Some(0) => save_intent = SaveIntent::SaveAll,
 3107                        Some(1) => save_intent = SaveIntent::Skip,
 3108                        Some(2) => return Ok(false),
 3109                        _ => {}
 3110                    }
 3111                }
 3112
 3113                remaining_dirty_items
 3114            } else {
 3115                dirty_items
 3116            };
 3117
 3118            for (pane, item) in dirty_items {
 3119                let (singleton, project_entry_ids) = cx.update(|_, cx| {
 3120                    (
 3121                        item.buffer_kind(cx) == ItemBufferKind::Singleton,
 3122                        item.project_entry_ids(cx),
 3123                    )
 3124                })?;
 3125                if (singleton || !project_entry_ids.is_empty())
 3126                    && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await?
 3127                {
 3128                    return Ok(false);
 3129                }
 3130            }
 3131            Ok(true)
 3132        })
 3133    }
 3134
 3135    pub fn open_workspace_for_paths(
 3136        &mut self,
 3137        replace_current_window: bool,
 3138        paths: Vec<PathBuf>,
 3139        window: &mut Window,
 3140        cx: &mut Context<Self>,
 3141    ) -> Task<Result<Entity<Workspace>>> {
 3142        let window_handle = window.window_handle().downcast::<MultiWorkspace>();
 3143        let is_remote = self.project.read(cx).is_via_collab();
 3144        let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
 3145        let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
 3146
 3147        let window_to_replace = if replace_current_window {
 3148            window_handle
 3149        } else if is_remote || has_worktree || has_dirty_items {
 3150            None
 3151        } else {
 3152            window_handle
 3153        };
 3154        let app_state = self.app_state.clone();
 3155
 3156        cx.spawn(async move |_, cx| {
 3157            let OpenResult { workspace, .. } = cx
 3158                .update(|cx| {
 3159                    open_paths(
 3160                        &paths,
 3161                        app_state,
 3162                        OpenOptions {
 3163                            replace_window: window_to_replace,
 3164                            ..Default::default()
 3165                        },
 3166                        cx,
 3167                    )
 3168                })
 3169                .await?;
 3170            Ok(workspace)
 3171        })
 3172    }
 3173
 3174    #[allow(clippy::type_complexity)]
 3175    pub fn open_paths(
 3176        &mut self,
 3177        mut abs_paths: Vec<PathBuf>,
 3178        options: OpenOptions,
 3179        pane: Option<WeakEntity<Pane>>,
 3180        window: &mut Window,
 3181        cx: &mut Context<Self>,
 3182    ) -> Task<Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>> {
 3183        let fs = self.app_state.fs.clone();
 3184
 3185        let caller_ordered_abs_paths = abs_paths.clone();
 3186
 3187        // Sort the paths to ensure we add worktrees for parents before their children.
 3188        abs_paths.sort_unstable();
 3189        cx.spawn_in(window, async move |this, cx| {
 3190            let mut tasks = Vec::with_capacity(abs_paths.len());
 3191
 3192            for abs_path in &abs_paths {
 3193                let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
 3194                    OpenVisible::All => Some(true),
 3195                    OpenVisible::None => Some(false),
 3196                    OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
 3197                        Some(Some(metadata)) => Some(!metadata.is_dir),
 3198                        Some(None) => Some(true),
 3199                        None => None,
 3200                    },
 3201                    OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
 3202                        Some(Some(metadata)) => Some(metadata.is_dir),
 3203                        Some(None) => Some(false),
 3204                        None => None,
 3205                    },
 3206                };
 3207                let project_path = match visible {
 3208                    Some(visible) => match this
 3209                        .update(cx, |this, cx| {
 3210                            Workspace::project_path_for_path(
 3211                                this.project.clone(),
 3212                                abs_path,
 3213                                visible,
 3214                                cx,
 3215                            )
 3216                        })
 3217                        .log_err()
 3218                    {
 3219                        Some(project_path) => project_path.await.log_err(),
 3220                        None => None,
 3221                    },
 3222                    None => None,
 3223                };
 3224
 3225                let this = this.clone();
 3226                let abs_path: Arc<Path> = SanitizedPath::new(&abs_path).as_path().into();
 3227                let fs = fs.clone();
 3228                let pane = pane.clone();
 3229                let task = cx.spawn(async move |cx| {
 3230                    let (_worktree, project_path) = project_path?;
 3231                    if fs.is_dir(&abs_path).await {
 3232                        // Opening a directory should not race to update the active entry.
 3233                        // We'll select/reveal a deterministic final entry after all paths finish opening.
 3234                        None
 3235                    } else {
 3236                        Some(
 3237                            this.update_in(cx, |this, window, cx| {
 3238                                this.open_path(
 3239                                    project_path,
 3240                                    pane,
 3241                                    options.focus.unwrap_or(true),
 3242                                    window,
 3243                                    cx,
 3244                                )
 3245                            })
 3246                            .ok()?
 3247                            .await,
 3248                        )
 3249                    }
 3250                });
 3251                tasks.push(task);
 3252            }
 3253
 3254            let results = futures::future::join_all(tasks).await;
 3255
 3256            // Determine the winner using the fake/abstract FS metadata, not `Path::is_dir`.
 3257            let mut winner: Option<(PathBuf, bool)> = None;
 3258            for abs_path in caller_ordered_abs_paths.into_iter().rev() {
 3259                if let Some(Some(metadata)) = fs.metadata(&abs_path).await.log_err() {
 3260                    if !metadata.is_dir {
 3261                        winner = Some((abs_path, false));
 3262                        break;
 3263                    }
 3264                    if winner.is_none() {
 3265                        winner = Some((abs_path, true));
 3266                    }
 3267                } else if winner.is_none() {
 3268                    winner = Some((abs_path, false));
 3269                }
 3270            }
 3271
 3272            // Compute the winner entry id on the foreground thread and emit once, after all
 3273            // paths finish opening. This avoids races between concurrently-opening paths
 3274            // (directories in particular) and makes the resulting project panel selection
 3275            // deterministic.
 3276            if let Some((winner_abs_path, winner_is_dir)) = winner {
 3277                'emit_winner: {
 3278                    let winner_abs_path: Arc<Path> =
 3279                        SanitizedPath::new(&winner_abs_path).as_path().into();
 3280
 3281                    let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) {
 3282                        OpenVisible::All => true,
 3283                        OpenVisible::None => false,
 3284                        OpenVisible::OnlyFiles => !winner_is_dir,
 3285                        OpenVisible::OnlyDirectories => winner_is_dir,
 3286                    };
 3287
 3288                    let Some(worktree_task) = this
 3289                        .update(cx, |workspace, cx| {
 3290                            workspace.project.update(cx, |project, cx| {
 3291                                project.find_or_create_worktree(
 3292                                    winner_abs_path.as_ref(),
 3293                                    visible,
 3294                                    cx,
 3295                                )
 3296                            })
 3297                        })
 3298                        .ok()
 3299                    else {
 3300                        break 'emit_winner;
 3301                    };
 3302
 3303                    let Ok((worktree, _)) = worktree_task.await else {
 3304                        break 'emit_winner;
 3305                    };
 3306
 3307                    let Ok(Some(entry_id)) = this.update(cx, |_, cx| {
 3308                        let worktree = worktree.read(cx);
 3309                        let worktree_abs_path = worktree.abs_path();
 3310                        let entry = if winner_abs_path.as_ref() == worktree_abs_path.as_ref() {
 3311                            worktree.root_entry()
 3312                        } else {
 3313                            winner_abs_path
 3314                                .strip_prefix(worktree_abs_path.as_ref())
 3315                                .ok()
 3316                                .and_then(|relative_path| {
 3317                                    let relative_path =
 3318                                        RelPath::new(relative_path, PathStyle::local())
 3319                                            .log_err()?;
 3320                                    worktree.entry_for_path(&relative_path)
 3321                                })
 3322                        }?;
 3323                        Some(entry.id)
 3324                    }) else {
 3325                        break 'emit_winner;
 3326                    };
 3327
 3328                    this.update(cx, |workspace, cx| {
 3329                        workspace.project.update(cx, |_, cx| {
 3330                            cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
 3331                        });
 3332                    })
 3333                    .ok();
 3334                }
 3335            }
 3336
 3337            results
 3338        })
 3339    }
 3340
 3341    pub fn open_resolved_path(
 3342        &mut self,
 3343        path: ResolvedPath,
 3344        window: &mut Window,
 3345        cx: &mut Context<Self>,
 3346    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 3347        match path {
 3348            ResolvedPath::ProjectPath { project_path, .. } => {
 3349                self.open_path(project_path, None, true, window, cx)
 3350            }
 3351            ResolvedPath::AbsPath { path, .. } => self.open_abs_path(
 3352                PathBuf::from(path),
 3353                OpenOptions {
 3354                    visible: Some(OpenVisible::None),
 3355                    ..Default::default()
 3356                },
 3357                window,
 3358                cx,
 3359            ),
 3360        }
 3361    }
 3362
 3363    pub fn absolute_path_of_worktree(
 3364        &self,
 3365        worktree_id: WorktreeId,
 3366        cx: &mut Context<Self>,
 3367    ) -> Option<PathBuf> {
 3368        self.project
 3369            .read(cx)
 3370            .worktree_for_id(worktree_id, cx)
 3371            // TODO: use `abs_path` or `root_dir`
 3372            .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
 3373    }
 3374
 3375    pub fn add_folder_to_project(
 3376        &mut self,
 3377        _: &AddFolderToProject,
 3378        window: &mut Window,
 3379        cx: &mut Context<Self>,
 3380    ) {
 3381        let project = self.project.read(cx);
 3382        if project.is_via_collab() {
 3383            self.show_error(
 3384                &anyhow!("You cannot add folders to someone else's project"),
 3385                cx,
 3386            );
 3387            return;
 3388        }
 3389        let paths = self.prompt_for_open_path(
 3390            PathPromptOptions {
 3391                files: false,
 3392                directories: true,
 3393                multiple: true,
 3394                prompt: None,
 3395            },
 3396            DirectoryLister::Project(self.project.clone()),
 3397            window,
 3398            cx,
 3399        );
 3400        cx.spawn_in(window, async move |this, cx| {
 3401            if let Some(paths) = paths.await.log_err().flatten() {
 3402                let results = this
 3403                    .update_in(cx, |this, window, cx| {
 3404                        this.open_paths(
 3405                            paths,
 3406                            OpenOptions {
 3407                                visible: Some(OpenVisible::All),
 3408                                ..Default::default()
 3409                            },
 3410                            None,
 3411                            window,
 3412                            cx,
 3413                        )
 3414                    })?
 3415                    .await;
 3416                for result in results.into_iter().flatten() {
 3417                    result.log_err();
 3418                }
 3419            }
 3420            anyhow::Ok(())
 3421        })
 3422        .detach_and_log_err(cx);
 3423    }
 3424
 3425    pub fn project_path_for_path(
 3426        project: Entity<Project>,
 3427        abs_path: &Path,
 3428        visible: bool,
 3429        cx: &mut App,
 3430    ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
 3431        let entry = project.update(cx, |project, cx| {
 3432            project.find_or_create_worktree(abs_path, visible, cx)
 3433        });
 3434        cx.spawn(async move |cx| {
 3435            let (worktree, path) = entry.await?;
 3436            let worktree_id = worktree.read_with(cx, |t, _| t.id());
 3437            Ok((worktree, ProjectPath { worktree_id, path }))
 3438        })
 3439    }
 3440
 3441    pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
 3442        self.panes.iter().flat_map(|pane| pane.read(cx).items())
 3443    }
 3444
 3445    pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
 3446        self.items_of_type(cx).max_by_key(|item| item.item_id())
 3447    }
 3448
 3449    pub fn items_of_type<'a, T: Item>(
 3450        &'a self,
 3451        cx: &'a App,
 3452    ) -> impl 'a + Iterator<Item = Entity<T>> {
 3453        self.panes
 3454            .iter()
 3455            .flat_map(|pane| pane.read(cx).items_of_type())
 3456    }
 3457
 3458    pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
 3459        self.active_pane().read(cx).active_item()
 3460    }
 3461
 3462    pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
 3463        let item = self.active_item(cx)?;
 3464        item.to_any_view().downcast::<I>().ok()
 3465    }
 3466
 3467    fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
 3468        self.active_item(cx).and_then(|item| item.project_path(cx))
 3469    }
 3470
 3471    pub fn most_recent_active_path(&self, cx: &App) -> Option<PathBuf> {
 3472        self.recent_navigation_history_iter(cx)
 3473            .filter_map(|(path, abs_path)| {
 3474                let worktree = self
 3475                    .project
 3476                    .read(cx)
 3477                    .worktree_for_id(path.worktree_id, cx)?;
 3478                if worktree.read(cx).is_visible() {
 3479                    abs_path
 3480                } else {
 3481                    None
 3482                }
 3483            })
 3484            .next()
 3485    }
 3486
 3487    pub fn save_active_item(
 3488        &mut self,
 3489        save_intent: SaveIntent,
 3490        window: &mut Window,
 3491        cx: &mut App,
 3492    ) -> Task<Result<()>> {
 3493        let project = self.project.clone();
 3494        let pane = self.active_pane();
 3495        let item = pane.read(cx).active_item();
 3496        let pane = pane.downgrade();
 3497
 3498        window.spawn(cx, async move |cx| {
 3499            if let Some(item) = item {
 3500                Pane::save_item(project, &pane, item.as_ref(), save_intent, cx)
 3501                    .await
 3502                    .map(|_| ())
 3503            } else {
 3504                Ok(())
 3505            }
 3506        })
 3507    }
 3508
 3509    pub fn close_inactive_items_and_panes(
 3510        &mut self,
 3511        action: &CloseInactiveTabsAndPanes,
 3512        window: &mut Window,
 3513        cx: &mut Context<Self>,
 3514    ) {
 3515        if let Some(task) = self.close_all_internal(
 3516            true,
 3517            action.save_intent.unwrap_or(SaveIntent::Close),
 3518            window,
 3519            cx,
 3520        ) {
 3521            task.detach_and_log_err(cx)
 3522        }
 3523    }
 3524
 3525    pub fn close_all_items_and_panes(
 3526        &mut self,
 3527        action: &CloseAllItemsAndPanes,
 3528        window: &mut Window,
 3529        cx: &mut Context<Self>,
 3530    ) {
 3531        if let Some(task) = self.close_all_internal(
 3532            false,
 3533            action.save_intent.unwrap_or(SaveIntent::Close),
 3534            window,
 3535            cx,
 3536        ) {
 3537            task.detach_and_log_err(cx)
 3538        }
 3539    }
 3540
 3541    /// Closes the active item across all panes.
 3542    pub fn close_item_in_all_panes(
 3543        &mut self,
 3544        action: &CloseItemInAllPanes,
 3545        window: &mut Window,
 3546        cx: &mut Context<Self>,
 3547    ) {
 3548        let Some(active_item) = self.active_pane().read(cx).active_item() else {
 3549            return;
 3550        };
 3551
 3552        let save_intent = action.save_intent.unwrap_or(SaveIntent::Close);
 3553        let close_pinned = action.close_pinned;
 3554
 3555        if let Some(project_path) = active_item.project_path(cx) {
 3556            self.close_items_with_project_path(
 3557                &project_path,
 3558                save_intent,
 3559                close_pinned,
 3560                window,
 3561                cx,
 3562            );
 3563        } else if close_pinned || !self.active_pane().read(cx).is_active_item_pinned() {
 3564            let item_id = active_item.item_id();
 3565            self.active_pane().update(cx, |pane, cx| {
 3566                pane.close_item_by_id(item_id, save_intent, window, cx)
 3567                    .detach_and_log_err(cx);
 3568            });
 3569        }
 3570    }
 3571
 3572    /// Closes all items with the given project path across all panes.
 3573    pub fn close_items_with_project_path(
 3574        &mut self,
 3575        project_path: &ProjectPath,
 3576        save_intent: SaveIntent,
 3577        close_pinned: bool,
 3578        window: &mut Window,
 3579        cx: &mut Context<Self>,
 3580    ) {
 3581        let panes = self.panes().to_vec();
 3582        for pane in panes {
 3583            pane.update(cx, |pane, cx| {
 3584                pane.close_items_for_project_path(
 3585                    project_path,
 3586                    save_intent,
 3587                    close_pinned,
 3588                    window,
 3589                    cx,
 3590                )
 3591                .detach_and_log_err(cx);
 3592            });
 3593        }
 3594    }
 3595
 3596    fn close_all_internal(
 3597        &mut self,
 3598        retain_active_pane: bool,
 3599        save_intent: SaveIntent,
 3600        window: &mut Window,
 3601        cx: &mut Context<Self>,
 3602    ) -> Option<Task<Result<()>>> {
 3603        let current_pane = self.active_pane();
 3604
 3605        let mut tasks = Vec::new();
 3606
 3607        if retain_active_pane {
 3608            let current_pane_close = current_pane.update(cx, |pane, cx| {
 3609                pane.close_other_items(
 3610                    &CloseOtherItems {
 3611                        save_intent: None,
 3612                        close_pinned: false,
 3613                    },
 3614                    None,
 3615                    window,
 3616                    cx,
 3617                )
 3618            });
 3619
 3620            tasks.push(current_pane_close);
 3621        }
 3622
 3623        for pane in self.panes() {
 3624            if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
 3625                continue;
 3626            }
 3627
 3628            let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| {
 3629                pane.close_all_items(
 3630                    &CloseAllItems {
 3631                        save_intent: Some(save_intent),
 3632                        close_pinned: false,
 3633                    },
 3634                    window,
 3635                    cx,
 3636                )
 3637            });
 3638
 3639            tasks.push(close_pane_items)
 3640        }
 3641
 3642        if tasks.is_empty() {
 3643            None
 3644        } else {
 3645            Some(cx.spawn_in(window, async move |_, _| {
 3646                for task in tasks {
 3647                    task.await?
 3648                }
 3649                Ok(())
 3650            }))
 3651        }
 3652    }
 3653
 3654    pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
 3655        self.dock_at_position(position).read(cx).is_open()
 3656    }
 3657
 3658    pub fn toggle_dock(
 3659        &mut self,
 3660        dock_side: DockPosition,
 3661        window: &mut Window,
 3662        cx: &mut Context<Self>,
 3663    ) {
 3664        let mut focus_center = false;
 3665        let mut reveal_dock = false;
 3666
 3667        let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
 3668        let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed;
 3669
 3670        if let Some(panel) = self.dock_at_position(dock_side).read(cx).active_panel() {
 3671            telemetry::event!(
 3672                "Panel Button Clicked",
 3673                name = panel.persistent_name(),
 3674                toggle_state = !was_visible
 3675            );
 3676        }
 3677        if was_visible {
 3678            self.save_open_dock_positions(cx);
 3679        }
 3680
 3681        let dock = self.dock_at_position(dock_side);
 3682        dock.update(cx, |dock, cx| {
 3683            dock.set_open(!was_visible, window, cx);
 3684
 3685            if dock.active_panel().is_none() {
 3686                let Some(panel_ix) = dock
 3687                    .first_enabled_panel_idx(cx)
 3688                    .log_with_level(log::Level::Info)
 3689                else {
 3690                    return;
 3691                };
 3692                dock.activate_panel(panel_ix, window, cx);
 3693            }
 3694
 3695            if let Some(active_panel) = dock.active_panel() {
 3696                if was_visible {
 3697                    if active_panel
 3698                        .panel_focus_handle(cx)
 3699                        .contains_focused(window, cx)
 3700                    {
 3701                        focus_center = true;
 3702                    }
 3703                } else {
 3704                    let focus_handle = &active_panel.panel_focus_handle(cx);
 3705                    window.focus(focus_handle, cx);
 3706                    reveal_dock = true;
 3707                }
 3708            }
 3709        });
 3710
 3711        if reveal_dock {
 3712            self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
 3713        }
 3714
 3715        if focus_center {
 3716            self.active_pane
 3717                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
 3718        }
 3719
 3720        cx.notify();
 3721        self.serialize_workspace(window, cx);
 3722    }
 3723
 3724    fn active_dock(&self, window: &Window, cx: &Context<Self>) -> Option<&Entity<Dock>> {
 3725        self.all_docks().into_iter().find(|&dock| {
 3726            dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx)
 3727        })
 3728    }
 3729
 3730    fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
 3731        if let Some(dock) = self.active_dock(window, cx).cloned() {
 3732            self.save_open_dock_positions(cx);
 3733            dock.update(cx, |dock, cx| {
 3734                dock.set_open(false, window, cx);
 3735            });
 3736            return true;
 3737        }
 3738        false
 3739    }
 3740
 3741    pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 3742        self.save_open_dock_positions(cx);
 3743        for dock in self.all_docks() {
 3744            dock.update(cx, |dock, cx| {
 3745                dock.set_open(false, window, cx);
 3746            });
 3747        }
 3748
 3749        cx.focus_self(window);
 3750        cx.notify();
 3751        self.serialize_workspace(window, cx);
 3752    }
 3753
 3754    fn get_open_dock_positions(&self, cx: &Context<Self>) -> Vec<DockPosition> {
 3755        self.all_docks()
 3756            .into_iter()
 3757            .filter_map(|dock| {
 3758                let dock_ref = dock.read(cx);
 3759                if dock_ref.is_open() {
 3760                    Some(dock_ref.position())
 3761                } else {
 3762                    None
 3763                }
 3764            })
 3765            .collect()
 3766    }
 3767
 3768    /// Saves the positions of currently open docks.
 3769    ///
 3770    /// Updates `last_open_dock_positions` with positions of all currently open
 3771    /// docks, to later be restored by the 'Toggle All Docks' action.
 3772    fn save_open_dock_positions(&mut self, cx: &mut Context<Self>) {
 3773        let open_dock_positions = self.get_open_dock_positions(cx);
 3774        if !open_dock_positions.is_empty() {
 3775            self.last_open_dock_positions = open_dock_positions;
 3776        }
 3777    }
 3778
 3779    /// Toggles all docks between open and closed states.
 3780    ///
 3781    /// If any docks are open, closes all and remembers their positions. If all
 3782    /// docks are closed, restores the last remembered dock configuration.
 3783    fn toggle_all_docks(
 3784        &mut self,
 3785        _: &ToggleAllDocks,
 3786        window: &mut Window,
 3787        cx: &mut Context<Self>,
 3788    ) {
 3789        let open_dock_positions = self.get_open_dock_positions(cx);
 3790
 3791        if !open_dock_positions.is_empty() {
 3792            self.close_all_docks(window, cx);
 3793        } else if !self.last_open_dock_positions.is_empty() {
 3794            self.restore_last_open_docks(window, cx);
 3795        }
 3796    }
 3797
 3798    /// Reopens docks from the most recently remembered configuration.
 3799    ///
 3800    /// Opens all docks whose positions are stored in `last_open_dock_positions`
 3801    /// and clears the stored positions.
 3802    fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 3803        let positions_to_open = std::mem::take(&mut self.last_open_dock_positions);
 3804
 3805        for position in positions_to_open {
 3806            let dock = self.dock_at_position(position);
 3807            dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
 3808        }
 3809
 3810        cx.focus_self(window);
 3811        cx.notify();
 3812        self.serialize_workspace(window, cx);
 3813    }
 3814
 3815    /// Transfer focus to the panel of the given type.
 3816    pub fn focus_panel<T: Panel>(
 3817        &mut self,
 3818        window: &mut Window,
 3819        cx: &mut Context<Self>,
 3820    ) -> Option<Entity<T>> {
 3821        let panel = self.focus_or_unfocus_panel::<T>(window, cx, &mut |_, _, _| true)?;
 3822        panel.to_any().downcast().ok()
 3823    }
 3824
 3825    /// Focus the panel of the given type if it isn't already focused. If it is
 3826    /// already focused, then transfer focus back to the workspace center.
 3827    /// When the `close_panel_on_toggle` setting is enabled, also closes the
 3828    /// panel when transferring focus back to the center.
 3829    pub fn toggle_panel_focus<T: Panel>(
 3830        &mut self,
 3831        window: &mut Window,
 3832        cx: &mut Context<Self>,
 3833    ) -> bool {
 3834        let mut did_focus_panel = false;
 3835        self.focus_or_unfocus_panel::<T>(window, cx, &mut |panel, window, cx| {
 3836            did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx);
 3837            did_focus_panel
 3838        });
 3839
 3840        if !did_focus_panel && WorkspaceSettings::get_global(cx).close_panel_on_toggle {
 3841            self.close_panel::<T>(window, cx);
 3842        }
 3843
 3844        telemetry::event!(
 3845            "Panel Button Clicked",
 3846            name = T::persistent_name(),
 3847            toggle_state = did_focus_panel
 3848        );
 3849
 3850        did_focus_panel
 3851    }
 3852
 3853    pub fn focus_center_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 3854        if let Some(item) = self.active_item(cx) {
 3855            item.item_focus_handle(cx).focus(window, cx);
 3856        } else {
 3857            log::error!("Could not find a focus target when switching focus to the center panes",);
 3858        }
 3859    }
 3860
 3861    pub fn activate_panel_for_proto_id(
 3862        &mut self,
 3863        panel_id: PanelId,
 3864        window: &mut Window,
 3865        cx: &mut Context<Self>,
 3866    ) -> Option<Arc<dyn PanelHandle>> {
 3867        let mut panel = None;
 3868        for dock in self.all_docks() {
 3869            if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
 3870                panel = dock.update(cx, |dock, cx| {
 3871                    dock.activate_panel(panel_index, window, cx);
 3872                    dock.set_open(true, window, cx);
 3873                    dock.active_panel().cloned()
 3874                });
 3875                break;
 3876            }
 3877        }
 3878
 3879        if panel.is_some() {
 3880            cx.notify();
 3881            self.serialize_workspace(window, cx);
 3882        }
 3883
 3884        panel
 3885    }
 3886
 3887    /// Focus or unfocus the given panel type, depending on the given callback.
 3888    fn focus_or_unfocus_panel<T: Panel>(
 3889        &mut self,
 3890        window: &mut Window,
 3891        cx: &mut Context<Self>,
 3892        should_focus: &mut dyn FnMut(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
 3893    ) -> Option<Arc<dyn PanelHandle>> {
 3894        let mut result_panel = None;
 3895        let mut serialize = false;
 3896        for dock in self.all_docks() {
 3897            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
 3898                let mut focus_center = false;
 3899                let panel = dock.update(cx, |dock, cx| {
 3900                    dock.activate_panel(panel_index, window, cx);
 3901
 3902                    let panel = dock.active_panel().cloned();
 3903                    if let Some(panel) = panel.as_ref() {
 3904                        if should_focus(&**panel, window, cx) {
 3905                            dock.set_open(true, window, cx);
 3906                            panel.panel_focus_handle(cx).focus(window, cx);
 3907                        } else {
 3908                            focus_center = true;
 3909                        }
 3910                    }
 3911                    panel
 3912                });
 3913
 3914                if focus_center {
 3915                    self.active_pane
 3916                        .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
 3917                }
 3918
 3919                result_panel = panel;
 3920                serialize = true;
 3921                break;
 3922            }
 3923        }
 3924
 3925        if serialize {
 3926            self.serialize_workspace(window, cx);
 3927        }
 3928
 3929        cx.notify();
 3930        result_panel
 3931    }
 3932
 3933    /// Open the panel of the given type
 3934    pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 3935        for dock in self.all_docks() {
 3936            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
 3937                dock.update(cx, |dock, cx| {
 3938                    dock.activate_panel(panel_index, window, cx);
 3939                    dock.set_open(true, window, cx);
 3940                });
 3941            }
 3942        }
 3943    }
 3944
 3945    pub fn close_panel<T: Panel>(&self, window: &mut Window, cx: &mut Context<Self>) {
 3946        for dock in self.all_docks().iter() {
 3947            dock.update(cx, |dock, cx| {
 3948                if dock.panel::<T>().is_some() {
 3949                    dock.set_open(false, window, cx)
 3950                }
 3951            })
 3952        }
 3953    }
 3954
 3955    pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
 3956        self.all_docks()
 3957            .iter()
 3958            .find_map(|dock| dock.read(cx).panel::<T>())
 3959    }
 3960
 3961    fn dismiss_zoomed_items_to_reveal(
 3962        &mut self,
 3963        dock_to_reveal: Option<DockPosition>,
 3964        window: &mut Window,
 3965        cx: &mut Context<Self>,
 3966    ) {
 3967        // If a center pane is zoomed, unzoom it.
 3968        for pane in &self.panes {
 3969            if pane != &self.active_pane || dock_to_reveal.is_some() {
 3970                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
 3971            }
 3972        }
 3973
 3974        // If another dock is zoomed, hide it.
 3975        let mut focus_center = false;
 3976        for dock in self.all_docks() {
 3977            dock.update(cx, |dock, cx| {
 3978                if Some(dock.position()) != dock_to_reveal
 3979                    && let Some(panel) = dock.active_panel()
 3980                    && panel.is_zoomed(window, cx)
 3981                {
 3982                    focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx);
 3983                    dock.set_open(false, window, cx);
 3984                }
 3985            });
 3986        }
 3987
 3988        if focus_center {
 3989            self.active_pane
 3990                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx))
 3991        }
 3992
 3993        if self.zoomed_position != dock_to_reveal {
 3994            self.zoomed = None;
 3995            self.zoomed_position = None;
 3996            cx.emit(Event::ZoomChanged);
 3997        }
 3998
 3999        cx.notify();
 4000    }
 4001
 4002    fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
 4003        let pane = cx.new(|cx| {
 4004            let mut pane = Pane::new(
 4005                self.weak_handle(),
 4006                self.project.clone(),
 4007                self.pane_history_timestamp.clone(),
 4008                None,
 4009                NewFile.boxed_clone(),
 4010                true,
 4011                window,
 4012                cx,
 4013            );
 4014            pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
 4015            pane
 4016        });
 4017        cx.subscribe_in(&pane, window, Self::handle_pane_event)
 4018            .detach();
 4019        self.panes.push(pane.clone());
 4020
 4021        window.focus(&pane.focus_handle(cx), cx);
 4022
 4023        cx.emit(Event::PaneAdded(pane.clone()));
 4024        pane
 4025    }
 4026
 4027    pub fn add_item_to_center(
 4028        &mut self,
 4029        item: Box<dyn ItemHandle>,
 4030        window: &mut Window,
 4031        cx: &mut Context<Self>,
 4032    ) -> bool {
 4033        if let Some(center_pane) = self.last_active_center_pane.clone() {
 4034            if let Some(center_pane) = center_pane.upgrade() {
 4035                center_pane.update(cx, |pane, cx| {
 4036                    pane.add_item(item, true, true, None, window, cx)
 4037                });
 4038                true
 4039            } else {
 4040                false
 4041            }
 4042        } else {
 4043            false
 4044        }
 4045    }
 4046
 4047    pub fn add_item_to_active_pane(
 4048        &mut self,
 4049        item: Box<dyn ItemHandle>,
 4050        destination_index: Option<usize>,
 4051        focus_item: bool,
 4052        window: &mut Window,
 4053        cx: &mut App,
 4054    ) {
 4055        self.add_item(
 4056            self.active_pane.clone(),
 4057            item,
 4058            destination_index,
 4059            false,
 4060            focus_item,
 4061            window,
 4062            cx,
 4063        )
 4064    }
 4065
 4066    pub fn add_item(
 4067        &mut self,
 4068        pane: Entity<Pane>,
 4069        item: Box<dyn ItemHandle>,
 4070        destination_index: Option<usize>,
 4071        activate_pane: bool,
 4072        focus_item: bool,
 4073        window: &mut Window,
 4074        cx: &mut App,
 4075    ) {
 4076        pane.update(cx, |pane, cx| {
 4077            pane.add_item(
 4078                item,
 4079                activate_pane,
 4080                focus_item,
 4081                destination_index,
 4082                window,
 4083                cx,
 4084            )
 4085        });
 4086    }
 4087
 4088    pub fn split_item(
 4089        &mut self,
 4090        split_direction: SplitDirection,
 4091        item: Box<dyn ItemHandle>,
 4092        window: &mut Window,
 4093        cx: &mut Context<Self>,
 4094    ) {
 4095        let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
 4096        self.add_item(new_pane, item, None, true, true, window, cx);
 4097    }
 4098
 4099    pub fn open_abs_path(
 4100        &mut self,
 4101        abs_path: PathBuf,
 4102        options: OpenOptions,
 4103        window: &mut Window,
 4104        cx: &mut Context<Self>,
 4105    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4106        cx.spawn_in(window, async move |workspace, cx| {
 4107            let open_paths_task_result = workspace
 4108                .update_in(cx, |workspace, window, cx| {
 4109                    workspace.open_paths(vec![abs_path.clone()], options, None, window, cx)
 4110                })
 4111                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
 4112                .await;
 4113            anyhow::ensure!(
 4114                open_paths_task_result.len() == 1,
 4115                "open abs path {abs_path:?} task returned incorrect number of results"
 4116            );
 4117            match open_paths_task_result
 4118                .into_iter()
 4119                .next()
 4120                .expect("ensured single task result")
 4121            {
 4122                Some(open_result) => {
 4123                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
 4124                }
 4125                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
 4126            }
 4127        })
 4128    }
 4129
 4130    pub fn split_abs_path(
 4131        &mut self,
 4132        abs_path: PathBuf,
 4133        visible: bool,
 4134        window: &mut Window,
 4135        cx: &mut Context<Self>,
 4136    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4137        let project_path_task =
 4138            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
 4139        cx.spawn_in(window, async move |this, cx| {
 4140            let (_, path) = project_path_task.await?;
 4141            this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))?
 4142                .await
 4143        })
 4144    }
 4145
 4146    pub fn open_path(
 4147        &mut self,
 4148        path: impl Into<ProjectPath>,
 4149        pane: Option<WeakEntity<Pane>>,
 4150        focus_item: bool,
 4151        window: &mut Window,
 4152        cx: &mut App,
 4153    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4154        self.open_path_preview(path, pane, focus_item, false, true, window, cx)
 4155    }
 4156
 4157    pub fn open_path_preview(
 4158        &mut self,
 4159        path: impl Into<ProjectPath>,
 4160        pane: Option<WeakEntity<Pane>>,
 4161        focus_item: bool,
 4162        allow_preview: bool,
 4163        activate: bool,
 4164        window: &mut Window,
 4165        cx: &mut App,
 4166    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4167        let pane = pane.unwrap_or_else(|| {
 4168            self.last_active_center_pane.clone().unwrap_or_else(|| {
 4169                self.panes
 4170                    .first()
 4171                    .expect("There must be an active pane")
 4172                    .downgrade()
 4173            })
 4174        });
 4175
 4176        let project_path = path.into();
 4177        let task = self.load_path(project_path.clone(), window, cx);
 4178        window.spawn(cx, async move |cx| {
 4179            let (project_entry_id, build_item) = task.await?;
 4180
 4181            pane.update_in(cx, |pane, window, cx| {
 4182                pane.open_item(
 4183                    project_entry_id,
 4184                    project_path,
 4185                    focus_item,
 4186                    allow_preview,
 4187                    activate,
 4188                    None,
 4189                    window,
 4190                    cx,
 4191                    build_item,
 4192                )
 4193            })
 4194        })
 4195    }
 4196
 4197    pub fn split_path(
 4198        &mut self,
 4199        path: impl Into<ProjectPath>,
 4200        window: &mut Window,
 4201        cx: &mut Context<Self>,
 4202    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4203        self.split_path_preview(path, false, None, window, cx)
 4204    }
 4205
 4206    pub fn split_path_preview(
 4207        &mut self,
 4208        path: impl Into<ProjectPath>,
 4209        allow_preview: bool,
 4210        split_direction: Option<SplitDirection>,
 4211        window: &mut Window,
 4212        cx: &mut Context<Self>,
 4213    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
 4214        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
 4215            self.panes
 4216                .first()
 4217                .expect("There must be an active pane")
 4218                .downgrade()
 4219        });
 4220
 4221        if let Member::Pane(center_pane) = &self.center.root
 4222            && center_pane.read(cx).items_len() == 0
 4223        {
 4224            return self.open_path(path, Some(pane), true, window, cx);
 4225        }
 4226
 4227        let project_path = path.into();
 4228        let task = self.load_path(project_path.clone(), window, cx);
 4229        cx.spawn_in(window, async move |this, cx| {
 4230            let (project_entry_id, build_item) = task.await?;
 4231            this.update_in(cx, move |this, window, cx| -> Option<_> {
 4232                let pane = pane.upgrade()?;
 4233                let new_pane = this.split_pane(
 4234                    pane,
 4235                    split_direction.unwrap_or(SplitDirection::Right),
 4236                    window,
 4237                    cx,
 4238                );
 4239                new_pane.update(cx, |new_pane, cx| {
 4240                    Some(new_pane.open_item(
 4241                        project_entry_id,
 4242                        project_path,
 4243                        true,
 4244                        allow_preview,
 4245                        true,
 4246                        None,
 4247                        window,
 4248                        cx,
 4249                        build_item,
 4250                    ))
 4251                })
 4252            })
 4253            .map(|option| option.context("pane was dropped"))?
 4254        })
 4255    }
 4256
 4257    fn load_path(
 4258        &mut self,
 4259        path: ProjectPath,
 4260        window: &mut Window,
 4261        cx: &mut App,
 4262    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
 4263        let registry = cx.default_global::<ProjectItemRegistry>().clone();
 4264        registry.open_path(self.project(), &path, window, cx)
 4265    }
 4266
 4267    pub fn find_project_item<T>(
 4268        &self,
 4269        pane: &Entity<Pane>,
 4270        project_item: &Entity<T::Item>,
 4271        cx: &App,
 4272    ) -> Option<Entity<T>>
 4273    where
 4274        T: ProjectItem,
 4275    {
 4276        use project::ProjectItem as _;
 4277        let project_item = project_item.read(cx);
 4278        let entry_id = project_item.entry_id(cx);
 4279        let project_path = project_item.project_path(cx);
 4280
 4281        let mut item = None;
 4282        if let Some(entry_id) = entry_id {
 4283            item = pane.read(cx).item_for_entry(entry_id, cx);
 4284        }
 4285        if item.is_none()
 4286            && let Some(project_path) = project_path
 4287        {
 4288            item = pane.read(cx).item_for_path(project_path, cx);
 4289        }
 4290
 4291        item.and_then(|item| item.downcast::<T>())
 4292    }
 4293
 4294    pub fn is_project_item_open<T>(
 4295        &self,
 4296        pane: &Entity<Pane>,
 4297        project_item: &Entity<T::Item>,
 4298        cx: &App,
 4299    ) -> bool
 4300    where
 4301        T: ProjectItem,
 4302    {
 4303        self.find_project_item::<T>(pane, project_item, cx)
 4304            .is_some()
 4305    }
 4306
 4307    pub fn open_project_item<T>(
 4308        &mut self,
 4309        pane: Entity<Pane>,
 4310        project_item: Entity<T::Item>,
 4311        activate_pane: bool,
 4312        focus_item: bool,
 4313        keep_old_preview: bool,
 4314        allow_new_preview: bool,
 4315        window: &mut Window,
 4316        cx: &mut Context<Self>,
 4317    ) -> Entity<T>
 4318    where
 4319        T: ProjectItem,
 4320    {
 4321        let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
 4322
 4323        if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
 4324            if !keep_old_preview
 4325                && let Some(old_id) = old_item_id
 4326                && old_id != item.item_id()
 4327            {
 4328                // switching to a different item, so unpreview old active item
 4329                pane.update(cx, |pane, _| {
 4330                    pane.unpreview_item_if_preview(old_id);
 4331                });
 4332            }
 4333
 4334            self.activate_item(&item, activate_pane, focus_item, window, cx);
 4335            if !allow_new_preview {
 4336                pane.update(cx, |pane, _| {
 4337                    pane.unpreview_item_if_preview(item.item_id());
 4338                });
 4339            }
 4340            return item;
 4341        }
 4342
 4343        let item = pane.update(cx, |pane, cx| {
 4344            cx.new(|cx| {
 4345                T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
 4346            })
 4347        });
 4348        let mut destination_index = None;
 4349        pane.update(cx, |pane, cx| {
 4350            if !keep_old_preview && let Some(old_id) = old_item_id {
 4351                pane.unpreview_item_if_preview(old_id);
 4352            }
 4353            if allow_new_preview {
 4354                destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
 4355            }
 4356        });
 4357
 4358        self.add_item(
 4359            pane,
 4360            Box::new(item.clone()),
 4361            destination_index,
 4362            activate_pane,
 4363            focus_item,
 4364            window,
 4365            cx,
 4366        );
 4367        item
 4368    }
 4369
 4370    pub fn open_shared_screen(
 4371        &mut self,
 4372        peer_id: PeerId,
 4373        window: &mut Window,
 4374        cx: &mut Context<Self>,
 4375    ) {
 4376        if let Some(shared_screen) =
 4377            self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
 4378        {
 4379            self.active_pane.update(cx, |pane, cx| {
 4380                pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
 4381            });
 4382        }
 4383    }
 4384
 4385    pub fn activate_item(
 4386        &mut self,
 4387        item: &dyn ItemHandle,
 4388        activate_pane: bool,
 4389        focus_item: bool,
 4390        window: &mut Window,
 4391        cx: &mut App,
 4392    ) -> bool {
 4393        let result = self.panes.iter().find_map(|pane| {
 4394            pane.read(cx)
 4395                .index_for_item(item)
 4396                .map(|ix| (pane.clone(), ix))
 4397        });
 4398        if let Some((pane, ix)) = result {
 4399            pane.update(cx, |pane, cx| {
 4400                pane.activate_item(ix, activate_pane, focus_item, window, cx)
 4401            });
 4402            true
 4403        } else {
 4404            false
 4405        }
 4406    }
 4407
 4408    fn activate_pane_at_index(
 4409        &mut self,
 4410        action: &ActivatePane,
 4411        window: &mut Window,
 4412        cx: &mut Context<Self>,
 4413    ) {
 4414        let panes = self.center.panes();
 4415        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
 4416            window.focus(&pane.focus_handle(cx), cx);
 4417        } else {
 4418            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx)
 4419                .detach();
 4420        }
 4421    }
 4422
 4423    fn move_item_to_pane_at_index(
 4424        &mut self,
 4425        action: &MoveItemToPane,
 4426        window: &mut Window,
 4427        cx: &mut Context<Self>,
 4428    ) {
 4429        let panes = self.center.panes();
 4430        let destination = match panes.get(action.destination) {
 4431            Some(&destination) => destination.clone(),
 4432            None => {
 4433                if !action.clone && self.active_pane.read(cx).items_len() < 2 {
 4434                    return;
 4435                }
 4436                let direction = SplitDirection::Right;
 4437                let split_off_pane = self
 4438                    .find_pane_in_direction(direction, cx)
 4439                    .unwrap_or_else(|| self.active_pane.clone());
 4440                let new_pane = self.add_pane(window, cx);
 4441                self.center.split(&split_off_pane, &new_pane, direction, cx);
 4442                new_pane
 4443            }
 4444        };
 4445
 4446        if action.clone {
 4447            if self
 4448                .active_pane
 4449                .read(cx)
 4450                .active_item()
 4451                .is_some_and(|item| item.can_split(cx))
 4452            {
 4453                clone_active_item(
 4454                    self.database_id(),
 4455                    &self.active_pane,
 4456                    &destination,
 4457                    action.focus,
 4458                    window,
 4459                    cx,
 4460                );
 4461                return;
 4462            }
 4463        }
 4464        move_active_item(
 4465            &self.active_pane,
 4466            &destination,
 4467            action.focus,
 4468            true,
 4469            window,
 4470            cx,
 4471        )
 4472    }
 4473
 4474    pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
 4475        let panes = self.center.panes();
 4476        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
 4477            let next_ix = (ix + 1) % panes.len();
 4478            let next_pane = panes[next_ix].clone();
 4479            window.focus(&next_pane.focus_handle(cx), cx);
 4480        }
 4481    }
 4482
 4483    pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
 4484        let panes = self.center.panes();
 4485        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
 4486            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
 4487            let prev_pane = panes[prev_ix].clone();
 4488            window.focus(&prev_pane.focus_handle(cx), cx);
 4489        }
 4490    }
 4491
 4492    pub fn activate_last_pane(&mut self, window: &mut Window, cx: &mut App) {
 4493        let last_pane = self.center.last_pane();
 4494        window.focus(&last_pane.focus_handle(cx), cx);
 4495    }
 4496
 4497    pub fn activate_pane_in_direction(
 4498        &mut self,
 4499        direction: SplitDirection,
 4500        window: &mut Window,
 4501        cx: &mut App,
 4502    ) {
 4503        use ActivateInDirectionTarget as Target;
 4504        enum Origin {
 4505            Sidebar,
 4506            LeftDock,
 4507            RightDock,
 4508            BottomDock,
 4509            Center,
 4510        }
 4511
 4512        let origin: Origin = if self
 4513            .sidebar_focus_handle
 4514            .as_ref()
 4515            .is_some_and(|h| h.contains_focused(window, cx))
 4516        {
 4517            Origin::Sidebar
 4518        } else {
 4519            [
 4520                (&self.left_dock, Origin::LeftDock),
 4521                (&self.right_dock, Origin::RightDock),
 4522                (&self.bottom_dock, Origin::BottomDock),
 4523            ]
 4524            .into_iter()
 4525            .find_map(|(dock, origin)| {
 4526                if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
 4527                    Some(origin)
 4528                } else {
 4529                    None
 4530                }
 4531            })
 4532            .unwrap_or(Origin::Center)
 4533        };
 4534
 4535        let get_last_active_pane = || {
 4536            let pane = self
 4537                .last_active_center_pane
 4538                .clone()
 4539                .unwrap_or_else(|| {
 4540                    self.panes
 4541                        .first()
 4542                        .expect("There must be an active pane")
 4543                        .downgrade()
 4544                })
 4545                .upgrade()?;
 4546            (pane.read(cx).items_len() != 0).then_some(pane)
 4547        };
 4548
 4549        let try_dock =
 4550            |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
 4551
 4552        let sidebar_target = self
 4553            .sidebar_focus_handle
 4554            .as_ref()
 4555            .map(|h| Target::Sidebar(h.clone()));
 4556
 4557        let target = match (origin, direction) {
 4558            // From the sidebar, only Right navigates into the workspace.
 4559            (Origin::Sidebar, SplitDirection::Right) => try_dock(&self.left_dock)
 4560                .or_else(|| get_last_active_pane().map(Target::Pane))
 4561                .or_else(|| try_dock(&self.bottom_dock))
 4562                .or_else(|| try_dock(&self.right_dock)),
 4563
 4564            (Origin::Sidebar, _) => None,
 4565
 4566            // We're in the center, so we first try to go to a different pane,
 4567            // otherwise try to go to a dock.
 4568            (Origin::Center, direction) => {
 4569                if let Some(pane) = self.find_pane_in_direction(direction, cx) {
 4570                    Some(Target::Pane(pane))
 4571                } else {
 4572                    match direction {
 4573                        SplitDirection::Up => None,
 4574                        SplitDirection::Down => try_dock(&self.bottom_dock),
 4575                        SplitDirection::Left => try_dock(&self.left_dock).or(sidebar_target),
 4576                        SplitDirection::Right => try_dock(&self.right_dock),
 4577                    }
 4578                }
 4579            }
 4580
 4581            (Origin::LeftDock, SplitDirection::Right) => {
 4582                if let Some(last_active_pane) = get_last_active_pane() {
 4583                    Some(Target::Pane(last_active_pane))
 4584                } else {
 4585                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
 4586                }
 4587            }
 4588
 4589            (Origin::LeftDock, SplitDirection::Left) => sidebar_target,
 4590
 4591            (Origin::LeftDock, SplitDirection::Down)
 4592            | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
 4593
 4594            (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
 4595            (Origin::BottomDock, SplitDirection::Left) => {
 4596                try_dock(&self.left_dock).or(sidebar_target)
 4597            }
 4598            (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
 4599
 4600            (Origin::RightDock, SplitDirection::Left) => {
 4601                if let Some(last_active_pane) = get_last_active_pane() {
 4602                    Some(Target::Pane(last_active_pane))
 4603                } else {
 4604                    try_dock(&self.bottom_dock)
 4605                        .or_else(|| try_dock(&self.left_dock))
 4606                        .or(sidebar_target)
 4607                }
 4608            }
 4609
 4610            _ => None,
 4611        };
 4612
 4613        match target {
 4614            Some(ActivateInDirectionTarget::Pane(pane)) => {
 4615                let pane = pane.read(cx);
 4616                if let Some(item) = pane.active_item() {
 4617                    item.item_focus_handle(cx).focus(window, cx);
 4618                } else {
 4619                    log::error!(
 4620                        "Could not find a focus target when in switching focus in {direction} direction for a pane",
 4621                    );
 4622                }
 4623            }
 4624            Some(ActivateInDirectionTarget::Dock(dock)) => {
 4625                // Defer this to avoid a panic when the dock's active panel is already on the stack.
 4626                window.defer(cx, move |window, cx| {
 4627                    let dock = dock.read(cx);
 4628                    if let Some(panel) = dock.active_panel() {
 4629                        panel.panel_focus_handle(cx).focus(window, cx);
 4630                    } else {
 4631                        log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
 4632                    }
 4633                })
 4634            }
 4635            Some(ActivateInDirectionTarget::Sidebar(focus_handle)) => {
 4636                focus_handle.focus(window, cx);
 4637            }
 4638            None => {}
 4639        }
 4640    }
 4641
 4642    pub fn move_item_to_pane_in_direction(
 4643        &mut self,
 4644        action: &MoveItemToPaneInDirection,
 4645        window: &mut Window,
 4646        cx: &mut Context<Self>,
 4647    ) {
 4648        let destination = match self.find_pane_in_direction(action.direction, cx) {
 4649            Some(destination) => destination,
 4650            None => {
 4651                if !action.clone && self.active_pane.read(cx).items_len() < 2 {
 4652                    return;
 4653                }
 4654                let new_pane = self.add_pane(window, cx);
 4655                self.center
 4656                    .split(&self.active_pane, &new_pane, action.direction, cx);
 4657                new_pane
 4658            }
 4659        };
 4660
 4661        if action.clone {
 4662            if self
 4663                .active_pane
 4664                .read(cx)
 4665                .active_item()
 4666                .is_some_and(|item| item.can_split(cx))
 4667            {
 4668                clone_active_item(
 4669                    self.database_id(),
 4670                    &self.active_pane,
 4671                    &destination,
 4672                    action.focus,
 4673                    window,
 4674                    cx,
 4675                );
 4676                return;
 4677            }
 4678        }
 4679        move_active_item(
 4680            &self.active_pane,
 4681            &destination,
 4682            action.focus,
 4683            true,
 4684            window,
 4685            cx,
 4686        );
 4687    }
 4688
 4689    pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
 4690        self.center.bounding_box_for_pane(pane)
 4691    }
 4692
 4693    pub fn find_pane_in_direction(
 4694        &mut self,
 4695        direction: SplitDirection,
 4696        cx: &App,
 4697    ) -> Option<Entity<Pane>> {
 4698        self.center
 4699            .find_pane_in_direction(&self.active_pane, direction, cx)
 4700            .cloned()
 4701    }
 4702
 4703    pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
 4704        if let Some(to) = self.find_pane_in_direction(direction, cx) {
 4705            self.center.swap(&self.active_pane, &to, cx);
 4706            cx.notify();
 4707        }
 4708    }
 4709
 4710    pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
 4711        if self
 4712            .center
 4713            .move_to_border(&self.active_pane, direction, cx)
 4714            .unwrap()
 4715        {
 4716            cx.notify();
 4717        }
 4718    }
 4719
 4720    pub fn resize_pane(
 4721        &mut self,
 4722        axis: gpui::Axis,
 4723        amount: Pixels,
 4724        window: &mut Window,
 4725        cx: &mut Context<Self>,
 4726    ) {
 4727        let docks = self.all_docks();
 4728        let active_dock = docks
 4729            .into_iter()
 4730            .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
 4731
 4732        if let Some(dock) = active_dock {
 4733            let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else {
 4734                return;
 4735            };
 4736            match dock.read(cx).position() {
 4737                DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx),
 4738                DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx),
 4739                DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx),
 4740            }
 4741        } else {
 4742            self.center
 4743                .resize(&self.active_pane, axis, amount, &self.bounds, cx);
 4744        }
 4745        cx.notify();
 4746    }
 4747
 4748    pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
 4749        self.center.reset_pane_sizes(cx);
 4750        cx.notify();
 4751    }
 4752
 4753    fn handle_pane_focused(
 4754        &mut self,
 4755        pane: Entity<Pane>,
 4756        window: &mut Window,
 4757        cx: &mut Context<Self>,
 4758    ) {
 4759        // This is explicitly hoisted out of the following check for pane identity as
 4760        // terminal panel panes are not registered as a center panes.
 4761        self.status_bar.update(cx, |status_bar, cx| {
 4762            status_bar.set_active_pane(&pane, window, cx);
 4763        });
 4764        if self.active_pane != pane {
 4765            self.set_active_pane(&pane, window, cx);
 4766        }
 4767
 4768        if self.last_active_center_pane.is_none() {
 4769            self.last_active_center_pane = Some(pane.downgrade());
 4770        }
 4771
 4772        // If this pane is in a dock, preserve that dock when dismissing zoomed items.
 4773        // This prevents the dock from closing when focus events fire during window activation.
 4774        // We also preserve any dock whose active panel itself has focus — this covers
 4775        // panels like AgentPanel that don't implement `pane()` but can still be zoomed.
 4776        let dock_to_preserve = self.all_docks().iter().find_map(|dock| {
 4777            let dock_read = dock.read(cx);
 4778            if let Some(panel) = dock_read.active_panel() {
 4779                if panel.pane(cx).is_some_and(|dock_pane| dock_pane == pane)
 4780                    || panel.panel_focus_handle(cx).contains_focused(window, cx)
 4781                {
 4782                    return Some(dock_read.position());
 4783                }
 4784            }
 4785            None
 4786        });
 4787
 4788        self.dismiss_zoomed_items_to_reveal(dock_to_preserve, window, cx);
 4789        if pane.read(cx).is_zoomed() {
 4790            self.zoomed = Some(pane.downgrade().into());
 4791        } else {
 4792            self.zoomed = None;
 4793        }
 4794        self.zoomed_position = None;
 4795        cx.emit(Event::ZoomChanged);
 4796        self.update_active_view_for_followers(window, cx);
 4797        pane.update(cx, |pane, _| {
 4798            pane.track_alternate_file_items();
 4799        });
 4800
 4801        cx.notify();
 4802    }
 4803
 4804    fn set_active_pane(
 4805        &mut self,
 4806        pane: &Entity<Pane>,
 4807        window: &mut Window,
 4808        cx: &mut Context<Self>,
 4809    ) {
 4810        self.active_pane = pane.clone();
 4811        self.active_item_path_changed(true, window, cx);
 4812        self.last_active_center_pane = Some(pane.downgrade());
 4813    }
 4814
 4815    fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 4816        self.update_active_view_for_followers(window, cx);
 4817    }
 4818
 4819    fn handle_pane_event(
 4820        &mut self,
 4821        pane: &Entity<Pane>,
 4822        event: &pane::Event,
 4823        window: &mut Window,
 4824        cx: &mut Context<Self>,
 4825    ) {
 4826        let mut serialize_workspace = true;
 4827        match event {
 4828            pane::Event::AddItem { item } => {
 4829                item.added_to_pane(self, pane.clone(), window, cx);
 4830                cx.emit(Event::ItemAdded {
 4831                    item: item.boxed_clone(),
 4832                });
 4833            }
 4834            pane::Event::Split { direction, mode } => {
 4835                match mode {
 4836                    SplitMode::ClonePane => {
 4837                        self.split_and_clone(pane.clone(), *direction, window, cx)
 4838                            .detach();
 4839                    }
 4840                    SplitMode::EmptyPane => {
 4841                        self.split_pane(pane.clone(), *direction, window, cx);
 4842                    }
 4843                    SplitMode::MovePane => {
 4844                        self.split_and_move(pane.clone(), *direction, window, cx);
 4845                    }
 4846                };
 4847            }
 4848            pane::Event::JoinIntoNext => {
 4849                self.join_pane_into_next(pane.clone(), window, cx);
 4850            }
 4851            pane::Event::JoinAll => {
 4852                self.join_all_panes(window, cx);
 4853            }
 4854            pane::Event::Remove { focus_on_pane } => {
 4855                self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
 4856            }
 4857            pane::Event::ActivateItem {
 4858                local,
 4859                focus_changed,
 4860            } => {
 4861                window.invalidate_character_coordinates();
 4862
 4863                pane.update(cx, |pane, _| {
 4864                    pane.track_alternate_file_items();
 4865                });
 4866                if *local {
 4867                    self.unfollow_in_pane(pane, window, cx);
 4868                }
 4869                serialize_workspace = *focus_changed || pane != self.active_pane();
 4870                if pane == self.active_pane() {
 4871                    self.active_item_path_changed(*focus_changed, window, cx);
 4872                    self.update_active_view_for_followers(window, cx);
 4873                } else if *local {
 4874                    self.set_active_pane(pane, window, cx);
 4875                }
 4876            }
 4877            pane::Event::UserSavedItem { item, save_intent } => {
 4878                cx.emit(Event::UserSavedItem {
 4879                    pane: pane.downgrade(),
 4880                    item: item.boxed_clone(),
 4881                    save_intent: *save_intent,
 4882                });
 4883                serialize_workspace = false;
 4884            }
 4885            pane::Event::ChangeItemTitle => {
 4886                if *pane == self.active_pane {
 4887                    self.active_item_path_changed(false, window, cx);
 4888                }
 4889                serialize_workspace = false;
 4890            }
 4891            pane::Event::RemovedItem { item } => {
 4892                cx.emit(Event::ActiveItemChanged);
 4893                self.update_window_edited(window, cx);
 4894                if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id())
 4895                    && entry.get().entity_id() == pane.entity_id()
 4896                {
 4897                    entry.remove();
 4898                }
 4899                cx.emit(Event::ItemRemoved {
 4900                    item_id: item.item_id(),
 4901                });
 4902            }
 4903            pane::Event::Focus => {
 4904                window.invalidate_character_coordinates();
 4905                self.handle_pane_focused(pane.clone(), window, cx);
 4906            }
 4907            pane::Event::ZoomIn => {
 4908                if *pane == self.active_pane {
 4909                    pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
 4910                    if pane.read(cx).has_focus(window, cx) {
 4911                        self.zoomed = Some(pane.downgrade().into());
 4912                        self.zoomed_position = None;
 4913                        cx.emit(Event::ZoomChanged);
 4914                    }
 4915                    cx.notify();
 4916                }
 4917            }
 4918            pane::Event::ZoomOut => {
 4919                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
 4920                if self.zoomed_position.is_none() {
 4921                    self.zoomed = None;
 4922                    cx.emit(Event::ZoomChanged);
 4923                }
 4924                cx.notify();
 4925            }
 4926            pane::Event::ItemPinned | pane::Event::ItemUnpinned => {}
 4927        }
 4928
 4929        if serialize_workspace {
 4930            self.serialize_workspace(window, cx);
 4931        }
 4932    }
 4933
 4934    pub fn unfollow_in_pane(
 4935        &mut self,
 4936        pane: &Entity<Pane>,
 4937        window: &mut Window,
 4938        cx: &mut Context<Workspace>,
 4939    ) -> Option<CollaboratorId> {
 4940        let leader_id = self.leader_for_pane(pane)?;
 4941        self.unfollow(leader_id, window, cx);
 4942        Some(leader_id)
 4943    }
 4944
 4945    pub fn split_pane(
 4946        &mut self,
 4947        pane_to_split: Entity<Pane>,
 4948        split_direction: SplitDirection,
 4949        window: &mut Window,
 4950        cx: &mut Context<Self>,
 4951    ) -> Entity<Pane> {
 4952        let new_pane = self.add_pane(window, cx);
 4953        self.center
 4954            .split(&pane_to_split, &new_pane, split_direction, cx);
 4955        cx.notify();
 4956        new_pane
 4957    }
 4958
 4959    pub fn split_and_move(
 4960        &mut self,
 4961        pane: Entity<Pane>,
 4962        direction: SplitDirection,
 4963        window: &mut Window,
 4964        cx: &mut Context<Self>,
 4965    ) {
 4966        let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else {
 4967            return;
 4968        };
 4969        let new_pane = self.add_pane(window, cx);
 4970        new_pane.update(cx, |pane, cx| {
 4971            pane.add_item(item, true, true, None, window, cx)
 4972        });
 4973        self.center.split(&pane, &new_pane, direction, cx);
 4974        cx.notify();
 4975    }
 4976
 4977    pub fn split_and_clone(
 4978        &mut self,
 4979        pane: Entity<Pane>,
 4980        direction: SplitDirection,
 4981        window: &mut Window,
 4982        cx: &mut Context<Self>,
 4983    ) -> Task<Option<Entity<Pane>>> {
 4984        let Some(item) = pane.read(cx).active_item() else {
 4985            return Task::ready(None);
 4986        };
 4987        if !item.can_split(cx) {
 4988            return Task::ready(None);
 4989        }
 4990        let task = item.clone_on_split(self.database_id(), window, cx);
 4991        cx.spawn_in(window, async move |this, cx| {
 4992            if let Some(clone) = task.await {
 4993                this.update_in(cx, |this, window, cx| {
 4994                    let new_pane = this.add_pane(window, cx);
 4995                    let nav_history = pane.read(cx).fork_nav_history();
 4996                    new_pane.update(cx, |pane, cx| {
 4997                        pane.set_nav_history(nav_history, cx);
 4998                        pane.add_item(clone, true, true, None, window, cx)
 4999                    });
 5000                    this.center.split(&pane, &new_pane, direction, cx);
 5001                    cx.notify();
 5002                    new_pane
 5003                })
 5004                .ok()
 5005            } else {
 5006                None
 5007            }
 5008        })
 5009    }
 5010
 5011    pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 5012        let active_item = self.active_pane.read(cx).active_item();
 5013        for pane in &self.panes {
 5014            join_pane_into_active(&self.active_pane, pane, window, cx);
 5015        }
 5016        if let Some(active_item) = active_item {
 5017            self.activate_item(active_item.as_ref(), true, true, window, cx);
 5018        }
 5019        cx.notify();
 5020    }
 5021
 5022    pub fn join_pane_into_next(
 5023        &mut self,
 5024        pane: Entity<Pane>,
 5025        window: &mut Window,
 5026        cx: &mut Context<Self>,
 5027    ) {
 5028        let next_pane = self
 5029            .find_pane_in_direction(SplitDirection::Right, cx)
 5030            .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
 5031            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
 5032            .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
 5033        let Some(next_pane) = next_pane else {
 5034            return;
 5035        };
 5036        move_all_items(&pane, &next_pane, window, cx);
 5037        cx.notify();
 5038    }
 5039
 5040    fn remove_pane(
 5041        &mut self,
 5042        pane: Entity<Pane>,
 5043        focus_on: Option<Entity<Pane>>,
 5044        window: &mut Window,
 5045        cx: &mut Context<Self>,
 5046    ) {
 5047        if self.center.remove(&pane, cx).unwrap() {
 5048            self.force_remove_pane(&pane, &focus_on, window, cx);
 5049            self.unfollow_in_pane(&pane, window, cx);
 5050            self.last_leaders_by_pane.remove(&pane.downgrade());
 5051            for removed_item in pane.read(cx).items() {
 5052                self.panes_by_item.remove(&removed_item.item_id());
 5053            }
 5054
 5055            cx.notify();
 5056        } else {
 5057            self.active_item_path_changed(true, window, cx);
 5058        }
 5059        cx.emit(Event::PaneRemoved);
 5060    }
 5061
 5062    pub fn panes_mut(&mut self) -> &mut [Entity<Pane>] {
 5063        &mut self.panes
 5064    }
 5065
 5066    pub fn panes(&self) -> &[Entity<Pane>] {
 5067        &self.panes
 5068    }
 5069
 5070    pub fn active_pane(&self) -> &Entity<Pane> {
 5071        &self.active_pane
 5072    }
 5073
 5074    pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
 5075        for dock in self.all_docks() {
 5076            if dock.focus_handle(cx).contains_focused(window, cx)
 5077                && let Some(pane) = dock
 5078                    .read(cx)
 5079                    .active_panel()
 5080                    .and_then(|panel| panel.pane(cx))
 5081            {
 5082                return pane;
 5083            }
 5084        }
 5085        self.active_pane().clone()
 5086    }
 5087
 5088    pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
 5089        self.find_pane_in_direction(SplitDirection::Right, cx)
 5090            .unwrap_or_else(|| {
 5091                self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
 5092            })
 5093    }
 5094
 5095    pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
 5096        self.pane_for_item_id(handle.item_id())
 5097    }
 5098
 5099    pub fn pane_for_item_id(&self, item_id: EntityId) -> Option<Entity<Pane>> {
 5100        let weak_pane = self.panes_by_item.get(&item_id)?;
 5101        weak_pane.upgrade()
 5102    }
 5103
 5104    pub fn pane_for_entity_id(&self, entity_id: EntityId) -> Option<Entity<Pane>> {
 5105        self.panes
 5106            .iter()
 5107            .find(|pane| pane.entity_id() == entity_id)
 5108            .cloned()
 5109    }
 5110
 5111    fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
 5112        self.follower_states.retain(|leader_id, state| {
 5113            if *leader_id == CollaboratorId::PeerId(peer_id) {
 5114                for item in state.items_by_leader_view_id.values() {
 5115                    item.view.set_leader_id(None, window, cx);
 5116                }
 5117                false
 5118            } else {
 5119                true
 5120            }
 5121        });
 5122        cx.notify();
 5123    }
 5124
 5125    pub fn start_following(
 5126        &mut self,
 5127        leader_id: impl Into<CollaboratorId>,
 5128        window: &mut Window,
 5129        cx: &mut Context<Self>,
 5130    ) -> Option<Task<Result<()>>> {
 5131        let leader_id = leader_id.into();
 5132        let pane = self.active_pane().clone();
 5133
 5134        self.last_leaders_by_pane
 5135            .insert(pane.downgrade(), leader_id);
 5136        self.unfollow(leader_id, window, cx);
 5137        self.unfollow_in_pane(&pane, window, cx);
 5138        self.follower_states.insert(
 5139            leader_id,
 5140            FollowerState {
 5141                center_pane: pane.clone(),
 5142                dock_pane: None,
 5143                active_view_id: None,
 5144                items_by_leader_view_id: Default::default(),
 5145            },
 5146        );
 5147        cx.notify();
 5148
 5149        match leader_id {
 5150            CollaboratorId::PeerId(leader_peer_id) => {
 5151                let room_id = self.active_call()?.room_id(cx)?;
 5152                let project_id = self.project.read(cx).remote_id();
 5153                let request = self.app_state.client.request(proto::Follow {
 5154                    room_id,
 5155                    project_id,
 5156                    leader_id: Some(leader_peer_id),
 5157                });
 5158
 5159                Some(cx.spawn_in(window, async move |this, cx| {
 5160                    let response = request.await?;
 5161                    this.update(cx, |this, _| {
 5162                        let state = this
 5163                            .follower_states
 5164                            .get_mut(&leader_id)
 5165                            .context("following interrupted")?;
 5166                        state.active_view_id = response
 5167                            .active_view
 5168                            .as_ref()
 5169                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
 5170                        anyhow::Ok(())
 5171                    })??;
 5172                    if let Some(view) = response.active_view {
 5173                        Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?;
 5174                    }
 5175                    this.update_in(cx, |this, window, cx| {
 5176                        this.leader_updated(leader_id, window, cx)
 5177                    })?;
 5178                    Ok(())
 5179                }))
 5180            }
 5181            CollaboratorId::Agent => {
 5182                self.leader_updated(leader_id, window, cx)?;
 5183                Some(Task::ready(Ok(())))
 5184            }
 5185        }
 5186    }
 5187
 5188    pub fn follow_next_collaborator(
 5189        &mut self,
 5190        _: &FollowNextCollaborator,
 5191        window: &mut Window,
 5192        cx: &mut Context<Self>,
 5193    ) {
 5194        let collaborators = self.project.read(cx).collaborators();
 5195        let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
 5196            let mut collaborators = collaborators.keys().copied();
 5197            for peer_id in collaborators.by_ref() {
 5198                if CollaboratorId::PeerId(peer_id) == leader_id {
 5199                    break;
 5200                }
 5201            }
 5202            collaborators.next().map(CollaboratorId::PeerId)
 5203        } else if let Some(last_leader_id) =
 5204            self.last_leaders_by_pane.get(&self.active_pane.downgrade())
 5205        {
 5206            match last_leader_id {
 5207                CollaboratorId::PeerId(peer_id) => {
 5208                    if collaborators.contains_key(peer_id) {
 5209                        Some(*last_leader_id)
 5210                    } else {
 5211                        None
 5212                    }
 5213                }
 5214                CollaboratorId::Agent => Some(CollaboratorId::Agent),
 5215            }
 5216        } else {
 5217            None
 5218        };
 5219
 5220        let pane = self.active_pane.clone();
 5221        let Some(leader_id) = next_leader_id.or_else(|| {
 5222            Some(CollaboratorId::PeerId(
 5223                collaborators.keys().copied().next()?,
 5224            ))
 5225        }) else {
 5226            return;
 5227        };
 5228        if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
 5229            return;
 5230        }
 5231        if let Some(task) = self.start_following(leader_id, window, cx) {
 5232            task.detach_and_log_err(cx)
 5233        }
 5234    }
 5235
 5236    pub fn follow(
 5237        &mut self,
 5238        leader_id: impl Into<CollaboratorId>,
 5239        window: &mut Window,
 5240        cx: &mut Context<Self>,
 5241    ) {
 5242        let leader_id = leader_id.into();
 5243
 5244        if let CollaboratorId::PeerId(peer_id) = leader_id {
 5245            let Some(active_call) = GlobalAnyActiveCall::try_global(cx) else {
 5246                return;
 5247            };
 5248            let Some(remote_participant) =
 5249                active_call.0.remote_participant_for_peer_id(peer_id, cx)
 5250            else {
 5251                return;
 5252            };
 5253
 5254            let project = self.project.read(cx);
 5255
 5256            let other_project_id = match remote_participant.location {
 5257                ParticipantLocation::External => None,
 5258                ParticipantLocation::UnsharedProject => None,
 5259                ParticipantLocation::SharedProject { project_id } => {
 5260                    if Some(project_id) == project.remote_id() {
 5261                        None
 5262                    } else {
 5263                        Some(project_id)
 5264                    }
 5265                }
 5266            };
 5267
 5268            // if they are active in another project, follow there.
 5269            if let Some(project_id) = other_project_id {
 5270                let app_state = self.app_state.clone();
 5271                crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
 5272                    .detach_and_log_err(cx);
 5273            }
 5274        }
 5275
 5276        // if you're already following, find the right pane and focus it.
 5277        if let Some(follower_state) = self.follower_states.get(&leader_id) {
 5278            window.focus(&follower_state.pane().focus_handle(cx), cx);
 5279
 5280            return;
 5281        }
 5282
 5283        // Otherwise, follow.
 5284        if let Some(task) = self.start_following(leader_id, window, cx) {
 5285            task.detach_and_log_err(cx)
 5286        }
 5287    }
 5288
 5289    pub fn unfollow(
 5290        &mut self,
 5291        leader_id: impl Into<CollaboratorId>,
 5292        window: &mut Window,
 5293        cx: &mut Context<Self>,
 5294    ) -> Option<()> {
 5295        cx.notify();
 5296
 5297        let leader_id = leader_id.into();
 5298        let state = self.follower_states.remove(&leader_id)?;
 5299        for (_, item) in state.items_by_leader_view_id {
 5300            item.view.set_leader_id(None, window, cx);
 5301        }
 5302
 5303        if let CollaboratorId::PeerId(leader_peer_id) = leader_id {
 5304            let project_id = self.project.read(cx).remote_id();
 5305            let room_id = self.active_call()?.room_id(cx)?;
 5306            self.app_state
 5307                .client
 5308                .send(proto::Unfollow {
 5309                    room_id,
 5310                    project_id,
 5311                    leader_id: Some(leader_peer_id),
 5312                })
 5313                .log_err();
 5314        }
 5315
 5316        Some(())
 5317    }
 5318
 5319    pub fn is_being_followed(&self, id: impl Into<CollaboratorId>) -> bool {
 5320        self.follower_states.contains_key(&id.into())
 5321    }
 5322
 5323    fn active_item_path_changed(
 5324        &mut self,
 5325        focus_changed: bool,
 5326        window: &mut Window,
 5327        cx: &mut Context<Self>,
 5328    ) {
 5329        cx.emit(Event::ActiveItemChanged);
 5330        let active_entry = self.active_project_path(cx);
 5331        self.project.update(cx, |project, cx| {
 5332            project.set_active_path(active_entry.clone(), cx)
 5333        });
 5334
 5335        if focus_changed && let Some(project_path) = &active_entry {
 5336            let git_store_entity = self.project.read(cx).git_store().clone();
 5337            git_store_entity.update(cx, |git_store, cx| {
 5338                git_store.set_active_repo_for_path(project_path, cx);
 5339            });
 5340        }
 5341
 5342        self.update_window_title(window, cx);
 5343    }
 5344
 5345    fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
 5346        let project = self.project().read(cx);
 5347        let mut title = String::new();
 5348
 5349        for (i, worktree) in project.visible_worktrees(cx).enumerate() {
 5350            let name = {
 5351                let settings_location = SettingsLocation {
 5352                    worktree_id: worktree.read(cx).id(),
 5353                    path: RelPath::empty(),
 5354                };
 5355
 5356                let settings = WorktreeSettings::get(Some(settings_location), cx);
 5357                match &settings.project_name {
 5358                    Some(name) => name.as_str(),
 5359                    None => worktree.read(cx).root_name_str(),
 5360                }
 5361            };
 5362            if i > 0 {
 5363                title.push_str(", ");
 5364            }
 5365            title.push_str(name);
 5366        }
 5367
 5368        if title.is_empty() {
 5369            title = "empty project".to_string();
 5370        }
 5371
 5372        if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
 5373            let filename = path.path.file_name().or_else(|| {
 5374                Some(
 5375                    project
 5376                        .worktree_for_id(path.worktree_id, cx)?
 5377                        .read(cx)
 5378                        .root_name_str(),
 5379                )
 5380            });
 5381
 5382            if let Some(filename) = filename {
 5383                title.push_str("");
 5384                title.push_str(filename.as_ref());
 5385            }
 5386        }
 5387
 5388        if project.is_via_collab() {
 5389            title.push_str("");
 5390        } else if project.is_shared() {
 5391            title.push_str("");
 5392        }
 5393
 5394        if let Some(last_title) = self.last_window_title.as_ref()
 5395            && &title == last_title
 5396        {
 5397            return;
 5398        }
 5399        window.set_window_title(&title);
 5400        SystemWindowTabController::update_tab_title(
 5401            cx,
 5402            window.window_handle().window_id(),
 5403            SharedString::from(&title),
 5404        );
 5405        self.last_window_title = Some(title);
 5406    }
 5407
 5408    fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
 5409        let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
 5410        if is_edited != self.window_edited {
 5411            self.window_edited = is_edited;
 5412            window.set_window_edited(self.window_edited)
 5413        }
 5414    }
 5415
 5416    fn update_item_dirty_state(
 5417        &mut self,
 5418        item: &dyn ItemHandle,
 5419        window: &mut Window,
 5420        cx: &mut App,
 5421    ) {
 5422        let is_dirty = item.is_dirty(cx);
 5423        let item_id = item.item_id();
 5424        let was_dirty = self.dirty_items.contains_key(&item_id);
 5425        if is_dirty == was_dirty {
 5426            return;
 5427        }
 5428        if was_dirty {
 5429            self.dirty_items.remove(&item_id);
 5430            self.update_window_edited(window, cx);
 5431            return;
 5432        }
 5433
 5434        let workspace = self.weak_handle();
 5435        let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
 5436            return;
 5437        };
 5438        let on_release_callback = Box::new(move |cx: &mut App| {
 5439            window_handle
 5440                .update(cx, |_, window, cx| {
 5441                    workspace
 5442                        .update(cx, |workspace, cx| {
 5443                            workspace.dirty_items.remove(&item_id);
 5444                            workspace.update_window_edited(window, cx)
 5445                        })
 5446                        .ok();
 5447                })
 5448                .ok();
 5449        });
 5450
 5451        let s = item.on_release(cx, on_release_callback);
 5452        self.dirty_items.insert(item_id, s);
 5453        self.update_window_edited(window, cx);
 5454    }
 5455
 5456    fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
 5457        if self.notifications.is_empty() {
 5458            None
 5459        } else {
 5460            Some(
 5461                div()
 5462                    .absolute()
 5463                    .right_3()
 5464                    .bottom_3()
 5465                    .w_112()
 5466                    .h_full()
 5467                    .flex()
 5468                    .flex_col()
 5469                    .justify_end()
 5470                    .gap_2()
 5471                    .children(
 5472                        self.notifications
 5473                            .iter()
 5474                            .map(|(_, notification)| notification.clone().into_any()),
 5475                    ),
 5476            )
 5477        }
 5478    }
 5479
 5480    // RPC handlers
 5481
 5482    fn active_view_for_follower(
 5483        &self,
 5484        follower_project_id: Option<u64>,
 5485        window: &mut Window,
 5486        cx: &mut Context<Self>,
 5487    ) -> Option<proto::View> {
 5488        let (item, panel_id) = self.active_item_for_followers(window, cx);
 5489        let item = item?;
 5490        let leader_id = self
 5491            .pane_for(&*item)
 5492            .and_then(|pane| self.leader_for_pane(&pane));
 5493        let leader_peer_id = match leader_id {
 5494            Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
 5495            Some(CollaboratorId::Agent) | None => None,
 5496        };
 5497
 5498        let item_handle = item.to_followable_item_handle(cx)?;
 5499        let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
 5500        let variant = item_handle.to_state_proto(window, cx)?;
 5501
 5502        if item_handle.is_project_item(window, cx)
 5503            && (follower_project_id.is_none()
 5504                || follower_project_id != self.project.read(cx).remote_id())
 5505        {
 5506            return None;
 5507        }
 5508
 5509        Some(proto::View {
 5510            id: id.to_proto(),
 5511            leader_id: leader_peer_id,
 5512            variant: Some(variant),
 5513            panel_id: panel_id.map(|id| id as i32),
 5514        })
 5515    }
 5516
 5517    fn handle_follow(
 5518        &mut self,
 5519        follower_project_id: Option<u64>,
 5520        window: &mut Window,
 5521        cx: &mut Context<Self>,
 5522    ) -> proto::FollowResponse {
 5523        let active_view = self.active_view_for_follower(follower_project_id, window, cx);
 5524
 5525        cx.notify();
 5526        proto::FollowResponse {
 5527            views: active_view.iter().cloned().collect(),
 5528            active_view,
 5529        }
 5530    }
 5531
 5532    fn handle_update_followers(
 5533        &mut self,
 5534        leader_id: PeerId,
 5535        message: proto::UpdateFollowers,
 5536        _window: &mut Window,
 5537        _cx: &mut Context<Self>,
 5538    ) {
 5539        self.leader_updates_tx
 5540            .unbounded_send((leader_id, message))
 5541            .ok();
 5542    }
 5543
 5544    async fn process_leader_update(
 5545        this: &WeakEntity<Self>,
 5546        leader_id: PeerId,
 5547        update: proto::UpdateFollowers,
 5548        cx: &mut AsyncWindowContext,
 5549    ) -> Result<()> {
 5550        match update.variant.context("invalid update")? {
 5551            proto::update_followers::Variant::CreateView(view) => {
 5552                let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
 5553                let should_add_view = this.update(cx, |this, _| {
 5554                    if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
 5555                        anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
 5556                    } else {
 5557                        anyhow::Ok(false)
 5558                    }
 5559                })??;
 5560
 5561                if should_add_view {
 5562                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
 5563                }
 5564            }
 5565            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
 5566                let should_add_view = this.update(cx, |this, _| {
 5567                    if let Some(state) = this.follower_states.get_mut(&leader_id.into()) {
 5568                        state.active_view_id = update_active_view
 5569                            .view
 5570                            .as_ref()
 5571                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
 5572
 5573                        if state.active_view_id.is_some_and(|view_id| {
 5574                            !state.items_by_leader_view_id.contains_key(&view_id)
 5575                        }) {
 5576                            anyhow::Ok(true)
 5577                        } else {
 5578                            anyhow::Ok(false)
 5579                        }
 5580                    } else {
 5581                        anyhow::Ok(false)
 5582                    }
 5583                })??;
 5584
 5585                if should_add_view && let Some(view) = update_active_view.view {
 5586                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
 5587                }
 5588            }
 5589            proto::update_followers::Variant::UpdateView(update_view) => {
 5590                let variant = update_view.variant.context("missing update view variant")?;
 5591                let id = update_view.id.context("missing update view id")?;
 5592                let mut tasks = Vec::new();
 5593                this.update_in(cx, |this, window, cx| {
 5594                    let project = this.project.clone();
 5595                    if let Some(state) = this.follower_states.get(&leader_id.into()) {
 5596                        let view_id = ViewId::from_proto(id.clone())?;
 5597                        if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
 5598                            tasks.push(item.view.apply_update_proto(
 5599                                &project,
 5600                                variant.clone(),
 5601                                window,
 5602                                cx,
 5603                            ));
 5604                        }
 5605                    }
 5606                    anyhow::Ok(())
 5607                })??;
 5608                try_join_all(tasks).await.log_err();
 5609            }
 5610        }
 5611        this.update_in(cx, |this, window, cx| {
 5612            this.leader_updated(leader_id, window, cx)
 5613        })?;
 5614        Ok(())
 5615    }
 5616
 5617    async fn add_view_from_leader(
 5618        this: WeakEntity<Self>,
 5619        leader_id: PeerId,
 5620        view: &proto::View,
 5621        cx: &mut AsyncWindowContext,
 5622    ) -> Result<()> {
 5623        let this = this.upgrade().context("workspace dropped")?;
 5624
 5625        let Some(id) = view.id.clone() else {
 5626            anyhow::bail!("no id for view");
 5627        };
 5628        let id = ViewId::from_proto(id)?;
 5629        let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
 5630
 5631        let pane = this.update(cx, |this, _cx| {
 5632            let state = this
 5633                .follower_states
 5634                .get(&leader_id.into())
 5635                .context("stopped following")?;
 5636            anyhow::Ok(state.pane().clone())
 5637        })?;
 5638        let existing_item = pane.update_in(cx, |pane, window, cx| {
 5639            let client = this.read(cx).client().clone();
 5640            pane.items().find_map(|item| {
 5641                let item = item.to_followable_item_handle(cx)?;
 5642                if item.remote_id(&client, window, cx) == Some(id) {
 5643                    Some(item)
 5644                } else {
 5645                    None
 5646                }
 5647            })
 5648        })?;
 5649        let item = if let Some(existing_item) = existing_item {
 5650            existing_item
 5651        } else {
 5652            let variant = view.variant.clone();
 5653            anyhow::ensure!(variant.is_some(), "missing view variant");
 5654
 5655            let task = cx.update(|window, cx| {
 5656                FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
 5657            })?;
 5658
 5659            let Some(task) = task else {
 5660                anyhow::bail!(
 5661                    "failed to construct view from leader (maybe from a different version of zed?)"
 5662                );
 5663            };
 5664
 5665            let mut new_item = task.await?;
 5666            pane.update_in(cx, |pane, window, cx| {
 5667                let mut item_to_remove = None;
 5668                for (ix, item) in pane.items().enumerate() {
 5669                    if let Some(item) = item.to_followable_item_handle(cx) {
 5670                        match new_item.dedup(item.as_ref(), window, cx) {
 5671                            Some(item::Dedup::KeepExisting) => {
 5672                                new_item =
 5673                                    item.boxed_clone().to_followable_item_handle(cx).unwrap();
 5674                                break;
 5675                            }
 5676                            Some(item::Dedup::ReplaceExisting) => {
 5677                                item_to_remove = Some((ix, item.item_id()));
 5678                                break;
 5679                            }
 5680                            None => {}
 5681                        }
 5682                    }
 5683                }
 5684
 5685                if let Some((ix, id)) = item_to_remove {
 5686                    pane.remove_item(id, false, false, window, cx);
 5687                    pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
 5688                }
 5689            })?;
 5690
 5691            new_item
 5692        };
 5693
 5694        this.update_in(cx, |this, window, cx| {
 5695            let state = this.follower_states.get_mut(&leader_id.into())?;
 5696            item.set_leader_id(Some(leader_id.into()), window, cx);
 5697            state.items_by_leader_view_id.insert(
 5698                id,
 5699                FollowerView {
 5700                    view: item,
 5701                    location: panel_id,
 5702                },
 5703            );
 5704
 5705            Some(())
 5706        })
 5707        .context("no follower state")?;
 5708
 5709        Ok(())
 5710    }
 5711
 5712    fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 5713        let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else {
 5714            return;
 5715        };
 5716
 5717        if let Some(agent_location) = self.project.read(cx).agent_location() {
 5718            let buffer_entity_id = agent_location.buffer.entity_id();
 5719            let view_id = ViewId {
 5720                creator: CollaboratorId::Agent,
 5721                id: buffer_entity_id.as_u64(),
 5722            };
 5723            follower_state.active_view_id = Some(view_id);
 5724
 5725            let item = match follower_state.items_by_leader_view_id.entry(view_id) {
 5726                hash_map::Entry::Occupied(entry) => Some(entry.into_mut()),
 5727                hash_map::Entry::Vacant(entry) => {
 5728                    let existing_view =
 5729                        follower_state
 5730                            .center_pane
 5731                            .read(cx)
 5732                            .items()
 5733                            .find_map(|item| {
 5734                                let item = item.to_followable_item_handle(cx)?;
 5735                                if item.buffer_kind(cx) == ItemBufferKind::Singleton
 5736                                    && item.project_item_model_ids(cx).as_slice()
 5737                                        == [buffer_entity_id]
 5738                                {
 5739                                    Some(item)
 5740                                } else {
 5741                                    None
 5742                                }
 5743                            });
 5744                    let view = existing_view.or_else(|| {
 5745                        agent_location.buffer.upgrade().and_then(|buffer| {
 5746                            cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| {
 5747                                registry.build_item(buffer, self.project.clone(), None, window, cx)
 5748                            })?
 5749                            .to_followable_item_handle(cx)
 5750                        })
 5751                    });
 5752
 5753                    view.map(|view| {
 5754                        entry.insert(FollowerView {
 5755                            view,
 5756                            location: None,
 5757                        })
 5758                    })
 5759                }
 5760            };
 5761
 5762            if let Some(item) = item {
 5763                item.view
 5764                    .set_leader_id(Some(CollaboratorId::Agent), window, cx);
 5765                item.view
 5766                    .update_agent_location(agent_location.position, window, cx);
 5767            }
 5768        } else {
 5769            follower_state.active_view_id = None;
 5770        }
 5771
 5772        self.leader_updated(CollaboratorId::Agent, window, cx);
 5773    }
 5774
 5775    pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
 5776        let mut is_project_item = true;
 5777        let mut update = proto::UpdateActiveView::default();
 5778        if window.is_window_active() {
 5779            let (active_item, panel_id) = self.active_item_for_followers(window, cx);
 5780
 5781            if let Some(item) = active_item
 5782                && item.item_focus_handle(cx).contains_focused(window, cx)
 5783            {
 5784                let leader_id = self
 5785                    .pane_for(&*item)
 5786                    .and_then(|pane| self.leader_for_pane(&pane));
 5787                let leader_peer_id = match leader_id {
 5788                    Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id),
 5789                    Some(CollaboratorId::Agent) | None => None,
 5790                };
 5791
 5792                if let Some(item) = item.to_followable_item_handle(cx) {
 5793                    let id = item
 5794                        .remote_id(&self.app_state.client, window, cx)
 5795                        .map(|id| id.to_proto());
 5796
 5797                    if let Some(id) = id
 5798                        && let Some(variant) = item.to_state_proto(window, cx)
 5799                    {
 5800                        let view = Some(proto::View {
 5801                            id,
 5802                            leader_id: leader_peer_id,
 5803                            variant: Some(variant),
 5804                            panel_id: panel_id.map(|id| id as i32),
 5805                        });
 5806
 5807                        is_project_item = item.is_project_item(window, cx);
 5808                        update = proto::UpdateActiveView { view };
 5809                    };
 5810                }
 5811            }
 5812        }
 5813
 5814        let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
 5815        if active_view_id != self.last_active_view_id.as_ref() {
 5816            self.last_active_view_id = active_view_id.cloned();
 5817            self.update_followers(
 5818                is_project_item,
 5819                proto::update_followers::Variant::UpdateActiveView(update),
 5820                window,
 5821                cx,
 5822            );
 5823        }
 5824    }
 5825
 5826    fn active_item_for_followers(
 5827        &self,
 5828        window: &mut Window,
 5829        cx: &mut App,
 5830    ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
 5831        let mut active_item = None;
 5832        let mut panel_id = None;
 5833        for dock in self.all_docks() {
 5834            if dock.focus_handle(cx).contains_focused(window, cx)
 5835                && let Some(panel) = dock.read(cx).active_panel()
 5836                && let Some(pane) = panel.pane(cx)
 5837                && let Some(item) = pane.read(cx).active_item()
 5838            {
 5839                active_item = Some(item);
 5840                panel_id = panel.remote_id();
 5841                break;
 5842            }
 5843        }
 5844
 5845        if active_item.is_none() {
 5846            active_item = self.active_pane().read(cx).active_item();
 5847        }
 5848        (active_item, panel_id)
 5849    }
 5850
 5851    fn update_followers(
 5852        &self,
 5853        project_only: bool,
 5854        update: proto::update_followers::Variant,
 5855        _: &mut Window,
 5856        cx: &mut App,
 5857    ) -> Option<()> {
 5858        // If this update only applies to for followers in the current project,
 5859        // then skip it unless this project is shared. If it applies to all
 5860        // followers, regardless of project, then set `project_id` to none,
 5861        // indicating that it goes to all followers.
 5862        let project_id = if project_only {
 5863            Some(self.project.read(cx).remote_id()?)
 5864        } else {
 5865            None
 5866        };
 5867        self.app_state().workspace_store.update(cx, |store, cx| {
 5868            store.update_followers(project_id, update, cx)
 5869        })
 5870    }
 5871
 5872    pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<CollaboratorId> {
 5873        self.follower_states.iter().find_map(|(leader_id, state)| {
 5874            if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
 5875                Some(*leader_id)
 5876            } else {
 5877                None
 5878            }
 5879        })
 5880    }
 5881
 5882    fn leader_updated(
 5883        &mut self,
 5884        leader_id: impl Into<CollaboratorId>,
 5885        window: &mut Window,
 5886        cx: &mut Context<Self>,
 5887    ) -> Option<Box<dyn ItemHandle>> {
 5888        cx.notify();
 5889
 5890        let leader_id = leader_id.into();
 5891        let (panel_id, item) = match leader_id {
 5892            CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?,
 5893            CollaboratorId::Agent => (None, self.active_item_for_agent()?),
 5894        };
 5895
 5896        let state = self.follower_states.get(&leader_id)?;
 5897        let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
 5898        let pane;
 5899        if let Some(panel_id) = panel_id {
 5900            pane = self
 5901                .activate_panel_for_proto_id(panel_id, window, cx)?
 5902                .pane(cx)?;
 5903            let state = self.follower_states.get_mut(&leader_id)?;
 5904            state.dock_pane = Some(pane.clone());
 5905        } else {
 5906            pane = state.center_pane.clone();
 5907            let state = self.follower_states.get_mut(&leader_id)?;
 5908            if let Some(dock_pane) = state.dock_pane.take() {
 5909                transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
 5910            }
 5911        }
 5912
 5913        pane.update(cx, |pane, cx| {
 5914            let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
 5915            if let Some(index) = pane.index_for_item(item.as_ref()) {
 5916                pane.activate_item(index, false, false, window, cx);
 5917            } else {
 5918                pane.add_item(item.boxed_clone(), false, false, None, window, cx)
 5919            }
 5920
 5921            if focus_active_item {
 5922                pane.focus_active_item(window, cx)
 5923            }
 5924        });
 5925
 5926        Some(item)
 5927    }
 5928
 5929    fn active_item_for_agent(&self) -> Option<Box<dyn ItemHandle>> {
 5930        let state = self.follower_states.get(&CollaboratorId::Agent)?;
 5931        let active_view_id = state.active_view_id?;
 5932        Some(
 5933            state
 5934                .items_by_leader_view_id
 5935                .get(&active_view_id)?
 5936                .view
 5937                .boxed_clone(),
 5938        )
 5939    }
 5940
 5941    fn active_item_for_peer(
 5942        &self,
 5943        peer_id: PeerId,
 5944        window: &mut Window,
 5945        cx: &mut Context<Self>,
 5946    ) -> Option<(Option<PanelId>, Box<dyn ItemHandle>)> {
 5947        let call = self.active_call()?;
 5948        let participant = call.remote_participant_for_peer_id(peer_id, cx)?;
 5949        let leader_in_this_app;
 5950        let leader_in_this_project;
 5951        match participant.location {
 5952            ParticipantLocation::SharedProject { project_id } => {
 5953                leader_in_this_app = true;
 5954                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
 5955            }
 5956            ParticipantLocation::UnsharedProject => {
 5957                leader_in_this_app = true;
 5958                leader_in_this_project = false;
 5959            }
 5960            ParticipantLocation::External => {
 5961                leader_in_this_app = false;
 5962                leader_in_this_project = false;
 5963            }
 5964        };
 5965        let state = self.follower_states.get(&peer_id.into())?;
 5966        let mut item_to_activate = None;
 5967        if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
 5968            if let Some(item) = state.items_by_leader_view_id.get(&active_view_id)
 5969                && (leader_in_this_project || !item.view.is_project_item(window, cx))
 5970            {
 5971                item_to_activate = Some((item.location, item.view.boxed_clone()));
 5972            }
 5973        } else if let Some(shared_screen) =
 5974            self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx)
 5975        {
 5976            item_to_activate = Some((None, Box::new(shared_screen)));
 5977        }
 5978        item_to_activate
 5979    }
 5980
 5981    fn shared_screen_for_peer(
 5982        &self,
 5983        peer_id: PeerId,
 5984        pane: &Entity<Pane>,
 5985        window: &mut Window,
 5986        cx: &mut App,
 5987    ) -> Option<Entity<SharedScreen>> {
 5988        self.active_call()?
 5989            .create_shared_screen(peer_id, pane, window, cx)
 5990    }
 5991
 5992    pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 5993        if window.is_window_active() {
 5994            self.update_active_view_for_followers(window, cx);
 5995
 5996            if let Some(database_id) = self.database_id {
 5997                let db = WorkspaceDb::global(cx);
 5998                cx.background_spawn(async move { db.update_timestamp(database_id).await })
 5999                    .detach();
 6000            }
 6001        } else {
 6002            for pane in &self.panes {
 6003                pane.update(cx, |pane, cx| {
 6004                    if let Some(item) = pane.active_item() {
 6005                        item.workspace_deactivated(window, cx);
 6006                    }
 6007                    for item in pane.items() {
 6008                        if matches!(
 6009                            item.workspace_settings(cx).autosave,
 6010                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
 6011                        ) {
 6012                            Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
 6013                                .detach_and_log_err(cx);
 6014                        }
 6015                    }
 6016                });
 6017            }
 6018        }
 6019    }
 6020
 6021    pub fn active_call(&self) -> Option<&dyn AnyActiveCall> {
 6022        self.active_call.as_ref().map(|(call, _)| &*call.0)
 6023    }
 6024
 6025    pub fn active_global_call(&self) -> Option<GlobalAnyActiveCall> {
 6026        self.active_call.as_ref().map(|(call, _)| call.clone())
 6027    }
 6028
 6029    fn on_active_call_event(
 6030        &mut self,
 6031        event: &ActiveCallEvent,
 6032        window: &mut Window,
 6033        cx: &mut Context<Self>,
 6034    ) {
 6035        match event {
 6036            ActiveCallEvent::ParticipantLocationChanged { participant_id }
 6037            | ActiveCallEvent::RemoteVideoTracksChanged { participant_id } => {
 6038                self.leader_updated(participant_id, window, cx);
 6039            }
 6040        }
 6041    }
 6042
 6043    pub fn database_id(&self) -> Option<WorkspaceId> {
 6044        self.database_id
 6045    }
 6046
 6047    #[cfg(any(test, feature = "test-support"))]
 6048    pub(crate) fn set_database_id(&mut self, id: WorkspaceId) {
 6049        self.database_id = Some(id);
 6050    }
 6051
 6052    pub fn session_id(&self) -> Option<String> {
 6053        self.session_id.clone()
 6054    }
 6055
 6056    fn save_window_bounds(&self, window: &mut Window, cx: &mut App) -> Task<()> {
 6057        let Some(display) = window.display(cx) else {
 6058            return Task::ready(());
 6059        };
 6060        let Ok(display_uuid) = display.uuid() else {
 6061            return Task::ready(());
 6062        };
 6063
 6064        let window_bounds = window.inner_window_bounds();
 6065        let database_id = self.database_id;
 6066        let has_paths = !self.root_paths(cx).is_empty();
 6067        let db = WorkspaceDb::global(cx);
 6068        let kvp = db::kvp::KeyValueStore::global(cx);
 6069
 6070        cx.background_executor().spawn(async move {
 6071            if !has_paths {
 6072                persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
 6073                    .await
 6074                    .log_err();
 6075            }
 6076            if let Some(database_id) = database_id {
 6077                db.set_window_open_status(
 6078                    database_id,
 6079                    SerializedWindowBounds(window_bounds),
 6080                    display_uuid,
 6081                )
 6082                .await
 6083                .log_err();
 6084            } else {
 6085                persistence::write_default_window_bounds(&kvp, window_bounds, display_uuid)
 6086                    .await
 6087                    .log_err();
 6088            }
 6089        })
 6090    }
 6091
 6092    /// Bypass the 200ms serialization throttle and write workspace state to
 6093    /// the DB immediately. Returns a task the caller can await to ensure the
 6094    /// write completes. Used by the quit handler so the most recent state
 6095    /// isn't lost to a pending throttle timer when the process exits.
 6096    pub fn flush_serialization(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
 6097        self._schedule_serialize_workspace.take();
 6098        self._serialize_workspace_task.take();
 6099        self.bounds_save_task_queued.take();
 6100
 6101        let bounds_task = self.save_window_bounds(window, cx);
 6102        let serialize_task = self.serialize_workspace_internal(window, cx);
 6103        cx.spawn(async move |_| {
 6104            bounds_task.await;
 6105            serialize_task.await;
 6106        })
 6107    }
 6108
 6109    pub fn root_paths(&self, cx: &App) -> Vec<Arc<Path>> {
 6110        let project = self.project().read(cx);
 6111        project
 6112            .visible_worktrees(cx)
 6113            .map(|worktree| worktree.read(cx).abs_path())
 6114            .collect::<Vec<_>>()
 6115    }
 6116
 6117    fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
 6118        match member {
 6119            Member::Axis(PaneAxis { members, .. }) => {
 6120                for child in members.iter() {
 6121                    self.remove_panes(child.clone(), window, cx)
 6122                }
 6123            }
 6124            Member::Pane(pane) => {
 6125                self.force_remove_pane(&pane, &None, window, cx);
 6126            }
 6127        }
 6128    }
 6129
 6130    fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
 6131        self.session_id.take();
 6132        self.serialize_workspace_internal(window, cx)
 6133    }
 6134
 6135    fn force_remove_pane(
 6136        &mut self,
 6137        pane: &Entity<Pane>,
 6138        focus_on: &Option<Entity<Pane>>,
 6139        window: &mut Window,
 6140        cx: &mut Context<Workspace>,
 6141    ) {
 6142        self.panes.retain(|p| p != pane);
 6143        if let Some(focus_on) = focus_on {
 6144            focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
 6145        } else if self.active_pane() == pane {
 6146            self.panes
 6147                .last()
 6148                .unwrap()
 6149                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
 6150        }
 6151        if self.last_active_center_pane == Some(pane.downgrade()) {
 6152            self.last_active_center_pane = None;
 6153        }
 6154        cx.notify();
 6155    }
 6156
 6157    fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 6158        if self._schedule_serialize_workspace.is_none() {
 6159            self._schedule_serialize_workspace =
 6160                Some(cx.spawn_in(window, async move |this, cx| {
 6161                    cx.background_executor()
 6162                        .timer(SERIALIZATION_THROTTLE_TIME)
 6163                        .await;
 6164                    this.update_in(cx, |this, window, cx| {
 6165                        this._serialize_workspace_task =
 6166                            Some(this.serialize_workspace_internal(window, cx));
 6167                        this._schedule_serialize_workspace.take();
 6168                    })
 6169                    .log_err();
 6170                }));
 6171        }
 6172    }
 6173
 6174    fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
 6175        let Some(database_id) = self.database_id() else {
 6176            return Task::ready(());
 6177        };
 6178
 6179        fn serialize_pane_handle(
 6180            pane_handle: &Entity<Pane>,
 6181            window: &mut Window,
 6182            cx: &mut App,
 6183        ) -> SerializedPane {
 6184            let (items, active, pinned_count) = {
 6185                let pane = pane_handle.read(cx);
 6186                let active_item_id = pane.active_item().map(|item| item.item_id());
 6187                (
 6188                    pane.items()
 6189                        .filter_map(|handle| {
 6190                            let handle = handle.to_serializable_item_handle(cx)?;
 6191
 6192                            Some(SerializedItem {
 6193                                kind: Arc::from(handle.serialized_item_kind()),
 6194                                item_id: handle.item_id().as_u64(),
 6195                                active: Some(handle.item_id()) == active_item_id,
 6196                                preview: pane.is_active_preview_item(handle.item_id()),
 6197                            })
 6198                        })
 6199                        .collect::<Vec<_>>(),
 6200                    pane.has_focus(window, cx),
 6201                    pane.pinned_count(),
 6202                )
 6203            };
 6204
 6205            SerializedPane::new(items, active, pinned_count)
 6206        }
 6207
 6208        fn build_serialized_pane_group(
 6209            pane_group: &Member,
 6210            window: &mut Window,
 6211            cx: &mut App,
 6212        ) -> SerializedPaneGroup {
 6213            match pane_group {
 6214                Member::Axis(PaneAxis {
 6215                    axis,
 6216                    members,
 6217                    flexes,
 6218                    bounding_boxes: _,
 6219                }) => SerializedPaneGroup::Group {
 6220                    axis: SerializedAxis(*axis),
 6221                    children: members
 6222                        .iter()
 6223                        .map(|member| build_serialized_pane_group(member, window, cx))
 6224                        .collect::<Vec<_>>(),
 6225                    flexes: Some(flexes.lock().clone()),
 6226                },
 6227                Member::Pane(pane_handle) => {
 6228                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
 6229                }
 6230            }
 6231        }
 6232
 6233        fn build_serialized_docks(
 6234            this: &Workspace,
 6235            window: &mut Window,
 6236            cx: &mut App,
 6237        ) -> DockStructure {
 6238            this.capture_dock_state(window, cx)
 6239        }
 6240
 6241        match self.workspace_location(cx) {
 6242            WorkspaceLocation::Location(location, paths) => {
 6243                let breakpoints = self.project.update(cx, |project, cx| {
 6244                    project
 6245                        .breakpoint_store()
 6246                        .read(cx)
 6247                        .all_source_breakpoints(cx)
 6248                });
 6249                let user_toolchains = self
 6250                    .project
 6251                    .read(cx)
 6252                    .user_toolchains(cx)
 6253                    .unwrap_or_default();
 6254
 6255                let center_group = build_serialized_pane_group(&self.center.root, window, cx);
 6256                let docks = build_serialized_docks(self, window, cx);
 6257                let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
 6258
 6259                let serialized_workspace = SerializedWorkspace {
 6260                    id: database_id,
 6261                    location,
 6262                    paths,
 6263                    center_group,
 6264                    window_bounds,
 6265                    display: Default::default(),
 6266                    docks,
 6267                    centered_layout: self.centered_layout,
 6268                    session_id: self.session_id.clone(),
 6269                    breakpoints,
 6270                    window_id: Some(window.window_handle().window_id().as_u64()),
 6271                    user_toolchains,
 6272                };
 6273
 6274                let db = WorkspaceDb::global(cx);
 6275                window.spawn(cx, async move |_| {
 6276                    db.save_workspace(serialized_workspace).await;
 6277                })
 6278            }
 6279            WorkspaceLocation::DetachFromSession => {
 6280                let window_bounds = SerializedWindowBounds(window.window_bounds());
 6281                let display = window.display(cx).and_then(|d| d.uuid().ok());
 6282                // Save dock state for empty local workspaces
 6283                let docks = build_serialized_docks(self, window, cx);
 6284                let db = WorkspaceDb::global(cx);
 6285                let kvp = db::kvp::KeyValueStore::global(cx);
 6286                window.spawn(cx, async move |_| {
 6287                    db.set_window_open_status(
 6288                        database_id,
 6289                        window_bounds,
 6290                        display.unwrap_or_default(),
 6291                    )
 6292                    .await
 6293                    .log_err();
 6294                    db.set_session_id(database_id, None).await.log_err();
 6295                    persistence::write_default_dock_state(&kvp, docks)
 6296                        .await
 6297                        .log_err();
 6298                })
 6299            }
 6300            WorkspaceLocation::None => {
 6301                // Save dock state for empty non-local workspaces
 6302                let docks = build_serialized_docks(self, window, cx);
 6303                let kvp = db::kvp::KeyValueStore::global(cx);
 6304                window.spawn(cx, async move |_| {
 6305                    persistence::write_default_dock_state(&kvp, docks)
 6306                        .await
 6307                        .log_err();
 6308                })
 6309            }
 6310        }
 6311    }
 6312
 6313    fn has_any_items_open(&self, cx: &App) -> bool {
 6314        self.panes.iter().any(|pane| pane.read(cx).items_len() > 0)
 6315    }
 6316
 6317    fn workspace_location(&self, cx: &App) -> WorkspaceLocation {
 6318        let paths = PathList::new(&self.root_paths(cx));
 6319        if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
 6320            WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
 6321        } else if self.project.read(cx).is_local() {
 6322            if !paths.is_empty() || self.has_any_items_open(cx) {
 6323                WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
 6324            } else {
 6325                WorkspaceLocation::DetachFromSession
 6326            }
 6327        } else {
 6328            WorkspaceLocation::None
 6329        }
 6330    }
 6331
 6332    fn update_history(&self, cx: &mut App) {
 6333        let Some(id) = self.database_id() else {
 6334            return;
 6335        };
 6336        if !self.project.read(cx).is_local() {
 6337            return;
 6338        }
 6339        if let Some(manager) = HistoryManager::global(cx) {
 6340            let paths = PathList::new(&self.root_paths(cx));
 6341            manager.update(cx, |this, cx| {
 6342                this.update_history(id, HistoryManagerEntry::new(id, &paths), cx);
 6343            });
 6344        }
 6345    }
 6346
 6347    async fn serialize_items(
 6348        this: &WeakEntity<Self>,
 6349        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
 6350        cx: &mut AsyncWindowContext,
 6351    ) -> Result<()> {
 6352        const CHUNK_SIZE: usize = 200;
 6353
 6354        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
 6355
 6356        while let Some(items_received) = serializable_items.next().await {
 6357            let unique_items =
 6358                items_received
 6359                    .into_iter()
 6360                    .fold(HashMap::default(), |mut acc, item| {
 6361                        acc.entry(item.item_id()).or_insert(item);
 6362                        acc
 6363                    });
 6364
 6365            // We use into_iter() here so that the references to the items are moved into
 6366            // the tasks and not kept alive while we're sleeping.
 6367            for (_, item) in unique_items.into_iter() {
 6368                if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
 6369                    item.serialize(workspace, false, window, cx)
 6370                }) {
 6371                    cx.background_spawn(async move { task.await.log_err() })
 6372                        .detach();
 6373                }
 6374            }
 6375
 6376            cx.background_executor()
 6377                .timer(SERIALIZATION_THROTTLE_TIME)
 6378                .await;
 6379        }
 6380
 6381        Ok(())
 6382    }
 6383
 6384    pub(crate) fn enqueue_item_serialization(
 6385        &mut self,
 6386        item: Box<dyn SerializableItemHandle>,
 6387    ) -> Result<()> {
 6388        self.serializable_items_tx
 6389            .unbounded_send(item)
 6390            .map_err(|err| anyhow!("failed to send serializable item over channel: {err}"))
 6391    }
 6392
 6393    pub(crate) fn load_workspace(
 6394        serialized_workspace: SerializedWorkspace,
 6395        paths_to_open: Vec<Option<ProjectPath>>,
 6396        window: &mut Window,
 6397        cx: &mut Context<Workspace>,
 6398    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
 6399        cx.spawn_in(window, async move |workspace, cx| {
 6400            let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?;
 6401
 6402            let mut center_group = None;
 6403            let mut center_items = None;
 6404
 6405            // Traverse the splits tree and add to things
 6406            if let Some((group, active_pane, items)) = serialized_workspace
 6407                .center_group
 6408                .deserialize(&project, serialized_workspace.id, workspace.clone(), cx)
 6409                .await
 6410            {
 6411                center_items = Some(items);
 6412                center_group = Some((group, active_pane))
 6413            }
 6414
 6415            let mut items_by_project_path = HashMap::default();
 6416            let mut item_ids_by_kind = HashMap::default();
 6417            let mut all_deserialized_items = Vec::default();
 6418            cx.update(|_, cx| {
 6419                for item in center_items.unwrap_or_default().into_iter().flatten() {
 6420                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
 6421                        item_ids_by_kind
 6422                            .entry(serializable_item_handle.serialized_item_kind())
 6423                            .or_insert(Vec::new())
 6424                            .push(item.item_id().as_u64() as ItemId);
 6425                    }
 6426
 6427                    if let Some(project_path) = item.project_path(cx) {
 6428                        items_by_project_path.insert(project_path, item.clone());
 6429                    }
 6430                    all_deserialized_items.push(item);
 6431                }
 6432            })?;
 6433
 6434            let opened_items = paths_to_open
 6435                .into_iter()
 6436                .map(|path_to_open| {
 6437                    path_to_open
 6438                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
 6439                })
 6440                .collect::<Vec<_>>();
 6441
 6442            // Remove old panes from workspace panes list
 6443            workspace.update_in(cx, |workspace, window, cx| {
 6444                if let Some((center_group, active_pane)) = center_group {
 6445                    workspace.remove_panes(workspace.center.root.clone(), window, cx);
 6446
 6447                    // Swap workspace center group
 6448                    workspace.center = PaneGroup::with_root(center_group);
 6449                    workspace.center.set_is_center(true);
 6450                    workspace.center.mark_positions(cx);
 6451
 6452                    if let Some(active_pane) = active_pane {
 6453                        workspace.set_active_pane(&active_pane, window, cx);
 6454                        cx.focus_self(window);
 6455                    } else {
 6456                        workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
 6457                    }
 6458                }
 6459
 6460                let docks = serialized_workspace.docks;
 6461
 6462                for (dock, serialized_dock) in [
 6463                    (&mut workspace.right_dock, docks.right),
 6464                    (&mut workspace.left_dock, docks.left),
 6465                    (&mut workspace.bottom_dock, docks.bottom),
 6466                ]
 6467                .iter_mut()
 6468                {
 6469                    dock.update(cx, |dock, cx| {
 6470                        dock.serialized_dock = Some(serialized_dock.clone());
 6471                        dock.restore_state(window, cx);
 6472                    });
 6473                }
 6474
 6475                cx.notify();
 6476            })?;
 6477
 6478            let _ = project
 6479                .update(cx, |project, cx| {
 6480                    project
 6481                        .breakpoint_store()
 6482                        .update(cx, |breakpoint_store, cx| {
 6483                            breakpoint_store
 6484                                .with_serialized_breakpoints(serialized_workspace.breakpoints, cx)
 6485                        })
 6486                })
 6487                .await;
 6488
 6489            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
 6490            // after loading the items, we might have different items and in order to avoid
 6491            // the database filling up, we delete items that haven't been loaded now.
 6492            //
 6493            // The items that have been loaded, have been saved after they've been added to the workspace.
 6494            let clean_up_tasks = workspace.update_in(cx, |_, window, cx| {
 6495                item_ids_by_kind
 6496                    .into_iter()
 6497                    .map(|(item_kind, loaded_items)| {
 6498                        SerializableItemRegistry::cleanup(
 6499                            item_kind,
 6500                            serialized_workspace.id,
 6501                            loaded_items,
 6502                            window,
 6503                            cx,
 6504                        )
 6505                        .log_err()
 6506                    })
 6507                    .collect::<Vec<_>>()
 6508            })?;
 6509
 6510            futures::future::join_all(clean_up_tasks).await;
 6511
 6512            workspace
 6513                .update_in(cx, |workspace, window, cx| {
 6514                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
 6515                    workspace.serialize_workspace_internal(window, cx).detach();
 6516
 6517                    // Ensure that we mark the window as edited if we did load dirty items
 6518                    workspace.update_window_edited(window, cx);
 6519                })
 6520                .ok();
 6521
 6522            Ok(opened_items)
 6523        })
 6524    }
 6525
 6526    pub fn key_context(&self, cx: &App) -> KeyContext {
 6527        let mut context = KeyContext::new_with_defaults();
 6528        context.add("Workspace");
 6529        context.set("keyboard_layout", cx.keyboard_layout().name().to_string());
 6530        if let Some(status) = self
 6531            .debugger_provider
 6532            .as_ref()
 6533            .and_then(|provider| provider.active_thread_state(cx))
 6534        {
 6535            match status {
 6536                ThreadStatus::Running | ThreadStatus::Stepping => {
 6537                    context.add("debugger_running");
 6538                }
 6539                ThreadStatus::Stopped => context.add("debugger_stopped"),
 6540                ThreadStatus::Exited | ThreadStatus::Ended => {}
 6541            }
 6542        }
 6543
 6544        if self.left_dock.read(cx).is_open() {
 6545            if let Some(active_panel) = self.left_dock.read(cx).active_panel() {
 6546                context.set("left_dock", active_panel.panel_key());
 6547            }
 6548        }
 6549
 6550        if self.right_dock.read(cx).is_open() {
 6551            if let Some(active_panel) = self.right_dock.read(cx).active_panel() {
 6552                context.set("right_dock", active_panel.panel_key());
 6553            }
 6554        }
 6555
 6556        if self.bottom_dock.read(cx).is_open() {
 6557            if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() {
 6558                context.set("bottom_dock", active_panel.panel_key());
 6559            }
 6560        }
 6561
 6562        context
 6563    }
 6564
 6565    /// Multiworkspace uses this to add workspace action handling to itself
 6566    pub fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
 6567        self.add_workspace_actions_listeners(div, window, cx)
 6568            .on_action(cx.listener(
 6569                |_workspace, action_sequence: &settings::ActionSequence, window, cx| {
 6570                    for action in &action_sequence.0 {
 6571                        window.dispatch_action(action.boxed_clone(), cx);
 6572                    }
 6573                },
 6574            ))
 6575            .on_action(cx.listener(Self::close_inactive_items_and_panes))
 6576            .on_action(cx.listener(Self::close_all_items_and_panes))
 6577            .on_action(cx.listener(Self::close_item_in_all_panes))
 6578            .on_action(cx.listener(Self::save_all))
 6579            .on_action(cx.listener(Self::send_keystrokes))
 6580            .on_action(cx.listener(Self::add_folder_to_project))
 6581            .on_action(cx.listener(Self::follow_next_collaborator))
 6582            .on_action(cx.listener(Self::activate_pane_at_index))
 6583            .on_action(cx.listener(Self::move_item_to_pane_at_index))
 6584            .on_action(cx.listener(Self::move_focused_panel_to_next_position))
 6585            .on_action(cx.listener(Self::toggle_edit_predictions_all_files))
 6586            .on_action(cx.listener(Self::toggle_theme_mode))
 6587            .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
 6588                let pane = workspace.active_pane().clone();
 6589                workspace.unfollow_in_pane(&pane, window, cx);
 6590            }))
 6591            .on_action(cx.listener(|workspace, action: &Save, window, cx| {
 6592                workspace
 6593                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
 6594                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
 6595            }))
 6596            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
 6597                workspace
 6598                    .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
 6599                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
 6600            }))
 6601            .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
 6602                workspace
 6603                    .save_active_item(SaveIntent::SaveAs, window, cx)
 6604                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
 6605            }))
 6606            .on_action(
 6607                cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
 6608                    workspace.activate_previous_pane(window, cx)
 6609                }),
 6610            )
 6611            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
 6612                workspace.activate_next_pane(window, cx)
 6613            }))
 6614            .on_action(cx.listener(|workspace, _: &ActivateLastPane, window, cx| {
 6615                workspace.activate_last_pane(window, cx)
 6616            }))
 6617            .on_action(
 6618                cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
 6619                    workspace.activate_next_window(cx)
 6620                }),
 6621            )
 6622            .on_action(
 6623                cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
 6624                    workspace.activate_previous_window(cx)
 6625                }),
 6626            )
 6627            .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
 6628                workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
 6629            }))
 6630            .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
 6631                workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
 6632            }))
 6633            .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
 6634                workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
 6635            }))
 6636            .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
 6637                workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
 6638            }))
 6639            .on_action(cx.listener(
 6640                |workspace, action: &MoveItemToPaneInDirection, window, cx| {
 6641                    workspace.move_item_to_pane_in_direction(action, window, cx)
 6642                },
 6643            ))
 6644            .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
 6645                workspace.swap_pane_in_direction(SplitDirection::Left, cx)
 6646            }))
 6647            .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
 6648                workspace.swap_pane_in_direction(SplitDirection::Right, cx)
 6649            }))
 6650            .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
 6651                workspace.swap_pane_in_direction(SplitDirection::Up, cx)
 6652            }))
 6653            .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
 6654                workspace.swap_pane_in_direction(SplitDirection::Down, cx)
 6655            }))
 6656            .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| {
 6657                const DIRECTION_PRIORITY: [SplitDirection; 4] = [
 6658                    SplitDirection::Down,
 6659                    SplitDirection::Up,
 6660                    SplitDirection::Right,
 6661                    SplitDirection::Left,
 6662                ];
 6663                for dir in DIRECTION_PRIORITY {
 6664                    if workspace.find_pane_in_direction(dir, cx).is_some() {
 6665                        workspace.swap_pane_in_direction(dir, cx);
 6666                        workspace.activate_pane_in_direction(dir.opposite(), window, cx);
 6667                        break;
 6668                    }
 6669                }
 6670            }))
 6671            .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| {
 6672                workspace.move_pane_to_border(SplitDirection::Left, cx)
 6673            }))
 6674            .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| {
 6675                workspace.move_pane_to_border(SplitDirection::Right, cx)
 6676            }))
 6677            .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| {
 6678                workspace.move_pane_to_border(SplitDirection::Up, cx)
 6679            }))
 6680            .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| {
 6681                workspace.move_pane_to_border(SplitDirection::Down, cx)
 6682            }))
 6683            .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
 6684                this.toggle_dock(DockPosition::Left, window, cx);
 6685            }))
 6686            .on_action(cx.listener(
 6687                |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
 6688                    workspace.toggle_dock(DockPosition::Right, window, cx);
 6689                },
 6690            ))
 6691            .on_action(cx.listener(
 6692                |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
 6693                    workspace.toggle_dock(DockPosition::Bottom, window, cx);
 6694                },
 6695            ))
 6696            .on_action(cx.listener(
 6697                |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| {
 6698                    if !workspace.close_active_dock(window, cx) {
 6699                        cx.propagate();
 6700                    }
 6701                },
 6702            ))
 6703            .on_action(
 6704                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
 6705                    workspace.close_all_docks(window, cx);
 6706                }),
 6707            )
 6708            .on_action(cx.listener(Self::toggle_all_docks))
 6709            .on_action(cx.listener(
 6710                |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
 6711                    workspace.clear_all_notifications(cx);
 6712                },
 6713            ))
 6714            .on_action(cx.listener(
 6715                |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| {
 6716                    workspace.clear_navigation_history(window, cx);
 6717                },
 6718            ))
 6719            .on_action(cx.listener(
 6720                |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
 6721                    if let Some((notification_id, _)) = workspace.notifications.pop() {
 6722                        workspace.suppress_notification(&notification_id, cx);
 6723                    }
 6724                },
 6725            ))
 6726            .on_action(cx.listener(
 6727                |workspace: &mut Workspace, _: &ToggleWorktreeSecurity, window, cx| {
 6728                    workspace.show_worktree_trust_security_modal(true, window, cx);
 6729                },
 6730            ))
 6731            .on_action(
 6732                cx.listener(|_: &mut Workspace, _: &ClearTrustedWorktrees, _, cx| {
 6733                    if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
 6734                        trusted_worktrees.update(cx, |trusted_worktrees, _| {
 6735                            trusted_worktrees.clear_trusted_paths()
 6736                        });
 6737                        let db = WorkspaceDb::global(cx);
 6738                        cx.spawn(async move |_, cx| {
 6739                            if db.clear_trusted_worktrees().await.log_err().is_some() {
 6740                                cx.update(|cx| reload(cx));
 6741                            }
 6742                        })
 6743                        .detach();
 6744                    }
 6745                }),
 6746            )
 6747            .on_action(cx.listener(
 6748                |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
 6749                    workspace.reopen_closed_item(window, cx).detach();
 6750                },
 6751            ))
 6752            .on_action(cx.listener(
 6753                |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| {
 6754                    for dock in workspace.all_docks() {
 6755                        if dock.focus_handle(cx).contains_focused(window, cx) {
 6756                            let Some(panel) = dock.read(cx).active_panel() else {
 6757                                return;
 6758                            };
 6759
 6760                            // Set to `None`, then the size will fall back to the default.
 6761                            panel.clone().set_size(None, window, cx);
 6762
 6763                            return;
 6764                        }
 6765                    }
 6766                },
 6767            ))
 6768            .on_action(cx.listener(
 6769                |workspace: &mut Workspace, _: &ResetOpenDocksSize, window, cx| {
 6770                    for dock in workspace.all_docks() {
 6771                        if let Some(panel) = dock.read(cx).visible_panel() {
 6772                            // Set to `None`, then the size will fall back to the default.
 6773                            panel.clone().set_size(None, window, cx);
 6774                        }
 6775                    }
 6776                },
 6777            ))
 6778            .on_action(cx.listener(
 6779                |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| {
 6780                    adjust_active_dock_size_by_px(
 6781                        px_with_ui_font_fallback(act.px, cx),
 6782                        workspace,
 6783                        window,
 6784                        cx,
 6785                    );
 6786                },
 6787            ))
 6788            .on_action(cx.listener(
 6789                |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| {
 6790                    adjust_active_dock_size_by_px(
 6791                        px_with_ui_font_fallback(act.px, cx) * -1.,
 6792                        workspace,
 6793                        window,
 6794                        cx,
 6795                    );
 6796                },
 6797            ))
 6798            .on_action(cx.listener(
 6799                |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| {
 6800                    adjust_open_docks_size_by_px(
 6801                        px_with_ui_font_fallback(act.px, cx),
 6802                        workspace,
 6803                        window,
 6804                        cx,
 6805                    );
 6806                },
 6807            ))
 6808            .on_action(cx.listener(
 6809                |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| {
 6810                    adjust_open_docks_size_by_px(
 6811                        px_with_ui_font_fallback(act.px, cx) * -1.,
 6812                        workspace,
 6813                        window,
 6814                        cx,
 6815                    );
 6816                },
 6817            ))
 6818            .on_action(cx.listener(Workspace::toggle_centered_layout))
 6819            .on_action(cx.listener(
 6820                |workspace: &mut Workspace, _action: &pane::ActivateNextItem, window, cx| {
 6821                    if let Some(active_dock) = workspace.active_dock(window, cx) {
 6822                        let dock = active_dock.read(cx);
 6823                        if let Some(active_panel) = dock.active_panel() {
 6824                            if active_panel.pane(cx).is_none() {
 6825                                let mut recent_pane: Option<Entity<Pane>> = None;
 6826                                let mut recent_timestamp = 0;
 6827                                for pane_handle in workspace.panes() {
 6828                                    let pane = pane_handle.read(cx);
 6829                                    for entry in pane.activation_history() {
 6830                                        if entry.timestamp > recent_timestamp {
 6831                                            recent_timestamp = entry.timestamp;
 6832                                            recent_pane = Some(pane_handle.clone());
 6833                                        }
 6834                                    }
 6835                                }
 6836
 6837                                if let Some(pane) = recent_pane {
 6838                                    pane.update(cx, |pane, cx| {
 6839                                        let current_index = pane.active_item_index();
 6840                                        let items_len = pane.items_len();
 6841                                        if items_len > 0 {
 6842                                            let next_index = if current_index + 1 < items_len {
 6843                                                current_index + 1
 6844                                            } else {
 6845                                                0
 6846                                            };
 6847                                            pane.activate_item(
 6848                                                next_index, false, false, window, cx,
 6849                                            );
 6850                                        }
 6851                                    });
 6852                                    return;
 6853                                }
 6854                            }
 6855                        }
 6856                    }
 6857                    cx.propagate();
 6858                },
 6859            ))
 6860            .on_action(cx.listener(
 6861                |workspace: &mut Workspace, _action: &pane::ActivatePreviousItem, window, cx| {
 6862                    if let Some(active_dock) = workspace.active_dock(window, cx) {
 6863                        let dock = active_dock.read(cx);
 6864                        if let Some(active_panel) = dock.active_panel() {
 6865                            if active_panel.pane(cx).is_none() {
 6866                                let mut recent_pane: Option<Entity<Pane>> = None;
 6867                                let mut recent_timestamp = 0;
 6868                                for pane_handle in workspace.panes() {
 6869                                    let pane = pane_handle.read(cx);
 6870                                    for entry in pane.activation_history() {
 6871                                        if entry.timestamp > recent_timestamp {
 6872                                            recent_timestamp = entry.timestamp;
 6873                                            recent_pane = Some(pane_handle.clone());
 6874                                        }
 6875                                    }
 6876                                }
 6877
 6878                                if let Some(pane) = recent_pane {
 6879                                    pane.update(cx, |pane, cx| {
 6880                                        let current_index = pane.active_item_index();
 6881                                        let items_len = pane.items_len();
 6882                                        if items_len > 0 {
 6883                                            let prev_index = if current_index > 0 {
 6884                                                current_index - 1
 6885                                            } else {
 6886                                                items_len.saturating_sub(1)
 6887                                            };
 6888                                            pane.activate_item(
 6889                                                prev_index, false, false, window, cx,
 6890                                            );
 6891                                        }
 6892                                    });
 6893                                    return;
 6894                                }
 6895                            }
 6896                        }
 6897                    }
 6898                    cx.propagate();
 6899                },
 6900            ))
 6901            .on_action(cx.listener(
 6902                |workspace: &mut Workspace, action: &pane::CloseActiveItem, window, cx| {
 6903                    if let Some(active_dock) = workspace.active_dock(window, cx) {
 6904                        let dock = active_dock.read(cx);
 6905                        if let Some(active_panel) = dock.active_panel() {
 6906                            if active_panel.pane(cx).is_none() {
 6907                                let active_pane = workspace.active_pane().clone();
 6908                                active_pane.update(cx, |pane, cx| {
 6909                                    pane.close_active_item(action, window, cx)
 6910                                        .detach_and_log_err(cx);
 6911                                });
 6912                                return;
 6913                            }
 6914                        }
 6915                    }
 6916                    cx.propagate();
 6917                },
 6918            ))
 6919            .on_action(
 6920                cx.listener(|workspace, _: &ToggleReadOnlyFile, window, cx| {
 6921                    let pane = workspace.active_pane().clone();
 6922                    if let Some(item) = pane.read(cx).active_item() {
 6923                        item.toggle_read_only(window, cx);
 6924                    }
 6925                }),
 6926            )
 6927            .on_action(cx.listener(|workspace, _: &FocusCenterPane, window, cx| {
 6928                workspace.focus_center_pane(window, cx);
 6929            }))
 6930            .on_action(cx.listener(Workspace::cancel))
 6931    }
 6932
 6933    #[cfg(any(test, feature = "test-support"))]
 6934    pub fn set_random_database_id(&mut self) {
 6935        self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64));
 6936    }
 6937
 6938    #[cfg(any(test, feature = "test-support"))]
 6939    pub(crate) fn test_new(
 6940        project: Entity<Project>,
 6941        window: &mut Window,
 6942        cx: &mut Context<Self>,
 6943    ) -> Self {
 6944        use node_runtime::NodeRuntime;
 6945        use session::Session;
 6946
 6947        let client = project.read(cx).client();
 6948        let user_store = project.read(cx).user_store();
 6949        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
 6950        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
 6951        window.activate_window();
 6952        let app_state = Arc::new(AppState {
 6953            languages: project.read(cx).languages().clone(),
 6954            workspace_store,
 6955            client,
 6956            user_store,
 6957            fs: project.read(cx).fs().clone(),
 6958            build_window_options: |_, _| Default::default(),
 6959            node_runtime: NodeRuntime::unavailable(),
 6960            session,
 6961        });
 6962        let workspace = Self::new(Default::default(), project, app_state, window, cx);
 6963        workspace
 6964            .active_pane
 6965            .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
 6966        workspace
 6967    }
 6968
 6969    pub fn register_action<A: Action>(
 6970        &mut self,
 6971        callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
 6972    ) -> &mut Self {
 6973        let callback = Arc::new(callback);
 6974
 6975        self.workspace_actions.push(Box::new(move |div, _, _, cx| {
 6976            let callback = callback.clone();
 6977            div.on_action(cx.listener(move |workspace, event, window, cx| {
 6978                (callback)(workspace, event, window, cx)
 6979            }))
 6980        }));
 6981        self
 6982    }
 6983    pub fn register_action_renderer(
 6984        &mut self,
 6985        callback: impl Fn(Div, &Workspace, &mut Window, &mut Context<Self>) -> Div + 'static,
 6986    ) -> &mut Self {
 6987        self.workspace_actions.push(Box::new(callback));
 6988        self
 6989    }
 6990
 6991    fn add_workspace_actions_listeners(
 6992        &self,
 6993        mut div: Div,
 6994        window: &mut Window,
 6995        cx: &mut Context<Self>,
 6996    ) -> Div {
 6997        for action in self.workspace_actions.iter() {
 6998            div = (action)(div, self, window, cx)
 6999        }
 7000        div
 7001    }
 7002
 7003    pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
 7004        self.modal_layer.read(cx).has_active_modal()
 7005    }
 7006
 7007    pub fn is_active_modal_command_palette(&self, cx: &mut App) -> bool {
 7008        self.modal_layer
 7009            .read(cx)
 7010            .is_active_modal_command_palette(cx)
 7011    }
 7012
 7013    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
 7014        self.modal_layer.read(cx).active_modal()
 7015    }
 7016
 7017    /// Toggles a modal of type `V`. If a modal of the same type is currently active,
 7018    /// it will be hidden. If a different modal is active, it will be replaced with the new one.
 7019    /// If no modal is active, the new modal will be shown.
 7020    ///
 7021    /// If closing the current modal fails (e.g., due to `on_before_dismiss` returning
 7022    /// `DismissDecision::Dismiss(false)` or `DismissDecision::Pending`), the new modal
 7023    /// will not be shown.
 7024    pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
 7025    where
 7026        B: FnOnce(&mut Window, &mut Context<V>) -> V,
 7027    {
 7028        self.modal_layer.update(cx, |modal_layer, cx| {
 7029            modal_layer.toggle_modal(window, cx, build)
 7030        })
 7031    }
 7032
 7033    pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool {
 7034        self.modal_layer
 7035            .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx))
 7036    }
 7037
 7038    pub fn toggle_status_toast<V: ToastView>(&mut self, entity: Entity<V>, cx: &mut App) {
 7039        self.toast_layer
 7040            .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity))
 7041    }
 7042
 7043    pub fn toggle_centered_layout(
 7044        &mut self,
 7045        _: &ToggleCenteredLayout,
 7046        _: &mut Window,
 7047        cx: &mut Context<Self>,
 7048    ) {
 7049        self.centered_layout = !self.centered_layout;
 7050        if let Some(database_id) = self.database_id() {
 7051            let db = WorkspaceDb::global(cx);
 7052            let centered_layout = self.centered_layout;
 7053            cx.background_spawn(async move {
 7054                db.set_centered_layout(database_id, centered_layout).await
 7055            })
 7056            .detach_and_log_err(cx);
 7057        }
 7058        cx.notify();
 7059    }
 7060
 7061    fn adjust_padding(padding: Option<f32>) -> f32 {
 7062        padding
 7063            .unwrap_or(CenteredPaddingSettings::default().0)
 7064            .clamp(
 7065                CenteredPaddingSettings::MIN_PADDING,
 7066                CenteredPaddingSettings::MAX_PADDING,
 7067            )
 7068    }
 7069
 7070    fn render_dock(
 7071        &self,
 7072        position: DockPosition,
 7073        dock: &Entity<Dock>,
 7074        window: &mut Window,
 7075        cx: &mut App,
 7076    ) -> Option<Div> {
 7077        if self.zoomed_position == Some(position) {
 7078            return None;
 7079        }
 7080
 7081        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
 7082            let pane = panel.pane(cx)?;
 7083            let follower_states = &self.follower_states;
 7084            leader_border_for_pane(follower_states, &pane, window, cx)
 7085        });
 7086
 7087        Some(
 7088            div()
 7089                .flex()
 7090                .flex_none()
 7091                .overflow_hidden()
 7092                .child(dock.clone())
 7093                .children(leader_border),
 7094        )
 7095    }
 7096
 7097    pub fn for_window(window: &Window, cx: &App) -> Option<Entity<Workspace>> {
 7098        window
 7099            .root::<MultiWorkspace>()
 7100            .flatten()
 7101            .map(|multi_workspace| multi_workspace.read(cx).workspace().clone())
 7102    }
 7103
 7104    pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
 7105        self.zoomed.as_ref()
 7106    }
 7107
 7108    pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
 7109        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
 7110            return;
 7111        };
 7112        let windows = cx.windows();
 7113        let next_window =
 7114            SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else(
 7115                || {
 7116                    windows
 7117                        .iter()
 7118                        .cycle()
 7119                        .skip_while(|window| window.window_id() != current_window_id)
 7120                        .nth(1)
 7121                },
 7122            );
 7123
 7124        if let Some(window) = next_window {
 7125            window
 7126                .update(cx, |_, window, _| window.activate_window())
 7127                .ok();
 7128        }
 7129    }
 7130
 7131    pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
 7132        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
 7133            return;
 7134        };
 7135        let windows = cx.windows();
 7136        let prev_window =
 7137            SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else(
 7138                || {
 7139                    windows
 7140                        .iter()
 7141                        .rev()
 7142                        .cycle()
 7143                        .skip_while(|window| window.window_id() != current_window_id)
 7144                        .nth(1)
 7145                },
 7146            );
 7147
 7148        if let Some(window) = prev_window {
 7149            window
 7150                .update(cx, |_, window, _| window.activate_window())
 7151                .ok();
 7152        }
 7153    }
 7154
 7155    pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 7156        if cx.stop_active_drag(window) {
 7157        } else if let Some((notification_id, _)) = self.notifications.pop() {
 7158            dismiss_app_notification(&notification_id, cx);
 7159        } else {
 7160            cx.propagate();
 7161        }
 7162    }
 7163
 7164    fn adjust_dock_size_by_px(
 7165        &mut self,
 7166        panel_size: Pixels,
 7167        dock_pos: DockPosition,
 7168        px: Pixels,
 7169        window: &mut Window,
 7170        cx: &mut Context<Self>,
 7171    ) {
 7172        match dock_pos {
 7173            DockPosition::Left => self.resize_left_dock(panel_size + px, window, cx),
 7174            DockPosition::Right => self.resize_right_dock(panel_size + px, window, cx),
 7175            DockPosition::Bottom => self.resize_bottom_dock(panel_size + px, window, cx),
 7176        }
 7177    }
 7178
 7179    fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
 7180        let workspace_width = self.bounds.size.width;
 7181        let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
 7182
 7183        self.right_dock.read_with(cx, |right_dock, cx| {
 7184            let right_dock_size = right_dock
 7185                .active_panel_size(window, cx)
 7186                .unwrap_or(Pixels::ZERO);
 7187            if right_dock_size + size > workspace_width {
 7188                size = workspace_width - right_dock_size
 7189            }
 7190        });
 7191
 7192        self.left_dock.update(cx, |left_dock, cx| {
 7193            if WorkspaceSettings::get_global(cx)
 7194                .resize_all_panels_in_dock
 7195                .contains(&DockPosition::Left)
 7196            {
 7197                left_dock.resize_all_panels(Some(size), window, cx);
 7198            } else {
 7199                left_dock.resize_active_panel(Some(size), window, cx);
 7200            }
 7201        });
 7202    }
 7203
 7204    fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
 7205        let workspace_width = self.bounds.size.width;
 7206        let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
 7207        self.left_dock.read_with(cx, |left_dock, cx| {
 7208            let left_dock_size = left_dock
 7209                .active_panel_size(window, cx)
 7210                .unwrap_or(Pixels::ZERO);
 7211            if left_dock_size + size > workspace_width {
 7212                size = workspace_width - left_dock_size
 7213            }
 7214        });
 7215        self.right_dock.update(cx, |right_dock, cx| {
 7216            if WorkspaceSettings::get_global(cx)
 7217                .resize_all_panels_in_dock
 7218                .contains(&DockPosition::Right)
 7219            {
 7220                right_dock.resize_all_panels(Some(size), window, cx);
 7221            } else {
 7222                right_dock.resize_active_panel(Some(size), window, cx);
 7223            }
 7224        });
 7225    }
 7226
 7227    fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
 7228        let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top());
 7229        self.bottom_dock.update(cx, |bottom_dock, cx| {
 7230            if WorkspaceSettings::get_global(cx)
 7231                .resize_all_panels_in_dock
 7232                .contains(&DockPosition::Bottom)
 7233            {
 7234                bottom_dock.resize_all_panels(Some(size), window, cx);
 7235            } else {
 7236                bottom_dock.resize_active_panel(Some(size), window, cx);
 7237            }
 7238        });
 7239    }
 7240
 7241    fn toggle_edit_predictions_all_files(
 7242        &mut self,
 7243        _: &ToggleEditPrediction,
 7244        _window: &mut Window,
 7245        cx: &mut Context<Self>,
 7246    ) {
 7247        let fs = self.project().read(cx).fs().clone();
 7248        let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx);
 7249        update_settings_file(fs, cx, move |file, _| {
 7250            file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions)
 7251        });
 7252    }
 7253
 7254    fn toggle_theme_mode(&mut self, _: &ToggleMode, _window: &mut Window, cx: &mut Context<Self>) {
 7255        let current_mode = ThemeSettings::get_global(cx).theme.mode();
 7256        let next_mode = match current_mode {
 7257            Some(theme::ThemeAppearanceMode::Light) => theme::ThemeAppearanceMode::Dark,
 7258            Some(theme::ThemeAppearanceMode::Dark) => theme::ThemeAppearanceMode::Light,
 7259            Some(theme::ThemeAppearanceMode::System) | None => match cx.theme().appearance() {
 7260                theme::Appearance::Light => theme::ThemeAppearanceMode::Dark,
 7261                theme::Appearance::Dark => theme::ThemeAppearanceMode::Light,
 7262            },
 7263        };
 7264
 7265        let fs = self.project().read(cx).fs().clone();
 7266        settings::update_settings_file(fs, cx, move |settings, _cx| {
 7267            theme::set_mode(settings, next_mode);
 7268        });
 7269    }
 7270
 7271    pub fn show_worktree_trust_security_modal(
 7272        &mut self,
 7273        toggle: bool,
 7274        window: &mut Window,
 7275        cx: &mut Context<Self>,
 7276    ) {
 7277        if let Some(security_modal) = self.active_modal::<SecurityModal>(cx) {
 7278            if toggle {
 7279                security_modal.update(cx, |security_modal, cx| {
 7280                    security_modal.dismiss(cx);
 7281                })
 7282            } else {
 7283                security_modal.update(cx, |security_modal, cx| {
 7284                    security_modal.refresh_restricted_paths(cx);
 7285                });
 7286            }
 7287        } else {
 7288            let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
 7289                .map(|trusted_worktrees| {
 7290                    trusted_worktrees
 7291                        .read(cx)
 7292                        .has_restricted_worktrees(&self.project().read(cx).worktree_store(), cx)
 7293                })
 7294                .unwrap_or(false);
 7295            if has_restricted_worktrees {
 7296                let project = self.project().read(cx);
 7297                let remote_host = project
 7298                    .remote_connection_options(cx)
 7299                    .map(RemoteHostLocation::from);
 7300                let worktree_store = project.worktree_store().downgrade();
 7301                self.toggle_modal(window, cx, |_, cx| {
 7302                    SecurityModal::new(worktree_store, remote_host, cx)
 7303                });
 7304            }
 7305        }
 7306    }
 7307}
 7308
 7309pub trait AnyActiveCall {
 7310    fn entity(&self) -> AnyEntity;
 7311    fn is_in_room(&self, _: &App) -> bool;
 7312    fn room_id(&self, _: &App) -> Option<u64>;
 7313    fn channel_id(&self, _: &App) -> Option<ChannelId>;
 7314    fn hang_up(&self, _: &mut App) -> Task<Result<()>>;
 7315    fn unshare_project(&self, _: Entity<Project>, _: &mut App) -> Result<()>;
 7316    fn remote_participant_for_peer_id(&self, _: PeerId, _: &App) -> Option<RemoteCollaborator>;
 7317    fn is_sharing_project(&self, _: &App) -> bool;
 7318    fn has_remote_participants(&self, _: &App) -> bool;
 7319    fn local_participant_is_guest(&self, _: &App) -> bool;
 7320    fn client(&self, _: &App) -> Arc<Client>;
 7321    fn share_on_join(&self, _: &App) -> bool;
 7322    fn join_channel(&self, _: ChannelId, _: &mut App) -> Task<Result<bool>>;
 7323    fn room_update_completed(&self, _: &mut App) -> Task<()>;
 7324    fn most_active_project(&self, _: &App) -> Option<(u64, u64)>;
 7325    fn share_project(&self, _: Entity<Project>, _: &mut App) -> Task<Result<u64>>;
 7326    fn join_project(
 7327        &self,
 7328        _: u64,
 7329        _: Arc<LanguageRegistry>,
 7330        _: Arc<dyn Fs>,
 7331        _: &mut App,
 7332    ) -> Task<Result<Entity<Project>>>;
 7333    fn peer_id_for_user_in_room(&self, _: u64, _: &App) -> Option<PeerId>;
 7334    fn subscribe(
 7335        &self,
 7336        _: &mut Window,
 7337        _: &mut Context<Workspace>,
 7338        _: Box<dyn Fn(&mut Workspace, &ActiveCallEvent, &mut Window, &mut Context<Workspace>)>,
 7339    ) -> Subscription;
 7340    fn create_shared_screen(
 7341        &self,
 7342        _: PeerId,
 7343        _: &Entity<Pane>,
 7344        _: &mut Window,
 7345        _: &mut App,
 7346    ) -> Option<Entity<SharedScreen>>;
 7347}
 7348
 7349#[derive(Clone)]
 7350pub struct GlobalAnyActiveCall(pub Arc<dyn AnyActiveCall>);
 7351impl Global for GlobalAnyActiveCall {}
 7352
 7353impl GlobalAnyActiveCall {
 7354    pub(crate) fn try_global(cx: &App) -> Option<&Self> {
 7355        cx.try_global()
 7356    }
 7357
 7358    pub(crate) fn global(cx: &App) -> &Self {
 7359        cx.global()
 7360    }
 7361}
 7362
 7363pub fn merge_conflict_notification_id() -> NotificationId {
 7364    struct MergeConflictNotification;
 7365    NotificationId::unique::<MergeConflictNotification>()
 7366}
 7367
 7368/// Workspace-local view of a remote participant's location.
 7369#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 7370pub enum ParticipantLocation {
 7371    SharedProject { project_id: u64 },
 7372    UnsharedProject,
 7373    External,
 7374}
 7375
 7376impl ParticipantLocation {
 7377    pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
 7378        match location
 7379            .and_then(|l| l.variant)
 7380            .context("participant location was not provided")?
 7381        {
 7382            proto::participant_location::Variant::SharedProject(project) => {
 7383                Ok(Self::SharedProject {
 7384                    project_id: project.id,
 7385                })
 7386            }
 7387            proto::participant_location::Variant::UnsharedProject(_) => Ok(Self::UnsharedProject),
 7388            proto::participant_location::Variant::External(_) => Ok(Self::External),
 7389        }
 7390    }
 7391}
 7392/// Workspace-local view of a remote collaborator's state.
 7393/// This is the subset of `call::RemoteParticipant` that workspace needs.
 7394#[derive(Clone)]
 7395pub struct RemoteCollaborator {
 7396    pub user: Arc<User>,
 7397    pub peer_id: PeerId,
 7398    pub location: ParticipantLocation,
 7399    pub participant_index: ParticipantIndex,
 7400}
 7401
 7402pub enum ActiveCallEvent {
 7403    ParticipantLocationChanged { participant_id: PeerId },
 7404    RemoteVideoTracksChanged { participant_id: PeerId },
 7405}
 7406
 7407fn leader_border_for_pane(
 7408    follower_states: &HashMap<CollaboratorId, FollowerState>,
 7409    pane: &Entity<Pane>,
 7410    _: &Window,
 7411    cx: &App,
 7412) -> Option<Div> {
 7413    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
 7414        if state.pane() == pane {
 7415            Some((*leader_id, state))
 7416        } else {
 7417            None
 7418        }
 7419    })?;
 7420
 7421    let mut leader_color = match leader_id {
 7422        CollaboratorId::PeerId(leader_peer_id) => {
 7423            let leader = GlobalAnyActiveCall::try_global(cx)?
 7424                .0
 7425                .remote_participant_for_peer_id(leader_peer_id, cx)?;
 7426
 7427            cx.theme()
 7428                .players()
 7429                .color_for_participant(leader.participant_index.0)
 7430                .cursor
 7431        }
 7432        CollaboratorId::Agent => cx.theme().players().agent().cursor,
 7433    };
 7434    leader_color.fade_out(0.3);
 7435    Some(
 7436        div()
 7437            .absolute()
 7438            .size_full()
 7439            .left_0()
 7440            .top_0()
 7441            .border_2()
 7442            .border_color(leader_color),
 7443    )
 7444}
 7445
 7446fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
 7447    ZED_WINDOW_POSITION
 7448        .zip(*ZED_WINDOW_SIZE)
 7449        .map(|(position, size)| Bounds {
 7450            origin: position,
 7451            size,
 7452        })
 7453}
 7454
 7455fn open_items(
 7456    serialized_workspace: Option<SerializedWorkspace>,
 7457    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
 7458    window: &mut Window,
 7459    cx: &mut Context<Workspace>,
 7460) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> + use<> {
 7461    let restored_items = serialized_workspace.map(|serialized_workspace| {
 7462        Workspace::load_workspace(
 7463            serialized_workspace,
 7464            project_paths_to_open
 7465                .iter()
 7466                .map(|(_, project_path)| project_path)
 7467                .cloned()
 7468                .collect(),
 7469            window,
 7470            cx,
 7471        )
 7472    });
 7473
 7474    cx.spawn_in(window, async move |workspace, cx| {
 7475        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
 7476
 7477        if let Some(restored_items) = restored_items {
 7478            let restored_items = restored_items.await?;
 7479
 7480            let restored_project_paths = restored_items
 7481                .iter()
 7482                .filter_map(|item| {
 7483                    cx.update(|_, cx| item.as_ref()?.project_path(cx))
 7484                        .ok()
 7485                        .flatten()
 7486                })
 7487                .collect::<HashSet<_>>();
 7488
 7489            for restored_item in restored_items {
 7490                opened_items.push(restored_item.map(Ok));
 7491            }
 7492
 7493            project_paths_to_open
 7494                .iter_mut()
 7495                .for_each(|(_, project_path)| {
 7496                    if let Some(project_path_to_open) = project_path
 7497                        && restored_project_paths.contains(project_path_to_open)
 7498                    {
 7499                        *project_path = None;
 7500                    }
 7501                });
 7502        } else {
 7503            for _ in 0..project_paths_to_open.len() {
 7504                opened_items.push(None);
 7505            }
 7506        }
 7507        assert!(opened_items.len() == project_paths_to_open.len());
 7508
 7509        let tasks =
 7510            project_paths_to_open
 7511                .into_iter()
 7512                .enumerate()
 7513                .map(|(ix, (abs_path, project_path))| {
 7514                    let workspace = workspace.clone();
 7515                    cx.spawn(async move |cx| {
 7516                        let file_project_path = project_path?;
 7517                        let abs_path_task = workspace.update(cx, |workspace, cx| {
 7518                            workspace.project().update(cx, |project, cx| {
 7519                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
 7520                            })
 7521                        });
 7522
 7523                        // We only want to open file paths here. If one of the items
 7524                        // here is a directory, it was already opened further above
 7525                        // with a `find_or_create_worktree`.
 7526                        if let Ok(task) = abs_path_task
 7527                            && task.await.is_none_or(|p| p.is_file())
 7528                        {
 7529                            return Some((
 7530                                ix,
 7531                                workspace
 7532                                    .update_in(cx, |workspace, window, cx| {
 7533                                        workspace.open_path(
 7534                                            file_project_path,
 7535                                            None,
 7536                                            true,
 7537                                            window,
 7538                                            cx,
 7539                                        )
 7540                                    })
 7541                                    .log_err()?
 7542                                    .await,
 7543                            ));
 7544                        }
 7545                        None
 7546                    })
 7547                });
 7548
 7549        let tasks = tasks.collect::<Vec<_>>();
 7550
 7551        let tasks = futures::future::join_all(tasks);
 7552        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
 7553            opened_items[ix] = Some(path_open_result);
 7554        }
 7555
 7556        Ok(opened_items)
 7557    })
 7558}
 7559
 7560#[derive(Clone)]
 7561enum ActivateInDirectionTarget {
 7562    Pane(Entity<Pane>),
 7563    Dock(Entity<Dock>),
 7564    Sidebar(FocusHandle),
 7565}
 7566
 7567fn notify_if_database_failed(window: WindowHandle<MultiWorkspace>, cx: &mut AsyncApp) {
 7568    window
 7569        .update(cx, |multi_workspace, _, cx| {
 7570            let workspace = multi_workspace.workspace().clone();
 7571            workspace.update(cx, |workspace, cx| {
 7572                if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
 7573                    struct DatabaseFailedNotification;
 7574
 7575                    workspace.show_notification(
 7576                        NotificationId::unique::<DatabaseFailedNotification>(),
 7577                        cx,
 7578                        |cx| {
 7579                            cx.new(|cx| {
 7580                                MessageNotification::new("Failed to load the database file.", cx)
 7581                                    .primary_message("File an Issue")
 7582                                    .primary_icon(IconName::Plus)
 7583                                    .primary_on_click(|window, cx| {
 7584                                        window.dispatch_action(Box::new(FileBugReport), cx)
 7585                                    })
 7586                            })
 7587                        },
 7588                    );
 7589                }
 7590            });
 7591        })
 7592        .log_err();
 7593}
 7594
 7595fn px_with_ui_font_fallback(val: u32, cx: &Context<Workspace>) -> Pixels {
 7596    if val == 0 {
 7597        ThemeSettings::get_global(cx).ui_font_size(cx)
 7598    } else {
 7599        px(val as f32)
 7600    }
 7601}
 7602
 7603fn adjust_active_dock_size_by_px(
 7604    px: Pixels,
 7605    workspace: &mut Workspace,
 7606    window: &mut Window,
 7607    cx: &mut Context<Workspace>,
 7608) {
 7609    let Some(active_dock) = workspace
 7610        .all_docks()
 7611        .into_iter()
 7612        .find(|dock| dock.focus_handle(cx).contains_focused(window, cx))
 7613    else {
 7614        return;
 7615    };
 7616    let dock = active_dock.read(cx);
 7617    let Some(panel_size) = dock.active_panel_size(window, cx) else {
 7618        return;
 7619    };
 7620    let dock_pos = dock.position();
 7621    workspace.adjust_dock_size_by_px(panel_size, dock_pos, px, window, cx);
 7622}
 7623
 7624fn adjust_open_docks_size_by_px(
 7625    px: Pixels,
 7626    workspace: &mut Workspace,
 7627    window: &mut Window,
 7628    cx: &mut Context<Workspace>,
 7629) {
 7630    let docks = workspace
 7631        .all_docks()
 7632        .into_iter()
 7633        .filter_map(|dock| {
 7634            if dock.read(cx).is_open() {
 7635                let dock = dock.read(cx);
 7636                let panel_size = dock.active_panel_size(window, cx)?;
 7637                let dock_pos = dock.position();
 7638                Some((panel_size, dock_pos, px))
 7639            } else {
 7640                None
 7641            }
 7642        })
 7643        .collect::<Vec<_>>();
 7644
 7645    docks
 7646        .into_iter()
 7647        .for_each(|(panel_size, dock_pos, offset)| {
 7648            workspace.adjust_dock_size_by_px(panel_size, dock_pos, offset, window, cx);
 7649        });
 7650}
 7651
 7652impl Focusable for Workspace {
 7653    fn focus_handle(&self, cx: &App) -> FocusHandle {
 7654        self.active_pane.focus_handle(cx)
 7655    }
 7656}
 7657
 7658#[derive(Clone)]
 7659struct DraggedDock(DockPosition);
 7660
 7661impl Render for DraggedDock {
 7662    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
 7663        gpui::Empty
 7664    }
 7665}
 7666
 7667impl Render for Workspace {
 7668    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 7669        static FIRST_PAINT: AtomicBool = AtomicBool::new(true);
 7670        if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) {
 7671            log::info!("Rendered first frame");
 7672        }
 7673
 7674        let centered_layout = self.centered_layout
 7675            && self.center.panes().len() == 1
 7676            && self.active_item(cx).is_some();
 7677        let render_padding = |size| {
 7678            (size > 0.0).then(|| {
 7679                div()
 7680                    .h_full()
 7681                    .w(relative(size))
 7682                    .bg(cx.theme().colors().editor_background)
 7683                    .border_color(cx.theme().colors().pane_group_border)
 7684            })
 7685        };
 7686        let paddings = if centered_layout {
 7687            let settings = WorkspaceSettings::get_global(cx).centered_layout;
 7688            (
 7689                render_padding(Self::adjust_padding(
 7690                    settings.left_padding.map(|padding| padding.0),
 7691                )),
 7692                render_padding(Self::adjust_padding(
 7693                    settings.right_padding.map(|padding| padding.0),
 7694                )),
 7695            )
 7696        } else {
 7697            (None, None)
 7698        };
 7699        let ui_font = theme::setup_ui_font(window, cx);
 7700
 7701        let theme = cx.theme().clone();
 7702        let colors = theme.colors();
 7703        let notification_entities = self
 7704            .notifications
 7705            .iter()
 7706            .map(|(_, notification)| notification.entity_id())
 7707            .collect::<Vec<_>>();
 7708        let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
 7709
 7710        div()
 7711            .relative()
 7712            .size_full()
 7713            .flex()
 7714            .flex_col()
 7715            .font(ui_font)
 7716            .gap_0()
 7717                .justify_start()
 7718                .items_start()
 7719                .text_color(colors.text)
 7720                .overflow_hidden()
 7721                .children(self.titlebar_item.clone())
 7722                .on_modifiers_changed(move |_, _, cx| {
 7723                    for &id in &notification_entities {
 7724                        cx.notify(id);
 7725                    }
 7726                })
 7727                .child(
 7728                    div()
 7729                        .size_full()
 7730                        .relative()
 7731                        .flex_1()
 7732                        .flex()
 7733                        .flex_col()
 7734                        .child(
 7735                            div()
 7736                                .id("workspace")
 7737                                .bg(colors.background)
 7738                                .relative()
 7739                                .flex_1()
 7740                                .w_full()
 7741                                .flex()
 7742                                .flex_col()
 7743                                .overflow_hidden()
 7744                                .border_t_1()
 7745                                .border_b_1()
 7746                                .border_color(colors.border)
 7747                                .child({
 7748                                    let this = cx.entity();
 7749                                    canvas(
 7750                                        move |bounds, window, cx| {
 7751                                            this.update(cx, |this, cx| {
 7752                                                let bounds_changed = this.bounds != bounds;
 7753                                                this.bounds = bounds;
 7754
 7755                                                if bounds_changed {
 7756                                                    this.left_dock.update(cx, |dock, cx| {
 7757                                                        dock.clamp_panel_size(
 7758                                                            bounds.size.width,
 7759                                                            window,
 7760                                                            cx,
 7761                                                        )
 7762                                                    });
 7763
 7764                                                    this.right_dock.update(cx, |dock, cx| {
 7765                                                        dock.clamp_panel_size(
 7766                                                            bounds.size.width,
 7767                                                            window,
 7768                                                            cx,
 7769                                                        )
 7770                                                    });
 7771
 7772                                                    this.bottom_dock.update(cx, |dock, cx| {
 7773                                                        dock.clamp_panel_size(
 7774                                                            bounds.size.height,
 7775                                                            window,
 7776                                                            cx,
 7777                                                        )
 7778                                                    });
 7779                                                }
 7780                                            })
 7781                                        },
 7782                                        |_, _, _, _| {},
 7783                                    )
 7784                                    .absolute()
 7785                                    .size_full()
 7786                                })
 7787                                .when(self.zoomed.is_none(), |this| {
 7788                                    this.on_drag_move(cx.listener(
 7789                                        move |workspace,
 7790                                              e: &DragMoveEvent<DraggedDock>,
 7791                                              window,
 7792                                              cx| {
 7793                                            if workspace.previous_dock_drag_coordinates
 7794                                                != Some(e.event.position)
 7795                                            {
 7796                                                workspace.previous_dock_drag_coordinates =
 7797                                                    Some(e.event.position);
 7798
 7799                                                match e.drag(cx).0 {
 7800                                                    DockPosition::Left => {
 7801                                                        workspace.resize_left_dock(
 7802                                                            e.event.position.x
 7803                                                                - workspace.bounds.left(),
 7804                                                            window,
 7805                                                            cx,
 7806                                                        );
 7807                                                    }
 7808                                                    DockPosition::Right => {
 7809                                                        workspace.resize_right_dock(
 7810                                                            workspace.bounds.right()
 7811                                                                - e.event.position.x,
 7812                                                            window,
 7813                                                            cx,
 7814                                                        );
 7815                                                    }
 7816                                                    DockPosition::Bottom => {
 7817                                                        workspace.resize_bottom_dock(
 7818                                                            workspace.bounds.bottom()
 7819                                                                - e.event.position.y,
 7820                                                            window,
 7821                                                            cx,
 7822                                                        );
 7823                                                    }
 7824                                                };
 7825                                                workspace.serialize_workspace(window, cx);
 7826                                            }
 7827                                        },
 7828                                    ))
 7829
 7830                                })
 7831                                .child({
 7832                                    match bottom_dock_layout {
 7833                                        BottomDockLayout::Full => div()
 7834                                            .flex()
 7835                                            .flex_col()
 7836                                            .h_full()
 7837                                            .child(
 7838                                                div()
 7839                                                    .flex()
 7840                                                    .flex_row()
 7841                                                    .flex_1()
 7842                                                    .overflow_hidden()
 7843                                                    .children(self.render_dock(
 7844                                                        DockPosition::Left,
 7845                                                        &self.left_dock,
 7846                                                        window,
 7847                                                        cx,
 7848                                                    ))
 7849
 7850                                                    .child(
 7851                                                        div()
 7852                                                            .flex()
 7853                                                            .flex_col()
 7854                                                            .flex_1()
 7855                                                            .overflow_hidden()
 7856                                                            .child(
 7857                                                                h_flex()
 7858                                                                    .flex_1()
 7859                                                                    .when_some(
 7860                                                                        paddings.0,
 7861                                                                        |this, p| {
 7862                                                                            this.child(
 7863                                                                                p.border_r_1(),
 7864                                                                            )
 7865                                                                        },
 7866                                                                    )
 7867                                                                    .child(self.center.render(
 7868                                                                        self.zoomed.as_ref(),
 7869                                                                        &PaneRenderContext {
 7870                                                                            follower_states:
 7871                                                                                &self.follower_states,
 7872                                                                            active_call: self.active_call(),
 7873                                                                            active_pane: &self.active_pane,
 7874                                                                            app_state: &self.app_state,
 7875                                                                            project: &self.project,
 7876                                                                            workspace: &self.weak_self,
 7877                                                                        },
 7878                                                                        window,
 7879                                                                        cx,
 7880                                                                    ))
 7881                                                                    .when_some(
 7882                                                                        paddings.1,
 7883                                                                        |this, p| {
 7884                                                                            this.child(
 7885                                                                                p.border_l_1(),
 7886                                                                            )
 7887                                                                        },
 7888                                                                    ),
 7889                                                            ),
 7890                                                    )
 7891
 7892                                                    .children(self.render_dock(
 7893                                                        DockPosition::Right,
 7894                                                        &self.right_dock,
 7895                                                        window,
 7896                                                        cx,
 7897                                                    )),
 7898                                            )
 7899                                            .child(div().w_full().children(self.render_dock(
 7900                                                DockPosition::Bottom,
 7901                                                &self.bottom_dock,
 7902                                                window,
 7903                                                cx
 7904                                            ))),
 7905
 7906                                        BottomDockLayout::LeftAligned => div()
 7907                                            .flex()
 7908                                            .flex_row()
 7909                                            .h_full()
 7910                                            .child(
 7911                                                div()
 7912                                                    .flex()
 7913                                                    .flex_col()
 7914                                                    .flex_1()
 7915                                                    .h_full()
 7916                                                    .child(
 7917                                                        div()
 7918                                                            .flex()
 7919                                                            .flex_row()
 7920                                                            .flex_1()
 7921                                                            .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
 7922
 7923                                                            .child(
 7924                                                                div()
 7925                                                                    .flex()
 7926                                                                    .flex_col()
 7927                                                                    .flex_1()
 7928                                                                    .overflow_hidden()
 7929                                                                    .child(
 7930                                                                        h_flex()
 7931                                                                            .flex_1()
 7932                                                                            .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
 7933                                                                            .child(self.center.render(
 7934                                                                                self.zoomed.as_ref(),
 7935                                                                                &PaneRenderContext {
 7936                                                                                    follower_states:
 7937                                                                                        &self.follower_states,
 7938                                                                                    active_call: self.active_call(),
 7939                                                                                    active_pane: &self.active_pane,
 7940                                                                                    app_state: &self.app_state,
 7941                                                                                    project: &self.project,
 7942                                                                                    workspace: &self.weak_self,
 7943                                                                                },
 7944                                                                                window,
 7945                                                                                cx,
 7946                                                                            ))
 7947                                                                            .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
 7948                                                                    )
 7949                                                            )
 7950
 7951                                                    )
 7952                                                    .child(
 7953                                                        div()
 7954                                                            .w_full()
 7955                                                            .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
 7956                                                    ),
 7957                                            )
 7958                                            .children(self.render_dock(
 7959                                                DockPosition::Right,
 7960                                                &self.right_dock,
 7961                                                window,
 7962                                                cx,
 7963                                            )),
 7964                                        BottomDockLayout::RightAligned => div()
 7965                                            .flex()
 7966                                            .flex_row()
 7967                                            .h_full()
 7968                                            .children(self.render_dock(
 7969                                                DockPosition::Left,
 7970                                                &self.left_dock,
 7971                                                window,
 7972                                                cx,
 7973                                            ))
 7974
 7975                                            .child(
 7976                                                div()
 7977                                                    .flex()
 7978                                                    .flex_col()
 7979                                                    .flex_1()
 7980                                                    .h_full()
 7981                                                    .child(
 7982                                                        div()
 7983                                                            .flex()
 7984                                                            .flex_row()
 7985                                                            .flex_1()
 7986                                                            .child(
 7987                                                                div()
 7988                                                                    .flex()
 7989                                                                    .flex_col()
 7990                                                                    .flex_1()
 7991                                                                    .overflow_hidden()
 7992                                                                    .child(
 7993                                                                        h_flex()
 7994                                                                            .flex_1()
 7995                                                                            .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
 7996                                                                            .child(self.center.render(
 7997                                                                                self.zoomed.as_ref(),
 7998                                                                                &PaneRenderContext {
 7999                                                                                    follower_states:
 8000                                                                                        &self.follower_states,
 8001                                                                                    active_call: self.active_call(),
 8002                                                                                    active_pane: &self.active_pane,
 8003                                                                                    app_state: &self.app_state,
 8004                                                                                    project: &self.project,
 8005                                                                                    workspace: &self.weak_self,
 8006                                                                                },
 8007                                                                                window,
 8008                                                                                cx,
 8009                                                                            ))
 8010                                                                            .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
 8011                                                                    )
 8012                                                            )
 8013
 8014                                                            .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
 8015                                                    )
 8016                                                    .child(
 8017                                                        div()
 8018                                                            .w_full()
 8019                                                            .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
 8020                                                    ),
 8021                                            ),
 8022                                        BottomDockLayout::Contained => div()
 8023                                            .flex()
 8024                                            .flex_row()
 8025                                            .h_full()
 8026                                            .children(self.render_dock(
 8027                                                DockPosition::Left,
 8028                                                &self.left_dock,
 8029                                                window,
 8030                                                cx,
 8031                                            ))
 8032
 8033                                            .child(
 8034                                                div()
 8035                                                    .flex()
 8036                                                    .flex_col()
 8037                                                    .flex_1()
 8038                                                    .overflow_hidden()
 8039                                                    .child(
 8040                                                        h_flex()
 8041                                                            .flex_1()
 8042                                                            .when_some(paddings.0, |this, p| {
 8043                                                                this.child(p.border_r_1())
 8044                                                            })
 8045                                                            .child(self.center.render(
 8046                                                                self.zoomed.as_ref(),
 8047                                                                &PaneRenderContext {
 8048                                                                    follower_states:
 8049                                                                        &self.follower_states,
 8050                                                                    active_call: self.active_call(),
 8051                                                                    active_pane: &self.active_pane,
 8052                                                                    app_state: &self.app_state,
 8053                                                                    project: &self.project,
 8054                                                                    workspace: &self.weak_self,
 8055                                                                },
 8056                                                                window,
 8057                                                                cx,
 8058                                                            ))
 8059                                                            .when_some(paddings.1, |this, p| {
 8060                                                                this.child(p.border_l_1())
 8061                                                            }),
 8062                                                    )
 8063                                                    .children(self.render_dock(
 8064                                                        DockPosition::Bottom,
 8065                                                        &self.bottom_dock,
 8066                                                        window,
 8067                                                        cx,
 8068                                                    )),
 8069                                            )
 8070
 8071                                            .children(self.render_dock(
 8072                                                DockPosition::Right,
 8073                                                &self.right_dock,
 8074                                                window,
 8075                                                cx,
 8076                                            )),
 8077                                    }
 8078                                })
 8079                                .children(self.zoomed.as_ref().and_then(|view| {
 8080                                    let zoomed_view = view.upgrade()?;
 8081                                    let div = div()
 8082                                        .occlude()
 8083                                        .absolute()
 8084                                        .overflow_hidden()
 8085                                        .border_color(colors.border)
 8086                                        .bg(colors.background)
 8087                                        .child(zoomed_view)
 8088                                        .inset_0()
 8089                                        .shadow_lg();
 8090
 8091                                    if !WorkspaceSettings::get_global(cx).zoomed_padding {
 8092                                       return Some(div);
 8093                                    }
 8094
 8095                                    Some(match self.zoomed_position {
 8096                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
 8097                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
 8098                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
 8099                                        None => {
 8100                                            div.top_2().bottom_2().left_2().right_2().border_1()
 8101                                        }
 8102                                    })
 8103                                }))
 8104                                .children(self.render_notifications(window, cx)),
 8105                        )
 8106                        .when(self.status_bar_visible(cx), |parent| {
 8107                            parent.child(self.status_bar.clone())
 8108                        })
 8109                        .child(self.toast_layer.clone()),
 8110                )
 8111    }
 8112}
 8113
 8114impl WorkspaceStore {
 8115    pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
 8116        Self {
 8117            workspaces: Default::default(),
 8118            _subscriptions: vec![
 8119                client.add_request_handler(cx.weak_entity(), Self::handle_follow),
 8120                client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
 8121            ],
 8122            client,
 8123        }
 8124    }
 8125
 8126    pub fn update_followers(
 8127        &self,
 8128        project_id: Option<u64>,
 8129        update: proto::update_followers::Variant,
 8130        cx: &App,
 8131    ) -> Option<()> {
 8132        let active_call = GlobalAnyActiveCall::try_global(cx)?;
 8133        let room_id = active_call.0.room_id(cx)?;
 8134        self.client
 8135            .send(proto::UpdateFollowers {
 8136                room_id,
 8137                project_id,
 8138                variant: Some(update),
 8139            })
 8140            .log_err()
 8141    }
 8142
 8143    pub async fn handle_follow(
 8144        this: Entity<Self>,
 8145        envelope: TypedEnvelope<proto::Follow>,
 8146        mut cx: AsyncApp,
 8147    ) -> Result<proto::FollowResponse> {
 8148        this.update(&mut cx, |this, cx| {
 8149            let follower = Follower {
 8150                project_id: envelope.payload.project_id,
 8151                peer_id: envelope.original_sender_id()?,
 8152            };
 8153
 8154            let mut response = proto::FollowResponse::default();
 8155
 8156            this.workspaces.retain(|(window_handle, weak_workspace)| {
 8157                let Some(workspace) = weak_workspace.upgrade() else {
 8158                    return false;
 8159                };
 8160                window_handle
 8161                    .update(cx, |_, window, cx| {
 8162                        workspace.update(cx, |workspace, cx| {
 8163                            let handler_response =
 8164                                workspace.handle_follow(follower.project_id, window, cx);
 8165                            if let Some(active_view) = handler_response.active_view
 8166                                && workspace.project.read(cx).remote_id() == follower.project_id
 8167                            {
 8168                                response.active_view = Some(active_view)
 8169                            }
 8170                        });
 8171                    })
 8172                    .is_ok()
 8173            });
 8174
 8175            Ok(response)
 8176        })
 8177    }
 8178
 8179    async fn handle_update_followers(
 8180        this: Entity<Self>,
 8181        envelope: TypedEnvelope<proto::UpdateFollowers>,
 8182        mut cx: AsyncApp,
 8183    ) -> Result<()> {
 8184        let leader_id = envelope.original_sender_id()?;
 8185        let update = envelope.payload;
 8186
 8187        this.update(&mut cx, |this, cx| {
 8188            this.workspaces.retain(|(window_handle, weak_workspace)| {
 8189                let Some(workspace) = weak_workspace.upgrade() else {
 8190                    return false;
 8191                };
 8192                window_handle
 8193                    .update(cx, |_, window, cx| {
 8194                        workspace.update(cx, |workspace, cx| {
 8195                            let project_id = workspace.project.read(cx).remote_id();
 8196                            if update.project_id != project_id && update.project_id.is_some() {
 8197                                return;
 8198                            }
 8199                            workspace.handle_update_followers(
 8200                                leader_id,
 8201                                update.clone(),
 8202                                window,
 8203                                cx,
 8204                            );
 8205                        });
 8206                    })
 8207                    .is_ok()
 8208            });
 8209            Ok(())
 8210        })
 8211    }
 8212
 8213    pub fn workspaces(&self) -> impl Iterator<Item = &WeakEntity<Workspace>> {
 8214        self.workspaces.iter().map(|(_, weak)| weak)
 8215    }
 8216
 8217    pub fn workspaces_with_windows(
 8218        &self,
 8219    ) -> impl Iterator<Item = (gpui::AnyWindowHandle, &WeakEntity<Workspace>)> {
 8220        self.workspaces.iter().map(|(window, weak)| (*window, weak))
 8221    }
 8222}
 8223
 8224impl ViewId {
 8225    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
 8226        Ok(Self {
 8227            creator: message
 8228                .creator
 8229                .map(CollaboratorId::PeerId)
 8230                .context("creator is missing")?,
 8231            id: message.id,
 8232        })
 8233    }
 8234
 8235    pub(crate) fn to_proto(self) -> Option<proto::ViewId> {
 8236        if let CollaboratorId::PeerId(peer_id) = self.creator {
 8237            Some(proto::ViewId {
 8238                creator: Some(peer_id),
 8239                id: self.id,
 8240            })
 8241        } else {
 8242            None
 8243        }
 8244    }
 8245}
 8246
 8247impl FollowerState {
 8248    fn pane(&self) -> &Entity<Pane> {
 8249        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
 8250    }
 8251}
 8252
 8253pub trait WorkspaceHandle {
 8254    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
 8255}
 8256
 8257impl WorkspaceHandle for Entity<Workspace> {
 8258    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
 8259        self.read(cx)
 8260            .worktrees(cx)
 8261            .flat_map(|worktree| {
 8262                let worktree_id = worktree.read(cx).id();
 8263                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
 8264                    worktree_id,
 8265                    path: f.path.clone(),
 8266                })
 8267            })
 8268            .collect::<Vec<_>>()
 8269    }
 8270}
 8271
 8272pub async fn last_opened_workspace_location(
 8273    db: &WorkspaceDb,
 8274    fs: &dyn fs::Fs,
 8275) -> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> {
 8276    db.last_workspace(fs)
 8277        .await
 8278        .log_err()
 8279        .flatten()
 8280        .map(|(id, location, paths, _timestamp)| (id, location, paths))
 8281}
 8282
 8283pub async fn last_session_workspace_locations(
 8284    db: &WorkspaceDb,
 8285    last_session_id: &str,
 8286    last_session_window_stack: Option<Vec<WindowId>>,
 8287    fs: &dyn fs::Fs,
 8288) -> Option<Vec<SessionWorkspace>> {
 8289    db.last_session_workspace_locations(last_session_id, last_session_window_stack, fs)
 8290        .await
 8291        .log_err()
 8292}
 8293
 8294pub struct MultiWorkspaceRestoreResult {
 8295    pub window_handle: WindowHandle<MultiWorkspace>,
 8296    pub errors: Vec<anyhow::Error>,
 8297}
 8298
 8299pub async fn restore_multiworkspace(
 8300    multi_workspace: SerializedMultiWorkspace,
 8301    app_state: Arc<AppState>,
 8302    cx: &mut AsyncApp,
 8303) -> anyhow::Result<MultiWorkspaceRestoreResult> {
 8304    let SerializedMultiWorkspace { workspaces, state } = multi_workspace;
 8305    let mut group_iter = workspaces.into_iter();
 8306    let first = group_iter
 8307        .next()
 8308        .context("window group must not be empty")?;
 8309
 8310    let window_handle = if first.paths.is_empty() {
 8311        cx.update(|cx| open_workspace_by_id(first.workspace_id, app_state.clone(), None, cx))
 8312            .await?
 8313    } else {
 8314        let OpenResult { window, .. } = cx
 8315            .update(|cx| {
 8316                Workspace::new_local(
 8317                    first.paths.paths().to_vec(),
 8318                    app_state.clone(),
 8319                    None,
 8320                    None,
 8321                    None,
 8322                    true,
 8323                    cx,
 8324                )
 8325            })
 8326            .await?;
 8327        window
 8328    };
 8329
 8330    let mut errors = Vec::new();
 8331
 8332    for session_workspace in group_iter {
 8333        let error = if session_workspace.paths.is_empty() {
 8334            cx.update(|cx| {
 8335                open_workspace_by_id(
 8336                    session_workspace.workspace_id,
 8337                    app_state.clone(),
 8338                    Some(window_handle),
 8339                    cx,
 8340                )
 8341            })
 8342            .await
 8343            .err()
 8344        } else {
 8345            cx.update(|cx| {
 8346                Workspace::new_local(
 8347                    session_workspace.paths.paths().to_vec(),
 8348                    app_state.clone(),
 8349                    Some(window_handle),
 8350                    None,
 8351                    None,
 8352                    false,
 8353                    cx,
 8354                )
 8355            })
 8356            .await
 8357            .err()
 8358        };
 8359
 8360        if let Some(error) = error {
 8361            errors.push(error);
 8362        }
 8363    }
 8364
 8365    if let Some(target_id) = state.active_workspace_id {
 8366        window_handle
 8367            .update(cx, |multi_workspace, window, cx| {
 8368                let target_index = multi_workspace
 8369                    .workspaces()
 8370                    .iter()
 8371                    .position(|ws| ws.read(cx).database_id() == Some(target_id));
 8372                if let Some(index) = target_index {
 8373                    multi_workspace.activate_index(index, window, cx);
 8374                } else if !multi_workspace.workspaces().is_empty() {
 8375                    multi_workspace.activate_index(0, window, cx);
 8376                }
 8377            })
 8378            .ok();
 8379    } else {
 8380        window_handle
 8381            .update(cx, |multi_workspace, window, cx| {
 8382                if !multi_workspace.workspaces().is_empty() {
 8383                    multi_workspace.activate_index(0, window, cx);
 8384                }
 8385            })
 8386            .ok();
 8387    }
 8388
 8389    if state.sidebar_open {
 8390        window_handle
 8391            .update(cx, |multi_workspace, _, cx| {
 8392                multi_workspace.open_sidebar(cx);
 8393            })
 8394            .ok();
 8395    }
 8396
 8397    window_handle
 8398        .update(cx, |_, window, _cx| {
 8399            window.activate_window();
 8400        })
 8401        .ok();
 8402
 8403    Ok(MultiWorkspaceRestoreResult {
 8404        window_handle,
 8405        errors,
 8406    })
 8407}
 8408
 8409actions!(
 8410    collab,
 8411    [
 8412        /// Opens the channel notes for the current call.
 8413        ///
 8414        /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected
 8415        /// channel in the collab panel.
 8416        ///
 8417        /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL -
 8418        /// can be copied via "Copy link to section" in the context menu of the channel notes
 8419        /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`.
 8420        OpenChannelNotes,
 8421        /// Mutes your microphone.
 8422        Mute,
 8423        /// Deafens yourself (mute both microphone and speakers).
 8424        Deafen,
 8425        /// Leaves the current call.
 8426        LeaveCall,
 8427        /// Shares the current project with collaborators.
 8428        ShareProject,
 8429        /// Shares your screen with collaborators.
 8430        ScreenShare,
 8431        /// Copies the current room name and session id for debugging purposes.
 8432        CopyRoomId,
 8433    ]
 8434);
 8435
 8436/// Opens the channel notes for a specific channel by its ID.
 8437#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
 8438#[action(namespace = collab)]
 8439#[serde(deny_unknown_fields)]
 8440pub struct OpenChannelNotesById {
 8441    pub channel_id: u64,
 8442}
 8443
 8444actions!(
 8445    zed,
 8446    [
 8447        /// Opens the Zed log file.
 8448        OpenLog,
 8449        /// Reveals the Zed log file in the system file manager.
 8450        RevealLogInFileManager
 8451    ]
 8452);
 8453
 8454async fn join_channel_internal(
 8455    channel_id: ChannelId,
 8456    app_state: &Arc<AppState>,
 8457    requesting_window: Option<WindowHandle<MultiWorkspace>>,
 8458    requesting_workspace: Option<WeakEntity<Workspace>>,
 8459    active_call: &dyn AnyActiveCall,
 8460    cx: &mut AsyncApp,
 8461) -> Result<bool> {
 8462    let (should_prompt, already_in_channel) = cx.update(|cx| {
 8463        if !active_call.is_in_room(cx) {
 8464            return (false, false);
 8465        }
 8466
 8467        let already_in_channel = active_call.channel_id(cx) == Some(channel_id);
 8468        let should_prompt = active_call.is_sharing_project(cx)
 8469            && active_call.has_remote_participants(cx)
 8470            && !already_in_channel;
 8471        (should_prompt, already_in_channel)
 8472    });
 8473
 8474    if already_in_channel {
 8475        let task = cx.update(|cx| {
 8476            if let Some((project, host)) = active_call.most_active_project(cx) {
 8477                Some(join_in_room_project(project, host, app_state.clone(), cx))
 8478            } else {
 8479                None
 8480            }
 8481        });
 8482        if let Some(task) = task {
 8483            task.await?;
 8484        }
 8485        return anyhow::Ok(true);
 8486    }
 8487
 8488    if should_prompt {
 8489        if let Some(multi_workspace) = requesting_window {
 8490            let answer = multi_workspace
 8491                .update(cx, |_, window, cx| {
 8492                    window.prompt(
 8493                        PromptLevel::Warning,
 8494                        "Do you want to switch channels?",
 8495                        Some("Leaving this call will unshare your current project."),
 8496                        &["Yes, Join Channel", "Cancel"],
 8497                        cx,
 8498                    )
 8499                })?
 8500                .await;
 8501
 8502            if answer == Ok(1) {
 8503                return Ok(false);
 8504            }
 8505        } else {
 8506            return Ok(false);
 8507        }
 8508    }
 8509
 8510    let client = cx.update(|cx| active_call.client(cx));
 8511
 8512    let mut client_status = client.status();
 8513
 8514    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
 8515    'outer: loop {
 8516        let Some(status) = client_status.recv().await else {
 8517            anyhow::bail!("error connecting");
 8518        };
 8519
 8520        match status {
 8521            Status::Connecting
 8522            | Status::Authenticating
 8523            | Status::Authenticated
 8524            | Status::Reconnecting
 8525            | Status::Reauthenticating
 8526            | Status::Reauthenticated => continue,
 8527            Status::Connected { .. } => break 'outer,
 8528            Status::SignedOut | Status::AuthenticationError => {
 8529                return Err(ErrorCode::SignedOut.into());
 8530            }
 8531            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
 8532            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
 8533                return Err(ErrorCode::Disconnected.into());
 8534            }
 8535        }
 8536    }
 8537
 8538    let joined = cx
 8539        .update(|cx| active_call.join_channel(channel_id, cx))
 8540        .await?;
 8541
 8542    if !joined {
 8543        return anyhow::Ok(true);
 8544    }
 8545
 8546    cx.update(|cx| active_call.room_update_completed(cx)).await;
 8547
 8548    let task = cx.update(|cx| {
 8549        if let Some((project, host)) = active_call.most_active_project(cx) {
 8550            return Some(join_in_room_project(project, host, app_state.clone(), cx));
 8551        }
 8552
 8553        // If you are the first to join a channel, see if you should share your project.
 8554        if !active_call.has_remote_participants(cx)
 8555            && !active_call.local_participant_is_guest(cx)
 8556            && let Some(workspace) = requesting_workspace.as_ref().and_then(|w| w.upgrade())
 8557        {
 8558            let project = workspace.update(cx, |workspace, cx| {
 8559                let project = workspace.project.read(cx);
 8560
 8561                if !active_call.share_on_join(cx) {
 8562                    return None;
 8563                }
 8564
 8565                if (project.is_local() || project.is_via_remote_server())
 8566                    && project.visible_worktrees(cx).any(|tree| {
 8567                        tree.read(cx)
 8568                            .root_entry()
 8569                            .is_some_and(|entry| entry.is_dir())
 8570                    })
 8571                {
 8572                    Some(workspace.project.clone())
 8573                } else {
 8574                    None
 8575                }
 8576            });
 8577            if let Some(project) = project {
 8578                let share_task = active_call.share_project(project, cx);
 8579                return Some(cx.spawn(async move |_cx| -> Result<()> {
 8580                    share_task.await?;
 8581                    Ok(())
 8582                }));
 8583            }
 8584        }
 8585
 8586        None
 8587    });
 8588    if let Some(task) = task {
 8589        task.await?;
 8590        return anyhow::Ok(true);
 8591    }
 8592    anyhow::Ok(false)
 8593}
 8594
 8595pub fn join_channel(
 8596    channel_id: ChannelId,
 8597    app_state: Arc<AppState>,
 8598    requesting_window: Option<WindowHandle<MultiWorkspace>>,
 8599    requesting_workspace: Option<WeakEntity<Workspace>>,
 8600    cx: &mut App,
 8601) -> Task<Result<()>> {
 8602    let active_call = GlobalAnyActiveCall::global(cx).clone();
 8603    cx.spawn(async move |cx| {
 8604        let result = join_channel_internal(
 8605            channel_id,
 8606            &app_state,
 8607            requesting_window,
 8608            requesting_workspace,
 8609            &*active_call.0,
 8610            cx,
 8611        )
 8612        .await;
 8613
 8614        // join channel succeeded, and opened a window
 8615        if matches!(result, Ok(true)) {
 8616            return anyhow::Ok(());
 8617        }
 8618
 8619        // find an existing workspace to focus and show call controls
 8620        let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx));
 8621        if active_window.is_none() {
 8622            // no open workspaces, make one to show the error in (blergh)
 8623            let OpenResult {
 8624                window: window_handle,
 8625                ..
 8626            } = cx
 8627                .update(|cx| {
 8628                    Workspace::new_local(
 8629                        vec![],
 8630                        app_state.clone(),
 8631                        requesting_window,
 8632                        None,
 8633                        None,
 8634                        true,
 8635                        cx,
 8636                    )
 8637                })
 8638                .await?;
 8639
 8640            window_handle
 8641                .update(cx, |_, window, _cx| {
 8642                    window.activate_window();
 8643                })
 8644                .ok();
 8645
 8646            if result.is_ok() {
 8647                cx.update(|cx| {
 8648                    cx.dispatch_action(&OpenChannelNotes);
 8649                });
 8650            }
 8651
 8652            active_window = Some(window_handle);
 8653        }
 8654
 8655        if let Err(err) = result {
 8656            log::error!("failed to join channel: {}", err);
 8657            if let Some(active_window) = active_window {
 8658                active_window
 8659                    .update(cx, |_, window, cx| {
 8660                        let detail: SharedString = match err.error_code() {
 8661                            ErrorCode::SignedOut => "Please sign in to continue.".into(),
 8662                            ErrorCode::UpgradeRequired => concat!(
 8663                                "Your are running an unsupported version of Zed. ",
 8664                                "Please update to continue."
 8665                            )
 8666                            .into(),
 8667                            ErrorCode::NoSuchChannel => concat!(
 8668                                "No matching channel was found. ",
 8669                                "Please check the link and try again."
 8670                            )
 8671                            .into(),
 8672                            ErrorCode::Forbidden => concat!(
 8673                                "This channel is private, and you do not have access. ",
 8674                                "Please ask someone to add you and try again."
 8675                            )
 8676                            .into(),
 8677                            ErrorCode::Disconnected => {
 8678                                "Please check your internet connection and try again.".into()
 8679                            }
 8680                            _ => format!("{}\n\nPlease try again.", err).into(),
 8681                        };
 8682                        window.prompt(
 8683                            PromptLevel::Critical,
 8684                            "Failed to join channel",
 8685                            Some(&detail),
 8686                            &["Ok"],
 8687                            cx,
 8688                        )
 8689                    })?
 8690                    .await
 8691                    .ok();
 8692            }
 8693        }
 8694
 8695        // return ok, we showed the error to the user.
 8696        anyhow::Ok(())
 8697    })
 8698}
 8699
 8700pub async fn get_any_active_multi_workspace(
 8701    app_state: Arc<AppState>,
 8702    mut cx: AsyncApp,
 8703) -> anyhow::Result<WindowHandle<MultiWorkspace>> {
 8704    // find an existing workspace to focus and show call controls
 8705    let active_window = activate_any_workspace_window(&mut cx);
 8706    if active_window.is_none() {
 8707        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, None, true, cx))
 8708            .await?;
 8709    }
 8710    activate_any_workspace_window(&mut cx).context("could not open zed")
 8711}
 8712
 8713fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
 8714    cx.update(|cx| {
 8715        if let Some(workspace_window) = cx
 8716            .active_window()
 8717            .and_then(|window| window.downcast::<MultiWorkspace>())
 8718        {
 8719            return Some(workspace_window);
 8720        }
 8721
 8722        for window in cx.windows() {
 8723            if let Some(workspace_window) = window.downcast::<MultiWorkspace>() {
 8724                workspace_window
 8725                    .update(cx, |_, window, _| window.activate_window())
 8726                    .ok();
 8727                return Some(workspace_window);
 8728            }
 8729        }
 8730        None
 8731    })
 8732}
 8733
 8734pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
 8735    workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
 8736}
 8737
 8738pub fn workspace_windows_for_location(
 8739    serialized_location: &SerializedWorkspaceLocation,
 8740    cx: &App,
 8741) -> Vec<WindowHandle<MultiWorkspace>> {
 8742    cx.windows()
 8743        .into_iter()
 8744        .filter_map(|window| window.downcast::<MultiWorkspace>())
 8745        .filter(|multi_workspace| {
 8746            let same_host = |left: &RemoteConnectionOptions, right: &RemoteConnectionOptions| match (left, right) {
 8747                (RemoteConnectionOptions::Ssh(a), RemoteConnectionOptions::Ssh(b)) => {
 8748                    (&a.host, &a.username, &a.port) == (&b.host, &b.username, &b.port)
 8749                }
 8750                (RemoteConnectionOptions::Wsl(a), RemoteConnectionOptions::Wsl(b)) => {
 8751                    // The WSL username is not consistently populated in the workspace location, so ignore it for now.
 8752                    a.distro_name == b.distro_name
 8753                }
 8754                (RemoteConnectionOptions::Docker(a), RemoteConnectionOptions::Docker(b)) => {
 8755                    a.container_id == b.container_id
 8756                }
 8757                #[cfg(any(test, feature = "test-support"))]
 8758                (RemoteConnectionOptions::Mock(a), RemoteConnectionOptions::Mock(b)) => {
 8759                    a.id == b.id
 8760                }
 8761                _ => false,
 8762            };
 8763
 8764            multi_workspace.read(cx).is_ok_and(|multi_workspace| {
 8765                multi_workspace.workspaces().iter().any(|workspace| {
 8766                    match workspace.read(cx).workspace_location(cx) {
 8767                        WorkspaceLocation::Location(location, _) => {
 8768                            match (&location, serialized_location) {
 8769                                (
 8770                                    SerializedWorkspaceLocation::Local,
 8771                                    SerializedWorkspaceLocation::Local,
 8772                                ) => true,
 8773                                (
 8774                                    SerializedWorkspaceLocation::Remote(a),
 8775                                    SerializedWorkspaceLocation::Remote(b),
 8776                                ) => same_host(a, b),
 8777                                _ => false,
 8778                            }
 8779                        }
 8780                        _ => false,
 8781                    }
 8782                })
 8783            })
 8784        })
 8785        .collect()
 8786}
 8787
 8788pub async fn find_existing_workspace(
 8789    abs_paths: &[PathBuf],
 8790    open_options: &OpenOptions,
 8791    location: &SerializedWorkspaceLocation,
 8792    cx: &mut AsyncApp,
 8793) -> (
 8794    Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)>,
 8795    OpenVisible,
 8796) {
 8797    let mut existing: Option<(WindowHandle<MultiWorkspace>, Entity<Workspace>)> = None;
 8798    let mut open_visible = OpenVisible::All;
 8799    let mut best_match = None;
 8800
 8801    if open_options.open_new_workspace != Some(true) {
 8802        cx.update(|cx| {
 8803            for window in workspace_windows_for_location(location, cx) {
 8804                if let Ok(multi_workspace) = window.read(cx) {
 8805                    for workspace in multi_workspace.workspaces() {
 8806                        let project = workspace.read(cx).project.read(cx);
 8807                        let m = project.visibility_for_paths(
 8808                            abs_paths,
 8809                            open_options.open_new_workspace == None,
 8810                            cx,
 8811                        );
 8812                        if m > best_match {
 8813                            existing = Some((window, workspace.clone()));
 8814                            best_match = m;
 8815                        } else if best_match.is_none()
 8816                            && open_options.open_new_workspace == Some(false)
 8817                        {
 8818                            existing = Some((window, workspace.clone()))
 8819                        }
 8820                    }
 8821                }
 8822            }
 8823        });
 8824
 8825        let all_paths_are_files = existing
 8826            .as_ref()
 8827            .and_then(|(_, target_workspace)| {
 8828                cx.update(|cx| {
 8829                    let workspace = target_workspace.read(cx);
 8830                    let project = workspace.project.read(cx);
 8831                    let path_style = workspace.path_style(cx);
 8832                    Some(!abs_paths.iter().any(|path| {
 8833                        let path = util::paths::SanitizedPath::new(path);
 8834                        project.worktrees(cx).any(|worktree| {
 8835                            let worktree = worktree.read(cx);
 8836                            let abs_path = worktree.abs_path();
 8837                            path_style
 8838                                .strip_prefix(path.as_ref(), abs_path.as_ref())
 8839                                .and_then(|rel| worktree.entry_for_path(&rel))
 8840                                .is_some_and(|e| e.is_dir())
 8841                        })
 8842                    }))
 8843                })
 8844            })
 8845            .unwrap_or(false);
 8846
 8847        if open_options.open_new_workspace.is_none()
 8848            && existing.is_some()
 8849            && open_options.wait
 8850            && all_paths_are_files
 8851        {
 8852            cx.update(|cx| {
 8853                let windows = workspace_windows_for_location(location, cx);
 8854                let window = cx
 8855                    .active_window()
 8856                    .and_then(|window| window.downcast::<MultiWorkspace>())
 8857                    .filter(|window| windows.contains(window))
 8858                    .or_else(|| windows.into_iter().next());
 8859                if let Some(window) = window {
 8860                    if let Ok(multi_workspace) = window.read(cx) {
 8861                        let active_workspace = multi_workspace.workspace().clone();
 8862                        existing = Some((window, active_workspace));
 8863                        open_visible = OpenVisible::None;
 8864                    }
 8865                }
 8866            });
 8867        }
 8868    }
 8869    (existing, open_visible)
 8870}
 8871
 8872#[derive(Default, Clone)]
 8873pub struct OpenOptions {
 8874    pub visible: Option<OpenVisible>,
 8875    pub focus: Option<bool>,
 8876    pub open_new_workspace: Option<bool>,
 8877    pub wait: bool,
 8878    pub replace_window: Option<WindowHandle<MultiWorkspace>>,
 8879    pub env: Option<HashMap<String, String>>,
 8880}
 8881
 8882/// The result of opening a workspace via [`open_paths`], [`Workspace::new_local`],
 8883/// or [`Workspace::open_workspace_for_paths`].
 8884pub struct OpenResult {
 8885    pub window: WindowHandle<MultiWorkspace>,
 8886    pub workspace: Entity<Workspace>,
 8887    pub opened_items: Vec<Option<anyhow::Result<Box<dyn ItemHandle>>>>,
 8888}
 8889
 8890/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content.
 8891pub fn open_workspace_by_id(
 8892    workspace_id: WorkspaceId,
 8893    app_state: Arc<AppState>,
 8894    requesting_window: Option<WindowHandle<MultiWorkspace>>,
 8895    cx: &mut App,
 8896) -> Task<anyhow::Result<WindowHandle<MultiWorkspace>>> {
 8897    let project_handle = Project::local(
 8898        app_state.client.clone(),
 8899        app_state.node_runtime.clone(),
 8900        app_state.user_store.clone(),
 8901        app_state.languages.clone(),
 8902        app_state.fs.clone(),
 8903        None,
 8904        project::LocalProjectFlags {
 8905            init_worktree_trust: true,
 8906            ..project::LocalProjectFlags::default()
 8907        },
 8908        cx,
 8909    );
 8910
 8911    let db = WorkspaceDb::global(cx);
 8912    let kvp = db::kvp::KeyValueStore::global(cx);
 8913    cx.spawn(async move |cx| {
 8914        let serialized_workspace = db
 8915            .workspace_for_id(workspace_id)
 8916            .with_context(|| format!("Workspace {workspace_id:?} not found"))?;
 8917
 8918        let centered_layout = serialized_workspace.centered_layout;
 8919
 8920        let (window, workspace) = if let Some(window) = requesting_window {
 8921            let workspace = window.update(cx, |multi_workspace, window, cx| {
 8922                let workspace = cx.new(|cx| {
 8923                    let mut workspace = Workspace::new(
 8924                        Some(workspace_id),
 8925                        project_handle.clone(),
 8926                        app_state.clone(),
 8927                        window,
 8928                        cx,
 8929                    );
 8930                    workspace.centered_layout = centered_layout;
 8931                    workspace
 8932                });
 8933                multi_workspace.add_workspace(workspace.clone(), cx);
 8934                workspace
 8935            })?;
 8936            (window, workspace)
 8937        } else {
 8938            let window_bounds_override = window_bounds_env_override();
 8939
 8940            let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
 8941                (Some(WindowBounds::Windowed(bounds)), None)
 8942            } else if let Some(display) = serialized_workspace.display
 8943                && let Some(bounds) = serialized_workspace.window_bounds.as_ref()
 8944            {
 8945                (Some(bounds.0), Some(display))
 8946            } else if let Some((display, bounds)) = persistence::read_default_window_bounds(&kvp) {
 8947                (Some(bounds), Some(display))
 8948            } else {
 8949                (None, None)
 8950            };
 8951
 8952            let options = cx.update(|cx| {
 8953                let mut options = (app_state.build_window_options)(display, cx);
 8954                options.window_bounds = window_bounds;
 8955                options
 8956            });
 8957
 8958            let window = cx.open_window(options, {
 8959                let app_state = app_state.clone();
 8960                let project_handle = project_handle.clone();
 8961                move |window, cx| {
 8962                    let workspace = cx.new(|cx| {
 8963                        let mut workspace = Workspace::new(
 8964                            Some(workspace_id),
 8965                            project_handle,
 8966                            app_state,
 8967                            window,
 8968                            cx,
 8969                        );
 8970                        workspace.centered_layout = centered_layout;
 8971                        workspace
 8972                    });
 8973                    cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
 8974                }
 8975            })?;
 8976
 8977            let workspace = window.update(cx, |multi_workspace: &mut MultiWorkspace, _, _cx| {
 8978                multi_workspace.workspace().clone()
 8979            })?;
 8980
 8981            (window, workspace)
 8982        };
 8983
 8984        notify_if_database_failed(window, cx);
 8985
 8986        // Restore items from the serialized workspace
 8987        window
 8988            .update(cx, |_, window, cx| {
 8989                workspace.update(cx, |_workspace, cx| {
 8990                    open_items(Some(serialized_workspace), vec![], window, cx)
 8991                })
 8992            })?
 8993            .await?;
 8994
 8995        window.update(cx, |_, window, cx| {
 8996            workspace.update(cx, |workspace, cx| {
 8997                workspace.serialize_workspace(window, cx);
 8998            });
 8999        })?;
 9000
 9001        Ok(window)
 9002    })
 9003}
 9004
 9005#[allow(clippy::type_complexity)]
 9006pub fn open_paths(
 9007    abs_paths: &[PathBuf],
 9008    app_state: Arc<AppState>,
 9009    open_options: OpenOptions,
 9010    cx: &mut App,
 9011) -> Task<anyhow::Result<OpenResult>> {
 9012    let abs_paths = abs_paths.to_vec();
 9013    #[cfg(target_os = "windows")]
 9014    let wsl_path = abs_paths
 9015        .iter()
 9016        .find_map(|p| util::paths::WslPath::from_path(p));
 9017
 9018    cx.spawn(async move |cx| {
 9019        let (mut existing, mut open_visible) = find_existing_workspace(
 9020            &abs_paths,
 9021            &open_options,
 9022            &SerializedWorkspaceLocation::Local,
 9023            cx,
 9024        )
 9025        .await;
 9026
 9027        // Fallback: if no workspace contains the paths and all paths are files,
 9028        // prefer an existing local workspace window (active window first).
 9029        if open_options.open_new_workspace.is_none() && existing.is_none() {
 9030            let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
 9031            let all_metadatas = futures::future::join_all(all_paths)
 9032                .await
 9033                .into_iter()
 9034                .filter_map(|result| result.ok().flatten())
 9035                .collect::<Vec<_>>();
 9036
 9037            if all_metadatas.iter().all(|file| !file.is_dir) {
 9038                cx.update(|cx| {
 9039                    let windows = workspace_windows_for_location(
 9040                        &SerializedWorkspaceLocation::Local,
 9041                        cx,
 9042                    );
 9043                    let window = cx
 9044                        .active_window()
 9045                        .and_then(|window| window.downcast::<MultiWorkspace>())
 9046                        .filter(|window| windows.contains(window))
 9047                        .or_else(|| windows.into_iter().next());
 9048                    if let Some(window) = window {
 9049                        if let Ok(multi_workspace) = window.read(cx) {
 9050                            let active_workspace = multi_workspace.workspace().clone();
 9051                            existing = Some((window, active_workspace));
 9052                            open_visible = OpenVisible::None;
 9053                        }
 9054                    }
 9055                });
 9056            }
 9057        }
 9058
 9059        let result = if let Some((existing, target_workspace)) = existing {
 9060            let open_task = existing
 9061                .update(cx, |multi_workspace, window, cx| {
 9062                    window.activate_window();
 9063                    multi_workspace.activate(target_workspace.clone(), cx);
 9064                    target_workspace.update(cx, |workspace, cx| {
 9065                        workspace.open_paths(
 9066                            abs_paths,
 9067                            OpenOptions {
 9068                                visible: Some(open_visible),
 9069                                ..Default::default()
 9070                            },
 9071                            None,
 9072                            window,
 9073                            cx,
 9074                        )
 9075                    })
 9076                })?
 9077                .await;
 9078
 9079            _ = existing.update(cx, |multi_workspace, _, cx| {
 9080                let workspace = multi_workspace.workspace().clone();
 9081                workspace.update(cx, |workspace, cx| {
 9082                    for item in open_task.iter().flatten() {
 9083                        if let Err(e) = item {
 9084                            workspace.show_error(&e, cx);
 9085                        }
 9086                    }
 9087                });
 9088            });
 9089
 9090            Ok(OpenResult { window: existing, workspace: target_workspace, opened_items: open_task })
 9091        } else {
 9092            let result = cx
 9093                .update(move |cx| {
 9094                    Workspace::new_local(
 9095                        abs_paths,
 9096                        app_state.clone(),
 9097                        open_options.replace_window,
 9098                        open_options.env,
 9099                        None,
 9100                        true,
 9101                        cx,
 9102                    )
 9103                })
 9104                .await;
 9105
 9106            if let Ok(ref result) = result {
 9107                result.window
 9108                    .update(cx, |_, window, _cx| {
 9109                        window.activate_window();
 9110                    })
 9111                    .log_err();
 9112            }
 9113
 9114            result
 9115        };
 9116
 9117        #[cfg(target_os = "windows")]
 9118        if let Some(util::paths::WslPath{distro, path}) = wsl_path
 9119            && let Ok(ref result) = result
 9120        {
 9121            result.window
 9122                .update(cx, move |multi_workspace, _window, cx| {
 9123                    struct OpenInWsl;
 9124                    let workspace = multi_workspace.workspace().clone();
 9125                    workspace.update(cx, |workspace, cx| {
 9126                        workspace.show_notification(NotificationId::unique::<OpenInWsl>(), cx, move |cx| {
 9127                            let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy());
 9128                            let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote");
 9129                            cx.new(move |cx| {
 9130                                MessageNotification::new(msg, cx)
 9131                                    .primary_message("Open in WSL")
 9132                                    .primary_icon(IconName::FolderOpen)
 9133                                    .primary_on_click(move |window, cx| {
 9134                                        window.dispatch_action(Box::new(remote::OpenWslPath {
 9135                                                distro: remote::WslConnectionOptions {
 9136                                                        distro_name: distro.clone(),
 9137                                                    user: None,
 9138                                                },
 9139                                                paths: vec![path.clone().into()],
 9140                                            }), cx)
 9141                                    })
 9142                            })
 9143                        });
 9144                    });
 9145                })
 9146                .unwrap();
 9147        };
 9148        result
 9149    })
 9150}
 9151
 9152pub fn open_new(
 9153    open_options: OpenOptions,
 9154    app_state: Arc<AppState>,
 9155    cx: &mut App,
 9156    init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
 9157) -> Task<anyhow::Result<()>> {
 9158    let task = Workspace::new_local(
 9159        Vec::new(),
 9160        app_state,
 9161        open_options.replace_window,
 9162        open_options.env,
 9163        Some(Box::new(init)),
 9164        true,
 9165        cx,
 9166    );
 9167    cx.spawn(async move |cx| {
 9168        let OpenResult { window, .. } = task.await?;
 9169        window
 9170            .update(cx, |_, window, _cx| {
 9171                window.activate_window();
 9172            })
 9173            .ok();
 9174        Ok(())
 9175    })
 9176}
 9177
 9178pub fn create_and_open_local_file(
 9179    path: &'static Path,
 9180    window: &mut Window,
 9181    cx: &mut Context<Workspace>,
 9182    default_content: impl 'static + Send + FnOnce() -> Rope,
 9183) -> Task<Result<Box<dyn ItemHandle>>> {
 9184    cx.spawn_in(window, async move |workspace, cx| {
 9185        let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?;
 9186        if !fs.is_file(path).await {
 9187            fs.create_file(path, Default::default()).await?;
 9188            fs.save(path, &default_content(), Default::default())
 9189                .await?;
 9190        }
 9191
 9192        workspace
 9193            .update_in(cx, |workspace, window, cx| {
 9194                workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
 9195                    let path = workspace
 9196                        .project
 9197                        .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
 9198                    cx.spawn_in(window, async move |workspace, cx| {
 9199                        let path = path.await?;
 9200                        let mut items = workspace
 9201                            .update_in(cx, |workspace, window, cx| {
 9202                                workspace.open_paths(
 9203                                    vec![path.to_path_buf()],
 9204                                    OpenOptions {
 9205                                        visible: Some(OpenVisible::None),
 9206                                        ..Default::default()
 9207                                    },
 9208                                    None,
 9209                                    window,
 9210                                    cx,
 9211                                )
 9212                            })?
 9213                            .await;
 9214                        let item = items.pop().flatten();
 9215                        item.with_context(|| format!("path {path:?} is not a file"))?
 9216                    })
 9217                })
 9218            })?
 9219            .await?
 9220            .await
 9221    })
 9222}
 9223
 9224pub fn open_remote_project_with_new_connection(
 9225    window: WindowHandle<MultiWorkspace>,
 9226    remote_connection: Arc<dyn RemoteConnection>,
 9227    cancel_rx: oneshot::Receiver<()>,
 9228    delegate: Arc<dyn RemoteClientDelegate>,
 9229    app_state: Arc<AppState>,
 9230    paths: Vec<PathBuf>,
 9231    cx: &mut App,
 9232) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
 9233    cx.spawn(async move |cx| {
 9234        let (workspace_id, serialized_workspace) =
 9235            deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx)
 9236                .await?;
 9237
 9238        let session = match cx
 9239            .update(|cx| {
 9240                remote::RemoteClient::new(
 9241                    ConnectionIdentifier::Workspace(workspace_id.0),
 9242                    remote_connection,
 9243                    cancel_rx,
 9244                    delegate,
 9245                    cx,
 9246                )
 9247            })
 9248            .await?
 9249        {
 9250            Some(result) => result,
 9251            None => return Ok(Vec::new()),
 9252        };
 9253
 9254        let project = cx.update(|cx| {
 9255            project::Project::remote(
 9256                session,
 9257                app_state.client.clone(),
 9258                app_state.node_runtime.clone(),
 9259                app_state.user_store.clone(),
 9260                app_state.languages.clone(),
 9261                app_state.fs.clone(),
 9262                true,
 9263                cx,
 9264            )
 9265        });
 9266
 9267        open_remote_project_inner(
 9268            project,
 9269            paths,
 9270            workspace_id,
 9271            serialized_workspace,
 9272            app_state,
 9273            window,
 9274            cx,
 9275        )
 9276        .await
 9277    })
 9278}
 9279
 9280pub fn open_remote_project_with_existing_connection(
 9281    connection_options: RemoteConnectionOptions,
 9282    project: Entity<Project>,
 9283    paths: Vec<PathBuf>,
 9284    app_state: Arc<AppState>,
 9285    window: WindowHandle<MultiWorkspace>,
 9286    cx: &mut AsyncApp,
 9287) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
 9288    cx.spawn(async move |cx| {
 9289        let (workspace_id, serialized_workspace) =
 9290            deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
 9291
 9292        open_remote_project_inner(
 9293            project,
 9294            paths,
 9295            workspace_id,
 9296            serialized_workspace,
 9297            app_state,
 9298            window,
 9299            cx,
 9300        )
 9301        .await
 9302    })
 9303}
 9304
 9305async fn open_remote_project_inner(
 9306    project: Entity<Project>,
 9307    paths: Vec<PathBuf>,
 9308    workspace_id: WorkspaceId,
 9309    serialized_workspace: Option<SerializedWorkspace>,
 9310    app_state: Arc<AppState>,
 9311    window: WindowHandle<MultiWorkspace>,
 9312    cx: &mut AsyncApp,
 9313) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
 9314    let db = cx.update(|cx| WorkspaceDb::global(cx));
 9315    let toolchains = db.toolchains(workspace_id).await?;
 9316    for (toolchain, worktree_path, path) in toolchains {
 9317        project
 9318            .update(cx, |this, cx| {
 9319                let Some(worktree_id) =
 9320                    this.find_worktree(&worktree_path, cx)
 9321                        .and_then(|(worktree, rel_path)| {
 9322                            if rel_path.is_empty() {
 9323                                Some(worktree.read(cx).id())
 9324                            } else {
 9325                                None
 9326                            }
 9327                        })
 9328                else {
 9329                    return Task::ready(None);
 9330                };
 9331
 9332                this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
 9333            })
 9334            .await;
 9335    }
 9336    let mut project_paths_to_open = vec![];
 9337    let mut project_path_errors = vec![];
 9338
 9339    for path in paths {
 9340        let result = cx
 9341            .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))
 9342            .await;
 9343        match result {
 9344            Ok((_, project_path)) => {
 9345                project_paths_to_open.push((path.clone(), Some(project_path)));
 9346            }
 9347            Err(error) => {
 9348                project_path_errors.push(error);
 9349            }
 9350        };
 9351    }
 9352
 9353    if project_paths_to_open.is_empty() {
 9354        return Err(project_path_errors.pop().context("no paths given")?);
 9355    }
 9356
 9357    let workspace = window.update(cx, |multi_workspace, window, cx| {
 9358        telemetry::event!("SSH Project Opened");
 9359
 9360        let new_workspace = cx.new(|cx| {
 9361            let mut workspace =
 9362                Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
 9363            workspace.update_history(cx);
 9364
 9365            if let Some(ref serialized) = serialized_workspace {
 9366                workspace.centered_layout = serialized.centered_layout;
 9367            }
 9368
 9369            workspace
 9370        });
 9371
 9372        multi_workspace.activate(new_workspace.clone(), cx);
 9373        new_workspace
 9374    })?;
 9375
 9376    let items = window
 9377        .update(cx, |_, window, cx| {
 9378            window.activate_window();
 9379            workspace.update(cx, |_workspace, cx| {
 9380                open_items(serialized_workspace, project_paths_to_open, window, cx)
 9381            })
 9382        })?
 9383        .await?;
 9384
 9385    workspace.update(cx, |workspace, cx| {
 9386        for error in project_path_errors {
 9387            if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
 9388                if let Some(path) = error.error_tag("path") {
 9389                    workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
 9390                }
 9391            } else {
 9392                workspace.show_error(&error, cx)
 9393            }
 9394        }
 9395    });
 9396
 9397    Ok(items.into_iter().map(|item| item?.ok()).collect())
 9398}
 9399
 9400fn deserialize_remote_project(
 9401    connection_options: RemoteConnectionOptions,
 9402    paths: Vec<PathBuf>,
 9403    cx: &AsyncApp,
 9404) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
 9405    let db = cx.update(|cx| WorkspaceDb::global(cx));
 9406    cx.background_spawn(async move {
 9407        let remote_connection_id = db
 9408            .get_or_create_remote_connection(connection_options)
 9409            .await?;
 9410
 9411        let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
 9412
 9413        let workspace_id = if let Some(workspace_id) =
 9414            serialized_workspace.as_ref().map(|workspace| workspace.id)
 9415        {
 9416            workspace_id
 9417        } else {
 9418            db.next_id().await?
 9419        };
 9420
 9421        Ok((workspace_id, serialized_workspace))
 9422    })
 9423}
 9424
 9425pub fn join_in_room_project(
 9426    project_id: u64,
 9427    follow_user_id: u64,
 9428    app_state: Arc<AppState>,
 9429    cx: &mut App,
 9430) -> Task<Result<()>> {
 9431    let windows = cx.windows();
 9432    cx.spawn(async move |cx| {
 9433        let existing_window_and_workspace: Option<(
 9434            WindowHandle<MultiWorkspace>,
 9435            Entity<Workspace>,
 9436        )> = windows.into_iter().find_map(|window_handle| {
 9437            window_handle
 9438                .downcast::<MultiWorkspace>()
 9439                .and_then(|window_handle| {
 9440                    window_handle
 9441                        .update(cx, |multi_workspace, _window, cx| {
 9442                            for workspace in multi_workspace.workspaces() {
 9443                                if workspace.read(cx).project().read(cx).remote_id()
 9444                                    == Some(project_id)
 9445                                {
 9446                                    return Some((window_handle, workspace.clone()));
 9447                                }
 9448                            }
 9449                            None
 9450                        })
 9451                        .unwrap_or(None)
 9452                })
 9453        });
 9454
 9455        let multi_workspace_window = if let Some((existing_window, target_workspace)) =
 9456            existing_window_and_workspace
 9457        {
 9458            existing_window
 9459                .update(cx, |multi_workspace, _, cx| {
 9460                    multi_workspace.activate(target_workspace, cx);
 9461                })
 9462                .ok();
 9463            existing_window
 9464        } else {
 9465            let active_call = cx.update(|cx| GlobalAnyActiveCall::global(cx).clone());
 9466            let project = cx
 9467                .update(|cx| {
 9468                    active_call.0.join_project(
 9469                        project_id,
 9470                        app_state.languages.clone(),
 9471                        app_state.fs.clone(),
 9472                        cx,
 9473                    )
 9474                })
 9475                .await?;
 9476
 9477            let window_bounds_override = window_bounds_env_override();
 9478            cx.update(|cx| {
 9479                let mut options = (app_state.build_window_options)(None, cx);
 9480                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
 9481                cx.open_window(options, |window, cx| {
 9482                    let workspace = cx.new(|cx| {
 9483                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
 9484                    });
 9485                    cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
 9486                })
 9487            })?
 9488        };
 9489
 9490        multi_workspace_window.update(cx, |multi_workspace, window, cx| {
 9491            cx.activate(true);
 9492            window.activate_window();
 9493
 9494            // We set the active workspace above, so this is the correct workspace.
 9495            let workspace = multi_workspace.workspace().clone();
 9496            workspace.update(cx, |workspace, cx| {
 9497                let follow_peer_id = GlobalAnyActiveCall::try_global(cx)
 9498                    .and_then(|call| call.0.peer_id_for_user_in_room(follow_user_id, cx))
 9499                    .or_else(|| {
 9500                        // If we couldn't follow the given user, follow the host instead.
 9501                        let collaborator = workspace
 9502                            .project()
 9503                            .read(cx)
 9504                            .collaborators()
 9505                            .values()
 9506                            .find(|collaborator| collaborator.is_host)?;
 9507                        Some(collaborator.peer_id)
 9508                    });
 9509
 9510                if let Some(follow_peer_id) = follow_peer_id {
 9511                    workspace.follow(follow_peer_id, window, cx);
 9512                }
 9513            });
 9514        })?;
 9515
 9516        anyhow::Ok(())
 9517    })
 9518}
 9519
 9520pub fn reload(cx: &mut App) {
 9521    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
 9522    let mut workspace_windows = cx
 9523        .windows()
 9524        .into_iter()
 9525        .filter_map(|window| window.downcast::<MultiWorkspace>())
 9526        .collect::<Vec<_>>();
 9527
 9528    // If multiple windows have unsaved changes, and need a save prompt,
 9529    // prompt in the active window before switching to a different window.
 9530    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
 9531
 9532    let mut prompt = None;
 9533    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
 9534        prompt = window
 9535            .update(cx, |_, window, cx| {
 9536                window.prompt(
 9537                    PromptLevel::Info,
 9538                    "Are you sure you want to restart?",
 9539                    None,
 9540                    &["Restart", "Cancel"],
 9541                    cx,
 9542                )
 9543            })
 9544            .ok();
 9545    }
 9546
 9547    cx.spawn(async move |cx| {
 9548        if let Some(prompt) = prompt {
 9549            let answer = prompt.await?;
 9550            if answer != 0 {
 9551                return anyhow::Ok(());
 9552            }
 9553        }
 9554
 9555        // If the user cancels any save prompt, then keep the app open.
 9556        for window in workspace_windows {
 9557            if let Ok(should_close) = window.update(cx, |multi_workspace, window, cx| {
 9558                let workspace = multi_workspace.workspace().clone();
 9559                workspace.update(cx, |workspace, cx| {
 9560                    workspace.prepare_to_close(CloseIntent::Quit, window, cx)
 9561                })
 9562            }) && !should_close.await?
 9563            {
 9564                return anyhow::Ok(());
 9565            }
 9566        }
 9567        cx.update(|cx| cx.restart());
 9568        anyhow::Ok(())
 9569    })
 9570    .detach_and_log_err(cx);
 9571}
 9572
 9573fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
 9574    let mut parts = value.split(',');
 9575    let x: usize = parts.next()?.parse().ok()?;
 9576    let y: usize = parts.next()?.parse().ok()?;
 9577    Some(point(px(x as f32), px(y as f32)))
 9578}
 9579
 9580fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
 9581    let mut parts = value.split(',');
 9582    let width: usize = parts.next()?.parse().ok()?;
 9583    let height: usize = parts.next()?.parse().ok()?;
 9584    Some(size(px(width as f32), px(height as f32)))
 9585}
 9586
 9587/// Add client-side decorations (rounded corners, shadows, resize handling) when
 9588/// appropriate.
 9589///
 9590/// The `border_radius_tiling` parameter allows overriding which corners get
 9591/// rounded, independently of the actual window tiling state. This is used
 9592/// specifically for the workspace switcher sidebar: when the sidebar is open,
 9593/// we want square corners on the left (so the sidebar appears flush with the
 9594/// window edge) but we still need the shadow padding for proper visual
 9595/// appearance. Unlike actual window tiling, this only affects border radius -
 9596/// not padding or shadows.
 9597pub fn client_side_decorations(
 9598    element: impl IntoElement,
 9599    window: &mut Window,
 9600    cx: &mut App,
 9601    border_radius_tiling: Tiling,
 9602) -> Stateful<Div> {
 9603    const BORDER_SIZE: Pixels = px(1.0);
 9604    let decorations = window.window_decorations();
 9605    let tiling = match decorations {
 9606        Decorations::Server => Tiling::default(),
 9607        Decorations::Client { tiling } => tiling,
 9608    };
 9609
 9610    match decorations {
 9611        Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW),
 9612        Decorations::Server => window.set_client_inset(px(0.0)),
 9613    }
 9614
 9615    struct GlobalResizeEdge(ResizeEdge);
 9616    impl Global for GlobalResizeEdge {}
 9617
 9618    div()
 9619        .id("window-backdrop")
 9620        .bg(transparent_black())
 9621        .map(|div| match decorations {
 9622            Decorations::Server => div,
 9623            Decorations::Client { .. } => div
 9624                .when(
 9625                    !(tiling.top
 9626                        || tiling.right
 9627                        || border_radius_tiling.top
 9628                        || border_radius_tiling.right),
 9629                    |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
 9630                )
 9631                .when(
 9632                    !(tiling.top
 9633                        || tiling.left
 9634                        || border_radius_tiling.top
 9635                        || border_radius_tiling.left),
 9636                    |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
 9637                )
 9638                .when(
 9639                    !(tiling.bottom
 9640                        || tiling.right
 9641                        || border_radius_tiling.bottom
 9642                        || border_radius_tiling.right),
 9643                    |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
 9644                )
 9645                .when(
 9646                    !(tiling.bottom
 9647                        || tiling.left
 9648                        || border_radius_tiling.bottom
 9649                        || border_radius_tiling.left),
 9650                    |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
 9651                )
 9652                .when(!tiling.top, |div| {
 9653                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
 9654                })
 9655                .when(!tiling.bottom, |div| {
 9656                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
 9657                })
 9658                .when(!tiling.left, |div| {
 9659                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
 9660                })
 9661                .when(!tiling.right, |div| {
 9662                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
 9663                })
 9664                .on_mouse_move(move |e, window, cx| {
 9665                    let size = window.window_bounds().get_bounds().size;
 9666                    let pos = e.position;
 9667
 9668                    let new_edge =
 9669                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
 9670
 9671                    let edge = cx.try_global::<GlobalResizeEdge>();
 9672                    if new_edge != edge.map(|edge| edge.0) {
 9673                        window
 9674                            .window_handle()
 9675                            .update(cx, |workspace, _, cx| {
 9676                                cx.notify(workspace.entity_id());
 9677                            })
 9678                            .ok();
 9679                    }
 9680                })
 9681                .on_mouse_down(MouseButton::Left, move |e, window, _| {
 9682                    let size = window.window_bounds().get_bounds().size;
 9683                    let pos = e.position;
 9684
 9685                    let edge = match resize_edge(
 9686                        pos,
 9687                        theme::CLIENT_SIDE_DECORATION_SHADOW,
 9688                        size,
 9689                        tiling,
 9690                    ) {
 9691                        Some(value) => value,
 9692                        None => return,
 9693                    };
 9694
 9695                    window.start_window_resize(edge);
 9696                }),
 9697        })
 9698        .size_full()
 9699        .child(
 9700            div()
 9701                .cursor(CursorStyle::Arrow)
 9702                .map(|div| match decorations {
 9703                    Decorations::Server => div,
 9704                    Decorations::Client { .. } => div
 9705                        .border_color(cx.theme().colors().border)
 9706                        .when(
 9707                            !(tiling.top
 9708                                || tiling.right
 9709                                || border_radius_tiling.top
 9710                                || border_radius_tiling.right),
 9711                            |div| div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING),
 9712                        )
 9713                        .when(
 9714                            !(tiling.top
 9715                                || tiling.left
 9716                                || border_radius_tiling.top
 9717                                || border_radius_tiling.left),
 9718                            |div| div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
 9719                        )
 9720                        .when(
 9721                            !(tiling.bottom
 9722                                || tiling.right
 9723                                || border_radius_tiling.bottom
 9724                                || border_radius_tiling.right),
 9725                            |div| div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING),
 9726                        )
 9727                        .when(
 9728                            !(tiling.bottom
 9729                                || tiling.left
 9730                                || border_radius_tiling.bottom
 9731                                || border_radius_tiling.left),
 9732                            |div| div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
 9733                        )
 9734                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
 9735                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
 9736                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
 9737                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
 9738                        .when(!tiling.is_tiled(), |div| {
 9739                            div.shadow(vec![gpui::BoxShadow {
 9740                                color: Hsla {
 9741                                    h: 0.,
 9742                                    s: 0.,
 9743                                    l: 0.,
 9744                                    a: 0.4,
 9745                                },
 9746                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
 9747                                spread_radius: px(0.),
 9748                                offset: point(px(0.0), px(0.0)),
 9749                            }])
 9750                        }),
 9751                })
 9752                .on_mouse_move(|_e, _, cx| {
 9753                    cx.stop_propagation();
 9754                })
 9755                .size_full()
 9756                .child(element),
 9757        )
 9758        .map(|div| match decorations {
 9759            Decorations::Server => div,
 9760            Decorations::Client { tiling, .. } => div.child(
 9761                canvas(
 9762                    |_bounds, window, _| {
 9763                        window.insert_hitbox(
 9764                            Bounds::new(
 9765                                point(px(0.0), px(0.0)),
 9766                                window.window_bounds().get_bounds().size,
 9767                            ),
 9768                            HitboxBehavior::Normal,
 9769                        )
 9770                    },
 9771                    move |_bounds, hitbox, window, cx| {
 9772                        let mouse = window.mouse_position();
 9773                        let size = window.window_bounds().get_bounds().size;
 9774                        let Some(edge) =
 9775                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
 9776                        else {
 9777                            return;
 9778                        };
 9779                        cx.set_global(GlobalResizeEdge(edge));
 9780                        window.set_cursor_style(
 9781                            match edge {
 9782                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
 9783                                ResizeEdge::Left | ResizeEdge::Right => {
 9784                                    CursorStyle::ResizeLeftRight
 9785                                }
 9786                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
 9787                                    CursorStyle::ResizeUpLeftDownRight
 9788                                }
 9789                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
 9790                                    CursorStyle::ResizeUpRightDownLeft
 9791                                }
 9792                            },
 9793                            &hitbox,
 9794                        );
 9795                    },
 9796                )
 9797                .size_full()
 9798                .absolute(),
 9799            ),
 9800        })
 9801}
 9802
 9803fn resize_edge(
 9804    pos: Point<Pixels>,
 9805    shadow_size: Pixels,
 9806    window_size: Size<Pixels>,
 9807    tiling: Tiling,
 9808) -> Option<ResizeEdge> {
 9809    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
 9810    if bounds.contains(&pos) {
 9811        return None;
 9812    }
 9813
 9814    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
 9815    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
 9816    if !tiling.top && top_left_bounds.contains(&pos) {
 9817        return Some(ResizeEdge::TopLeft);
 9818    }
 9819
 9820    let top_right_bounds = Bounds::new(
 9821        Point::new(window_size.width - corner_size.width, px(0.)),
 9822        corner_size,
 9823    );
 9824    if !tiling.top && top_right_bounds.contains(&pos) {
 9825        return Some(ResizeEdge::TopRight);
 9826    }
 9827
 9828    let bottom_left_bounds = Bounds::new(
 9829        Point::new(px(0.), window_size.height - corner_size.height),
 9830        corner_size,
 9831    );
 9832    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
 9833        return Some(ResizeEdge::BottomLeft);
 9834    }
 9835
 9836    let bottom_right_bounds = Bounds::new(
 9837        Point::new(
 9838            window_size.width - corner_size.width,
 9839            window_size.height - corner_size.height,
 9840        ),
 9841        corner_size,
 9842    );
 9843    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
 9844        return Some(ResizeEdge::BottomRight);
 9845    }
 9846
 9847    if !tiling.top && pos.y < shadow_size {
 9848        Some(ResizeEdge::Top)
 9849    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
 9850        Some(ResizeEdge::Bottom)
 9851    } else if !tiling.left && pos.x < shadow_size {
 9852        Some(ResizeEdge::Left)
 9853    } else if !tiling.right && pos.x > window_size.width - shadow_size {
 9854        Some(ResizeEdge::Right)
 9855    } else {
 9856        None
 9857    }
 9858}
 9859
 9860fn join_pane_into_active(
 9861    active_pane: &Entity<Pane>,
 9862    pane: &Entity<Pane>,
 9863    window: &mut Window,
 9864    cx: &mut App,
 9865) {
 9866    if pane == active_pane {
 9867    } else if pane.read(cx).items_len() == 0 {
 9868        pane.update(cx, |_, cx| {
 9869            cx.emit(pane::Event::Remove {
 9870                focus_on_pane: None,
 9871            });
 9872        })
 9873    } else {
 9874        move_all_items(pane, active_pane, window, cx);
 9875    }
 9876}
 9877
 9878fn move_all_items(
 9879    from_pane: &Entity<Pane>,
 9880    to_pane: &Entity<Pane>,
 9881    window: &mut Window,
 9882    cx: &mut App,
 9883) {
 9884    let destination_is_different = from_pane != to_pane;
 9885    let mut moved_items = 0;
 9886    for (item_ix, item_handle) in from_pane
 9887        .read(cx)
 9888        .items()
 9889        .enumerate()
 9890        .map(|(ix, item)| (ix, item.clone()))
 9891        .collect::<Vec<_>>()
 9892    {
 9893        let ix = item_ix - moved_items;
 9894        if destination_is_different {
 9895            // Close item from previous pane
 9896            from_pane.update(cx, |source, cx| {
 9897                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
 9898            });
 9899            moved_items += 1;
 9900        }
 9901
 9902        // This automatically removes duplicate items in the pane
 9903        to_pane.update(cx, |destination, cx| {
 9904            destination.add_item(item_handle, true, true, None, window, cx);
 9905            window.focus(&destination.focus_handle(cx), cx)
 9906        });
 9907    }
 9908}
 9909
 9910pub fn move_item(
 9911    source: &Entity<Pane>,
 9912    destination: &Entity<Pane>,
 9913    item_id_to_move: EntityId,
 9914    destination_index: usize,
 9915    activate: bool,
 9916    window: &mut Window,
 9917    cx: &mut App,
 9918) {
 9919    let Some((item_ix, item_handle)) = source
 9920        .read(cx)
 9921        .items()
 9922        .enumerate()
 9923        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
 9924        .map(|(ix, item)| (ix, item.clone()))
 9925    else {
 9926        // Tab was closed during drag
 9927        return;
 9928    };
 9929
 9930    if source != destination {
 9931        // Close item from previous pane
 9932        source.update(cx, |source, cx| {
 9933            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
 9934        });
 9935    }
 9936
 9937    // This automatically removes duplicate items in the pane
 9938    destination.update(cx, |destination, cx| {
 9939        destination.add_item_inner(
 9940            item_handle,
 9941            activate,
 9942            activate,
 9943            activate,
 9944            Some(destination_index),
 9945            window,
 9946            cx,
 9947        );
 9948        if activate {
 9949            window.focus(&destination.focus_handle(cx), cx)
 9950        }
 9951    });
 9952}
 9953
 9954pub fn move_active_item(
 9955    source: &Entity<Pane>,
 9956    destination: &Entity<Pane>,
 9957    focus_destination: bool,
 9958    close_if_empty: bool,
 9959    window: &mut Window,
 9960    cx: &mut App,
 9961) {
 9962    if source == destination {
 9963        return;
 9964    }
 9965    let Some(active_item) = source.read(cx).active_item() else {
 9966        return;
 9967    };
 9968    source.update(cx, |source_pane, cx| {
 9969        let item_id = active_item.item_id();
 9970        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
 9971        destination.update(cx, |target_pane, cx| {
 9972            target_pane.add_item(
 9973                active_item,
 9974                focus_destination,
 9975                focus_destination,
 9976                Some(target_pane.items_len()),
 9977                window,
 9978                cx,
 9979            );
 9980        });
 9981    });
 9982}
 9983
 9984pub fn clone_active_item(
 9985    workspace_id: Option<WorkspaceId>,
 9986    source: &Entity<Pane>,
 9987    destination: &Entity<Pane>,
 9988    focus_destination: bool,
 9989    window: &mut Window,
 9990    cx: &mut App,
 9991) {
 9992    if source == destination {
 9993        return;
 9994    }
 9995    let Some(active_item) = source.read(cx).active_item() else {
 9996        return;
 9997    };
 9998    if !active_item.can_split(cx) {
 9999        return;
10000    }
10001    let destination = destination.downgrade();
10002    let task = active_item.clone_on_split(workspace_id, window, cx);
10003    window
10004        .spawn(cx, async move |cx| {
10005            let Some(clone) = task.await else {
10006                return;
10007            };
10008            destination
10009                .update_in(cx, |target_pane, window, cx| {
10010                    target_pane.add_item(
10011                        clone,
10012                        focus_destination,
10013                        focus_destination,
10014                        Some(target_pane.items_len()),
10015                        window,
10016                        cx,
10017                    );
10018                })
10019                .log_err();
10020        })
10021        .detach();
10022}
10023
10024#[derive(Debug)]
10025pub struct WorkspacePosition {
10026    pub window_bounds: Option<WindowBounds>,
10027    pub display: Option<Uuid>,
10028    pub centered_layout: bool,
10029}
10030
10031pub fn remote_workspace_position_from_db(
10032    connection_options: RemoteConnectionOptions,
10033    paths_to_open: &[PathBuf],
10034    cx: &App,
10035) -> Task<Result<WorkspacePosition>> {
10036    let paths = paths_to_open.to_vec();
10037    let db = WorkspaceDb::global(cx);
10038    let kvp = db::kvp::KeyValueStore::global(cx);
10039
10040    cx.background_spawn(async move {
10041        let remote_connection_id = db
10042            .get_or_create_remote_connection(connection_options)
10043            .await
10044            .context("fetching serialized ssh project")?;
10045        let serialized_workspace = db.remote_workspace_for_roots(&paths, remote_connection_id);
10046
10047        let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
10048            (Some(WindowBounds::Windowed(bounds)), None)
10049        } else {
10050            let restorable_bounds = serialized_workspace
10051                .as_ref()
10052                .and_then(|workspace| {
10053                    Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
10054                })
10055                .or_else(|| persistence::read_default_window_bounds(&kvp));
10056
10057            if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
10058                (Some(serialized_bounds), Some(serialized_display))
10059            } else {
10060                (None, None)
10061            }
10062        };
10063
10064        let centered_layout = serialized_workspace
10065            .as_ref()
10066            .map(|w| w.centered_layout)
10067            .unwrap_or(false);
10068
10069        Ok(WorkspacePosition {
10070            window_bounds,
10071            display,
10072            centered_layout,
10073        })
10074    })
10075}
10076
10077pub fn with_active_or_new_workspace(
10078    cx: &mut App,
10079    f: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send + 'static,
10080) {
10081    match cx
10082        .active_window()
10083        .and_then(|w| w.downcast::<MultiWorkspace>())
10084    {
10085        Some(multi_workspace) => {
10086            cx.defer(move |cx| {
10087                multi_workspace
10088                    .update(cx, |multi_workspace, window, cx| {
10089                        let workspace = multi_workspace.workspace().clone();
10090                        workspace.update(cx, |workspace, cx| f(workspace, window, cx));
10091                    })
10092                    .log_err();
10093            });
10094        }
10095        None => {
10096            let app_state = AppState::global(cx);
10097            if let Some(app_state) = app_state.upgrade() {
10098                open_new(
10099                    OpenOptions::default(),
10100                    app_state,
10101                    cx,
10102                    move |workspace, window, cx| f(workspace, window, cx),
10103                )
10104                .detach_and_log_err(cx);
10105            }
10106        }
10107    }
10108}
10109
10110#[cfg(test)]
10111mod tests {
10112    use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
10113
10114    use super::*;
10115    use crate::{
10116        dock::{PanelEvent, test::TestPanel},
10117        item::{
10118            ItemBufferKind, ItemEvent,
10119            test::{TestItem, TestProjectItem},
10120        },
10121    };
10122    use fs::FakeFs;
10123    use gpui::{
10124        DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
10125        UpdateGlobal, VisualTestContext, px,
10126    };
10127    use project::{Project, ProjectEntryId};
10128    use serde_json::json;
10129    use settings::SettingsStore;
10130    use util::path;
10131    use util::rel_path::rel_path;
10132
10133    #[gpui::test]
10134    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
10135        init_test(cx);
10136
10137        let fs = FakeFs::new(cx.executor());
10138        let project = Project::test(fs, [], cx).await;
10139        let (workspace, cx) =
10140            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10141
10142        // Adding an item with no ambiguity renders the tab without detail.
10143        let item1 = cx.new(|cx| {
10144            let mut item = TestItem::new(cx);
10145            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
10146            item
10147        });
10148        workspace.update_in(cx, |workspace, window, cx| {
10149            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10150        });
10151        item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
10152
10153        // Adding an item that creates ambiguity increases the level of detail on
10154        // both tabs.
10155        let item2 = cx.new_window_entity(|_window, cx| {
10156            let mut item = TestItem::new(cx);
10157            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10158            item
10159        });
10160        workspace.update_in(cx, |workspace, window, cx| {
10161            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10162        });
10163        item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10164        item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10165
10166        // Adding an item that creates ambiguity increases the level of detail only
10167        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
10168        // we stop at the highest detail available.
10169        let item3 = cx.new(|cx| {
10170            let mut item = TestItem::new(cx);
10171            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
10172            item
10173        });
10174        workspace.update_in(cx, |workspace, window, cx| {
10175            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10176        });
10177        item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
10178        item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10179        item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
10180    }
10181
10182    #[gpui::test]
10183    async fn test_tracking_active_path(cx: &mut TestAppContext) {
10184        init_test(cx);
10185
10186        let fs = FakeFs::new(cx.executor());
10187        fs.insert_tree(
10188            "/root1",
10189            json!({
10190                "one.txt": "",
10191                "two.txt": "",
10192            }),
10193        )
10194        .await;
10195        fs.insert_tree(
10196            "/root2",
10197            json!({
10198                "three.txt": "",
10199            }),
10200        )
10201        .await;
10202
10203        let project = Project::test(fs, ["root1".as_ref()], cx).await;
10204        let (workspace, cx) =
10205            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10206        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10207        let worktree_id = project.update(cx, |project, cx| {
10208            project.worktrees(cx).next().unwrap().read(cx).id()
10209        });
10210
10211        let item1 = cx.new(|cx| {
10212            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
10213        });
10214        let item2 = cx.new(|cx| {
10215            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
10216        });
10217
10218        // Add an item to an empty pane
10219        workspace.update_in(cx, |workspace, window, cx| {
10220            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
10221        });
10222        project.update(cx, |project, cx| {
10223            assert_eq!(
10224                project.active_entry(),
10225                project
10226                    .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10227                    .map(|e| e.id)
10228            );
10229        });
10230        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10231
10232        // Add a second item to a non-empty pane
10233        workspace.update_in(cx, |workspace, window, cx| {
10234            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
10235        });
10236        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
10237        project.update(cx, |project, cx| {
10238            assert_eq!(
10239                project.active_entry(),
10240                project
10241                    .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx)
10242                    .map(|e| e.id)
10243            );
10244        });
10245
10246        // Close the active item
10247        pane.update_in(cx, |pane, window, cx| {
10248            pane.close_active_item(&Default::default(), window, cx)
10249        })
10250        .await
10251        .unwrap();
10252        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
10253        project.update(cx, |project, cx| {
10254            assert_eq!(
10255                project.active_entry(),
10256                project
10257                    .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx)
10258                    .map(|e| e.id)
10259            );
10260        });
10261
10262        // Add a project folder
10263        project
10264            .update(cx, |project, cx| {
10265                project.find_or_create_worktree("root2", true, cx)
10266            })
10267            .await
10268            .unwrap();
10269        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
10270
10271        // Remove a project folder
10272        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
10273        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
10274    }
10275
10276    #[gpui::test]
10277    async fn test_close_window(cx: &mut TestAppContext) {
10278        init_test(cx);
10279
10280        let fs = FakeFs::new(cx.executor());
10281        fs.insert_tree("/root", json!({ "one": "" })).await;
10282
10283        let project = Project::test(fs, ["root".as_ref()], cx).await;
10284        let (workspace, cx) =
10285            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10286
10287        // When there are no dirty items, there's nothing to do.
10288        let item1 = cx.new(TestItem::new);
10289        workspace.update_in(cx, |w, window, cx| {
10290            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
10291        });
10292        let task = workspace.update_in(cx, |w, window, cx| {
10293            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10294        });
10295        assert!(task.await.unwrap());
10296
10297        // When there are dirty untitled items, prompt to save each one. If the user
10298        // cancels any prompt, then abort.
10299        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10300        let item3 = cx.new(|cx| {
10301            TestItem::new(cx)
10302                .with_dirty(true)
10303                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10304        });
10305        workspace.update_in(cx, |w, window, cx| {
10306            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10307            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10308        });
10309        let task = workspace.update_in(cx, |w, window, cx| {
10310            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10311        });
10312        cx.executor().run_until_parked();
10313        cx.simulate_prompt_answer("Cancel"); // cancel save all
10314        cx.executor().run_until_parked();
10315        assert!(!cx.has_pending_prompt());
10316        assert!(!task.await.unwrap());
10317    }
10318
10319    #[gpui::test]
10320    async fn test_multi_workspace_close_window_multiple_workspaces_cancel(cx: &mut TestAppContext) {
10321        init_test(cx);
10322
10323        let fs = FakeFs::new(cx.executor());
10324        fs.insert_tree("/root", json!({ "one": "" })).await;
10325
10326        let project_a = Project::test(fs.clone(), ["root".as_ref()], cx).await;
10327        let project_b = Project::test(fs, ["root".as_ref()], cx).await;
10328        let multi_workspace_handle =
10329            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
10330        cx.run_until_parked();
10331
10332        let workspace_a = multi_workspace_handle
10333            .read_with(cx, |mw, _| mw.workspace().clone())
10334            .unwrap();
10335
10336        let workspace_b = multi_workspace_handle
10337            .update(cx, |mw, window, cx| {
10338                mw.test_add_workspace(project_b, window, cx)
10339            })
10340            .unwrap();
10341
10342        // Activate workspace A
10343        multi_workspace_handle
10344            .update(cx, |mw, window, cx| {
10345                mw.activate_index(0, window, cx);
10346            })
10347            .unwrap();
10348
10349        let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
10350
10351        // Workspace A has a clean item
10352        let item_a = cx.new(TestItem::new);
10353        workspace_a.update_in(cx, |w, window, cx| {
10354            w.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
10355        });
10356
10357        // Workspace B has a dirty item
10358        let item_b = cx.new(|cx| TestItem::new(cx).with_dirty(true));
10359        workspace_b.update_in(cx, |w, window, cx| {
10360            w.add_item_to_active_pane(Box::new(item_b.clone()), None, true, window, cx)
10361        });
10362
10363        // Verify workspace A is active
10364        multi_workspace_handle
10365            .read_with(cx, |mw, _| {
10366                assert_eq!(mw.active_workspace_index(), 0);
10367            })
10368            .unwrap();
10369
10370        // Dispatch CloseWindow — workspace A will pass, workspace B will prompt
10371        multi_workspace_handle
10372            .update(cx, |mw, window, cx| {
10373                mw.close_window(&CloseWindow, window, cx);
10374            })
10375            .unwrap();
10376        cx.run_until_parked();
10377
10378        // Workspace B should now be active since it has dirty items that need attention
10379        multi_workspace_handle
10380            .read_with(cx, |mw, _| {
10381                assert_eq!(
10382                    mw.active_workspace_index(),
10383                    1,
10384                    "workspace B should be activated when it prompts"
10385                );
10386            })
10387            .unwrap();
10388
10389        // User cancels the save prompt from workspace B
10390        cx.simulate_prompt_answer("Cancel");
10391        cx.run_until_parked();
10392
10393        // Window should still exist because workspace B's close was cancelled
10394        assert!(
10395            multi_workspace_handle.update(cx, |_, _, _| ()).is_ok(),
10396            "window should still exist after cancelling one workspace's close"
10397        );
10398    }
10399
10400    #[gpui::test]
10401    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
10402        init_test(cx);
10403
10404        // Register TestItem as a serializable item
10405        cx.update(|cx| {
10406            register_serializable_item::<TestItem>(cx);
10407        });
10408
10409        let fs = FakeFs::new(cx.executor());
10410        fs.insert_tree("/root", json!({ "one": "" })).await;
10411
10412        let project = Project::test(fs, ["root".as_ref()], cx).await;
10413        let (workspace, cx) =
10414            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
10415
10416        // When there are dirty untitled items, but they can serialize, then there is no prompt.
10417        let item1 = cx.new(|cx| {
10418            TestItem::new(cx)
10419                .with_dirty(true)
10420                .with_serialize(|| Some(Task::ready(Ok(()))))
10421        });
10422        let item2 = cx.new(|cx| {
10423            TestItem::new(cx)
10424                .with_dirty(true)
10425                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10426                .with_serialize(|| Some(Task::ready(Ok(()))))
10427        });
10428        workspace.update_in(cx, |w, window, cx| {
10429            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10430            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10431        });
10432        let task = workspace.update_in(cx, |w, window, cx| {
10433            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
10434        });
10435        assert!(task.await.unwrap());
10436    }
10437
10438    #[gpui::test]
10439    async fn test_close_pane_items(cx: &mut TestAppContext) {
10440        init_test(cx);
10441
10442        let fs = FakeFs::new(cx.executor());
10443
10444        let project = Project::test(fs, None, cx).await;
10445        let (workspace, cx) =
10446            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10447
10448        let item1 = cx.new(|cx| {
10449            TestItem::new(cx)
10450                .with_dirty(true)
10451                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
10452        });
10453        let item2 = cx.new(|cx| {
10454            TestItem::new(cx)
10455                .with_dirty(true)
10456                .with_conflict(true)
10457                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
10458        });
10459        let item3 = cx.new(|cx| {
10460            TestItem::new(cx)
10461                .with_dirty(true)
10462                .with_conflict(true)
10463                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
10464        });
10465        let item4 = cx.new(|cx| {
10466            TestItem::new(cx).with_dirty(true).with_project_items(&[{
10467                let project_item = TestProjectItem::new_untitled(cx);
10468                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10469                project_item
10470            }])
10471        });
10472        let pane = workspace.update_in(cx, |workspace, window, cx| {
10473            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
10474            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
10475            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
10476            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
10477            workspace.active_pane().clone()
10478        });
10479
10480        let close_items = pane.update_in(cx, |pane, window, cx| {
10481            pane.activate_item(1, true, true, window, cx);
10482            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
10483            let item1_id = item1.item_id();
10484            let item3_id = item3.item_id();
10485            let item4_id = item4.item_id();
10486            pane.close_items(window, cx, SaveIntent::Close, &move |id| {
10487                [item1_id, item3_id, item4_id].contains(&id)
10488            })
10489        });
10490        cx.executor().run_until_parked();
10491
10492        assert!(cx.has_pending_prompt());
10493        cx.simulate_prompt_answer("Save all");
10494
10495        cx.executor().run_until_parked();
10496
10497        // Item 1 is saved. There's a prompt to save item 3.
10498        pane.update(cx, |pane, cx| {
10499            assert_eq!(item1.read(cx).save_count, 1);
10500            assert_eq!(item1.read(cx).save_as_count, 0);
10501            assert_eq!(item1.read(cx).reload_count, 0);
10502            assert_eq!(pane.items_len(), 3);
10503            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
10504        });
10505        assert!(cx.has_pending_prompt());
10506
10507        // Cancel saving item 3.
10508        cx.simulate_prompt_answer("Discard");
10509        cx.executor().run_until_parked();
10510
10511        // Item 3 is reloaded. There's a prompt to save item 4.
10512        pane.update(cx, |pane, cx| {
10513            assert_eq!(item3.read(cx).save_count, 0);
10514            assert_eq!(item3.read(cx).save_as_count, 0);
10515            assert_eq!(item3.read(cx).reload_count, 1);
10516            assert_eq!(pane.items_len(), 2);
10517            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
10518        });
10519
10520        // There's a prompt for a path for item 4.
10521        cx.simulate_new_path_selection(|_| Some(Default::default()));
10522        close_items.await.unwrap();
10523
10524        // The requested items are closed.
10525        pane.update(cx, |pane, cx| {
10526            assert_eq!(item4.read(cx).save_count, 0);
10527            assert_eq!(item4.read(cx).save_as_count, 1);
10528            assert_eq!(item4.read(cx).reload_count, 0);
10529            assert_eq!(pane.items_len(), 1);
10530            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
10531        });
10532    }
10533
10534    #[gpui::test]
10535    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
10536        init_test(cx);
10537
10538        let fs = FakeFs::new(cx.executor());
10539        let project = Project::test(fs, [], cx).await;
10540        let (workspace, cx) =
10541            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10542
10543        // Create several workspace items with single project entries, and two
10544        // workspace items with multiple project entries.
10545        let single_entry_items = (0..=4)
10546            .map(|project_entry_id| {
10547                cx.new(|cx| {
10548                    TestItem::new(cx)
10549                        .with_dirty(true)
10550                        .with_project_items(&[dirty_project_item(
10551                            project_entry_id,
10552                            &format!("{project_entry_id}.txt"),
10553                            cx,
10554                        )])
10555                })
10556            })
10557            .collect::<Vec<_>>();
10558        let item_2_3 = cx.new(|cx| {
10559            TestItem::new(cx)
10560                .with_dirty(true)
10561                .with_buffer_kind(ItemBufferKind::Multibuffer)
10562                .with_project_items(&[
10563                    single_entry_items[2].read(cx).project_items[0].clone(),
10564                    single_entry_items[3].read(cx).project_items[0].clone(),
10565                ])
10566        });
10567        let item_3_4 = cx.new(|cx| {
10568            TestItem::new(cx)
10569                .with_dirty(true)
10570                .with_buffer_kind(ItemBufferKind::Multibuffer)
10571                .with_project_items(&[
10572                    single_entry_items[3].read(cx).project_items[0].clone(),
10573                    single_entry_items[4].read(cx).project_items[0].clone(),
10574                ])
10575        });
10576
10577        // Create two panes that contain the following project entries:
10578        //   left pane:
10579        //     multi-entry items:   (2, 3)
10580        //     single-entry items:  0, 2, 3, 4
10581        //   right pane:
10582        //     single-entry items:  4, 1
10583        //     multi-entry items:   (3, 4)
10584        let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
10585            let left_pane = workspace.active_pane().clone();
10586            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
10587            workspace.add_item_to_active_pane(
10588                single_entry_items[0].boxed_clone(),
10589                None,
10590                true,
10591                window,
10592                cx,
10593            );
10594            workspace.add_item_to_active_pane(
10595                single_entry_items[2].boxed_clone(),
10596                None,
10597                true,
10598                window,
10599                cx,
10600            );
10601            workspace.add_item_to_active_pane(
10602                single_entry_items[3].boxed_clone(),
10603                None,
10604                true,
10605                window,
10606                cx,
10607            );
10608            workspace.add_item_to_active_pane(
10609                single_entry_items[4].boxed_clone(),
10610                None,
10611                true,
10612                window,
10613                cx,
10614            );
10615
10616            let right_pane =
10617                workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx);
10618
10619            let boxed_clone = single_entry_items[1].boxed_clone();
10620            let right_pane = window.spawn(cx, async move |cx| {
10621                right_pane.await.inspect(|right_pane| {
10622                    right_pane
10623                        .update_in(cx, |pane, window, cx| {
10624                            pane.add_item(boxed_clone, true, true, None, window, cx);
10625                            pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
10626                        })
10627                        .unwrap();
10628                })
10629            });
10630
10631            (left_pane, right_pane)
10632        });
10633        let right_pane = right_pane.await.unwrap();
10634        cx.focus(&right_pane);
10635
10636        let close = right_pane.update_in(cx, |pane, window, cx| {
10637            pane.close_all_items(&CloseAllItems::default(), window, cx)
10638                .unwrap()
10639        });
10640        cx.executor().run_until_parked();
10641
10642        let msg = cx.pending_prompt().unwrap().0;
10643        assert!(msg.contains("1.txt"));
10644        assert!(!msg.contains("2.txt"));
10645        assert!(!msg.contains("3.txt"));
10646        assert!(!msg.contains("4.txt"));
10647
10648        // With best-effort close, cancelling item 1 keeps it open but items 4
10649        // and (3,4) still close since their entries exist in left pane.
10650        cx.simulate_prompt_answer("Cancel");
10651        close.await;
10652
10653        right_pane.read_with(cx, |pane, _| {
10654            assert_eq!(pane.items_len(), 1);
10655        });
10656
10657        // Remove item 3 from left pane, making (2,3) the only item with entry 3.
10658        left_pane
10659            .update_in(cx, |left_pane, window, cx| {
10660                left_pane.close_item_by_id(
10661                    single_entry_items[3].entity_id(),
10662                    SaveIntent::Skip,
10663                    window,
10664                    cx,
10665                )
10666            })
10667            .await
10668            .unwrap();
10669
10670        let close = left_pane.update_in(cx, |pane, window, cx| {
10671            pane.close_all_items(&CloseAllItems::default(), window, cx)
10672                .unwrap()
10673        });
10674        cx.executor().run_until_parked();
10675
10676        let details = cx.pending_prompt().unwrap().1;
10677        assert!(details.contains("0.txt"));
10678        assert!(details.contains("3.txt"));
10679        assert!(details.contains("4.txt"));
10680        // Ideally 2.txt wouldn't appear since entry 2 still exists in item 2.
10681        // But we can only save whole items, so saving (2,3) for entry 3 includes 2.
10682        // assert!(!details.contains("2.txt"));
10683
10684        cx.simulate_prompt_answer("Save all");
10685        cx.executor().run_until_parked();
10686        close.await;
10687
10688        left_pane.read_with(cx, |pane, _| {
10689            assert_eq!(pane.items_len(), 0);
10690        });
10691    }
10692
10693    #[gpui::test]
10694    async fn test_autosave(cx: &mut gpui::TestAppContext) {
10695        init_test(cx);
10696
10697        let fs = FakeFs::new(cx.executor());
10698        let project = Project::test(fs, [], cx).await;
10699        let (workspace, cx) =
10700            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10701        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10702
10703        let item = cx.new(|cx| {
10704            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10705        });
10706        let item_id = item.entity_id();
10707        workspace.update_in(cx, |workspace, window, cx| {
10708            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10709        });
10710
10711        // Autosave on window change.
10712        item.update(cx, |item, cx| {
10713            SettingsStore::update_global(cx, |settings, cx| {
10714                settings.update_user_settings(cx, |settings| {
10715                    settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange);
10716                })
10717            });
10718            item.is_dirty = true;
10719        });
10720
10721        // Deactivating the window saves the file.
10722        cx.deactivate_window();
10723        item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
10724
10725        // Re-activating the window doesn't save the file.
10726        cx.update(|window, _| window.activate_window());
10727        cx.executor().run_until_parked();
10728        item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
10729
10730        // Autosave on focus change.
10731        item.update_in(cx, |item, window, cx| {
10732            cx.focus_self(window);
10733            SettingsStore::update_global(cx, |settings, cx| {
10734                settings.update_user_settings(cx, |settings| {
10735                    settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
10736                })
10737            });
10738            item.is_dirty = true;
10739        });
10740        // Blurring the item saves the file.
10741        item.update_in(cx, |_, window, _| window.blur());
10742        cx.executor().run_until_parked();
10743        item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
10744
10745        // Deactivating the window still saves the file.
10746        item.update_in(cx, |item, window, cx| {
10747            cx.focus_self(window);
10748            item.is_dirty = true;
10749        });
10750        cx.deactivate_window();
10751        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
10752
10753        // Autosave after delay.
10754        item.update(cx, |item, cx| {
10755            SettingsStore::update_global(cx, |settings, cx| {
10756                settings.update_user_settings(cx, |settings| {
10757                    settings.workspace.autosave = Some(AutosaveSetting::AfterDelay {
10758                        milliseconds: 500.into(),
10759                    });
10760                })
10761            });
10762            item.is_dirty = true;
10763            cx.emit(ItemEvent::Edit);
10764        });
10765
10766        // Delay hasn't fully expired, so the file is still dirty and unsaved.
10767        cx.executor().advance_clock(Duration::from_millis(250));
10768        item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
10769
10770        // After delay expires, the file is saved.
10771        cx.executor().advance_clock(Duration::from_millis(250));
10772        item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
10773
10774        // Autosave after delay, should save earlier than delay if tab is closed
10775        item.update(cx, |item, cx| {
10776            item.is_dirty = true;
10777            cx.emit(ItemEvent::Edit);
10778        });
10779        cx.executor().advance_clock(Duration::from_millis(250));
10780        item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
10781
10782        // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out.
10783        pane.update_in(cx, |pane, window, cx| {
10784            pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
10785        })
10786        .await
10787        .unwrap();
10788        assert!(!cx.has_pending_prompt());
10789        item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
10790
10791        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
10792        workspace.update_in(cx, |workspace, window, cx| {
10793            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10794        });
10795        item.update_in(cx, |item, _window, cx| {
10796            item.is_dirty = true;
10797            for project_item in &mut item.project_items {
10798                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10799            }
10800        });
10801        cx.run_until_parked();
10802        item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
10803
10804        // Autosave on focus change, ensuring closing the tab counts as such.
10805        item.update(cx, |item, cx| {
10806            SettingsStore::update_global(cx, |settings, cx| {
10807                settings.update_user_settings(cx, |settings| {
10808                    settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
10809                })
10810            });
10811            item.is_dirty = true;
10812            for project_item in &mut item.project_items {
10813                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
10814            }
10815        });
10816
10817        pane.update_in(cx, |pane, window, cx| {
10818            pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
10819        })
10820        .await
10821        .unwrap();
10822        assert!(!cx.has_pending_prompt());
10823        item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10824
10825        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
10826        workspace.update_in(cx, |workspace, window, cx| {
10827            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10828        });
10829        item.update_in(cx, |item, window, cx| {
10830            item.project_items[0].update(cx, |item, _| {
10831                item.entry_id = None;
10832            });
10833            item.is_dirty = true;
10834            window.blur();
10835        });
10836        cx.run_until_parked();
10837        item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10838
10839        // Ensure autosave is prevented for deleted files also when closing the buffer.
10840        let _close_items = pane.update_in(cx, |pane, window, cx| {
10841            pane.close_items(window, cx, SaveIntent::Close, &move |id| id == item_id)
10842        });
10843        cx.run_until_parked();
10844        assert!(cx.has_pending_prompt());
10845        item.read_with(cx, |item, _| assert_eq!(item.save_count, 6));
10846    }
10847
10848    #[gpui::test]
10849    async fn test_autosave_on_focus_change_in_multibuffer(cx: &mut gpui::TestAppContext) {
10850        init_test(cx);
10851
10852        let fs = FakeFs::new(cx.executor());
10853        let project = Project::test(fs, [], cx).await;
10854        let (workspace, cx) =
10855            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10856
10857        // Create a multibuffer-like item with two child focus handles,
10858        // simulating individual buffer editors within a multibuffer.
10859        let item = cx.new(|cx| {
10860            TestItem::new(cx)
10861                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10862                .with_child_focus_handles(2, cx)
10863        });
10864        workspace.update_in(cx, |workspace, window, cx| {
10865            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10866        });
10867
10868        // Set autosave to OnFocusChange and focus the first child handle,
10869        // simulating the user's cursor being inside one of the multibuffer's excerpts.
10870        item.update_in(cx, |item, window, cx| {
10871            SettingsStore::update_global(cx, |settings, cx| {
10872                settings.update_user_settings(cx, |settings| {
10873                    settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange);
10874                })
10875            });
10876            item.is_dirty = true;
10877            window.focus(&item.child_focus_handles[0], cx);
10878        });
10879        cx.executor().run_until_parked();
10880        item.read_with(cx, |item, _| assert_eq!(item.save_count, 0));
10881
10882        // Moving focus from one child to another within the same item should
10883        // NOT trigger autosave — focus is still within the item's focus hierarchy.
10884        item.update_in(cx, |item, window, cx| {
10885            window.focus(&item.child_focus_handles[1], cx);
10886        });
10887        cx.executor().run_until_parked();
10888        item.read_with(cx, |item, _| {
10889            assert_eq!(
10890                item.save_count, 0,
10891                "Switching focus between children within the same item should not autosave"
10892            );
10893        });
10894
10895        // Blurring the item saves the file. This is the core regression scenario:
10896        // with `on_blur`, this would NOT trigger because `on_blur` only fires when
10897        // the item's own focus handle is the leaf that lost focus. In a multibuffer,
10898        // the leaf is always a child focus handle, so `on_blur` never detected
10899        // focus leaving the item.
10900        item.update_in(cx, |_, window, _| window.blur());
10901        cx.executor().run_until_parked();
10902        item.read_with(cx, |item, _| {
10903            assert_eq!(
10904                item.save_count, 1,
10905                "Blurring should trigger autosave when focus was on a child of the item"
10906            );
10907        });
10908
10909        // Deactivating the window should also trigger autosave when a child of
10910        // the multibuffer item currently owns focus.
10911        item.update_in(cx, |item, window, cx| {
10912            item.is_dirty = true;
10913            window.focus(&item.child_focus_handles[0], cx);
10914        });
10915        cx.executor().run_until_parked();
10916        item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
10917
10918        cx.deactivate_window();
10919        item.read_with(cx, |item, _| {
10920            assert_eq!(
10921                item.save_count, 2,
10922                "Deactivating window should trigger autosave when focus was on a child"
10923            );
10924        });
10925    }
10926
10927    #[gpui::test]
10928    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
10929        init_test(cx);
10930
10931        let fs = FakeFs::new(cx.executor());
10932
10933        let project = Project::test(fs, [], cx).await;
10934        let (workspace, cx) =
10935            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
10936
10937        let item = cx.new(|cx| {
10938            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10939        });
10940        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
10941        let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
10942        let toolbar_notify_count = Rc::new(RefCell::new(0));
10943
10944        workspace.update_in(cx, |workspace, window, cx| {
10945            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
10946            let toolbar_notification_count = toolbar_notify_count.clone();
10947            cx.observe_in(&toolbar, window, move |_, _, _, _| {
10948                *toolbar_notification_count.borrow_mut() += 1
10949            })
10950            .detach();
10951        });
10952
10953        pane.read_with(cx, |pane, _| {
10954            assert!(!pane.can_navigate_backward());
10955            assert!(!pane.can_navigate_forward());
10956        });
10957
10958        item.update_in(cx, |item, _, cx| {
10959            item.set_state("one".to_string(), cx);
10960        });
10961
10962        // Toolbar must be notified to re-render the navigation buttons
10963        assert_eq!(*toolbar_notify_count.borrow(), 1);
10964
10965        pane.read_with(cx, |pane, _| {
10966            assert!(pane.can_navigate_backward());
10967            assert!(!pane.can_navigate_forward());
10968        });
10969
10970        workspace
10971            .update_in(cx, |workspace, window, cx| {
10972                workspace.go_back(pane.downgrade(), window, cx)
10973            })
10974            .await
10975            .unwrap();
10976
10977        assert_eq!(*toolbar_notify_count.borrow(), 2);
10978        pane.read_with(cx, |pane, _| {
10979            assert!(!pane.can_navigate_backward());
10980            assert!(pane.can_navigate_forward());
10981        });
10982    }
10983
10984    #[gpui::test]
10985    async fn test_activate_last_pane(cx: &mut gpui::TestAppContext) {
10986        init_test(cx);
10987        let fs = FakeFs::new(cx.executor());
10988        let project = Project::test(fs, [], cx).await;
10989        let (multi_workspace, cx) =
10990            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
10991        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
10992
10993        workspace.update_in(cx, |workspace, window, cx| {
10994            let first_item = cx.new(|cx| {
10995                TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
10996            });
10997            workspace.add_item_to_active_pane(Box::new(first_item), None, true, window, cx);
10998            workspace.split_pane(
10999                workspace.active_pane().clone(),
11000                SplitDirection::Right,
11001                window,
11002                cx,
11003            );
11004            workspace.split_pane(
11005                workspace.active_pane().clone(),
11006                SplitDirection::Right,
11007                window,
11008                cx,
11009            );
11010        });
11011
11012        let (first_pane_id, target_last_pane_id) = workspace.update(cx, |workspace, _cx| {
11013            let panes = workspace.center.panes();
11014            assert!(panes.len() >= 2);
11015            (
11016                panes.first().expect("at least one pane").entity_id(),
11017                panes.last().expect("at least one pane").entity_id(),
11018            )
11019        });
11020
11021        workspace.update_in(cx, |workspace, window, cx| {
11022            workspace.activate_pane_at_index(&ActivatePane(0), window, cx);
11023        });
11024        workspace.update(cx, |workspace, _| {
11025            assert_eq!(workspace.active_pane().entity_id(), first_pane_id);
11026            assert_ne!(workspace.active_pane().entity_id(), target_last_pane_id);
11027        });
11028
11029        cx.dispatch_action(ActivateLastPane);
11030
11031        workspace.update(cx, |workspace, _| {
11032            assert_eq!(workspace.active_pane().entity_id(), target_last_pane_id);
11033        });
11034    }
11035
11036    #[gpui::test]
11037    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
11038        init_test(cx);
11039        let fs = FakeFs::new(cx.executor());
11040
11041        let project = Project::test(fs, [], cx).await;
11042        let (workspace, cx) =
11043            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11044
11045        let panel = workspace.update_in(cx, |workspace, window, cx| {
11046            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11047            workspace.add_panel(panel.clone(), window, cx);
11048
11049            workspace
11050                .right_dock()
11051                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
11052
11053            panel
11054        });
11055
11056        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11057        pane.update_in(cx, |pane, window, cx| {
11058            let item = cx.new(TestItem::new);
11059            pane.add_item(Box::new(item), true, true, None, window, cx);
11060        });
11061
11062        // Transfer focus from center to panel
11063        workspace.update_in(cx, |workspace, window, cx| {
11064            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11065        });
11066
11067        workspace.update_in(cx, |workspace, window, cx| {
11068            assert!(workspace.right_dock().read(cx).is_open());
11069            assert!(!panel.is_zoomed(window, cx));
11070            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11071        });
11072
11073        // Transfer focus from panel to center
11074        workspace.update_in(cx, |workspace, window, cx| {
11075            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11076        });
11077
11078        workspace.update_in(cx, |workspace, window, cx| {
11079            assert!(workspace.right_dock().read(cx).is_open());
11080            assert!(!panel.is_zoomed(window, cx));
11081            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11082        });
11083
11084        // Close the dock
11085        workspace.update_in(cx, |workspace, window, cx| {
11086            workspace.toggle_dock(DockPosition::Right, window, cx);
11087        });
11088
11089        workspace.update_in(cx, |workspace, window, cx| {
11090            assert!(!workspace.right_dock().read(cx).is_open());
11091            assert!(!panel.is_zoomed(window, cx));
11092            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11093        });
11094
11095        // Open the dock
11096        workspace.update_in(cx, |workspace, window, cx| {
11097            workspace.toggle_dock(DockPosition::Right, window, cx);
11098        });
11099
11100        workspace.update_in(cx, |workspace, window, cx| {
11101            assert!(workspace.right_dock().read(cx).is_open());
11102            assert!(!panel.is_zoomed(window, cx));
11103            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11104        });
11105
11106        // Focus and zoom panel
11107        panel.update_in(cx, |panel, window, cx| {
11108            cx.focus_self(window);
11109            panel.set_zoomed(true, window, cx)
11110        });
11111
11112        workspace.update_in(cx, |workspace, window, cx| {
11113            assert!(workspace.right_dock().read(cx).is_open());
11114            assert!(panel.is_zoomed(window, cx));
11115            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11116        });
11117
11118        // Transfer focus to the center closes the dock
11119        workspace.update_in(cx, |workspace, window, cx| {
11120            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11121        });
11122
11123        workspace.update_in(cx, |workspace, window, cx| {
11124            assert!(!workspace.right_dock().read(cx).is_open());
11125            assert!(panel.is_zoomed(window, cx));
11126            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11127        });
11128
11129        // Transferring focus back to the panel keeps it zoomed
11130        workspace.update_in(cx, |workspace, window, cx| {
11131            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11132        });
11133
11134        workspace.update_in(cx, |workspace, window, cx| {
11135            assert!(workspace.right_dock().read(cx).is_open());
11136            assert!(panel.is_zoomed(window, cx));
11137            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11138        });
11139
11140        // Close the dock while it is zoomed
11141        workspace.update_in(cx, |workspace, window, cx| {
11142            workspace.toggle_dock(DockPosition::Right, window, cx)
11143        });
11144
11145        workspace.update_in(cx, |workspace, window, cx| {
11146            assert!(!workspace.right_dock().read(cx).is_open());
11147            assert!(panel.is_zoomed(window, cx));
11148            assert!(workspace.zoomed.is_none());
11149            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11150        });
11151
11152        // Opening the dock, when it's zoomed, retains focus
11153        workspace.update_in(cx, |workspace, window, cx| {
11154            workspace.toggle_dock(DockPosition::Right, window, cx)
11155        });
11156
11157        workspace.update_in(cx, |workspace, window, cx| {
11158            assert!(workspace.right_dock().read(cx).is_open());
11159            assert!(panel.is_zoomed(window, cx));
11160            assert!(workspace.zoomed.is_some());
11161            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11162        });
11163
11164        // Unzoom and close the panel, zoom the active pane.
11165        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
11166        workspace.update_in(cx, |workspace, window, cx| {
11167            workspace.toggle_dock(DockPosition::Right, window, cx)
11168        });
11169        pane.update_in(cx, |pane, window, cx| {
11170            pane.toggle_zoom(&Default::default(), window, cx)
11171        });
11172
11173        // Opening a dock unzooms the pane.
11174        workspace.update_in(cx, |workspace, window, cx| {
11175            workspace.toggle_dock(DockPosition::Right, window, cx)
11176        });
11177        workspace.update_in(cx, |workspace, window, cx| {
11178            let pane = pane.read(cx);
11179            assert!(!pane.is_zoomed());
11180            assert!(!pane.focus_handle(cx).is_focused(window));
11181            assert!(workspace.right_dock().read(cx).is_open());
11182            assert!(workspace.zoomed.is_none());
11183        });
11184    }
11185
11186    #[gpui::test]
11187    async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) {
11188        init_test(cx);
11189        let fs = FakeFs::new(cx.executor());
11190
11191        let project = Project::test(fs, [], cx).await;
11192        let (workspace, cx) =
11193            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11194
11195        let panel = workspace.update_in(cx, |workspace, window, cx| {
11196            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11197            workspace.add_panel(panel.clone(), window, cx);
11198            panel
11199        });
11200
11201        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11202        pane.update_in(cx, |pane, window, cx| {
11203            let item = cx.new(TestItem::new);
11204            pane.add_item(Box::new(item), true, true, None, window, cx);
11205        });
11206
11207        // Enable close_panel_on_toggle
11208        cx.update_global(|store: &mut SettingsStore, cx| {
11209            store.update_user_settings(cx, |settings| {
11210                settings.workspace.close_panel_on_toggle = Some(true);
11211            });
11212        });
11213
11214        // Panel starts closed. Toggling should open and focus it.
11215        workspace.update_in(cx, |workspace, window, cx| {
11216            assert!(!workspace.right_dock().read(cx).is_open());
11217            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11218        });
11219
11220        workspace.update_in(cx, |workspace, window, cx| {
11221            assert!(
11222                workspace.right_dock().read(cx).is_open(),
11223                "Dock should be open after toggling from center"
11224            );
11225            assert!(
11226                panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11227                "Panel should be focused after toggling from center"
11228            );
11229        });
11230
11231        // Panel is open and focused. Toggling should close the panel and
11232        // return focus to the center.
11233        workspace.update_in(cx, |workspace, window, cx| {
11234            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11235        });
11236
11237        workspace.update_in(cx, |workspace, window, cx| {
11238            assert!(
11239                !workspace.right_dock().read(cx).is_open(),
11240                "Dock should be closed after toggling from focused panel"
11241            );
11242            assert!(
11243                !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11244                "Panel should not be focused after toggling from focused panel"
11245            );
11246        });
11247
11248        // Open the dock and focus something else so the panel is open but not
11249        // focused. Toggling should focus the panel (not close it).
11250        workspace.update_in(cx, |workspace, window, cx| {
11251            workspace
11252                .right_dock()
11253                .update(cx, |dock, cx| dock.set_open(true, window, cx));
11254            window.focus(&pane.read(cx).focus_handle(cx), cx);
11255        });
11256
11257        workspace.update_in(cx, |workspace, window, cx| {
11258            assert!(workspace.right_dock().read(cx).is_open());
11259            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11260            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11261        });
11262
11263        workspace.update_in(cx, |workspace, window, cx| {
11264            assert!(
11265                workspace.right_dock().read(cx).is_open(),
11266                "Dock should remain open when toggling focuses an open-but-unfocused panel"
11267            );
11268            assert!(
11269                panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11270                "Panel should be focused after toggling an open-but-unfocused panel"
11271            );
11272        });
11273
11274        // Now disable the setting and verify the original behavior: toggling
11275        // from a focused panel moves focus to center but leaves the dock open.
11276        cx.update_global(|store: &mut SettingsStore, cx| {
11277            store.update_user_settings(cx, |settings| {
11278                settings.workspace.close_panel_on_toggle = Some(false);
11279            });
11280        });
11281
11282        workspace.update_in(cx, |workspace, window, cx| {
11283            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11284        });
11285
11286        workspace.update_in(cx, |workspace, window, cx| {
11287            assert!(
11288                workspace.right_dock().read(cx).is_open(),
11289                "Dock should remain open when setting is disabled"
11290            );
11291            assert!(
11292                !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11293                "Panel should not be focused after toggling with setting disabled"
11294            );
11295        });
11296    }
11297
11298    #[gpui::test]
11299    async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
11300        init_test(cx);
11301        let fs = FakeFs::new(cx.executor());
11302
11303        let project = Project::test(fs, [], cx).await;
11304        let (workspace, cx) =
11305            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11306
11307        let pane = workspace.update_in(cx, |workspace, _window, _cx| {
11308            workspace.active_pane().clone()
11309        });
11310
11311        // Add an item to the pane so it can be zoomed
11312        workspace.update_in(cx, |workspace, window, cx| {
11313            let item = cx.new(TestItem::new);
11314            workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
11315        });
11316
11317        // Initially not zoomed
11318        workspace.update_in(cx, |workspace, _window, cx| {
11319            assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
11320            assert!(
11321                workspace.zoomed.is_none(),
11322                "Workspace should track no zoomed pane"
11323            );
11324            assert!(pane.read(cx).items_len() > 0, "Pane should have items");
11325        });
11326
11327        // Zoom In
11328        pane.update_in(cx, |pane, window, cx| {
11329            pane.zoom_in(&crate::ZoomIn, window, cx);
11330        });
11331
11332        workspace.update_in(cx, |workspace, window, cx| {
11333            assert!(
11334                pane.read(cx).is_zoomed(),
11335                "Pane should be zoomed after ZoomIn"
11336            );
11337            assert!(
11338                workspace.zoomed.is_some(),
11339                "Workspace should track the zoomed pane"
11340            );
11341            assert!(
11342                pane.read(cx).focus_handle(cx).contains_focused(window, cx),
11343                "ZoomIn should focus the pane"
11344            );
11345        });
11346
11347        // Zoom In again is a no-op
11348        pane.update_in(cx, |pane, window, cx| {
11349            pane.zoom_in(&crate::ZoomIn, window, cx);
11350        });
11351
11352        workspace.update_in(cx, |workspace, window, cx| {
11353            assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
11354            assert!(
11355                workspace.zoomed.is_some(),
11356                "Workspace still tracks zoomed pane"
11357            );
11358            assert!(
11359                pane.read(cx).focus_handle(cx).contains_focused(window, cx),
11360                "Pane remains focused after repeated ZoomIn"
11361            );
11362        });
11363
11364        // Zoom Out
11365        pane.update_in(cx, |pane, window, cx| {
11366            pane.zoom_out(&crate::ZoomOut, window, cx);
11367        });
11368
11369        workspace.update_in(cx, |workspace, _window, cx| {
11370            assert!(
11371                !pane.read(cx).is_zoomed(),
11372                "Pane should unzoom after ZoomOut"
11373            );
11374            assert!(
11375                workspace.zoomed.is_none(),
11376                "Workspace clears zoom tracking after ZoomOut"
11377            );
11378        });
11379
11380        // Zoom Out again is a no-op
11381        pane.update_in(cx, |pane, window, cx| {
11382            pane.zoom_out(&crate::ZoomOut, window, cx);
11383        });
11384
11385        workspace.update_in(cx, |workspace, _window, cx| {
11386            assert!(
11387                !pane.read(cx).is_zoomed(),
11388                "Second ZoomOut keeps pane unzoomed"
11389            );
11390            assert!(
11391                workspace.zoomed.is_none(),
11392                "Workspace remains without zoomed pane"
11393            );
11394        });
11395    }
11396
11397    #[gpui::test]
11398    async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
11399        init_test(cx);
11400        let fs = FakeFs::new(cx.executor());
11401
11402        let project = Project::test(fs, [], cx).await;
11403        let (workspace, cx) =
11404            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11405        workspace.update_in(cx, |workspace, window, cx| {
11406            // Open two docks
11407            let left_dock = workspace.dock_at_position(DockPosition::Left);
11408            let right_dock = workspace.dock_at_position(DockPosition::Right);
11409
11410            left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11411            right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11412
11413            assert!(left_dock.read(cx).is_open());
11414            assert!(right_dock.read(cx).is_open());
11415        });
11416
11417        workspace.update_in(cx, |workspace, window, cx| {
11418            // Toggle all docks - should close both
11419            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11420
11421            let left_dock = workspace.dock_at_position(DockPosition::Left);
11422            let right_dock = workspace.dock_at_position(DockPosition::Right);
11423            assert!(!left_dock.read(cx).is_open());
11424            assert!(!right_dock.read(cx).is_open());
11425        });
11426
11427        workspace.update_in(cx, |workspace, window, cx| {
11428            // Toggle again - should reopen both
11429            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11430
11431            let left_dock = workspace.dock_at_position(DockPosition::Left);
11432            let right_dock = workspace.dock_at_position(DockPosition::Right);
11433            assert!(left_dock.read(cx).is_open());
11434            assert!(right_dock.read(cx).is_open());
11435        });
11436    }
11437
11438    #[gpui::test]
11439    async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
11440        init_test(cx);
11441        let fs = FakeFs::new(cx.executor());
11442
11443        let project = Project::test(fs, [], cx).await;
11444        let (workspace, cx) =
11445            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11446        workspace.update_in(cx, |workspace, window, cx| {
11447            // Open two docks
11448            let left_dock = workspace.dock_at_position(DockPosition::Left);
11449            let right_dock = workspace.dock_at_position(DockPosition::Right);
11450
11451            left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11452            right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11453
11454            assert!(left_dock.read(cx).is_open());
11455            assert!(right_dock.read(cx).is_open());
11456        });
11457
11458        workspace.update_in(cx, |workspace, window, cx| {
11459            // Close them manually
11460            workspace.toggle_dock(DockPosition::Left, window, cx);
11461            workspace.toggle_dock(DockPosition::Right, window, cx);
11462
11463            let left_dock = workspace.dock_at_position(DockPosition::Left);
11464            let right_dock = workspace.dock_at_position(DockPosition::Right);
11465            assert!(!left_dock.read(cx).is_open());
11466            assert!(!right_dock.read(cx).is_open());
11467        });
11468
11469        workspace.update_in(cx, |workspace, window, cx| {
11470            // Toggle all docks - only last closed (right dock) should reopen
11471            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11472
11473            let left_dock = workspace.dock_at_position(DockPosition::Left);
11474            let right_dock = workspace.dock_at_position(DockPosition::Right);
11475            assert!(!left_dock.read(cx).is_open());
11476            assert!(right_dock.read(cx).is_open());
11477        });
11478    }
11479
11480    #[gpui::test]
11481    async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
11482        init_test(cx);
11483        let fs = FakeFs::new(cx.executor());
11484        let project = Project::test(fs, [], cx).await;
11485        let (multi_workspace, cx) =
11486            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
11487        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
11488
11489        // Open two docks (left and right) with one panel each
11490        let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
11491            let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
11492            workspace.add_panel(left_panel.clone(), window, cx);
11493
11494            let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
11495            workspace.add_panel(right_panel.clone(), window, cx);
11496
11497            workspace.toggle_dock(DockPosition::Left, window, cx);
11498            workspace.toggle_dock(DockPosition::Right, window, cx);
11499
11500            // Verify initial state
11501            assert!(
11502                workspace.left_dock().read(cx).is_open(),
11503                "Left dock should be open"
11504            );
11505            assert_eq!(
11506                workspace
11507                    .left_dock()
11508                    .read(cx)
11509                    .visible_panel()
11510                    .unwrap()
11511                    .panel_id(),
11512                left_panel.panel_id(),
11513                "Left panel should be visible in left dock"
11514            );
11515            assert!(
11516                workspace.right_dock().read(cx).is_open(),
11517                "Right dock should be open"
11518            );
11519            assert_eq!(
11520                workspace
11521                    .right_dock()
11522                    .read(cx)
11523                    .visible_panel()
11524                    .unwrap()
11525                    .panel_id(),
11526                right_panel.panel_id(),
11527                "Right panel should be visible in right dock"
11528            );
11529            assert!(
11530                !workspace.bottom_dock().read(cx).is_open(),
11531                "Bottom dock should be closed"
11532            );
11533
11534            (left_panel, right_panel)
11535        });
11536
11537        // Focus the left panel and move it to the next position (bottom dock)
11538        workspace.update_in(cx, |workspace, window, cx| {
11539            workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
11540            assert!(
11541                left_panel.read(cx).focus_handle(cx).is_focused(window),
11542                "Left panel should be focused"
11543            );
11544        });
11545
11546        cx.dispatch_action(MoveFocusedPanelToNextPosition);
11547
11548        // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
11549        workspace.update(cx, |workspace, cx| {
11550            assert!(
11551                !workspace.left_dock().read(cx).is_open(),
11552                "Left dock should be closed"
11553            );
11554            assert!(
11555                workspace.bottom_dock().read(cx).is_open(),
11556                "Bottom dock should now be open"
11557            );
11558            assert_eq!(
11559                left_panel.read(cx).position,
11560                DockPosition::Bottom,
11561                "Left panel should now be in the bottom dock"
11562            );
11563            assert_eq!(
11564                workspace
11565                    .bottom_dock()
11566                    .read(cx)
11567                    .visible_panel()
11568                    .unwrap()
11569                    .panel_id(),
11570                left_panel.panel_id(),
11571                "Left panel should be the visible panel in the bottom dock"
11572            );
11573        });
11574
11575        // Toggle all docks off
11576        workspace.update_in(cx, |workspace, window, cx| {
11577            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11578            assert!(
11579                !workspace.left_dock().read(cx).is_open(),
11580                "Left dock should be closed"
11581            );
11582            assert!(
11583                !workspace.right_dock().read(cx).is_open(),
11584                "Right dock should be closed"
11585            );
11586            assert!(
11587                !workspace.bottom_dock().read(cx).is_open(),
11588                "Bottom dock should be closed"
11589            );
11590        });
11591
11592        // Toggle all docks back on and verify positions are restored
11593        workspace.update_in(cx, |workspace, window, cx| {
11594            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11595            assert!(
11596                !workspace.left_dock().read(cx).is_open(),
11597                "Left dock should remain closed"
11598            );
11599            assert!(
11600                workspace.right_dock().read(cx).is_open(),
11601                "Right dock should remain open"
11602            );
11603            assert!(
11604                workspace.bottom_dock().read(cx).is_open(),
11605                "Bottom dock should remain open"
11606            );
11607            assert_eq!(
11608                left_panel.read(cx).position,
11609                DockPosition::Bottom,
11610                "Left panel should remain in the bottom dock"
11611            );
11612            assert_eq!(
11613                right_panel.read(cx).position,
11614                DockPosition::Right,
11615                "Right panel should remain in the right dock"
11616            );
11617            assert_eq!(
11618                workspace
11619                    .bottom_dock()
11620                    .read(cx)
11621                    .visible_panel()
11622                    .unwrap()
11623                    .panel_id(),
11624                left_panel.panel_id(),
11625                "Left panel should be the visible panel in the right dock"
11626            );
11627        });
11628    }
11629
11630    #[gpui::test]
11631    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
11632        init_test(cx);
11633
11634        let fs = FakeFs::new(cx.executor());
11635
11636        let project = Project::test(fs, None, cx).await;
11637        let (workspace, cx) =
11638            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11639
11640        // Let's arrange the panes like this:
11641        //
11642        // +-----------------------+
11643        // |         top           |
11644        // +------+--------+-------+
11645        // | left | center | right |
11646        // +------+--------+-------+
11647        // |        bottom         |
11648        // +-----------------------+
11649
11650        let top_item = cx.new(|cx| {
11651            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
11652        });
11653        let bottom_item = cx.new(|cx| {
11654            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
11655        });
11656        let left_item = cx.new(|cx| {
11657            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
11658        });
11659        let right_item = cx.new(|cx| {
11660            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
11661        });
11662        let center_item = cx.new(|cx| {
11663            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
11664        });
11665
11666        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11667            let top_pane_id = workspace.active_pane().entity_id();
11668            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
11669            workspace.split_pane(
11670                workspace.active_pane().clone(),
11671                SplitDirection::Down,
11672                window,
11673                cx,
11674            );
11675            top_pane_id
11676        });
11677        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11678            let bottom_pane_id = workspace.active_pane().entity_id();
11679            workspace.add_item_to_active_pane(
11680                Box::new(bottom_item.clone()),
11681                None,
11682                false,
11683                window,
11684                cx,
11685            );
11686            workspace.split_pane(
11687                workspace.active_pane().clone(),
11688                SplitDirection::Up,
11689                window,
11690                cx,
11691            );
11692            bottom_pane_id
11693        });
11694        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11695            let left_pane_id = workspace.active_pane().entity_id();
11696            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
11697            workspace.split_pane(
11698                workspace.active_pane().clone(),
11699                SplitDirection::Right,
11700                window,
11701                cx,
11702            );
11703            left_pane_id
11704        });
11705        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11706            let right_pane_id = workspace.active_pane().entity_id();
11707            workspace.add_item_to_active_pane(
11708                Box::new(right_item.clone()),
11709                None,
11710                false,
11711                window,
11712                cx,
11713            );
11714            workspace.split_pane(
11715                workspace.active_pane().clone(),
11716                SplitDirection::Left,
11717                window,
11718                cx,
11719            );
11720            right_pane_id
11721        });
11722        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11723            let center_pane_id = workspace.active_pane().entity_id();
11724            workspace.add_item_to_active_pane(
11725                Box::new(center_item.clone()),
11726                None,
11727                false,
11728                window,
11729                cx,
11730            );
11731            center_pane_id
11732        });
11733        cx.executor().run_until_parked();
11734
11735        workspace.update_in(cx, |workspace, window, cx| {
11736            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
11737
11738            // Join into next from center pane into right
11739            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11740        });
11741
11742        workspace.update_in(cx, |workspace, window, cx| {
11743            let active_pane = workspace.active_pane();
11744            assert_eq!(right_pane_id, active_pane.entity_id());
11745            assert_eq!(2, active_pane.read(cx).items_len());
11746            let item_ids_in_pane =
11747                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11748            assert!(item_ids_in_pane.contains(&center_item.item_id()));
11749            assert!(item_ids_in_pane.contains(&right_item.item_id()));
11750
11751            // Join into next from right pane into bottom
11752            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11753        });
11754
11755        workspace.update_in(cx, |workspace, window, cx| {
11756            let active_pane = workspace.active_pane();
11757            assert_eq!(bottom_pane_id, active_pane.entity_id());
11758            assert_eq!(3, active_pane.read(cx).items_len());
11759            let item_ids_in_pane =
11760                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11761            assert!(item_ids_in_pane.contains(&center_item.item_id()));
11762            assert!(item_ids_in_pane.contains(&right_item.item_id()));
11763            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11764
11765            // Join into next from bottom pane into left
11766            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11767        });
11768
11769        workspace.update_in(cx, |workspace, window, cx| {
11770            let active_pane = workspace.active_pane();
11771            assert_eq!(left_pane_id, active_pane.entity_id());
11772            assert_eq!(4, active_pane.read(cx).items_len());
11773            let item_ids_in_pane =
11774                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11775            assert!(item_ids_in_pane.contains(&center_item.item_id()));
11776            assert!(item_ids_in_pane.contains(&right_item.item_id()));
11777            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11778            assert!(item_ids_in_pane.contains(&left_item.item_id()));
11779
11780            // Join into next from left pane into top
11781            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11782        });
11783
11784        workspace.update_in(cx, |workspace, window, cx| {
11785            let active_pane = workspace.active_pane();
11786            assert_eq!(top_pane_id, active_pane.entity_id());
11787            assert_eq!(5, active_pane.read(cx).items_len());
11788            let item_ids_in_pane =
11789                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11790            assert!(item_ids_in_pane.contains(&center_item.item_id()));
11791            assert!(item_ids_in_pane.contains(&right_item.item_id()));
11792            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11793            assert!(item_ids_in_pane.contains(&left_item.item_id()));
11794            assert!(item_ids_in_pane.contains(&top_item.item_id()));
11795
11796            // Single pane left: no-op
11797            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
11798        });
11799
11800        workspace.update(cx, |workspace, _cx| {
11801            let active_pane = workspace.active_pane();
11802            assert_eq!(top_pane_id, active_pane.entity_id());
11803        });
11804    }
11805
11806    fn add_an_item_to_active_pane(
11807        cx: &mut VisualTestContext,
11808        workspace: &Entity<Workspace>,
11809        item_id: u64,
11810    ) -> Entity<TestItem> {
11811        let item = cx.new(|cx| {
11812            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
11813                item_id,
11814                "item{item_id}.txt",
11815                cx,
11816            )])
11817        });
11818        workspace.update_in(cx, |workspace, window, cx| {
11819            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
11820        });
11821        item
11822    }
11823
11824    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
11825        workspace.update_in(cx, |workspace, window, cx| {
11826            workspace.split_pane(
11827                workspace.active_pane().clone(),
11828                SplitDirection::Right,
11829                window,
11830                cx,
11831            )
11832        })
11833    }
11834
11835    #[gpui::test]
11836    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
11837        init_test(cx);
11838        let fs = FakeFs::new(cx.executor());
11839        let project = Project::test(fs, None, cx).await;
11840        let (workspace, cx) =
11841            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11842
11843        add_an_item_to_active_pane(cx, &workspace, 1);
11844        split_pane(cx, &workspace);
11845        add_an_item_to_active_pane(cx, &workspace, 2);
11846        split_pane(cx, &workspace); // empty pane
11847        split_pane(cx, &workspace);
11848        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
11849
11850        cx.executor().run_until_parked();
11851
11852        workspace.update(cx, |workspace, cx| {
11853            let num_panes = workspace.panes().len();
11854            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
11855            let active_item = workspace
11856                .active_pane()
11857                .read(cx)
11858                .active_item()
11859                .expect("item is in focus");
11860
11861            assert_eq!(num_panes, 4);
11862            assert_eq!(num_items_in_current_pane, 1);
11863            assert_eq!(active_item.item_id(), last_item.item_id());
11864        });
11865
11866        workspace.update_in(cx, |workspace, window, cx| {
11867            workspace.join_all_panes(window, cx);
11868        });
11869
11870        workspace.update(cx, |workspace, cx| {
11871            let num_panes = workspace.panes().len();
11872            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
11873            let active_item = workspace
11874                .active_pane()
11875                .read(cx)
11876                .active_item()
11877                .expect("item is in focus");
11878
11879            assert_eq!(num_panes, 1);
11880            assert_eq!(num_items_in_current_pane, 3);
11881            assert_eq!(active_item.item_id(), last_item.item_id());
11882        });
11883    }
11884    struct TestModal(FocusHandle);
11885
11886    impl TestModal {
11887        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
11888            Self(cx.focus_handle())
11889        }
11890    }
11891
11892    impl EventEmitter<DismissEvent> for TestModal {}
11893
11894    impl Focusable for TestModal {
11895        fn focus_handle(&self, _cx: &App) -> FocusHandle {
11896            self.0.clone()
11897        }
11898    }
11899
11900    impl ModalView for TestModal {}
11901
11902    impl Render for TestModal {
11903        fn render(
11904            &mut self,
11905            _window: &mut Window,
11906            _cx: &mut Context<TestModal>,
11907        ) -> impl IntoElement {
11908            div().track_focus(&self.0)
11909        }
11910    }
11911
11912    #[gpui::test]
11913    async fn test_panels(cx: &mut gpui::TestAppContext) {
11914        init_test(cx);
11915        let fs = FakeFs::new(cx.executor());
11916
11917        let project = Project::test(fs, [], cx).await;
11918        let (multi_workspace, cx) =
11919            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
11920        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
11921
11922        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
11923            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
11924            workspace.add_panel(panel_1.clone(), window, cx);
11925            workspace.toggle_dock(DockPosition::Left, window, cx);
11926            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
11927            workspace.add_panel(panel_2.clone(), window, cx);
11928            workspace.toggle_dock(DockPosition::Right, window, cx);
11929
11930            let left_dock = workspace.left_dock();
11931            assert_eq!(
11932                left_dock.read(cx).visible_panel().unwrap().panel_id(),
11933                panel_1.panel_id()
11934            );
11935            assert_eq!(
11936                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
11937                panel_1.size(window, cx)
11938            );
11939
11940            left_dock.update(cx, |left_dock, cx| {
11941                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
11942            });
11943            assert_eq!(
11944                workspace
11945                    .right_dock()
11946                    .read(cx)
11947                    .visible_panel()
11948                    .unwrap()
11949                    .panel_id(),
11950                panel_2.panel_id(),
11951            );
11952
11953            (panel_1, panel_2)
11954        });
11955
11956        // Move panel_1 to the right
11957        panel_1.update_in(cx, |panel_1, window, cx| {
11958            panel_1.set_position(DockPosition::Right, window, cx)
11959        });
11960
11961        workspace.update_in(cx, |workspace, window, cx| {
11962            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
11963            // Since it was the only panel on the left, the left dock should now be closed.
11964            assert!(!workspace.left_dock().read(cx).is_open());
11965            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
11966            let right_dock = workspace.right_dock();
11967            assert_eq!(
11968                right_dock.read(cx).visible_panel().unwrap().panel_id(),
11969                panel_1.panel_id()
11970            );
11971            assert_eq!(
11972                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
11973                px(1337.)
11974            );
11975
11976            // Now we move panel_2 to the left
11977            panel_2.set_position(DockPosition::Left, window, cx);
11978        });
11979
11980        workspace.update(cx, |workspace, cx| {
11981            // Since panel_2 was not visible on the right, we don't open the left dock.
11982            assert!(!workspace.left_dock().read(cx).is_open());
11983            // And the right dock is unaffected in its displaying of panel_1
11984            assert!(workspace.right_dock().read(cx).is_open());
11985            assert_eq!(
11986                workspace
11987                    .right_dock()
11988                    .read(cx)
11989                    .visible_panel()
11990                    .unwrap()
11991                    .panel_id(),
11992                panel_1.panel_id(),
11993            );
11994        });
11995
11996        // Move panel_1 back to the left
11997        panel_1.update_in(cx, |panel_1, window, cx| {
11998            panel_1.set_position(DockPosition::Left, window, cx)
11999        });
12000
12001        workspace.update_in(cx, |workspace, window, cx| {
12002            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
12003            let left_dock = workspace.left_dock();
12004            assert!(left_dock.read(cx).is_open());
12005            assert_eq!(
12006                left_dock.read(cx).visible_panel().unwrap().panel_id(),
12007                panel_1.panel_id()
12008            );
12009            assert_eq!(
12010                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
12011                px(1337.)
12012            );
12013            // And the right dock should be closed as it no longer has any panels.
12014            assert!(!workspace.right_dock().read(cx).is_open());
12015
12016            // Now we move panel_1 to the bottom
12017            panel_1.set_position(DockPosition::Bottom, window, cx);
12018        });
12019
12020        workspace.update_in(cx, |workspace, window, cx| {
12021            // Since panel_1 was visible on the left, we close the left dock.
12022            assert!(!workspace.left_dock().read(cx).is_open());
12023            // The bottom dock is sized based on the panel's default size,
12024            // since the panel orientation changed from vertical to horizontal.
12025            let bottom_dock = workspace.bottom_dock();
12026            assert_eq!(
12027                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
12028                panel_1.size(window, cx),
12029            );
12030            // Close bottom dock and move panel_1 back to the left.
12031            bottom_dock.update(cx, |bottom_dock, cx| {
12032                bottom_dock.set_open(false, window, cx)
12033            });
12034            panel_1.set_position(DockPosition::Left, window, cx);
12035        });
12036
12037        // Emit activated event on panel 1
12038        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
12039
12040        // Now the left dock is open and panel_1 is active and focused.
12041        workspace.update_in(cx, |workspace, window, cx| {
12042            let left_dock = workspace.left_dock();
12043            assert!(left_dock.read(cx).is_open());
12044            assert_eq!(
12045                left_dock.read(cx).visible_panel().unwrap().panel_id(),
12046                panel_1.panel_id(),
12047            );
12048            assert!(panel_1.focus_handle(cx).is_focused(window));
12049        });
12050
12051        // Emit closed event on panel 2, which is not active
12052        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
12053
12054        // Wo don't close the left dock, because panel_2 wasn't the active panel
12055        workspace.update(cx, |workspace, cx| {
12056            let left_dock = workspace.left_dock();
12057            assert!(left_dock.read(cx).is_open());
12058            assert_eq!(
12059                left_dock.read(cx).visible_panel().unwrap().panel_id(),
12060                panel_1.panel_id(),
12061            );
12062        });
12063
12064        // Emitting a ZoomIn event shows the panel as zoomed.
12065        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
12066        workspace.read_with(cx, |workspace, _| {
12067            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12068            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
12069        });
12070
12071        // Move panel to another dock while it is zoomed
12072        panel_1.update_in(cx, |panel, window, cx| {
12073            panel.set_position(DockPosition::Right, window, cx)
12074        });
12075        workspace.read_with(cx, |workspace, _| {
12076            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12077
12078            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12079        });
12080
12081        // This is a helper for getting a:
12082        // - valid focus on an element,
12083        // - that isn't a part of the panes and panels system of the Workspace,
12084        // - and doesn't trigger the 'on_focus_lost' API.
12085        let focus_other_view = {
12086            let workspace = workspace.clone();
12087            move |cx: &mut VisualTestContext| {
12088                workspace.update_in(cx, |workspace, window, cx| {
12089                    if workspace.active_modal::<TestModal>(cx).is_some() {
12090                        workspace.toggle_modal(window, cx, TestModal::new);
12091                        workspace.toggle_modal(window, cx, TestModal::new);
12092                    } else {
12093                        workspace.toggle_modal(window, cx, TestModal::new);
12094                    }
12095                })
12096            }
12097        };
12098
12099        // If focus is transferred to another view that's not a panel or another pane, we still show
12100        // the panel as zoomed.
12101        focus_other_view(cx);
12102        workspace.read_with(cx, |workspace, _| {
12103            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12104            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12105        });
12106
12107        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
12108        workspace.update_in(cx, |_workspace, window, cx| {
12109            cx.focus_self(window);
12110        });
12111        workspace.read_with(cx, |workspace, _| {
12112            assert_eq!(workspace.zoomed, None);
12113            assert_eq!(workspace.zoomed_position, None);
12114        });
12115
12116        // If focus is transferred again to another view that's not a panel or a pane, we won't
12117        // show the panel as zoomed because it wasn't zoomed before.
12118        focus_other_view(cx);
12119        workspace.read_with(cx, |workspace, _| {
12120            assert_eq!(workspace.zoomed, None);
12121            assert_eq!(workspace.zoomed_position, None);
12122        });
12123
12124        // When the panel is activated, it is zoomed again.
12125        cx.dispatch_action(ToggleRightDock);
12126        workspace.read_with(cx, |workspace, _| {
12127            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12128            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12129        });
12130
12131        // Emitting a ZoomOut event unzooms the panel.
12132        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
12133        workspace.read_with(cx, |workspace, _| {
12134            assert_eq!(workspace.zoomed, None);
12135            assert_eq!(workspace.zoomed_position, None);
12136        });
12137
12138        // Emit closed event on panel 1, which is active
12139        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
12140
12141        // Now the left dock is closed, because panel_1 was the active panel
12142        workspace.update(cx, |workspace, cx| {
12143            let right_dock = workspace.right_dock();
12144            assert!(!right_dock.read(cx).is_open());
12145        });
12146    }
12147
12148    #[gpui::test]
12149    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
12150        init_test(cx);
12151
12152        let fs = FakeFs::new(cx.background_executor.clone());
12153        let project = Project::test(fs, [], cx).await;
12154        let (workspace, cx) =
12155            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12156        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12157
12158        let dirty_regular_buffer = cx.new(|cx| {
12159            TestItem::new(cx)
12160                .with_dirty(true)
12161                .with_label("1.txt")
12162                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
12163        });
12164        let dirty_regular_buffer_2 = cx.new(|cx| {
12165            TestItem::new(cx)
12166                .with_dirty(true)
12167                .with_label("2.txt")
12168                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
12169        });
12170        let dirty_multi_buffer_with_both = cx.new(|cx| {
12171            TestItem::new(cx)
12172                .with_dirty(true)
12173                .with_buffer_kind(ItemBufferKind::Multibuffer)
12174                .with_label("Fake Project Search")
12175                .with_project_items(&[
12176                    dirty_regular_buffer.read(cx).project_items[0].clone(),
12177                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
12178                ])
12179        });
12180        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
12181        workspace.update_in(cx, |workspace, window, cx| {
12182            workspace.add_item(
12183                pane.clone(),
12184                Box::new(dirty_regular_buffer.clone()),
12185                None,
12186                false,
12187                false,
12188                window,
12189                cx,
12190            );
12191            workspace.add_item(
12192                pane.clone(),
12193                Box::new(dirty_regular_buffer_2.clone()),
12194                None,
12195                false,
12196                false,
12197                window,
12198                cx,
12199            );
12200            workspace.add_item(
12201                pane.clone(),
12202                Box::new(dirty_multi_buffer_with_both.clone()),
12203                None,
12204                false,
12205                false,
12206                window,
12207                cx,
12208            );
12209        });
12210
12211        pane.update_in(cx, |pane, window, cx| {
12212            pane.activate_item(2, true, true, window, cx);
12213            assert_eq!(
12214                pane.active_item().unwrap().item_id(),
12215                multi_buffer_with_both_files_id,
12216                "Should select the multi buffer in the pane"
12217            );
12218        });
12219        let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12220            pane.close_other_items(
12221                &CloseOtherItems {
12222                    save_intent: Some(SaveIntent::Save),
12223                    close_pinned: true,
12224                },
12225                None,
12226                window,
12227                cx,
12228            )
12229        });
12230        cx.background_executor.run_until_parked();
12231        assert!(!cx.has_pending_prompt());
12232        close_all_but_multi_buffer_task
12233            .await
12234            .expect("Closing all buffers but the multi buffer failed");
12235        pane.update(cx, |pane, cx| {
12236            assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
12237            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
12238            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
12239            assert_eq!(pane.items_len(), 1);
12240            assert_eq!(
12241                pane.active_item().unwrap().item_id(),
12242                multi_buffer_with_both_files_id,
12243                "Should have only the multi buffer left in the pane"
12244            );
12245            assert!(
12246                dirty_multi_buffer_with_both.read(cx).is_dirty,
12247                "The multi buffer containing the unsaved buffer should still be dirty"
12248            );
12249        });
12250
12251        dirty_regular_buffer.update(cx, |buffer, cx| {
12252            buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
12253        });
12254
12255        let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12256            pane.close_active_item(
12257                &CloseActiveItem {
12258                    save_intent: Some(SaveIntent::Close),
12259                    close_pinned: false,
12260                },
12261                window,
12262                cx,
12263            )
12264        });
12265        cx.background_executor.run_until_parked();
12266        assert!(
12267            cx.has_pending_prompt(),
12268            "Dirty multi buffer should prompt a save dialog"
12269        );
12270        cx.simulate_prompt_answer("Save");
12271        cx.background_executor.run_until_parked();
12272        close_multi_buffer_task
12273            .await
12274            .expect("Closing the multi buffer failed");
12275        pane.update(cx, |pane, cx| {
12276            assert_eq!(
12277                dirty_multi_buffer_with_both.read(cx).save_count,
12278                1,
12279                "Multi buffer item should get be saved"
12280            );
12281            // Test impl does not save inner items, so we do not assert them
12282            assert_eq!(
12283                pane.items_len(),
12284                0,
12285                "No more items should be left in the pane"
12286            );
12287            assert!(pane.active_item().is_none());
12288        });
12289    }
12290
12291    #[gpui::test]
12292    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
12293        cx: &mut TestAppContext,
12294    ) {
12295        init_test(cx);
12296
12297        let fs = FakeFs::new(cx.background_executor.clone());
12298        let project = Project::test(fs, [], cx).await;
12299        let (workspace, cx) =
12300            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12301        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12302
12303        let dirty_regular_buffer = cx.new(|cx| {
12304            TestItem::new(cx)
12305                .with_dirty(true)
12306                .with_label("1.txt")
12307                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
12308        });
12309        let dirty_regular_buffer_2 = cx.new(|cx| {
12310            TestItem::new(cx)
12311                .with_dirty(true)
12312                .with_label("2.txt")
12313                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
12314        });
12315        let clear_regular_buffer = cx.new(|cx| {
12316            TestItem::new(cx)
12317                .with_label("3.txt")
12318                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
12319        });
12320
12321        let dirty_multi_buffer_with_both = cx.new(|cx| {
12322            TestItem::new(cx)
12323                .with_dirty(true)
12324                .with_buffer_kind(ItemBufferKind::Multibuffer)
12325                .with_label("Fake Project Search")
12326                .with_project_items(&[
12327                    dirty_regular_buffer.read(cx).project_items[0].clone(),
12328                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
12329                    clear_regular_buffer.read(cx).project_items[0].clone(),
12330                ])
12331        });
12332        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
12333        workspace.update_in(cx, |workspace, window, cx| {
12334            workspace.add_item(
12335                pane.clone(),
12336                Box::new(dirty_regular_buffer.clone()),
12337                None,
12338                false,
12339                false,
12340                window,
12341                cx,
12342            );
12343            workspace.add_item(
12344                pane.clone(),
12345                Box::new(dirty_multi_buffer_with_both.clone()),
12346                None,
12347                false,
12348                false,
12349                window,
12350                cx,
12351            );
12352        });
12353
12354        pane.update_in(cx, |pane, window, cx| {
12355            pane.activate_item(1, true, true, window, cx);
12356            assert_eq!(
12357                pane.active_item().unwrap().item_id(),
12358                multi_buffer_with_both_files_id,
12359                "Should select the multi buffer in the pane"
12360            );
12361        });
12362        let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12363            pane.close_active_item(
12364                &CloseActiveItem {
12365                    save_intent: None,
12366                    close_pinned: false,
12367                },
12368                window,
12369                cx,
12370            )
12371        });
12372        cx.background_executor.run_until_parked();
12373        assert!(
12374            cx.has_pending_prompt(),
12375            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
12376        );
12377    }
12378
12379    /// Tests that when `close_on_file_delete` is enabled, files are automatically
12380    /// closed when they are deleted from disk.
12381    #[gpui::test]
12382    async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
12383        init_test(cx);
12384
12385        // Enable the close_on_disk_deletion setting
12386        cx.update_global(|store: &mut SettingsStore, cx| {
12387            store.update_user_settings(cx, |settings| {
12388                settings.workspace.close_on_file_delete = Some(true);
12389            });
12390        });
12391
12392        let fs = FakeFs::new(cx.background_executor.clone());
12393        let project = Project::test(fs, [], cx).await;
12394        let (workspace, cx) =
12395            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12396        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12397
12398        // Create a test item that simulates a file
12399        let item = cx.new(|cx| {
12400            TestItem::new(cx)
12401                .with_label("test.txt")
12402                .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12403        });
12404
12405        // Add item to workspace
12406        workspace.update_in(cx, |workspace, window, cx| {
12407            workspace.add_item(
12408                pane.clone(),
12409                Box::new(item.clone()),
12410                None,
12411                false,
12412                false,
12413                window,
12414                cx,
12415            );
12416        });
12417
12418        // Verify the item is in the pane
12419        pane.read_with(cx, |pane, _| {
12420            assert_eq!(pane.items().count(), 1);
12421        });
12422
12423        // Simulate file deletion by setting the item's deleted state
12424        item.update(cx, |item, _| {
12425            item.set_has_deleted_file(true);
12426        });
12427
12428        // Emit UpdateTab event to trigger the close behavior
12429        cx.run_until_parked();
12430        item.update(cx, |_, cx| {
12431            cx.emit(ItemEvent::UpdateTab);
12432        });
12433
12434        // Allow the close operation to complete
12435        cx.run_until_parked();
12436
12437        // Verify the item was automatically closed
12438        pane.read_with(cx, |pane, _| {
12439            assert_eq!(
12440                pane.items().count(),
12441                0,
12442                "Item should be automatically closed when file is deleted"
12443            );
12444        });
12445    }
12446
12447    /// Tests that when `close_on_file_delete` is disabled (default), files remain
12448    /// open with a strikethrough when they are deleted from disk.
12449    #[gpui::test]
12450    async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
12451        init_test(cx);
12452
12453        // Ensure close_on_disk_deletion is disabled (default)
12454        cx.update_global(|store: &mut SettingsStore, cx| {
12455            store.update_user_settings(cx, |settings| {
12456                settings.workspace.close_on_file_delete = Some(false);
12457            });
12458        });
12459
12460        let fs = FakeFs::new(cx.background_executor.clone());
12461        let project = Project::test(fs, [], cx).await;
12462        let (workspace, cx) =
12463            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12464        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12465
12466        // Create a test item that simulates a file
12467        let item = cx.new(|cx| {
12468            TestItem::new(cx)
12469                .with_label("test.txt")
12470                .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12471        });
12472
12473        // Add item to workspace
12474        workspace.update_in(cx, |workspace, window, cx| {
12475            workspace.add_item(
12476                pane.clone(),
12477                Box::new(item.clone()),
12478                None,
12479                false,
12480                false,
12481                window,
12482                cx,
12483            );
12484        });
12485
12486        // Verify the item is in the pane
12487        pane.read_with(cx, |pane, _| {
12488            assert_eq!(pane.items().count(), 1);
12489        });
12490
12491        // Simulate file deletion
12492        item.update(cx, |item, _| {
12493            item.set_has_deleted_file(true);
12494        });
12495
12496        // Emit UpdateTab event
12497        cx.run_until_parked();
12498        item.update(cx, |_, cx| {
12499            cx.emit(ItemEvent::UpdateTab);
12500        });
12501
12502        // Allow any potential close operation to complete
12503        cx.run_until_parked();
12504
12505        // Verify the item remains open (with strikethrough)
12506        pane.read_with(cx, |pane, _| {
12507            assert_eq!(
12508                pane.items().count(),
12509                1,
12510                "Item should remain open when close_on_disk_deletion is disabled"
12511            );
12512        });
12513
12514        // Verify the item shows as deleted
12515        item.read_with(cx, |item, _| {
12516            assert!(
12517                item.has_deleted_file,
12518                "Item should be marked as having deleted file"
12519            );
12520        });
12521    }
12522
12523    /// Tests that dirty files are not automatically closed when deleted from disk,
12524    /// even when `close_on_file_delete` is enabled. This ensures users don't lose
12525    /// unsaved changes without being prompted.
12526    #[gpui::test]
12527    async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
12528        init_test(cx);
12529
12530        // Enable the close_on_file_delete setting
12531        cx.update_global(|store: &mut SettingsStore, cx| {
12532            store.update_user_settings(cx, |settings| {
12533                settings.workspace.close_on_file_delete = Some(true);
12534            });
12535        });
12536
12537        let fs = FakeFs::new(cx.background_executor.clone());
12538        let project = Project::test(fs, [], cx).await;
12539        let (workspace, cx) =
12540            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12541        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12542
12543        // Create a dirty test item
12544        let item = cx.new(|cx| {
12545            TestItem::new(cx)
12546                .with_dirty(true)
12547                .with_label("test.txt")
12548                .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12549        });
12550
12551        // Add item to workspace
12552        workspace.update_in(cx, |workspace, window, cx| {
12553            workspace.add_item(
12554                pane.clone(),
12555                Box::new(item.clone()),
12556                None,
12557                false,
12558                false,
12559                window,
12560                cx,
12561            );
12562        });
12563
12564        // Simulate file deletion
12565        item.update(cx, |item, _| {
12566            item.set_has_deleted_file(true);
12567        });
12568
12569        // Emit UpdateTab event to trigger the close behavior
12570        cx.run_until_parked();
12571        item.update(cx, |_, cx| {
12572            cx.emit(ItemEvent::UpdateTab);
12573        });
12574
12575        // Allow any potential close operation to complete
12576        cx.run_until_parked();
12577
12578        // Verify the item remains open (dirty files are not auto-closed)
12579        pane.read_with(cx, |pane, _| {
12580            assert_eq!(
12581                pane.items().count(),
12582                1,
12583                "Dirty items should not be automatically closed even when file is deleted"
12584            );
12585        });
12586
12587        // Verify the item is marked as deleted and still dirty
12588        item.read_with(cx, |item, _| {
12589            assert!(
12590                item.has_deleted_file,
12591                "Item should be marked as having deleted file"
12592            );
12593            assert!(item.is_dirty, "Item should still be dirty");
12594        });
12595    }
12596
12597    /// Tests that navigation history is cleaned up when files are auto-closed
12598    /// due to deletion from disk.
12599    #[gpui::test]
12600    async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
12601        init_test(cx);
12602
12603        // Enable the close_on_file_delete setting
12604        cx.update_global(|store: &mut SettingsStore, cx| {
12605            store.update_user_settings(cx, |settings| {
12606                settings.workspace.close_on_file_delete = Some(true);
12607            });
12608        });
12609
12610        let fs = FakeFs::new(cx.background_executor.clone());
12611        let project = Project::test(fs, [], cx).await;
12612        let (workspace, cx) =
12613            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12614        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12615
12616        // Create test items
12617        let item1 = cx.new(|cx| {
12618            TestItem::new(cx)
12619                .with_label("test1.txt")
12620                .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
12621        });
12622        let item1_id = item1.item_id();
12623
12624        let item2 = cx.new(|cx| {
12625            TestItem::new(cx)
12626                .with_label("test2.txt")
12627                .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
12628        });
12629
12630        // Add items to workspace
12631        workspace.update_in(cx, |workspace, window, cx| {
12632            workspace.add_item(
12633                pane.clone(),
12634                Box::new(item1.clone()),
12635                None,
12636                false,
12637                false,
12638                window,
12639                cx,
12640            );
12641            workspace.add_item(
12642                pane.clone(),
12643                Box::new(item2.clone()),
12644                None,
12645                false,
12646                false,
12647                window,
12648                cx,
12649            );
12650        });
12651
12652        // Activate item1 to ensure it gets navigation entries
12653        pane.update_in(cx, |pane, window, cx| {
12654            pane.activate_item(0, true, true, window, cx);
12655        });
12656
12657        // Switch to item2 and back to create navigation history
12658        pane.update_in(cx, |pane, window, cx| {
12659            pane.activate_item(1, true, true, window, cx);
12660        });
12661        cx.run_until_parked();
12662
12663        pane.update_in(cx, |pane, window, cx| {
12664            pane.activate_item(0, true, true, window, cx);
12665        });
12666        cx.run_until_parked();
12667
12668        // Simulate file deletion for item1
12669        item1.update(cx, |item, _| {
12670            item.set_has_deleted_file(true);
12671        });
12672
12673        // Emit UpdateTab event to trigger the close behavior
12674        item1.update(cx, |_, cx| {
12675            cx.emit(ItemEvent::UpdateTab);
12676        });
12677        cx.run_until_parked();
12678
12679        // Verify item1 was closed
12680        pane.read_with(cx, |pane, _| {
12681            assert_eq!(
12682                pane.items().count(),
12683                1,
12684                "Should have 1 item remaining after auto-close"
12685            );
12686        });
12687
12688        // Check navigation history after close
12689        let has_item = pane.read_with(cx, |pane, cx| {
12690            let mut has_item = false;
12691            pane.nav_history().for_each_entry(cx, &mut |entry, _| {
12692                if entry.item.id() == item1_id {
12693                    has_item = true;
12694                }
12695            });
12696            has_item
12697        });
12698
12699        assert!(
12700            !has_item,
12701            "Navigation history should not contain closed item entries"
12702        );
12703    }
12704
12705    #[gpui::test]
12706    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
12707        cx: &mut TestAppContext,
12708    ) {
12709        init_test(cx);
12710
12711        let fs = FakeFs::new(cx.background_executor.clone());
12712        let project = Project::test(fs, [], cx).await;
12713        let (workspace, cx) =
12714            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12715        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12716
12717        let dirty_regular_buffer = cx.new(|cx| {
12718            TestItem::new(cx)
12719                .with_dirty(true)
12720                .with_label("1.txt")
12721                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
12722        });
12723        let dirty_regular_buffer_2 = cx.new(|cx| {
12724            TestItem::new(cx)
12725                .with_dirty(true)
12726                .with_label("2.txt")
12727                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
12728        });
12729        let clear_regular_buffer = cx.new(|cx| {
12730            TestItem::new(cx)
12731                .with_label("3.txt")
12732                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
12733        });
12734
12735        let dirty_multi_buffer = cx.new(|cx| {
12736            TestItem::new(cx)
12737                .with_dirty(true)
12738                .with_buffer_kind(ItemBufferKind::Multibuffer)
12739                .with_label("Fake Project Search")
12740                .with_project_items(&[
12741                    dirty_regular_buffer.read(cx).project_items[0].clone(),
12742                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
12743                    clear_regular_buffer.read(cx).project_items[0].clone(),
12744                ])
12745        });
12746        workspace.update_in(cx, |workspace, window, cx| {
12747            workspace.add_item(
12748                pane.clone(),
12749                Box::new(dirty_regular_buffer.clone()),
12750                None,
12751                false,
12752                false,
12753                window,
12754                cx,
12755            );
12756            workspace.add_item(
12757                pane.clone(),
12758                Box::new(dirty_regular_buffer_2.clone()),
12759                None,
12760                false,
12761                false,
12762                window,
12763                cx,
12764            );
12765            workspace.add_item(
12766                pane.clone(),
12767                Box::new(dirty_multi_buffer.clone()),
12768                None,
12769                false,
12770                false,
12771                window,
12772                cx,
12773            );
12774        });
12775
12776        pane.update_in(cx, |pane, window, cx| {
12777            pane.activate_item(2, true, true, window, cx);
12778            assert_eq!(
12779                pane.active_item().unwrap().item_id(),
12780                dirty_multi_buffer.item_id(),
12781                "Should select the multi buffer in the pane"
12782            );
12783        });
12784        let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12785            pane.close_active_item(
12786                &CloseActiveItem {
12787                    save_intent: None,
12788                    close_pinned: false,
12789                },
12790                window,
12791                cx,
12792            )
12793        });
12794        cx.background_executor.run_until_parked();
12795        assert!(
12796            !cx.has_pending_prompt(),
12797            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
12798        );
12799        close_multi_buffer_task
12800            .await
12801            .expect("Closing multi buffer failed");
12802        pane.update(cx, |pane, cx| {
12803            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
12804            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
12805            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
12806            assert_eq!(
12807                pane.items()
12808                    .map(|item| item.item_id())
12809                    .sorted()
12810                    .collect::<Vec<_>>(),
12811                vec![
12812                    dirty_regular_buffer.item_id(),
12813                    dirty_regular_buffer_2.item_id(),
12814                ],
12815                "Should have no multi buffer left in the pane"
12816            );
12817            assert!(dirty_regular_buffer.read(cx).is_dirty);
12818            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
12819        });
12820    }
12821
12822    #[gpui::test]
12823    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
12824        init_test(cx);
12825        let fs = FakeFs::new(cx.executor());
12826        let project = Project::test(fs, [], cx).await;
12827        let (multi_workspace, cx) =
12828            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12829        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12830
12831        // Add a new panel to the right dock, opening the dock and setting the
12832        // focus to the new panel.
12833        let panel = workspace.update_in(cx, |workspace, window, cx| {
12834            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12835            workspace.add_panel(panel.clone(), window, cx);
12836
12837            workspace
12838                .right_dock()
12839                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
12840
12841            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12842
12843            panel
12844        });
12845
12846        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
12847        // panel to the next valid position which, in this case, is the left
12848        // dock.
12849        cx.dispatch_action(MoveFocusedPanelToNextPosition);
12850        workspace.update(cx, |workspace, cx| {
12851            assert!(workspace.left_dock().read(cx).is_open());
12852            assert_eq!(panel.read(cx).position, DockPosition::Left);
12853        });
12854
12855        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
12856        // panel to the next valid position which, in this case, is the bottom
12857        // dock.
12858        cx.dispatch_action(MoveFocusedPanelToNextPosition);
12859        workspace.update(cx, |workspace, cx| {
12860            assert!(workspace.bottom_dock().read(cx).is_open());
12861            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
12862        });
12863
12864        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
12865        // around moving the panel to its initial position, the right dock.
12866        cx.dispatch_action(MoveFocusedPanelToNextPosition);
12867        workspace.update(cx, |workspace, cx| {
12868            assert!(workspace.right_dock().read(cx).is_open());
12869            assert_eq!(panel.read(cx).position, DockPosition::Right);
12870        });
12871
12872        // Remove focus from the panel, ensuring that, if the panel is not
12873        // focused, the `MoveFocusedPanelToNextPosition` action does not update
12874        // the panel's position, so the panel is still in the right dock.
12875        workspace.update_in(cx, |workspace, window, cx| {
12876            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12877        });
12878
12879        cx.dispatch_action(MoveFocusedPanelToNextPosition);
12880        workspace.update(cx, |workspace, cx| {
12881            assert!(workspace.right_dock().read(cx).is_open());
12882            assert_eq!(panel.read(cx).position, DockPosition::Right);
12883        });
12884    }
12885
12886    #[gpui::test]
12887    async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
12888        init_test(cx);
12889
12890        let fs = FakeFs::new(cx.executor());
12891        let project = Project::test(fs, [], cx).await;
12892        let (workspace, cx) =
12893            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12894
12895        let item_1 = cx.new(|cx| {
12896            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
12897        });
12898        workspace.update_in(cx, |workspace, window, cx| {
12899            workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
12900            workspace.move_item_to_pane_in_direction(
12901                &MoveItemToPaneInDirection {
12902                    direction: SplitDirection::Right,
12903                    focus: true,
12904                    clone: false,
12905                },
12906                window,
12907                cx,
12908            );
12909            workspace.move_item_to_pane_at_index(
12910                &MoveItemToPane {
12911                    destination: 3,
12912                    focus: true,
12913                    clone: false,
12914                },
12915                window,
12916                cx,
12917            );
12918
12919            assert_eq!(workspace.panes.len(), 1, "No new panes were created");
12920            assert_eq!(
12921                pane_items_paths(&workspace.active_pane, cx),
12922                vec!["first.txt".to_string()],
12923                "Single item was not moved anywhere"
12924            );
12925        });
12926
12927        let item_2 = cx.new(|cx| {
12928            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
12929        });
12930        workspace.update_in(cx, |workspace, window, cx| {
12931            workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
12932            assert_eq!(
12933                pane_items_paths(&workspace.panes[0], cx),
12934                vec!["first.txt".to_string(), "second.txt".to_string()],
12935            );
12936            workspace.move_item_to_pane_in_direction(
12937                &MoveItemToPaneInDirection {
12938                    direction: SplitDirection::Right,
12939                    focus: true,
12940                    clone: false,
12941                },
12942                window,
12943                cx,
12944            );
12945
12946            assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
12947            assert_eq!(
12948                pane_items_paths(&workspace.panes[0], cx),
12949                vec!["first.txt".to_string()],
12950                "After moving, one item should be left in the original pane"
12951            );
12952            assert_eq!(
12953                pane_items_paths(&workspace.panes[1], cx),
12954                vec!["second.txt".to_string()],
12955                "New item should have been moved to the new pane"
12956            );
12957        });
12958
12959        let item_3 = cx.new(|cx| {
12960            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
12961        });
12962        workspace.update_in(cx, |workspace, window, cx| {
12963            let original_pane = workspace.panes[0].clone();
12964            workspace.set_active_pane(&original_pane, window, cx);
12965            workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
12966            assert_eq!(workspace.panes.len(), 2, "No new panes were created");
12967            assert_eq!(
12968                pane_items_paths(&workspace.active_pane, cx),
12969                vec!["first.txt".to_string(), "third.txt".to_string()],
12970                "New pane should be ready to move one item out"
12971            );
12972
12973            workspace.move_item_to_pane_at_index(
12974                &MoveItemToPane {
12975                    destination: 3,
12976                    focus: true,
12977                    clone: false,
12978                },
12979                window,
12980                cx,
12981            );
12982            assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
12983            assert_eq!(
12984                pane_items_paths(&workspace.active_pane, cx),
12985                vec!["first.txt".to_string()],
12986                "After moving, one item should be left in the original pane"
12987            );
12988            assert_eq!(
12989                pane_items_paths(&workspace.panes[1], cx),
12990                vec!["second.txt".to_string()],
12991                "Previously created pane should be unchanged"
12992            );
12993            assert_eq!(
12994                pane_items_paths(&workspace.panes[2], cx),
12995                vec!["third.txt".to_string()],
12996                "New item should have been moved to the new pane"
12997            );
12998        });
12999    }
13000
13001    #[gpui::test]
13002    async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
13003        init_test(cx);
13004
13005        let fs = FakeFs::new(cx.executor());
13006        let project = Project::test(fs, [], cx).await;
13007        let (workspace, cx) =
13008            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13009
13010        let item_1 = cx.new(|cx| {
13011            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
13012        });
13013        workspace.update_in(cx, |workspace, window, cx| {
13014            workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
13015            workspace.move_item_to_pane_in_direction(
13016                &MoveItemToPaneInDirection {
13017                    direction: SplitDirection::Right,
13018                    focus: true,
13019                    clone: true,
13020                },
13021                window,
13022                cx,
13023            );
13024        });
13025        cx.run_until_parked();
13026        workspace.update_in(cx, |workspace, window, cx| {
13027            workspace.move_item_to_pane_at_index(
13028                &MoveItemToPane {
13029                    destination: 3,
13030                    focus: true,
13031                    clone: true,
13032                },
13033                window,
13034                cx,
13035            );
13036        });
13037        cx.run_until_parked();
13038
13039        workspace.update(cx, |workspace, cx| {
13040            assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
13041            for pane in workspace.panes() {
13042                assert_eq!(
13043                    pane_items_paths(pane, cx),
13044                    vec!["first.txt".to_string()],
13045                    "Single item exists in all panes"
13046                );
13047            }
13048        });
13049
13050        // verify that the active pane has been updated after waiting for the
13051        // pane focus event to fire and resolve
13052        workspace.read_with(cx, |workspace, _app| {
13053            assert_eq!(
13054                workspace.active_pane(),
13055                &workspace.panes[2],
13056                "The third pane should be the active one: {:?}",
13057                workspace.panes
13058            );
13059        })
13060    }
13061
13062    #[gpui::test]
13063    async fn test_close_item_in_all_panes(cx: &mut TestAppContext) {
13064        init_test(cx);
13065
13066        let fs = FakeFs::new(cx.executor());
13067        fs.insert_tree("/root", json!({ "test.txt": "" })).await;
13068
13069        let project = Project::test(fs, ["root".as_ref()], cx).await;
13070        let (workspace, cx) =
13071            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13072
13073        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13074        // Add item to pane A with project path
13075        let item_a = cx.new(|cx| {
13076            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13077        });
13078        workspace.update_in(cx, |workspace, window, cx| {
13079            workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
13080        });
13081
13082        // Split to create pane B
13083        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
13084            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
13085        });
13086
13087        // Add item with SAME project path to pane B, and pin it
13088        let item_b = cx.new(|cx| {
13089            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13090        });
13091        pane_b.update_in(cx, |pane, window, cx| {
13092            pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
13093            pane.set_pinned_count(1);
13094        });
13095
13096        assert_eq!(pane_a.read_with(cx, |pane, _| pane.items_len()), 1);
13097        assert_eq!(pane_b.read_with(cx, |pane, _| pane.items_len()), 1);
13098
13099        // close_pinned: false should only close the unpinned copy
13100        workspace.update_in(cx, |workspace, window, cx| {
13101            workspace.close_item_in_all_panes(
13102                &CloseItemInAllPanes {
13103                    save_intent: Some(SaveIntent::Close),
13104                    close_pinned: false,
13105                },
13106                window,
13107                cx,
13108            )
13109        });
13110        cx.executor().run_until_parked();
13111
13112        let item_count_a = pane_a.read_with(cx, |pane, _| pane.items_len());
13113        let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
13114        assert_eq!(item_count_a, 0, "Unpinned item in pane A should be closed");
13115        assert_eq!(item_count_b, 1, "Pinned item in pane B should remain");
13116
13117        // Split again, seeing as closing the previous item also closed its
13118        // pane, so only pane remains, which does not allow us to properly test
13119        // that both items close when `close_pinned: true`.
13120        let pane_c = workspace.update_in(cx, |workspace, window, cx| {
13121            workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
13122        });
13123
13124        // Add an item with the same project path to pane C so that
13125        // close_item_in_all_panes can determine what to close across all panes
13126        // (it reads the active item from the active pane, and split_pane
13127        // creates an empty pane).
13128        let item_c = cx.new(|cx| {
13129            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13130        });
13131        pane_c.update_in(cx, |pane, window, cx| {
13132            pane.add_item(Box::new(item_c.clone()), true, true, None, window, cx);
13133        });
13134
13135        // close_pinned: true should close the pinned copy too
13136        workspace.update_in(cx, |workspace, window, cx| {
13137            let panes_count = workspace.panes().len();
13138            assert_eq!(panes_count, 2, "Workspace should have two panes (B and C)");
13139
13140            workspace.close_item_in_all_panes(
13141                &CloseItemInAllPanes {
13142                    save_intent: Some(SaveIntent::Close),
13143                    close_pinned: true,
13144                },
13145                window,
13146                cx,
13147            )
13148        });
13149        cx.executor().run_until_parked();
13150
13151        let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
13152        let item_count_c = pane_c.read_with(cx, |pane, _| pane.items_len());
13153        assert_eq!(item_count_b, 0, "Pinned item in pane B should be closed");
13154        assert_eq!(item_count_c, 0, "Unpinned item in pane C should be closed");
13155    }
13156
13157    mod register_project_item_tests {
13158
13159        use super::*;
13160
13161        // View
13162        struct TestPngItemView {
13163            focus_handle: FocusHandle,
13164        }
13165        // Model
13166        struct TestPngItem {}
13167
13168        impl project::ProjectItem for TestPngItem {
13169            fn try_open(
13170                _project: &Entity<Project>,
13171                path: &ProjectPath,
13172                cx: &mut App,
13173            ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
13174                if path.path.extension().unwrap() == "png" {
13175                    Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
13176                } else {
13177                    None
13178                }
13179            }
13180
13181            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
13182                None
13183            }
13184
13185            fn project_path(&self, _: &App) -> Option<ProjectPath> {
13186                None
13187            }
13188
13189            fn is_dirty(&self) -> bool {
13190                false
13191            }
13192        }
13193
13194        impl Item for TestPngItemView {
13195            type Event = ();
13196            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
13197                "".into()
13198            }
13199        }
13200        impl EventEmitter<()> for TestPngItemView {}
13201        impl Focusable for TestPngItemView {
13202            fn focus_handle(&self, _cx: &App) -> FocusHandle {
13203                self.focus_handle.clone()
13204            }
13205        }
13206
13207        impl Render for TestPngItemView {
13208            fn render(
13209                &mut self,
13210                _window: &mut Window,
13211                _cx: &mut Context<Self>,
13212            ) -> impl IntoElement {
13213                Empty
13214            }
13215        }
13216
13217        impl ProjectItem for TestPngItemView {
13218            type Item = TestPngItem;
13219
13220            fn for_project_item(
13221                _project: Entity<Project>,
13222                _pane: Option<&Pane>,
13223                _item: Entity<Self::Item>,
13224                _: &mut Window,
13225                cx: &mut Context<Self>,
13226            ) -> Self
13227            where
13228                Self: Sized,
13229            {
13230                Self {
13231                    focus_handle: cx.focus_handle(),
13232                }
13233            }
13234        }
13235
13236        // View
13237        struct TestIpynbItemView {
13238            focus_handle: FocusHandle,
13239        }
13240        // Model
13241        struct TestIpynbItem {}
13242
13243        impl project::ProjectItem for TestIpynbItem {
13244            fn try_open(
13245                _project: &Entity<Project>,
13246                path: &ProjectPath,
13247                cx: &mut App,
13248            ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
13249                if path.path.extension().unwrap() == "ipynb" {
13250                    Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
13251                } else {
13252                    None
13253                }
13254            }
13255
13256            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
13257                None
13258            }
13259
13260            fn project_path(&self, _: &App) -> Option<ProjectPath> {
13261                None
13262            }
13263
13264            fn is_dirty(&self) -> bool {
13265                false
13266            }
13267        }
13268
13269        impl Item for TestIpynbItemView {
13270            type Event = ();
13271            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
13272                "".into()
13273            }
13274        }
13275        impl EventEmitter<()> for TestIpynbItemView {}
13276        impl Focusable for TestIpynbItemView {
13277            fn focus_handle(&self, _cx: &App) -> FocusHandle {
13278                self.focus_handle.clone()
13279            }
13280        }
13281
13282        impl Render for TestIpynbItemView {
13283            fn render(
13284                &mut self,
13285                _window: &mut Window,
13286                _cx: &mut Context<Self>,
13287            ) -> impl IntoElement {
13288                Empty
13289            }
13290        }
13291
13292        impl ProjectItem for TestIpynbItemView {
13293            type Item = TestIpynbItem;
13294
13295            fn for_project_item(
13296                _project: Entity<Project>,
13297                _pane: Option<&Pane>,
13298                _item: Entity<Self::Item>,
13299                _: &mut Window,
13300                cx: &mut Context<Self>,
13301            ) -> Self
13302            where
13303                Self: Sized,
13304            {
13305                Self {
13306                    focus_handle: cx.focus_handle(),
13307                }
13308            }
13309        }
13310
13311        struct TestAlternatePngItemView {
13312            focus_handle: FocusHandle,
13313        }
13314
13315        impl Item for TestAlternatePngItemView {
13316            type Event = ();
13317            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
13318                "".into()
13319            }
13320        }
13321
13322        impl EventEmitter<()> for TestAlternatePngItemView {}
13323        impl Focusable for TestAlternatePngItemView {
13324            fn focus_handle(&self, _cx: &App) -> FocusHandle {
13325                self.focus_handle.clone()
13326            }
13327        }
13328
13329        impl Render for TestAlternatePngItemView {
13330            fn render(
13331                &mut self,
13332                _window: &mut Window,
13333                _cx: &mut Context<Self>,
13334            ) -> impl IntoElement {
13335                Empty
13336            }
13337        }
13338
13339        impl ProjectItem for TestAlternatePngItemView {
13340            type Item = TestPngItem;
13341
13342            fn for_project_item(
13343                _project: Entity<Project>,
13344                _pane: Option<&Pane>,
13345                _item: Entity<Self::Item>,
13346                _: &mut Window,
13347                cx: &mut Context<Self>,
13348            ) -> Self
13349            where
13350                Self: Sized,
13351            {
13352                Self {
13353                    focus_handle: cx.focus_handle(),
13354                }
13355            }
13356        }
13357
13358        #[gpui::test]
13359        async fn test_register_project_item(cx: &mut TestAppContext) {
13360            init_test(cx);
13361
13362            cx.update(|cx| {
13363                register_project_item::<TestPngItemView>(cx);
13364                register_project_item::<TestIpynbItemView>(cx);
13365            });
13366
13367            let fs = FakeFs::new(cx.executor());
13368            fs.insert_tree(
13369                "/root1",
13370                json!({
13371                    "one.png": "BINARYDATAHERE",
13372                    "two.ipynb": "{ totally a notebook }",
13373                    "three.txt": "editing text, sure why not?"
13374                }),
13375            )
13376            .await;
13377
13378            let project = Project::test(fs, ["root1".as_ref()], cx).await;
13379            let (workspace, cx) =
13380                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13381
13382            let worktree_id = project.update(cx, |project, cx| {
13383                project.worktrees(cx).next().unwrap().read(cx).id()
13384            });
13385
13386            let handle = workspace
13387                .update_in(cx, |workspace, window, cx| {
13388                    let project_path = (worktree_id, rel_path("one.png"));
13389                    workspace.open_path(project_path, None, true, window, cx)
13390                })
13391                .await
13392                .unwrap();
13393
13394            // Now we can check if the handle we got back errored or not
13395            assert_eq!(
13396                handle.to_any_view().entity_type(),
13397                TypeId::of::<TestPngItemView>()
13398            );
13399
13400            let handle = workspace
13401                .update_in(cx, |workspace, window, cx| {
13402                    let project_path = (worktree_id, rel_path("two.ipynb"));
13403                    workspace.open_path(project_path, None, true, window, cx)
13404                })
13405                .await
13406                .unwrap();
13407
13408            assert_eq!(
13409                handle.to_any_view().entity_type(),
13410                TypeId::of::<TestIpynbItemView>()
13411            );
13412
13413            let handle = workspace
13414                .update_in(cx, |workspace, window, cx| {
13415                    let project_path = (worktree_id, rel_path("three.txt"));
13416                    workspace.open_path(project_path, None, true, window, cx)
13417                })
13418                .await;
13419            assert!(handle.is_err());
13420        }
13421
13422        #[gpui::test]
13423        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
13424            init_test(cx);
13425
13426            cx.update(|cx| {
13427                register_project_item::<TestPngItemView>(cx);
13428                register_project_item::<TestAlternatePngItemView>(cx);
13429            });
13430
13431            let fs = FakeFs::new(cx.executor());
13432            fs.insert_tree(
13433                "/root1",
13434                json!({
13435                    "one.png": "BINARYDATAHERE",
13436                    "two.ipynb": "{ totally a notebook }",
13437                    "three.txt": "editing text, sure why not?"
13438                }),
13439            )
13440            .await;
13441            let project = Project::test(fs, ["root1".as_ref()], cx).await;
13442            let (workspace, cx) =
13443                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13444            let worktree_id = project.update(cx, |project, cx| {
13445                project.worktrees(cx).next().unwrap().read(cx).id()
13446            });
13447
13448            let handle = workspace
13449                .update_in(cx, |workspace, window, cx| {
13450                    let project_path = (worktree_id, rel_path("one.png"));
13451                    workspace.open_path(project_path, None, true, window, cx)
13452                })
13453                .await
13454                .unwrap();
13455
13456            // This _must_ be the second item registered
13457            assert_eq!(
13458                handle.to_any_view().entity_type(),
13459                TypeId::of::<TestAlternatePngItemView>()
13460            );
13461
13462            let handle = workspace
13463                .update_in(cx, |workspace, window, cx| {
13464                    let project_path = (worktree_id, rel_path("three.txt"));
13465                    workspace.open_path(project_path, None, true, window, cx)
13466                })
13467                .await;
13468            assert!(handle.is_err());
13469        }
13470    }
13471
13472    #[gpui::test]
13473    async fn test_status_bar_visibility(cx: &mut TestAppContext) {
13474        init_test(cx);
13475
13476        let fs = FakeFs::new(cx.executor());
13477        let project = Project::test(fs, [], cx).await;
13478        let (workspace, _cx) =
13479            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13480
13481        // Test with status bar shown (default)
13482        workspace.read_with(cx, |workspace, cx| {
13483            let visible = workspace.status_bar_visible(cx);
13484            assert!(visible, "Status bar should be visible by default");
13485        });
13486
13487        // Test with status bar hidden
13488        cx.update_global(|store: &mut SettingsStore, cx| {
13489            store.update_user_settings(cx, |settings| {
13490                settings.status_bar.get_or_insert_default().show = Some(false);
13491            });
13492        });
13493
13494        workspace.read_with(cx, |workspace, cx| {
13495            let visible = workspace.status_bar_visible(cx);
13496            assert!(!visible, "Status bar should be hidden when show is false");
13497        });
13498
13499        // Test with status bar shown explicitly
13500        cx.update_global(|store: &mut SettingsStore, cx| {
13501            store.update_user_settings(cx, |settings| {
13502                settings.status_bar.get_or_insert_default().show = Some(true);
13503            });
13504        });
13505
13506        workspace.read_with(cx, |workspace, cx| {
13507            let visible = workspace.status_bar_visible(cx);
13508            assert!(visible, "Status bar should be visible when show is true");
13509        });
13510    }
13511
13512    #[gpui::test]
13513    async fn test_pane_close_active_item(cx: &mut TestAppContext) {
13514        init_test(cx);
13515
13516        let fs = FakeFs::new(cx.executor());
13517        let project = Project::test(fs, [], cx).await;
13518        let (multi_workspace, cx) =
13519            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13520        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13521        let panel = workspace.update_in(cx, |workspace, window, cx| {
13522            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
13523            workspace.add_panel(panel.clone(), window, cx);
13524
13525            workspace
13526                .right_dock()
13527                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
13528
13529            panel
13530        });
13531
13532        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13533        let item_a = cx.new(TestItem::new);
13534        let item_b = cx.new(TestItem::new);
13535        let item_a_id = item_a.entity_id();
13536        let item_b_id = item_b.entity_id();
13537
13538        pane.update_in(cx, |pane, window, cx| {
13539            pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
13540            pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
13541        });
13542
13543        pane.read_with(cx, |pane, _| {
13544            assert_eq!(pane.items_len(), 2);
13545            assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
13546        });
13547
13548        workspace.update_in(cx, |workspace, window, cx| {
13549            workspace.toggle_panel_focus::<TestPanel>(window, cx);
13550        });
13551
13552        workspace.update_in(cx, |_, window, cx| {
13553            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
13554        });
13555
13556        // Assert that the `pane::CloseActiveItem` action is handled at the
13557        // workspace level when one of the dock panels is focused and, in that
13558        // case, the center pane's active item is closed but the focus is not
13559        // moved.
13560        cx.dispatch_action(pane::CloseActiveItem::default());
13561        cx.run_until_parked();
13562
13563        pane.read_with(cx, |pane, _| {
13564            assert_eq!(pane.items_len(), 1);
13565            assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
13566        });
13567
13568        workspace.update_in(cx, |workspace, window, cx| {
13569            assert!(workspace.right_dock().read(cx).is_open());
13570            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
13571        });
13572    }
13573
13574    #[gpui::test]
13575    async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
13576        init_test(cx);
13577        let fs = FakeFs::new(cx.executor());
13578
13579        let project_a = Project::test(fs.clone(), [], cx).await;
13580        let project_b = Project::test(fs, [], cx).await;
13581
13582        let multi_workspace_handle =
13583            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
13584        cx.run_until_parked();
13585
13586        let workspace_a = multi_workspace_handle
13587            .read_with(cx, |mw, _| mw.workspace().clone())
13588            .unwrap();
13589
13590        let _workspace_b = multi_workspace_handle
13591            .update(cx, |mw, window, cx| {
13592                mw.test_add_workspace(project_b, window, cx)
13593            })
13594            .unwrap();
13595
13596        // Switch to workspace A
13597        multi_workspace_handle
13598            .update(cx, |mw, window, cx| {
13599                mw.activate_index(0, window, cx);
13600            })
13601            .unwrap();
13602
13603        let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
13604
13605        // Add a panel to workspace A's right dock and open the dock
13606        let panel = workspace_a.update_in(cx, |workspace, window, cx| {
13607            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
13608            workspace.add_panel(panel.clone(), window, cx);
13609            workspace
13610                .right_dock()
13611                .update(cx, |dock, cx| dock.set_open(true, window, cx));
13612            panel
13613        });
13614
13615        // Focus the panel through the workspace (matching existing test pattern)
13616        workspace_a.update_in(cx, |workspace, window, cx| {
13617            workspace.toggle_panel_focus::<TestPanel>(window, cx);
13618        });
13619
13620        // Zoom the panel
13621        panel.update_in(cx, |panel, window, cx| {
13622            panel.set_zoomed(true, window, cx);
13623        });
13624
13625        // Verify the panel is zoomed and the dock is open
13626        workspace_a.update_in(cx, |workspace, window, cx| {
13627            assert!(
13628                workspace.right_dock().read(cx).is_open(),
13629                "dock should be open before switch"
13630            );
13631            assert!(
13632                panel.is_zoomed(window, cx),
13633                "panel should be zoomed before switch"
13634            );
13635            assert!(
13636                panel.read(cx).focus_handle(cx).contains_focused(window, cx),
13637                "panel should be focused before switch"
13638            );
13639        });
13640
13641        // Switch to workspace B
13642        multi_workspace_handle
13643            .update(cx, |mw, window, cx| {
13644                mw.activate_index(1, window, cx);
13645            })
13646            .unwrap();
13647        cx.run_until_parked();
13648
13649        // Switch back to workspace A
13650        multi_workspace_handle
13651            .update(cx, |mw, window, cx| {
13652                mw.activate_index(0, window, cx);
13653            })
13654            .unwrap();
13655        cx.run_until_parked();
13656
13657        // Verify the panel is still zoomed and the dock is still open
13658        workspace_a.update_in(cx, |workspace, window, cx| {
13659            assert!(
13660                workspace.right_dock().read(cx).is_open(),
13661                "dock should still be open after switching back"
13662            );
13663            assert!(
13664                panel.is_zoomed(window, cx),
13665                "panel should still be zoomed after switching back"
13666            );
13667        });
13668    }
13669
13670    fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
13671        pane.read(cx)
13672            .items()
13673            .flat_map(|item| {
13674                item.project_paths(cx)
13675                    .into_iter()
13676                    .map(|path| path.path.display(PathStyle::local()).into_owned())
13677            })
13678            .collect()
13679    }
13680
13681    pub fn init_test(cx: &mut TestAppContext) {
13682        cx.update(|cx| {
13683            let settings_store = SettingsStore::test(cx);
13684            cx.set_global(settings_store);
13685            cx.set_global(db::AppDatabase::test_new());
13686            theme::init(theme::LoadThemes::JustBase, cx);
13687        });
13688    }
13689
13690    #[gpui::test]
13691    async fn test_toggle_theme_mode_persists_and_updates_active_theme(cx: &mut TestAppContext) {
13692        use settings::{ThemeName, ThemeSelection};
13693        use theme::SystemAppearance;
13694        use zed_actions::theme::ToggleMode;
13695
13696        init_test(cx);
13697
13698        let fs = FakeFs::new(cx.executor());
13699        let settings_fs: Arc<dyn fs::Fs> = fs.clone();
13700
13701        fs.insert_tree(path!("/root"), json!({ "file.rs": "fn main() {}\n" }))
13702            .await;
13703
13704        // Build a test project and workspace view so the test can invoke
13705        // the workspace action handler the same way the UI would.
13706        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
13707        let (workspace, cx) =
13708            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13709
13710        // Seed the settings file with a plain static light theme so the
13711        // first toggle always starts from a known persisted state.
13712        workspace.update_in(cx, |_workspace, _window, cx| {
13713            *SystemAppearance::global_mut(cx) = SystemAppearance(theme::Appearance::Light);
13714            settings::update_settings_file(settings_fs.clone(), cx, |settings, _cx| {
13715                settings.theme.theme = Some(ThemeSelection::Static(ThemeName("One Light".into())));
13716            });
13717        });
13718        cx.executor().advance_clock(Duration::from_millis(200));
13719        cx.run_until_parked();
13720
13721        // Confirm the initial persisted settings contain the static theme
13722        // we just wrote before any toggling happens.
13723        let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
13724        assert!(settings_text.contains(r#""theme": "One Light""#));
13725
13726        // Toggle once. This should migrate the persisted theme settings
13727        // into light/dark slots and enable system mode.
13728        workspace.update_in(cx, |workspace, window, cx| {
13729            workspace.toggle_theme_mode(&ToggleMode, window, cx);
13730        });
13731        cx.executor().advance_clock(Duration::from_millis(200));
13732        cx.run_until_parked();
13733
13734        // 1. Static -> Dynamic
13735        // this assertion checks theme changed from static to dynamic.
13736        let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
13737        let parsed: serde_json::Value = settings::parse_json_with_comments(&settings_text).unwrap();
13738        assert_eq!(
13739            parsed["theme"],
13740            serde_json::json!({
13741                "mode": "system",
13742                "light": "One Light",
13743                "dark": "One Dark"
13744            })
13745        );
13746
13747        // 2. Toggle again, suppose it will change the mode to light
13748        workspace.update_in(cx, |workspace, window, cx| {
13749            workspace.toggle_theme_mode(&ToggleMode, window, cx);
13750        });
13751        cx.executor().advance_clock(Duration::from_millis(200));
13752        cx.run_until_parked();
13753
13754        let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
13755        assert!(settings_text.contains(r#""mode": "light""#));
13756    }
13757
13758    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
13759        let item = TestProjectItem::new(id, path, cx);
13760        item.update(cx, |item, _| {
13761            item.is_dirty = true;
13762        });
13763        item
13764    }
13765
13766    #[gpui::test]
13767    async fn test_zoomed_panel_without_pane_preserved_on_center_focus(
13768        cx: &mut gpui::TestAppContext,
13769    ) {
13770        init_test(cx);
13771        let fs = FakeFs::new(cx.executor());
13772
13773        let project = Project::test(fs, [], cx).await;
13774        let (workspace, cx) =
13775            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13776
13777        let panel = workspace.update_in(cx, |workspace, window, cx| {
13778            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
13779            workspace.add_panel(panel.clone(), window, cx);
13780            workspace
13781                .right_dock()
13782                .update(cx, |dock, cx| dock.set_open(true, window, cx));
13783            panel
13784        });
13785
13786        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13787        pane.update_in(cx, |pane, window, cx| {
13788            let item = cx.new(TestItem::new);
13789            pane.add_item(Box::new(item), true, true, None, window, cx);
13790        });
13791
13792        // Transfer focus to the panel, then zoom it. Using toggle_panel_focus
13793        // mirrors the real-world flow and avoids side effects from directly
13794        // focusing the panel while the center pane is active.
13795        workspace.update_in(cx, |workspace, window, cx| {
13796            workspace.toggle_panel_focus::<TestPanel>(window, cx);
13797        });
13798
13799        panel.update_in(cx, |panel, window, cx| {
13800            panel.set_zoomed(true, window, cx);
13801        });
13802
13803        workspace.update_in(cx, |workspace, window, cx| {
13804            assert!(workspace.right_dock().read(cx).is_open());
13805            assert!(panel.is_zoomed(window, cx));
13806            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
13807        });
13808
13809        // Simulate a spurious pane::Event::Focus on the center pane while the
13810        // panel still has focus. This mirrors what happens during macOS window
13811        // activation: the center pane fires a focus event even though actual
13812        // focus remains on the dock panel.
13813        pane.update_in(cx, |_, _, cx| {
13814            cx.emit(pane::Event::Focus);
13815        });
13816
13817        // The dock must remain open because the panel had focus at the time the
13818        // event was processed. Before the fix, dock_to_preserve was None for
13819        // panels that don't implement pane(), causing the dock to close.
13820        workspace.update_in(cx, |workspace, window, cx| {
13821            assert!(
13822                workspace.right_dock().read(cx).is_open(),
13823                "Dock should stay open when its zoomed panel (without pane()) still has focus"
13824            );
13825            assert!(panel.is_zoomed(window, cx));
13826        });
13827    }
13828}