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