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