workspace.rs

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