workspace.rs

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