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            assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11083        });
11084
11085        // Close the dock
11086        workspace.update_in(cx, |workspace, window, cx| {
11087            workspace.toggle_dock(DockPosition::Right, window, cx);
11088        });
11089
11090        workspace.update_in(cx, |workspace, window, cx| {
11091            assert!(!workspace.right_dock().read(cx).is_open());
11092            assert!(!panel.is_zoomed(window, cx));
11093            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11094            assert!(pane.read(cx).focus_handle(cx).contains_focused(window, cx));
11095        });
11096
11097        // Open the dock
11098        workspace.update_in(cx, |workspace, window, cx| {
11099            workspace.toggle_dock(DockPosition::Right, window, cx);
11100        });
11101
11102        workspace.update_in(cx, |workspace, window, cx| {
11103            assert!(workspace.right_dock().read(cx).is_open());
11104            assert!(!panel.is_zoomed(window, cx));
11105            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11106        });
11107
11108        // Focus and zoom panel
11109        panel.update_in(cx, |panel, window, cx| {
11110            cx.focus_self(window);
11111            panel.set_zoomed(true, window, cx)
11112        });
11113
11114        workspace.update_in(cx, |workspace, window, cx| {
11115            assert!(workspace.right_dock().read(cx).is_open());
11116            assert!(panel.is_zoomed(window, cx));
11117            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11118        });
11119
11120        // Transfer focus to the center closes the dock
11121        workspace.update_in(cx, |workspace, window, cx| {
11122            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11123        });
11124
11125        workspace.update_in(cx, |workspace, window, cx| {
11126            assert!(!workspace.right_dock().read(cx).is_open());
11127            assert!(panel.is_zoomed(window, cx));
11128            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11129        });
11130
11131        // Transferring focus back to the panel keeps it zoomed
11132        workspace.update_in(cx, |workspace, window, cx| {
11133            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11134        });
11135
11136        workspace.update_in(cx, |workspace, window, cx| {
11137            assert!(workspace.right_dock().read(cx).is_open());
11138            assert!(panel.is_zoomed(window, cx));
11139            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11140        });
11141
11142        // Close the dock while it is zoomed
11143        workspace.update_in(cx, |workspace, window, cx| {
11144            workspace.toggle_dock(DockPosition::Right, window, cx)
11145        });
11146
11147        workspace.update_in(cx, |workspace, window, cx| {
11148            assert!(!workspace.right_dock().read(cx).is_open());
11149            assert!(panel.is_zoomed(window, cx));
11150            assert!(workspace.zoomed.is_none());
11151            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11152        });
11153
11154        // Opening the dock, when it's zoomed, retains focus
11155        workspace.update_in(cx, |workspace, window, cx| {
11156            workspace.toggle_dock(DockPosition::Right, window, cx)
11157        });
11158
11159        workspace.update_in(cx, |workspace, window, cx| {
11160            assert!(workspace.right_dock().read(cx).is_open());
11161            assert!(panel.is_zoomed(window, cx));
11162            assert!(workspace.zoomed.is_some());
11163            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11164        });
11165
11166        // Unzoom and close the panel, zoom the active pane.
11167        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
11168        workspace.update_in(cx, |workspace, window, cx| {
11169            workspace.toggle_dock(DockPosition::Right, window, cx)
11170        });
11171        pane.update_in(cx, |pane, window, cx| {
11172            pane.toggle_zoom(&Default::default(), window, cx)
11173        });
11174
11175        // Opening a dock unzooms the pane.
11176        workspace.update_in(cx, |workspace, window, cx| {
11177            workspace.toggle_dock(DockPosition::Right, window, cx)
11178        });
11179        workspace.update_in(cx, |workspace, window, cx| {
11180            let pane = pane.read(cx);
11181            assert!(!pane.is_zoomed());
11182            assert!(!pane.focus_handle(cx).is_focused(window));
11183            assert!(workspace.right_dock().read(cx).is_open());
11184            assert!(workspace.zoomed.is_none());
11185        });
11186    }
11187
11188    #[gpui::test]
11189    async fn test_close_panel_on_toggle(cx: &mut gpui::TestAppContext) {
11190        init_test(cx);
11191        let fs = FakeFs::new(cx.executor());
11192
11193        let project = Project::test(fs, [], cx).await;
11194        let (workspace, cx) =
11195            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11196
11197        let panel = workspace.update_in(cx, |workspace, window, cx| {
11198            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
11199            workspace.add_panel(panel.clone(), window, cx);
11200            panel
11201        });
11202
11203        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
11204        pane.update_in(cx, |pane, window, cx| {
11205            let item = cx.new(TestItem::new);
11206            pane.add_item(Box::new(item), true, true, None, window, cx);
11207        });
11208
11209        // Enable close_panel_on_toggle
11210        cx.update_global(|store: &mut SettingsStore, cx| {
11211            store.update_user_settings(cx, |settings| {
11212                settings.workspace.close_panel_on_toggle = Some(true);
11213            });
11214        });
11215
11216        // Panel starts closed. Toggling should open and focus it.
11217        workspace.update_in(cx, |workspace, window, cx| {
11218            assert!(!workspace.right_dock().read(cx).is_open());
11219            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11220        });
11221
11222        workspace.update_in(cx, |workspace, window, cx| {
11223            assert!(
11224                workspace.right_dock().read(cx).is_open(),
11225                "Dock should be open after toggling from center"
11226            );
11227            assert!(
11228                panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11229                "Panel should be focused after toggling from center"
11230            );
11231        });
11232
11233        // Panel is open and focused. Toggling should close the panel and
11234        // return focus to the center.
11235        workspace.update_in(cx, |workspace, window, cx| {
11236            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11237        });
11238
11239        workspace.update_in(cx, |workspace, window, cx| {
11240            assert!(
11241                !workspace.right_dock().read(cx).is_open(),
11242                "Dock should be closed after toggling from focused panel"
11243            );
11244            assert!(
11245                !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11246                "Panel should not be focused after toggling from focused panel"
11247            );
11248        });
11249
11250        // Open the dock and focus something else so the panel is open but not
11251        // focused. Toggling should focus the panel (not close it).
11252        workspace.update_in(cx, |workspace, window, cx| {
11253            workspace
11254                .right_dock()
11255                .update(cx, |dock, cx| dock.set_open(true, window, cx));
11256            window.focus(&pane.read(cx).focus_handle(cx), cx);
11257        });
11258
11259        workspace.update_in(cx, |workspace, window, cx| {
11260            assert!(workspace.right_dock().read(cx).is_open());
11261            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
11262            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11263        });
11264
11265        workspace.update_in(cx, |workspace, window, cx| {
11266            assert!(
11267                workspace.right_dock().read(cx).is_open(),
11268                "Dock should remain open when toggling focuses an open-but-unfocused panel"
11269            );
11270            assert!(
11271                panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11272                "Panel should be focused after toggling an open-but-unfocused panel"
11273            );
11274        });
11275
11276        // Now disable the setting and verify the original behavior: toggling
11277        // from a focused panel moves focus to center but leaves the dock open.
11278        cx.update_global(|store: &mut SettingsStore, cx| {
11279            store.update_user_settings(cx, |settings| {
11280                settings.workspace.close_panel_on_toggle = Some(false);
11281            });
11282        });
11283
11284        workspace.update_in(cx, |workspace, window, cx| {
11285            workspace.toggle_panel_focus::<TestPanel>(window, cx);
11286        });
11287
11288        workspace.update_in(cx, |workspace, window, cx| {
11289            assert!(
11290                workspace.right_dock().read(cx).is_open(),
11291                "Dock should remain open when setting is disabled"
11292            );
11293            assert!(
11294                !panel.read(cx).focus_handle(cx).contains_focused(window, cx),
11295                "Panel should not be focused after toggling with setting disabled"
11296            );
11297        });
11298    }
11299
11300    #[gpui::test]
11301    async fn test_pane_zoom_in_out(cx: &mut TestAppContext) {
11302        init_test(cx);
11303        let fs = FakeFs::new(cx.executor());
11304
11305        let project = Project::test(fs, [], cx).await;
11306        let (workspace, cx) =
11307            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11308
11309        let pane = workspace.update_in(cx, |workspace, _window, _cx| {
11310            workspace.active_pane().clone()
11311        });
11312
11313        // Add an item to the pane so it can be zoomed
11314        workspace.update_in(cx, |workspace, window, cx| {
11315            let item = cx.new(TestItem::new);
11316            workspace.add_item(pane.clone(), Box::new(item), None, true, true, window, cx);
11317        });
11318
11319        // Initially not zoomed
11320        workspace.update_in(cx, |workspace, _window, cx| {
11321            assert!(!pane.read(cx).is_zoomed(), "Pane starts unzoomed");
11322            assert!(
11323                workspace.zoomed.is_none(),
11324                "Workspace should track no zoomed pane"
11325            );
11326            assert!(pane.read(cx).items_len() > 0, "Pane should have items");
11327        });
11328
11329        // Zoom In
11330        pane.update_in(cx, |pane, window, cx| {
11331            pane.zoom_in(&crate::ZoomIn, window, cx);
11332        });
11333
11334        workspace.update_in(cx, |workspace, window, cx| {
11335            assert!(
11336                pane.read(cx).is_zoomed(),
11337                "Pane should be zoomed after ZoomIn"
11338            );
11339            assert!(
11340                workspace.zoomed.is_some(),
11341                "Workspace should track the zoomed pane"
11342            );
11343            assert!(
11344                pane.read(cx).focus_handle(cx).contains_focused(window, cx),
11345                "ZoomIn should focus the pane"
11346            );
11347        });
11348
11349        // Zoom In again is a no-op
11350        pane.update_in(cx, |pane, window, cx| {
11351            pane.zoom_in(&crate::ZoomIn, window, cx);
11352        });
11353
11354        workspace.update_in(cx, |workspace, window, cx| {
11355            assert!(pane.read(cx).is_zoomed(), "Second ZoomIn keeps pane zoomed");
11356            assert!(
11357                workspace.zoomed.is_some(),
11358                "Workspace still tracks zoomed pane"
11359            );
11360            assert!(
11361                pane.read(cx).focus_handle(cx).contains_focused(window, cx),
11362                "Pane remains focused after repeated ZoomIn"
11363            );
11364        });
11365
11366        // Zoom Out
11367        pane.update_in(cx, |pane, window, cx| {
11368            pane.zoom_out(&crate::ZoomOut, window, cx);
11369        });
11370
11371        workspace.update_in(cx, |workspace, _window, cx| {
11372            assert!(
11373                !pane.read(cx).is_zoomed(),
11374                "Pane should unzoom after ZoomOut"
11375            );
11376            assert!(
11377                workspace.zoomed.is_none(),
11378                "Workspace clears zoom tracking after ZoomOut"
11379            );
11380        });
11381
11382        // Zoom Out again is a no-op
11383        pane.update_in(cx, |pane, window, cx| {
11384            pane.zoom_out(&crate::ZoomOut, window, cx);
11385        });
11386
11387        workspace.update_in(cx, |workspace, _window, cx| {
11388            assert!(
11389                !pane.read(cx).is_zoomed(),
11390                "Second ZoomOut keeps pane unzoomed"
11391            );
11392            assert!(
11393                workspace.zoomed.is_none(),
11394                "Workspace remains without zoomed pane"
11395            );
11396        });
11397    }
11398
11399    #[gpui::test]
11400    async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
11401        init_test(cx);
11402        let fs = FakeFs::new(cx.executor());
11403
11404        let project = Project::test(fs, [], cx).await;
11405        let (workspace, cx) =
11406            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11407        workspace.update_in(cx, |workspace, window, cx| {
11408            // Open two docks
11409            let left_dock = workspace.dock_at_position(DockPosition::Left);
11410            let right_dock = workspace.dock_at_position(DockPosition::Right);
11411
11412            left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11413            right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11414
11415            assert!(left_dock.read(cx).is_open());
11416            assert!(right_dock.read(cx).is_open());
11417        });
11418
11419        workspace.update_in(cx, |workspace, window, cx| {
11420            // Toggle all docks - should close both
11421            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11422
11423            let left_dock = workspace.dock_at_position(DockPosition::Left);
11424            let right_dock = workspace.dock_at_position(DockPosition::Right);
11425            assert!(!left_dock.read(cx).is_open());
11426            assert!(!right_dock.read(cx).is_open());
11427        });
11428
11429        workspace.update_in(cx, |workspace, window, cx| {
11430            // Toggle again - should reopen both
11431            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11432
11433            let left_dock = workspace.dock_at_position(DockPosition::Left);
11434            let right_dock = workspace.dock_at_position(DockPosition::Right);
11435            assert!(left_dock.read(cx).is_open());
11436            assert!(right_dock.read(cx).is_open());
11437        });
11438    }
11439
11440    #[gpui::test]
11441    async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
11442        init_test(cx);
11443        let fs = FakeFs::new(cx.executor());
11444
11445        let project = Project::test(fs, [], cx).await;
11446        let (workspace, cx) =
11447            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11448        workspace.update_in(cx, |workspace, window, cx| {
11449            // Open two docks
11450            let left_dock = workspace.dock_at_position(DockPosition::Left);
11451            let right_dock = workspace.dock_at_position(DockPosition::Right);
11452
11453            left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11454            right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
11455
11456            assert!(left_dock.read(cx).is_open());
11457            assert!(right_dock.read(cx).is_open());
11458        });
11459
11460        workspace.update_in(cx, |workspace, window, cx| {
11461            // Close them manually
11462            workspace.toggle_dock(DockPosition::Left, window, cx);
11463            workspace.toggle_dock(DockPosition::Right, window, cx);
11464
11465            let left_dock = workspace.dock_at_position(DockPosition::Left);
11466            let right_dock = workspace.dock_at_position(DockPosition::Right);
11467            assert!(!left_dock.read(cx).is_open());
11468            assert!(!right_dock.read(cx).is_open());
11469        });
11470
11471        workspace.update_in(cx, |workspace, window, cx| {
11472            // Toggle all docks - only last closed (right dock) should reopen
11473            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11474
11475            let left_dock = workspace.dock_at_position(DockPosition::Left);
11476            let right_dock = workspace.dock_at_position(DockPosition::Right);
11477            assert!(!left_dock.read(cx).is_open());
11478            assert!(right_dock.read(cx).is_open());
11479        });
11480    }
11481
11482    #[gpui::test]
11483    async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
11484        init_test(cx);
11485        let fs = FakeFs::new(cx.executor());
11486        let project = Project::test(fs, [], cx).await;
11487        let (multi_workspace, cx) =
11488            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
11489        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
11490
11491        // Open two docks (left and right) with one panel each
11492        let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
11493            let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
11494            workspace.add_panel(left_panel.clone(), window, cx);
11495
11496            let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
11497            workspace.add_panel(right_panel.clone(), window, cx);
11498
11499            workspace.toggle_dock(DockPosition::Left, window, cx);
11500            workspace.toggle_dock(DockPosition::Right, window, cx);
11501
11502            // Verify initial state
11503            assert!(
11504                workspace.left_dock().read(cx).is_open(),
11505                "Left dock should be open"
11506            );
11507            assert_eq!(
11508                workspace
11509                    .left_dock()
11510                    .read(cx)
11511                    .visible_panel()
11512                    .unwrap()
11513                    .panel_id(),
11514                left_panel.panel_id(),
11515                "Left panel should be visible in left dock"
11516            );
11517            assert!(
11518                workspace.right_dock().read(cx).is_open(),
11519                "Right dock should be open"
11520            );
11521            assert_eq!(
11522                workspace
11523                    .right_dock()
11524                    .read(cx)
11525                    .visible_panel()
11526                    .unwrap()
11527                    .panel_id(),
11528                right_panel.panel_id(),
11529                "Right panel should be visible in right dock"
11530            );
11531            assert!(
11532                !workspace.bottom_dock().read(cx).is_open(),
11533                "Bottom dock should be closed"
11534            );
11535
11536            (left_panel, right_panel)
11537        });
11538
11539        // Focus the left panel and move it to the next position (bottom dock)
11540        workspace.update_in(cx, |workspace, window, cx| {
11541            workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
11542            assert!(
11543                left_panel.read(cx).focus_handle(cx).is_focused(window),
11544                "Left panel should be focused"
11545            );
11546        });
11547
11548        cx.dispatch_action(MoveFocusedPanelToNextPosition);
11549
11550        // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
11551        workspace.update(cx, |workspace, cx| {
11552            assert!(
11553                !workspace.left_dock().read(cx).is_open(),
11554                "Left dock should be closed"
11555            );
11556            assert!(
11557                workspace.bottom_dock().read(cx).is_open(),
11558                "Bottom dock should now be open"
11559            );
11560            assert_eq!(
11561                left_panel.read(cx).position,
11562                DockPosition::Bottom,
11563                "Left panel should now be in the bottom dock"
11564            );
11565            assert_eq!(
11566                workspace
11567                    .bottom_dock()
11568                    .read(cx)
11569                    .visible_panel()
11570                    .unwrap()
11571                    .panel_id(),
11572                left_panel.panel_id(),
11573                "Left panel should be the visible panel in the bottom dock"
11574            );
11575        });
11576
11577        // Toggle all docks off
11578        workspace.update_in(cx, |workspace, window, cx| {
11579            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11580            assert!(
11581                !workspace.left_dock().read(cx).is_open(),
11582                "Left dock should be closed"
11583            );
11584            assert!(
11585                !workspace.right_dock().read(cx).is_open(),
11586                "Right dock should be closed"
11587            );
11588            assert!(
11589                !workspace.bottom_dock().read(cx).is_open(),
11590                "Bottom dock should be closed"
11591            );
11592        });
11593
11594        // Toggle all docks back on and verify positions are restored
11595        workspace.update_in(cx, |workspace, window, cx| {
11596            workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
11597            assert!(
11598                !workspace.left_dock().read(cx).is_open(),
11599                "Left dock should remain closed"
11600            );
11601            assert!(
11602                workspace.right_dock().read(cx).is_open(),
11603                "Right dock should remain open"
11604            );
11605            assert!(
11606                workspace.bottom_dock().read(cx).is_open(),
11607                "Bottom dock should remain open"
11608            );
11609            assert_eq!(
11610                left_panel.read(cx).position,
11611                DockPosition::Bottom,
11612                "Left panel should remain in the bottom dock"
11613            );
11614            assert_eq!(
11615                right_panel.read(cx).position,
11616                DockPosition::Right,
11617                "Right panel should remain in the right dock"
11618            );
11619            assert_eq!(
11620                workspace
11621                    .bottom_dock()
11622                    .read(cx)
11623                    .visible_panel()
11624                    .unwrap()
11625                    .panel_id(),
11626                left_panel.panel_id(),
11627                "Left panel should be the visible panel in the right dock"
11628            );
11629        });
11630    }
11631
11632    #[gpui::test]
11633    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
11634        init_test(cx);
11635
11636        let fs = FakeFs::new(cx.executor());
11637
11638        let project = Project::test(fs, None, cx).await;
11639        let (workspace, cx) =
11640            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11641
11642        // Let's arrange the panes like this:
11643        //
11644        // +-----------------------+
11645        // |         top           |
11646        // +------+--------+-------+
11647        // | left | center | right |
11648        // +------+--------+-------+
11649        // |        bottom         |
11650        // +-----------------------+
11651
11652        let top_item = cx.new(|cx| {
11653            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
11654        });
11655        let bottom_item = cx.new(|cx| {
11656            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
11657        });
11658        let left_item = cx.new(|cx| {
11659            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
11660        });
11661        let right_item = cx.new(|cx| {
11662            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
11663        });
11664        let center_item = cx.new(|cx| {
11665            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
11666        });
11667
11668        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11669            let top_pane_id = workspace.active_pane().entity_id();
11670            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
11671            workspace.split_pane(
11672                workspace.active_pane().clone(),
11673                SplitDirection::Down,
11674                window,
11675                cx,
11676            );
11677            top_pane_id
11678        });
11679        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11680            let bottom_pane_id = workspace.active_pane().entity_id();
11681            workspace.add_item_to_active_pane(
11682                Box::new(bottom_item.clone()),
11683                None,
11684                false,
11685                window,
11686                cx,
11687            );
11688            workspace.split_pane(
11689                workspace.active_pane().clone(),
11690                SplitDirection::Up,
11691                window,
11692                cx,
11693            );
11694            bottom_pane_id
11695        });
11696        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11697            let left_pane_id = workspace.active_pane().entity_id();
11698            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
11699            workspace.split_pane(
11700                workspace.active_pane().clone(),
11701                SplitDirection::Right,
11702                window,
11703                cx,
11704            );
11705            left_pane_id
11706        });
11707        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11708            let right_pane_id = workspace.active_pane().entity_id();
11709            workspace.add_item_to_active_pane(
11710                Box::new(right_item.clone()),
11711                None,
11712                false,
11713                window,
11714                cx,
11715            );
11716            workspace.split_pane(
11717                workspace.active_pane().clone(),
11718                SplitDirection::Left,
11719                window,
11720                cx,
11721            );
11722            right_pane_id
11723        });
11724        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
11725            let center_pane_id = workspace.active_pane().entity_id();
11726            workspace.add_item_to_active_pane(
11727                Box::new(center_item.clone()),
11728                None,
11729                false,
11730                window,
11731                cx,
11732            );
11733            center_pane_id
11734        });
11735        cx.executor().run_until_parked();
11736
11737        workspace.update_in(cx, |workspace, window, cx| {
11738            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
11739
11740            // Join into next from center pane into right
11741            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11742        });
11743
11744        workspace.update_in(cx, |workspace, window, cx| {
11745            let active_pane = workspace.active_pane();
11746            assert_eq!(right_pane_id, active_pane.entity_id());
11747            assert_eq!(2, active_pane.read(cx).items_len());
11748            let item_ids_in_pane =
11749                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11750            assert!(item_ids_in_pane.contains(&center_item.item_id()));
11751            assert!(item_ids_in_pane.contains(&right_item.item_id()));
11752
11753            // Join into next from right pane into bottom
11754            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11755        });
11756
11757        workspace.update_in(cx, |workspace, window, cx| {
11758            let active_pane = workspace.active_pane();
11759            assert_eq!(bottom_pane_id, active_pane.entity_id());
11760            assert_eq!(3, active_pane.read(cx).items_len());
11761            let item_ids_in_pane =
11762                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11763            assert!(item_ids_in_pane.contains(&center_item.item_id()));
11764            assert!(item_ids_in_pane.contains(&right_item.item_id()));
11765            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11766
11767            // Join into next from bottom pane into left
11768            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11769        });
11770
11771        workspace.update_in(cx, |workspace, window, cx| {
11772            let active_pane = workspace.active_pane();
11773            assert_eq!(left_pane_id, active_pane.entity_id());
11774            assert_eq!(4, active_pane.read(cx).items_len());
11775            let item_ids_in_pane =
11776                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11777            assert!(item_ids_in_pane.contains(&center_item.item_id()));
11778            assert!(item_ids_in_pane.contains(&right_item.item_id()));
11779            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11780            assert!(item_ids_in_pane.contains(&left_item.item_id()));
11781
11782            // Join into next from left pane into top
11783            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
11784        });
11785
11786        workspace.update_in(cx, |workspace, window, cx| {
11787            let active_pane = workspace.active_pane();
11788            assert_eq!(top_pane_id, active_pane.entity_id());
11789            assert_eq!(5, active_pane.read(cx).items_len());
11790            let item_ids_in_pane =
11791                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
11792            assert!(item_ids_in_pane.contains(&center_item.item_id()));
11793            assert!(item_ids_in_pane.contains(&right_item.item_id()));
11794            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
11795            assert!(item_ids_in_pane.contains(&left_item.item_id()));
11796            assert!(item_ids_in_pane.contains(&top_item.item_id()));
11797
11798            // Single pane left: no-op
11799            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
11800        });
11801
11802        workspace.update(cx, |workspace, _cx| {
11803            let active_pane = workspace.active_pane();
11804            assert_eq!(top_pane_id, active_pane.entity_id());
11805        });
11806    }
11807
11808    fn add_an_item_to_active_pane(
11809        cx: &mut VisualTestContext,
11810        workspace: &Entity<Workspace>,
11811        item_id: u64,
11812    ) -> Entity<TestItem> {
11813        let item = cx.new(|cx| {
11814            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
11815                item_id,
11816                "item{item_id}.txt",
11817                cx,
11818            )])
11819        });
11820        workspace.update_in(cx, |workspace, window, cx| {
11821            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
11822        });
11823        item
11824    }
11825
11826    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
11827        workspace.update_in(cx, |workspace, window, cx| {
11828            workspace.split_pane(
11829                workspace.active_pane().clone(),
11830                SplitDirection::Right,
11831                window,
11832                cx,
11833            )
11834        })
11835    }
11836
11837    #[gpui::test]
11838    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
11839        init_test(cx);
11840        let fs = FakeFs::new(cx.executor());
11841        let project = Project::test(fs, None, cx).await;
11842        let (workspace, cx) =
11843            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
11844
11845        add_an_item_to_active_pane(cx, &workspace, 1);
11846        split_pane(cx, &workspace);
11847        add_an_item_to_active_pane(cx, &workspace, 2);
11848        split_pane(cx, &workspace); // empty pane
11849        split_pane(cx, &workspace);
11850        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
11851
11852        cx.executor().run_until_parked();
11853
11854        workspace.update(cx, |workspace, cx| {
11855            let num_panes = workspace.panes().len();
11856            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
11857            let active_item = workspace
11858                .active_pane()
11859                .read(cx)
11860                .active_item()
11861                .expect("item is in focus");
11862
11863            assert_eq!(num_panes, 4);
11864            assert_eq!(num_items_in_current_pane, 1);
11865            assert_eq!(active_item.item_id(), last_item.item_id());
11866        });
11867
11868        workspace.update_in(cx, |workspace, window, cx| {
11869            workspace.join_all_panes(window, cx);
11870        });
11871
11872        workspace.update(cx, |workspace, cx| {
11873            let num_panes = workspace.panes().len();
11874            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
11875            let active_item = workspace
11876                .active_pane()
11877                .read(cx)
11878                .active_item()
11879                .expect("item is in focus");
11880
11881            assert_eq!(num_panes, 1);
11882            assert_eq!(num_items_in_current_pane, 3);
11883            assert_eq!(active_item.item_id(), last_item.item_id());
11884        });
11885    }
11886    struct TestModal(FocusHandle);
11887
11888    impl TestModal {
11889        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
11890            Self(cx.focus_handle())
11891        }
11892    }
11893
11894    impl EventEmitter<DismissEvent> for TestModal {}
11895
11896    impl Focusable for TestModal {
11897        fn focus_handle(&self, _cx: &App) -> FocusHandle {
11898            self.0.clone()
11899        }
11900    }
11901
11902    impl ModalView for TestModal {}
11903
11904    impl Render for TestModal {
11905        fn render(
11906            &mut self,
11907            _window: &mut Window,
11908            _cx: &mut Context<TestModal>,
11909        ) -> impl IntoElement {
11910            div().track_focus(&self.0)
11911        }
11912    }
11913
11914    #[gpui::test]
11915    async fn test_panels(cx: &mut gpui::TestAppContext) {
11916        init_test(cx);
11917        let fs = FakeFs::new(cx.executor());
11918
11919        let project = Project::test(fs, [], cx).await;
11920        let (multi_workspace, cx) =
11921            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
11922        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
11923
11924        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
11925            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
11926            workspace.add_panel(panel_1.clone(), window, cx);
11927            workspace.toggle_dock(DockPosition::Left, window, cx);
11928            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx));
11929            workspace.add_panel(panel_2.clone(), window, cx);
11930            workspace.toggle_dock(DockPosition::Right, window, cx);
11931
11932            let left_dock = workspace.left_dock();
11933            assert_eq!(
11934                left_dock.read(cx).visible_panel().unwrap().panel_id(),
11935                panel_1.panel_id()
11936            );
11937            assert_eq!(
11938                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
11939                panel_1.size(window, cx)
11940            );
11941
11942            left_dock.update(cx, |left_dock, cx| {
11943                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
11944            });
11945            assert_eq!(
11946                workspace
11947                    .right_dock()
11948                    .read(cx)
11949                    .visible_panel()
11950                    .unwrap()
11951                    .panel_id(),
11952                panel_2.panel_id(),
11953            );
11954
11955            (panel_1, panel_2)
11956        });
11957
11958        // Move panel_1 to the right
11959        panel_1.update_in(cx, |panel_1, window, cx| {
11960            panel_1.set_position(DockPosition::Right, window, cx)
11961        });
11962
11963        workspace.update_in(cx, |workspace, window, cx| {
11964            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
11965            // Since it was the only panel on the left, the left dock should now be closed.
11966            assert!(!workspace.left_dock().read(cx).is_open());
11967            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
11968            let right_dock = workspace.right_dock();
11969            assert_eq!(
11970                right_dock.read(cx).visible_panel().unwrap().panel_id(),
11971                panel_1.panel_id()
11972            );
11973            assert_eq!(
11974                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
11975                px(1337.)
11976            );
11977
11978            // Now we move panel_2 to the left
11979            panel_2.set_position(DockPosition::Left, window, cx);
11980        });
11981
11982        workspace.update(cx, |workspace, cx| {
11983            // Since panel_2 was not visible on the right, we don't open the left dock.
11984            assert!(!workspace.left_dock().read(cx).is_open());
11985            // And the right dock is unaffected in its displaying of panel_1
11986            assert!(workspace.right_dock().read(cx).is_open());
11987            assert_eq!(
11988                workspace
11989                    .right_dock()
11990                    .read(cx)
11991                    .visible_panel()
11992                    .unwrap()
11993                    .panel_id(),
11994                panel_1.panel_id(),
11995            );
11996        });
11997
11998        // Move panel_1 back to the left
11999        panel_1.update_in(cx, |panel_1, window, cx| {
12000            panel_1.set_position(DockPosition::Left, window, cx)
12001        });
12002
12003        workspace.update_in(cx, |workspace, window, cx| {
12004            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
12005            let left_dock = workspace.left_dock();
12006            assert!(left_dock.read(cx).is_open());
12007            assert_eq!(
12008                left_dock.read(cx).visible_panel().unwrap().panel_id(),
12009                panel_1.panel_id()
12010            );
12011            assert_eq!(
12012                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
12013                px(1337.)
12014            );
12015            // And the right dock should be closed as it no longer has any panels.
12016            assert!(!workspace.right_dock().read(cx).is_open());
12017
12018            // Now we move panel_1 to the bottom
12019            panel_1.set_position(DockPosition::Bottom, window, cx);
12020        });
12021
12022        workspace.update_in(cx, |workspace, window, cx| {
12023            // Since panel_1 was visible on the left, we close the left dock.
12024            assert!(!workspace.left_dock().read(cx).is_open());
12025            // The bottom dock is sized based on the panel's default size,
12026            // since the panel orientation changed from vertical to horizontal.
12027            let bottom_dock = workspace.bottom_dock();
12028            assert_eq!(
12029                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
12030                panel_1.size(window, cx),
12031            );
12032            // Close bottom dock and move panel_1 back to the left.
12033            bottom_dock.update(cx, |bottom_dock, cx| {
12034                bottom_dock.set_open(false, window, cx)
12035            });
12036            panel_1.set_position(DockPosition::Left, window, cx);
12037        });
12038
12039        // Emit activated event on panel 1
12040        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
12041
12042        // Now the left dock is open and panel_1 is active and focused.
12043        workspace.update_in(cx, |workspace, window, cx| {
12044            let left_dock = workspace.left_dock();
12045            assert!(left_dock.read(cx).is_open());
12046            assert_eq!(
12047                left_dock.read(cx).visible_panel().unwrap().panel_id(),
12048                panel_1.panel_id(),
12049            );
12050            assert!(panel_1.focus_handle(cx).is_focused(window));
12051        });
12052
12053        // Emit closed event on panel 2, which is not active
12054        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
12055
12056        // Wo don't close the left dock, because panel_2 wasn't the active panel
12057        workspace.update(cx, |workspace, cx| {
12058            let left_dock = workspace.left_dock();
12059            assert!(left_dock.read(cx).is_open());
12060            assert_eq!(
12061                left_dock.read(cx).visible_panel().unwrap().panel_id(),
12062                panel_1.panel_id(),
12063            );
12064        });
12065
12066        // Emitting a ZoomIn event shows the panel as zoomed.
12067        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
12068        workspace.read_with(cx, |workspace, _| {
12069            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12070            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
12071        });
12072
12073        // Move panel to another dock while it is zoomed
12074        panel_1.update_in(cx, |panel, window, cx| {
12075            panel.set_position(DockPosition::Right, window, cx)
12076        });
12077        workspace.read_with(cx, |workspace, _| {
12078            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12079
12080            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12081        });
12082
12083        // This is a helper for getting a:
12084        // - valid focus on an element,
12085        // - that isn't a part of the panes and panels system of the Workspace,
12086        // - and doesn't trigger the 'on_focus_lost' API.
12087        let focus_other_view = {
12088            let workspace = workspace.clone();
12089            move |cx: &mut VisualTestContext| {
12090                workspace.update_in(cx, |workspace, window, cx| {
12091                    if workspace.active_modal::<TestModal>(cx).is_some() {
12092                        workspace.toggle_modal(window, cx, TestModal::new);
12093                        workspace.toggle_modal(window, cx, TestModal::new);
12094                    } else {
12095                        workspace.toggle_modal(window, cx, TestModal::new);
12096                    }
12097                })
12098            }
12099        };
12100
12101        // If focus is transferred to another view that's not a panel or another pane, we still show
12102        // the panel as zoomed.
12103        focus_other_view(cx);
12104        workspace.read_with(cx, |workspace, _| {
12105            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12106            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12107        });
12108
12109        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
12110        workspace.update_in(cx, |_workspace, window, cx| {
12111            cx.focus_self(window);
12112        });
12113        workspace.read_with(cx, |workspace, _| {
12114            assert_eq!(workspace.zoomed, None);
12115            assert_eq!(workspace.zoomed_position, None);
12116        });
12117
12118        // If focus is transferred again to another view that's not a panel or a pane, we won't
12119        // show the panel as zoomed because it wasn't zoomed before.
12120        focus_other_view(cx);
12121        workspace.read_with(cx, |workspace, _| {
12122            assert_eq!(workspace.zoomed, None);
12123            assert_eq!(workspace.zoomed_position, None);
12124        });
12125
12126        // When the panel is activated, it is zoomed again.
12127        cx.dispatch_action(ToggleRightDock);
12128        workspace.read_with(cx, |workspace, _| {
12129            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
12130            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
12131        });
12132
12133        // Emitting a ZoomOut event unzooms the panel.
12134        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
12135        workspace.read_with(cx, |workspace, _| {
12136            assert_eq!(workspace.zoomed, None);
12137            assert_eq!(workspace.zoomed_position, None);
12138        });
12139
12140        // Emit closed event on panel 1, which is active
12141        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
12142
12143        // Now the left dock is closed, because panel_1 was the active panel
12144        workspace.update(cx, |workspace, cx| {
12145            let right_dock = workspace.right_dock();
12146            assert!(!right_dock.read(cx).is_open());
12147        });
12148    }
12149
12150    #[gpui::test]
12151    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
12152        init_test(cx);
12153
12154        let fs = FakeFs::new(cx.background_executor.clone());
12155        let project = Project::test(fs, [], cx).await;
12156        let (workspace, cx) =
12157            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12158        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12159
12160        let dirty_regular_buffer = cx.new(|cx| {
12161            TestItem::new(cx)
12162                .with_dirty(true)
12163                .with_label("1.txt")
12164                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
12165        });
12166        let dirty_regular_buffer_2 = cx.new(|cx| {
12167            TestItem::new(cx)
12168                .with_dirty(true)
12169                .with_label("2.txt")
12170                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
12171        });
12172        let dirty_multi_buffer_with_both = cx.new(|cx| {
12173            TestItem::new(cx)
12174                .with_dirty(true)
12175                .with_buffer_kind(ItemBufferKind::Multibuffer)
12176                .with_label("Fake Project Search")
12177                .with_project_items(&[
12178                    dirty_regular_buffer.read(cx).project_items[0].clone(),
12179                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
12180                ])
12181        });
12182        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
12183        workspace.update_in(cx, |workspace, window, cx| {
12184            workspace.add_item(
12185                pane.clone(),
12186                Box::new(dirty_regular_buffer.clone()),
12187                None,
12188                false,
12189                false,
12190                window,
12191                cx,
12192            );
12193            workspace.add_item(
12194                pane.clone(),
12195                Box::new(dirty_regular_buffer_2.clone()),
12196                None,
12197                false,
12198                false,
12199                window,
12200                cx,
12201            );
12202            workspace.add_item(
12203                pane.clone(),
12204                Box::new(dirty_multi_buffer_with_both.clone()),
12205                None,
12206                false,
12207                false,
12208                window,
12209                cx,
12210            );
12211        });
12212
12213        pane.update_in(cx, |pane, window, cx| {
12214            pane.activate_item(2, true, true, window, cx);
12215            assert_eq!(
12216                pane.active_item().unwrap().item_id(),
12217                multi_buffer_with_both_files_id,
12218                "Should select the multi buffer in the pane"
12219            );
12220        });
12221        let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12222            pane.close_other_items(
12223                &CloseOtherItems {
12224                    save_intent: Some(SaveIntent::Save),
12225                    close_pinned: true,
12226                },
12227                None,
12228                window,
12229                cx,
12230            )
12231        });
12232        cx.background_executor.run_until_parked();
12233        assert!(!cx.has_pending_prompt());
12234        close_all_but_multi_buffer_task
12235            .await
12236            .expect("Closing all buffers but the multi buffer failed");
12237        pane.update(cx, |pane, cx| {
12238            assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
12239            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
12240            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
12241            assert_eq!(pane.items_len(), 1);
12242            assert_eq!(
12243                pane.active_item().unwrap().item_id(),
12244                multi_buffer_with_both_files_id,
12245                "Should have only the multi buffer left in the pane"
12246            );
12247            assert!(
12248                dirty_multi_buffer_with_both.read(cx).is_dirty,
12249                "The multi buffer containing the unsaved buffer should still be dirty"
12250            );
12251        });
12252
12253        dirty_regular_buffer.update(cx, |buffer, cx| {
12254            buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
12255        });
12256
12257        let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12258            pane.close_active_item(
12259                &CloseActiveItem {
12260                    save_intent: Some(SaveIntent::Close),
12261                    close_pinned: false,
12262                },
12263                window,
12264                cx,
12265            )
12266        });
12267        cx.background_executor.run_until_parked();
12268        assert!(
12269            cx.has_pending_prompt(),
12270            "Dirty multi buffer should prompt a save dialog"
12271        );
12272        cx.simulate_prompt_answer("Save");
12273        cx.background_executor.run_until_parked();
12274        close_multi_buffer_task
12275            .await
12276            .expect("Closing the multi buffer failed");
12277        pane.update(cx, |pane, cx| {
12278            assert_eq!(
12279                dirty_multi_buffer_with_both.read(cx).save_count,
12280                1,
12281                "Multi buffer item should get be saved"
12282            );
12283            // Test impl does not save inner items, so we do not assert them
12284            assert_eq!(
12285                pane.items_len(),
12286                0,
12287                "No more items should be left in the pane"
12288            );
12289            assert!(pane.active_item().is_none());
12290        });
12291    }
12292
12293    #[gpui::test]
12294    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
12295        cx: &mut TestAppContext,
12296    ) {
12297        init_test(cx);
12298
12299        let fs = FakeFs::new(cx.background_executor.clone());
12300        let project = Project::test(fs, [], cx).await;
12301        let (workspace, cx) =
12302            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12303        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12304
12305        let dirty_regular_buffer = cx.new(|cx| {
12306            TestItem::new(cx)
12307                .with_dirty(true)
12308                .with_label("1.txt")
12309                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
12310        });
12311        let dirty_regular_buffer_2 = cx.new(|cx| {
12312            TestItem::new(cx)
12313                .with_dirty(true)
12314                .with_label("2.txt")
12315                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
12316        });
12317        let clear_regular_buffer = cx.new(|cx| {
12318            TestItem::new(cx)
12319                .with_label("3.txt")
12320                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
12321        });
12322
12323        let dirty_multi_buffer_with_both = cx.new(|cx| {
12324            TestItem::new(cx)
12325                .with_dirty(true)
12326                .with_buffer_kind(ItemBufferKind::Multibuffer)
12327                .with_label("Fake Project Search")
12328                .with_project_items(&[
12329                    dirty_regular_buffer.read(cx).project_items[0].clone(),
12330                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
12331                    clear_regular_buffer.read(cx).project_items[0].clone(),
12332                ])
12333        });
12334        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
12335        workspace.update_in(cx, |workspace, window, cx| {
12336            workspace.add_item(
12337                pane.clone(),
12338                Box::new(dirty_regular_buffer.clone()),
12339                None,
12340                false,
12341                false,
12342                window,
12343                cx,
12344            );
12345            workspace.add_item(
12346                pane.clone(),
12347                Box::new(dirty_multi_buffer_with_both.clone()),
12348                None,
12349                false,
12350                false,
12351                window,
12352                cx,
12353            );
12354        });
12355
12356        pane.update_in(cx, |pane, window, cx| {
12357            pane.activate_item(1, true, true, window, cx);
12358            assert_eq!(
12359                pane.active_item().unwrap().item_id(),
12360                multi_buffer_with_both_files_id,
12361                "Should select the multi buffer in the pane"
12362            );
12363        });
12364        let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12365            pane.close_active_item(
12366                &CloseActiveItem {
12367                    save_intent: None,
12368                    close_pinned: false,
12369                },
12370                window,
12371                cx,
12372            )
12373        });
12374        cx.background_executor.run_until_parked();
12375        assert!(
12376            cx.has_pending_prompt(),
12377            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
12378        );
12379    }
12380
12381    /// Tests that when `close_on_file_delete` is enabled, files are automatically
12382    /// closed when they are deleted from disk.
12383    #[gpui::test]
12384    async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) {
12385        init_test(cx);
12386
12387        // Enable the close_on_disk_deletion setting
12388        cx.update_global(|store: &mut SettingsStore, cx| {
12389            store.update_user_settings(cx, |settings| {
12390                settings.workspace.close_on_file_delete = Some(true);
12391            });
12392        });
12393
12394        let fs = FakeFs::new(cx.background_executor.clone());
12395        let project = Project::test(fs, [], cx).await;
12396        let (workspace, cx) =
12397            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12398        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12399
12400        // Create a test item that simulates a file
12401        let item = cx.new(|cx| {
12402            TestItem::new(cx)
12403                .with_label("test.txt")
12404                .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12405        });
12406
12407        // Add item to workspace
12408        workspace.update_in(cx, |workspace, window, cx| {
12409            workspace.add_item(
12410                pane.clone(),
12411                Box::new(item.clone()),
12412                None,
12413                false,
12414                false,
12415                window,
12416                cx,
12417            );
12418        });
12419
12420        // Verify the item is in the pane
12421        pane.read_with(cx, |pane, _| {
12422            assert_eq!(pane.items().count(), 1);
12423        });
12424
12425        // Simulate file deletion by setting the item's deleted state
12426        item.update(cx, |item, _| {
12427            item.set_has_deleted_file(true);
12428        });
12429
12430        // Emit UpdateTab event to trigger the close behavior
12431        cx.run_until_parked();
12432        item.update(cx, |_, cx| {
12433            cx.emit(ItemEvent::UpdateTab);
12434        });
12435
12436        // Allow the close operation to complete
12437        cx.run_until_parked();
12438
12439        // Verify the item was automatically closed
12440        pane.read_with(cx, |pane, _| {
12441            assert_eq!(
12442                pane.items().count(),
12443                0,
12444                "Item should be automatically closed when file is deleted"
12445            );
12446        });
12447    }
12448
12449    /// Tests that when `close_on_file_delete` is disabled (default), files remain
12450    /// open with a strikethrough when they are deleted from disk.
12451    #[gpui::test]
12452    async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) {
12453        init_test(cx);
12454
12455        // Ensure close_on_disk_deletion is disabled (default)
12456        cx.update_global(|store: &mut SettingsStore, cx| {
12457            store.update_user_settings(cx, |settings| {
12458                settings.workspace.close_on_file_delete = Some(false);
12459            });
12460        });
12461
12462        let fs = FakeFs::new(cx.background_executor.clone());
12463        let project = Project::test(fs, [], cx).await;
12464        let (workspace, cx) =
12465            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12466        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12467
12468        // Create a test item that simulates a file
12469        let item = cx.new(|cx| {
12470            TestItem::new(cx)
12471                .with_label("test.txt")
12472                .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12473        });
12474
12475        // Add item to workspace
12476        workspace.update_in(cx, |workspace, window, cx| {
12477            workspace.add_item(
12478                pane.clone(),
12479                Box::new(item.clone()),
12480                None,
12481                false,
12482                false,
12483                window,
12484                cx,
12485            );
12486        });
12487
12488        // Verify the item is in the pane
12489        pane.read_with(cx, |pane, _| {
12490            assert_eq!(pane.items().count(), 1);
12491        });
12492
12493        // Simulate file deletion
12494        item.update(cx, |item, _| {
12495            item.set_has_deleted_file(true);
12496        });
12497
12498        // Emit UpdateTab event
12499        cx.run_until_parked();
12500        item.update(cx, |_, cx| {
12501            cx.emit(ItemEvent::UpdateTab);
12502        });
12503
12504        // Allow any potential close operation to complete
12505        cx.run_until_parked();
12506
12507        // Verify the item remains open (with strikethrough)
12508        pane.read_with(cx, |pane, _| {
12509            assert_eq!(
12510                pane.items().count(),
12511                1,
12512                "Item should remain open when close_on_disk_deletion is disabled"
12513            );
12514        });
12515
12516        // Verify the item shows as deleted
12517        item.read_with(cx, |item, _| {
12518            assert!(
12519                item.has_deleted_file,
12520                "Item should be marked as having deleted file"
12521            );
12522        });
12523    }
12524
12525    /// Tests that dirty files are not automatically closed when deleted from disk,
12526    /// even when `close_on_file_delete` is enabled. This ensures users don't lose
12527    /// unsaved changes without being prompted.
12528    #[gpui::test]
12529    async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) {
12530        init_test(cx);
12531
12532        // Enable the close_on_file_delete setting
12533        cx.update_global(|store: &mut SettingsStore, cx| {
12534            store.update_user_settings(cx, |settings| {
12535                settings.workspace.close_on_file_delete = Some(true);
12536            });
12537        });
12538
12539        let fs = FakeFs::new(cx.background_executor.clone());
12540        let project = Project::test(fs, [], cx).await;
12541        let (workspace, cx) =
12542            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12543        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12544
12545        // Create a dirty test item
12546        let item = cx.new(|cx| {
12547            TestItem::new(cx)
12548                .with_dirty(true)
12549                .with_label("test.txt")
12550                .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
12551        });
12552
12553        // Add item to workspace
12554        workspace.update_in(cx, |workspace, window, cx| {
12555            workspace.add_item(
12556                pane.clone(),
12557                Box::new(item.clone()),
12558                None,
12559                false,
12560                false,
12561                window,
12562                cx,
12563            );
12564        });
12565
12566        // Simulate file deletion
12567        item.update(cx, |item, _| {
12568            item.set_has_deleted_file(true);
12569        });
12570
12571        // Emit UpdateTab event to trigger the close behavior
12572        cx.run_until_parked();
12573        item.update(cx, |_, cx| {
12574            cx.emit(ItemEvent::UpdateTab);
12575        });
12576
12577        // Allow any potential close operation to complete
12578        cx.run_until_parked();
12579
12580        // Verify the item remains open (dirty files are not auto-closed)
12581        pane.read_with(cx, |pane, _| {
12582            assert_eq!(
12583                pane.items().count(),
12584                1,
12585                "Dirty items should not be automatically closed even when file is deleted"
12586            );
12587        });
12588
12589        // Verify the item is marked as deleted and still dirty
12590        item.read_with(cx, |item, _| {
12591            assert!(
12592                item.has_deleted_file,
12593                "Item should be marked as having deleted file"
12594            );
12595            assert!(item.is_dirty, "Item should still be dirty");
12596        });
12597    }
12598
12599    /// Tests that navigation history is cleaned up when files are auto-closed
12600    /// due to deletion from disk.
12601    #[gpui::test]
12602    async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) {
12603        init_test(cx);
12604
12605        // Enable the close_on_file_delete setting
12606        cx.update_global(|store: &mut SettingsStore, cx| {
12607            store.update_user_settings(cx, |settings| {
12608                settings.workspace.close_on_file_delete = Some(true);
12609            });
12610        });
12611
12612        let fs = FakeFs::new(cx.background_executor.clone());
12613        let project = Project::test(fs, [], cx).await;
12614        let (workspace, cx) =
12615            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12616        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12617
12618        // Create test items
12619        let item1 = cx.new(|cx| {
12620            TestItem::new(cx)
12621                .with_label("test1.txt")
12622                .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)])
12623        });
12624        let item1_id = item1.item_id();
12625
12626        let item2 = cx.new(|cx| {
12627            TestItem::new(cx)
12628                .with_label("test2.txt")
12629                .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)])
12630        });
12631
12632        // Add items to workspace
12633        workspace.update_in(cx, |workspace, window, cx| {
12634            workspace.add_item(
12635                pane.clone(),
12636                Box::new(item1.clone()),
12637                None,
12638                false,
12639                false,
12640                window,
12641                cx,
12642            );
12643            workspace.add_item(
12644                pane.clone(),
12645                Box::new(item2.clone()),
12646                None,
12647                false,
12648                false,
12649                window,
12650                cx,
12651            );
12652        });
12653
12654        // Activate item1 to ensure it gets navigation entries
12655        pane.update_in(cx, |pane, window, cx| {
12656            pane.activate_item(0, true, true, window, cx);
12657        });
12658
12659        // Switch to item2 and back to create navigation history
12660        pane.update_in(cx, |pane, window, cx| {
12661            pane.activate_item(1, true, true, window, cx);
12662        });
12663        cx.run_until_parked();
12664
12665        pane.update_in(cx, |pane, window, cx| {
12666            pane.activate_item(0, true, true, window, cx);
12667        });
12668        cx.run_until_parked();
12669
12670        // Simulate file deletion for item1
12671        item1.update(cx, |item, _| {
12672            item.set_has_deleted_file(true);
12673        });
12674
12675        // Emit UpdateTab event to trigger the close behavior
12676        item1.update(cx, |_, cx| {
12677            cx.emit(ItemEvent::UpdateTab);
12678        });
12679        cx.run_until_parked();
12680
12681        // Verify item1 was closed
12682        pane.read_with(cx, |pane, _| {
12683            assert_eq!(
12684                pane.items().count(),
12685                1,
12686                "Should have 1 item remaining after auto-close"
12687            );
12688        });
12689
12690        // Check navigation history after close
12691        let has_item = pane.read_with(cx, |pane, cx| {
12692            let mut has_item = false;
12693            pane.nav_history().for_each_entry(cx, &mut |entry, _| {
12694                if entry.item.id() == item1_id {
12695                    has_item = true;
12696                }
12697            });
12698            has_item
12699        });
12700
12701        assert!(
12702            !has_item,
12703            "Navigation history should not contain closed item entries"
12704        );
12705    }
12706
12707    #[gpui::test]
12708    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
12709        cx: &mut TestAppContext,
12710    ) {
12711        init_test(cx);
12712
12713        let fs = FakeFs::new(cx.background_executor.clone());
12714        let project = Project::test(fs, [], cx).await;
12715        let (workspace, cx) =
12716            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
12717        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
12718
12719        let dirty_regular_buffer = cx.new(|cx| {
12720            TestItem::new(cx)
12721                .with_dirty(true)
12722                .with_label("1.txt")
12723                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
12724        });
12725        let dirty_regular_buffer_2 = cx.new(|cx| {
12726            TestItem::new(cx)
12727                .with_dirty(true)
12728                .with_label("2.txt")
12729                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
12730        });
12731        let clear_regular_buffer = cx.new(|cx| {
12732            TestItem::new(cx)
12733                .with_label("3.txt")
12734                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
12735        });
12736
12737        let dirty_multi_buffer = cx.new(|cx| {
12738            TestItem::new(cx)
12739                .with_dirty(true)
12740                .with_buffer_kind(ItemBufferKind::Multibuffer)
12741                .with_label("Fake Project Search")
12742                .with_project_items(&[
12743                    dirty_regular_buffer.read(cx).project_items[0].clone(),
12744                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
12745                    clear_regular_buffer.read(cx).project_items[0].clone(),
12746                ])
12747        });
12748        workspace.update_in(cx, |workspace, window, cx| {
12749            workspace.add_item(
12750                pane.clone(),
12751                Box::new(dirty_regular_buffer.clone()),
12752                None,
12753                false,
12754                false,
12755                window,
12756                cx,
12757            );
12758            workspace.add_item(
12759                pane.clone(),
12760                Box::new(dirty_regular_buffer_2.clone()),
12761                None,
12762                false,
12763                false,
12764                window,
12765                cx,
12766            );
12767            workspace.add_item(
12768                pane.clone(),
12769                Box::new(dirty_multi_buffer.clone()),
12770                None,
12771                false,
12772                false,
12773                window,
12774                cx,
12775            );
12776        });
12777
12778        pane.update_in(cx, |pane, window, cx| {
12779            pane.activate_item(2, true, true, window, cx);
12780            assert_eq!(
12781                pane.active_item().unwrap().item_id(),
12782                dirty_multi_buffer.item_id(),
12783                "Should select the multi buffer in the pane"
12784            );
12785        });
12786        let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| {
12787            pane.close_active_item(
12788                &CloseActiveItem {
12789                    save_intent: None,
12790                    close_pinned: false,
12791                },
12792                window,
12793                cx,
12794            )
12795        });
12796        cx.background_executor.run_until_parked();
12797        assert!(
12798            !cx.has_pending_prompt(),
12799            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
12800        );
12801        close_multi_buffer_task
12802            .await
12803            .expect("Closing multi buffer failed");
12804        pane.update(cx, |pane, cx| {
12805            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
12806            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
12807            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
12808            assert_eq!(
12809                pane.items()
12810                    .map(|item| item.item_id())
12811                    .sorted()
12812                    .collect::<Vec<_>>(),
12813                vec![
12814                    dirty_regular_buffer.item_id(),
12815                    dirty_regular_buffer_2.item_id(),
12816                ],
12817                "Should have no multi buffer left in the pane"
12818            );
12819            assert!(dirty_regular_buffer.read(cx).is_dirty);
12820            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
12821        });
12822    }
12823
12824    #[gpui::test]
12825    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
12826        init_test(cx);
12827        let fs = FakeFs::new(cx.executor());
12828        let project = Project::test(fs, [], cx).await;
12829        let (multi_workspace, cx) =
12830            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
12831        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
12832
12833        // Add a new panel to the right dock, opening the dock and setting the
12834        // focus to the new panel.
12835        let panel = workspace.update_in(cx, |workspace, window, cx| {
12836            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
12837            workspace.add_panel(panel.clone(), window, cx);
12838
12839            workspace
12840                .right_dock()
12841                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
12842
12843            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12844
12845            panel
12846        });
12847
12848        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
12849        // panel to the next valid position which, in this case, is the left
12850        // dock.
12851        cx.dispatch_action(MoveFocusedPanelToNextPosition);
12852        workspace.update(cx, |workspace, cx| {
12853            assert!(workspace.left_dock().read(cx).is_open());
12854            assert_eq!(panel.read(cx).position, DockPosition::Left);
12855        });
12856
12857        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
12858        // panel to the next valid position which, in this case, is the bottom
12859        // dock.
12860        cx.dispatch_action(MoveFocusedPanelToNextPosition);
12861        workspace.update(cx, |workspace, cx| {
12862            assert!(workspace.bottom_dock().read(cx).is_open());
12863            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
12864        });
12865
12866        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
12867        // around moving the panel to its initial position, the right dock.
12868        cx.dispatch_action(MoveFocusedPanelToNextPosition);
12869        workspace.update(cx, |workspace, cx| {
12870            assert!(workspace.right_dock().read(cx).is_open());
12871            assert_eq!(panel.read(cx).position, DockPosition::Right);
12872        });
12873
12874        // Remove focus from the panel, ensuring that, if the panel is not
12875        // focused, the `MoveFocusedPanelToNextPosition` action does not update
12876        // the panel's position, so the panel is still in the right dock.
12877        workspace.update_in(cx, |workspace, window, cx| {
12878            workspace.toggle_panel_focus::<TestPanel>(window, cx);
12879        });
12880
12881        cx.dispatch_action(MoveFocusedPanelToNextPosition);
12882        workspace.update(cx, |workspace, cx| {
12883            assert!(workspace.right_dock().read(cx).is_open());
12884            assert_eq!(panel.read(cx).position, DockPosition::Right);
12885        });
12886    }
12887
12888    #[gpui::test]
12889    async fn test_moving_items_create_panes(cx: &mut TestAppContext) {
12890        init_test(cx);
12891
12892        let fs = FakeFs::new(cx.executor());
12893        let project = Project::test(fs, [], cx).await;
12894        let (workspace, cx) =
12895            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
12896
12897        let item_1 = cx.new(|cx| {
12898            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
12899        });
12900        workspace.update_in(cx, |workspace, window, cx| {
12901            workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
12902            workspace.move_item_to_pane_in_direction(
12903                &MoveItemToPaneInDirection {
12904                    direction: SplitDirection::Right,
12905                    focus: true,
12906                    clone: false,
12907                },
12908                window,
12909                cx,
12910            );
12911            workspace.move_item_to_pane_at_index(
12912                &MoveItemToPane {
12913                    destination: 3,
12914                    focus: true,
12915                    clone: false,
12916                },
12917                window,
12918                cx,
12919            );
12920
12921            assert_eq!(workspace.panes.len(), 1, "No new panes were created");
12922            assert_eq!(
12923                pane_items_paths(&workspace.active_pane, cx),
12924                vec!["first.txt".to_string()],
12925                "Single item was not moved anywhere"
12926            );
12927        });
12928
12929        let item_2 = cx.new(|cx| {
12930            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)])
12931        });
12932        workspace.update_in(cx, |workspace, window, cx| {
12933            workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx);
12934            assert_eq!(
12935                pane_items_paths(&workspace.panes[0], cx),
12936                vec!["first.txt".to_string(), "second.txt".to_string()],
12937            );
12938            workspace.move_item_to_pane_in_direction(
12939                &MoveItemToPaneInDirection {
12940                    direction: SplitDirection::Right,
12941                    focus: true,
12942                    clone: false,
12943                },
12944                window,
12945                cx,
12946            );
12947
12948            assert_eq!(workspace.panes.len(), 2, "A new pane should be created");
12949            assert_eq!(
12950                pane_items_paths(&workspace.panes[0], cx),
12951                vec!["first.txt".to_string()],
12952                "After moving, one item should be left in the original pane"
12953            );
12954            assert_eq!(
12955                pane_items_paths(&workspace.panes[1], cx),
12956                vec!["second.txt".to_string()],
12957                "New item should have been moved to the new pane"
12958            );
12959        });
12960
12961        let item_3 = cx.new(|cx| {
12962            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)])
12963        });
12964        workspace.update_in(cx, |workspace, window, cx| {
12965            let original_pane = workspace.panes[0].clone();
12966            workspace.set_active_pane(&original_pane, window, cx);
12967            workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx);
12968            assert_eq!(workspace.panes.len(), 2, "No new panes were created");
12969            assert_eq!(
12970                pane_items_paths(&workspace.active_pane, cx),
12971                vec!["first.txt".to_string(), "third.txt".to_string()],
12972                "New pane should be ready to move one item out"
12973            );
12974
12975            workspace.move_item_to_pane_at_index(
12976                &MoveItemToPane {
12977                    destination: 3,
12978                    focus: true,
12979                    clone: false,
12980                },
12981                window,
12982                cx,
12983            );
12984            assert_eq!(workspace.panes.len(), 3, "A new pane should be created");
12985            assert_eq!(
12986                pane_items_paths(&workspace.active_pane, cx),
12987                vec!["first.txt".to_string()],
12988                "After moving, one item should be left in the original pane"
12989            );
12990            assert_eq!(
12991                pane_items_paths(&workspace.panes[1], cx),
12992                vec!["second.txt".to_string()],
12993                "Previously created pane should be unchanged"
12994            );
12995            assert_eq!(
12996                pane_items_paths(&workspace.panes[2], cx),
12997                vec!["third.txt".to_string()],
12998                "New item should have been moved to the new pane"
12999            );
13000        });
13001    }
13002
13003    #[gpui::test]
13004    async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) {
13005        init_test(cx);
13006
13007        let fs = FakeFs::new(cx.executor());
13008        let project = Project::test(fs, [], cx).await;
13009        let (workspace, cx) =
13010            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13011
13012        let item_1 = cx.new(|cx| {
13013            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)])
13014        });
13015        workspace.update_in(cx, |workspace, window, cx| {
13016            workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx);
13017            workspace.move_item_to_pane_in_direction(
13018                &MoveItemToPaneInDirection {
13019                    direction: SplitDirection::Right,
13020                    focus: true,
13021                    clone: true,
13022                },
13023                window,
13024                cx,
13025            );
13026        });
13027        cx.run_until_parked();
13028        workspace.update_in(cx, |workspace, window, cx| {
13029            workspace.move_item_to_pane_at_index(
13030                &MoveItemToPane {
13031                    destination: 3,
13032                    focus: true,
13033                    clone: true,
13034                },
13035                window,
13036                cx,
13037            );
13038        });
13039        cx.run_until_parked();
13040
13041        workspace.update(cx, |workspace, cx| {
13042            assert_eq!(workspace.panes.len(), 3, "Two new panes were created");
13043            for pane in workspace.panes() {
13044                assert_eq!(
13045                    pane_items_paths(pane, cx),
13046                    vec!["first.txt".to_string()],
13047                    "Single item exists in all panes"
13048                );
13049            }
13050        });
13051
13052        // verify that the active pane has been updated after waiting for the
13053        // pane focus event to fire and resolve
13054        workspace.read_with(cx, |workspace, _app| {
13055            assert_eq!(
13056                workspace.active_pane(),
13057                &workspace.panes[2],
13058                "The third pane should be the active one: {:?}",
13059                workspace.panes
13060            );
13061        })
13062    }
13063
13064    #[gpui::test]
13065    async fn test_close_item_in_all_panes(cx: &mut TestAppContext) {
13066        init_test(cx);
13067
13068        let fs = FakeFs::new(cx.executor());
13069        fs.insert_tree("/root", json!({ "test.txt": "" })).await;
13070
13071        let project = Project::test(fs, ["root".as_ref()], cx).await;
13072        let (workspace, cx) =
13073            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13074
13075        let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13076        // Add item to pane A with project path
13077        let item_a = cx.new(|cx| {
13078            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13079        });
13080        workspace.update_in(cx, |workspace, window, cx| {
13081            workspace.add_item_to_active_pane(Box::new(item_a.clone()), None, true, window, cx)
13082        });
13083
13084        // Split to create pane B
13085        let pane_b = workspace.update_in(cx, |workspace, window, cx| {
13086            workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
13087        });
13088
13089        // Add item with SAME project path to pane B, and pin it
13090        let item_b = cx.new(|cx| {
13091            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13092        });
13093        pane_b.update_in(cx, |pane, window, cx| {
13094            pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
13095            pane.set_pinned_count(1);
13096        });
13097
13098        assert_eq!(pane_a.read_with(cx, |pane, _| pane.items_len()), 1);
13099        assert_eq!(pane_b.read_with(cx, |pane, _| pane.items_len()), 1);
13100
13101        // close_pinned: false should only close the unpinned copy
13102        workspace.update_in(cx, |workspace, window, cx| {
13103            workspace.close_item_in_all_panes(
13104                &CloseItemInAllPanes {
13105                    save_intent: Some(SaveIntent::Close),
13106                    close_pinned: false,
13107                },
13108                window,
13109                cx,
13110            )
13111        });
13112        cx.executor().run_until_parked();
13113
13114        let item_count_a = pane_a.read_with(cx, |pane, _| pane.items_len());
13115        let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
13116        assert_eq!(item_count_a, 0, "Unpinned item in pane A should be closed");
13117        assert_eq!(item_count_b, 1, "Pinned item in pane B should remain");
13118
13119        // Split again, seeing as closing the previous item also closed its
13120        // pane, so only pane remains, which does not allow us to properly test
13121        // that both items close when `close_pinned: true`.
13122        let pane_c = workspace.update_in(cx, |workspace, window, cx| {
13123            workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
13124        });
13125
13126        // Add an item with the same project path to pane C so that
13127        // close_item_in_all_panes can determine what to close across all panes
13128        // (it reads the active item from the active pane, and split_pane
13129        // creates an empty pane).
13130        let item_c = cx.new(|cx| {
13131            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "test.txt", cx)])
13132        });
13133        pane_c.update_in(cx, |pane, window, cx| {
13134            pane.add_item(Box::new(item_c.clone()), true, true, None, window, cx);
13135        });
13136
13137        // close_pinned: true should close the pinned copy too
13138        workspace.update_in(cx, |workspace, window, cx| {
13139            let panes_count = workspace.panes().len();
13140            assert_eq!(panes_count, 2, "Workspace should have two panes (B and C)");
13141
13142            workspace.close_item_in_all_panes(
13143                &CloseItemInAllPanes {
13144                    save_intent: Some(SaveIntent::Close),
13145                    close_pinned: true,
13146                },
13147                window,
13148                cx,
13149            )
13150        });
13151        cx.executor().run_until_parked();
13152
13153        let item_count_b = pane_b.read_with(cx, |pane, _| pane.items_len());
13154        let item_count_c = pane_c.read_with(cx, |pane, _| pane.items_len());
13155        assert_eq!(item_count_b, 0, "Pinned item in pane B should be closed");
13156        assert_eq!(item_count_c, 0, "Unpinned item in pane C should be closed");
13157    }
13158
13159    mod register_project_item_tests {
13160
13161        use super::*;
13162
13163        // View
13164        struct TestPngItemView {
13165            focus_handle: FocusHandle,
13166        }
13167        // Model
13168        struct TestPngItem {}
13169
13170        impl project::ProjectItem for TestPngItem {
13171            fn try_open(
13172                _project: &Entity<Project>,
13173                path: &ProjectPath,
13174                cx: &mut App,
13175            ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
13176                if path.path.extension().unwrap() == "png" {
13177                    Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestPngItem {}))))
13178                } else {
13179                    None
13180                }
13181            }
13182
13183            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
13184                None
13185            }
13186
13187            fn project_path(&self, _: &App) -> Option<ProjectPath> {
13188                None
13189            }
13190
13191            fn is_dirty(&self) -> bool {
13192                false
13193            }
13194        }
13195
13196        impl Item for TestPngItemView {
13197            type Event = ();
13198            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
13199                "".into()
13200            }
13201        }
13202        impl EventEmitter<()> for TestPngItemView {}
13203        impl Focusable for TestPngItemView {
13204            fn focus_handle(&self, _cx: &App) -> FocusHandle {
13205                self.focus_handle.clone()
13206            }
13207        }
13208
13209        impl Render for TestPngItemView {
13210            fn render(
13211                &mut self,
13212                _window: &mut Window,
13213                _cx: &mut Context<Self>,
13214            ) -> impl IntoElement {
13215                Empty
13216            }
13217        }
13218
13219        impl ProjectItem for TestPngItemView {
13220            type Item = TestPngItem;
13221
13222            fn for_project_item(
13223                _project: Entity<Project>,
13224                _pane: Option<&Pane>,
13225                _item: Entity<Self::Item>,
13226                _: &mut Window,
13227                cx: &mut Context<Self>,
13228            ) -> Self
13229            where
13230                Self: Sized,
13231            {
13232                Self {
13233                    focus_handle: cx.focus_handle(),
13234                }
13235            }
13236        }
13237
13238        // View
13239        struct TestIpynbItemView {
13240            focus_handle: FocusHandle,
13241        }
13242        // Model
13243        struct TestIpynbItem {}
13244
13245        impl project::ProjectItem for TestIpynbItem {
13246            fn try_open(
13247                _project: &Entity<Project>,
13248                path: &ProjectPath,
13249                cx: &mut App,
13250            ) -> Option<Task<anyhow::Result<Entity<Self>>>> {
13251                if path.path.extension().unwrap() == "ipynb" {
13252                    Some(cx.spawn(async move |cx| Ok(cx.new(|_| TestIpynbItem {}))))
13253                } else {
13254                    None
13255                }
13256            }
13257
13258            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
13259                None
13260            }
13261
13262            fn project_path(&self, _: &App) -> Option<ProjectPath> {
13263                None
13264            }
13265
13266            fn is_dirty(&self) -> bool {
13267                false
13268            }
13269        }
13270
13271        impl Item for TestIpynbItemView {
13272            type Event = ();
13273            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
13274                "".into()
13275            }
13276        }
13277        impl EventEmitter<()> for TestIpynbItemView {}
13278        impl Focusable for TestIpynbItemView {
13279            fn focus_handle(&self, _cx: &App) -> FocusHandle {
13280                self.focus_handle.clone()
13281            }
13282        }
13283
13284        impl Render for TestIpynbItemView {
13285            fn render(
13286                &mut self,
13287                _window: &mut Window,
13288                _cx: &mut Context<Self>,
13289            ) -> impl IntoElement {
13290                Empty
13291            }
13292        }
13293
13294        impl ProjectItem for TestIpynbItemView {
13295            type Item = TestIpynbItem;
13296
13297            fn for_project_item(
13298                _project: Entity<Project>,
13299                _pane: Option<&Pane>,
13300                _item: Entity<Self::Item>,
13301                _: &mut Window,
13302                cx: &mut Context<Self>,
13303            ) -> Self
13304            where
13305                Self: Sized,
13306            {
13307                Self {
13308                    focus_handle: cx.focus_handle(),
13309                }
13310            }
13311        }
13312
13313        struct TestAlternatePngItemView {
13314            focus_handle: FocusHandle,
13315        }
13316
13317        impl Item for TestAlternatePngItemView {
13318            type Event = ();
13319            fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
13320                "".into()
13321            }
13322        }
13323
13324        impl EventEmitter<()> for TestAlternatePngItemView {}
13325        impl Focusable for TestAlternatePngItemView {
13326            fn focus_handle(&self, _cx: &App) -> FocusHandle {
13327                self.focus_handle.clone()
13328            }
13329        }
13330
13331        impl Render for TestAlternatePngItemView {
13332            fn render(
13333                &mut self,
13334                _window: &mut Window,
13335                _cx: &mut Context<Self>,
13336            ) -> impl IntoElement {
13337                Empty
13338            }
13339        }
13340
13341        impl ProjectItem for TestAlternatePngItemView {
13342            type Item = TestPngItem;
13343
13344            fn for_project_item(
13345                _project: Entity<Project>,
13346                _pane: Option<&Pane>,
13347                _item: Entity<Self::Item>,
13348                _: &mut Window,
13349                cx: &mut Context<Self>,
13350            ) -> Self
13351            where
13352                Self: Sized,
13353            {
13354                Self {
13355                    focus_handle: cx.focus_handle(),
13356                }
13357            }
13358        }
13359
13360        #[gpui::test]
13361        async fn test_register_project_item(cx: &mut TestAppContext) {
13362            init_test(cx);
13363
13364            cx.update(|cx| {
13365                register_project_item::<TestPngItemView>(cx);
13366                register_project_item::<TestIpynbItemView>(cx);
13367            });
13368
13369            let fs = FakeFs::new(cx.executor());
13370            fs.insert_tree(
13371                "/root1",
13372                json!({
13373                    "one.png": "BINARYDATAHERE",
13374                    "two.ipynb": "{ totally a notebook }",
13375                    "three.txt": "editing text, sure why not?"
13376                }),
13377            )
13378            .await;
13379
13380            let project = Project::test(fs, ["root1".as_ref()], cx).await;
13381            let (workspace, cx) =
13382                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13383
13384            let worktree_id = project.update(cx, |project, cx| {
13385                project.worktrees(cx).next().unwrap().read(cx).id()
13386            });
13387
13388            let handle = workspace
13389                .update_in(cx, |workspace, window, cx| {
13390                    let project_path = (worktree_id, rel_path("one.png"));
13391                    workspace.open_path(project_path, None, true, window, cx)
13392                })
13393                .await
13394                .unwrap();
13395
13396            // Now we can check if the handle we got back errored or not
13397            assert_eq!(
13398                handle.to_any_view().entity_type(),
13399                TypeId::of::<TestPngItemView>()
13400            );
13401
13402            let handle = workspace
13403                .update_in(cx, |workspace, window, cx| {
13404                    let project_path = (worktree_id, rel_path("two.ipynb"));
13405                    workspace.open_path(project_path, None, true, window, cx)
13406                })
13407                .await
13408                .unwrap();
13409
13410            assert_eq!(
13411                handle.to_any_view().entity_type(),
13412                TypeId::of::<TestIpynbItemView>()
13413            );
13414
13415            let handle = workspace
13416                .update_in(cx, |workspace, window, cx| {
13417                    let project_path = (worktree_id, rel_path("three.txt"));
13418                    workspace.open_path(project_path, None, true, window, cx)
13419                })
13420                .await;
13421            assert!(handle.is_err());
13422        }
13423
13424        #[gpui::test]
13425        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
13426            init_test(cx);
13427
13428            cx.update(|cx| {
13429                register_project_item::<TestPngItemView>(cx);
13430                register_project_item::<TestAlternatePngItemView>(cx);
13431            });
13432
13433            let fs = FakeFs::new(cx.executor());
13434            fs.insert_tree(
13435                "/root1",
13436                json!({
13437                    "one.png": "BINARYDATAHERE",
13438                    "two.ipynb": "{ totally a notebook }",
13439                    "three.txt": "editing text, sure why not?"
13440                }),
13441            )
13442            .await;
13443            let project = Project::test(fs, ["root1".as_ref()], cx).await;
13444            let (workspace, cx) =
13445                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13446            let worktree_id = project.update(cx, |project, cx| {
13447                project.worktrees(cx).next().unwrap().read(cx).id()
13448            });
13449
13450            let handle = workspace
13451                .update_in(cx, |workspace, window, cx| {
13452                    let project_path = (worktree_id, rel_path("one.png"));
13453                    workspace.open_path(project_path, None, true, window, cx)
13454                })
13455                .await
13456                .unwrap();
13457
13458            // This _must_ be the second item registered
13459            assert_eq!(
13460                handle.to_any_view().entity_type(),
13461                TypeId::of::<TestAlternatePngItemView>()
13462            );
13463
13464            let handle = workspace
13465                .update_in(cx, |workspace, window, cx| {
13466                    let project_path = (worktree_id, rel_path("three.txt"));
13467                    workspace.open_path(project_path, None, true, window, cx)
13468                })
13469                .await;
13470            assert!(handle.is_err());
13471        }
13472    }
13473
13474    #[gpui::test]
13475    async fn test_status_bar_visibility(cx: &mut TestAppContext) {
13476        init_test(cx);
13477
13478        let fs = FakeFs::new(cx.executor());
13479        let project = Project::test(fs, [], cx).await;
13480        let (workspace, _cx) =
13481            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13482
13483        // Test with status bar shown (default)
13484        workspace.read_with(cx, |workspace, cx| {
13485            let visible = workspace.status_bar_visible(cx);
13486            assert!(visible, "Status bar should be visible by default");
13487        });
13488
13489        // Test with status bar hidden
13490        cx.update_global(|store: &mut SettingsStore, cx| {
13491            store.update_user_settings(cx, |settings| {
13492                settings.status_bar.get_or_insert_default().show = Some(false);
13493            });
13494        });
13495
13496        workspace.read_with(cx, |workspace, cx| {
13497            let visible = workspace.status_bar_visible(cx);
13498            assert!(!visible, "Status bar should be hidden when show is false");
13499        });
13500
13501        // Test with status bar shown explicitly
13502        cx.update_global(|store: &mut SettingsStore, cx| {
13503            store.update_user_settings(cx, |settings| {
13504                settings.status_bar.get_or_insert_default().show = Some(true);
13505            });
13506        });
13507
13508        workspace.read_with(cx, |workspace, cx| {
13509            let visible = workspace.status_bar_visible(cx);
13510            assert!(visible, "Status bar should be visible when show is true");
13511        });
13512    }
13513
13514    #[gpui::test]
13515    async fn test_pane_close_active_item(cx: &mut TestAppContext) {
13516        init_test(cx);
13517
13518        let fs = FakeFs::new(cx.executor());
13519        let project = Project::test(fs, [], cx).await;
13520        let (multi_workspace, cx) =
13521            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
13522        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
13523        let panel = workspace.update_in(cx, |workspace, window, cx| {
13524            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
13525            workspace.add_panel(panel.clone(), window, cx);
13526
13527            workspace
13528                .right_dock()
13529                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
13530
13531            panel
13532        });
13533
13534        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13535        let item_a = cx.new(TestItem::new);
13536        let item_b = cx.new(TestItem::new);
13537        let item_a_id = item_a.entity_id();
13538        let item_b_id = item_b.entity_id();
13539
13540        pane.update_in(cx, |pane, window, cx| {
13541            pane.add_item(Box::new(item_a.clone()), true, true, None, window, cx);
13542            pane.add_item(Box::new(item_b.clone()), true, true, None, window, cx);
13543        });
13544
13545        pane.read_with(cx, |pane, _| {
13546            assert_eq!(pane.items_len(), 2);
13547            assert_eq!(pane.active_item().unwrap().item_id(), item_b_id);
13548        });
13549
13550        workspace.update_in(cx, |workspace, window, cx| {
13551            workspace.toggle_panel_focus::<TestPanel>(window, cx);
13552        });
13553
13554        workspace.update_in(cx, |_, window, cx| {
13555            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
13556        });
13557
13558        // Assert that the `pane::CloseActiveItem` action is handled at the
13559        // workspace level when one of the dock panels is focused and, in that
13560        // case, the center pane's active item is closed but the focus is not
13561        // moved.
13562        cx.dispatch_action(pane::CloseActiveItem::default());
13563        cx.run_until_parked();
13564
13565        pane.read_with(cx, |pane, _| {
13566            assert_eq!(pane.items_len(), 1);
13567            assert_eq!(pane.active_item().unwrap().item_id(), item_a_id);
13568        });
13569
13570        workspace.update_in(cx, |workspace, window, cx| {
13571            assert!(workspace.right_dock().read(cx).is_open());
13572            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
13573        });
13574    }
13575
13576    #[gpui::test]
13577    async fn test_panel_zoom_preserved_across_workspace_switch(cx: &mut TestAppContext) {
13578        init_test(cx);
13579        let fs = FakeFs::new(cx.executor());
13580
13581        let project_a = Project::test(fs.clone(), [], cx).await;
13582        let project_b = Project::test(fs, [], cx).await;
13583
13584        let multi_workspace_handle =
13585            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
13586        cx.run_until_parked();
13587
13588        let workspace_a = multi_workspace_handle
13589            .read_with(cx, |mw, _| mw.workspace().clone())
13590            .unwrap();
13591
13592        let _workspace_b = multi_workspace_handle
13593            .update(cx, |mw, window, cx| {
13594                mw.test_add_workspace(project_b, window, cx)
13595            })
13596            .unwrap();
13597
13598        // Switch to workspace A
13599        multi_workspace_handle
13600            .update(cx, |mw, window, cx| {
13601                mw.activate_index(0, window, cx);
13602            })
13603            .unwrap();
13604
13605        let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
13606
13607        // Add a panel to workspace A's right dock and open the dock
13608        let panel = workspace_a.update_in(cx, |workspace, window, cx| {
13609            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
13610            workspace.add_panel(panel.clone(), window, cx);
13611            workspace
13612                .right_dock()
13613                .update(cx, |dock, cx| dock.set_open(true, window, cx));
13614            panel
13615        });
13616
13617        // Focus the panel through the workspace (matching existing test pattern)
13618        workspace_a.update_in(cx, |workspace, window, cx| {
13619            workspace.toggle_panel_focus::<TestPanel>(window, cx);
13620        });
13621
13622        // Zoom the panel
13623        panel.update_in(cx, |panel, window, cx| {
13624            panel.set_zoomed(true, window, cx);
13625        });
13626
13627        // Verify the panel is zoomed and the dock is open
13628        workspace_a.update_in(cx, |workspace, window, cx| {
13629            assert!(
13630                workspace.right_dock().read(cx).is_open(),
13631                "dock should be open before switch"
13632            );
13633            assert!(
13634                panel.is_zoomed(window, cx),
13635                "panel should be zoomed before switch"
13636            );
13637            assert!(
13638                panel.read(cx).focus_handle(cx).contains_focused(window, cx),
13639                "panel should be focused before switch"
13640            );
13641        });
13642
13643        // Switch to workspace B
13644        multi_workspace_handle
13645            .update(cx, |mw, window, cx| {
13646                mw.activate_index(1, window, cx);
13647            })
13648            .unwrap();
13649        cx.run_until_parked();
13650
13651        // Switch back to workspace A
13652        multi_workspace_handle
13653            .update(cx, |mw, window, cx| {
13654                mw.activate_index(0, window, cx);
13655            })
13656            .unwrap();
13657        cx.run_until_parked();
13658
13659        // Verify the panel is still zoomed and the dock is still open
13660        workspace_a.update_in(cx, |workspace, window, cx| {
13661            assert!(
13662                workspace.right_dock().read(cx).is_open(),
13663                "dock should still be open after switching back"
13664            );
13665            assert!(
13666                panel.is_zoomed(window, cx),
13667                "panel should still be zoomed after switching back"
13668            );
13669        });
13670    }
13671
13672    fn pane_items_paths(pane: &Entity<Pane>, cx: &App) -> Vec<String> {
13673        pane.read(cx)
13674            .items()
13675            .flat_map(|item| {
13676                item.project_paths(cx)
13677                    .into_iter()
13678                    .map(|path| path.path.display(PathStyle::local()).into_owned())
13679            })
13680            .collect()
13681    }
13682
13683    pub fn init_test(cx: &mut TestAppContext) {
13684        cx.update(|cx| {
13685            let settings_store = SettingsStore::test(cx);
13686            cx.set_global(settings_store);
13687            cx.set_global(db::AppDatabase::test_new());
13688            theme::init(theme::LoadThemes::JustBase, cx);
13689        });
13690    }
13691
13692    #[gpui::test]
13693    async fn test_toggle_theme_mode_persists_and_updates_active_theme(cx: &mut TestAppContext) {
13694        use settings::{ThemeName, ThemeSelection};
13695        use theme::SystemAppearance;
13696        use zed_actions::theme::ToggleMode;
13697
13698        init_test(cx);
13699
13700        let fs = FakeFs::new(cx.executor());
13701        let settings_fs: Arc<dyn fs::Fs> = fs.clone();
13702
13703        fs.insert_tree(path!("/root"), json!({ "file.rs": "fn main() {}\n" }))
13704            .await;
13705
13706        // Build a test project and workspace view so the test can invoke
13707        // the workspace action handler the same way the UI would.
13708        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
13709        let (workspace, cx) =
13710            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
13711
13712        // Seed the settings file with a plain static light theme so the
13713        // first toggle always starts from a known persisted state.
13714        workspace.update_in(cx, |_workspace, _window, cx| {
13715            *SystemAppearance::global_mut(cx) = SystemAppearance(theme::Appearance::Light);
13716            settings::update_settings_file(settings_fs.clone(), cx, |settings, _cx| {
13717                settings.theme.theme = Some(ThemeSelection::Static(ThemeName("One Light".into())));
13718            });
13719        });
13720        cx.executor().advance_clock(Duration::from_millis(200));
13721        cx.run_until_parked();
13722
13723        // Confirm the initial persisted settings contain the static theme
13724        // we just wrote before any toggling happens.
13725        let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
13726        assert!(settings_text.contains(r#""theme": "One Light""#));
13727
13728        // Toggle once. This should migrate the persisted theme settings
13729        // into light/dark slots and enable system mode.
13730        workspace.update_in(cx, |workspace, window, cx| {
13731            workspace.toggle_theme_mode(&ToggleMode, window, cx);
13732        });
13733        cx.executor().advance_clock(Duration::from_millis(200));
13734        cx.run_until_parked();
13735
13736        // 1. Static -> Dynamic
13737        // this assertion checks theme changed from static to dynamic.
13738        let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
13739        let parsed: serde_json::Value = settings::parse_json_with_comments(&settings_text).unwrap();
13740        assert_eq!(
13741            parsed["theme"],
13742            serde_json::json!({
13743                "mode": "system",
13744                "light": "One Light",
13745                "dark": "One Dark"
13746            })
13747        );
13748
13749        // 2. Toggle again, suppose it will change the mode to light
13750        workspace.update_in(cx, |workspace, window, cx| {
13751            workspace.toggle_theme_mode(&ToggleMode, window, cx);
13752        });
13753        cx.executor().advance_clock(Duration::from_millis(200));
13754        cx.run_until_parked();
13755
13756        let settings_text = SettingsStore::load_settings(&settings_fs).await.unwrap();
13757        assert!(settings_text.contains(r#""mode": "light""#));
13758    }
13759
13760    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
13761        let item = TestProjectItem::new(id, path, cx);
13762        item.update(cx, |item, _| {
13763            item.is_dirty = true;
13764        });
13765        item
13766    }
13767
13768    #[gpui::test]
13769    async fn test_zoomed_panel_without_pane_preserved_on_center_focus(
13770        cx: &mut gpui::TestAppContext,
13771    ) {
13772        init_test(cx);
13773        let fs = FakeFs::new(cx.executor());
13774
13775        let project = Project::test(fs, [], cx).await;
13776        let (workspace, cx) =
13777            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
13778
13779        let panel = workspace.update_in(cx, |workspace, window, cx| {
13780            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx));
13781            workspace.add_panel(panel.clone(), window, cx);
13782            workspace
13783                .right_dock()
13784                .update(cx, |dock, cx| dock.set_open(true, window, cx));
13785            panel
13786        });
13787
13788        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
13789        pane.update_in(cx, |pane, window, cx| {
13790            let item = cx.new(TestItem::new);
13791            pane.add_item(Box::new(item), true, true, None, window, cx);
13792        });
13793
13794        // Transfer focus to the panel, then zoom it. Using toggle_panel_focus
13795        // mirrors the real-world flow and avoids side effects from directly
13796        // focusing the panel while the center pane is active.
13797        workspace.update_in(cx, |workspace, window, cx| {
13798            workspace.toggle_panel_focus::<TestPanel>(window, cx);
13799        });
13800
13801        panel.update_in(cx, |panel, window, cx| {
13802            panel.set_zoomed(true, window, cx);
13803        });
13804
13805        workspace.update_in(cx, |workspace, window, cx| {
13806            assert!(workspace.right_dock().read(cx).is_open());
13807            assert!(panel.is_zoomed(window, cx));
13808            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
13809        });
13810
13811        // Simulate a spurious pane::Event::Focus on the center pane while the
13812        // panel still has focus. This mirrors what happens during macOS window
13813        // activation: the center pane fires a focus event even though actual
13814        // focus remains on the dock panel.
13815        pane.update_in(cx, |_, _, cx| {
13816            cx.emit(pane::Event::Focus);
13817        });
13818
13819        // The dock must remain open because the panel had focus at the time the
13820        // event was processed. Before the fix, dock_to_preserve was None for
13821        // panels that don't implement pane(), causing the dock to close.
13822        workspace.update_in(cx, |workspace, window, cx| {
13823            assert!(
13824                workspace.right_dock().read(cx).is_open(),
13825                "Dock should stay open when its zoomed panel (without pane()) still has focus"
13826            );
13827            assert!(panel.is_zoomed(window, cx));
13828        });
13829    }
13830}