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