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