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