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