workspace.rs

   1pub mod dock;
   2pub mod item;
   3mod modal_layer;
   4pub mod notifications;
   5pub mod pane;
   6pub mod pane_group;
   7mod persistence;
   8pub mod searchable;
   9pub mod shared_screen;
  10mod status_bar;
  11pub mod tasks;
  12mod theme_preview;
  13mod toast_layer;
  14mod toolbar;
  15mod workspace_settings;
  16
  17pub use toast_layer::{ToastLayer, ToastView};
  18
  19use anyhow::{anyhow, Context as _, Result};
  20use call::{call_settings::CallSettings, ActiveCall};
  21use client::{
  22    proto::{self, ErrorCode, PanelId, PeerId},
  23    ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
  24};
  25use collections::{hash_map, HashMap, HashSet};
  26use derive_more::{Deref, DerefMut};
  27pub use dock::Panel;
  28use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
  29use futures::{
  30    channel::{
  31        mpsc::{self, UnboundedReceiver, UnboundedSender},
  32        oneshot,
  33    },
  34    future::try_join_all,
  35    Future, FutureExt, StreamExt,
  36};
  37use gpui::{
  38    action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
  39    transparent_black, Action, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds,
  40    Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
  41    Focusable, Global, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions,
  42    Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity,
  43    WindowBounds, WindowHandle, WindowId, WindowOptions,
  44};
  45pub use item::{
  46    FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
  47    ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
  48};
  49use itertools::Itertools;
  50use language::{LanguageRegistry, Rope};
  51pub use modal_layer::*;
  52use node_runtime::NodeRuntime;
  53use notifications::{
  54    simple_message_notification::MessageNotification, DetachAndPromptErr, Notifications,
  55};
  56pub use pane::*;
  57pub use pane_group::*;
  58pub use persistence::{
  59    model::{ItemId, LocalPaths, SerializedWorkspaceLocation},
  60    WorkspaceDb, DB as WORKSPACE_DB,
  61};
  62use persistence::{
  63    model::{SerializedSshProject, SerializedWorkspace},
  64    SerializedWindowBounds, DB,
  65};
  66use postage::stream::Stream;
  67use project::{
  68    DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
  69};
  70use remote::{ssh_session::ConnectionIdentifier, SshClientDelegate, SshConnectionOptions};
  71use schemars::JsonSchema;
  72use serde::Deserialize;
  73use session::AppSession;
  74use settings::Settings;
  75use shared_screen::SharedScreen;
  76use sqlez::{
  77    bindable::{Bind, Column, StaticColumnCount},
  78    statement::Statement,
  79};
  80use status_bar::StatusBar;
  81pub use status_bar::StatusItemView;
  82use std::{
  83    any::TypeId,
  84    borrow::Cow,
  85    cell::RefCell,
  86    cmp,
  87    collections::hash_map::DefaultHasher,
  88    env,
  89    hash::{Hash, Hasher},
  90    path::{Path, PathBuf},
  91    rc::Rc,
  92    sync::{atomic::AtomicUsize, Arc, LazyLock, Weak},
  93    time::Duration,
  94};
  95use task::SpawnInTerminal;
  96use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
  97pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
  98pub use ui;
  99use ui::prelude::*;
 100use util::{paths::SanitizedPath, serde::default_true, ResultExt, TryFutureExt};
 101use uuid::Uuid;
 102pub use workspace_settings::{
 103    AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
 104};
 105
 106use crate::notifications::NotificationId;
 107use crate::persistence::{
 108    model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
 109    SerializedAxis,
 110};
 111
 112pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
 113
 114static ZED_WINDOW_SIZE: LazyLock<Option<Size<Pixels>>> = LazyLock::new(|| {
 115    env::var("ZED_WINDOW_SIZE")
 116        .ok()
 117        .as_deref()
 118        .and_then(parse_pixel_size_env_var)
 119});
 120
 121static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
 122    env::var("ZED_WINDOW_POSITION")
 123        .ok()
 124        .as_deref()
 125        .and_then(parse_pixel_position_env_var)
 126});
 127
 128actions!(assistant, [ShowConfiguration]);
 129
 130actions!(
 131    workspace,
 132    [
 133        ActivateNextPane,
 134        ActivatePreviousPane,
 135        ActivateNextWindow,
 136        ActivatePreviousWindow,
 137        AddFolderToProject,
 138        ClearAllNotifications,
 139        CloseAllDocks,
 140        CloseWindow,
 141        Feedback,
 142        FollowNextCollaborator,
 143        MoveFocusedPanelToNextPosition,
 144        NewCenterTerminal,
 145        NewFile,
 146        NewFileSplitVertical,
 147        NewFileSplitHorizontal,
 148        NewSearch,
 149        NewTerminal,
 150        NewWindow,
 151        Open,
 152        OpenFiles,
 153        OpenInTerminal,
 154        OpenComponentPreview,
 155        ReloadActiveItem,
 156        SaveAs,
 157        SaveWithoutFormat,
 158        ToggleBottomDock,
 159        ToggleCenteredLayout,
 160        ToggleLeftDock,
 161        ToggleRightDock,
 162        ToggleZoom,
 163        Unfollow,
 164        Welcome,
 165    ]
 166);
 167
 168#[derive(Clone, PartialEq)]
 169pub struct OpenPaths {
 170    pub paths: Vec<PathBuf>,
 171}
 172
 173#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 174pub struct ActivatePane(pub usize);
 175
 176#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 177#[serde(deny_unknown_fields)]
 178pub struct MoveItemToPane {
 179    pub destination: usize,
 180    #[serde(default = "default_true")]
 181    pub focus: bool,
 182}
 183
 184#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 185#[serde(deny_unknown_fields)]
 186pub struct MoveItemToPaneInDirection {
 187    pub direction: SplitDirection,
 188    #[serde(default = "default_true")]
 189    pub focus: bool,
 190}
 191
 192#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
 193#[serde(deny_unknown_fields)]
 194pub struct SaveAll {
 195    pub save_intent: Option<SaveIntent>,
 196}
 197
 198#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
 199#[serde(deny_unknown_fields)]
 200pub struct Save {
 201    pub save_intent: Option<SaveIntent>,
 202}
 203
 204#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
 205#[serde(deny_unknown_fields)]
 206pub struct CloseAllItemsAndPanes {
 207    pub save_intent: Option<SaveIntent>,
 208}
 209
 210#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
 211#[serde(deny_unknown_fields)]
 212pub struct CloseInactiveTabsAndPanes {
 213    pub save_intent: Option<SaveIntent>,
 214}
 215
 216#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
 217pub struct SendKeystrokes(pub String);
 218
 219#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema)]
 220#[serde(deny_unknown_fields)]
 221pub struct Reload {
 222    pub binary_path: Option<PathBuf>,
 223}
 224
 225action_as!(project_symbols, ToggleProjectSymbols as Toggle);
 226
 227#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema)]
 228pub struct ToggleFileFinder {
 229    #[serde(default)]
 230    pub separate_history: bool,
 231}
 232
 233impl_action_as!(file_finder, ToggleFileFinder as Toggle);
 234
 235impl_actions!(
 236    workspace,
 237    [
 238        ActivatePane,
 239        CloseAllItemsAndPanes,
 240        CloseInactiveTabsAndPanes,
 241        MoveItemToPane,
 242        MoveItemToPaneInDirection,
 243        OpenTerminal,
 244        Reload,
 245        Save,
 246        SaveAll,
 247        SendKeystrokes,
 248    ]
 249);
 250
 251actions!(
 252    workspace,
 253    [
 254        ActivatePaneLeft,
 255        ActivatePaneRight,
 256        ActivatePaneUp,
 257        ActivatePaneDown,
 258        SwapPaneLeft,
 259        SwapPaneRight,
 260        SwapPaneUp,
 261        SwapPaneDown,
 262    ]
 263);
 264
 265#[derive(PartialEq, Eq, Debug)]
 266pub enum CloseIntent {
 267    /// Quit the program entirely.
 268    Quit,
 269    /// Close a window.
 270    CloseWindow,
 271    /// Replace the workspace in an existing window.
 272    ReplaceWindow,
 273}
 274
 275#[derive(Clone)]
 276pub struct Toast {
 277    id: NotificationId,
 278    msg: Cow<'static, str>,
 279    autohide: bool,
 280    on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut Window, &mut App)>)>,
 281}
 282
 283impl Toast {
 284    pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
 285        Toast {
 286            id,
 287            msg: msg.into(),
 288            on_click: None,
 289            autohide: false,
 290        }
 291    }
 292
 293    pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
 294    where
 295        M: Into<Cow<'static, str>>,
 296        F: Fn(&mut Window, &mut App) + 'static,
 297    {
 298        self.on_click = Some((message.into(), Arc::new(on_click)));
 299        self
 300    }
 301
 302    pub fn autohide(mut self) -> Self {
 303        self.autohide = true;
 304        self
 305    }
 306}
 307
 308impl PartialEq for Toast {
 309    fn eq(&self, other: &Self) -> bool {
 310        self.id == other.id
 311            && self.msg == other.msg
 312            && self.on_click.is_some() == other.on_click.is_some()
 313    }
 314}
 315
 316#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema)]
 317#[serde(deny_unknown_fields)]
 318pub struct OpenTerminal {
 319    pub working_directory: PathBuf,
 320}
 321
 322#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
 323pub struct WorkspaceId(i64);
 324
 325impl StaticColumnCount for WorkspaceId {}
 326impl Bind for WorkspaceId {
 327    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
 328        self.0.bind(statement, start_index)
 329    }
 330}
 331impl Column for WorkspaceId {
 332    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 333        i64::column(statement, start_index)
 334            .map(|(i, next_index)| (Self(i), next_index))
 335            .with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
 336    }
 337}
 338impl From<WorkspaceId> for i64 {
 339    fn from(val: WorkspaceId) -> Self {
 340        val.0
 341    }
 342}
 343
 344pub fn init_settings(cx: &mut App) {
 345    WorkspaceSettings::register(cx);
 346    ItemSettings::register(cx);
 347    PreviewTabsSettings::register(cx);
 348    TabBarSettings::register(cx);
 349}
 350
 351fn prompt_and_open_paths(app_state: Arc<AppState>, options: PathPromptOptions, cx: &mut App) {
 352    let paths = cx.prompt_for_paths(options);
 353    cx.spawn(|cx| async move {
 354        match paths.await.anyhow().and_then(|res| res) {
 355            Ok(Some(paths)) => {
 356                cx.update(|cx| {
 357                    open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx)
 358                })
 359                .ok();
 360            }
 361            Ok(None) => {}
 362            Err(err) => {
 363                util::log_err(&err);
 364                cx.update(|cx| {
 365                    if let Some(workspace_window) = cx
 366                        .active_window()
 367                        .and_then(|window| window.downcast::<Workspace>())
 368                    {
 369                        workspace_window
 370                            .update(cx, |workspace, _, cx| {
 371                                workspace.show_portal_error(err.to_string(), cx);
 372                            })
 373                            .ok();
 374                    }
 375                })
 376                .ok();
 377            }
 378        }
 379    })
 380    .detach();
 381}
 382
 383pub fn init(app_state: Arc<AppState>, cx: &mut App) {
 384    init_settings(cx);
 385    component::init();
 386    theme_preview::init(cx);
 387
 388    cx.on_action(Workspace::close_global);
 389    cx.on_action(reload);
 390
 391    cx.on_action({
 392        let app_state = Arc::downgrade(&app_state);
 393        move |_: &Open, cx: &mut App| {
 394            if let Some(app_state) = app_state.upgrade() {
 395                prompt_and_open_paths(
 396                    app_state,
 397                    PathPromptOptions {
 398                        files: true,
 399                        directories: true,
 400                        multiple: true,
 401                    },
 402                    cx,
 403                );
 404            }
 405        }
 406    });
 407    cx.on_action({
 408        let app_state = Arc::downgrade(&app_state);
 409        move |_: &OpenFiles, cx: &mut App| {
 410            let directories = cx.can_select_mixed_files_and_dirs();
 411            if let Some(app_state) = app_state.upgrade() {
 412                prompt_and_open_paths(
 413                    app_state,
 414                    PathPromptOptions {
 415                        files: true,
 416                        directories,
 417                        multiple: true,
 418                    },
 419                    cx,
 420                );
 421            }
 422        }
 423    });
 424}
 425
 426#[derive(Clone, Default, Deref, DerefMut)]
 427struct ProjectItemOpeners(Vec<ProjectItemOpener>);
 428
 429type ProjectItemOpener = fn(
 430    &Entity<Project>,
 431    &ProjectPath,
 432    &mut Window,
 433    &mut App,
 434)
 435    -> Option<Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>>>;
 436
 437type WorkspaceItemBuilder = Box<dyn FnOnce(&mut Window, &mut Context<Pane>) -> Box<dyn ItemHandle>>;
 438
 439impl Global for ProjectItemOpeners {}
 440
 441/// Registers a [ProjectItem] for the app. When opening a file, all the registered
 442/// items will get a chance to open the file, starting from the project item that
 443/// was added last.
 444pub fn register_project_item<I: ProjectItem>(cx: &mut App) {
 445    let builders = cx.default_global::<ProjectItemOpeners>();
 446    builders.push(|project, project_path, window, cx| {
 447        let project_item = <I::Item as project::ProjectItem>::try_open(project, project_path, cx)?;
 448        let project = project.clone();
 449        Some(window.spawn(cx, |cx| async move {
 450            let project_item = project_item.await?;
 451            let project_entry_id: Option<ProjectEntryId> =
 452                project_item.read_with(&cx, project::ProjectItem::entry_id)?;
 453            let build_workspace_item = Box::new(|window: &mut Window, cx: &mut Context<Pane>| {
 454                Box::new(cx.new(|cx| I::for_project_item(project, project_item, window, cx)))
 455                    as Box<dyn ItemHandle>
 456            }) as Box<_>;
 457            Ok((project_entry_id, build_workspace_item))
 458        }))
 459    });
 460}
 461
 462#[derive(Default)]
 463pub struct FollowableViewRegistry(HashMap<TypeId, FollowableViewDescriptor>);
 464
 465struct FollowableViewDescriptor {
 466    from_state_proto: fn(
 467        Entity<Workspace>,
 468        ViewId,
 469        &mut Option<proto::view::Variant>,
 470        &mut Window,
 471        &mut App,
 472    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>,
 473    to_followable_view: fn(&AnyView) -> Box<dyn FollowableItemHandle>,
 474}
 475
 476impl Global for FollowableViewRegistry {}
 477
 478impl FollowableViewRegistry {
 479    pub fn register<I: FollowableItem>(cx: &mut App) {
 480        cx.default_global::<Self>().0.insert(
 481            TypeId::of::<I>(),
 482            FollowableViewDescriptor {
 483                from_state_proto: |workspace, id, state, window, cx| {
 484                    I::from_state_proto(workspace, id, state, window, cx).map(|task| {
 485                        cx.foreground_executor()
 486                            .spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
 487                    })
 488                },
 489                to_followable_view: |view| Box::new(view.clone().downcast::<I>().unwrap()),
 490            },
 491        );
 492    }
 493
 494    pub fn from_state_proto(
 495        workspace: Entity<Workspace>,
 496        view_id: ViewId,
 497        mut state: Option<proto::view::Variant>,
 498        window: &mut Window,
 499        cx: &mut App,
 500    ) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>> {
 501        cx.update_default_global(|this: &mut Self, cx| {
 502            this.0.values().find_map(|descriptor| {
 503                (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
 504            })
 505        })
 506    }
 507
 508    pub fn to_followable_view(
 509        view: impl Into<AnyView>,
 510        cx: &App,
 511    ) -> Option<Box<dyn FollowableItemHandle>> {
 512        let this = cx.try_global::<Self>()?;
 513        let view = view.into();
 514        let descriptor = this.0.get(&view.entity_type())?;
 515        Some((descriptor.to_followable_view)(&view))
 516    }
 517}
 518
 519#[derive(Copy, Clone)]
 520struct SerializableItemDescriptor {
 521    deserialize: fn(
 522        Entity<Project>,
 523        WeakEntity<Workspace>,
 524        WorkspaceId,
 525        ItemId,
 526        &mut Window,
 527        &mut Context<Pane>,
 528    ) -> Task<Result<Box<dyn ItemHandle>>>,
 529    cleanup: fn(WorkspaceId, Vec<ItemId>, &mut Window, &mut App) -> Task<Result<()>>,
 530    view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
 531}
 532
 533#[derive(Default)]
 534struct SerializableItemRegistry {
 535    descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
 536    descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
 537}
 538
 539impl Global for SerializableItemRegistry {}
 540
 541impl SerializableItemRegistry {
 542    fn deserialize(
 543        item_kind: &str,
 544        project: Entity<Project>,
 545        workspace: WeakEntity<Workspace>,
 546        workspace_id: WorkspaceId,
 547        item_item: ItemId,
 548        window: &mut Window,
 549        cx: &mut Context<Pane>,
 550    ) -> Task<Result<Box<dyn ItemHandle>>> {
 551        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
 552            return Task::ready(Err(anyhow!(
 553                "cannot deserialize {}, descriptor not found",
 554                item_kind
 555            )));
 556        };
 557
 558        (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
 559    }
 560
 561    fn cleanup(
 562        item_kind: &str,
 563        workspace_id: WorkspaceId,
 564        loaded_items: Vec<ItemId>,
 565        window: &mut Window,
 566        cx: &mut App,
 567    ) -> Task<Result<()>> {
 568        let Some(descriptor) = Self::descriptor(item_kind, cx) else {
 569            return Task::ready(Err(anyhow!(
 570                "cannot cleanup {}, descriptor not found",
 571                item_kind
 572            )));
 573        };
 574
 575        (descriptor.cleanup)(workspace_id, loaded_items, window, cx)
 576    }
 577
 578    fn view_to_serializable_item_handle(
 579        view: AnyView,
 580        cx: &App,
 581    ) -> Option<Box<dyn SerializableItemHandle>> {
 582        let this = cx.try_global::<Self>()?;
 583        let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
 584        Some((descriptor.view_to_serializable_item)(view))
 585    }
 586
 587    fn descriptor(item_kind: &str, cx: &App) -> Option<SerializableItemDescriptor> {
 588        let this = cx.try_global::<Self>()?;
 589        this.descriptors_by_kind.get(item_kind).copied()
 590    }
 591}
 592
 593pub fn register_serializable_item<I: SerializableItem>(cx: &mut App) {
 594    let serialized_item_kind = I::serialized_item_kind();
 595
 596    let registry = cx.default_global::<SerializableItemRegistry>();
 597    let descriptor = SerializableItemDescriptor {
 598        deserialize: |project, workspace, workspace_id, item_id, window, cx| {
 599            let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
 600            cx.foreground_executor()
 601                .spawn(async { Ok(Box::new(task.await?) as Box<_>) })
 602        },
 603        cleanup: |workspace_id, loaded_items, window, cx| {
 604            I::cleanup(workspace_id, loaded_items, window, cx)
 605        },
 606        view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
 607    };
 608    registry
 609        .descriptors_by_kind
 610        .insert(Arc::from(serialized_item_kind), descriptor);
 611    registry
 612        .descriptors_by_type
 613        .insert(TypeId::of::<I>(), descriptor);
 614}
 615
 616pub struct AppState {
 617    pub languages: Arc<LanguageRegistry>,
 618    pub client: Arc<Client>,
 619    pub user_store: Entity<UserStore>,
 620    pub workspace_store: Entity<WorkspaceStore>,
 621    pub fs: Arc<dyn fs::Fs>,
 622    pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
 623    pub node_runtime: NodeRuntime,
 624    pub session: Entity<AppSession>,
 625}
 626
 627struct GlobalAppState(Weak<AppState>);
 628
 629impl Global for GlobalAppState {}
 630
 631pub struct WorkspaceStore {
 632    workspaces: HashSet<WindowHandle<Workspace>>,
 633    client: Arc<Client>,
 634    _subscriptions: Vec<client::Subscription>,
 635}
 636
 637#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
 638struct Follower {
 639    project_id: Option<u64>,
 640    peer_id: PeerId,
 641}
 642
 643impl AppState {
 644    #[track_caller]
 645    pub fn global(cx: &App) -> Weak<Self> {
 646        cx.global::<GlobalAppState>().0.clone()
 647    }
 648    pub fn try_global(cx: &App) -> Option<Weak<Self>> {
 649        cx.try_global::<GlobalAppState>()
 650            .map(|state| state.0.clone())
 651    }
 652    pub fn set_global(state: Weak<AppState>, cx: &mut App) {
 653        cx.set_global(GlobalAppState(state));
 654    }
 655
 656    #[cfg(any(test, feature = "test-support"))]
 657    pub fn test(cx: &mut App) -> Arc<Self> {
 658        use node_runtime::NodeRuntime;
 659        use session::Session;
 660        use settings::SettingsStore;
 661
 662        if !cx.has_global::<SettingsStore>() {
 663            let settings_store = SettingsStore::test(cx);
 664            cx.set_global(settings_store);
 665        }
 666
 667        let fs = fs::FakeFs::new(cx.background_executor().clone());
 668        let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
 669        let clock = Arc::new(clock::FakeSystemClock::new());
 670        let http_client = http_client::FakeHttpClient::with_404_response();
 671        let client = Client::new(clock, http_client.clone(), cx);
 672        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
 673        let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
 674        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
 675
 676        theme::init(theme::LoadThemes::JustBase, cx);
 677        client::init(&client, cx);
 678        crate::init_settings(cx);
 679
 680        Arc::new(Self {
 681            client,
 682            fs,
 683            languages,
 684            user_store,
 685            workspace_store,
 686            node_runtime: NodeRuntime::unavailable(),
 687            build_window_options: |_, _| Default::default(),
 688            session,
 689        })
 690    }
 691}
 692
 693struct DelayedDebouncedEditAction {
 694    task: Option<Task<()>>,
 695    cancel_channel: Option<oneshot::Sender<()>>,
 696}
 697
 698impl DelayedDebouncedEditAction {
 699    fn new() -> DelayedDebouncedEditAction {
 700        DelayedDebouncedEditAction {
 701            task: None,
 702            cancel_channel: None,
 703        }
 704    }
 705
 706    fn fire_new<F>(
 707        &mut self,
 708        delay: Duration,
 709        window: &mut Window,
 710        cx: &mut Context<Workspace>,
 711        func: F,
 712    ) where
 713        F: 'static
 714            + Send
 715            + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Task<Result<()>>,
 716    {
 717        if let Some(channel) = self.cancel_channel.take() {
 718            _ = channel.send(());
 719        }
 720
 721        let (sender, mut receiver) = oneshot::channel::<()>();
 722        self.cancel_channel = Some(sender);
 723
 724        let previous_task = self.task.take();
 725        self.task = Some(cx.spawn_in(window, move |workspace, mut cx| async move {
 726            let mut timer = cx.background_executor().timer(delay).fuse();
 727            if let Some(previous_task) = previous_task {
 728                previous_task.await;
 729            }
 730
 731            futures::select_biased! {
 732                _ = receiver => return,
 733                    _ = timer => {}
 734            }
 735
 736            if let Some(result) = workspace
 737                .update_in(&mut cx, |workspace, window, cx| {
 738                    (func)(workspace, window, cx)
 739                })
 740                .log_err()
 741            {
 742                result.await.log_err();
 743            }
 744        }));
 745    }
 746}
 747
 748pub enum Event {
 749    PaneAdded(Entity<Pane>),
 750    PaneRemoved,
 751    ItemAdded {
 752        item: Box<dyn ItemHandle>,
 753    },
 754    ItemRemoved,
 755    ActiveItemChanged,
 756    UserSavedItem {
 757        pane: WeakEntity<Pane>,
 758        item: Box<dyn WeakItemHandle>,
 759        save_intent: SaveIntent,
 760    },
 761    ContactRequestedJoin(u64),
 762    WorkspaceCreated(WeakEntity<Workspace>),
 763    SpawnTask {
 764        action: Box<SpawnInTerminal>,
 765    },
 766    OpenBundledFile {
 767        text: Cow<'static, str>,
 768        title: &'static str,
 769        language: &'static str,
 770    },
 771    ZoomChanged,
 772}
 773
 774#[derive(Debug)]
 775pub enum OpenVisible {
 776    All,
 777    None,
 778    OnlyFiles,
 779    OnlyDirectories,
 780}
 781
 782type PromptForNewPath = Box<
 783    dyn Fn(
 784        &mut Workspace,
 785        &mut Window,
 786        &mut Context<Workspace>,
 787    ) -> oneshot::Receiver<Option<ProjectPath>>,
 788>;
 789
 790type PromptForOpenPath = Box<
 791    dyn Fn(
 792        &mut Workspace,
 793        DirectoryLister,
 794        &mut Window,
 795        &mut Context<Workspace>,
 796    ) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
 797>;
 798
 799/// Collects everything project-related for a certain window opened.
 800/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
 801///
 802/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
 803/// The `Workspace` owns everybody's state and serves as a default, "global context",
 804/// that can be used to register a global action to be triggered from any place in the window.
 805pub struct Workspace {
 806    weak_self: WeakEntity<Self>,
 807    workspace_actions: Vec<Box<dyn Fn(Div, &mut Window, &mut Context<Self>) -> Div>>,
 808    zoomed: Option<AnyWeakView>,
 809    previous_dock_drag_coordinates: Option<Point<Pixels>>,
 810    zoomed_position: Option<DockPosition>,
 811    center: PaneGroup,
 812    left_dock: Entity<Dock>,
 813    bottom_dock: Entity<Dock>,
 814    right_dock: Entity<Dock>,
 815    panes: Vec<Entity<Pane>>,
 816    panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
 817    active_pane: Entity<Pane>,
 818    last_active_center_pane: Option<WeakEntity<Pane>>,
 819    last_active_view_id: Option<proto::ViewId>,
 820    status_bar: Entity<StatusBar>,
 821    modal_layer: Entity<ModalLayer>,
 822    toast_layer: Entity<ToastLayer>,
 823    titlebar_item: Option<AnyView>,
 824    notifications: Notifications,
 825    project: Entity<Project>,
 826    follower_states: HashMap<PeerId, FollowerState>,
 827    last_leaders_by_pane: HashMap<WeakEntity<Pane>, PeerId>,
 828    window_edited: bool,
 829    dirty_items: HashMap<EntityId, Subscription>,
 830    active_call: Option<(Entity<ActiveCall>, Vec<Subscription>)>,
 831    leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
 832    database_id: Option<WorkspaceId>,
 833    app_state: Arc<AppState>,
 834    dispatching_keystrokes: Rc<RefCell<(HashSet<String>, Vec<Keystroke>)>>,
 835    _subscriptions: Vec<Subscription>,
 836    _apply_leader_updates: Task<Result<()>>,
 837    _observe_current_user: Task<Result<()>>,
 838    _schedule_serialize: Option<Task<()>>,
 839    pane_history_timestamp: Arc<AtomicUsize>,
 840    bounds: Bounds<Pixels>,
 841    centered_layout: bool,
 842    bounds_save_task_queued: Option<Task<()>>,
 843    on_prompt_for_new_path: Option<PromptForNewPath>,
 844    on_prompt_for_open_path: Option<PromptForOpenPath>,
 845    serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
 846    serialized_ssh_project: Option<SerializedSshProject>,
 847    _items_serializer: Task<Result<()>>,
 848    session_id: Option<String>,
 849}
 850
 851impl EventEmitter<Event> for Workspace {}
 852
 853#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 854pub struct ViewId {
 855    pub creator: PeerId,
 856    pub id: u64,
 857}
 858
 859pub struct FollowerState {
 860    center_pane: Entity<Pane>,
 861    dock_pane: Option<Entity<Pane>>,
 862    active_view_id: Option<ViewId>,
 863    items_by_leader_view_id: HashMap<ViewId, FollowerView>,
 864}
 865
 866struct FollowerView {
 867    view: Box<dyn FollowableItemHandle>,
 868    location: Option<proto::PanelId>,
 869}
 870
 871impl Workspace {
 872    const DEFAULT_PADDING: f32 = 0.2;
 873    const MAX_PADDING: f32 = 0.4;
 874
 875    pub fn new(
 876        workspace_id: Option<WorkspaceId>,
 877        project: Entity<Project>,
 878        app_state: Arc<AppState>,
 879        window: &mut Window,
 880        cx: &mut Context<Self>,
 881    ) -> Self {
 882        cx.observe_in(&project, window, |_, _, _, cx| cx.notify())
 883            .detach();
 884        cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
 885            match event {
 886                project::Event::RemoteIdChanged(_) => {
 887                    this.update_window_title(window, cx);
 888                }
 889
 890                project::Event::CollaboratorLeft(peer_id) => {
 891                    this.collaborator_left(*peer_id, window, cx);
 892                }
 893
 894                project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
 895                    this.update_window_title(window, cx);
 896                    this.serialize_workspace(window, cx);
 897                }
 898
 899                project::Event::DisconnectedFromHost => {
 900                    this.update_window_edited(window, cx);
 901                    let leaders_to_unfollow =
 902                        this.follower_states.keys().copied().collect::<Vec<_>>();
 903                    for leader_id in leaders_to_unfollow {
 904                        this.unfollow(leader_id, window, cx);
 905                    }
 906                }
 907
 908                project::Event::DisconnectedFromSshRemote => {
 909                    this.update_window_edited(window, cx);
 910                }
 911
 912                project::Event::Closed => {
 913                    window.remove_window();
 914                }
 915
 916                project::Event::DeletedEntry(_, entry_id) => {
 917                    for pane in this.panes.iter() {
 918                        pane.update(cx, |pane, cx| {
 919                            pane.handle_deleted_project_item(*entry_id, window, cx)
 920                        });
 921                    }
 922                }
 923
 924                project::Event::Toast {
 925                    notification_id,
 926                    message,
 927                } => this.show_notification(
 928                    NotificationId::named(notification_id.clone()),
 929                    cx,
 930                    |cx| cx.new(|cx| MessageNotification::new(message.clone(), cx)),
 931                ),
 932
 933                project::Event::HideToast { notification_id } => {
 934                    this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
 935                }
 936
 937                project::Event::LanguageServerPrompt(request) => {
 938                    struct LanguageServerPrompt;
 939
 940                    let mut hasher = DefaultHasher::new();
 941                    request.lsp_name.as_str().hash(&mut hasher);
 942                    let id = hasher.finish();
 943
 944                    this.show_notification(
 945                        NotificationId::composite::<LanguageServerPrompt>(id as usize),
 946                        cx,
 947                        |cx| {
 948                            cx.new(|cx| {
 949                                notifications::LanguageServerPrompt::new(request.clone(), cx)
 950                            })
 951                        },
 952                    );
 953                }
 954
 955                _ => {}
 956            }
 957            cx.notify()
 958        })
 959        .detach();
 960
 961        cx.on_focus_lost(window, |this, window, cx| {
 962            let focus_handle = this.focus_handle(cx);
 963            window.focus(&focus_handle);
 964        })
 965        .detach();
 966
 967        let weak_handle = cx.entity().downgrade();
 968        let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
 969
 970        let center_pane = cx.new(|cx| {
 971            let mut center_pane = Pane::new(
 972                weak_handle.clone(),
 973                project.clone(),
 974                pane_history_timestamp.clone(),
 975                None,
 976                NewFile.boxed_clone(),
 977                window,
 978                cx,
 979            );
 980            center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
 981            center_pane
 982        });
 983        cx.subscribe_in(&center_pane, window, Self::handle_pane_event)
 984            .detach();
 985
 986        window.focus(&center_pane.focus_handle(cx));
 987
 988        cx.emit(Event::PaneAdded(center_pane.clone()));
 989
 990        let window_handle = window.window_handle().downcast::<Workspace>().unwrap();
 991        app_state.workspace_store.update(cx, |store, _| {
 992            store.workspaces.insert(window_handle);
 993        });
 994
 995        let mut current_user = app_state.user_store.read(cx).watch_current_user();
 996        let mut connection_status = app_state.client.status();
 997        let _observe_current_user = cx.spawn_in(window, |this, mut cx| async move {
 998            current_user.next().await;
 999            connection_status.next().await;
1000            let mut stream =
1001                Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
1002
1003            while stream.recv().await.is_some() {
1004                this.update(&mut cx, |_, cx| cx.notify())?;
1005            }
1006            anyhow::Ok(())
1007        });
1008
1009        // All leader updates are enqueued and then processed in a single task, so
1010        // that each asynchronous operation can be run in order.
1011        let (leader_updates_tx, mut leader_updates_rx) =
1012            mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
1013        let _apply_leader_updates = cx.spawn_in(window, |this, mut cx| async move {
1014            while let Some((leader_id, update)) = leader_updates_rx.next().await {
1015                Self::process_leader_update(&this, leader_id, update, &mut cx)
1016                    .await
1017                    .log_err();
1018            }
1019
1020            Ok(())
1021        });
1022
1023        cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
1024
1025        let left_dock = Dock::new(DockPosition::Left, window, cx);
1026        let bottom_dock = Dock::new(DockPosition::Bottom, window, cx);
1027        let right_dock = Dock::new(DockPosition::Right, window, cx);
1028        let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
1029        let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
1030        let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
1031        let status_bar = cx.new(|cx| {
1032            let mut status_bar = StatusBar::new(&center_pane.clone(), window, cx);
1033            status_bar.add_left_item(left_dock_buttons, window, cx);
1034            status_bar.add_right_item(right_dock_buttons, window, cx);
1035            status_bar.add_right_item(bottom_dock_buttons, window, cx);
1036            status_bar
1037        });
1038
1039        let modal_layer = cx.new(|_| ModalLayer::new());
1040        let toast_layer = cx.new(|_| ToastLayer::new());
1041
1042        let session_id = app_state.session.read(cx).id().to_owned();
1043
1044        let mut active_call = None;
1045        if let Some(call) = ActiveCall::try_global(cx) {
1046            let call = call.clone();
1047            let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
1048            active_call = Some((call, subscriptions));
1049        }
1050
1051        let (serializable_items_tx, serializable_items_rx) =
1052            mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
1053        let _items_serializer = cx.spawn_in(window, |this, mut cx| async move {
1054            Self::serialize_items(&this, serializable_items_rx, &mut cx).await
1055        });
1056
1057        let subscriptions = vec![
1058            cx.observe_window_activation(window, Self::on_window_activation_changed),
1059            cx.observe_window_bounds(window, move |this, window, cx| {
1060                if this.bounds_save_task_queued.is_some() {
1061                    return;
1062                }
1063                this.bounds_save_task_queued =
1064                    Some(cx.spawn_in(window, |this, mut cx| async move {
1065                        cx.background_executor()
1066                            .timer(Duration::from_millis(100))
1067                            .await;
1068                        this.update_in(&mut cx, |this, window, cx| {
1069                            if let Some(display) = window.display(cx) {
1070                                if let Ok(display_uuid) = display.uuid() {
1071                                    let window_bounds = window.inner_window_bounds();
1072                                    if let Some(database_id) = workspace_id {
1073                                        cx.background_executor()
1074                                            .spawn(DB.set_window_open_status(
1075                                                database_id,
1076                                                SerializedWindowBounds(window_bounds),
1077                                                display_uuid,
1078                                            ))
1079                                            .detach_and_log_err(cx);
1080                                    }
1081                                }
1082                            }
1083                            this.bounds_save_task_queued.take();
1084                        })
1085                        .ok();
1086                    }));
1087                cx.notify();
1088            }),
1089            cx.observe_window_appearance(window, |_, window, cx| {
1090                let window_appearance = window.appearance();
1091
1092                *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
1093
1094                ThemeSettings::reload_current_theme(cx);
1095                ThemeSettings::reload_current_icon_theme(cx);
1096            }),
1097            cx.on_release(move |this, cx| {
1098                this.app_state.workspace_store.update(cx, move |store, _| {
1099                    store.workspaces.remove(&window_handle.clone());
1100                })
1101            }),
1102        ];
1103
1104        cx.defer_in(window, |this, window, cx| {
1105            this.update_window_title(window, cx);
1106            this.show_initial_notifications(cx);
1107        });
1108        Workspace {
1109            weak_self: weak_handle.clone(),
1110            zoomed: None,
1111            zoomed_position: None,
1112            previous_dock_drag_coordinates: None,
1113            center: PaneGroup::new(center_pane.clone()),
1114            panes: vec![center_pane.clone()],
1115            panes_by_item: Default::default(),
1116            active_pane: center_pane.clone(),
1117            last_active_center_pane: Some(center_pane.downgrade()),
1118            last_active_view_id: None,
1119            status_bar,
1120            modal_layer,
1121            toast_layer,
1122            titlebar_item: None,
1123            notifications: Default::default(),
1124            left_dock,
1125            bottom_dock,
1126            right_dock,
1127            project: project.clone(),
1128            follower_states: Default::default(),
1129            last_leaders_by_pane: Default::default(),
1130            dispatching_keystrokes: Default::default(),
1131            window_edited: false,
1132            dirty_items: Default::default(),
1133            active_call,
1134            database_id: workspace_id,
1135            app_state,
1136            _observe_current_user,
1137            _apply_leader_updates,
1138            _schedule_serialize: None,
1139            leader_updates_tx,
1140            _subscriptions: subscriptions,
1141            pane_history_timestamp,
1142            workspace_actions: Default::default(),
1143            // This data will be incorrect, but it will be overwritten by the time it needs to be used.
1144            bounds: Default::default(),
1145            centered_layout: false,
1146            bounds_save_task_queued: None,
1147            on_prompt_for_new_path: None,
1148            on_prompt_for_open_path: None,
1149            serializable_items_tx,
1150            _items_serializer,
1151            session_id: Some(session_id),
1152            serialized_ssh_project: None,
1153        }
1154    }
1155
1156    pub fn new_local(
1157        abs_paths: Vec<PathBuf>,
1158        app_state: Arc<AppState>,
1159        requesting_window: Option<WindowHandle<Workspace>>,
1160        env: Option<HashMap<String, String>>,
1161        cx: &mut App,
1162    ) -> Task<
1163        anyhow::Result<(
1164            WindowHandle<Workspace>,
1165            Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
1166        )>,
1167    > {
1168        let project_handle = Project::local(
1169            app_state.client.clone(),
1170            app_state.node_runtime.clone(),
1171            app_state.user_store.clone(),
1172            app_state.languages.clone(),
1173            app_state.fs.clone(),
1174            env,
1175            cx,
1176        );
1177
1178        cx.spawn(|mut cx| async move {
1179            let mut paths_to_open = Vec::with_capacity(abs_paths.len());
1180            for path in abs_paths.into_iter() {
1181                if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
1182                    paths_to_open.push(canonical)
1183                } else {
1184                    paths_to_open.push(path)
1185                }
1186            }
1187
1188            let serialized_workspace: Option<SerializedWorkspace> =
1189                persistence::DB.workspace_for_roots(paths_to_open.as_slice());
1190
1191            let workspace_location = serialized_workspace
1192                .as_ref()
1193                .map(|ws| &ws.location)
1194                .and_then(|loc| match loc {
1195                    SerializedWorkspaceLocation::Local(_, order) => {
1196                        Some((loc.sorted_paths(), order.order()))
1197                    }
1198                    _ => None,
1199                });
1200
1201            if let Some((paths, order)) = workspace_location {
1202                paths_to_open = paths.iter().cloned().collect();
1203
1204                if order.iter().enumerate().any(|(i, &j)| i != j) {
1205                    project_handle
1206                        .update(&mut cx, |project, cx| {
1207                            project.set_worktrees_reordered(true, cx);
1208                        })
1209                        .log_err();
1210                }
1211            }
1212
1213            // Get project paths for all of the abs_paths
1214            let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
1215                Vec::with_capacity(paths_to_open.len());
1216            for path in paths_to_open.into_iter() {
1217                if let Some((_, project_entry)) = cx
1218                    .update(|cx| {
1219                        Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
1220                    })?
1221                    .await
1222                    .log_err()
1223                {
1224                    project_paths.push((path, Some(project_entry)));
1225                } else {
1226                    project_paths.push((path, None));
1227                }
1228            }
1229
1230            let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
1231                serialized_workspace.id
1232            } else {
1233                DB.next_id().await.unwrap_or_else(|_| Default::default())
1234            };
1235
1236            let toolchains = DB.toolchains(workspace_id).await?;
1237            for (toolchain, worktree_id) in toolchains {
1238                project_handle
1239                    .update(&mut cx, |this, cx| {
1240                        this.activate_toolchain(worktree_id, toolchain, cx)
1241                    })?
1242                    .await;
1243            }
1244            let window = if let Some(window) = requesting_window {
1245                cx.update_window(window.into(), |_, window, cx| {
1246                    window.replace_root(cx, |window, cx| {
1247                        Workspace::new(
1248                            Some(workspace_id),
1249                            project_handle.clone(),
1250                            app_state.clone(),
1251                            window,
1252                            cx,
1253                        )
1254                    });
1255                })?;
1256                window
1257            } else {
1258                let window_bounds_override = window_bounds_env_override();
1259
1260                let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
1261                    (Some(WindowBounds::Windowed(bounds)), None)
1262                } else {
1263                    let restorable_bounds = serialized_workspace
1264                        .as_ref()
1265                        .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
1266                        .or_else(|| {
1267                            let (display, window_bounds) = DB.last_window().log_err()?;
1268                            Some((display?, window_bounds?))
1269                        });
1270
1271                    if let Some((serialized_display, serialized_status)) = restorable_bounds {
1272                        (Some(serialized_status.0), Some(serialized_display))
1273                    } else {
1274                        (None, None)
1275                    }
1276                };
1277
1278                // Use the serialized workspace to construct the new window
1279                let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
1280                options.window_bounds = window_bounds;
1281                let centered_layout = serialized_workspace
1282                    .as_ref()
1283                    .map(|w| w.centered_layout)
1284                    .unwrap_or(false);
1285                cx.open_window(options, {
1286                    let app_state = app_state.clone();
1287                    let project_handle = project_handle.clone();
1288                    move |window, cx| {
1289                        cx.new(|cx| {
1290                            let mut workspace = Workspace::new(
1291                                Some(workspace_id),
1292                                project_handle,
1293                                app_state,
1294                                window,
1295                                cx,
1296                            );
1297                            workspace.centered_layout = centered_layout;
1298                            workspace
1299                        })
1300                    }
1301                })?
1302            };
1303
1304            notify_if_database_failed(window, &mut cx);
1305            let opened_items = window
1306                .update(&mut cx, |_workspace, window, cx| {
1307                    open_items(serialized_workspace, project_paths, window, cx)
1308                })?
1309                .await
1310                .unwrap_or_default();
1311
1312            window
1313                .update(&mut cx, |_, window, _| window.activate_window())
1314                .log_err();
1315            Ok((window, opened_items))
1316        })
1317    }
1318
1319    pub fn weak_handle(&self) -> WeakEntity<Self> {
1320        self.weak_self.clone()
1321    }
1322
1323    pub fn left_dock(&self) -> &Entity<Dock> {
1324        &self.left_dock
1325    }
1326
1327    pub fn bottom_dock(&self) -> &Entity<Dock> {
1328        &self.bottom_dock
1329    }
1330
1331    pub fn right_dock(&self) -> &Entity<Dock> {
1332        &self.right_dock
1333    }
1334
1335    pub fn all_docks(&self) -> [&Entity<Dock>; 3] {
1336        [&self.left_dock, &self.bottom_dock, &self.right_dock]
1337    }
1338
1339    pub fn dock_at_position(&self, position: DockPosition) -> &Entity<Dock> {
1340        match position {
1341            DockPosition::Left => &self.left_dock,
1342            DockPosition::Bottom => &self.bottom_dock,
1343            DockPosition::Right => &self.right_dock,
1344        }
1345    }
1346
1347    pub fn is_edited(&self) -> bool {
1348        self.window_edited
1349    }
1350
1351    pub fn add_panel<T: Panel>(
1352        &mut self,
1353        panel: Entity<T>,
1354        window: &mut Window,
1355        cx: &mut Context<Self>,
1356    ) {
1357        let focus_handle = panel.panel_focus_handle(cx);
1358        cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
1359            .detach();
1360
1361        let dock_position = panel.position(window, cx);
1362        let dock = self.dock_at_position(dock_position);
1363
1364        dock.update(cx, |dock, cx| {
1365            dock.add_panel(panel, self.weak_self.clone(), window, cx)
1366        });
1367    }
1368
1369    pub fn status_bar(&self) -> &Entity<StatusBar> {
1370        &self.status_bar
1371    }
1372
1373    pub fn app_state(&self) -> &Arc<AppState> {
1374        &self.app_state
1375    }
1376
1377    pub fn user_store(&self) -> &Entity<UserStore> {
1378        &self.app_state.user_store
1379    }
1380
1381    pub fn project(&self) -> &Entity<Project> {
1382        &self.project
1383    }
1384
1385    pub fn recent_navigation_history_iter(
1386        &self,
1387        cx: &App,
1388    ) -> impl Iterator<Item = (ProjectPath, Option<PathBuf>)> {
1389        let mut abs_paths_opened: HashMap<PathBuf, HashSet<ProjectPath>> = HashMap::default();
1390        let mut history: HashMap<ProjectPath, (Option<PathBuf>, usize)> = HashMap::default();
1391        for pane in &self.panes {
1392            let pane = pane.read(cx);
1393            pane.nav_history()
1394                .for_each_entry(cx, |entry, (project_path, fs_path)| {
1395                    if let Some(fs_path) = &fs_path {
1396                        abs_paths_opened
1397                            .entry(fs_path.clone())
1398                            .or_default()
1399                            .insert(project_path.clone());
1400                    }
1401                    let timestamp = entry.timestamp;
1402                    match history.entry(project_path) {
1403                        hash_map::Entry::Occupied(mut entry) => {
1404                            let (_, old_timestamp) = entry.get();
1405                            if &timestamp > old_timestamp {
1406                                entry.insert((fs_path, timestamp));
1407                            }
1408                        }
1409                        hash_map::Entry::Vacant(entry) => {
1410                            entry.insert((fs_path, timestamp));
1411                        }
1412                    }
1413                });
1414        }
1415
1416        history
1417            .into_iter()
1418            .sorted_by_key(|(_, (_, timestamp))| *timestamp)
1419            .map(|(project_path, (fs_path, _))| (project_path, fs_path))
1420            .rev()
1421            .filter(move |(history_path, abs_path)| {
1422                let latest_project_path_opened = abs_path
1423                    .as_ref()
1424                    .and_then(|abs_path| abs_paths_opened.get(abs_path))
1425                    .and_then(|project_paths| {
1426                        project_paths
1427                            .iter()
1428                            .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
1429                    });
1430
1431                match latest_project_path_opened {
1432                    Some(latest_project_path_opened) => latest_project_path_opened == history_path,
1433                    None => true,
1434                }
1435            })
1436    }
1437
1438    pub fn recent_navigation_history(
1439        &self,
1440        limit: Option<usize>,
1441        cx: &App,
1442    ) -> Vec<(ProjectPath, Option<PathBuf>)> {
1443        self.recent_navigation_history_iter(cx)
1444            .take(limit.unwrap_or(usize::MAX))
1445            .collect()
1446    }
1447
1448    fn navigate_history(
1449        &mut self,
1450        pane: WeakEntity<Pane>,
1451        mode: NavigationMode,
1452        window: &mut Window,
1453        cx: &mut Context<Workspace>,
1454    ) -> Task<Result<()>> {
1455        let to_load = if let Some(pane) = pane.upgrade() {
1456            pane.update(cx, |pane, cx| {
1457                window.focus(&pane.focus_handle(cx));
1458                loop {
1459                    // Retrieve the weak item handle from the history.
1460                    let entry = pane.nav_history_mut().pop(mode, cx)?;
1461
1462                    // If the item is still present in this pane, then activate it.
1463                    if let Some(index) = entry
1464                        .item
1465                        .upgrade()
1466                        .and_then(|v| pane.index_for_item(v.as_ref()))
1467                    {
1468                        let prev_active_item_index = pane.active_item_index();
1469                        pane.nav_history_mut().set_mode(mode);
1470                        pane.activate_item(index, true, true, window, cx);
1471                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
1472
1473                        let mut navigated = prev_active_item_index != pane.active_item_index();
1474                        if let Some(data) = entry.data {
1475                            navigated |= pane.active_item()?.navigate(data, window, cx);
1476                        }
1477
1478                        if navigated {
1479                            break None;
1480                        }
1481                    } else {
1482                        // If the item is no longer present in this pane, then retrieve its
1483                        // path info in order to reopen it.
1484                        break pane
1485                            .nav_history()
1486                            .path_for_item(entry.item.id())
1487                            .map(|(project_path, abs_path)| (project_path, abs_path, entry));
1488                    }
1489                }
1490            })
1491        } else {
1492            None
1493        };
1494
1495        if let Some((project_path, abs_path, entry)) = to_load {
1496            // If the item was no longer present, then load it again from its previous path, first try the local path
1497            let open_by_project_path = self.load_path(project_path.clone(), window, cx);
1498
1499            cx.spawn_in(window, |workspace, mut cx| async move {
1500                let open_by_project_path = open_by_project_path.await;
1501                let mut navigated = false;
1502                match open_by_project_path
1503                    .with_context(|| format!("Navigating to {project_path:?}"))
1504                {
1505                    Ok((project_entry_id, build_item)) => {
1506                        let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1507                            pane.nav_history_mut().set_mode(mode);
1508                            pane.active_item().map(|p| p.item_id())
1509                        })?;
1510
1511                        pane.update_in(&mut cx, |pane, window, cx| {
1512                            let item = pane.open_item(
1513                                project_entry_id,
1514                                true,
1515                                entry.is_preview,
1516                                true,
1517                                None,
1518                                window, cx,
1519                                build_item,
1520                            );
1521                            navigated |= Some(item.item_id()) != prev_active_item_id;
1522                            pane.nav_history_mut().set_mode(NavigationMode::Normal);
1523                            if let Some(data) = entry.data {
1524                                navigated |= item.navigate(data, window, cx);
1525                            }
1526                        })?;
1527                    }
1528                    Err(open_by_project_path_e) => {
1529                        // Fall back to opening by abs path, in case an external file was opened and closed,
1530                        // and its worktree is now dropped
1531                        if let Some(abs_path) = abs_path {
1532                            let prev_active_item_id = pane.update(&mut cx, |pane, _| {
1533                                pane.nav_history_mut().set_mode(mode);
1534                                pane.active_item().map(|p| p.item_id())
1535                            })?;
1536                            let open_by_abs_path = workspace.update_in(&mut cx, |workspace, window, cx| {
1537                                workspace.open_abs_path(abs_path.clone(), false, window, cx)
1538                            })?;
1539                            match open_by_abs_path
1540                                .await
1541                                .with_context(|| format!("Navigating to {abs_path:?}"))
1542                            {
1543                                Ok(item) => {
1544                                    pane.update_in(&mut cx, |pane, window, cx| {
1545                                        navigated |= Some(item.item_id()) != prev_active_item_id;
1546                                        pane.nav_history_mut().set_mode(NavigationMode::Normal);
1547                                        if let Some(data) = entry.data {
1548                                            navigated |= item.navigate(data, window, cx);
1549                                        }
1550                                    })?;
1551                                }
1552                                Err(open_by_abs_path_e) => {
1553                                    log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
1554                                }
1555                            }
1556                        }
1557                    }
1558                }
1559
1560                if !navigated {
1561                    workspace
1562                        .update_in(&mut cx, |workspace, window, cx| {
1563                            Self::navigate_history(workspace, pane, mode, window, cx)
1564                        })?
1565                        .await?;
1566                }
1567
1568                Ok(())
1569            })
1570        } else {
1571            Task::ready(Ok(()))
1572        }
1573    }
1574
1575    pub fn go_back(
1576        &mut self,
1577        pane: WeakEntity<Pane>,
1578        window: &mut Window,
1579        cx: &mut Context<Workspace>,
1580    ) -> Task<Result<()>> {
1581        self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
1582    }
1583
1584    pub fn go_forward(
1585        &mut self,
1586        pane: WeakEntity<Pane>,
1587        window: &mut Window,
1588        cx: &mut Context<Workspace>,
1589    ) -> Task<Result<()>> {
1590        self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
1591    }
1592
1593    pub fn reopen_closed_item(
1594        &mut self,
1595        window: &mut Window,
1596        cx: &mut Context<Workspace>,
1597    ) -> Task<Result<()>> {
1598        self.navigate_history(
1599            self.active_pane().downgrade(),
1600            NavigationMode::ReopeningClosedItem,
1601            window,
1602            cx,
1603        )
1604    }
1605
1606    pub fn client(&self) -> &Arc<Client> {
1607        &self.app_state.client
1608    }
1609
1610    pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context<Self>) {
1611        self.titlebar_item = Some(item);
1612        cx.notify();
1613    }
1614
1615    pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
1616        self.on_prompt_for_new_path = Some(prompt)
1617    }
1618
1619    pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
1620        self.on_prompt_for_open_path = Some(prompt)
1621    }
1622
1623    pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
1624        self.serialized_ssh_project.clone()
1625    }
1626
1627    pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) {
1628        self.serialized_ssh_project = Some(serialized_ssh_project);
1629    }
1630
1631    pub fn prompt_for_open_path(
1632        &mut self,
1633        path_prompt_options: PathPromptOptions,
1634        lister: DirectoryLister,
1635        window: &mut Window,
1636        cx: &mut Context<Self>,
1637    ) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
1638        if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
1639            let prompt = self.on_prompt_for_open_path.take().unwrap();
1640            let rx = prompt(self, lister, window, cx);
1641            self.on_prompt_for_open_path = Some(prompt);
1642            rx
1643        } else {
1644            let (tx, rx) = oneshot::channel();
1645            let abs_path = cx.prompt_for_paths(path_prompt_options);
1646
1647            cx.spawn_in(window, |this, mut cx| async move {
1648                let Ok(result) = abs_path.await else {
1649                    return Ok(());
1650                };
1651
1652                match result {
1653                    Ok(result) => {
1654                        tx.send(result).log_err();
1655                    }
1656                    Err(err) => {
1657                        let rx = this.update_in(&mut cx, |this, window, cx| {
1658                            this.show_portal_error(err.to_string(), cx);
1659                            let prompt = this.on_prompt_for_open_path.take().unwrap();
1660                            let rx = prompt(this, lister, window, cx);
1661                            this.on_prompt_for_open_path = Some(prompt);
1662                            rx
1663                        })?;
1664                        if let Ok(path) = rx.await {
1665                            tx.send(path).log_err();
1666                        }
1667                    }
1668                };
1669                anyhow::Ok(())
1670            })
1671            .detach();
1672
1673            rx
1674        }
1675    }
1676
1677    pub fn prompt_for_new_path(
1678        &mut self,
1679        window: &mut Window,
1680        cx: &mut Context<Self>,
1681    ) -> oneshot::Receiver<Option<ProjectPath>> {
1682        if (self.project.read(cx).is_via_collab() || self.project.read(cx).is_via_ssh())
1683            || !WorkspaceSettings::get_global(cx).use_system_path_prompts
1684        {
1685            let prompt = self.on_prompt_for_new_path.take().unwrap();
1686            let rx = prompt(self, window, cx);
1687            self.on_prompt_for_new_path = Some(prompt);
1688            rx
1689        } else {
1690            let start_abs_path = self
1691                .project
1692                .update(cx, |project, cx| {
1693                    let worktree = project.visible_worktrees(cx).next()?;
1694                    Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
1695                })
1696                .unwrap_or_else(|| Path::new("").into());
1697
1698            let (tx, rx) = oneshot::channel();
1699            let abs_path = cx.prompt_for_new_path(&start_abs_path);
1700            cx.spawn_in(window, |this, mut cx| async move {
1701                let abs_path = match abs_path.await? {
1702                    Ok(path) => path,
1703                    Err(err) => {
1704                        let rx = this.update_in(&mut cx, |this, window, cx| {
1705                            this.show_portal_error(err.to_string(), cx);
1706
1707                            let prompt = this.on_prompt_for_new_path.take().unwrap();
1708                            let rx = prompt(this, window, cx);
1709                            this.on_prompt_for_new_path = Some(prompt);
1710                            rx
1711                        })?;
1712                        if let Ok(path) = rx.await {
1713                            tx.send(path).log_err();
1714                        }
1715                        return anyhow::Ok(());
1716                    }
1717                };
1718
1719                let project_path = abs_path.and_then(|abs_path| {
1720                    this.update(&mut cx, |this, cx| {
1721                        this.project.update(cx, |project, cx| {
1722                            project.find_or_create_worktree(abs_path, true, cx)
1723                        })
1724                    })
1725                    .ok()
1726                });
1727
1728                if let Some(project_path) = project_path {
1729                    let (worktree, path) = project_path.await?;
1730                    let worktree_id = worktree.read_with(&cx, |worktree, _| worktree.id())?;
1731                    tx.send(Some(ProjectPath {
1732                        worktree_id,
1733                        path: path.into(),
1734                    }))
1735                    .ok();
1736                } else {
1737                    tx.send(None).ok();
1738                }
1739                anyhow::Ok(())
1740            })
1741            .detach_and_log_err(cx);
1742
1743            rx
1744        }
1745    }
1746
1747    pub fn titlebar_item(&self) -> Option<AnyView> {
1748        self.titlebar_item.clone()
1749    }
1750
1751    /// Call the given callback with a workspace whose project is local.
1752    ///
1753    /// If the given workspace has a local project, then it will be passed
1754    /// to the callback. Otherwise, a new empty window will be created.
1755    pub fn with_local_workspace<T, F>(
1756        &mut self,
1757        window: &mut Window,
1758        cx: &mut Context<Self>,
1759        callback: F,
1760    ) -> Task<Result<T>>
1761    where
1762        T: 'static,
1763        F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
1764    {
1765        if self.project.read(cx).is_local() {
1766            Task::ready(Ok(callback(self, window, cx)))
1767        } else {
1768            let env = self.project.read(cx).cli_environment(cx);
1769            let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx);
1770            cx.spawn_in(window, |_vh, mut cx| async move {
1771                let (workspace, _) = task.await?;
1772                workspace.update(&mut cx, callback)
1773            })
1774        }
1775    }
1776
1777    pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
1778        self.project.read(cx).worktrees(cx)
1779    }
1780
1781    pub fn visible_worktrees<'a>(
1782        &self,
1783        cx: &'a App,
1784    ) -> impl 'a + Iterator<Item = Entity<Worktree>> {
1785        self.project.read(cx).visible_worktrees(cx)
1786    }
1787
1788    #[cfg(any(test, feature = "test-support"))]
1789    pub fn worktree_scans_complete(&self, cx: &App) -> impl Future<Output = ()> + 'static {
1790        let futures = self
1791            .worktrees(cx)
1792            .filter_map(|worktree| worktree.read(cx).as_local())
1793            .map(|worktree| worktree.scan_complete())
1794            .collect::<Vec<_>>();
1795        async move {
1796            for future in futures {
1797                future.await;
1798            }
1799        }
1800    }
1801
1802    pub fn close_global(_: &CloseWindow, cx: &mut App) {
1803        cx.defer(|cx| {
1804            cx.windows().iter().find(|window| {
1805                window
1806                    .update(cx, |_, window, _| {
1807                        if window.is_window_active() {
1808                            //This can only get called when the window's project connection has been lost
1809                            //so we don't need to prompt the user for anything and instead just close the window
1810                            window.remove_window();
1811                            true
1812                        } else {
1813                            false
1814                        }
1815                    })
1816                    .unwrap_or(false)
1817            });
1818        });
1819    }
1820
1821    pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
1822        let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx);
1823        cx.spawn_in(window, |_, mut cx| async move {
1824            if prepare.await? {
1825                cx.update(|window, _cx| window.remove_window())?;
1826            }
1827            anyhow::Ok(())
1828        })
1829        .detach_and_log_err(cx)
1830    }
1831
1832    pub fn move_focused_panel_to_next_position(
1833        &mut self,
1834        _: &MoveFocusedPanelToNextPosition,
1835        window: &mut Window,
1836        cx: &mut Context<Self>,
1837    ) {
1838        let docks = self.all_docks();
1839        let active_dock = docks
1840            .into_iter()
1841            .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
1842
1843        if let Some(dock) = active_dock {
1844            dock.update(cx, |dock, cx| {
1845                let active_panel = dock
1846                    .active_panel()
1847                    .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
1848
1849                if let Some(panel) = active_panel {
1850                    panel.move_to_next_position(window, cx);
1851                }
1852            })
1853        }
1854    }
1855
1856    pub fn prepare_to_close(
1857        &mut self,
1858        close_intent: CloseIntent,
1859        window: &mut Window,
1860        cx: &mut Context<Self>,
1861    ) -> Task<Result<bool>> {
1862        let active_call = self.active_call().cloned();
1863
1864        // On Linux and Windows, closing the last window should restore the last workspace.
1865        let save_last_workspace = cfg!(not(target_os = "macos"))
1866            && close_intent != CloseIntent::ReplaceWindow
1867            && cx.windows().len() == 1;
1868
1869        cx.spawn_in(window, |this, mut cx| async move {
1870            let workspace_count = cx.update(|_window, cx| {
1871                cx.windows()
1872                    .iter()
1873                    .filter(|window| window.downcast::<Workspace>().is_some())
1874                    .count()
1875            })?;
1876
1877            if let Some(active_call) = active_call {
1878                if close_intent != CloseIntent::Quit
1879                    && workspace_count == 1
1880                    && active_call.read_with(&cx, |call, _| call.room().is_some())?
1881                {
1882                    let answer = cx.update(|window, cx| {
1883                        window.prompt(
1884                            PromptLevel::Warning,
1885                            "Do you want to leave the current call?",
1886                            None,
1887                            &["Close window and hang up", "Cancel"],
1888                            cx,
1889                        )
1890                    })?;
1891
1892                    if answer.await.log_err() == Some(1) {
1893                        return anyhow::Ok(false);
1894                    } else {
1895                        active_call
1896                            .update(&mut cx, |call, cx| call.hang_up(cx))?
1897                            .await
1898                            .log_err();
1899                    }
1900                }
1901            }
1902
1903            let save_result = this
1904                .update_in(&mut cx, |this, window, cx| {
1905                    this.save_all_internal(SaveIntent::Close, window, cx)
1906                })?
1907                .await;
1908
1909            // If we're not quitting, but closing, we remove the workspace from
1910            // the current session.
1911            if close_intent != CloseIntent::Quit
1912                && !save_last_workspace
1913                && save_result.as_ref().map_or(false, |&res| res)
1914            {
1915                this.update_in(&mut cx, |this, window, cx| {
1916                    this.remove_from_session(window, cx)
1917                })?
1918                .await;
1919            }
1920
1921            save_result
1922        })
1923    }
1924
1925    fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context<Self>) {
1926        self.save_all_internal(
1927            action.save_intent.unwrap_or(SaveIntent::SaveAll),
1928            window,
1929            cx,
1930        )
1931        .detach_and_log_err(cx);
1932    }
1933
1934    fn send_keystrokes(
1935        &mut self,
1936        action: &SendKeystrokes,
1937        window: &mut Window,
1938        cx: &mut Context<Self>,
1939    ) {
1940        let mut state = self.dispatching_keystrokes.borrow_mut();
1941        if !state.0.insert(action.0.clone()) {
1942            cx.propagate();
1943            return;
1944        }
1945        let mut keystrokes: Vec<Keystroke> = action
1946            .0
1947            .split(' ')
1948            .flat_map(|k| Keystroke::parse(k).log_err())
1949            .collect();
1950        keystrokes.reverse();
1951
1952        state.1.append(&mut keystrokes);
1953        drop(state);
1954
1955        let keystrokes = self.dispatching_keystrokes.clone();
1956        window
1957            .spawn(cx, |mut cx| async move {
1958                // limit to 100 keystrokes to avoid infinite recursion.
1959                for _ in 0..100 {
1960                    let Some(keystroke) = keystrokes.borrow_mut().1.pop() else {
1961                        keystrokes.borrow_mut().0.clear();
1962                        return Ok(());
1963                    };
1964                    cx.update(|window, cx| {
1965                        let focused = window.focused(cx);
1966                        window.dispatch_keystroke(keystroke.clone(), cx);
1967                        if window.focused(cx) != focused {
1968                            // dispatch_keystroke may cause the focus to change.
1969                            // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
1970                            // And we need that to happen before the next keystroke to keep vim mode happy...
1971                            // (Note that the tests always do this implicitly, so you must manually test with something like:
1972                            //   "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
1973                            // )
1974                            window.draw(cx);
1975                        }
1976                    })?;
1977                }
1978
1979                *keystrokes.borrow_mut() = Default::default();
1980                Err(anyhow!("over 100 keystrokes passed to send_keystrokes"))
1981            })
1982            .detach_and_log_err(cx);
1983    }
1984
1985    fn save_all_internal(
1986        &mut self,
1987        mut save_intent: SaveIntent,
1988        window: &mut Window,
1989        cx: &mut Context<Self>,
1990    ) -> Task<Result<bool>> {
1991        if self.project.read(cx).is_disconnected(cx) {
1992            return Task::ready(Ok(true));
1993        }
1994        let dirty_items = self
1995            .panes
1996            .iter()
1997            .flat_map(|pane| {
1998                pane.read(cx).items().filter_map(|item| {
1999                    if item.is_dirty(cx) {
2000                        item.tab_description(0, cx);
2001                        Some((pane.downgrade(), item.boxed_clone()))
2002                    } else {
2003                        None
2004                    }
2005                })
2006            })
2007            .collect::<Vec<_>>();
2008
2009        let project = self.project.clone();
2010        cx.spawn_in(window, |workspace, mut cx| async move {
2011            let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
2012                let (serialize_tasks, remaining_dirty_items) =
2013                    workspace.update_in(&mut cx, |workspace, window, cx| {
2014                        let mut remaining_dirty_items = Vec::new();
2015                        let mut serialize_tasks = Vec::new();
2016                        for (pane, item) in dirty_items {
2017                            if let Some(task) = item
2018                                .to_serializable_item_handle(cx)
2019                                .and_then(|handle| handle.serialize(workspace, true, window, cx))
2020                            {
2021                                serialize_tasks.push(task);
2022                            } else {
2023                                remaining_dirty_items.push((pane, item));
2024                            }
2025                        }
2026                        (serialize_tasks, remaining_dirty_items)
2027                    })?;
2028
2029                futures::future::try_join_all(serialize_tasks).await?;
2030
2031                if remaining_dirty_items.len() > 1 {
2032                    let answer = workspace.update_in(&mut cx, |_, window, cx| {
2033                        let detail = Pane::file_names_for_prompt(
2034                            &mut remaining_dirty_items.iter().map(|(_, handle)| handle),
2035                            cx,
2036                        );
2037                        window.prompt(
2038                            PromptLevel::Warning,
2039                            &"Do you want to save all changes in the following files?",
2040                            Some(&detail),
2041                            &["Save all", "Discard all", "Cancel"],
2042                            cx,
2043                        )
2044                    })?;
2045                    match answer.await.log_err() {
2046                        Some(0) => save_intent = SaveIntent::SaveAll,
2047                        Some(1) => save_intent = SaveIntent::Skip,
2048                        Some(2) => return Ok(false),
2049                        _ => {}
2050                    }
2051                }
2052
2053                remaining_dirty_items
2054            } else {
2055                dirty_items
2056            };
2057
2058            for (pane, item) in dirty_items {
2059                let (singleton, project_entry_ids) =
2060                    cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
2061                if singleton || !project_entry_ids.is_empty() {
2062                    if !Pane::save_item(project.clone(), &pane, &*item, save_intent, &mut cx)
2063                        .await?
2064                    {
2065                        return Ok(false);
2066                    }
2067                }
2068            }
2069            Ok(true)
2070        })
2071    }
2072
2073    pub fn open_workspace_for_paths(
2074        &mut self,
2075        replace_current_window: bool,
2076        paths: Vec<PathBuf>,
2077        window: &mut Window,
2078        cx: &mut Context<Self>,
2079    ) -> Task<Result<()>> {
2080        let window_handle = window.window_handle().downcast::<Self>();
2081        let is_remote = self.project.read(cx).is_via_collab();
2082        let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
2083        let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
2084
2085        let window_to_replace = if replace_current_window {
2086            window_handle
2087        } else if is_remote || has_worktree || has_dirty_items {
2088            None
2089        } else {
2090            window_handle
2091        };
2092        let app_state = self.app_state.clone();
2093
2094        cx.spawn(|_, cx| async move {
2095            cx.update(|cx| {
2096                open_paths(
2097                    &paths,
2098                    app_state,
2099                    OpenOptions {
2100                        replace_window: window_to_replace,
2101                        ..Default::default()
2102                    },
2103                    cx,
2104                )
2105            })?
2106            .await?;
2107            Ok(())
2108        })
2109    }
2110
2111    #[allow(clippy::type_complexity)]
2112    pub fn open_paths(
2113        &mut self,
2114        mut abs_paths: Vec<PathBuf>,
2115        visible: OpenVisible,
2116        pane: Option<WeakEntity<Pane>>,
2117        window: &mut Window,
2118        cx: &mut Context<Self>,
2119    ) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
2120        log::info!("open paths {abs_paths:?}");
2121
2122        let fs = self.app_state.fs.clone();
2123
2124        // Sort the paths to ensure we add worktrees for parents before their children.
2125        abs_paths.sort_unstable();
2126        cx.spawn_in(window, move |this, mut cx| async move {
2127            let mut tasks = Vec::with_capacity(abs_paths.len());
2128
2129            for abs_path in &abs_paths {
2130                let visible = match visible {
2131                    OpenVisible::All => Some(true),
2132                    OpenVisible::None => Some(false),
2133                    OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
2134                        Some(Some(metadata)) => Some(!metadata.is_dir),
2135                        Some(None) => Some(true),
2136                        None => None,
2137                    },
2138                    OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
2139                        Some(Some(metadata)) => Some(metadata.is_dir),
2140                        Some(None) => Some(false),
2141                        None => None,
2142                    },
2143                };
2144                let project_path = match visible {
2145                    Some(visible) => match this
2146                        .update(&mut cx, |this, cx| {
2147                            Workspace::project_path_for_path(
2148                                this.project.clone(),
2149                                abs_path,
2150                                visible,
2151                                cx,
2152                            )
2153                        })
2154                        .log_err()
2155                    {
2156                        Some(project_path) => project_path.await.log_err(),
2157                        None => None,
2158                    },
2159                    None => None,
2160                };
2161
2162                let this = this.clone();
2163                let abs_path: Arc<Path> = SanitizedPath::from(abs_path.clone()).into();
2164                let fs = fs.clone();
2165                let pane = pane.clone();
2166                let task = cx.spawn(move |mut cx| async move {
2167                    let (worktree, project_path) = project_path?;
2168                    if fs.is_dir(&abs_path).await {
2169                        this.update(&mut cx, |workspace, cx| {
2170                            let worktree = worktree.read(cx);
2171                            let worktree_abs_path = worktree.abs_path();
2172                            let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() {
2173                                worktree.root_entry()
2174                            } else {
2175                                abs_path
2176                                    .strip_prefix(worktree_abs_path.as_ref())
2177                                    .ok()
2178                                    .and_then(|relative_path| {
2179                                        worktree.entry_for_path(relative_path)
2180                                    })
2181                            }
2182                            .map(|entry| entry.id);
2183                            if let Some(entry_id) = entry_id {
2184                                workspace.project.update(cx, |_, cx| {
2185                                    cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
2186                                })
2187                            }
2188                        })
2189                        .log_err()?;
2190                        None
2191                    } else {
2192                        Some(
2193                            this.update_in(&mut cx, |this, window, cx| {
2194                                this.open_path(project_path, pane, true, window, cx)
2195                            })
2196                            .log_err()?
2197                            .await,
2198                        )
2199                    }
2200                });
2201                tasks.push(task);
2202            }
2203
2204            futures::future::join_all(tasks).await
2205        })
2206    }
2207
2208    pub fn open_resolved_path(
2209        &mut self,
2210        path: ResolvedPath,
2211        window: &mut Window,
2212        cx: &mut Context<Self>,
2213    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2214        match path {
2215            ResolvedPath::ProjectPath { project_path, .. } => {
2216                self.open_path(project_path, None, true, window, cx)
2217            }
2218            ResolvedPath::AbsPath { path, .. } => self.open_abs_path(path, false, window, cx),
2219        }
2220    }
2221
2222    pub fn absolute_path_of_worktree(
2223        &self,
2224        worktree_id: WorktreeId,
2225        cx: &mut Context<Self>,
2226    ) -> Option<PathBuf> {
2227        self.project
2228            .read(cx)
2229            .worktree_for_id(worktree_id, cx)
2230            // TODO: use `abs_path` or `root_dir`
2231            .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf())
2232    }
2233
2234    fn add_folder_to_project(
2235        &mut self,
2236        _: &AddFolderToProject,
2237        window: &mut Window,
2238        cx: &mut Context<Self>,
2239    ) {
2240        let project = self.project.read(cx);
2241        if project.is_via_collab() {
2242            self.show_error(
2243                &anyhow!("You cannot add folders to someone else's project"),
2244                cx,
2245            );
2246            return;
2247        }
2248        let paths = self.prompt_for_open_path(
2249            PathPromptOptions {
2250                files: false,
2251                directories: true,
2252                multiple: true,
2253            },
2254            DirectoryLister::Project(self.project.clone()),
2255            window,
2256            cx,
2257        );
2258        cx.spawn_in(window, |this, mut cx| async move {
2259            if let Some(paths) = paths.await.log_err().flatten() {
2260                let results = this
2261                    .update_in(&mut cx, |this, window, cx| {
2262                        this.open_paths(paths, OpenVisible::All, None, window, cx)
2263                    })?
2264                    .await;
2265                for result in results.into_iter().flatten() {
2266                    result.log_err();
2267                }
2268            }
2269            anyhow::Ok(())
2270        })
2271        .detach_and_log_err(cx);
2272    }
2273
2274    pub fn project_path_for_path(
2275        project: Entity<Project>,
2276        abs_path: &Path,
2277        visible: bool,
2278        cx: &mut App,
2279    ) -> Task<Result<(Entity<Worktree>, ProjectPath)>> {
2280        let entry = project.update(cx, |project, cx| {
2281            project.find_or_create_worktree(abs_path, visible, cx)
2282        });
2283        cx.spawn(|mut cx| async move {
2284            let (worktree, path) = entry.await?;
2285            let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
2286            Ok((
2287                worktree,
2288                ProjectPath {
2289                    worktree_id,
2290                    path: path.into(),
2291                },
2292            ))
2293        })
2294    }
2295
2296    pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
2297        self.panes.iter().flat_map(|pane| pane.read(cx).items())
2298    }
2299
2300    pub fn item_of_type<T: Item>(&self, cx: &App) -> Option<Entity<T>> {
2301        self.items_of_type(cx).max_by_key(|item| item.item_id())
2302    }
2303
2304    pub fn items_of_type<'a, T: Item>(
2305        &'a self,
2306        cx: &'a App,
2307    ) -> impl 'a + Iterator<Item = Entity<T>> {
2308        self.panes
2309            .iter()
2310            .flat_map(|pane| pane.read(cx).items_of_type())
2311    }
2312
2313    pub fn active_item(&self, cx: &App) -> Option<Box<dyn ItemHandle>> {
2314        self.active_pane().read(cx).active_item()
2315    }
2316
2317    pub fn active_item_as<I: 'static>(&self, cx: &App) -> Option<Entity<I>> {
2318        let item = self.active_item(cx)?;
2319        item.to_any().downcast::<I>().ok()
2320    }
2321
2322    fn active_project_path(&self, cx: &App) -> Option<ProjectPath> {
2323        self.active_item(cx).and_then(|item| item.project_path(cx))
2324    }
2325
2326    pub fn save_active_item(
2327        &mut self,
2328        save_intent: SaveIntent,
2329        window: &mut Window,
2330        cx: &mut App,
2331    ) -> Task<Result<()>> {
2332        let project = self.project.clone();
2333        let pane = self.active_pane();
2334        let item = pane.read(cx).active_item();
2335        let pane = pane.downgrade();
2336
2337        window.spawn(cx, |mut cx| async move {
2338            if let Some(item) = item {
2339                Pane::save_item(project, &pane, item.as_ref(), save_intent, &mut cx)
2340                    .await
2341                    .map(|_| ())
2342            } else {
2343                Ok(())
2344            }
2345        })
2346    }
2347
2348    pub fn close_inactive_items_and_panes(
2349        &mut self,
2350        action: &CloseInactiveTabsAndPanes,
2351        window: &mut Window,
2352        cx: &mut Context<Self>,
2353    ) {
2354        if let Some(task) = self.close_all_internal(
2355            true,
2356            action.save_intent.unwrap_or(SaveIntent::Close),
2357            window,
2358            cx,
2359        ) {
2360            task.detach_and_log_err(cx)
2361        }
2362    }
2363
2364    pub fn close_all_items_and_panes(
2365        &mut self,
2366        action: &CloseAllItemsAndPanes,
2367        window: &mut Window,
2368        cx: &mut Context<Self>,
2369    ) {
2370        if let Some(task) = self.close_all_internal(
2371            false,
2372            action.save_intent.unwrap_or(SaveIntent::Close),
2373            window,
2374            cx,
2375        ) {
2376            task.detach_and_log_err(cx)
2377        }
2378    }
2379
2380    fn close_all_internal(
2381        &mut self,
2382        retain_active_pane: bool,
2383        save_intent: SaveIntent,
2384        window: &mut Window,
2385        cx: &mut Context<Self>,
2386    ) -> Option<Task<Result<()>>> {
2387        let current_pane = self.active_pane();
2388
2389        let mut tasks = Vec::new();
2390
2391        if retain_active_pane {
2392            if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
2393                pane.close_inactive_items(
2394                    &CloseInactiveItems {
2395                        save_intent: None,
2396                        close_pinned: false,
2397                    },
2398                    window,
2399                    cx,
2400                )
2401            }) {
2402                tasks.push(current_pane_close);
2403            };
2404        }
2405
2406        for pane in self.panes() {
2407            if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
2408                continue;
2409            }
2410
2411            if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
2412                pane.close_all_items(
2413                    &CloseAllItems {
2414                        save_intent: Some(save_intent),
2415                        close_pinned: false,
2416                    },
2417                    window,
2418                    cx,
2419                )
2420            }) {
2421                tasks.push(close_pane_items)
2422            }
2423        }
2424
2425        if tasks.is_empty() {
2426            None
2427        } else {
2428            Some(cx.spawn_in(window, |_, _| async move {
2429                for task in tasks {
2430                    task.await?
2431                }
2432                Ok(())
2433            }))
2434        }
2435    }
2436
2437    pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context<Self>) -> bool {
2438        self.dock_at_position(position).read(cx).is_open()
2439    }
2440
2441    pub fn toggle_dock(
2442        &mut self,
2443        dock_side: DockPosition,
2444        window: &mut Window,
2445        cx: &mut Context<Self>,
2446    ) {
2447        let dock = self.dock_at_position(dock_side);
2448        let mut focus_center = false;
2449        let mut reveal_dock = false;
2450        dock.update(cx, |dock, cx| {
2451            let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
2452            let was_visible = dock.is_open() && !other_is_zoomed;
2453            dock.set_open(!was_visible, window, cx);
2454
2455            if dock.active_panel().is_none() && dock.panels_len() > 0 {
2456                dock.activate_panel(0, window, cx);
2457            }
2458
2459            if let Some(active_panel) = dock.active_panel() {
2460                if was_visible {
2461                    if active_panel
2462                        .panel_focus_handle(cx)
2463                        .contains_focused(window, cx)
2464                    {
2465                        focus_center = true;
2466                    }
2467                } else {
2468                    let focus_handle = &active_panel.panel_focus_handle(cx);
2469                    window.focus(focus_handle);
2470                    reveal_dock = true;
2471                }
2472            }
2473        });
2474
2475        if reveal_dock {
2476            self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
2477        }
2478
2479        if focus_center {
2480            self.active_pane
2481                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
2482        }
2483
2484        cx.notify();
2485        self.serialize_workspace(window, cx);
2486    }
2487
2488    pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2489        for dock in self.all_docks() {
2490            dock.update(cx, |dock, cx| {
2491                dock.set_open(false, window, cx);
2492            });
2493        }
2494
2495        cx.focus_self(window);
2496        cx.notify();
2497        self.serialize_workspace(window, cx);
2498    }
2499
2500    /// Transfer focus to the panel of the given type.
2501    pub fn focus_panel<T: Panel>(
2502        &mut self,
2503        window: &mut Window,
2504        cx: &mut Context<Self>,
2505    ) -> Option<Entity<T>> {
2506        let panel = self.focus_or_unfocus_panel::<T>(window, cx, |_, _, _| true)?;
2507        panel.to_any().downcast().ok()
2508    }
2509
2510    /// Focus the panel of the given type if it isn't already focused. If it is
2511    /// already focused, then transfer focus back to the workspace center.
2512    pub fn toggle_panel_focus<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2513        self.focus_or_unfocus_panel::<T>(window, cx, |panel, window, cx| {
2514            !panel.panel_focus_handle(cx).contains_focused(window, cx)
2515        });
2516    }
2517
2518    pub fn activate_panel_for_proto_id(
2519        &mut self,
2520        panel_id: PanelId,
2521        window: &mut Window,
2522        cx: &mut Context<Self>,
2523    ) -> Option<Arc<dyn PanelHandle>> {
2524        let mut panel = None;
2525        for dock in self.all_docks() {
2526            if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
2527                panel = dock.update(cx, |dock, cx| {
2528                    dock.activate_panel(panel_index, window, cx);
2529                    dock.set_open(true, window, cx);
2530                    dock.active_panel().cloned()
2531                });
2532                break;
2533            }
2534        }
2535
2536        if panel.is_some() {
2537            cx.notify();
2538            self.serialize_workspace(window, cx);
2539        }
2540
2541        panel
2542    }
2543
2544    /// Focus or unfocus the given panel type, depending on the given callback.
2545    fn focus_or_unfocus_panel<T: Panel>(
2546        &mut self,
2547        window: &mut Window,
2548        cx: &mut Context<Self>,
2549        should_focus: impl Fn(&dyn PanelHandle, &mut Window, &mut Context<Dock>) -> bool,
2550    ) -> Option<Arc<dyn PanelHandle>> {
2551        let mut result_panel = None;
2552        let mut serialize = false;
2553        for dock in self.all_docks() {
2554            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2555                let mut focus_center = false;
2556                let panel = dock.update(cx, |dock, cx| {
2557                    dock.activate_panel(panel_index, window, cx);
2558
2559                    let panel = dock.active_panel().cloned();
2560                    if let Some(panel) = panel.as_ref() {
2561                        if should_focus(&**panel, window, cx) {
2562                            dock.set_open(true, window, cx);
2563                            panel.panel_focus_handle(cx).focus(window);
2564                        } else {
2565                            focus_center = true;
2566                        }
2567                    }
2568                    panel
2569                });
2570
2571                if focus_center {
2572                    self.active_pane
2573                        .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
2574                }
2575
2576                result_panel = panel;
2577                serialize = true;
2578                break;
2579            }
2580        }
2581
2582        if serialize {
2583            self.serialize_workspace(window, cx);
2584        }
2585
2586        cx.notify();
2587        result_panel
2588    }
2589
2590    /// Open the panel of the given type
2591    pub fn open_panel<T: Panel>(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2592        for dock in self.all_docks() {
2593            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2594                dock.update(cx, |dock, cx| {
2595                    dock.activate_panel(panel_index, window, cx);
2596                    dock.set_open(true, window, cx);
2597                });
2598            }
2599        }
2600    }
2601
2602    pub fn panel<T: Panel>(&self, cx: &App) -> Option<Entity<T>> {
2603        self.all_docks()
2604            .iter()
2605            .find_map(|dock| dock.read(cx).panel::<T>())
2606    }
2607
2608    fn dismiss_zoomed_items_to_reveal(
2609        &mut self,
2610        dock_to_reveal: Option<DockPosition>,
2611        window: &mut Window,
2612        cx: &mut Context<Self>,
2613    ) {
2614        // If a center pane is zoomed, unzoom it.
2615        for pane in &self.panes {
2616            if pane != &self.active_pane || dock_to_reveal.is_some() {
2617                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2618            }
2619        }
2620
2621        // If another dock is zoomed, hide it.
2622        let mut focus_center = false;
2623        for dock in self.all_docks() {
2624            dock.update(cx, |dock, cx| {
2625                if Some(dock.position()) != dock_to_reveal {
2626                    if let Some(panel) = dock.active_panel() {
2627                        if panel.is_zoomed(window, cx) {
2628                            focus_center |=
2629                                panel.panel_focus_handle(cx).contains_focused(window, cx);
2630                            dock.set_open(false, window, cx);
2631                        }
2632                    }
2633                }
2634            });
2635        }
2636
2637        if focus_center {
2638            self.active_pane
2639                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
2640        }
2641
2642        if self.zoomed_position != dock_to_reveal {
2643            self.zoomed = None;
2644            self.zoomed_position = None;
2645            cx.emit(Event::ZoomChanged);
2646        }
2647
2648        cx.notify();
2649    }
2650
2651    fn add_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
2652        let pane = cx.new(|cx| {
2653            let mut pane = Pane::new(
2654                self.weak_handle(),
2655                self.project.clone(),
2656                self.pane_history_timestamp.clone(),
2657                None,
2658                NewFile.boxed_clone(),
2659                window,
2660                cx,
2661            );
2662            pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
2663            pane
2664        });
2665        cx.subscribe_in(&pane, window, Self::handle_pane_event)
2666            .detach();
2667        self.panes.push(pane.clone());
2668
2669        window.focus(&pane.focus_handle(cx));
2670
2671        cx.emit(Event::PaneAdded(pane.clone()));
2672        pane
2673    }
2674
2675    pub fn add_item_to_center(
2676        &mut self,
2677        item: Box<dyn ItemHandle>,
2678        window: &mut Window,
2679        cx: &mut Context<Self>,
2680    ) -> bool {
2681        if let Some(center_pane) = self.last_active_center_pane.clone() {
2682            if let Some(center_pane) = center_pane.upgrade() {
2683                center_pane.update(cx, |pane, cx| {
2684                    pane.add_item(item, true, true, None, window, cx)
2685                });
2686                true
2687            } else {
2688                false
2689            }
2690        } else {
2691            false
2692        }
2693    }
2694
2695    pub fn add_item_to_active_pane(
2696        &mut self,
2697        item: Box<dyn ItemHandle>,
2698        destination_index: Option<usize>,
2699        focus_item: bool,
2700        window: &mut Window,
2701        cx: &mut App,
2702    ) {
2703        self.add_item(
2704            self.active_pane.clone(),
2705            item,
2706            destination_index,
2707            false,
2708            focus_item,
2709            window,
2710            cx,
2711        )
2712    }
2713
2714    #[allow(clippy::too_many_arguments)]
2715    pub fn add_item(
2716        &mut self,
2717        pane: Entity<Pane>,
2718        item: Box<dyn ItemHandle>,
2719        destination_index: Option<usize>,
2720        activate_pane: bool,
2721        focus_item: bool,
2722        window: &mut Window,
2723        cx: &mut App,
2724    ) {
2725        if let Some(text) = item.telemetry_event_text(cx) {
2726            telemetry::event!(text);
2727        }
2728
2729        pane.update(cx, |pane, cx| {
2730            pane.add_item(
2731                item,
2732                activate_pane,
2733                focus_item,
2734                destination_index,
2735                window,
2736                cx,
2737            )
2738        });
2739    }
2740
2741    pub fn split_item(
2742        &mut self,
2743        split_direction: SplitDirection,
2744        item: Box<dyn ItemHandle>,
2745        window: &mut Window,
2746        cx: &mut Context<Self>,
2747    ) {
2748        let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx);
2749        self.add_item(new_pane, item, None, true, true, window, cx);
2750    }
2751
2752    pub fn open_abs_path(
2753        &mut self,
2754        abs_path: PathBuf,
2755        visible: bool,
2756        window: &mut Window,
2757        cx: &mut Context<Self>,
2758    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2759        cx.spawn_in(window, |workspace, mut cx| async move {
2760            let open_paths_task_result = workspace
2761                .update_in(&mut cx, |workspace, window, cx| {
2762                    workspace.open_paths(
2763                        vec![abs_path.clone()],
2764                        if visible {
2765                            OpenVisible::All
2766                        } else {
2767                            OpenVisible::None
2768                        },
2769                        None,
2770                        window,
2771                        cx,
2772                    )
2773                })
2774                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2775                .await;
2776            anyhow::ensure!(
2777                open_paths_task_result.len() == 1,
2778                "open abs path {abs_path:?} task returned incorrect number of results"
2779            );
2780            match open_paths_task_result
2781                .into_iter()
2782                .next()
2783                .expect("ensured single task result")
2784            {
2785                Some(open_result) => {
2786                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2787                }
2788                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2789            }
2790        })
2791    }
2792
2793    pub fn split_abs_path(
2794        &mut self,
2795        abs_path: PathBuf,
2796        visible: bool,
2797        window: &mut Window,
2798        cx: &mut Context<Self>,
2799    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2800        let project_path_task =
2801            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2802        cx.spawn_in(window, |this, mut cx| async move {
2803            let (_, path) = project_path_task.await?;
2804            this.update_in(&mut cx, |this, window, cx| {
2805                this.split_path(path, window, cx)
2806            })?
2807            .await
2808        })
2809    }
2810
2811    pub fn open_path(
2812        &mut self,
2813        path: impl Into<ProjectPath>,
2814        pane: Option<WeakEntity<Pane>>,
2815        focus_item: bool,
2816        window: &mut Window,
2817        cx: &mut App,
2818    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2819        self.open_path_preview(path, pane, focus_item, false, true, window, cx)
2820    }
2821
2822    #[allow(clippy::too_many_arguments)]
2823    pub fn open_path_preview(
2824        &mut self,
2825        path: impl Into<ProjectPath>,
2826        pane: Option<WeakEntity<Pane>>,
2827        focus_item: bool,
2828        allow_preview: bool,
2829        activate: bool,
2830        window: &mut Window,
2831        cx: &mut App,
2832    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2833        let pane = pane.unwrap_or_else(|| {
2834            self.last_active_center_pane.clone().unwrap_or_else(|| {
2835                self.panes
2836                    .first()
2837                    .expect("There must be an active pane")
2838                    .downgrade()
2839            })
2840        });
2841
2842        let task = self.load_path(path.into(), window, cx);
2843        window.spawn(cx, move |mut cx| async move {
2844            let (project_entry_id, build_item) = task.await?;
2845            let result = pane.update_in(&mut cx, |pane, window, cx| {
2846                let result = pane.open_item(
2847                    project_entry_id,
2848                    focus_item,
2849                    allow_preview,
2850                    activate,
2851                    None,
2852                    window,
2853                    cx,
2854                    build_item,
2855                );
2856
2857                result
2858            });
2859            result
2860        })
2861    }
2862
2863    pub fn split_path(
2864        &mut self,
2865        path: impl Into<ProjectPath>,
2866        window: &mut Window,
2867        cx: &mut Context<Self>,
2868    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2869        self.split_path_preview(path, false, None, window, cx)
2870    }
2871
2872    pub fn split_path_preview(
2873        &mut self,
2874        path: impl Into<ProjectPath>,
2875        allow_preview: bool,
2876        split_direction: Option<SplitDirection>,
2877        window: &mut Window,
2878        cx: &mut Context<Self>,
2879    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2880        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2881            self.panes
2882                .first()
2883                .expect("There must be an active pane")
2884                .downgrade()
2885        });
2886
2887        if let Member::Pane(center_pane) = &self.center.root {
2888            if center_pane.read(cx).items_len() == 0 {
2889                return self.open_path(path, Some(pane), true, window, cx);
2890            }
2891        }
2892
2893        let task = self.load_path(path.into(), window, cx);
2894        cx.spawn_in(window, |this, mut cx| async move {
2895            let (project_entry_id, build_item) = task.await?;
2896            this.update_in(&mut cx, move |this, window, cx| -> Option<_> {
2897                let pane = pane.upgrade()?;
2898                let new_pane = this.split_pane(
2899                    pane,
2900                    split_direction.unwrap_or(SplitDirection::Right),
2901                    window,
2902                    cx,
2903                );
2904                new_pane.update(cx, |new_pane, cx| {
2905                    Some(new_pane.open_item(
2906                        project_entry_id,
2907                        true,
2908                        allow_preview,
2909                        true,
2910                        None,
2911                        window,
2912                        cx,
2913                        build_item,
2914                    ))
2915                })
2916            })
2917            .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2918        })
2919    }
2920
2921    fn load_path(
2922        &mut self,
2923        path: ProjectPath,
2924        window: &mut Window,
2925        cx: &mut App,
2926    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2927        let project = self.project().clone();
2928        let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2929        let Some(open_project_item) = project_item_builders
2930            .iter()
2931            .rev()
2932            .find_map(|open_project_item| open_project_item(&project, &path, window, cx))
2933        else {
2934            return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2935        };
2936        open_project_item
2937    }
2938
2939    pub fn find_project_item<T>(
2940        &self,
2941        pane: &Entity<Pane>,
2942        project_item: &Entity<T::Item>,
2943        cx: &App,
2944    ) -> Option<Entity<T>>
2945    where
2946        T: ProjectItem,
2947    {
2948        use project::ProjectItem as _;
2949        let project_item = project_item.read(cx);
2950        let entry_id = project_item.entry_id(cx);
2951        let project_path = project_item.project_path(cx);
2952
2953        let mut item = None;
2954        if let Some(entry_id) = entry_id {
2955            item = pane.read(cx).item_for_entry(entry_id, cx);
2956        }
2957        if item.is_none() {
2958            if let Some(project_path) = project_path {
2959                item = pane.read(cx).item_for_path(project_path, cx);
2960            }
2961        }
2962
2963        item.and_then(|item| item.downcast::<T>())
2964    }
2965
2966    pub fn is_project_item_open<T>(
2967        &self,
2968        pane: &Entity<Pane>,
2969        project_item: &Entity<T::Item>,
2970        cx: &App,
2971    ) -> bool
2972    where
2973        T: ProjectItem,
2974    {
2975        self.find_project_item::<T>(pane, project_item, cx)
2976            .is_some()
2977    }
2978
2979    pub fn open_project_item<T>(
2980        &mut self,
2981        pane: Entity<Pane>,
2982        project_item: Entity<T::Item>,
2983        activate_pane: bool,
2984        focus_item: bool,
2985        window: &mut Window,
2986        cx: &mut Context<Self>,
2987    ) -> Entity<T>
2988    where
2989        T: ProjectItem,
2990    {
2991        if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
2992            self.activate_item(&item, activate_pane, focus_item, window, cx);
2993            return item;
2994        }
2995
2996        let item =
2997            cx.new(|cx| T::for_project_item(self.project().clone(), project_item, window, cx));
2998        let item_id = item.item_id();
2999        let mut destination_index = None;
3000        pane.update(cx, |pane, cx| {
3001            if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
3002                if let Some(preview_item_id) = pane.preview_item_id() {
3003                    if preview_item_id != item_id {
3004                        destination_index = pane.close_current_preview_item(window, cx);
3005                    }
3006                }
3007            }
3008            pane.set_preview_item_id(Some(item.item_id()), cx)
3009        });
3010
3011        self.add_item(
3012            pane,
3013            Box::new(item.clone()),
3014            destination_index,
3015            activate_pane,
3016            focus_item,
3017            window,
3018            cx,
3019        );
3020        item
3021    }
3022
3023    pub fn open_shared_screen(
3024        &mut self,
3025        peer_id: PeerId,
3026        window: &mut Window,
3027        cx: &mut Context<Self>,
3028    ) {
3029        if let Some(shared_screen) =
3030            self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx)
3031        {
3032            self.active_pane.update(cx, |pane, cx| {
3033                pane.add_item(Box::new(shared_screen), false, true, None, window, cx)
3034            });
3035        }
3036    }
3037
3038    pub fn activate_item(
3039        &mut self,
3040        item: &dyn ItemHandle,
3041        activate_pane: bool,
3042        focus_item: bool,
3043        window: &mut Window,
3044        cx: &mut App,
3045    ) -> bool {
3046        let result = self.panes.iter().find_map(|pane| {
3047            pane.read(cx)
3048                .index_for_item(item)
3049                .map(|ix| (pane.clone(), ix))
3050        });
3051        if let Some((pane, ix)) = result {
3052            pane.update(cx, |pane, cx| {
3053                pane.activate_item(ix, activate_pane, focus_item, window, cx)
3054            });
3055            true
3056        } else {
3057            false
3058        }
3059    }
3060
3061    fn activate_pane_at_index(
3062        &mut self,
3063        action: &ActivatePane,
3064        window: &mut Window,
3065        cx: &mut Context<Self>,
3066    ) {
3067        let panes = self.center.panes();
3068        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
3069            window.focus(&pane.focus_handle(cx));
3070        } else {
3071            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx);
3072        }
3073    }
3074
3075    fn move_item_to_pane_at_index(
3076        &mut self,
3077        action: &MoveItemToPane,
3078        window: &mut Window,
3079        cx: &mut Context<Self>,
3080    ) {
3081        let Some(&target_pane) = self.center.panes().get(action.destination) else {
3082            return;
3083        };
3084        move_active_item(
3085            &self.active_pane,
3086            target_pane,
3087            action.focus,
3088            true,
3089            window,
3090            cx,
3091        );
3092    }
3093
3094    pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
3095        let panes = self.center.panes();
3096        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
3097            let next_ix = (ix + 1) % panes.len();
3098            let next_pane = panes[next_ix].clone();
3099            window.focus(&next_pane.focus_handle(cx));
3100        }
3101    }
3102
3103    pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) {
3104        let panes = self.center.panes();
3105        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
3106            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
3107            let prev_pane = panes[prev_ix].clone();
3108            window.focus(&prev_pane.focus_handle(cx));
3109        }
3110    }
3111
3112    pub fn activate_pane_in_direction(
3113        &mut self,
3114        direction: SplitDirection,
3115        window: &mut Window,
3116        cx: &mut App,
3117    ) {
3118        use ActivateInDirectionTarget as Target;
3119        enum Origin {
3120            LeftDock,
3121            RightDock,
3122            BottomDock,
3123            Center,
3124        }
3125
3126        let origin: Origin = [
3127            (&self.left_dock, Origin::LeftDock),
3128            (&self.right_dock, Origin::RightDock),
3129            (&self.bottom_dock, Origin::BottomDock),
3130        ]
3131        .into_iter()
3132        .find_map(|(dock, origin)| {
3133            if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() {
3134                Some(origin)
3135            } else {
3136                None
3137            }
3138        })
3139        .unwrap_or(Origin::Center);
3140
3141        let get_last_active_pane = || {
3142            let pane = self
3143                .last_active_center_pane
3144                .clone()
3145                .unwrap_or_else(|| {
3146                    self.panes
3147                        .first()
3148                        .expect("There must be an active pane")
3149                        .downgrade()
3150                })
3151                .upgrade()?;
3152            (pane.read(cx).items_len() != 0).then_some(pane)
3153        };
3154
3155        let try_dock =
3156            |dock: &Entity<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
3157
3158        let target = match (origin, direction) {
3159            // We're in the center, so we first try to go to a different pane,
3160            // otherwise try to go to a dock.
3161            (Origin::Center, direction) => {
3162                if let Some(pane) = self.find_pane_in_direction(direction, cx) {
3163                    Some(Target::Pane(pane))
3164                } else {
3165                    match direction {
3166                        SplitDirection::Up => None,
3167                        SplitDirection::Down => try_dock(&self.bottom_dock),
3168                        SplitDirection::Left => try_dock(&self.left_dock),
3169                        SplitDirection::Right => try_dock(&self.right_dock),
3170                    }
3171                }
3172            }
3173
3174            (Origin::LeftDock, SplitDirection::Right) => {
3175                if let Some(last_active_pane) = get_last_active_pane() {
3176                    Some(Target::Pane(last_active_pane))
3177                } else {
3178                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
3179                }
3180            }
3181
3182            (Origin::LeftDock, SplitDirection::Down)
3183            | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
3184
3185            (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
3186            (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
3187            (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
3188
3189            (Origin::RightDock, SplitDirection::Left) => {
3190                if let Some(last_active_pane) = get_last_active_pane() {
3191                    Some(Target::Pane(last_active_pane))
3192                } else {
3193                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
3194                }
3195            }
3196
3197            _ => None,
3198        };
3199
3200        match target {
3201            Some(ActivateInDirectionTarget::Pane(pane)) => {
3202                window.focus(&pane.focus_handle(cx));
3203            }
3204            Some(ActivateInDirectionTarget::Dock(dock)) => {
3205                // Defer this to avoid a panic when the dock's active panel is already on the stack.
3206                window.defer(cx, move |window, cx| {
3207                    let dock = dock.read(cx);
3208                    if let Some(panel) = dock.active_panel() {
3209                        panel.panel_focus_handle(cx).focus(window);
3210                    } else {
3211                        log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
3212                    }
3213                })
3214            }
3215            None => {}
3216        }
3217    }
3218
3219    pub fn move_item_to_pane_in_direction(
3220        &mut self,
3221        action: &MoveItemToPaneInDirection,
3222        window: &mut Window,
3223        cx: &mut App,
3224    ) {
3225        if let Some(destination) = self.find_pane_in_direction(action.direction, cx) {
3226            move_active_item(
3227                &self.active_pane,
3228                &destination,
3229                action.focus,
3230                true,
3231                window,
3232                cx,
3233            );
3234        }
3235    }
3236
3237    pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
3238        self.center.bounding_box_for_pane(pane)
3239    }
3240
3241    pub fn find_pane_in_direction(
3242        &mut self,
3243        direction: SplitDirection,
3244        cx: &App,
3245    ) -> Option<Entity<Pane>> {
3246        self.center
3247            .find_pane_in_direction(&self.active_pane, direction, cx)
3248            .cloned()
3249    }
3250
3251    pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
3252        if let Some(to) = self.find_pane_in_direction(direction, cx) {
3253            self.center.swap(&self.active_pane, &to);
3254            cx.notify();
3255        }
3256    }
3257
3258    pub fn resize_pane(
3259        &mut self,
3260        axis: gpui::Axis,
3261        amount: Pixels,
3262        window: &mut Window,
3263        cx: &mut Context<Self>,
3264    ) {
3265        let docks = self.all_docks();
3266        let active_dock = docks
3267            .into_iter()
3268            .find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
3269
3270        if let Some(dock) = active_dock {
3271            let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else {
3272                return;
3273            };
3274            match dock.read(cx).position() {
3275                DockPosition::Left => resize_left_dock(panel_size + amount, self, window, cx),
3276                DockPosition::Bottom => resize_bottom_dock(panel_size + amount, self, window, cx),
3277                DockPosition::Right => resize_right_dock(panel_size + amount, self, window, cx),
3278            }
3279        } else {
3280            self.center
3281                .resize(&self.active_pane, axis, amount, &self.bounds);
3282        }
3283        cx.notify();
3284    }
3285
3286    pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
3287        self.center.reset_pane_sizes();
3288        cx.notify();
3289    }
3290
3291    fn handle_pane_focused(
3292        &mut self,
3293        pane: Entity<Pane>,
3294        window: &mut Window,
3295        cx: &mut Context<Self>,
3296    ) {
3297        // This is explicitly hoisted out of the following check for pane identity as
3298        // terminal panel panes are not registered as a center panes.
3299        self.status_bar.update(cx, |status_bar, cx| {
3300            status_bar.set_active_pane(&pane, window, cx);
3301        });
3302        if self.active_pane != pane {
3303            self.set_active_pane(&pane, window, cx);
3304        }
3305
3306        if self.last_active_center_pane.is_none() {
3307            self.last_active_center_pane = Some(pane.downgrade());
3308        }
3309
3310        self.dismiss_zoomed_items_to_reveal(None, window, cx);
3311        if pane.read(cx).is_zoomed() {
3312            self.zoomed = Some(pane.downgrade().into());
3313        } else {
3314            self.zoomed = None;
3315        }
3316        self.zoomed_position = None;
3317        cx.emit(Event::ZoomChanged);
3318        self.update_active_view_for_followers(window, cx);
3319        pane.update(cx, |pane, _| {
3320            pane.track_alternate_file_items();
3321        });
3322
3323        cx.notify();
3324    }
3325
3326    fn set_active_pane(
3327        &mut self,
3328        pane: &Entity<Pane>,
3329        window: &mut Window,
3330        cx: &mut Context<Self>,
3331    ) {
3332        self.active_pane = pane.clone();
3333        self.active_item_path_changed(window, cx);
3334        self.last_active_center_pane = Some(pane.downgrade());
3335    }
3336
3337    fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3338        self.update_active_view_for_followers(window, cx);
3339    }
3340
3341    fn handle_pane_event(
3342        &mut self,
3343        pane: &Entity<Pane>,
3344        event: &pane::Event,
3345        window: &mut Window,
3346        cx: &mut Context<Self>,
3347    ) {
3348        let mut serialize_workspace = true;
3349        match event {
3350            pane::Event::AddItem { item } => {
3351                item.added_to_pane(self, pane.clone(), window, cx);
3352                cx.emit(Event::ItemAdded {
3353                    item: item.boxed_clone(),
3354                });
3355            }
3356            pane::Event::Split(direction) => {
3357                self.split_and_clone(pane.clone(), *direction, window, cx);
3358            }
3359            pane::Event::JoinIntoNext => {
3360                self.join_pane_into_next(pane.clone(), window, cx);
3361            }
3362            pane::Event::JoinAll => {
3363                self.join_all_panes(window, cx);
3364            }
3365            pane::Event::Remove { focus_on_pane } => {
3366                self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
3367            }
3368            pane::Event::ActivateItem {
3369                local,
3370                focus_changed,
3371            } => {
3372                cx.on_next_frame(window, |_, window, _| {
3373                    window.invalidate_character_coordinates();
3374                });
3375
3376                pane.update(cx, |pane, _| {
3377                    pane.track_alternate_file_items();
3378                });
3379                if *local {
3380                    self.unfollow_in_pane(&pane, window, cx);
3381                }
3382                if pane == self.active_pane() {
3383                    self.active_item_path_changed(window, cx);
3384                    self.update_active_view_for_followers(window, cx);
3385                }
3386                serialize_workspace = *focus_changed || pane != self.active_pane();
3387            }
3388            pane::Event::UserSavedItem { item, save_intent } => {
3389                cx.emit(Event::UserSavedItem {
3390                    pane: pane.downgrade(),
3391                    item: item.boxed_clone(),
3392                    save_intent: *save_intent,
3393                });
3394                serialize_workspace = false;
3395            }
3396            pane::Event::ChangeItemTitle => {
3397                if *pane == self.active_pane {
3398                    self.active_item_path_changed(window, cx);
3399                }
3400                serialize_workspace = false;
3401            }
3402            pane::Event::RemoveItem { .. } => {}
3403            pane::Event::RemovedItem { item_id } => {
3404                cx.emit(Event::ActiveItemChanged);
3405                if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3406                    if entry.get().entity_id() == pane.entity_id() {
3407                        entry.remove();
3408                    }
3409                }
3410            }
3411            pane::Event::Focus => {
3412                cx.on_next_frame(window, |_, window, _| {
3413                    window.invalidate_character_coordinates();
3414                });
3415                self.handle_pane_focused(pane.clone(), window, cx);
3416            }
3417            pane::Event::ZoomIn => {
3418                if *pane == self.active_pane {
3419                    pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3420                    if pane.read(cx).has_focus(window, cx) {
3421                        self.zoomed = Some(pane.downgrade().into());
3422                        self.zoomed_position = None;
3423                        cx.emit(Event::ZoomChanged);
3424                    }
3425                    cx.notify();
3426                }
3427            }
3428            pane::Event::ZoomOut => {
3429                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3430                if self.zoomed_position.is_none() {
3431                    self.zoomed = None;
3432                    cx.emit(Event::ZoomChanged);
3433                }
3434                cx.notify();
3435            }
3436        }
3437
3438        if serialize_workspace {
3439            self.serialize_workspace(window, cx);
3440        }
3441    }
3442
3443    pub fn unfollow_in_pane(
3444        &mut self,
3445        pane: &Entity<Pane>,
3446        window: &mut Window,
3447        cx: &mut Context<Workspace>,
3448    ) -> Option<PeerId> {
3449        let leader_id = self.leader_for_pane(pane)?;
3450        self.unfollow(leader_id, window, cx);
3451        Some(leader_id)
3452    }
3453
3454    pub fn split_pane(
3455        &mut self,
3456        pane_to_split: Entity<Pane>,
3457        split_direction: SplitDirection,
3458        window: &mut Window,
3459        cx: &mut Context<Self>,
3460    ) -> Entity<Pane> {
3461        let new_pane = self.add_pane(window, cx);
3462        self.center
3463            .split(&pane_to_split, &new_pane, split_direction)
3464            .unwrap();
3465        cx.notify();
3466        new_pane
3467    }
3468
3469    pub fn split_and_clone(
3470        &mut self,
3471        pane: Entity<Pane>,
3472        direction: SplitDirection,
3473        window: &mut Window,
3474        cx: &mut Context<Self>,
3475    ) -> Option<Entity<Pane>> {
3476        let item = pane.read(cx).active_item()?;
3477        let maybe_pane_handle =
3478            if let Some(clone) = item.clone_on_split(self.database_id(), window, cx) {
3479                let new_pane = self.add_pane(window, cx);
3480                new_pane.update(cx, |pane, cx| {
3481                    pane.add_item(clone, true, true, None, window, cx)
3482                });
3483                self.center.split(&pane, &new_pane, direction).unwrap();
3484                Some(new_pane)
3485            } else {
3486                None
3487            };
3488        cx.notify();
3489        maybe_pane_handle
3490    }
3491
3492    pub fn split_pane_with_item(
3493        &mut self,
3494        pane_to_split: WeakEntity<Pane>,
3495        split_direction: SplitDirection,
3496        from: WeakEntity<Pane>,
3497        item_id_to_move: EntityId,
3498        window: &mut Window,
3499        cx: &mut Context<Self>,
3500    ) {
3501        let Some(pane_to_split) = pane_to_split.upgrade() else {
3502            return;
3503        };
3504        let Some(from) = from.upgrade() else {
3505            return;
3506        };
3507
3508        let new_pane = self.add_pane(window, cx);
3509        move_item(&from, &new_pane, item_id_to_move, 0, window, cx);
3510        self.center
3511            .split(&pane_to_split, &new_pane, split_direction)
3512            .unwrap();
3513        cx.notify();
3514    }
3515
3516    pub fn split_pane_with_project_entry(
3517        &mut self,
3518        pane_to_split: WeakEntity<Pane>,
3519        split_direction: SplitDirection,
3520        project_entry: ProjectEntryId,
3521        window: &mut Window,
3522        cx: &mut Context<Self>,
3523    ) -> Option<Task<Result<()>>> {
3524        let pane_to_split = pane_to_split.upgrade()?;
3525        let new_pane = self.add_pane(window, cx);
3526        self.center
3527            .split(&pane_to_split, &new_pane, split_direction)
3528            .unwrap();
3529
3530        let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3531        let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx);
3532        Some(cx.foreground_executor().spawn(async move {
3533            task.await?;
3534            Ok(())
3535        }))
3536    }
3537
3538    pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3539        let active_item = self.active_pane.read(cx).active_item();
3540        for pane in &self.panes {
3541            join_pane_into_active(&self.active_pane, pane, window, cx);
3542        }
3543        if let Some(active_item) = active_item {
3544            self.activate_item(active_item.as_ref(), true, true, window, cx);
3545        }
3546        cx.notify();
3547    }
3548
3549    pub fn join_pane_into_next(
3550        &mut self,
3551        pane: Entity<Pane>,
3552        window: &mut Window,
3553        cx: &mut Context<Self>,
3554    ) {
3555        let next_pane = self
3556            .find_pane_in_direction(SplitDirection::Right, cx)
3557            .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3558            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3559            .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3560        let Some(next_pane) = next_pane else {
3561            return;
3562        };
3563        move_all_items(&pane, &next_pane, window, cx);
3564        cx.notify();
3565    }
3566
3567    fn remove_pane(
3568        &mut self,
3569        pane: Entity<Pane>,
3570        focus_on: Option<Entity<Pane>>,
3571        window: &mut Window,
3572        cx: &mut Context<Self>,
3573    ) {
3574        if self.center.remove(&pane).unwrap() {
3575            self.force_remove_pane(&pane, &focus_on, window, cx);
3576            self.unfollow_in_pane(&pane, window, cx);
3577            self.last_leaders_by_pane.remove(&pane.downgrade());
3578            for removed_item in pane.read(cx).items() {
3579                self.panes_by_item.remove(&removed_item.item_id());
3580            }
3581
3582            cx.notify();
3583        } else {
3584            self.active_item_path_changed(window, cx);
3585        }
3586        cx.emit(Event::PaneRemoved);
3587    }
3588
3589    pub fn panes(&self) -> &[Entity<Pane>] {
3590        &self.panes
3591    }
3592
3593    pub fn active_pane(&self) -> &Entity<Pane> {
3594        &self.active_pane
3595    }
3596
3597    pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
3598        for dock in self.all_docks() {
3599            if dock.focus_handle(cx).contains_focused(window, cx) {
3600                if let Some(pane) = dock
3601                    .read(cx)
3602                    .active_panel()
3603                    .and_then(|panel| panel.pane(cx))
3604                {
3605                    return pane;
3606                }
3607            }
3608        }
3609        self.active_pane().clone()
3610    }
3611
3612    pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
3613        self.find_pane_in_direction(SplitDirection::Right, cx)
3614            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3615            .unwrap_or_else(|| {
3616                self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
3617            })
3618            .clone()
3619    }
3620
3621    pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
3622        let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3623        weak_pane.upgrade()
3624    }
3625
3626    fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
3627        self.follower_states.retain(|leader_id, state| {
3628            if *leader_id == peer_id {
3629                for item in state.items_by_leader_view_id.values() {
3630                    item.view.set_leader_peer_id(None, window, cx);
3631                }
3632                false
3633            } else {
3634                true
3635            }
3636        });
3637        cx.notify();
3638    }
3639
3640    pub fn start_following(
3641        &mut self,
3642        leader_id: PeerId,
3643        window: &mut Window,
3644        cx: &mut Context<Self>,
3645    ) -> Option<Task<Result<()>>> {
3646        let pane = self.active_pane().clone();
3647
3648        self.last_leaders_by_pane
3649            .insert(pane.downgrade(), leader_id);
3650        self.unfollow(leader_id, window, cx);
3651        self.unfollow_in_pane(&pane, window, cx);
3652        self.follower_states.insert(
3653            leader_id,
3654            FollowerState {
3655                center_pane: pane.clone(),
3656                dock_pane: None,
3657                active_view_id: None,
3658                items_by_leader_view_id: Default::default(),
3659            },
3660        );
3661        cx.notify();
3662
3663        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3664        let project_id = self.project.read(cx).remote_id();
3665        let request = self.app_state.client.request(proto::Follow {
3666            room_id,
3667            project_id,
3668            leader_id: Some(leader_id),
3669        });
3670
3671        Some(cx.spawn_in(window, |this, mut cx| async move {
3672            let response = request.await?;
3673            this.update(&mut cx, |this, _| {
3674                let state = this
3675                    .follower_states
3676                    .get_mut(&leader_id)
3677                    .ok_or_else(|| anyhow!("following interrupted"))?;
3678                state.active_view_id = response
3679                    .active_view
3680                    .as_ref()
3681                    .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3682                Ok::<_, anyhow::Error>(())
3683            })??;
3684            if let Some(view) = response.active_view {
3685                Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3686            }
3687            this.update_in(&mut cx, |this, window, cx| {
3688                this.leader_updated(leader_id, window, cx)
3689            })?;
3690            Ok(())
3691        }))
3692    }
3693
3694    pub fn follow_next_collaborator(
3695        &mut self,
3696        _: &FollowNextCollaborator,
3697        window: &mut Window,
3698        cx: &mut Context<Self>,
3699    ) {
3700        let collaborators = self.project.read(cx).collaborators();
3701        let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3702            let mut collaborators = collaborators.keys().copied();
3703            for peer_id in collaborators.by_ref() {
3704                if peer_id == leader_id {
3705                    break;
3706                }
3707            }
3708            collaborators.next()
3709        } else if let Some(last_leader_id) =
3710            self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3711        {
3712            if collaborators.contains_key(last_leader_id) {
3713                Some(*last_leader_id)
3714            } else {
3715                None
3716            }
3717        } else {
3718            None
3719        };
3720
3721        let pane = self.active_pane.clone();
3722        let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3723        else {
3724            return;
3725        };
3726        if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
3727            return;
3728        }
3729        if let Some(task) = self.start_following(leader_id, window, cx) {
3730            task.detach_and_log_err(cx)
3731        }
3732    }
3733
3734    pub fn follow(&mut self, leader_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
3735        let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3736            return;
3737        };
3738        let room = room.read(cx);
3739        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3740            return;
3741        };
3742
3743        let project = self.project.read(cx);
3744
3745        let other_project_id = match remote_participant.location {
3746            call::ParticipantLocation::External => None,
3747            call::ParticipantLocation::UnsharedProject => None,
3748            call::ParticipantLocation::SharedProject { project_id } => {
3749                if Some(project_id) == project.remote_id() {
3750                    None
3751                } else {
3752                    Some(project_id)
3753                }
3754            }
3755        };
3756
3757        // if they are active in another project, follow there.
3758        if let Some(project_id) = other_project_id {
3759            let app_state = self.app_state.clone();
3760            crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3761                .detach_and_log_err(cx);
3762        }
3763
3764        // if you're already following, find the right pane and focus it.
3765        if let Some(follower_state) = self.follower_states.get(&leader_id) {
3766            window.focus(&follower_state.pane().focus_handle(cx));
3767
3768            return;
3769        }
3770
3771        // Otherwise, follow.
3772        if let Some(task) = self.start_following(leader_id, window, cx) {
3773            task.detach_and_log_err(cx)
3774        }
3775    }
3776
3777    pub fn unfollow(
3778        &mut self,
3779        leader_id: PeerId,
3780        window: &mut Window,
3781        cx: &mut Context<Self>,
3782    ) -> Option<()> {
3783        cx.notify();
3784        let state = self.follower_states.remove(&leader_id)?;
3785        for (_, item) in state.items_by_leader_view_id {
3786            item.view.set_leader_peer_id(None, window, cx);
3787        }
3788
3789        let project_id = self.project.read(cx).remote_id();
3790        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3791        self.app_state
3792            .client
3793            .send(proto::Unfollow {
3794                room_id,
3795                project_id,
3796                leader_id: Some(leader_id),
3797            })
3798            .log_err();
3799
3800        Some(())
3801    }
3802
3803    pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3804        self.follower_states.contains_key(&peer_id)
3805    }
3806
3807    fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3808        cx.emit(Event::ActiveItemChanged);
3809        let active_entry = self.active_project_path(cx);
3810        self.project
3811            .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3812
3813        self.update_window_title(window, cx);
3814    }
3815
3816    fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
3817        let project = self.project().read(cx);
3818        let mut title = String::new();
3819
3820        for (i, name) in project.worktree_root_names(cx).enumerate() {
3821            if i > 0 {
3822                title.push_str(", ");
3823            }
3824            title.push_str(name);
3825        }
3826
3827        if title.is_empty() {
3828            title = "empty project".to_string();
3829        }
3830
3831        if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3832            let filename = path
3833                .path
3834                .file_name()
3835                .map(|s| s.to_string_lossy())
3836                .or_else(|| {
3837                    Some(Cow::Borrowed(
3838                        project
3839                            .worktree_for_id(path.worktree_id, cx)?
3840                            .read(cx)
3841                            .root_name(),
3842                    ))
3843                });
3844
3845            if let Some(filename) = filename {
3846                title.push_str("");
3847                title.push_str(filename.as_ref());
3848            }
3849        }
3850
3851        if project.is_via_collab() {
3852            title.push_str("");
3853        } else if project.is_shared() {
3854            title.push_str("");
3855        }
3856
3857        window.set_window_title(&title);
3858    }
3859
3860    fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
3861        let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty();
3862        if is_edited != self.window_edited {
3863            self.window_edited = is_edited;
3864            window.set_window_edited(self.window_edited)
3865        }
3866    }
3867
3868    fn update_item_dirty_state(
3869        &mut self,
3870        item: &dyn ItemHandle,
3871        window: &mut Window,
3872        cx: &mut App,
3873    ) {
3874        let is_dirty = item.is_dirty(cx);
3875        let item_id = item.item_id();
3876        let was_dirty = self.dirty_items.contains_key(&item_id);
3877        if is_dirty == was_dirty {
3878            return;
3879        }
3880        if was_dirty {
3881            self.dirty_items.remove(&item_id);
3882            self.update_window_edited(window, cx);
3883            return;
3884        }
3885        if let Some(window_handle) = window.window_handle().downcast::<Self>() {
3886            let s = item.on_release(
3887                cx,
3888                Box::new(move |cx| {
3889                    window_handle
3890                        .update(cx, |this, window, cx| {
3891                            this.dirty_items.remove(&item_id);
3892                            this.update_window_edited(window, cx)
3893                        })
3894                        .ok();
3895                }),
3896            );
3897            self.dirty_items.insert(item_id, s);
3898            self.update_window_edited(window, cx);
3899        }
3900    }
3901
3902    fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
3903        if self.notifications.is_empty() {
3904            None
3905        } else {
3906            Some(
3907                div()
3908                    .absolute()
3909                    .right_3()
3910                    .bottom_3()
3911                    .w_112()
3912                    .h_full()
3913                    .flex()
3914                    .flex_col()
3915                    .justify_end()
3916                    .gap_2()
3917                    .children(
3918                        self.notifications
3919                            .iter()
3920                            .map(|(_, notification)| notification.clone().into_any()),
3921                    ),
3922            )
3923        }
3924    }
3925
3926    // RPC handlers
3927
3928    fn active_view_for_follower(
3929        &self,
3930        follower_project_id: Option<u64>,
3931        window: &mut Window,
3932        cx: &mut Context<Self>,
3933    ) -> Option<proto::View> {
3934        let (item, panel_id) = self.active_item_for_followers(window, cx);
3935        let item = item?;
3936        let leader_id = self
3937            .pane_for(&*item)
3938            .and_then(|pane| self.leader_for_pane(&pane));
3939
3940        let item_handle = item.to_followable_item_handle(cx)?;
3941        let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
3942        let variant = item_handle.to_state_proto(window, cx)?;
3943
3944        if item_handle.is_project_item(window, cx)
3945            && (follower_project_id.is_none()
3946                || follower_project_id != self.project.read(cx).remote_id())
3947        {
3948            return None;
3949        }
3950
3951        Some(proto::View {
3952            id: Some(id.to_proto()),
3953            leader_id,
3954            variant: Some(variant),
3955            panel_id: panel_id.map(|id| id as i32),
3956        })
3957    }
3958
3959    fn handle_follow(
3960        &mut self,
3961        follower_project_id: Option<u64>,
3962        window: &mut Window,
3963        cx: &mut Context<Self>,
3964    ) -> proto::FollowResponse {
3965        let active_view = self.active_view_for_follower(follower_project_id, window, cx);
3966
3967        cx.notify();
3968        proto::FollowResponse {
3969            // TODO: Remove after version 0.145.x stabilizes.
3970            active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3971            views: active_view.iter().cloned().collect(),
3972            active_view,
3973        }
3974    }
3975
3976    fn handle_update_followers(
3977        &mut self,
3978        leader_id: PeerId,
3979        message: proto::UpdateFollowers,
3980        _window: &mut Window,
3981        _cx: &mut Context<Self>,
3982    ) {
3983        self.leader_updates_tx
3984            .unbounded_send((leader_id, message))
3985            .ok();
3986    }
3987
3988    async fn process_leader_update(
3989        this: &WeakEntity<Self>,
3990        leader_id: PeerId,
3991        update: proto::UpdateFollowers,
3992        cx: &mut AsyncWindowContext,
3993    ) -> Result<()> {
3994        match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3995            proto::update_followers::Variant::CreateView(view) => {
3996                let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3997                let should_add_view = this.update(cx, |this, _| {
3998                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3999                        anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
4000                    } else {
4001                        anyhow::Ok(false)
4002                    }
4003                })??;
4004
4005                if should_add_view {
4006                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
4007                }
4008            }
4009            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
4010                let should_add_view = this.update(cx, |this, _| {
4011                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
4012                        state.active_view_id = update_active_view
4013                            .view
4014                            .as_ref()
4015                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
4016
4017                        if state.active_view_id.is_some_and(|view_id| {
4018                            !state.items_by_leader_view_id.contains_key(&view_id)
4019                        }) {
4020                            anyhow::Ok(true)
4021                        } else {
4022                            anyhow::Ok(false)
4023                        }
4024                    } else {
4025                        anyhow::Ok(false)
4026                    }
4027                })??;
4028
4029                if should_add_view {
4030                    if let Some(view) = update_active_view.view {
4031                        Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
4032                    }
4033                }
4034            }
4035            proto::update_followers::Variant::UpdateView(update_view) => {
4036                let variant = update_view
4037                    .variant
4038                    .ok_or_else(|| anyhow!("missing update view variant"))?;
4039                let id = update_view
4040                    .id
4041                    .ok_or_else(|| anyhow!("missing update view id"))?;
4042                let mut tasks = Vec::new();
4043                this.update_in(cx, |this, window, cx| {
4044                    let project = this.project.clone();
4045                    if let Some(state) = this.follower_states.get(&leader_id) {
4046                        let view_id = ViewId::from_proto(id.clone())?;
4047                        if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
4048                            tasks.push(item.view.apply_update_proto(
4049                                &project,
4050                                variant.clone(),
4051                                window,
4052                                cx,
4053                            ));
4054                        }
4055                    }
4056                    anyhow::Ok(())
4057                })??;
4058                try_join_all(tasks).await.log_err();
4059            }
4060        }
4061        this.update_in(cx, |this, window, cx| {
4062            this.leader_updated(leader_id, window, cx)
4063        })?;
4064        Ok(())
4065    }
4066
4067    async fn add_view_from_leader(
4068        this: WeakEntity<Self>,
4069        leader_id: PeerId,
4070        view: &proto::View,
4071        cx: &mut AsyncWindowContext,
4072    ) -> Result<()> {
4073        let this = this.upgrade().context("workspace dropped")?;
4074
4075        let Some(id) = view.id.clone() else {
4076            return Err(anyhow!("no id for view"));
4077        };
4078        let id = ViewId::from_proto(id)?;
4079        let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
4080
4081        let pane = this.update(cx, |this, _cx| {
4082            let state = this
4083                .follower_states
4084                .get(&leader_id)
4085                .context("stopped following")?;
4086            anyhow::Ok(state.pane().clone())
4087        })??;
4088        let existing_item = pane.update_in(cx, |pane, window, cx| {
4089            let client = this.read(cx).client().clone();
4090            pane.items().find_map(|item| {
4091                let item = item.to_followable_item_handle(cx)?;
4092                if item.remote_id(&client, window, cx) == Some(id) {
4093                    Some(item)
4094                } else {
4095                    None
4096                }
4097            })
4098        })?;
4099        let item = if let Some(existing_item) = existing_item {
4100            existing_item
4101        } else {
4102            let variant = view.variant.clone();
4103            if variant.is_none() {
4104                Err(anyhow!("missing view variant"))?;
4105            }
4106
4107            let task = cx.update(|window, cx| {
4108                FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
4109            })?;
4110
4111            let Some(task) = task else {
4112                return Err(anyhow!(
4113                    "failed to construct view from leader (maybe from a different version of zed?)"
4114                ));
4115            };
4116
4117            let mut new_item = task.await?;
4118            pane.update_in(cx, |pane, window, cx| {
4119                let mut item_to_remove = None;
4120                for (ix, item) in pane.items().enumerate() {
4121                    if let Some(item) = item.to_followable_item_handle(cx) {
4122                        match new_item.dedup(item.as_ref(), window, cx) {
4123                            Some(item::Dedup::KeepExisting) => {
4124                                new_item =
4125                                    item.boxed_clone().to_followable_item_handle(cx).unwrap();
4126                                break;
4127                            }
4128                            Some(item::Dedup::ReplaceExisting) => {
4129                                item_to_remove = Some((ix, item.item_id()));
4130                                break;
4131                            }
4132                            None => {}
4133                        }
4134                    }
4135                }
4136
4137                if let Some((ix, id)) = item_to_remove {
4138                    pane.remove_item(id, false, false, window, cx);
4139                    pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
4140                }
4141            })?;
4142
4143            new_item
4144        };
4145
4146        this.update_in(cx, |this, window, cx| {
4147            let state = this.follower_states.get_mut(&leader_id)?;
4148            item.set_leader_peer_id(Some(leader_id), window, cx);
4149            state.items_by_leader_view_id.insert(
4150                id,
4151                FollowerView {
4152                    view: item,
4153                    location: panel_id,
4154                },
4155            );
4156
4157            Some(())
4158        })?;
4159
4160        Ok(())
4161    }
4162
4163    pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
4164        let mut is_project_item = true;
4165        let mut update = proto::UpdateActiveView::default();
4166        if window.is_window_active() {
4167            let (active_item, panel_id) = self.active_item_for_followers(window, cx);
4168
4169            if let Some(item) = active_item {
4170                if item.item_focus_handle(cx).contains_focused(window, cx) {
4171                    let leader_id = self
4172                        .pane_for(&*item)
4173                        .and_then(|pane| self.leader_for_pane(&pane));
4174
4175                    if let Some(item) = item.to_followable_item_handle(cx) {
4176                        let id = item
4177                            .remote_id(&self.app_state.client, window, cx)
4178                            .map(|id| id.to_proto());
4179
4180                        if let Some(id) = id.clone() {
4181                            if let Some(variant) = item.to_state_proto(window, cx) {
4182                                let view = Some(proto::View {
4183                                    id: Some(id.clone()),
4184                                    leader_id,
4185                                    variant: Some(variant),
4186                                    panel_id: panel_id.map(|id| id as i32),
4187                                });
4188
4189                                is_project_item = item.is_project_item(window, cx);
4190                                update = proto::UpdateActiveView {
4191                                    view,
4192                                    // TODO: Remove after version 0.145.x stabilizes.
4193                                    id: Some(id.clone()),
4194                                    leader_id,
4195                                };
4196                            }
4197                        };
4198                    }
4199                }
4200            }
4201        }
4202
4203        let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
4204        if active_view_id != self.last_active_view_id.as_ref() {
4205            self.last_active_view_id = active_view_id.cloned();
4206            self.update_followers(
4207                is_project_item,
4208                proto::update_followers::Variant::UpdateActiveView(update),
4209                window,
4210                cx,
4211            );
4212        }
4213    }
4214
4215    fn active_item_for_followers(
4216        &self,
4217        window: &mut Window,
4218        cx: &mut App,
4219    ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
4220        let mut active_item = None;
4221        let mut panel_id = None;
4222        for dock in self.all_docks() {
4223            if dock.focus_handle(cx).contains_focused(window, cx) {
4224                if let Some(panel) = dock.read(cx).active_panel() {
4225                    if let Some(pane) = panel.pane(cx) {
4226                        if let Some(item) = pane.read(cx).active_item() {
4227                            active_item = Some(item);
4228                            panel_id = panel.remote_id();
4229                            break;
4230                        }
4231                    }
4232                }
4233            }
4234        }
4235
4236        if active_item.is_none() {
4237            active_item = self.active_pane().read(cx).active_item();
4238        }
4239        (active_item, panel_id)
4240    }
4241
4242    fn update_followers(
4243        &self,
4244        project_only: bool,
4245        update: proto::update_followers::Variant,
4246        _: &mut Window,
4247        cx: &mut App,
4248    ) -> Option<()> {
4249        // If this update only applies to for followers in the current project,
4250        // then skip it unless this project is shared. If it applies to all
4251        // followers, regardless of project, then set `project_id` to none,
4252        // indicating that it goes to all followers.
4253        let project_id = if project_only {
4254            Some(self.project.read(cx).remote_id()?)
4255        } else {
4256            None
4257        };
4258        self.app_state().workspace_store.update(cx, |store, cx| {
4259            store.update_followers(project_id, update, cx)
4260        })
4261    }
4262
4263    pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<PeerId> {
4264        self.follower_states.iter().find_map(|(leader_id, state)| {
4265            if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
4266                Some(*leader_id)
4267            } else {
4268                None
4269            }
4270        })
4271    }
4272
4273    fn leader_updated(
4274        &mut self,
4275        leader_id: PeerId,
4276        window: &mut Window,
4277        cx: &mut Context<Self>,
4278    ) -> Option<()> {
4279        cx.notify();
4280
4281        let call = self.active_call()?;
4282        let room = call.read(cx).room()?.read(cx);
4283        let participant = room.remote_participant_for_peer_id(leader_id)?;
4284
4285        let leader_in_this_app;
4286        let leader_in_this_project;
4287        match participant.location {
4288            call::ParticipantLocation::SharedProject { project_id } => {
4289                leader_in_this_app = true;
4290                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
4291            }
4292            call::ParticipantLocation::UnsharedProject => {
4293                leader_in_this_app = true;
4294                leader_in_this_project = false;
4295            }
4296            call::ParticipantLocation::External => {
4297                leader_in_this_app = false;
4298                leader_in_this_project = false;
4299            }
4300        };
4301
4302        let state = self.follower_states.get(&leader_id)?;
4303        let mut item_to_activate = None;
4304        if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
4305            if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
4306                if leader_in_this_project || !item.view.is_project_item(window, cx) {
4307                    item_to_activate = Some((item.location, item.view.boxed_clone()));
4308                }
4309            }
4310        } else if let Some(shared_screen) =
4311            self.shared_screen_for_peer(leader_id, &state.center_pane, window, cx)
4312        {
4313            item_to_activate = Some((None, Box::new(shared_screen)));
4314        }
4315
4316        let (panel_id, item) = item_to_activate?;
4317
4318        let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
4319        let pane;
4320        if let Some(panel_id) = panel_id {
4321            pane = self
4322                .activate_panel_for_proto_id(panel_id, window, cx)?
4323                .pane(cx)?;
4324            let state = self.follower_states.get_mut(&leader_id)?;
4325            state.dock_pane = Some(pane.clone());
4326        } else {
4327            pane = state.center_pane.clone();
4328            let state = self.follower_states.get_mut(&leader_id)?;
4329            if let Some(dock_pane) = state.dock_pane.take() {
4330                transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
4331            }
4332        }
4333
4334        pane.update(cx, |pane, cx| {
4335            let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
4336            if let Some(index) = pane.index_for_item(item.as_ref()) {
4337                pane.activate_item(index, false, false, window, cx);
4338            } else {
4339                pane.add_item(item.boxed_clone(), false, false, None, window, cx)
4340            }
4341
4342            if focus_active_item {
4343                pane.focus_active_item(window, cx)
4344            }
4345        });
4346
4347        None
4348    }
4349
4350    #[cfg(target_os = "windows")]
4351    fn shared_screen_for_peer(
4352        &self,
4353        _peer_id: PeerId,
4354        _pane: &Entity<Pane>,
4355        _window: &mut Window,
4356        _cx: &mut App,
4357    ) -> Option<Entity<SharedScreen>> {
4358        None
4359    }
4360
4361    #[cfg(not(target_os = "windows"))]
4362    fn shared_screen_for_peer(
4363        &self,
4364        peer_id: PeerId,
4365        pane: &Entity<Pane>,
4366        window: &mut Window,
4367        cx: &mut App,
4368    ) -> Option<Entity<SharedScreen>> {
4369        let call = self.active_call()?;
4370        let room = call.read(cx).room()?.read(cx);
4371        let participant = room.remote_participant_for_peer_id(peer_id)?;
4372        let track = participant.video_tracks.values().next()?.clone();
4373        let user = participant.user.clone();
4374
4375        for item in pane.read(cx).items_of_type::<SharedScreen>() {
4376            if item.read(cx).peer_id == peer_id {
4377                return Some(item);
4378            }
4379        }
4380
4381        Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), window, cx)))
4382    }
4383
4384    pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4385        if window.is_window_active() {
4386            self.update_active_view_for_followers(window, cx);
4387
4388            if let Some(database_id) = self.database_id {
4389                cx.background_spawn(persistence::DB.update_timestamp(database_id))
4390                    .detach();
4391            }
4392        } else {
4393            for pane in &self.panes {
4394                pane.update(cx, |pane, cx| {
4395                    if let Some(item) = pane.active_item() {
4396                        item.workspace_deactivated(window, cx);
4397                    }
4398                    for item in pane.items() {
4399                        if matches!(
4400                            item.workspace_settings(cx).autosave,
4401                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4402                        ) {
4403                            Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
4404                                .detach_and_log_err(cx);
4405                        }
4406                    }
4407                });
4408            }
4409        }
4410    }
4411
4412    pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
4413        self.active_call.as_ref().map(|(call, _)| call)
4414    }
4415
4416    fn on_active_call_event(
4417        &mut self,
4418        _: &Entity<ActiveCall>,
4419        event: &call::room::Event,
4420        window: &mut Window,
4421        cx: &mut Context<Self>,
4422    ) {
4423        match event {
4424            call::room::Event::ParticipantLocationChanged { participant_id }
4425            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4426                self.leader_updated(*participant_id, window, cx);
4427            }
4428            _ => {}
4429        }
4430    }
4431
4432    pub fn database_id(&self) -> Option<WorkspaceId> {
4433        self.database_id
4434    }
4435
4436    pub fn session_id(&self) -> Option<String> {
4437        self.session_id.clone()
4438    }
4439
4440    fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4441        let project = self.project().read(cx);
4442
4443        if project.is_local() {
4444            Some(
4445                project
4446                    .visible_worktrees(cx)
4447                    .map(|worktree| worktree.read(cx).abs_path())
4448                    .collect::<Vec<_>>(),
4449            )
4450        } else {
4451            None
4452        }
4453    }
4454
4455    fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4456        match member {
4457            Member::Axis(PaneAxis { members, .. }) => {
4458                for child in members.iter() {
4459                    self.remove_panes(child.clone(), window, cx)
4460                }
4461            }
4462            Member::Pane(pane) => {
4463                self.force_remove_pane(&pane, &None, window, cx);
4464            }
4465        }
4466    }
4467
4468    fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4469        self.session_id.take();
4470        self.serialize_workspace_internal(window, cx)
4471    }
4472
4473    fn force_remove_pane(
4474        &mut self,
4475        pane: &Entity<Pane>,
4476        focus_on: &Option<Entity<Pane>>,
4477        window: &mut Window,
4478        cx: &mut Context<Workspace>,
4479    ) {
4480        self.panes.retain(|p| p != pane);
4481        if let Some(focus_on) = focus_on {
4482            focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4483        } else {
4484            if self.active_pane() == pane {
4485                self.panes
4486                    .last()
4487                    .unwrap()
4488                    .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4489            }
4490        }
4491        if self.last_active_center_pane == Some(pane.downgrade()) {
4492            self.last_active_center_pane = None;
4493        }
4494        cx.notify();
4495    }
4496
4497    fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4498        if self._schedule_serialize.is_none() {
4499            self._schedule_serialize = Some(cx.spawn_in(window, |this, mut cx| async move {
4500                cx.background_executor()
4501                    .timer(Duration::from_millis(100))
4502                    .await;
4503                this.update_in(&mut cx, |this, window, cx| {
4504                    this.serialize_workspace_internal(window, cx).detach();
4505                    this._schedule_serialize.take();
4506                })
4507                .log_err();
4508            }));
4509        }
4510    }
4511
4512    fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4513        let Some(database_id) = self.database_id() else {
4514            return Task::ready(());
4515        };
4516
4517        fn serialize_pane_handle(
4518            pane_handle: &Entity<Pane>,
4519            window: &mut Window,
4520            cx: &mut App,
4521        ) -> SerializedPane {
4522            let (items, active, pinned_count) = {
4523                let pane = pane_handle.read(cx);
4524                let active_item_id = pane.active_item().map(|item| item.item_id());
4525                (
4526                    pane.items()
4527                        .filter_map(|handle| {
4528                            let handle = handle.to_serializable_item_handle(cx)?;
4529
4530                            Some(SerializedItem {
4531                                kind: Arc::from(handle.serialized_item_kind()),
4532                                item_id: handle.item_id().as_u64(),
4533                                active: Some(handle.item_id()) == active_item_id,
4534                                preview: pane.is_active_preview_item(handle.item_id()),
4535                            })
4536                        })
4537                        .collect::<Vec<_>>(),
4538                    pane.has_focus(window, cx),
4539                    pane.pinned_count(),
4540                )
4541            };
4542
4543            SerializedPane::new(items, active, pinned_count)
4544        }
4545
4546        fn build_serialized_pane_group(
4547            pane_group: &Member,
4548            window: &mut Window,
4549            cx: &mut App,
4550        ) -> SerializedPaneGroup {
4551            match pane_group {
4552                Member::Axis(PaneAxis {
4553                    axis,
4554                    members,
4555                    flexes,
4556                    bounding_boxes: _,
4557                }) => SerializedPaneGroup::Group {
4558                    axis: SerializedAxis(*axis),
4559                    children: members
4560                        .iter()
4561                        .map(|member| build_serialized_pane_group(member, window, cx))
4562                        .collect::<Vec<_>>(),
4563                    flexes: Some(flexes.lock().clone()),
4564                },
4565                Member::Pane(pane_handle) => {
4566                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4567                }
4568            }
4569        }
4570
4571        fn build_serialized_docks(
4572            this: &Workspace,
4573            window: &mut Window,
4574            cx: &mut App,
4575        ) -> DockStructure {
4576            let left_dock = this.left_dock.read(cx);
4577            let left_visible = left_dock.is_open();
4578            let left_active_panel = left_dock
4579                .active_panel()
4580                .map(|panel| panel.persistent_name().to_string());
4581            let left_dock_zoom = left_dock
4582                .active_panel()
4583                .map(|panel| panel.is_zoomed(window, cx))
4584                .unwrap_or(false);
4585
4586            let right_dock = this.right_dock.read(cx);
4587            let right_visible = right_dock.is_open();
4588            let right_active_panel = right_dock
4589                .active_panel()
4590                .map(|panel| panel.persistent_name().to_string());
4591            let right_dock_zoom = right_dock
4592                .active_panel()
4593                .map(|panel| panel.is_zoomed(window, cx))
4594                .unwrap_or(false);
4595
4596            let bottom_dock = this.bottom_dock.read(cx);
4597            let bottom_visible = bottom_dock.is_open();
4598            let bottom_active_panel = bottom_dock
4599                .active_panel()
4600                .map(|panel| panel.persistent_name().to_string());
4601            let bottom_dock_zoom = bottom_dock
4602                .active_panel()
4603                .map(|panel| panel.is_zoomed(window, cx))
4604                .unwrap_or(false);
4605
4606            DockStructure {
4607                left: DockData {
4608                    visible: left_visible,
4609                    active_panel: left_active_panel,
4610                    zoom: left_dock_zoom,
4611                },
4612                right: DockData {
4613                    visible: right_visible,
4614                    active_panel: right_active_panel,
4615                    zoom: right_dock_zoom,
4616                },
4617                bottom: DockData {
4618                    visible: bottom_visible,
4619                    active_panel: bottom_active_panel,
4620                    zoom: bottom_dock_zoom,
4621                },
4622            }
4623        }
4624
4625        let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4626            Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4627        } else if let Some(local_paths) = self.local_paths(cx) {
4628            if !local_paths.is_empty() {
4629                Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4630            } else {
4631                None
4632            }
4633        } else {
4634            None
4635        };
4636
4637        if let Some(location) = location {
4638            let center_group = build_serialized_pane_group(&self.center.root, window, cx);
4639            let docks = build_serialized_docks(self, window, cx);
4640            let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
4641            let serialized_workspace = SerializedWorkspace {
4642                id: database_id,
4643                location,
4644                center_group,
4645                window_bounds,
4646                display: Default::default(),
4647                docks,
4648                centered_layout: self.centered_layout,
4649                session_id: self.session_id.clone(),
4650                window_id: Some(window.window_handle().window_id().as_u64()),
4651            };
4652            return window.spawn(cx, |_| persistence::DB.save_workspace(serialized_workspace));
4653        }
4654        Task::ready(())
4655    }
4656
4657    async fn serialize_items(
4658        this: &WeakEntity<Self>,
4659        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4660        cx: &mut AsyncWindowContext,
4661    ) -> Result<()> {
4662        const CHUNK_SIZE: usize = 200;
4663
4664        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4665
4666        while let Some(items_received) = serializable_items.next().await {
4667            let unique_items =
4668                items_received
4669                    .into_iter()
4670                    .fold(HashMap::default(), |mut acc, item| {
4671                        acc.entry(item.item_id()).or_insert(item);
4672                        acc
4673                    });
4674
4675            // We use into_iter() here so that the references to the items are moved into
4676            // the tasks and not kept alive while we're sleeping.
4677            for (_, item) in unique_items.into_iter() {
4678                if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
4679                    item.serialize(workspace, false, window, cx)
4680                }) {
4681                    cx.background_spawn(async move { task.await.log_err() })
4682                        .detach();
4683                }
4684            }
4685
4686            cx.background_executor()
4687                .timer(SERIALIZATION_THROTTLE_TIME)
4688                .await;
4689        }
4690
4691        Ok(())
4692    }
4693
4694    pub(crate) fn enqueue_item_serialization(
4695        &mut self,
4696        item: Box<dyn SerializableItemHandle>,
4697    ) -> Result<()> {
4698        self.serializable_items_tx
4699            .unbounded_send(item)
4700            .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4701    }
4702
4703    pub(crate) fn load_workspace(
4704        serialized_workspace: SerializedWorkspace,
4705        paths_to_open: Vec<Option<ProjectPath>>,
4706        window: &mut Window,
4707        cx: &mut Context<Workspace>,
4708    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4709        cx.spawn_in(window, |workspace, mut cx| async move {
4710            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4711
4712            let mut center_group = None;
4713            let mut center_items = None;
4714
4715            // Traverse the splits tree and add to things
4716            if let Some((group, active_pane, items)) = serialized_workspace
4717                .center_group
4718                .deserialize(
4719                    &project,
4720                    serialized_workspace.id,
4721                    workspace.clone(),
4722                    &mut cx,
4723                )
4724                .await
4725            {
4726                center_items = Some(items);
4727                center_group = Some((group, active_pane))
4728            }
4729
4730            let mut items_by_project_path = HashMap::default();
4731            let mut item_ids_by_kind = HashMap::default();
4732            let mut all_deserialized_items = Vec::default();
4733            cx.update(|_, cx| {
4734                for item in center_items.unwrap_or_default().into_iter().flatten() {
4735                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4736                        item_ids_by_kind
4737                            .entry(serializable_item_handle.serialized_item_kind())
4738                            .or_insert(Vec::new())
4739                            .push(item.item_id().as_u64() as ItemId);
4740                    }
4741
4742                    if let Some(project_path) = item.project_path(cx) {
4743                        items_by_project_path.insert(project_path, item.clone());
4744                    }
4745                    all_deserialized_items.push(item);
4746                }
4747            })?;
4748
4749            let opened_items = paths_to_open
4750                .into_iter()
4751                .map(|path_to_open| {
4752                    path_to_open
4753                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4754                })
4755                .collect::<Vec<_>>();
4756
4757            // Remove old panes from workspace panes list
4758            workspace.update_in(&mut cx, |workspace, window, cx| {
4759                if let Some((center_group, active_pane)) = center_group {
4760                    workspace.remove_panes(workspace.center.root.clone(), window, cx);
4761
4762                    // Swap workspace center group
4763                    workspace.center = PaneGroup::with_root(center_group);
4764                    if let Some(active_pane) = active_pane {
4765                        workspace.set_active_pane(&active_pane, window, cx);
4766                        cx.focus_self(window);
4767                    } else {
4768                        workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
4769                    }
4770                }
4771
4772                let docks = serialized_workspace.docks;
4773
4774                for (dock, serialized_dock) in [
4775                    (&mut workspace.right_dock, docks.right),
4776                    (&mut workspace.left_dock, docks.left),
4777                    (&mut workspace.bottom_dock, docks.bottom),
4778                ]
4779                .iter_mut()
4780                {
4781                    dock.update(cx, |dock, cx| {
4782                        dock.serialized_dock = Some(serialized_dock.clone());
4783                        dock.restore_state(window, cx);
4784                    });
4785                }
4786
4787                cx.notify();
4788            })?;
4789
4790            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4791            // after loading the items, we might have different items and in order to avoid
4792            // the database filling up, we delete items that haven't been loaded now.
4793            //
4794            // The items that have been loaded, have been saved after they've been added to the workspace.
4795            let clean_up_tasks = workspace.update_in(&mut cx, |_, window, cx| {
4796                item_ids_by_kind
4797                    .into_iter()
4798                    .map(|(item_kind, loaded_items)| {
4799                        SerializableItemRegistry::cleanup(
4800                            item_kind,
4801                            serialized_workspace.id,
4802                            loaded_items,
4803                            window,
4804                            cx,
4805                        )
4806                        .log_err()
4807                    })
4808                    .collect::<Vec<_>>()
4809            })?;
4810
4811            futures::future::join_all(clean_up_tasks).await;
4812
4813            workspace
4814                .update_in(&mut cx, |workspace, window, cx| {
4815                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4816                    workspace.serialize_workspace_internal(window, cx).detach();
4817
4818                    // Ensure that we mark the window as edited if we did load dirty items
4819                    workspace.update_window_edited(window, cx);
4820                })
4821                .ok();
4822
4823            Ok(opened_items)
4824        })
4825    }
4826
4827    fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
4828        self.add_workspace_actions_listeners(div, window, cx)
4829            .on_action(cx.listener(Self::close_inactive_items_and_panes))
4830            .on_action(cx.listener(Self::close_all_items_and_panes))
4831            .on_action(cx.listener(Self::save_all))
4832            .on_action(cx.listener(Self::send_keystrokes))
4833            .on_action(cx.listener(Self::add_folder_to_project))
4834            .on_action(cx.listener(Self::follow_next_collaborator))
4835            .on_action(cx.listener(Self::close_window))
4836            .on_action(cx.listener(Self::activate_pane_at_index))
4837            .on_action(cx.listener(Self::move_item_to_pane_at_index))
4838            .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4839            .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
4840                let pane = workspace.active_pane().clone();
4841                workspace.unfollow_in_pane(&pane, window, cx);
4842            }))
4843            .on_action(cx.listener(|workspace, action: &Save, window, cx| {
4844                workspace
4845                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
4846                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4847            }))
4848            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
4849                workspace
4850                    .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
4851                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4852            }))
4853            .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
4854                workspace
4855                    .save_active_item(SaveIntent::SaveAs, window, cx)
4856                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4857            }))
4858            .on_action(
4859                cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
4860                    workspace.activate_previous_pane(window, cx)
4861                }),
4862            )
4863            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4864                workspace.activate_next_pane(window, cx)
4865            }))
4866            .on_action(
4867                cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
4868                    workspace.activate_next_window(cx)
4869                }),
4870            )
4871            .on_action(
4872                cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
4873                    workspace.activate_previous_window(cx)
4874                }),
4875            )
4876            .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| {
4877                workspace.activate_pane_in_direction(SplitDirection::Left, window, cx)
4878            }))
4879            .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| {
4880                workspace.activate_pane_in_direction(SplitDirection::Right, window, cx)
4881            }))
4882            .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| {
4883                workspace.activate_pane_in_direction(SplitDirection::Up, window, cx)
4884            }))
4885            .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| {
4886                workspace.activate_pane_in_direction(SplitDirection::Down, window, cx)
4887            }))
4888            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4889                workspace.activate_next_pane(window, cx)
4890            }))
4891            .on_action(cx.listener(
4892                |workspace, action: &MoveItemToPaneInDirection, window, cx| {
4893                    workspace.move_item_to_pane_in_direction(action, window, cx)
4894                },
4895            ))
4896            .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| {
4897                workspace.swap_pane_in_direction(SplitDirection::Left, cx)
4898            }))
4899            .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| {
4900                workspace.swap_pane_in_direction(SplitDirection::Right, cx)
4901            }))
4902            .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| {
4903                workspace.swap_pane_in_direction(SplitDirection::Up, cx)
4904            }))
4905            .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| {
4906                workspace.swap_pane_in_direction(SplitDirection::Down, cx)
4907            }))
4908            .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
4909                this.toggle_dock(DockPosition::Left, window, cx);
4910            }))
4911            .on_action(cx.listener(
4912                |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
4913                    workspace.toggle_dock(DockPosition::Right, window, cx);
4914                },
4915            ))
4916            .on_action(cx.listener(
4917                |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
4918                    workspace.toggle_dock(DockPosition::Bottom, window, cx);
4919                },
4920            ))
4921            .on_action(
4922                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
4923                    workspace.close_all_docks(window, cx);
4924                }),
4925            )
4926            .on_action(cx.listener(
4927                |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
4928                    workspace.clear_all_notifications(cx);
4929                },
4930            ))
4931            .on_action(cx.listener(
4932                |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
4933                    workspace.reopen_closed_item(window, cx).detach();
4934                },
4935            ))
4936            .on_action(cx.listener(Workspace::toggle_centered_layout))
4937    }
4938
4939    #[cfg(any(test, feature = "test-support"))]
4940    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
4941        use node_runtime::NodeRuntime;
4942        use session::Session;
4943
4944        let client = project.read(cx).client();
4945        let user_store = project.read(cx).user_store();
4946
4947        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
4948        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
4949        window.activate_window();
4950        let app_state = Arc::new(AppState {
4951            languages: project.read(cx).languages().clone(),
4952            workspace_store,
4953            client,
4954            user_store,
4955            fs: project.read(cx).fs().clone(),
4956            build_window_options: |_, _| Default::default(),
4957            node_runtime: NodeRuntime::unavailable(),
4958            session,
4959        });
4960        let workspace = Self::new(Default::default(), project, app_state, window, cx);
4961        workspace
4962            .active_pane
4963            .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4964        workspace
4965    }
4966
4967    pub fn register_action<A: Action>(
4968        &mut self,
4969        callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
4970    ) -> &mut Self {
4971        let callback = Arc::new(callback);
4972
4973        self.workspace_actions.push(Box::new(move |div, _, cx| {
4974            let callback = callback.clone();
4975            div.on_action(cx.listener(move |workspace, event, window, cx| {
4976                (callback.clone())(workspace, event, window, cx)
4977            }))
4978        }));
4979        self
4980    }
4981
4982    fn add_workspace_actions_listeners(
4983        &self,
4984        mut div: Div,
4985        window: &mut Window,
4986        cx: &mut Context<Self>,
4987    ) -> Div {
4988        for action in self.workspace_actions.iter() {
4989            div = (action)(div, window, cx)
4990        }
4991        div
4992    }
4993
4994    pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
4995        self.modal_layer.read(cx).has_active_modal()
4996    }
4997
4998    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
4999        self.modal_layer.read(cx).active_modal()
5000    }
5001
5002    pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
5003    where
5004        B: FnOnce(&mut Window, &mut Context<V>) -> V,
5005    {
5006        self.modal_layer.update(cx, |modal_layer, cx| {
5007            modal_layer.toggle_modal(window, cx, build)
5008        })
5009    }
5010
5011    pub fn toggle_status_toast<V: ToastView>(
5012        &mut self,
5013        window: &mut Window,
5014        cx: &mut App,
5015        entity: Entity<V>,
5016    ) {
5017        self.toast_layer.update(cx, |toast_layer, cx| {
5018            toast_layer.toggle_toast(window, cx, entity)
5019        })
5020    }
5021
5022    pub fn toggle_centered_layout(
5023        &mut self,
5024        _: &ToggleCenteredLayout,
5025        _: &mut Window,
5026        cx: &mut Context<Self>,
5027    ) {
5028        self.centered_layout = !self.centered_layout;
5029        if let Some(database_id) = self.database_id() {
5030            cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout))
5031                .detach_and_log_err(cx);
5032        }
5033        cx.notify();
5034    }
5035
5036    fn adjust_padding(padding: Option<f32>) -> f32 {
5037        padding
5038            .unwrap_or(Self::DEFAULT_PADDING)
5039            .clamp(0.0, Self::MAX_PADDING)
5040    }
5041
5042    fn render_dock(
5043        &self,
5044        position: DockPosition,
5045        dock: &Entity<Dock>,
5046        window: &mut Window,
5047        cx: &mut App,
5048    ) -> Option<Div> {
5049        if self.zoomed_position == Some(position) {
5050            return None;
5051        }
5052
5053        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
5054            let pane = panel.pane(cx)?;
5055            let follower_states = &self.follower_states;
5056            leader_border_for_pane(follower_states, &pane, window, cx)
5057        });
5058
5059        Some(
5060            div()
5061                .flex()
5062                .flex_none()
5063                .overflow_hidden()
5064                .child(dock.clone())
5065                .children(leader_border),
5066        )
5067    }
5068
5069    pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
5070        window.root().flatten()
5071    }
5072
5073    pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
5074        self.zoomed.as_ref()
5075    }
5076
5077    pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
5078        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5079            return;
5080        };
5081        let windows = cx.windows();
5082        let Some(next_window) = windows
5083            .iter()
5084            .cycle()
5085            .skip_while(|window| window.window_id() != current_window_id)
5086            .nth(1)
5087        else {
5088            return;
5089        };
5090        next_window
5091            .update(cx, |_, window, _| window.activate_window())
5092            .ok();
5093    }
5094
5095    pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5096        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5097            return;
5098        };
5099        let windows = cx.windows();
5100        let Some(prev_window) = windows
5101            .iter()
5102            .rev()
5103            .cycle()
5104            .skip_while(|window| window.window_id() != current_window_id)
5105            .nth(1)
5106        else {
5107            return;
5108        };
5109        prev_window
5110            .update(cx, |_, window, _| window.activate_window())
5111            .ok();
5112    }
5113}
5114
5115fn leader_border_for_pane(
5116    follower_states: &HashMap<PeerId, FollowerState>,
5117    pane: &Entity<Pane>,
5118    _: &Window,
5119    cx: &App,
5120) -> Option<Div> {
5121    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5122        if state.pane() == pane {
5123            Some((*leader_id, state))
5124        } else {
5125            None
5126        }
5127    })?;
5128
5129    let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5130    let leader = room.remote_participant_for_peer_id(leader_id)?;
5131
5132    let mut leader_color = cx
5133        .theme()
5134        .players()
5135        .color_for_participant(leader.participant_index.0)
5136        .cursor;
5137    leader_color.fade_out(0.3);
5138    Some(
5139        div()
5140            .absolute()
5141            .size_full()
5142            .left_0()
5143            .top_0()
5144            .border_2()
5145            .border_color(leader_color),
5146    )
5147}
5148
5149fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5150    ZED_WINDOW_POSITION
5151        .zip(*ZED_WINDOW_SIZE)
5152        .map(|(position, size)| Bounds {
5153            origin: position,
5154            size,
5155        })
5156}
5157
5158fn open_items(
5159    serialized_workspace: Option<SerializedWorkspace>,
5160    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5161    window: &mut Window,
5162    cx: &mut Context<Workspace>,
5163) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
5164    let restored_items = serialized_workspace.map(|serialized_workspace| {
5165        Workspace::load_workspace(
5166            serialized_workspace,
5167            project_paths_to_open
5168                .iter()
5169                .map(|(_, project_path)| project_path)
5170                .cloned()
5171                .collect(),
5172            window,
5173            cx,
5174        )
5175    });
5176
5177    cx.spawn_in(window, |workspace, mut cx| async move {
5178        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5179
5180        if let Some(restored_items) = restored_items {
5181            let restored_items = restored_items.await?;
5182
5183            let restored_project_paths = restored_items
5184                .iter()
5185                .filter_map(|item| {
5186                    cx.update(|_, cx| item.as_ref()?.project_path(cx))
5187                        .ok()
5188                        .flatten()
5189                })
5190                .collect::<HashSet<_>>();
5191
5192            for restored_item in restored_items {
5193                opened_items.push(restored_item.map(Ok));
5194            }
5195
5196            project_paths_to_open
5197                .iter_mut()
5198                .for_each(|(_, project_path)| {
5199                    if let Some(project_path_to_open) = project_path {
5200                        if restored_project_paths.contains(project_path_to_open) {
5201                            *project_path = None;
5202                        }
5203                    }
5204                });
5205        } else {
5206            for _ in 0..project_paths_to_open.len() {
5207                opened_items.push(None);
5208            }
5209        }
5210        assert!(opened_items.len() == project_paths_to_open.len());
5211
5212        let tasks =
5213            project_paths_to_open
5214                .into_iter()
5215                .enumerate()
5216                .map(|(ix, (abs_path, project_path))| {
5217                    let workspace = workspace.clone();
5218                    cx.spawn(|mut cx| async move {
5219                        let file_project_path = project_path?;
5220                        let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
5221                            workspace.project().update(cx, |project, cx| {
5222                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5223                            })
5224                        });
5225
5226                        // We only want to open file paths here. If one of the items
5227                        // here is a directory, it was already opened further above
5228                        // with a `find_or_create_worktree`.
5229                        if let Ok(task) = abs_path_task {
5230                            if task.await.map_or(true, |p| p.is_file()) {
5231                                return Some((
5232                                    ix,
5233                                    workspace
5234                                        .update_in(&mut cx, |workspace, window, cx| {
5235                                            workspace.open_path(
5236                                                file_project_path,
5237                                                None,
5238                                                true,
5239                                                window,
5240                                                cx,
5241                                            )
5242                                        })
5243                                        .log_err()?
5244                                        .await,
5245                                ));
5246                            }
5247                        }
5248                        None
5249                    })
5250                });
5251
5252        let tasks = tasks.collect::<Vec<_>>();
5253
5254        let tasks = futures::future::join_all(tasks);
5255        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5256            opened_items[ix] = Some(path_open_result);
5257        }
5258
5259        Ok(opened_items)
5260    })
5261}
5262
5263enum ActivateInDirectionTarget {
5264    Pane(Entity<Pane>),
5265    Dock(Entity<Dock>),
5266}
5267
5268fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5269    const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=admin+read%2Ctriage%2Cbug&projects=&template=1_bug_report.yml";
5270
5271    workspace
5272        .update(cx, |workspace, _, cx| {
5273            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5274                struct DatabaseFailedNotification;
5275
5276                workspace.show_notification(
5277                    NotificationId::unique::<DatabaseFailedNotification>(),
5278                    cx,
5279                    |cx| {
5280                        cx.new(|cx| {
5281                            MessageNotification::new("Failed to load the database file.", cx)
5282                                .primary_message("File an Issue")
5283                                .primary_icon(IconName::Plus)
5284                                .primary_on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5285                        })
5286                    },
5287                );
5288            }
5289        })
5290        .log_err();
5291}
5292
5293impl Focusable for Workspace {
5294    fn focus_handle(&self, cx: &App) -> FocusHandle {
5295        self.active_pane.focus_handle(cx)
5296    }
5297}
5298
5299#[derive(Clone)]
5300struct DraggedDock(DockPosition);
5301
5302impl Render for DraggedDock {
5303    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5304        gpui::Empty
5305    }
5306}
5307
5308impl Render for Workspace {
5309    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5310        let mut context = KeyContext::new_with_defaults();
5311        context.add("Workspace");
5312        context.set("keyboard_layout", cx.keyboard_layout().clone());
5313        let centered_layout = self.centered_layout
5314            && self.center.panes().len() == 1
5315            && self.active_item(cx).is_some();
5316        let render_padding = |size| {
5317            (size > 0.0).then(|| {
5318                div()
5319                    .h_full()
5320                    .w(relative(size))
5321                    .bg(cx.theme().colors().editor_background)
5322                    .border_color(cx.theme().colors().pane_group_border)
5323            })
5324        };
5325        let paddings = if centered_layout {
5326            let settings = WorkspaceSettings::get_global(cx).centered_layout;
5327            (
5328                render_padding(Self::adjust_padding(settings.left_padding)),
5329                render_padding(Self::adjust_padding(settings.right_padding)),
5330            )
5331        } else {
5332            (None, None)
5333        };
5334        let ui_font = theme::setup_ui_font(window, cx);
5335
5336        let theme = cx.theme().clone();
5337        let colors = theme.colors();
5338
5339        client_side_decorations(
5340            self.actions(div(), window, cx)
5341                .key_context(context)
5342                .relative()
5343                .size_full()
5344                .flex()
5345                .flex_col()
5346                .font(ui_font)
5347                .gap_0()
5348                .justify_start()
5349                .items_start()
5350                .text_color(colors.text)
5351                .overflow_hidden()
5352                .children(self.titlebar_item.clone())
5353                .child(
5354                    div()
5355                        .size_full()
5356                        .relative()
5357                        .flex_1()
5358                        .flex()
5359                        .flex_col()
5360                        .child(
5361                            div()
5362                                .id("workspace")
5363                                .bg(colors.background)
5364                                .relative()
5365                                .flex_1()
5366                                .w_full()
5367                                .flex()
5368                                .flex_col()
5369                                .overflow_hidden()
5370                                .border_t_1()
5371                                .border_b_1()
5372                                .border_color(colors.border)
5373                                .child({
5374                                    let this = cx.entity().clone();
5375                                    canvas(
5376                                        move |bounds, window, cx| {
5377                                            this.update(cx, |this, cx| {
5378                                                let bounds_changed = this.bounds != bounds;
5379                                                this.bounds = bounds;
5380
5381                                                if bounds_changed {
5382                                                    this.left_dock.update(cx, |dock, cx| {
5383                                                        dock.clamp_panel_size(
5384                                                            bounds.size.width,
5385                                                            window,
5386                                                            cx,
5387                                                        )
5388                                                    });
5389
5390                                                    this.right_dock.update(cx, |dock, cx| {
5391                                                        dock.clamp_panel_size(
5392                                                            bounds.size.width,
5393                                                            window,
5394                                                            cx,
5395                                                        )
5396                                                    });
5397
5398                                                    this.bottom_dock.update(cx, |dock, cx| {
5399                                                        dock.clamp_panel_size(
5400                                                            bounds.size.height,
5401                                                            window,
5402                                                            cx,
5403                                                        )
5404                                                    });
5405                                                }
5406                                            })
5407                                        },
5408                                        |_, _, _, _| {},
5409                                    )
5410                                    .absolute()
5411                                    .size_full()
5412                                })
5413                                .when(self.zoomed.is_none(), |this| {
5414                                    this.on_drag_move(cx.listener(
5415                                        move |workspace,
5416                                              e: &DragMoveEvent<DraggedDock>,
5417                                              window,
5418                                              cx| {
5419                                            if workspace.previous_dock_drag_coordinates
5420                                                != Some(e.event.position)
5421                                            {
5422                                                workspace.previous_dock_drag_coordinates =
5423                                                    Some(e.event.position);
5424                                                match e.drag(cx).0 {
5425                                                    DockPosition::Left => {
5426                                                        resize_left_dock(
5427                                                            e.event.position.x
5428                                                                - workspace.bounds.left(),
5429                                                            workspace,
5430                                                            window,
5431                                                            cx,
5432                                                        );
5433                                                    }
5434                                                    DockPosition::Right => {
5435                                                        resize_right_dock(
5436                                                            workspace.bounds.right()
5437                                                                - e.event.position.x,
5438                                                            workspace,
5439                                                            window,
5440                                                            cx,
5441                                                        );
5442                                                    }
5443                                                    DockPosition::Bottom => {
5444                                                        resize_bottom_dock(
5445                                                            workspace.bounds.bottom()
5446                                                                - e.event.position.y,
5447                                                            workspace,
5448                                                            window,
5449                                                            cx,
5450                                                        );
5451                                                    }
5452                                                };
5453                                                workspace.serialize_workspace(window, cx);
5454                                            }
5455                                        },
5456                                    ))
5457                                })
5458                                .child(
5459                                    div()
5460                                        .flex()
5461                                        .flex_row()
5462                                        .h_full()
5463                                        // Left Dock
5464                                        .children(self.render_dock(
5465                                            DockPosition::Left,
5466                                            &self.left_dock,
5467                                            window,
5468                                            cx,
5469                                        ))
5470                                        // Panes
5471                                        .child(
5472                                            div()
5473                                                .flex()
5474                                                .flex_col()
5475                                                .flex_1()
5476                                                .overflow_hidden()
5477                                                .child(
5478                                                    h_flex()
5479                                                        .flex_1()
5480                                                        .when_some(paddings.0, |this, p| {
5481                                                            this.child(p.border_r_1())
5482                                                        })
5483                                                        .child(self.center.render(
5484                                                            &self.project,
5485                                                            &self.follower_states,
5486                                                            self.active_call(),
5487                                                            &self.active_pane,
5488                                                            self.zoomed.as_ref(),
5489                                                            &self.app_state,
5490                                                            window,
5491                                                            cx,
5492                                                        ))
5493                                                        .when_some(paddings.1, |this, p| {
5494                                                            this.child(p.border_l_1())
5495                                                        }),
5496                                                )
5497                                                .children(self.render_dock(
5498                                                    DockPosition::Bottom,
5499                                                    &self.bottom_dock,
5500                                                    window,
5501                                                    cx,
5502                                                )),
5503                                        )
5504                                        // Right Dock
5505                                        .children(self.render_dock(
5506                                            DockPosition::Right,
5507                                            &self.right_dock,
5508                                            window,
5509                                            cx,
5510                                        )),
5511                                )
5512                                .children(self.zoomed.as_ref().and_then(|view| {
5513                                    let zoomed_view = view.upgrade()?;
5514                                    let div = div()
5515                                        .occlude()
5516                                        .absolute()
5517                                        .overflow_hidden()
5518                                        .border_color(colors.border)
5519                                        .bg(colors.background)
5520                                        .child(zoomed_view)
5521                                        .inset_0()
5522                                        .shadow_lg();
5523
5524                                    Some(match self.zoomed_position {
5525                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
5526                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
5527                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5528                                        None => {
5529                                            div.top_2().bottom_2().left_2().right_2().border_1()
5530                                        }
5531                                    })
5532                                }))
5533                                .children(self.render_notifications(window, cx)),
5534                        )
5535                        .child(self.status_bar.clone())
5536                        .child(self.modal_layer.clone())
5537                        .child(self.toast_layer.clone()),
5538                ),
5539            window,
5540            cx,
5541        )
5542    }
5543}
5544
5545fn resize_bottom_dock(
5546    new_size: Pixels,
5547    workspace: &mut Workspace,
5548    window: &mut Window,
5549    cx: &mut App,
5550) {
5551    let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5552    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5553        bottom_dock.resize_active_panel(Some(size), window, cx);
5554    });
5555}
5556
5557fn resize_right_dock(
5558    new_size: Pixels,
5559    workspace: &mut Workspace,
5560    window: &mut Window,
5561    cx: &mut App,
5562) {
5563    let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5564    workspace.right_dock.update(cx, |right_dock, cx| {
5565        right_dock.resize_active_panel(Some(size), window, cx);
5566    });
5567}
5568
5569fn resize_left_dock(
5570    new_size: Pixels,
5571    workspace: &mut Workspace,
5572    window: &mut Window,
5573    cx: &mut App,
5574) {
5575    let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5576
5577    workspace.left_dock.update(cx, |left_dock, cx| {
5578        left_dock.resize_active_panel(Some(size), window, cx);
5579    });
5580}
5581
5582impl WorkspaceStore {
5583    pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5584        Self {
5585            workspaces: Default::default(),
5586            _subscriptions: vec![
5587                client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5588                client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5589            ],
5590            client,
5591        }
5592    }
5593
5594    pub fn update_followers(
5595        &self,
5596        project_id: Option<u64>,
5597        update: proto::update_followers::Variant,
5598        cx: &App,
5599    ) -> Option<()> {
5600        let active_call = ActiveCall::try_global(cx)?;
5601        let room_id = active_call.read(cx).room()?.read(cx).id();
5602        self.client
5603            .send(proto::UpdateFollowers {
5604                room_id,
5605                project_id,
5606                variant: Some(update),
5607            })
5608            .log_err()
5609    }
5610
5611    pub async fn handle_follow(
5612        this: Entity<Self>,
5613        envelope: TypedEnvelope<proto::Follow>,
5614        mut cx: AsyncApp,
5615    ) -> Result<proto::FollowResponse> {
5616        this.update(&mut cx, |this, cx| {
5617            let follower = Follower {
5618                project_id: envelope.payload.project_id,
5619                peer_id: envelope.original_sender_id()?,
5620            };
5621
5622            let mut response = proto::FollowResponse::default();
5623            this.workspaces.retain(|workspace| {
5624                workspace
5625                    .update(cx, |workspace, window, cx| {
5626                        let handler_response =
5627                            workspace.handle_follow(follower.project_id, window, cx);
5628                        if let Some(active_view) = handler_response.active_view.clone() {
5629                            if workspace.project.read(cx).remote_id() == follower.project_id {
5630                                response.active_view = Some(active_view)
5631                            }
5632                        }
5633                    })
5634                    .is_ok()
5635            });
5636
5637            Ok(response)
5638        })?
5639    }
5640
5641    async fn handle_update_followers(
5642        this: Entity<Self>,
5643        envelope: TypedEnvelope<proto::UpdateFollowers>,
5644        mut cx: AsyncApp,
5645    ) -> Result<()> {
5646        let leader_id = envelope.original_sender_id()?;
5647        let update = envelope.payload;
5648
5649        this.update(&mut cx, |this, cx| {
5650            this.workspaces.retain(|workspace| {
5651                workspace
5652                    .update(cx, |workspace, window, cx| {
5653                        let project_id = workspace.project.read(cx).remote_id();
5654                        if update.project_id != project_id && update.project_id.is_some() {
5655                            return;
5656                        }
5657                        workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5658                    })
5659                    .is_ok()
5660            });
5661            Ok(())
5662        })?
5663    }
5664}
5665
5666impl ViewId {
5667    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5668        Ok(Self {
5669            creator: message
5670                .creator
5671                .ok_or_else(|| anyhow!("creator is missing"))?,
5672            id: message.id,
5673        })
5674    }
5675
5676    pub(crate) fn to_proto(self) -> proto::ViewId {
5677        proto::ViewId {
5678            creator: Some(self.creator),
5679            id: self.id,
5680        }
5681    }
5682}
5683
5684impl FollowerState {
5685    fn pane(&self) -> &Entity<Pane> {
5686        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5687    }
5688}
5689
5690pub trait WorkspaceHandle {
5691    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5692}
5693
5694impl WorkspaceHandle for Entity<Workspace> {
5695    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5696        self.read(cx)
5697            .worktrees(cx)
5698            .flat_map(|worktree| {
5699                let worktree_id = worktree.read(cx).id();
5700                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5701                    worktree_id,
5702                    path: f.path.clone(),
5703                })
5704            })
5705            .collect::<Vec<_>>()
5706    }
5707}
5708
5709impl std::fmt::Debug for OpenPaths {
5710    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5711        f.debug_struct("OpenPaths")
5712            .field("paths", &self.paths)
5713            .finish()
5714    }
5715}
5716
5717pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5718    DB.last_workspace().await.log_err().flatten()
5719}
5720
5721pub fn last_session_workspace_locations(
5722    last_session_id: &str,
5723    last_session_window_stack: Option<Vec<WindowId>>,
5724) -> Option<Vec<SerializedWorkspaceLocation>> {
5725    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5726        .log_err()
5727}
5728
5729actions!(collab, [OpenChannelNotes]);
5730actions!(zed, [OpenLog]);
5731
5732async fn join_channel_internal(
5733    channel_id: ChannelId,
5734    app_state: &Arc<AppState>,
5735    requesting_window: Option<WindowHandle<Workspace>>,
5736    active_call: &Entity<ActiveCall>,
5737    cx: &mut AsyncApp,
5738) -> Result<bool> {
5739    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5740        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5741            return (false, None);
5742        };
5743
5744        let already_in_channel = room.channel_id() == Some(channel_id);
5745        let should_prompt = room.is_sharing_project()
5746            && !room.remote_participants().is_empty()
5747            && !already_in_channel;
5748        let open_room = if already_in_channel {
5749            active_call.room().cloned()
5750        } else {
5751            None
5752        };
5753        (should_prompt, open_room)
5754    })?;
5755
5756    if let Some(room) = open_room {
5757        let task = room.update(cx, |room, cx| {
5758            if let Some((project, host)) = room.most_active_project(cx) {
5759                return Some(join_in_room_project(project, host, app_state.clone(), cx));
5760            }
5761
5762            None
5763        })?;
5764        if let Some(task) = task {
5765            task.await?;
5766        }
5767        return anyhow::Ok(true);
5768    }
5769
5770    if should_prompt {
5771        if let Some(workspace) = requesting_window {
5772            let answer = workspace
5773                .update(cx, |_, window, cx| {
5774                    window.prompt(
5775                        PromptLevel::Warning,
5776                        "Do you want to switch channels?",
5777                        Some("Leaving this call will unshare your current project."),
5778                        &["Yes, Join Channel", "Cancel"],
5779                        cx,
5780                    )
5781                })?
5782                .await;
5783
5784            if answer == Ok(1) {
5785                return Ok(false);
5786            }
5787        } else {
5788            return Ok(false); // unreachable!() hopefully
5789        }
5790    }
5791
5792    let client = cx.update(|cx| active_call.read(cx).client())?;
5793
5794    let mut client_status = client.status();
5795
5796    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5797    'outer: loop {
5798        let Some(status) = client_status.recv().await else {
5799            return Err(anyhow!("error connecting"));
5800        };
5801
5802        match status {
5803            Status::Connecting
5804            | Status::Authenticating
5805            | Status::Reconnecting
5806            | Status::Reauthenticating => continue,
5807            Status::Connected { .. } => break 'outer,
5808            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5809            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5810            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5811                return Err(ErrorCode::Disconnected.into());
5812            }
5813        }
5814    }
5815
5816    let room = active_call
5817        .update(cx, |active_call, cx| {
5818            active_call.join_channel(channel_id, cx)
5819        })?
5820        .await?;
5821
5822    let Some(room) = room else {
5823        return anyhow::Ok(true);
5824    };
5825
5826    room.update(cx, |room, _| room.room_update_completed())?
5827        .await;
5828
5829    let task = room.update(cx, |room, cx| {
5830        if let Some((project, host)) = room.most_active_project(cx) {
5831            return Some(join_in_room_project(project, host, app_state.clone(), cx));
5832        }
5833
5834        // If you are the first to join a channel, see if you should share your project.
5835        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5836            if let Some(workspace) = requesting_window {
5837                let project = workspace.update(cx, |workspace, _, cx| {
5838                    let project = workspace.project.read(cx);
5839
5840                    if !CallSettings::get_global(cx).share_on_join {
5841                        return None;
5842                    }
5843
5844                    if (project.is_local() || project.is_via_ssh())
5845                        && project.visible_worktrees(cx).any(|tree| {
5846                            tree.read(cx)
5847                                .root_entry()
5848                                .map_or(false, |entry| entry.is_dir())
5849                        })
5850                    {
5851                        Some(workspace.project.clone())
5852                    } else {
5853                        None
5854                    }
5855                });
5856                if let Ok(Some(project)) = project {
5857                    return Some(cx.spawn(|room, mut cx| async move {
5858                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5859                            .await?;
5860                        Ok(())
5861                    }));
5862                }
5863            }
5864        }
5865
5866        None
5867    })?;
5868    if let Some(task) = task {
5869        task.await?;
5870        return anyhow::Ok(true);
5871    }
5872    anyhow::Ok(false)
5873}
5874
5875pub fn join_channel(
5876    channel_id: ChannelId,
5877    app_state: Arc<AppState>,
5878    requesting_window: Option<WindowHandle<Workspace>>,
5879    cx: &mut App,
5880) -> Task<Result<()>> {
5881    let active_call = ActiveCall::global(cx);
5882    cx.spawn(|mut cx| async move {
5883        let result = join_channel_internal(
5884            channel_id,
5885            &app_state,
5886            requesting_window,
5887            &active_call,
5888            &mut cx,
5889        )
5890            .await;
5891
5892        // join channel succeeded, and opened a window
5893        if matches!(result, Ok(true)) {
5894            return anyhow::Ok(());
5895        }
5896
5897        // find an existing workspace to focus and show call controls
5898        let mut active_window =
5899            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5900        if active_window.is_none() {
5901            // no open workspaces, make one to show the error in (blergh)
5902            let (window_handle, _) = cx
5903                .update(|cx| {
5904                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5905                })?
5906                .await?;
5907
5908            if result.is_ok() {
5909                cx.update(|cx| {
5910                    cx.dispatch_action(&OpenChannelNotes);
5911                }).log_err();
5912            }
5913
5914            active_window = Some(window_handle);
5915        }
5916
5917        if let Err(err) = result {
5918            log::error!("failed to join channel: {}", err);
5919            if let Some(active_window) = active_window {
5920                active_window
5921                    .update(&mut cx, |_, window, cx| {
5922                        let detail: SharedString = match err.error_code() {
5923                            ErrorCode::SignedOut => {
5924                                "Please sign in to continue.".into()
5925                            }
5926                            ErrorCode::UpgradeRequired => {
5927                                "Your are running an unsupported version of Zed. Please update to continue.".into()
5928                            }
5929                            ErrorCode::NoSuchChannel => {
5930                                "No matching channel was found. Please check the link and try again.".into()
5931                            }
5932                            ErrorCode::Forbidden => {
5933                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5934                            }
5935                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5936                            _ => format!("{}\n\nPlease try again.", err).into(),
5937                        };
5938                        window.prompt(
5939                            PromptLevel::Critical,
5940                            "Failed to join channel",
5941                            Some(&detail),
5942                            &["Ok"],
5943                        cx)
5944                    })?
5945                    .await
5946                    .ok();
5947            }
5948        }
5949
5950        // return ok, we showed the error to the user.
5951        anyhow::Ok(())
5952    })
5953}
5954
5955pub async fn get_any_active_workspace(
5956    app_state: Arc<AppState>,
5957    mut cx: AsyncApp,
5958) -> anyhow::Result<WindowHandle<Workspace>> {
5959    // find an existing workspace to focus and show call controls
5960    let active_window = activate_any_workspace_window(&mut cx);
5961    if active_window.is_none() {
5962        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5963            .await?;
5964    }
5965    activate_any_workspace_window(&mut cx).context("could not open zed")
5966}
5967
5968fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
5969    cx.update(|cx| {
5970        if let Some(workspace_window) = cx
5971            .active_window()
5972            .and_then(|window| window.downcast::<Workspace>())
5973        {
5974            return Some(workspace_window);
5975        }
5976
5977        for window in cx.windows() {
5978            if let Some(workspace_window) = window.downcast::<Workspace>() {
5979                workspace_window
5980                    .update(cx, |_, window, _| window.activate_window())
5981                    .ok();
5982                return Some(workspace_window);
5983            }
5984        }
5985        None
5986    })
5987    .ok()
5988    .flatten()
5989}
5990
5991pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
5992    cx.windows()
5993        .into_iter()
5994        .filter_map(|window| window.downcast::<Workspace>())
5995        .filter(|workspace| {
5996            workspace
5997                .read(cx)
5998                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5999        })
6000        .collect()
6001}
6002
6003#[derive(Default)]
6004pub struct OpenOptions {
6005    pub open_new_workspace: Option<bool>,
6006    pub replace_window: Option<WindowHandle<Workspace>>,
6007    pub env: Option<HashMap<String, String>>,
6008}
6009#[allow(clippy::type_complexity)]
6010pub fn open_paths(
6011    abs_paths: &[PathBuf],
6012    app_state: Arc<AppState>,
6013    open_options: OpenOptions,
6014    cx: &mut App,
6015) -> Task<
6016    anyhow::Result<(
6017        WindowHandle<Workspace>,
6018        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
6019    )>,
6020> {
6021    let abs_paths = abs_paths.to_vec();
6022    let mut existing = None;
6023    let mut best_match = None;
6024    let mut open_visible = OpenVisible::All;
6025
6026    cx.spawn(move |mut cx| async move {
6027        if open_options.open_new_workspace != Some(true) {
6028            let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path));
6029            let all_metadatas = futures::future::join_all(all_paths)
6030                .await
6031                .into_iter()
6032                .filter_map(|result| result.ok().flatten())
6033                .collect::<Vec<_>>();
6034
6035            cx.update(|cx| {
6036                for window in local_workspace_windows(&cx) {
6037                    if let Ok(workspace) = window.read(&cx) {
6038                        let m = workspace.project.read(&cx).visibility_for_paths(
6039                            &abs_paths,
6040                            &all_metadatas,
6041                            open_options.open_new_workspace == None,
6042                            cx,
6043                        );
6044                        if m > best_match {
6045                            existing = Some(window);
6046                            best_match = m;
6047                        } else if best_match.is_none()
6048                            && open_options.open_new_workspace == Some(false)
6049                        {
6050                            existing = Some(window)
6051                        }
6052                    }
6053                }
6054            })?;
6055
6056            if open_options.open_new_workspace.is_none() && existing.is_none() {
6057                if all_metadatas.iter().all(|file| !file.is_dir) {
6058                    cx.update(|cx| {
6059                        if let Some(window) = cx
6060                            .active_window()
6061                            .and_then(|window| window.downcast::<Workspace>())
6062                        {
6063                            if let Ok(workspace) = window.read(cx) {
6064                                let project = workspace.project().read(cx);
6065                                if project.is_local() && !project.is_via_collab() {
6066                                    existing = Some(window);
6067                                    open_visible = OpenVisible::None;
6068                                    return;
6069                                }
6070                            }
6071                        }
6072                        for window in local_workspace_windows(cx) {
6073                            if let Ok(workspace) = window.read(cx) {
6074                                let project = workspace.project().read(cx);
6075                                if project.is_via_collab() {
6076                                    continue;
6077                                }
6078                                existing = Some(window);
6079                                open_visible = OpenVisible::None;
6080                                break;
6081                            }
6082                        }
6083                    })?;
6084                }
6085            }
6086        }
6087
6088        if let Some(existing) = existing {
6089            let open_task = existing
6090                .update(&mut cx, |workspace, window, cx| {
6091                    window.activate_window();
6092                    workspace.open_paths(abs_paths, open_visible, None, window, cx)
6093                })?
6094                .await;
6095
6096            _ = existing.update(&mut cx, |workspace, _, cx| {
6097                for item in open_task.iter().flatten() {
6098                    if let Err(e) = item {
6099                        workspace.show_error(&e, cx);
6100                    }
6101                }
6102            });
6103
6104            Ok((existing, open_task))
6105        } else {
6106            cx.update(move |cx| {
6107                Workspace::new_local(
6108                    abs_paths,
6109                    app_state.clone(),
6110                    open_options.replace_window,
6111                    open_options.env,
6112                    cx,
6113                )
6114            })?
6115            .await
6116        }
6117    })
6118}
6119
6120pub fn open_new(
6121    open_options: OpenOptions,
6122    app_state: Arc<AppState>,
6123    cx: &mut App,
6124    init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6125) -> Task<anyhow::Result<()>> {
6126    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6127    cx.spawn(|mut cx| async move {
6128        let (workspace, opened_paths) = task.await?;
6129        workspace.update(&mut cx, |workspace, window, cx| {
6130            if opened_paths.is_empty() {
6131                init(workspace, window, cx)
6132            }
6133        })?;
6134        Ok(())
6135    })
6136}
6137
6138pub fn create_and_open_local_file(
6139    path: &'static Path,
6140    window: &mut Window,
6141    cx: &mut Context<Workspace>,
6142    default_content: impl 'static + Send + FnOnce() -> Rope,
6143) -> Task<Result<Box<dyn ItemHandle>>> {
6144    cx.spawn_in(window, |workspace, mut cx| async move {
6145        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6146        if !fs.is_file(path).await {
6147            fs.create_file(path, Default::default()).await?;
6148            fs.save(path, &default_content(), Default::default())
6149                .await?;
6150        }
6151
6152        let mut items = workspace
6153            .update_in(&mut cx, |workspace, window, cx| {
6154                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6155                    workspace.open_paths(
6156                        vec![path.to_path_buf()],
6157                        OpenVisible::None,
6158                        None,
6159                        window,
6160                        cx,
6161                    )
6162                })
6163            })?
6164            .await?
6165            .await;
6166
6167        let item = items.pop().flatten();
6168        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6169    })
6170}
6171
6172pub fn open_ssh_project(
6173    window: WindowHandle<Workspace>,
6174    connection_options: SshConnectionOptions,
6175    cancel_rx: oneshot::Receiver<()>,
6176    delegate: Arc<dyn SshClientDelegate>,
6177    app_state: Arc<AppState>,
6178    paths: Vec<PathBuf>,
6179    cx: &mut App,
6180) -> Task<Result<()>> {
6181    cx.spawn(|mut cx| async move {
6182        let (serialized_ssh_project, workspace_id, serialized_workspace) =
6183            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6184
6185        let session = match cx
6186            .update(|cx| {
6187                remote::SshRemoteClient::new(
6188                    ConnectionIdentifier::Workspace(workspace_id.0),
6189                    connection_options,
6190                    cancel_rx,
6191                    delegate,
6192                    cx,
6193                )
6194            })?
6195            .await?
6196        {
6197            Some(result) => result,
6198            None => return Ok(()),
6199        };
6200
6201        let project = cx.update(|cx| {
6202            project::Project::ssh(
6203                session,
6204                app_state.client.clone(),
6205                app_state.node_runtime.clone(),
6206                app_state.user_store.clone(),
6207                app_state.languages.clone(),
6208                app_state.fs.clone(),
6209                cx,
6210            )
6211        })?;
6212
6213        let toolchains = DB.toolchains(workspace_id).await?;
6214        for (toolchain, worktree_id) in toolchains {
6215            project
6216                .update(&mut cx, |this, cx| {
6217                    this.activate_toolchain(worktree_id, toolchain, cx)
6218                })?
6219                .await;
6220        }
6221        let mut project_paths_to_open = vec![];
6222        let mut project_path_errors = vec![];
6223
6224        for path in paths {
6225            let result = cx
6226                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6227                .await;
6228            match result {
6229                Ok((_, project_path)) => {
6230                    project_paths_to_open.push((path.clone(), Some(project_path)));
6231                }
6232                Err(error) => {
6233                    project_path_errors.push(error);
6234                }
6235            };
6236        }
6237
6238        if project_paths_to_open.is_empty() {
6239            return Err(project_path_errors
6240                .pop()
6241                .unwrap_or_else(|| anyhow!("no paths given")));
6242        }
6243
6244        cx.update_window(window.into(), |_, window, cx| {
6245            window.replace_root(cx, |window, cx| {
6246                telemetry::event!("SSH Project Opened");
6247
6248                let mut workspace =
6249                    Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6250                workspace.set_serialized_ssh_project(serialized_ssh_project);
6251                workspace
6252            });
6253        })?;
6254
6255        window
6256            .update(&mut cx, |_, window, cx| {
6257                window.activate_window();
6258
6259                open_items(serialized_workspace, project_paths_to_open, window, cx)
6260            })?
6261            .await?;
6262
6263        window.update(&mut cx, |workspace, _, cx| {
6264            for error in project_path_errors {
6265                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6266                    if let Some(path) = error.error_tag("path") {
6267                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6268                    }
6269                } else {
6270                    workspace.show_error(&error, cx)
6271                }
6272            }
6273        })
6274    })
6275}
6276
6277fn serialize_ssh_project(
6278    connection_options: SshConnectionOptions,
6279    paths: Vec<PathBuf>,
6280    cx: &AsyncApp,
6281) -> Task<
6282    Result<(
6283        SerializedSshProject,
6284        WorkspaceId,
6285        Option<SerializedWorkspace>,
6286    )>,
6287> {
6288    cx.background_spawn(async move {
6289        let serialized_ssh_project = persistence::DB
6290            .get_or_create_ssh_project(
6291                connection_options.host.clone(),
6292                connection_options.port,
6293                paths
6294                    .iter()
6295                    .map(|path| path.to_string_lossy().to_string())
6296                    .collect::<Vec<_>>(),
6297                connection_options.username.clone(),
6298            )
6299            .await?;
6300
6301        let serialized_workspace =
6302            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6303
6304        let workspace_id = if let Some(workspace_id) =
6305            serialized_workspace.as_ref().map(|workspace| workspace.id)
6306        {
6307            workspace_id
6308        } else {
6309            persistence::DB.next_id().await?
6310        };
6311
6312        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6313    })
6314}
6315
6316pub fn join_in_room_project(
6317    project_id: u64,
6318    follow_user_id: u64,
6319    app_state: Arc<AppState>,
6320    cx: &mut App,
6321) -> Task<Result<()>> {
6322    let windows = cx.windows();
6323    cx.spawn(|mut cx| async move {
6324        let existing_workspace = windows.into_iter().find_map(|window_handle| {
6325            window_handle
6326                .downcast::<Workspace>()
6327                .and_then(|window_handle| {
6328                    window_handle
6329                        .update(&mut cx, |workspace, _window, cx| {
6330                            if workspace.project().read(cx).remote_id() == Some(project_id) {
6331                                Some(window_handle)
6332                            } else {
6333                                None
6334                            }
6335                        })
6336                        .unwrap_or(None)
6337                })
6338        });
6339
6340        let workspace = if let Some(existing_workspace) = existing_workspace {
6341            existing_workspace
6342        } else {
6343            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6344            let room = active_call
6345                .read_with(&cx, |call, _| call.room().cloned())?
6346                .ok_or_else(|| anyhow!("not in a call"))?;
6347            let project = room
6348                .update(&mut cx, |room, cx| {
6349                    room.join_project(
6350                        project_id,
6351                        app_state.languages.clone(),
6352                        app_state.fs.clone(),
6353                        cx,
6354                    )
6355                })?
6356                .await?;
6357
6358            let window_bounds_override = window_bounds_env_override();
6359            cx.update(|cx| {
6360                let mut options = (app_state.build_window_options)(None, cx);
6361                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6362                cx.open_window(options, |window, cx| {
6363                    cx.new(|cx| {
6364                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6365                    })
6366                })
6367            })??
6368        };
6369
6370        workspace.update(&mut cx, |workspace, window, cx| {
6371            cx.activate(true);
6372            window.activate_window();
6373
6374            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6375                let follow_peer_id = room
6376                    .read(cx)
6377                    .remote_participants()
6378                    .iter()
6379                    .find(|(_, participant)| participant.user.id == follow_user_id)
6380                    .map(|(_, p)| p.peer_id)
6381                    .or_else(|| {
6382                        // If we couldn't follow the given user, follow the host instead.
6383                        let collaborator = workspace
6384                            .project()
6385                            .read(cx)
6386                            .collaborators()
6387                            .values()
6388                            .find(|collaborator| collaborator.is_host)?;
6389                        Some(collaborator.peer_id)
6390                    });
6391
6392                if let Some(follow_peer_id) = follow_peer_id {
6393                    workspace.follow(follow_peer_id, window, cx);
6394                }
6395            }
6396        })?;
6397
6398        anyhow::Ok(())
6399    })
6400}
6401
6402pub fn reload(reload: &Reload, cx: &mut App) {
6403    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6404    let mut workspace_windows = cx
6405        .windows()
6406        .into_iter()
6407        .filter_map(|window| window.downcast::<Workspace>())
6408        .collect::<Vec<_>>();
6409
6410    // If multiple windows have unsaved changes, and need a save prompt,
6411    // prompt in the active window before switching to a different window.
6412    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6413
6414    let mut prompt = None;
6415    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6416        prompt = window
6417            .update(cx, |_, window, cx| {
6418                window.prompt(
6419                    PromptLevel::Info,
6420                    "Are you sure you want to restart?",
6421                    None,
6422                    &["Restart", "Cancel"],
6423                    cx,
6424                )
6425            })
6426            .ok();
6427    }
6428
6429    let binary_path = reload.binary_path.clone();
6430    cx.spawn(|mut cx| async move {
6431        if let Some(prompt) = prompt {
6432            let answer = prompt.await?;
6433            if answer != 0 {
6434                return Ok(());
6435            }
6436        }
6437
6438        // If the user cancels any save prompt, then keep the app open.
6439        for window in workspace_windows {
6440            if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6441                workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6442            }) {
6443                if !should_close.await? {
6444                    return Ok(());
6445                }
6446            }
6447        }
6448
6449        cx.update(|cx| cx.restart(binary_path))
6450    })
6451    .detach_and_log_err(cx);
6452}
6453
6454fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6455    let mut parts = value.split(',');
6456    let x: usize = parts.next()?.parse().ok()?;
6457    let y: usize = parts.next()?.parse().ok()?;
6458    Some(point(px(x as f32), px(y as f32)))
6459}
6460
6461fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6462    let mut parts = value.split(',');
6463    let width: usize = parts.next()?.parse().ok()?;
6464    let height: usize = parts.next()?.parse().ok()?;
6465    Some(size(px(width as f32), px(height as f32)))
6466}
6467
6468pub fn client_side_decorations(
6469    element: impl IntoElement,
6470    window: &mut Window,
6471    cx: &mut App,
6472) -> Stateful<Div> {
6473    const BORDER_SIZE: Pixels = px(1.0);
6474    let decorations = window.window_decorations();
6475
6476    if matches!(decorations, Decorations::Client { .. }) {
6477        window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6478    }
6479
6480    struct GlobalResizeEdge(ResizeEdge);
6481    impl Global for GlobalResizeEdge {}
6482
6483    div()
6484        .id("window-backdrop")
6485        .bg(transparent_black())
6486        .map(|div| match decorations {
6487            Decorations::Server => div,
6488            Decorations::Client { tiling, .. } => div
6489                .when(!(tiling.top || tiling.right), |div| {
6490                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6491                })
6492                .when(!(tiling.top || tiling.left), |div| {
6493                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6494                })
6495                .when(!(tiling.bottom || tiling.right), |div| {
6496                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6497                })
6498                .when(!(tiling.bottom || tiling.left), |div| {
6499                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6500                })
6501                .when(!tiling.top, |div| {
6502                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6503                })
6504                .when(!tiling.bottom, |div| {
6505                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6506                })
6507                .when(!tiling.left, |div| {
6508                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6509                })
6510                .when(!tiling.right, |div| {
6511                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6512                })
6513                .on_mouse_move(move |e, window, cx| {
6514                    let size = window.window_bounds().get_bounds().size;
6515                    let pos = e.position;
6516
6517                    let new_edge =
6518                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6519
6520                    let edge = cx.try_global::<GlobalResizeEdge>();
6521                    if new_edge != edge.map(|edge| edge.0) {
6522                        window
6523                            .window_handle()
6524                            .update(cx, |workspace, _, cx| {
6525                                cx.notify(workspace.entity_id());
6526                            })
6527                            .ok();
6528                    }
6529                })
6530                .on_mouse_down(MouseButton::Left, move |e, window, _| {
6531                    let size = window.window_bounds().get_bounds().size;
6532                    let pos = e.position;
6533
6534                    let edge = match resize_edge(
6535                        pos,
6536                        theme::CLIENT_SIDE_DECORATION_SHADOW,
6537                        size,
6538                        tiling,
6539                    ) {
6540                        Some(value) => value,
6541                        None => return,
6542                    };
6543
6544                    window.start_window_resize(edge);
6545                }),
6546        })
6547        .size_full()
6548        .child(
6549            div()
6550                .cursor(CursorStyle::Arrow)
6551                .map(|div| match decorations {
6552                    Decorations::Server => div,
6553                    Decorations::Client { tiling } => div
6554                        .border_color(cx.theme().colors().border)
6555                        .when(!(tiling.top || tiling.right), |div| {
6556                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6557                        })
6558                        .when(!(tiling.top || tiling.left), |div| {
6559                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6560                        })
6561                        .when(!(tiling.bottom || tiling.right), |div| {
6562                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6563                        })
6564                        .when(!(tiling.bottom || tiling.left), |div| {
6565                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6566                        })
6567                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6568                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6569                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6570                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6571                        .when(!tiling.is_tiled(), |div| {
6572                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
6573                                color: Hsla {
6574                                    h: 0.,
6575                                    s: 0.,
6576                                    l: 0.,
6577                                    a: 0.4,
6578                                },
6579                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6580                                spread_radius: px(0.),
6581                                offset: point(px(0.0), px(0.0)),
6582                            }])
6583                        }),
6584                })
6585                .on_mouse_move(|_e, _, cx| {
6586                    cx.stop_propagation();
6587                })
6588                .size_full()
6589                .child(element),
6590        )
6591        .map(|div| match decorations {
6592            Decorations::Server => div,
6593            Decorations::Client { tiling, .. } => div.child(
6594                canvas(
6595                    |_bounds, window, _| {
6596                        window.insert_hitbox(
6597                            Bounds::new(
6598                                point(px(0.0), px(0.0)),
6599                                window.window_bounds().get_bounds().size,
6600                            ),
6601                            false,
6602                        )
6603                    },
6604                    move |_bounds, hitbox, window, cx| {
6605                        let mouse = window.mouse_position();
6606                        let size = window.window_bounds().get_bounds().size;
6607                        let Some(edge) =
6608                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6609                        else {
6610                            return;
6611                        };
6612                        cx.set_global(GlobalResizeEdge(edge));
6613                        window.set_cursor_style(
6614                            match edge {
6615                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6616                                ResizeEdge::Left | ResizeEdge::Right => {
6617                                    CursorStyle::ResizeLeftRight
6618                                }
6619                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6620                                    CursorStyle::ResizeUpLeftDownRight
6621                                }
6622                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6623                                    CursorStyle::ResizeUpRightDownLeft
6624                                }
6625                            },
6626                            &hitbox,
6627                        );
6628                    },
6629                )
6630                .size_full()
6631                .absolute(),
6632            ),
6633        })
6634}
6635
6636fn resize_edge(
6637    pos: Point<Pixels>,
6638    shadow_size: Pixels,
6639    window_size: Size<Pixels>,
6640    tiling: Tiling,
6641) -> Option<ResizeEdge> {
6642    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6643    if bounds.contains(&pos) {
6644        return None;
6645    }
6646
6647    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6648    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6649    if !tiling.top && top_left_bounds.contains(&pos) {
6650        return Some(ResizeEdge::TopLeft);
6651    }
6652
6653    let top_right_bounds = Bounds::new(
6654        Point::new(window_size.width - corner_size.width, px(0.)),
6655        corner_size,
6656    );
6657    if !tiling.top && top_right_bounds.contains(&pos) {
6658        return Some(ResizeEdge::TopRight);
6659    }
6660
6661    let bottom_left_bounds = Bounds::new(
6662        Point::new(px(0.), window_size.height - corner_size.height),
6663        corner_size,
6664    );
6665    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6666        return Some(ResizeEdge::BottomLeft);
6667    }
6668
6669    let bottom_right_bounds = Bounds::new(
6670        Point::new(
6671            window_size.width - corner_size.width,
6672            window_size.height - corner_size.height,
6673        ),
6674        corner_size,
6675    );
6676    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6677        return Some(ResizeEdge::BottomRight);
6678    }
6679
6680    if !tiling.top && pos.y < shadow_size {
6681        Some(ResizeEdge::Top)
6682    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6683        Some(ResizeEdge::Bottom)
6684    } else if !tiling.left && pos.x < shadow_size {
6685        Some(ResizeEdge::Left)
6686    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6687        Some(ResizeEdge::Right)
6688    } else {
6689        None
6690    }
6691}
6692
6693fn join_pane_into_active(
6694    active_pane: &Entity<Pane>,
6695    pane: &Entity<Pane>,
6696    window: &mut Window,
6697    cx: &mut App,
6698) {
6699    if pane == active_pane {
6700        return;
6701    } else if pane.read(cx).items_len() == 0 {
6702        pane.update(cx, |_, cx| {
6703            cx.emit(pane::Event::Remove {
6704                focus_on_pane: None,
6705            });
6706        })
6707    } else {
6708        move_all_items(pane, active_pane, window, cx);
6709    }
6710}
6711
6712fn move_all_items(
6713    from_pane: &Entity<Pane>,
6714    to_pane: &Entity<Pane>,
6715    window: &mut Window,
6716    cx: &mut App,
6717) {
6718    let destination_is_different = from_pane != to_pane;
6719    let mut moved_items = 0;
6720    for (item_ix, item_handle) in from_pane
6721        .read(cx)
6722        .items()
6723        .enumerate()
6724        .map(|(ix, item)| (ix, item.clone()))
6725        .collect::<Vec<_>>()
6726    {
6727        let ix = item_ix - moved_items;
6728        if destination_is_different {
6729            // Close item from previous pane
6730            from_pane.update(cx, |source, cx| {
6731                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6732            });
6733            moved_items += 1;
6734        }
6735
6736        // This automatically removes duplicate items in the pane
6737        to_pane.update(cx, |destination, cx| {
6738            destination.add_item(item_handle, true, true, None, window, cx);
6739            window.focus(&destination.focus_handle(cx))
6740        });
6741    }
6742}
6743
6744pub fn move_item(
6745    source: &Entity<Pane>,
6746    destination: &Entity<Pane>,
6747    item_id_to_move: EntityId,
6748    destination_index: usize,
6749    window: &mut Window,
6750    cx: &mut App,
6751) {
6752    let Some((item_ix, item_handle)) = source
6753        .read(cx)
6754        .items()
6755        .enumerate()
6756        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6757        .map(|(ix, item)| (ix, item.clone()))
6758    else {
6759        // Tab was closed during drag
6760        return;
6761    };
6762
6763    if source != destination {
6764        // Close item from previous pane
6765        source.update(cx, |source, cx| {
6766            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6767        });
6768    }
6769
6770    // This automatically removes duplicate items in the pane
6771    destination.update(cx, |destination, cx| {
6772        destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6773        window.focus(&destination.focus_handle(cx))
6774    });
6775}
6776
6777pub fn move_active_item(
6778    source: &Entity<Pane>,
6779    destination: &Entity<Pane>,
6780    focus_destination: bool,
6781    close_if_empty: bool,
6782    window: &mut Window,
6783    cx: &mut App,
6784) {
6785    if source == destination {
6786        return;
6787    }
6788    let Some(active_item) = source.read(cx).active_item() else {
6789        return;
6790    };
6791    source.update(cx, |source_pane, cx| {
6792        let item_id = active_item.item_id();
6793        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6794        destination.update(cx, |target_pane, cx| {
6795            target_pane.add_item(
6796                active_item,
6797                focus_destination,
6798                focus_destination,
6799                Some(target_pane.items_len()),
6800                window,
6801                cx,
6802            );
6803        });
6804    });
6805}
6806
6807#[cfg(test)]
6808mod tests {
6809    use std::{cell::RefCell, rc::Rc};
6810
6811    use super::*;
6812    use crate::{
6813        dock::{test::TestPanel, PanelEvent},
6814        item::{
6815            test::{TestItem, TestProjectItem},
6816            ItemEvent,
6817        },
6818    };
6819    use fs::FakeFs;
6820    use gpui::{
6821        px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6822        UpdateGlobal, VisualTestContext,
6823    };
6824    use project::{Project, ProjectEntryId};
6825    use serde_json::json;
6826    use settings::SettingsStore;
6827
6828    #[gpui::test]
6829    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6830        init_test(cx);
6831
6832        let fs = FakeFs::new(cx.executor());
6833        let project = Project::test(fs, [], cx).await;
6834        let (workspace, cx) =
6835            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6836
6837        // Adding an item with no ambiguity renders the tab without detail.
6838        let item1 = cx.new(|cx| {
6839            let mut item = TestItem::new(cx);
6840            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6841            item
6842        });
6843        workspace.update_in(cx, |workspace, window, cx| {
6844            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6845        });
6846        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6847
6848        // Adding an item that creates ambiguity increases the level of detail on
6849        // both tabs.
6850        let item2 = cx.new_window_entity(|_window, cx| {
6851            let mut item = TestItem::new(cx);
6852            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6853            item
6854        });
6855        workspace.update_in(cx, |workspace, window, cx| {
6856            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6857        });
6858        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6859        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6860
6861        // Adding an item that creates ambiguity increases the level of detail only
6862        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6863        // we stop at the highest detail available.
6864        let item3 = cx.new(|cx| {
6865            let mut item = TestItem::new(cx);
6866            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6867            item
6868        });
6869        workspace.update_in(cx, |workspace, window, cx| {
6870            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6871        });
6872        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6873        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6874        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6875    }
6876
6877    #[gpui::test]
6878    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6879        init_test(cx);
6880
6881        let fs = FakeFs::new(cx.executor());
6882        fs.insert_tree(
6883            "/root1",
6884            json!({
6885                "one.txt": "",
6886                "two.txt": "",
6887            }),
6888        )
6889        .await;
6890        fs.insert_tree(
6891            "/root2",
6892            json!({
6893                "three.txt": "",
6894            }),
6895        )
6896        .await;
6897
6898        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6899        let (workspace, cx) =
6900            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6901        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6902        let worktree_id = project.update(cx, |project, cx| {
6903            project.worktrees(cx).next().unwrap().read(cx).id()
6904        });
6905
6906        let item1 = cx.new(|cx| {
6907            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6908        });
6909        let item2 = cx.new(|cx| {
6910            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6911        });
6912
6913        // Add an item to an empty pane
6914        workspace.update_in(cx, |workspace, window, cx| {
6915            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6916        });
6917        project.update(cx, |project, cx| {
6918            assert_eq!(
6919                project.active_entry(),
6920                project
6921                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6922                    .map(|e| e.id)
6923            );
6924        });
6925        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6926
6927        // Add a second item to a non-empty pane
6928        workspace.update_in(cx, |workspace, window, cx| {
6929            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6930        });
6931        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6932        project.update(cx, |project, cx| {
6933            assert_eq!(
6934                project.active_entry(),
6935                project
6936                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6937                    .map(|e| e.id)
6938            );
6939        });
6940
6941        // Close the active item
6942        pane.update_in(cx, |pane, window, cx| {
6943            pane.close_active_item(&Default::default(), window, cx)
6944                .unwrap()
6945        })
6946        .await
6947        .unwrap();
6948        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6949        project.update(cx, |project, cx| {
6950            assert_eq!(
6951                project.active_entry(),
6952                project
6953                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6954                    .map(|e| e.id)
6955            );
6956        });
6957
6958        // Add a project folder
6959        project
6960            .update(cx, |project, cx| {
6961                project.find_or_create_worktree("root2", true, cx)
6962            })
6963            .await
6964            .unwrap();
6965        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6966
6967        // Remove a project folder
6968        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6969        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6970    }
6971
6972    #[gpui::test]
6973    async fn test_close_window(cx: &mut TestAppContext) {
6974        init_test(cx);
6975
6976        let fs = FakeFs::new(cx.executor());
6977        fs.insert_tree("/root", json!({ "one": "" })).await;
6978
6979        let project = Project::test(fs, ["root".as_ref()], cx).await;
6980        let (workspace, cx) =
6981            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6982
6983        // When there are no dirty items, there's nothing to do.
6984        let item1 = cx.new(TestItem::new);
6985        workspace.update_in(cx, |w, window, cx| {
6986            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6987        });
6988        let task = workspace.update_in(cx, |w, window, cx| {
6989            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6990        });
6991        assert!(task.await.unwrap());
6992
6993        // When there are dirty untitled items, prompt to save each one. If the user
6994        // cancels any prompt, then abort.
6995        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6996        let item3 = cx.new(|cx| {
6997            TestItem::new(cx)
6998                .with_dirty(true)
6999                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7000        });
7001        workspace.update_in(cx, |w, window, cx| {
7002            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7003            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7004        });
7005        let task = workspace.update_in(cx, |w, window, cx| {
7006            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7007        });
7008        cx.executor().run_until_parked();
7009        cx.simulate_prompt_answer("Cancel"); // cancel save all
7010        cx.executor().run_until_parked();
7011        assert!(!cx.has_pending_prompt());
7012        assert!(!task.await.unwrap());
7013    }
7014
7015    #[gpui::test]
7016    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
7017        init_test(cx);
7018
7019        // Register TestItem as a serializable item
7020        cx.update(|cx| {
7021            register_serializable_item::<TestItem>(cx);
7022        });
7023
7024        let fs = FakeFs::new(cx.executor());
7025        fs.insert_tree("/root", json!({ "one": "" })).await;
7026
7027        let project = Project::test(fs, ["root".as_ref()], cx).await;
7028        let (workspace, cx) =
7029            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
7030
7031        // When there are dirty untitled items, but they can serialize, then there is no prompt.
7032        let item1 = cx.new(|cx| {
7033            TestItem::new(cx)
7034                .with_dirty(true)
7035                .with_serialize(|| Some(Task::ready(Ok(()))))
7036        });
7037        let item2 = cx.new(|cx| {
7038            TestItem::new(cx)
7039                .with_dirty(true)
7040                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7041                .with_serialize(|| Some(Task::ready(Ok(()))))
7042        });
7043        workspace.update_in(cx, |w, window, cx| {
7044            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7045            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7046        });
7047        let task = workspace.update_in(cx, |w, window, cx| {
7048            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
7049        });
7050        assert!(task.await.unwrap());
7051    }
7052
7053    #[gpui::test]
7054    async fn test_close_pane_items(cx: &mut TestAppContext) {
7055        init_test(cx);
7056
7057        let fs = FakeFs::new(cx.executor());
7058
7059        let project = Project::test(fs, None, cx).await;
7060        let (workspace, cx) =
7061            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7062
7063        let item1 = cx.new(|cx| {
7064            TestItem::new(cx)
7065                .with_dirty(true)
7066                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7067        });
7068        let item2 = cx.new(|cx| {
7069            TestItem::new(cx)
7070                .with_dirty(true)
7071                .with_conflict(true)
7072                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7073        });
7074        let item3 = cx.new(|cx| {
7075            TestItem::new(cx)
7076                .with_dirty(true)
7077                .with_conflict(true)
7078                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7079        });
7080        let item4 = cx.new(|cx| {
7081            TestItem::new(cx).with_dirty(true).with_project_items(&[{
7082                let project_item = TestProjectItem::new_untitled(cx);
7083                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7084                project_item
7085            }])
7086        });
7087        let pane = workspace.update_in(cx, |workspace, window, cx| {
7088            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7089            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7090            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7091            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7092            workspace.active_pane().clone()
7093        });
7094
7095        let close_items = pane.update_in(cx, |pane, window, cx| {
7096            pane.activate_item(1, true, true, window, cx);
7097            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7098            let item1_id = item1.item_id();
7099            let item3_id = item3.item_id();
7100            let item4_id = item4.item_id();
7101            pane.close_items(window, cx, SaveIntent::Close, move |id| {
7102                [item1_id, item3_id, item4_id].contains(&id)
7103            })
7104        });
7105        cx.executor().run_until_parked();
7106
7107        assert!(cx.has_pending_prompt());
7108        cx.simulate_prompt_answer("Save all");
7109
7110        cx.executor().run_until_parked();
7111
7112        // Item 1 is saved. There's a prompt to save item 3.
7113        pane.update(cx, |pane, cx| {
7114            assert_eq!(item1.read(cx).save_count, 1);
7115            assert_eq!(item1.read(cx).save_as_count, 0);
7116            assert_eq!(item1.read(cx).reload_count, 0);
7117            assert_eq!(pane.items_len(), 3);
7118            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7119        });
7120        assert!(cx.has_pending_prompt());
7121
7122        // Cancel saving item 3.
7123        cx.simulate_prompt_answer("Discard");
7124        cx.executor().run_until_parked();
7125
7126        // Item 3 is reloaded. There's a prompt to save item 4.
7127        pane.update(cx, |pane, cx| {
7128            assert_eq!(item3.read(cx).save_count, 0);
7129            assert_eq!(item3.read(cx).save_as_count, 0);
7130            assert_eq!(item3.read(cx).reload_count, 1);
7131            assert_eq!(pane.items_len(), 2);
7132            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7133        });
7134
7135        // There's a prompt for a path for item 4.
7136        cx.simulate_new_path_selection(|_| Some(Default::default()));
7137        close_items.await.unwrap();
7138
7139        // The requested items are closed.
7140        pane.update(cx, |pane, cx| {
7141            assert_eq!(item4.read(cx).save_count, 0);
7142            assert_eq!(item4.read(cx).save_as_count, 1);
7143            assert_eq!(item4.read(cx).reload_count, 0);
7144            assert_eq!(pane.items_len(), 1);
7145            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7146        });
7147    }
7148
7149    #[gpui::test]
7150    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7151        init_test(cx);
7152
7153        let fs = FakeFs::new(cx.executor());
7154        let project = Project::test(fs, [], cx).await;
7155        let (workspace, cx) =
7156            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7157
7158        // Create several workspace items with single project entries, and two
7159        // workspace items with multiple project entries.
7160        let single_entry_items = (0..=4)
7161            .map(|project_entry_id| {
7162                cx.new(|cx| {
7163                    TestItem::new(cx)
7164                        .with_dirty(true)
7165                        .with_project_items(&[dirty_project_item(
7166                            project_entry_id,
7167                            &format!("{project_entry_id}.txt"),
7168                            cx,
7169                        )])
7170                })
7171            })
7172            .collect::<Vec<_>>();
7173        let item_2_3 = cx.new(|cx| {
7174            TestItem::new(cx)
7175                .with_dirty(true)
7176                .with_singleton(false)
7177                .with_project_items(&[
7178                    single_entry_items[2].read(cx).project_items[0].clone(),
7179                    single_entry_items[3].read(cx).project_items[0].clone(),
7180                ])
7181        });
7182        let item_3_4 = cx.new(|cx| {
7183            TestItem::new(cx)
7184                .with_dirty(true)
7185                .with_singleton(false)
7186                .with_project_items(&[
7187                    single_entry_items[3].read(cx).project_items[0].clone(),
7188                    single_entry_items[4].read(cx).project_items[0].clone(),
7189                ])
7190        });
7191
7192        // Create two panes that contain the following project entries:
7193        //   left pane:
7194        //     multi-entry items:   (2, 3)
7195        //     single-entry items:  0, 2, 3, 4
7196        //   right pane:
7197        //     single-entry items:  4, 1
7198        //     multi-entry items:   (3, 4)
7199        let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| {
7200            let left_pane = workspace.active_pane().clone();
7201            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7202            workspace.add_item_to_active_pane(
7203                single_entry_items[0].boxed_clone(),
7204                None,
7205                true,
7206                window,
7207                cx,
7208            );
7209            workspace.add_item_to_active_pane(
7210                single_entry_items[2].boxed_clone(),
7211                None,
7212                true,
7213                window,
7214                cx,
7215            );
7216            workspace.add_item_to_active_pane(
7217                single_entry_items[3].boxed_clone(),
7218                None,
7219                true,
7220                window,
7221                cx,
7222            );
7223            workspace.add_item_to_active_pane(
7224                single_entry_items[4].boxed_clone(),
7225                None,
7226                true,
7227                window,
7228                cx,
7229            );
7230
7231            let right_pane = workspace
7232                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7233                .unwrap();
7234
7235            right_pane.update(cx, |pane, cx| {
7236                pane.add_item(
7237                    single_entry_items[1].boxed_clone(),
7238                    true,
7239                    true,
7240                    None,
7241                    window,
7242                    cx,
7243                );
7244                pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7245            });
7246
7247            (left_pane, right_pane)
7248        });
7249
7250        cx.focus(&right_pane);
7251
7252        let mut close = right_pane.update_in(cx, |pane, window, cx| {
7253            pane.close_all_items(&CloseAllItems::default(), window, cx)
7254                .unwrap()
7255        });
7256        cx.executor().run_until_parked();
7257
7258        let msg = cx.pending_prompt().unwrap().0;
7259        assert!(msg.contains("1.txt"));
7260        assert!(!msg.contains("2.txt"));
7261        assert!(!msg.contains("3.txt"));
7262        assert!(!msg.contains("4.txt"));
7263
7264        cx.simulate_prompt_answer("Cancel");
7265        close.await.unwrap();
7266
7267        left_pane
7268            .update_in(cx, |left_pane, window, cx| {
7269                left_pane.close_item_by_id(
7270                    single_entry_items[3].entity_id(),
7271                    SaveIntent::Skip,
7272                    window,
7273                    cx,
7274                )
7275            })
7276            .await
7277            .unwrap();
7278
7279        close = right_pane.update_in(cx, |pane, window, cx| {
7280            pane.close_all_items(&CloseAllItems::default(), window, cx)
7281                .unwrap()
7282        });
7283        cx.executor().run_until_parked();
7284
7285        let details = cx.pending_prompt().unwrap().1;
7286        assert!(details.contains("1.txt"));
7287        assert!(!details.contains("2.txt"));
7288        assert!(details.contains("3.txt"));
7289        // ideally this assertion could be made, but today we can only
7290        // save whole items not project items, so the orphaned item 3 causes
7291        // 4 to be saved too.
7292        // assert!(!details.contains("4.txt"));
7293
7294        cx.simulate_prompt_answer("Save all");
7295
7296        cx.executor().run_until_parked();
7297        close.await.unwrap();
7298        right_pane.update(cx, |pane, _| {
7299            assert_eq!(pane.items_len(), 0);
7300        });
7301    }
7302
7303    #[gpui::test]
7304    async fn test_autosave(cx: &mut gpui::TestAppContext) {
7305        init_test(cx);
7306
7307        let fs = FakeFs::new(cx.executor());
7308        let project = Project::test(fs, [], cx).await;
7309        let (workspace, cx) =
7310            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7311        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7312
7313        let item = cx.new(|cx| {
7314            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7315        });
7316        let item_id = item.entity_id();
7317        workspace.update_in(cx, |workspace, window, cx| {
7318            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7319        });
7320
7321        // Autosave on window change.
7322        item.update(cx, |item, cx| {
7323            SettingsStore::update_global(cx, |settings, cx| {
7324                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7325                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
7326                })
7327            });
7328            item.is_dirty = true;
7329        });
7330
7331        // Deactivating the window saves the file.
7332        cx.deactivate_window();
7333        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7334
7335        // Re-activating the window doesn't save the file.
7336        cx.update(|window, _| window.activate_window());
7337        cx.executor().run_until_parked();
7338        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7339
7340        // Autosave on focus change.
7341        item.update_in(cx, |item, window, cx| {
7342            cx.focus_self(window);
7343            SettingsStore::update_global(cx, |settings, cx| {
7344                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7345                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7346                })
7347            });
7348            item.is_dirty = true;
7349        });
7350
7351        // Blurring the item saves the file.
7352        item.update_in(cx, |_, window, _| window.blur());
7353        cx.executor().run_until_parked();
7354        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7355
7356        // Deactivating the window still saves the file.
7357        item.update_in(cx, |item, window, cx| {
7358            cx.focus_self(window);
7359            item.is_dirty = true;
7360        });
7361        cx.deactivate_window();
7362        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7363
7364        // Autosave after delay.
7365        item.update(cx, |item, cx| {
7366            SettingsStore::update_global(cx, |settings, cx| {
7367                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7368                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7369                })
7370            });
7371            item.is_dirty = true;
7372            cx.emit(ItemEvent::Edit);
7373        });
7374
7375        // Delay hasn't fully expired, so the file is still dirty and unsaved.
7376        cx.executor().advance_clock(Duration::from_millis(250));
7377        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7378
7379        // After delay expires, the file is saved.
7380        cx.executor().advance_clock(Duration::from_millis(250));
7381        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7382
7383        // Autosave on focus change, ensuring closing the tab counts as such.
7384        item.update(cx, |item, cx| {
7385            SettingsStore::update_global(cx, |settings, cx| {
7386                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7387                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7388                })
7389            });
7390            item.is_dirty = true;
7391            for project_item in &mut item.project_items {
7392                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7393            }
7394        });
7395
7396        pane.update_in(cx, |pane, window, cx| {
7397            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7398        })
7399        .await
7400        .unwrap();
7401        assert!(!cx.has_pending_prompt());
7402        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7403
7404        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7405        workspace.update_in(cx, |workspace, window, cx| {
7406            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7407        });
7408        item.update_in(cx, |item, window, cx| {
7409            item.project_items[0].update(cx, |item, _| {
7410                item.entry_id = None;
7411            });
7412            item.is_dirty = true;
7413            window.blur();
7414        });
7415        cx.run_until_parked();
7416        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7417
7418        // Ensure autosave is prevented for deleted files also when closing the buffer.
7419        let _close_items = pane.update_in(cx, |pane, window, cx| {
7420            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7421        });
7422        cx.run_until_parked();
7423        assert!(cx.has_pending_prompt());
7424        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7425    }
7426
7427    #[gpui::test]
7428    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7429        init_test(cx);
7430
7431        let fs = FakeFs::new(cx.executor());
7432
7433        let project = Project::test(fs, [], cx).await;
7434        let (workspace, cx) =
7435            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7436
7437        let item = cx.new(|cx| {
7438            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7439        });
7440        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7441        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7442        let toolbar_notify_count = Rc::new(RefCell::new(0));
7443
7444        workspace.update_in(cx, |workspace, window, cx| {
7445            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7446            let toolbar_notification_count = toolbar_notify_count.clone();
7447            cx.observe_in(&toolbar, window, move |_, _, _, _| {
7448                *toolbar_notification_count.borrow_mut() += 1
7449            })
7450            .detach();
7451        });
7452
7453        pane.update(cx, |pane, _| {
7454            assert!(!pane.can_navigate_backward());
7455            assert!(!pane.can_navigate_forward());
7456        });
7457
7458        item.update_in(cx, |item, _, cx| {
7459            item.set_state("one".to_string(), cx);
7460        });
7461
7462        // Toolbar must be notified to re-render the navigation buttons
7463        assert_eq!(*toolbar_notify_count.borrow(), 1);
7464
7465        pane.update(cx, |pane, _| {
7466            assert!(pane.can_navigate_backward());
7467            assert!(!pane.can_navigate_forward());
7468        });
7469
7470        workspace
7471            .update_in(cx, |workspace, window, cx| {
7472                workspace.go_back(pane.downgrade(), window, cx)
7473            })
7474            .await
7475            .unwrap();
7476
7477        assert_eq!(*toolbar_notify_count.borrow(), 2);
7478        pane.update(cx, |pane, _| {
7479            assert!(!pane.can_navigate_backward());
7480            assert!(pane.can_navigate_forward());
7481        });
7482    }
7483
7484    #[gpui::test]
7485    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7486        init_test(cx);
7487        let fs = FakeFs::new(cx.executor());
7488
7489        let project = Project::test(fs, [], cx).await;
7490        let (workspace, cx) =
7491            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7492
7493        let panel = workspace.update_in(cx, |workspace, window, cx| {
7494            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7495            workspace.add_panel(panel.clone(), window, cx);
7496
7497            workspace
7498                .right_dock()
7499                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7500
7501            panel
7502        });
7503
7504        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7505        pane.update_in(cx, |pane, window, cx| {
7506            let item = cx.new(TestItem::new);
7507            pane.add_item(Box::new(item), true, true, None, window, cx);
7508        });
7509
7510        // Transfer focus from center to panel
7511        workspace.update_in(cx, |workspace, window, cx| {
7512            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7513        });
7514
7515        workspace.update_in(cx, |workspace, window, cx| {
7516            assert!(workspace.right_dock().read(cx).is_open());
7517            assert!(!panel.is_zoomed(window, cx));
7518            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7519        });
7520
7521        // Transfer focus from panel to center
7522        workspace.update_in(cx, |workspace, window, cx| {
7523            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7524        });
7525
7526        workspace.update_in(cx, |workspace, window, cx| {
7527            assert!(workspace.right_dock().read(cx).is_open());
7528            assert!(!panel.is_zoomed(window, cx));
7529            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7530        });
7531
7532        // Close the dock
7533        workspace.update_in(cx, |workspace, window, cx| {
7534            workspace.toggle_dock(DockPosition::Right, window, cx);
7535        });
7536
7537        workspace.update_in(cx, |workspace, window, cx| {
7538            assert!(!workspace.right_dock().read(cx).is_open());
7539            assert!(!panel.is_zoomed(window, cx));
7540            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7541        });
7542
7543        // Open the dock
7544        workspace.update_in(cx, |workspace, window, cx| {
7545            workspace.toggle_dock(DockPosition::Right, window, cx);
7546        });
7547
7548        workspace.update_in(cx, |workspace, window, cx| {
7549            assert!(workspace.right_dock().read(cx).is_open());
7550            assert!(!panel.is_zoomed(window, cx));
7551            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7552        });
7553
7554        // Focus and zoom panel
7555        panel.update_in(cx, |panel, window, cx| {
7556            cx.focus_self(window);
7557            panel.set_zoomed(true, window, cx)
7558        });
7559
7560        workspace.update_in(cx, |workspace, window, cx| {
7561            assert!(workspace.right_dock().read(cx).is_open());
7562            assert!(panel.is_zoomed(window, cx));
7563            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7564        });
7565
7566        // Transfer focus to the center closes the dock
7567        workspace.update_in(cx, |workspace, window, cx| {
7568            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7569        });
7570
7571        workspace.update_in(cx, |workspace, window, cx| {
7572            assert!(!workspace.right_dock().read(cx).is_open());
7573            assert!(panel.is_zoomed(window, cx));
7574            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7575        });
7576
7577        // Transferring focus back to the panel keeps it zoomed
7578        workspace.update_in(cx, |workspace, window, cx| {
7579            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7580        });
7581
7582        workspace.update_in(cx, |workspace, window, cx| {
7583            assert!(workspace.right_dock().read(cx).is_open());
7584            assert!(panel.is_zoomed(window, cx));
7585            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7586        });
7587
7588        // Close the dock while it is zoomed
7589        workspace.update_in(cx, |workspace, window, cx| {
7590            workspace.toggle_dock(DockPosition::Right, window, cx)
7591        });
7592
7593        workspace.update_in(cx, |workspace, window, cx| {
7594            assert!(!workspace.right_dock().read(cx).is_open());
7595            assert!(panel.is_zoomed(window, cx));
7596            assert!(workspace.zoomed.is_none());
7597            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7598        });
7599
7600        // Opening the dock, when it's zoomed, retains focus
7601        workspace.update_in(cx, |workspace, window, cx| {
7602            workspace.toggle_dock(DockPosition::Right, window, cx)
7603        });
7604
7605        workspace.update_in(cx, |workspace, window, cx| {
7606            assert!(workspace.right_dock().read(cx).is_open());
7607            assert!(panel.is_zoomed(window, cx));
7608            assert!(workspace.zoomed.is_some());
7609            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7610        });
7611
7612        // Unzoom and close the panel, zoom the active pane.
7613        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7614        workspace.update_in(cx, |workspace, window, cx| {
7615            workspace.toggle_dock(DockPosition::Right, window, cx)
7616        });
7617        pane.update_in(cx, |pane, window, cx| {
7618            pane.toggle_zoom(&Default::default(), window, cx)
7619        });
7620
7621        // Opening a dock unzooms the pane.
7622        workspace.update_in(cx, |workspace, window, cx| {
7623            workspace.toggle_dock(DockPosition::Right, window, cx)
7624        });
7625        workspace.update_in(cx, |workspace, window, cx| {
7626            let pane = pane.read(cx);
7627            assert!(!pane.is_zoomed());
7628            assert!(!pane.focus_handle(cx).is_focused(window));
7629            assert!(workspace.right_dock().read(cx).is_open());
7630            assert!(workspace.zoomed.is_none());
7631        });
7632    }
7633
7634    #[gpui::test]
7635    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7636        init_test(cx);
7637
7638        let fs = FakeFs::new(cx.executor());
7639
7640        let project = Project::test(fs, None, cx).await;
7641        let (workspace, cx) =
7642            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7643
7644        // Let's arrange the panes like this:
7645        //
7646        // +-----------------------+
7647        // |         top           |
7648        // +------+--------+-------+
7649        // | left | center | right |
7650        // +------+--------+-------+
7651        // |        bottom         |
7652        // +-----------------------+
7653
7654        let top_item = cx.new(|cx| {
7655            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7656        });
7657        let bottom_item = cx.new(|cx| {
7658            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7659        });
7660        let left_item = cx.new(|cx| {
7661            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7662        });
7663        let right_item = cx.new(|cx| {
7664            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7665        });
7666        let center_item = cx.new(|cx| {
7667            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7668        });
7669
7670        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7671            let top_pane_id = workspace.active_pane().entity_id();
7672            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7673            workspace.split_pane(
7674                workspace.active_pane().clone(),
7675                SplitDirection::Down,
7676                window,
7677                cx,
7678            );
7679            top_pane_id
7680        });
7681        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7682            let bottom_pane_id = workspace.active_pane().entity_id();
7683            workspace.add_item_to_active_pane(
7684                Box::new(bottom_item.clone()),
7685                None,
7686                false,
7687                window,
7688                cx,
7689            );
7690            workspace.split_pane(
7691                workspace.active_pane().clone(),
7692                SplitDirection::Up,
7693                window,
7694                cx,
7695            );
7696            bottom_pane_id
7697        });
7698        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7699            let left_pane_id = workspace.active_pane().entity_id();
7700            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7701            workspace.split_pane(
7702                workspace.active_pane().clone(),
7703                SplitDirection::Right,
7704                window,
7705                cx,
7706            );
7707            left_pane_id
7708        });
7709        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7710            let right_pane_id = workspace.active_pane().entity_id();
7711            workspace.add_item_to_active_pane(
7712                Box::new(right_item.clone()),
7713                None,
7714                false,
7715                window,
7716                cx,
7717            );
7718            workspace.split_pane(
7719                workspace.active_pane().clone(),
7720                SplitDirection::Left,
7721                window,
7722                cx,
7723            );
7724            right_pane_id
7725        });
7726        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7727            let center_pane_id = workspace.active_pane().entity_id();
7728            workspace.add_item_to_active_pane(
7729                Box::new(center_item.clone()),
7730                None,
7731                false,
7732                window,
7733                cx,
7734            );
7735            center_pane_id
7736        });
7737        cx.executor().run_until_parked();
7738
7739        workspace.update_in(cx, |workspace, window, cx| {
7740            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7741
7742            // Join into next from center pane into right
7743            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7744        });
7745
7746        workspace.update_in(cx, |workspace, window, cx| {
7747            let active_pane = workspace.active_pane();
7748            assert_eq!(right_pane_id, active_pane.entity_id());
7749            assert_eq!(2, active_pane.read(cx).items_len());
7750            let item_ids_in_pane =
7751                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7752            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7753            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7754
7755            // Join into next from right pane into bottom
7756            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7757        });
7758
7759        workspace.update_in(cx, |workspace, window, cx| {
7760            let active_pane = workspace.active_pane();
7761            assert_eq!(bottom_pane_id, active_pane.entity_id());
7762            assert_eq!(3, active_pane.read(cx).items_len());
7763            let item_ids_in_pane =
7764                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7765            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7766            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7767            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7768
7769            // Join into next from bottom pane into left
7770            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7771        });
7772
7773        workspace.update_in(cx, |workspace, window, cx| {
7774            let active_pane = workspace.active_pane();
7775            assert_eq!(left_pane_id, active_pane.entity_id());
7776            assert_eq!(4, active_pane.read(cx).items_len());
7777            let item_ids_in_pane =
7778                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7779            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7780            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7781            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7782            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7783
7784            // Join into next from left pane into top
7785            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7786        });
7787
7788        workspace.update_in(cx, |workspace, window, cx| {
7789            let active_pane = workspace.active_pane();
7790            assert_eq!(top_pane_id, active_pane.entity_id());
7791            assert_eq!(5, active_pane.read(cx).items_len());
7792            let item_ids_in_pane =
7793                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7794            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7795            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7796            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7797            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7798            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7799
7800            // Single pane left: no-op
7801            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7802        });
7803
7804        workspace.update(cx, |workspace, _cx| {
7805            let active_pane = workspace.active_pane();
7806            assert_eq!(top_pane_id, active_pane.entity_id());
7807        });
7808    }
7809
7810    fn add_an_item_to_active_pane(
7811        cx: &mut VisualTestContext,
7812        workspace: &Entity<Workspace>,
7813        item_id: u64,
7814    ) -> Entity<TestItem> {
7815        let item = cx.new(|cx| {
7816            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7817                item_id,
7818                "item{item_id}.txt",
7819                cx,
7820            )])
7821        });
7822        workspace.update_in(cx, |workspace, window, cx| {
7823            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7824        });
7825        return item;
7826    }
7827
7828    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7829        return workspace.update_in(cx, |workspace, window, cx| {
7830            let new_pane = workspace.split_pane(
7831                workspace.active_pane().clone(),
7832                SplitDirection::Right,
7833                window,
7834                cx,
7835            );
7836            new_pane
7837        });
7838    }
7839
7840    #[gpui::test]
7841    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7842        init_test(cx);
7843        let fs = FakeFs::new(cx.executor());
7844        let project = Project::test(fs, None, cx).await;
7845        let (workspace, cx) =
7846            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7847
7848        add_an_item_to_active_pane(cx, &workspace, 1);
7849        split_pane(cx, &workspace);
7850        add_an_item_to_active_pane(cx, &workspace, 2);
7851        split_pane(cx, &workspace); // empty pane
7852        split_pane(cx, &workspace);
7853        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7854
7855        cx.executor().run_until_parked();
7856
7857        workspace.update(cx, |workspace, cx| {
7858            let num_panes = workspace.panes().len();
7859            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7860            let active_item = workspace
7861                .active_pane()
7862                .read(cx)
7863                .active_item()
7864                .expect("item is in focus");
7865
7866            assert_eq!(num_panes, 4);
7867            assert_eq!(num_items_in_current_pane, 1);
7868            assert_eq!(active_item.item_id(), last_item.item_id());
7869        });
7870
7871        workspace.update_in(cx, |workspace, window, cx| {
7872            workspace.join_all_panes(window, cx);
7873        });
7874
7875        workspace.update(cx, |workspace, cx| {
7876            let num_panes = workspace.panes().len();
7877            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7878            let active_item = workspace
7879                .active_pane()
7880                .read(cx)
7881                .active_item()
7882                .expect("item is in focus");
7883
7884            assert_eq!(num_panes, 1);
7885            assert_eq!(num_items_in_current_pane, 3);
7886            assert_eq!(active_item.item_id(), last_item.item_id());
7887        });
7888    }
7889    struct TestModal(FocusHandle);
7890
7891    impl TestModal {
7892        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7893            Self(cx.focus_handle())
7894        }
7895    }
7896
7897    impl EventEmitter<DismissEvent> for TestModal {}
7898
7899    impl Focusable for TestModal {
7900        fn focus_handle(&self, _cx: &App) -> FocusHandle {
7901            self.0.clone()
7902        }
7903    }
7904
7905    impl ModalView for TestModal {}
7906
7907    impl Render for TestModal {
7908        fn render(
7909            &mut self,
7910            _window: &mut Window,
7911            _cx: &mut Context<TestModal>,
7912        ) -> impl IntoElement {
7913            div().track_focus(&self.0)
7914        }
7915    }
7916
7917    #[gpui::test]
7918    async fn test_panels(cx: &mut gpui::TestAppContext) {
7919        init_test(cx);
7920        let fs = FakeFs::new(cx.executor());
7921
7922        let project = Project::test(fs, [], cx).await;
7923        let (workspace, cx) =
7924            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7925
7926        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7927            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7928            workspace.add_panel(panel_1.clone(), window, cx);
7929            workspace.toggle_dock(DockPosition::Left, window, cx);
7930            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7931            workspace.add_panel(panel_2.clone(), window, cx);
7932            workspace.toggle_dock(DockPosition::Right, window, cx);
7933
7934            let left_dock = workspace.left_dock();
7935            assert_eq!(
7936                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7937                panel_1.panel_id()
7938            );
7939            assert_eq!(
7940                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7941                panel_1.size(window, cx)
7942            );
7943
7944            left_dock.update(cx, |left_dock, cx| {
7945                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7946            });
7947            assert_eq!(
7948                workspace
7949                    .right_dock()
7950                    .read(cx)
7951                    .visible_panel()
7952                    .unwrap()
7953                    .panel_id(),
7954                panel_2.panel_id(),
7955            );
7956
7957            (panel_1, panel_2)
7958        });
7959
7960        // Move panel_1 to the right
7961        panel_1.update_in(cx, |panel_1, window, cx| {
7962            panel_1.set_position(DockPosition::Right, window, cx)
7963        });
7964
7965        workspace.update_in(cx, |workspace, window, cx| {
7966            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7967            // Since it was the only panel on the left, the left dock should now be closed.
7968            assert!(!workspace.left_dock().read(cx).is_open());
7969            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7970            let right_dock = workspace.right_dock();
7971            assert_eq!(
7972                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7973                panel_1.panel_id()
7974            );
7975            assert_eq!(
7976                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7977                px(1337.)
7978            );
7979
7980            // Now we move panel_2 to the left
7981            panel_2.set_position(DockPosition::Left, window, cx);
7982        });
7983
7984        workspace.update(cx, |workspace, cx| {
7985            // Since panel_2 was not visible on the right, we don't open the left dock.
7986            assert!(!workspace.left_dock().read(cx).is_open());
7987            // And the right dock is unaffected in its displaying of panel_1
7988            assert!(workspace.right_dock().read(cx).is_open());
7989            assert_eq!(
7990                workspace
7991                    .right_dock()
7992                    .read(cx)
7993                    .visible_panel()
7994                    .unwrap()
7995                    .panel_id(),
7996                panel_1.panel_id(),
7997            );
7998        });
7999
8000        // Move panel_1 back to the left
8001        panel_1.update_in(cx, |panel_1, window, cx| {
8002            panel_1.set_position(DockPosition::Left, window, cx)
8003        });
8004
8005        workspace.update_in(cx, |workspace, window, cx| {
8006            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
8007            let left_dock = workspace.left_dock();
8008            assert!(left_dock.read(cx).is_open());
8009            assert_eq!(
8010                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8011                panel_1.panel_id()
8012            );
8013            assert_eq!(
8014                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
8015                px(1337.)
8016            );
8017            // And the right dock should be closed as it no longer has any panels.
8018            assert!(!workspace.right_dock().read(cx).is_open());
8019
8020            // Now we move panel_1 to the bottom
8021            panel_1.set_position(DockPosition::Bottom, window, cx);
8022        });
8023
8024        workspace.update_in(cx, |workspace, window, cx| {
8025            // Since panel_1 was visible on the left, we close the left dock.
8026            assert!(!workspace.left_dock().read(cx).is_open());
8027            // The bottom dock is sized based on the panel's default size,
8028            // since the panel orientation changed from vertical to horizontal.
8029            let bottom_dock = workspace.bottom_dock();
8030            assert_eq!(
8031                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
8032                panel_1.size(window, cx),
8033            );
8034            // Close bottom dock and move panel_1 back to the left.
8035            bottom_dock.update(cx, |bottom_dock, cx| {
8036                bottom_dock.set_open(false, window, cx)
8037            });
8038            panel_1.set_position(DockPosition::Left, window, cx);
8039        });
8040
8041        // Emit activated event on panel 1
8042        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
8043
8044        // Now the left dock is open and panel_1 is active and focused.
8045        workspace.update_in(cx, |workspace, window, cx| {
8046            let left_dock = workspace.left_dock();
8047            assert!(left_dock.read(cx).is_open());
8048            assert_eq!(
8049                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8050                panel_1.panel_id(),
8051            );
8052            assert!(panel_1.focus_handle(cx).is_focused(window));
8053        });
8054
8055        // Emit closed event on panel 2, which is not active
8056        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8057
8058        // Wo don't close the left dock, because panel_2 wasn't the active panel
8059        workspace.update(cx, |workspace, cx| {
8060            let left_dock = workspace.left_dock();
8061            assert!(left_dock.read(cx).is_open());
8062            assert_eq!(
8063                left_dock.read(cx).visible_panel().unwrap().panel_id(),
8064                panel_1.panel_id(),
8065            );
8066        });
8067
8068        // Emitting a ZoomIn event shows the panel as zoomed.
8069        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
8070        workspace.update(cx, |workspace, _| {
8071            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8072            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
8073        });
8074
8075        // Move panel to another dock while it is zoomed
8076        panel_1.update_in(cx, |panel, window, cx| {
8077            panel.set_position(DockPosition::Right, window, cx)
8078        });
8079        workspace.update(cx, |workspace, _| {
8080            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8081
8082            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8083        });
8084
8085        // This is a helper for getting a:
8086        // - valid focus on an element,
8087        // - that isn't a part of the panes and panels system of the Workspace,
8088        // - and doesn't trigger the 'on_focus_lost' API.
8089        let focus_other_view = {
8090            let workspace = workspace.clone();
8091            move |cx: &mut VisualTestContext| {
8092                workspace.update_in(cx, |workspace, window, cx| {
8093                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
8094                        workspace.toggle_modal(window, cx, TestModal::new);
8095                        workspace.toggle_modal(window, cx, TestModal::new);
8096                    } else {
8097                        workspace.toggle_modal(window, cx, TestModal::new);
8098                    }
8099                })
8100            }
8101        };
8102
8103        // If focus is transferred to another view that's not a panel or another pane, we still show
8104        // the panel as zoomed.
8105        focus_other_view(cx);
8106        workspace.update(cx, |workspace, _| {
8107            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8108            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8109        });
8110
8111        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8112        workspace.update_in(cx, |_workspace, window, cx| {
8113            cx.focus_self(window);
8114        });
8115        workspace.update(cx, |workspace, _| {
8116            assert_eq!(workspace.zoomed, None);
8117            assert_eq!(workspace.zoomed_position, None);
8118        });
8119
8120        // If focus is transferred again to another view that's not a panel or a pane, we won't
8121        // show the panel as zoomed because it wasn't zoomed before.
8122        focus_other_view(cx);
8123        workspace.update(cx, |workspace, _| {
8124            assert_eq!(workspace.zoomed, None);
8125            assert_eq!(workspace.zoomed_position, None);
8126        });
8127
8128        // When the panel is activated, it is zoomed again.
8129        cx.dispatch_action(ToggleRightDock);
8130        workspace.update(cx, |workspace, _| {
8131            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8132            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8133        });
8134
8135        // Emitting a ZoomOut event unzooms the panel.
8136        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8137        workspace.update(cx, |workspace, _| {
8138            assert_eq!(workspace.zoomed, None);
8139            assert_eq!(workspace.zoomed_position, None);
8140        });
8141
8142        // Emit closed event on panel 1, which is active
8143        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8144
8145        // Now the left dock is closed, because panel_1 was the active panel
8146        workspace.update(cx, |workspace, cx| {
8147            let right_dock = workspace.right_dock();
8148            assert!(!right_dock.read(cx).is_open());
8149        });
8150    }
8151
8152    #[gpui::test]
8153    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8154        init_test(cx);
8155
8156        let fs = FakeFs::new(cx.background_executor.clone());
8157        let project = Project::test(fs, [], cx).await;
8158        let (workspace, cx) =
8159            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8160        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8161
8162        let dirty_regular_buffer = cx.new(|cx| {
8163            TestItem::new(cx)
8164                .with_dirty(true)
8165                .with_label("1.txt")
8166                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8167        });
8168        let dirty_regular_buffer_2 = cx.new(|cx| {
8169            TestItem::new(cx)
8170                .with_dirty(true)
8171                .with_label("2.txt")
8172                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8173        });
8174        let dirty_multi_buffer_with_both = cx.new(|cx| {
8175            TestItem::new(cx)
8176                .with_dirty(true)
8177                .with_singleton(false)
8178                .with_label("Fake Project Search")
8179                .with_project_items(&[
8180                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8181                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8182                ])
8183        });
8184        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8185        workspace.update_in(cx, |workspace, window, cx| {
8186            workspace.add_item(
8187                pane.clone(),
8188                Box::new(dirty_regular_buffer.clone()),
8189                None,
8190                false,
8191                false,
8192                window,
8193                cx,
8194            );
8195            workspace.add_item(
8196                pane.clone(),
8197                Box::new(dirty_regular_buffer_2.clone()),
8198                None,
8199                false,
8200                false,
8201                window,
8202                cx,
8203            );
8204            workspace.add_item(
8205                pane.clone(),
8206                Box::new(dirty_multi_buffer_with_both.clone()),
8207                None,
8208                false,
8209                false,
8210                window,
8211                cx,
8212            );
8213        });
8214
8215        pane.update_in(cx, |pane, window, cx| {
8216            pane.activate_item(2, true, true, window, cx);
8217            assert_eq!(
8218                pane.active_item().unwrap().item_id(),
8219                multi_buffer_with_both_files_id,
8220                "Should select the multi buffer in the pane"
8221            );
8222        });
8223        let close_all_but_multi_buffer_task = pane
8224            .update_in(cx, |pane, window, cx| {
8225                pane.close_inactive_items(
8226                    &CloseInactiveItems {
8227                        save_intent: Some(SaveIntent::Save),
8228                        close_pinned: true,
8229                    },
8230                    window,
8231                    cx,
8232                )
8233            })
8234            .expect("should have inactive files to close");
8235        cx.background_executor.run_until_parked();
8236        assert!(!cx.has_pending_prompt());
8237        close_all_but_multi_buffer_task
8238            .await
8239            .expect("Closing all buffers but the multi buffer failed");
8240        pane.update(cx, |pane, cx| {
8241            assert_eq!(dirty_regular_buffer.read(cx).save_count, 1);
8242            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8243            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1);
8244            assert_eq!(pane.items_len(), 1);
8245            assert_eq!(
8246                pane.active_item().unwrap().item_id(),
8247                multi_buffer_with_both_files_id,
8248                "Should have only the multi buffer left in the pane"
8249            );
8250            assert!(
8251                dirty_multi_buffer_with_both.read(cx).is_dirty,
8252                "The multi buffer containing the unsaved buffer should still be dirty"
8253            );
8254        });
8255
8256        dirty_regular_buffer.update(cx, |buffer, cx| {
8257            buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true)
8258        });
8259
8260        let close_multi_buffer_task = pane
8261            .update_in(cx, |pane, window, cx| {
8262                pane.close_active_item(
8263                    &CloseActiveItem {
8264                        save_intent: Some(SaveIntent::Close),
8265                        close_pinned: false,
8266                    },
8267                    window,
8268                    cx,
8269                )
8270            })
8271            .expect("should have the multi buffer to close");
8272        cx.background_executor.run_until_parked();
8273        assert!(
8274            cx.has_pending_prompt(),
8275            "Dirty multi buffer should prompt a save dialog"
8276        );
8277        cx.simulate_prompt_answer("Save");
8278        cx.background_executor.run_until_parked();
8279        close_multi_buffer_task
8280            .await
8281            .expect("Closing the multi buffer failed");
8282        pane.update(cx, |pane, cx| {
8283            assert_eq!(
8284                dirty_multi_buffer_with_both.read(cx).save_count,
8285                1,
8286                "Multi buffer item should get be saved"
8287            );
8288            // Test impl does not save inner items, so we do not assert them
8289            assert_eq!(
8290                pane.items_len(),
8291                0,
8292                "No more items should be left in the pane"
8293            );
8294            assert!(pane.active_item().is_none());
8295        });
8296    }
8297
8298    #[gpui::test]
8299    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8300        cx: &mut TestAppContext,
8301    ) {
8302        init_test(cx);
8303
8304        let fs = FakeFs::new(cx.background_executor.clone());
8305        let project = Project::test(fs, [], cx).await;
8306        let (workspace, cx) =
8307            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8308        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8309
8310        let dirty_regular_buffer = cx.new(|cx| {
8311            TestItem::new(cx)
8312                .with_dirty(true)
8313                .with_label("1.txt")
8314                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8315        });
8316        let dirty_regular_buffer_2 = cx.new(|cx| {
8317            TestItem::new(cx)
8318                .with_dirty(true)
8319                .with_label("2.txt")
8320                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8321        });
8322        let clear_regular_buffer = cx.new(|cx| {
8323            TestItem::new(cx)
8324                .with_label("3.txt")
8325                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8326        });
8327
8328        let dirty_multi_buffer_with_both = cx.new(|cx| {
8329            TestItem::new(cx)
8330                .with_dirty(true)
8331                .with_singleton(false)
8332                .with_label("Fake Project Search")
8333                .with_project_items(&[
8334                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8335                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8336                    clear_regular_buffer.read(cx).project_items[0].clone(),
8337                ])
8338        });
8339        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8340        workspace.update_in(cx, |workspace, window, cx| {
8341            workspace.add_item(
8342                pane.clone(),
8343                Box::new(dirty_regular_buffer.clone()),
8344                None,
8345                false,
8346                false,
8347                window,
8348                cx,
8349            );
8350            workspace.add_item(
8351                pane.clone(),
8352                Box::new(dirty_multi_buffer_with_both.clone()),
8353                None,
8354                false,
8355                false,
8356                window,
8357                cx,
8358            );
8359        });
8360
8361        pane.update_in(cx, |pane, window, cx| {
8362            pane.activate_item(1, true, true, window, cx);
8363            assert_eq!(
8364                pane.active_item().unwrap().item_id(),
8365                multi_buffer_with_both_files_id,
8366                "Should select the multi buffer in the pane"
8367            );
8368        });
8369        let _close_multi_buffer_task = pane
8370            .update_in(cx, |pane, window, cx| {
8371                pane.close_active_item(
8372                    &CloseActiveItem {
8373                        save_intent: None,
8374                        close_pinned: false,
8375                    },
8376                    window,
8377                    cx,
8378                )
8379            })
8380            .expect("should have active multi buffer to close");
8381        cx.background_executor.run_until_parked();
8382        assert!(
8383            cx.has_pending_prompt(),
8384            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8385        );
8386    }
8387
8388    #[gpui::test]
8389    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8390        cx: &mut TestAppContext,
8391    ) {
8392        init_test(cx);
8393
8394        let fs = FakeFs::new(cx.background_executor.clone());
8395        let project = Project::test(fs, [], cx).await;
8396        let (workspace, cx) =
8397            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8398        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8399
8400        let dirty_regular_buffer = cx.new(|cx| {
8401            TestItem::new(cx)
8402                .with_dirty(true)
8403                .with_label("1.txt")
8404                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8405        });
8406        let dirty_regular_buffer_2 = cx.new(|cx| {
8407            TestItem::new(cx)
8408                .with_dirty(true)
8409                .with_label("2.txt")
8410                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8411        });
8412        let clear_regular_buffer = cx.new(|cx| {
8413            TestItem::new(cx)
8414                .with_label("3.txt")
8415                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8416        });
8417
8418        let dirty_multi_buffer = cx.new(|cx| {
8419            TestItem::new(cx)
8420                .with_dirty(true)
8421                .with_singleton(false)
8422                .with_label("Fake Project Search")
8423                .with_project_items(&[
8424                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8425                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8426                    clear_regular_buffer.read(cx).project_items[0].clone(),
8427                ])
8428        });
8429        workspace.update_in(cx, |workspace, window, cx| {
8430            workspace.add_item(
8431                pane.clone(),
8432                Box::new(dirty_regular_buffer.clone()),
8433                None,
8434                false,
8435                false,
8436                window,
8437                cx,
8438            );
8439            workspace.add_item(
8440                pane.clone(),
8441                Box::new(dirty_regular_buffer_2.clone()),
8442                None,
8443                false,
8444                false,
8445                window,
8446                cx,
8447            );
8448            workspace.add_item(
8449                pane.clone(),
8450                Box::new(dirty_multi_buffer.clone()),
8451                None,
8452                false,
8453                false,
8454                window,
8455                cx,
8456            );
8457        });
8458
8459        pane.update_in(cx, |pane, window, cx| {
8460            pane.activate_item(2, true, true, window, cx);
8461            assert_eq!(
8462                pane.active_item().unwrap().item_id(),
8463                dirty_multi_buffer.item_id(),
8464                "Should select the multi buffer in the pane"
8465            );
8466        });
8467        let close_multi_buffer_task = pane
8468            .update_in(cx, |pane, window, cx| {
8469                pane.close_active_item(
8470                    &CloseActiveItem {
8471                        save_intent: None,
8472                        close_pinned: false,
8473                    },
8474                    window,
8475                    cx,
8476                )
8477            })
8478            .expect("should have active multi buffer to close");
8479        cx.background_executor.run_until_parked();
8480        assert!(
8481            !cx.has_pending_prompt(),
8482            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8483        );
8484        close_multi_buffer_task
8485            .await
8486            .expect("Closing multi buffer failed");
8487        pane.update(cx, |pane, cx| {
8488            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8489            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8490            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8491            assert_eq!(
8492                pane.items()
8493                    .map(|item| item.item_id())
8494                    .sorted()
8495                    .collect::<Vec<_>>(),
8496                vec![
8497                    dirty_regular_buffer.item_id(),
8498                    dirty_regular_buffer_2.item_id(),
8499                ],
8500                "Should have no multi buffer left in the pane"
8501            );
8502            assert!(dirty_regular_buffer.read(cx).is_dirty);
8503            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8504        });
8505    }
8506
8507    #[gpui::test]
8508    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8509        init_test(cx);
8510        let fs = FakeFs::new(cx.executor());
8511        let project = Project::test(fs, [], cx).await;
8512        let (workspace, cx) =
8513            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8514
8515        // Add a new panel to the right dock, opening the dock and setting the
8516        // focus to the new panel.
8517        let panel = workspace.update_in(cx, |workspace, window, cx| {
8518            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8519            workspace.add_panel(panel.clone(), window, cx);
8520
8521            workspace
8522                .right_dock()
8523                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8524
8525            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8526
8527            panel
8528        });
8529
8530        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8531        // panel to the next valid position which, in this case, is the left
8532        // dock.
8533        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8534        workspace.update(cx, |workspace, cx| {
8535            assert!(workspace.left_dock().read(cx).is_open());
8536            assert_eq!(panel.read(cx).position, DockPosition::Left);
8537        });
8538
8539        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8540        // panel to the next valid position which, in this case, is the bottom
8541        // dock.
8542        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8543        workspace.update(cx, |workspace, cx| {
8544            assert!(workspace.bottom_dock().read(cx).is_open());
8545            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8546        });
8547
8548        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8549        // around moving the panel to its initial position, the right dock.
8550        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8551        workspace.update(cx, |workspace, cx| {
8552            assert!(workspace.right_dock().read(cx).is_open());
8553            assert_eq!(panel.read(cx).position, DockPosition::Right);
8554        });
8555
8556        // Remove focus from the panel, ensuring that, if the panel is not
8557        // focused, the `MoveFocusedPanelToNextPosition` action does not update
8558        // the panel's position, so the panel is still in the right dock.
8559        workspace.update_in(cx, |workspace, window, cx| {
8560            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8561        });
8562
8563        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8564        workspace.update(cx, |workspace, cx| {
8565            assert!(workspace.right_dock().read(cx).is_open());
8566            assert_eq!(panel.read(cx).position, DockPosition::Right);
8567        });
8568    }
8569
8570    mod register_project_item_tests {
8571
8572        use super::*;
8573
8574        // View
8575        struct TestPngItemView {
8576            focus_handle: FocusHandle,
8577        }
8578        // Model
8579        struct TestPngItem {}
8580
8581        impl project::ProjectItem for TestPngItem {
8582            fn try_open(
8583                _project: &Entity<Project>,
8584                path: &ProjectPath,
8585                cx: &mut App,
8586            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8587                if path.path.extension().unwrap() == "png" {
8588                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8589                } else {
8590                    None
8591                }
8592            }
8593
8594            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8595                None
8596            }
8597
8598            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8599                None
8600            }
8601
8602            fn is_dirty(&self) -> bool {
8603                false
8604            }
8605        }
8606
8607        impl Item for TestPngItemView {
8608            type Event = ();
8609        }
8610        impl EventEmitter<()> for TestPngItemView {}
8611        impl Focusable for TestPngItemView {
8612            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8613                self.focus_handle.clone()
8614            }
8615        }
8616
8617        impl Render for TestPngItemView {
8618            fn render(
8619                &mut self,
8620                _window: &mut Window,
8621                _cx: &mut Context<Self>,
8622            ) -> impl IntoElement {
8623                Empty
8624            }
8625        }
8626
8627        impl ProjectItem for TestPngItemView {
8628            type Item = TestPngItem;
8629
8630            fn for_project_item(
8631                _project: Entity<Project>,
8632                _item: Entity<Self::Item>,
8633                _: &mut Window,
8634                cx: &mut Context<Self>,
8635            ) -> Self
8636            where
8637                Self: Sized,
8638            {
8639                Self {
8640                    focus_handle: cx.focus_handle(),
8641                }
8642            }
8643        }
8644
8645        // View
8646        struct TestIpynbItemView {
8647            focus_handle: FocusHandle,
8648        }
8649        // Model
8650        struct TestIpynbItem {}
8651
8652        impl project::ProjectItem for TestIpynbItem {
8653            fn try_open(
8654                _project: &Entity<Project>,
8655                path: &ProjectPath,
8656                cx: &mut App,
8657            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8658                if path.path.extension().unwrap() == "ipynb" {
8659                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8660                } else {
8661                    None
8662                }
8663            }
8664
8665            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8666                None
8667            }
8668
8669            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8670                None
8671            }
8672
8673            fn is_dirty(&self) -> bool {
8674                false
8675            }
8676        }
8677
8678        impl Item for TestIpynbItemView {
8679            type Event = ();
8680        }
8681        impl EventEmitter<()> for TestIpynbItemView {}
8682        impl Focusable for TestIpynbItemView {
8683            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8684                self.focus_handle.clone()
8685            }
8686        }
8687
8688        impl Render for TestIpynbItemView {
8689            fn render(
8690                &mut self,
8691                _window: &mut Window,
8692                _cx: &mut Context<Self>,
8693            ) -> impl IntoElement {
8694                Empty
8695            }
8696        }
8697
8698        impl ProjectItem for TestIpynbItemView {
8699            type Item = TestIpynbItem;
8700
8701            fn for_project_item(
8702                _project: Entity<Project>,
8703                _item: Entity<Self::Item>,
8704                _: &mut Window,
8705                cx: &mut Context<Self>,
8706            ) -> Self
8707            where
8708                Self: Sized,
8709            {
8710                Self {
8711                    focus_handle: cx.focus_handle(),
8712                }
8713            }
8714        }
8715
8716        struct TestAlternatePngItemView {
8717            focus_handle: FocusHandle,
8718        }
8719
8720        impl Item for TestAlternatePngItemView {
8721            type Event = ();
8722        }
8723
8724        impl EventEmitter<()> for TestAlternatePngItemView {}
8725        impl Focusable for TestAlternatePngItemView {
8726            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8727                self.focus_handle.clone()
8728            }
8729        }
8730
8731        impl Render for TestAlternatePngItemView {
8732            fn render(
8733                &mut self,
8734                _window: &mut Window,
8735                _cx: &mut Context<Self>,
8736            ) -> impl IntoElement {
8737                Empty
8738            }
8739        }
8740
8741        impl ProjectItem for TestAlternatePngItemView {
8742            type Item = TestPngItem;
8743
8744            fn for_project_item(
8745                _project: Entity<Project>,
8746                _item: Entity<Self::Item>,
8747                _: &mut Window,
8748                cx: &mut Context<Self>,
8749            ) -> Self
8750            where
8751                Self: Sized,
8752            {
8753                Self {
8754                    focus_handle: cx.focus_handle(),
8755                }
8756            }
8757        }
8758
8759        #[gpui::test]
8760        async fn test_register_project_item(cx: &mut TestAppContext) {
8761            init_test(cx);
8762
8763            cx.update(|cx| {
8764                register_project_item::<TestPngItemView>(cx);
8765                register_project_item::<TestIpynbItemView>(cx);
8766            });
8767
8768            let fs = FakeFs::new(cx.executor());
8769            fs.insert_tree(
8770                "/root1",
8771                json!({
8772                    "one.png": "BINARYDATAHERE",
8773                    "two.ipynb": "{ totally a notebook }",
8774                    "three.txt": "editing text, sure why not?"
8775                }),
8776            )
8777            .await;
8778
8779            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8780            let (workspace, cx) =
8781                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8782
8783            let worktree_id = project.update(cx, |project, cx| {
8784                project.worktrees(cx).next().unwrap().read(cx).id()
8785            });
8786
8787            let handle = workspace
8788                .update_in(cx, |workspace, window, cx| {
8789                    let project_path = (worktree_id, "one.png");
8790                    workspace.open_path(project_path, None, true, window, cx)
8791                })
8792                .await
8793                .unwrap();
8794
8795            // Now we can check if the handle we got back errored or not
8796            assert_eq!(
8797                handle.to_any().entity_type(),
8798                TypeId::of::<TestPngItemView>()
8799            );
8800
8801            let handle = workspace
8802                .update_in(cx, |workspace, window, cx| {
8803                    let project_path = (worktree_id, "two.ipynb");
8804                    workspace.open_path(project_path, None, true, window, cx)
8805                })
8806                .await
8807                .unwrap();
8808
8809            assert_eq!(
8810                handle.to_any().entity_type(),
8811                TypeId::of::<TestIpynbItemView>()
8812            );
8813
8814            let handle = workspace
8815                .update_in(cx, |workspace, window, cx| {
8816                    let project_path = (worktree_id, "three.txt");
8817                    workspace.open_path(project_path, None, true, window, cx)
8818                })
8819                .await;
8820            assert!(handle.is_err());
8821        }
8822
8823        #[gpui::test]
8824        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8825            init_test(cx);
8826
8827            cx.update(|cx| {
8828                register_project_item::<TestPngItemView>(cx);
8829                register_project_item::<TestAlternatePngItemView>(cx);
8830            });
8831
8832            let fs = FakeFs::new(cx.executor());
8833            fs.insert_tree(
8834                "/root1",
8835                json!({
8836                    "one.png": "BINARYDATAHERE",
8837                    "two.ipynb": "{ totally a notebook }",
8838                    "three.txt": "editing text, sure why not?"
8839                }),
8840            )
8841            .await;
8842            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8843            let (workspace, cx) =
8844                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8845            let worktree_id = project.update(cx, |project, cx| {
8846                project.worktrees(cx).next().unwrap().read(cx).id()
8847            });
8848
8849            let handle = workspace
8850                .update_in(cx, |workspace, window, cx| {
8851                    let project_path = (worktree_id, "one.png");
8852                    workspace.open_path(project_path, None, true, window, cx)
8853                })
8854                .await
8855                .unwrap();
8856
8857            // This _must_ be the second item registered
8858            assert_eq!(
8859                handle.to_any().entity_type(),
8860                TypeId::of::<TestAlternatePngItemView>()
8861            );
8862
8863            let handle = workspace
8864                .update_in(cx, |workspace, window, cx| {
8865                    let project_path = (worktree_id, "three.txt");
8866                    workspace.open_path(project_path, None, true, window, cx)
8867                })
8868                .await;
8869            assert!(handle.is_err());
8870        }
8871    }
8872
8873    pub fn init_test(cx: &mut TestAppContext) {
8874        cx.update(|cx| {
8875            let settings_store = SettingsStore::test(cx);
8876            cx.set_global(settings_store);
8877            theme::init(theme::LoadThemes::JustBase, cx);
8878            language::init(cx);
8879            crate::init_settings(cx);
8880            Project::init_settings(cx);
8881        });
8882    }
8883
8884    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8885        let item = TestProjectItem::new(id, path, cx);
8886        item.update(cx, |item, _| {
8887            item.is_dirty = true;
8888        });
8889        item
8890    }
8891}