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(&mut self, axis: gpui::Axis, amount: Pixels, cx: &mut Context<Self>) {
3244        self.center
3245            .resize(&self.active_pane, axis, amount, &self.bounds);
3246        cx.notify();
3247    }
3248
3249    pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
3250        self.center.reset_pane_sizes();
3251        cx.notify();
3252    }
3253
3254    fn handle_pane_focused(
3255        &mut self,
3256        pane: Entity<Pane>,
3257        window: &mut Window,
3258        cx: &mut Context<Self>,
3259    ) {
3260        // This is explicitly hoisted out of the following check for pane identity as
3261        // terminal panel panes are not registered as a center panes.
3262        self.status_bar.update(cx, |status_bar, cx| {
3263            status_bar.set_active_pane(&pane, window, cx);
3264        });
3265        if self.active_pane != pane {
3266            self.set_active_pane(&pane, window, cx);
3267        }
3268
3269        if self.last_active_center_pane.is_none() {
3270            self.last_active_center_pane = Some(pane.downgrade());
3271        }
3272
3273        self.dismiss_zoomed_items_to_reveal(None, window, cx);
3274        if pane.read(cx).is_zoomed() {
3275            self.zoomed = Some(pane.downgrade().into());
3276        } else {
3277            self.zoomed = None;
3278        }
3279        self.zoomed_position = None;
3280        cx.emit(Event::ZoomChanged);
3281        self.update_active_view_for_followers(window, cx);
3282        pane.update(cx, |pane, _| {
3283            pane.track_alternate_file_items();
3284        });
3285
3286        cx.notify();
3287    }
3288
3289    fn set_active_pane(
3290        &mut self,
3291        pane: &Entity<Pane>,
3292        window: &mut Window,
3293        cx: &mut Context<Self>,
3294    ) {
3295        self.active_pane = pane.clone();
3296        self.active_item_path_changed(window, cx);
3297        self.last_active_center_pane = Some(pane.downgrade());
3298    }
3299
3300    fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3301        self.update_active_view_for_followers(window, cx);
3302    }
3303
3304    fn handle_pane_event(
3305        &mut self,
3306        pane: &Entity<Pane>,
3307        event: &pane::Event,
3308        window: &mut Window,
3309        cx: &mut Context<Self>,
3310    ) {
3311        let mut serialize_workspace = true;
3312        match event {
3313            pane::Event::AddItem { item } => {
3314                item.added_to_pane(self, pane.clone(), window, cx);
3315                cx.emit(Event::ItemAdded {
3316                    item: item.boxed_clone(),
3317                });
3318            }
3319            pane::Event::Split(direction) => {
3320                self.split_and_clone(pane.clone(), *direction, window, cx);
3321            }
3322            pane::Event::JoinIntoNext => {
3323                self.join_pane_into_next(pane.clone(), window, cx);
3324            }
3325            pane::Event::JoinAll => {
3326                self.join_all_panes(window, cx);
3327            }
3328            pane::Event::Remove { focus_on_pane } => {
3329                self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx);
3330            }
3331            pane::Event::ActivateItem {
3332                local,
3333                focus_changed,
3334            } => {
3335                cx.on_next_frame(window, |_, window, _| {
3336                    window.invalidate_character_coordinates();
3337                });
3338
3339                pane.update(cx, |pane, _| {
3340                    pane.track_alternate_file_items();
3341                });
3342                if *local {
3343                    self.unfollow_in_pane(&pane, window, cx);
3344                }
3345                if pane == self.active_pane() {
3346                    self.active_item_path_changed(window, cx);
3347                    self.update_active_view_for_followers(window, cx);
3348                }
3349                serialize_workspace = *focus_changed || pane != self.active_pane();
3350            }
3351            pane::Event::UserSavedItem { item, save_intent } => {
3352                cx.emit(Event::UserSavedItem {
3353                    pane: pane.downgrade(),
3354                    item: item.boxed_clone(),
3355                    save_intent: *save_intent,
3356                });
3357                serialize_workspace = false;
3358            }
3359            pane::Event::ChangeItemTitle => {
3360                if *pane == self.active_pane {
3361                    self.active_item_path_changed(window, cx);
3362                }
3363                self.update_window_edited(window, cx);
3364                serialize_workspace = false;
3365            }
3366            pane::Event::RemoveItem { .. } => {}
3367            pane::Event::RemovedItem { item_id } => {
3368                cx.emit(Event::ActiveItemChanged);
3369                self.update_window_edited(window, cx);
3370                if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3371                    if entry.get().entity_id() == pane.entity_id() {
3372                        entry.remove();
3373                    }
3374                }
3375            }
3376            pane::Event::Focus => {
3377                cx.on_next_frame(window, |_, window, _| {
3378                    window.invalidate_character_coordinates();
3379                });
3380                self.handle_pane_focused(pane.clone(), window, cx);
3381            }
3382            pane::Event::ZoomIn => {
3383                if *pane == self.active_pane {
3384                    pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3385                    if pane.read(cx).has_focus(window, cx) {
3386                        self.zoomed = Some(pane.downgrade().into());
3387                        self.zoomed_position = None;
3388                        cx.emit(Event::ZoomChanged);
3389                    }
3390                    cx.notify();
3391                }
3392            }
3393            pane::Event::ZoomOut => {
3394                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3395                if self.zoomed_position.is_none() {
3396                    self.zoomed = None;
3397                    cx.emit(Event::ZoomChanged);
3398                }
3399                cx.notify();
3400            }
3401        }
3402
3403        if serialize_workspace {
3404            self.serialize_workspace(window, cx);
3405        }
3406    }
3407
3408    pub fn unfollow_in_pane(
3409        &mut self,
3410        pane: &Entity<Pane>,
3411        window: &mut Window,
3412        cx: &mut Context<Workspace>,
3413    ) -> Option<PeerId> {
3414        let leader_id = self.leader_for_pane(pane)?;
3415        self.unfollow(leader_id, window, cx);
3416        Some(leader_id)
3417    }
3418
3419    pub fn split_pane(
3420        &mut self,
3421        pane_to_split: Entity<Pane>,
3422        split_direction: SplitDirection,
3423        window: &mut Window,
3424        cx: &mut Context<Self>,
3425    ) -> Entity<Pane> {
3426        let new_pane = self.add_pane(window, cx);
3427        self.center
3428            .split(&pane_to_split, &new_pane, split_direction)
3429            .unwrap();
3430        cx.notify();
3431        new_pane
3432    }
3433
3434    pub fn split_and_clone(
3435        &mut self,
3436        pane: Entity<Pane>,
3437        direction: SplitDirection,
3438        window: &mut Window,
3439        cx: &mut Context<Self>,
3440    ) -> Option<Entity<Pane>> {
3441        let item = pane.read(cx).active_item()?;
3442        let maybe_pane_handle =
3443            if let Some(clone) = item.clone_on_split(self.database_id(), window, cx) {
3444                let new_pane = self.add_pane(window, cx);
3445                new_pane.update(cx, |pane, cx| {
3446                    pane.add_item(clone, true, true, None, window, cx)
3447                });
3448                self.center.split(&pane, &new_pane, direction).unwrap();
3449                Some(new_pane)
3450            } else {
3451                None
3452            };
3453        cx.notify();
3454        maybe_pane_handle
3455    }
3456
3457    pub fn split_pane_with_item(
3458        &mut self,
3459        pane_to_split: WeakEntity<Pane>,
3460        split_direction: SplitDirection,
3461        from: WeakEntity<Pane>,
3462        item_id_to_move: EntityId,
3463        window: &mut Window,
3464        cx: &mut Context<Self>,
3465    ) {
3466        let Some(pane_to_split) = pane_to_split.upgrade() else {
3467            return;
3468        };
3469        let Some(from) = from.upgrade() else {
3470            return;
3471        };
3472
3473        let new_pane = self.add_pane(window, cx);
3474        move_item(&from, &new_pane, item_id_to_move, 0, window, cx);
3475        self.center
3476            .split(&pane_to_split, &new_pane, split_direction)
3477            .unwrap();
3478        cx.notify();
3479    }
3480
3481    pub fn split_pane_with_project_entry(
3482        &mut self,
3483        pane_to_split: WeakEntity<Pane>,
3484        split_direction: SplitDirection,
3485        project_entry: ProjectEntryId,
3486        window: &mut Window,
3487        cx: &mut Context<Self>,
3488    ) -> Option<Task<Result<()>>> {
3489        let pane_to_split = pane_to_split.upgrade()?;
3490        let new_pane = self.add_pane(window, cx);
3491        self.center
3492            .split(&pane_to_split, &new_pane, split_direction)
3493            .unwrap();
3494
3495        let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3496        let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx);
3497        Some(cx.foreground_executor().spawn(async move {
3498            task.await?;
3499            Ok(())
3500        }))
3501    }
3502
3503    pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3504        let active_item = self.active_pane.read(cx).active_item();
3505        for pane in &self.panes {
3506            join_pane_into_active(&self.active_pane, pane, window, cx);
3507        }
3508        if let Some(active_item) = active_item {
3509            self.activate_item(active_item.as_ref(), true, true, window, cx);
3510        }
3511        cx.notify();
3512    }
3513
3514    pub fn join_pane_into_next(
3515        &mut self,
3516        pane: Entity<Pane>,
3517        window: &mut Window,
3518        cx: &mut Context<Self>,
3519    ) {
3520        let next_pane = self
3521            .find_pane_in_direction(SplitDirection::Right, cx)
3522            .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3523            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3524            .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3525        let Some(next_pane) = next_pane else {
3526            return;
3527        };
3528        move_all_items(&pane, &next_pane, window, cx);
3529        cx.notify();
3530    }
3531
3532    fn remove_pane(
3533        &mut self,
3534        pane: Entity<Pane>,
3535        focus_on: Option<Entity<Pane>>,
3536        window: &mut Window,
3537        cx: &mut Context<Self>,
3538    ) {
3539        if self.center.remove(&pane).unwrap() {
3540            self.force_remove_pane(&pane, &focus_on, window, cx);
3541            self.unfollow_in_pane(&pane, window, cx);
3542            self.last_leaders_by_pane.remove(&pane.downgrade());
3543            for removed_item in pane.read(cx).items() {
3544                self.panes_by_item.remove(&removed_item.item_id());
3545            }
3546
3547            cx.notify();
3548        } else {
3549            self.active_item_path_changed(window, cx);
3550        }
3551        cx.emit(Event::PaneRemoved);
3552    }
3553
3554    pub fn panes(&self) -> &[Entity<Pane>] {
3555        &self.panes
3556    }
3557
3558    pub fn active_pane(&self) -> &Entity<Pane> {
3559        &self.active_pane
3560    }
3561
3562    pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity<Pane> {
3563        for dock in self.all_docks() {
3564            if dock.focus_handle(cx).contains_focused(window, cx) {
3565                if let Some(pane) = dock
3566                    .read(cx)
3567                    .active_panel()
3568                    .and_then(|panel| panel.pane(cx))
3569                {
3570                    return pane;
3571                }
3572            }
3573        }
3574        self.active_pane().clone()
3575    }
3576
3577    pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<Pane> {
3578        self.find_pane_in_direction(SplitDirection::Right, cx)
3579            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3580            .unwrap_or_else(|| {
3581                self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx)
3582            })
3583            .clone()
3584    }
3585
3586    pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<Entity<Pane>> {
3587        let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3588        weak_pane.upgrade()
3589    }
3590
3591    fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
3592        self.follower_states.retain(|leader_id, state| {
3593            if *leader_id == peer_id {
3594                for item in state.items_by_leader_view_id.values() {
3595                    item.view.set_leader_peer_id(None, window, cx);
3596                }
3597                false
3598            } else {
3599                true
3600            }
3601        });
3602        cx.notify();
3603    }
3604
3605    pub fn start_following(
3606        &mut self,
3607        leader_id: PeerId,
3608        window: &mut Window,
3609        cx: &mut Context<Self>,
3610    ) -> Option<Task<Result<()>>> {
3611        let pane = self.active_pane().clone();
3612
3613        self.last_leaders_by_pane
3614            .insert(pane.downgrade(), leader_id);
3615        self.unfollow(leader_id, window, cx);
3616        self.unfollow_in_pane(&pane, window, cx);
3617        self.follower_states.insert(
3618            leader_id,
3619            FollowerState {
3620                center_pane: pane.clone(),
3621                dock_pane: None,
3622                active_view_id: None,
3623                items_by_leader_view_id: Default::default(),
3624            },
3625        );
3626        cx.notify();
3627
3628        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3629        let project_id = self.project.read(cx).remote_id();
3630        let request = self.app_state.client.request(proto::Follow {
3631            room_id,
3632            project_id,
3633            leader_id: Some(leader_id),
3634        });
3635
3636        Some(cx.spawn_in(window, |this, mut cx| async move {
3637            let response = request.await?;
3638            this.update(&mut cx, |this, _| {
3639                let state = this
3640                    .follower_states
3641                    .get_mut(&leader_id)
3642                    .ok_or_else(|| anyhow!("following interrupted"))?;
3643                state.active_view_id = response
3644                    .active_view
3645                    .as_ref()
3646                    .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3647                Ok::<_, anyhow::Error>(())
3648            })??;
3649            if let Some(view) = response.active_view {
3650                Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3651            }
3652            this.update_in(&mut cx, |this, window, cx| {
3653                this.leader_updated(leader_id, window, cx)
3654            })?;
3655            Ok(())
3656        }))
3657    }
3658
3659    pub fn follow_next_collaborator(
3660        &mut self,
3661        _: &FollowNextCollaborator,
3662        window: &mut Window,
3663        cx: &mut Context<Self>,
3664    ) {
3665        let collaborators = self.project.read(cx).collaborators();
3666        let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3667            let mut collaborators = collaborators.keys().copied();
3668            for peer_id in collaborators.by_ref() {
3669                if peer_id == leader_id {
3670                    break;
3671                }
3672            }
3673            collaborators.next()
3674        } else if let Some(last_leader_id) =
3675            self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3676        {
3677            if collaborators.contains_key(last_leader_id) {
3678                Some(*last_leader_id)
3679            } else {
3680                None
3681            }
3682        } else {
3683            None
3684        };
3685
3686        let pane = self.active_pane.clone();
3687        let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3688        else {
3689            return;
3690        };
3691        if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) {
3692            return;
3693        }
3694        if let Some(task) = self.start_following(leader_id, window, cx) {
3695            task.detach_and_log_err(cx)
3696        }
3697    }
3698
3699    pub fn follow(&mut self, leader_id: PeerId, window: &mut Window, cx: &mut Context<Self>) {
3700        let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3701            return;
3702        };
3703        let room = room.read(cx);
3704        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3705            return;
3706        };
3707
3708        let project = self.project.read(cx);
3709
3710        let other_project_id = match remote_participant.location {
3711            call::ParticipantLocation::External => None,
3712            call::ParticipantLocation::UnsharedProject => None,
3713            call::ParticipantLocation::SharedProject { project_id } => {
3714                if Some(project_id) == project.remote_id() {
3715                    None
3716                } else {
3717                    Some(project_id)
3718                }
3719            }
3720        };
3721
3722        // if they are active in another project, follow there.
3723        if let Some(project_id) = other_project_id {
3724            let app_state = self.app_state.clone();
3725            crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3726                .detach_and_log_err(cx);
3727        }
3728
3729        // if you're already following, find the right pane and focus it.
3730        if let Some(follower_state) = self.follower_states.get(&leader_id) {
3731            window.focus(&follower_state.pane().focus_handle(cx));
3732
3733            return;
3734        }
3735
3736        // Otherwise, follow.
3737        if let Some(task) = self.start_following(leader_id, window, cx) {
3738            task.detach_and_log_err(cx)
3739        }
3740    }
3741
3742    pub fn unfollow(
3743        &mut self,
3744        leader_id: PeerId,
3745        window: &mut Window,
3746        cx: &mut Context<Self>,
3747    ) -> Option<()> {
3748        cx.notify();
3749        let state = self.follower_states.remove(&leader_id)?;
3750        for (_, item) in state.items_by_leader_view_id {
3751            item.view.set_leader_peer_id(None, window, cx);
3752        }
3753
3754        let project_id = self.project.read(cx).remote_id();
3755        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3756        self.app_state
3757            .client
3758            .send(proto::Unfollow {
3759                room_id,
3760                project_id,
3761                leader_id: Some(leader_id),
3762            })
3763            .log_err();
3764
3765        Some(())
3766    }
3767
3768    pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3769        self.follower_states.contains_key(&peer_id)
3770    }
3771
3772    fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3773        cx.emit(Event::ActiveItemChanged);
3774        let active_entry = self.active_project_path(cx);
3775        self.project
3776            .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3777
3778        self.update_window_title(window, cx);
3779    }
3780
3781    fn update_window_title(&mut self, window: &mut Window, cx: &mut App) {
3782        let project = self.project().read(cx);
3783        let mut title = String::new();
3784
3785        for (i, name) in project.worktree_root_names(cx).enumerate() {
3786            if i > 0 {
3787                title.push_str(", ");
3788            }
3789            title.push_str(name);
3790        }
3791
3792        if title.is_empty() {
3793            title = "empty project".to_string();
3794        }
3795
3796        if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3797            let filename = path
3798                .path
3799                .file_name()
3800                .map(|s| s.to_string_lossy())
3801                .or_else(|| {
3802                    Some(Cow::Borrowed(
3803                        project
3804                            .worktree_for_id(path.worktree_id, cx)?
3805                            .read(cx)
3806                            .root_name(),
3807                    ))
3808                });
3809
3810            if let Some(filename) = filename {
3811                title.push_str("");
3812                title.push_str(filename.as_ref());
3813            }
3814        }
3815
3816        if project.is_via_collab() {
3817            title.push_str("");
3818        } else if project.is_shared() {
3819            title.push_str("");
3820        }
3821
3822        window.set_window_title(&title);
3823    }
3824
3825    fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) {
3826        let is_edited = !self.project.read(cx).is_disconnected(cx)
3827            && self
3828                .items(cx)
3829                .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3830        if is_edited != self.window_edited {
3831            self.window_edited = is_edited;
3832            window.set_window_edited(self.window_edited)
3833        }
3834    }
3835
3836    fn render_notifications(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Option<Div> {
3837        if self.notifications.is_empty() {
3838            None
3839        } else {
3840            Some(
3841                div()
3842                    .absolute()
3843                    .right_3()
3844                    .bottom_3()
3845                    .w_112()
3846                    .h_full()
3847                    .flex()
3848                    .flex_col()
3849                    .justify_end()
3850                    .gap_2()
3851                    .children(
3852                        self.notifications
3853                            .iter()
3854                            .map(|(_, notification)| notification.clone().into_any()),
3855                    ),
3856            )
3857        }
3858    }
3859
3860    // RPC handlers
3861
3862    fn active_view_for_follower(
3863        &self,
3864        follower_project_id: Option<u64>,
3865        window: &mut Window,
3866        cx: &mut Context<Self>,
3867    ) -> Option<proto::View> {
3868        let (item, panel_id) = self.active_item_for_followers(window, cx);
3869        let item = item?;
3870        let leader_id = self
3871            .pane_for(&*item)
3872            .and_then(|pane| self.leader_for_pane(&pane));
3873
3874        let item_handle = item.to_followable_item_handle(cx)?;
3875        let id = item_handle.remote_id(&self.app_state.client, window, cx)?;
3876        let variant = item_handle.to_state_proto(window, cx)?;
3877
3878        if item_handle.is_project_item(window, cx)
3879            && (follower_project_id.is_none()
3880                || follower_project_id != self.project.read(cx).remote_id())
3881        {
3882            return None;
3883        }
3884
3885        Some(proto::View {
3886            id: Some(id.to_proto()),
3887            leader_id,
3888            variant: Some(variant),
3889            panel_id: panel_id.map(|id| id as i32),
3890        })
3891    }
3892
3893    fn handle_follow(
3894        &mut self,
3895        follower_project_id: Option<u64>,
3896        window: &mut Window,
3897        cx: &mut Context<Self>,
3898    ) -> proto::FollowResponse {
3899        let active_view = self.active_view_for_follower(follower_project_id, window, cx);
3900
3901        cx.notify();
3902        proto::FollowResponse {
3903            // TODO: Remove after version 0.145.x stabilizes.
3904            active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3905            views: active_view.iter().cloned().collect(),
3906            active_view,
3907        }
3908    }
3909
3910    fn handle_update_followers(
3911        &mut self,
3912        leader_id: PeerId,
3913        message: proto::UpdateFollowers,
3914        _window: &mut Window,
3915        _cx: &mut Context<Self>,
3916    ) {
3917        self.leader_updates_tx
3918            .unbounded_send((leader_id, message))
3919            .ok();
3920    }
3921
3922    async fn process_leader_update(
3923        this: &WeakEntity<Self>,
3924        leader_id: PeerId,
3925        update: proto::UpdateFollowers,
3926        cx: &mut AsyncWindowContext,
3927    ) -> Result<()> {
3928        match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3929            proto::update_followers::Variant::CreateView(view) => {
3930                let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3931                let should_add_view = this.update(cx, |this, _| {
3932                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3933                        anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3934                    } else {
3935                        anyhow::Ok(false)
3936                    }
3937                })??;
3938
3939                if should_add_view {
3940                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3941                }
3942            }
3943            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3944                let should_add_view = this.update(cx, |this, _| {
3945                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3946                        state.active_view_id = update_active_view
3947                            .view
3948                            .as_ref()
3949                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3950
3951                        if state.active_view_id.is_some_and(|view_id| {
3952                            !state.items_by_leader_view_id.contains_key(&view_id)
3953                        }) {
3954                            anyhow::Ok(true)
3955                        } else {
3956                            anyhow::Ok(false)
3957                        }
3958                    } else {
3959                        anyhow::Ok(false)
3960                    }
3961                })??;
3962
3963                if should_add_view {
3964                    if let Some(view) = update_active_view.view {
3965                        Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3966                    }
3967                }
3968            }
3969            proto::update_followers::Variant::UpdateView(update_view) => {
3970                let variant = update_view
3971                    .variant
3972                    .ok_or_else(|| anyhow!("missing update view variant"))?;
3973                let id = update_view
3974                    .id
3975                    .ok_or_else(|| anyhow!("missing update view id"))?;
3976                let mut tasks = Vec::new();
3977                this.update_in(cx, |this, window, cx| {
3978                    let project = this.project.clone();
3979                    if let Some(state) = this.follower_states.get(&leader_id) {
3980                        let view_id = ViewId::from_proto(id.clone())?;
3981                        if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3982                            tasks.push(item.view.apply_update_proto(
3983                                &project,
3984                                variant.clone(),
3985                                window,
3986                                cx,
3987                            ));
3988                        }
3989                    }
3990                    anyhow::Ok(())
3991                })??;
3992                try_join_all(tasks).await.log_err();
3993            }
3994        }
3995        this.update_in(cx, |this, window, cx| {
3996            this.leader_updated(leader_id, window, cx)
3997        })?;
3998        Ok(())
3999    }
4000
4001    async fn add_view_from_leader(
4002        this: WeakEntity<Self>,
4003        leader_id: PeerId,
4004        view: &proto::View,
4005        cx: &mut AsyncWindowContext,
4006    ) -> Result<()> {
4007        let this = this.upgrade().context("workspace dropped")?;
4008
4009        let Some(id) = view.id.clone() else {
4010            return Err(anyhow!("no id for view"));
4011        };
4012        let id = ViewId::from_proto(id)?;
4013        let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
4014
4015        let pane = this.update(cx, |this, _cx| {
4016            let state = this
4017                .follower_states
4018                .get(&leader_id)
4019                .context("stopped following")?;
4020            anyhow::Ok(state.pane().clone())
4021        })??;
4022        let existing_item = pane.update_in(cx, |pane, window, cx| {
4023            let client = this.read(cx).client().clone();
4024            pane.items().find_map(|item| {
4025                let item = item.to_followable_item_handle(cx)?;
4026                if item.remote_id(&client, window, cx) == Some(id) {
4027                    Some(item)
4028                } else {
4029                    None
4030                }
4031            })
4032        })?;
4033        let item = if let Some(existing_item) = existing_item {
4034            existing_item
4035        } else {
4036            let variant = view.variant.clone();
4037            if variant.is_none() {
4038                Err(anyhow!("missing view variant"))?;
4039            }
4040
4041            let task = cx.update(|window, cx| {
4042                FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx)
4043            })?;
4044
4045            let Some(task) = task else {
4046                return Err(anyhow!(
4047                    "failed to construct view from leader (maybe from a different version of zed?)"
4048                ));
4049            };
4050
4051            let mut new_item = task.await?;
4052            pane.update_in(cx, |pane, window, cx| {
4053                let mut item_to_remove = None;
4054                for (ix, item) in pane.items().enumerate() {
4055                    if let Some(item) = item.to_followable_item_handle(cx) {
4056                        match new_item.dedup(item.as_ref(), window, cx) {
4057                            Some(item::Dedup::KeepExisting) => {
4058                                new_item =
4059                                    item.boxed_clone().to_followable_item_handle(cx).unwrap();
4060                                break;
4061                            }
4062                            Some(item::Dedup::ReplaceExisting) => {
4063                                item_to_remove = Some((ix, item.item_id()));
4064                                break;
4065                            }
4066                            None => {}
4067                        }
4068                    }
4069                }
4070
4071                if let Some((ix, id)) = item_to_remove {
4072                    pane.remove_item(id, false, false, window, cx);
4073                    pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx);
4074                }
4075            })?;
4076
4077            new_item
4078        };
4079
4080        this.update_in(cx, |this, window, cx| {
4081            let state = this.follower_states.get_mut(&leader_id)?;
4082            item.set_leader_peer_id(Some(leader_id), window, cx);
4083            state.items_by_leader_view_id.insert(
4084                id,
4085                FollowerView {
4086                    view: item,
4087                    location: panel_id,
4088                },
4089            );
4090
4091            Some(())
4092        })?;
4093
4094        Ok(())
4095    }
4096
4097    pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) {
4098        let mut is_project_item = true;
4099        let mut update = proto::UpdateActiveView::default();
4100        if window.is_window_active() {
4101            let (active_item, panel_id) = self.active_item_for_followers(window, cx);
4102
4103            if let Some(item) = active_item {
4104                if item.item_focus_handle(cx).contains_focused(window, cx) {
4105                    let leader_id = self
4106                        .pane_for(&*item)
4107                        .and_then(|pane| self.leader_for_pane(&pane));
4108
4109                    if let Some(item) = item.to_followable_item_handle(cx) {
4110                        let id = item
4111                            .remote_id(&self.app_state.client, window, cx)
4112                            .map(|id| id.to_proto());
4113
4114                        if let Some(id) = id.clone() {
4115                            if let Some(variant) = item.to_state_proto(window, cx) {
4116                                let view = Some(proto::View {
4117                                    id: Some(id.clone()),
4118                                    leader_id,
4119                                    variant: Some(variant),
4120                                    panel_id: panel_id.map(|id| id as i32),
4121                                });
4122
4123                                is_project_item = item.is_project_item(window, cx);
4124                                update = proto::UpdateActiveView {
4125                                    view,
4126                                    // TODO: Remove after version 0.145.x stabilizes.
4127                                    id: Some(id.clone()),
4128                                    leader_id,
4129                                };
4130                            }
4131                        };
4132                    }
4133                }
4134            }
4135        }
4136
4137        let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
4138        if active_view_id != self.last_active_view_id.as_ref() {
4139            self.last_active_view_id = active_view_id.cloned();
4140            self.update_followers(
4141                is_project_item,
4142                proto::update_followers::Variant::UpdateActiveView(update),
4143                window,
4144                cx,
4145            );
4146        }
4147    }
4148
4149    fn active_item_for_followers(
4150        &self,
4151        window: &mut Window,
4152        cx: &mut App,
4153    ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
4154        let mut active_item = None;
4155        let mut panel_id = None;
4156        for dock in self.all_docks() {
4157            if dock.focus_handle(cx).contains_focused(window, cx) {
4158                if let Some(panel) = dock.read(cx).active_panel() {
4159                    if let Some(pane) = panel.pane(cx) {
4160                        if let Some(item) = pane.read(cx).active_item() {
4161                            active_item = Some(item);
4162                            panel_id = panel.remote_id();
4163                            break;
4164                        }
4165                    }
4166                }
4167            }
4168        }
4169
4170        if active_item.is_none() {
4171            active_item = self.active_pane().read(cx).active_item();
4172        }
4173        (active_item, panel_id)
4174    }
4175
4176    fn update_followers(
4177        &self,
4178        project_only: bool,
4179        update: proto::update_followers::Variant,
4180        _: &mut Window,
4181        cx: &mut App,
4182    ) -> Option<()> {
4183        // If this update only applies to for followers in the current project,
4184        // then skip it unless this project is shared. If it applies to all
4185        // followers, regardless of project, then set `project_id` to none,
4186        // indicating that it goes to all followers.
4187        let project_id = if project_only {
4188            Some(self.project.read(cx).remote_id()?)
4189        } else {
4190            None
4191        };
4192        self.app_state().workspace_store.update(cx, |store, cx| {
4193            store.update_followers(project_id, update, cx)
4194        })
4195    }
4196
4197    pub fn leader_for_pane(&self, pane: &Entity<Pane>) -> Option<PeerId> {
4198        self.follower_states.iter().find_map(|(leader_id, state)| {
4199            if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
4200                Some(*leader_id)
4201            } else {
4202                None
4203            }
4204        })
4205    }
4206
4207    fn leader_updated(
4208        &mut self,
4209        leader_id: PeerId,
4210        window: &mut Window,
4211        cx: &mut Context<Self>,
4212    ) -> Option<()> {
4213        cx.notify();
4214
4215        let call = self.active_call()?;
4216        let room = call.read(cx).room()?.read(cx);
4217        let participant = room.remote_participant_for_peer_id(leader_id)?;
4218
4219        let leader_in_this_app;
4220        let leader_in_this_project;
4221        match participant.location {
4222            call::ParticipantLocation::SharedProject { project_id } => {
4223                leader_in_this_app = true;
4224                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
4225            }
4226            call::ParticipantLocation::UnsharedProject => {
4227                leader_in_this_app = true;
4228                leader_in_this_project = false;
4229            }
4230            call::ParticipantLocation::External => {
4231                leader_in_this_app = false;
4232                leader_in_this_project = false;
4233            }
4234        };
4235
4236        let state = self.follower_states.get(&leader_id)?;
4237        let mut item_to_activate = None;
4238        if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
4239            if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
4240                if leader_in_this_project || !item.view.is_project_item(window, cx) {
4241                    item_to_activate = Some((item.location, item.view.boxed_clone()));
4242                }
4243            }
4244        } else if let Some(shared_screen) =
4245            self.shared_screen_for_peer(leader_id, &state.center_pane, window, cx)
4246        {
4247            item_to_activate = Some((None, Box::new(shared_screen)));
4248        }
4249
4250        let (panel_id, item) = item_to_activate?;
4251
4252        let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx);
4253        let pane;
4254        if let Some(panel_id) = panel_id {
4255            pane = self
4256                .activate_panel_for_proto_id(panel_id, window, cx)?
4257                .pane(cx)?;
4258            let state = self.follower_states.get_mut(&leader_id)?;
4259            state.dock_pane = Some(pane.clone());
4260        } else {
4261            pane = state.center_pane.clone();
4262            let state = self.follower_states.get_mut(&leader_id)?;
4263            if let Some(dock_pane) = state.dock_pane.take() {
4264                transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx);
4265            }
4266        }
4267
4268        pane.update(cx, |pane, cx| {
4269            let focus_active_item = pane.has_focus(window, cx) || transfer_focus;
4270            if let Some(index) = pane.index_for_item(item.as_ref()) {
4271                pane.activate_item(index, false, false, window, cx);
4272            } else {
4273                pane.add_item(item.boxed_clone(), false, false, None, window, cx)
4274            }
4275
4276            if focus_active_item {
4277                pane.focus_active_item(window, cx)
4278            }
4279        });
4280
4281        None
4282    }
4283
4284    #[cfg(target_os = "windows")]
4285    fn shared_screen_for_peer(
4286        &self,
4287        _peer_id: PeerId,
4288        _pane: &Entity<Pane>,
4289        _window: &mut Window,
4290        _cx: &mut App,
4291    ) -> Option<Entity<SharedScreen>> {
4292        None
4293    }
4294
4295    #[cfg(not(target_os = "windows"))]
4296    fn shared_screen_for_peer(
4297        &self,
4298        peer_id: PeerId,
4299        pane: &Entity<Pane>,
4300        window: &mut Window,
4301        cx: &mut App,
4302    ) -> Option<Entity<SharedScreen>> {
4303        let call = self.active_call()?;
4304        let room = call.read(cx).room()?.read(cx);
4305        let participant = room.remote_participant_for_peer_id(peer_id)?;
4306        let track = participant.video_tracks.values().next()?.clone();
4307        let user = participant.user.clone();
4308
4309        for item in pane.read(cx).items_of_type::<SharedScreen>() {
4310            if item.read(cx).peer_id == peer_id {
4311                return Some(item);
4312            }
4313        }
4314
4315        Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), window, cx)))
4316    }
4317
4318    pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4319        if window.is_window_active() {
4320            self.update_active_view_for_followers(window, cx);
4321
4322            if let Some(database_id) = self.database_id {
4323                cx.background_executor()
4324                    .spawn(persistence::DB.update_timestamp(database_id))
4325                    .detach();
4326            }
4327        } else {
4328            for pane in &self.panes {
4329                pane.update(cx, |pane, cx| {
4330                    if let Some(item) = pane.active_item() {
4331                        item.workspace_deactivated(window, cx);
4332                    }
4333                    for item in pane.items() {
4334                        if matches!(
4335                            item.workspace_settings(cx).autosave,
4336                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4337                        ) {
4338                            Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx)
4339                                .detach_and_log_err(cx);
4340                        }
4341                    }
4342                });
4343            }
4344        }
4345    }
4346
4347    pub fn active_call(&self) -> Option<&Entity<ActiveCall>> {
4348        self.active_call.as_ref().map(|(call, _)| call)
4349    }
4350
4351    fn on_active_call_event(
4352        &mut self,
4353        _: &Entity<ActiveCall>,
4354        event: &call::room::Event,
4355        window: &mut Window,
4356        cx: &mut Context<Self>,
4357    ) {
4358        match event {
4359            call::room::Event::ParticipantLocationChanged { participant_id }
4360            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4361                self.leader_updated(*participant_id, window, cx);
4362            }
4363            _ => {}
4364        }
4365    }
4366
4367    pub fn database_id(&self) -> Option<WorkspaceId> {
4368        self.database_id
4369    }
4370
4371    fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
4372        let project = self.project().read(cx);
4373
4374        if project.is_local() {
4375            Some(
4376                project
4377                    .visible_worktrees(cx)
4378                    .map(|worktree| worktree.read(cx).abs_path())
4379                    .collect::<Vec<_>>(),
4380            )
4381        } else {
4382            None
4383        }
4384    }
4385
4386    fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
4387        match member {
4388            Member::Axis(PaneAxis { members, .. }) => {
4389                for child in members.iter() {
4390                    self.remove_panes(child.clone(), window, cx)
4391                }
4392            }
4393            Member::Pane(pane) => {
4394                self.force_remove_pane(&pane, &None, window, cx);
4395            }
4396        }
4397    }
4398
4399    fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> {
4400        self.session_id.take();
4401        self.serialize_workspace_internal(window, cx)
4402    }
4403
4404    fn force_remove_pane(
4405        &mut self,
4406        pane: &Entity<Pane>,
4407        focus_on: &Option<Entity<Pane>>,
4408        window: &mut Window,
4409        cx: &mut Context<Workspace>,
4410    ) {
4411        self.panes.retain(|p| p != pane);
4412        if let Some(focus_on) = focus_on {
4413            focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4414        } else {
4415            self.panes
4416                .last()
4417                .unwrap()
4418                .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4419        }
4420        if self.last_active_center_pane == Some(pane.downgrade()) {
4421            self.last_active_center_pane = None;
4422        }
4423        cx.notify();
4424    }
4425
4426    fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4427        if self._schedule_serialize.is_none() {
4428            self._schedule_serialize = Some(cx.spawn_in(window, |this, mut cx| async move {
4429                cx.background_executor()
4430                    .timer(Duration::from_millis(100))
4431                    .await;
4432                this.update_in(&mut cx, |this, window, cx| {
4433                    this.serialize_workspace_internal(window, cx).detach();
4434                    this._schedule_serialize.take();
4435                })
4436                .log_err();
4437            }));
4438        }
4439    }
4440
4441    fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> {
4442        let Some(database_id) = self.database_id() else {
4443            return Task::ready(());
4444        };
4445
4446        fn serialize_pane_handle(
4447            pane_handle: &Entity<Pane>,
4448            window: &mut Window,
4449            cx: &mut App,
4450        ) -> SerializedPane {
4451            let (items, active, pinned_count) = {
4452                let pane = pane_handle.read(cx);
4453                let active_item_id = pane.active_item().map(|item| item.item_id());
4454                (
4455                    pane.items()
4456                        .filter_map(|handle| {
4457                            let handle = handle.to_serializable_item_handle(cx)?;
4458
4459                            Some(SerializedItem {
4460                                kind: Arc::from(handle.serialized_item_kind()),
4461                                item_id: handle.item_id().as_u64(),
4462                                active: Some(handle.item_id()) == active_item_id,
4463                                preview: pane.is_active_preview_item(handle.item_id()),
4464                            })
4465                        })
4466                        .collect::<Vec<_>>(),
4467                    pane.has_focus(window, cx),
4468                    pane.pinned_count(),
4469                )
4470            };
4471
4472            SerializedPane::new(items, active, pinned_count)
4473        }
4474
4475        fn build_serialized_pane_group(
4476            pane_group: &Member,
4477            window: &mut Window,
4478            cx: &mut App,
4479        ) -> SerializedPaneGroup {
4480            match pane_group {
4481                Member::Axis(PaneAxis {
4482                    axis,
4483                    members,
4484                    flexes,
4485                    bounding_boxes: _,
4486                }) => SerializedPaneGroup::Group {
4487                    axis: SerializedAxis(*axis),
4488                    children: members
4489                        .iter()
4490                        .map(|member| build_serialized_pane_group(member, window, cx))
4491                        .collect::<Vec<_>>(),
4492                    flexes: Some(flexes.lock().clone()),
4493                },
4494                Member::Pane(pane_handle) => {
4495                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx))
4496                }
4497            }
4498        }
4499
4500        fn build_serialized_docks(
4501            this: &Workspace,
4502            window: &mut Window,
4503            cx: &mut App,
4504        ) -> DockStructure {
4505            let left_dock = this.left_dock.read(cx);
4506            let left_visible = left_dock.is_open();
4507            let left_active_panel = left_dock
4508                .active_panel()
4509                .map(|panel| panel.persistent_name().to_string());
4510            let left_dock_zoom = left_dock
4511                .active_panel()
4512                .map(|panel| panel.is_zoomed(window, cx))
4513                .unwrap_or(false);
4514
4515            let right_dock = this.right_dock.read(cx);
4516            let right_visible = right_dock.is_open();
4517            let right_active_panel = right_dock
4518                .active_panel()
4519                .map(|panel| panel.persistent_name().to_string());
4520            let right_dock_zoom = right_dock
4521                .active_panel()
4522                .map(|panel| panel.is_zoomed(window, cx))
4523                .unwrap_or(false);
4524
4525            let bottom_dock = this.bottom_dock.read(cx);
4526            let bottom_visible = bottom_dock.is_open();
4527            let bottom_active_panel = bottom_dock
4528                .active_panel()
4529                .map(|panel| panel.persistent_name().to_string());
4530            let bottom_dock_zoom = bottom_dock
4531                .active_panel()
4532                .map(|panel| panel.is_zoomed(window, cx))
4533                .unwrap_or(false);
4534
4535            DockStructure {
4536                left: DockData {
4537                    visible: left_visible,
4538                    active_panel: left_active_panel,
4539                    zoom: left_dock_zoom,
4540                },
4541                right: DockData {
4542                    visible: right_visible,
4543                    active_panel: right_active_panel,
4544                    zoom: right_dock_zoom,
4545                },
4546                bottom: DockData {
4547                    visible: bottom_visible,
4548                    active_panel: bottom_active_panel,
4549                    zoom: bottom_dock_zoom,
4550                },
4551            }
4552        }
4553
4554        let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4555            Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4556        } else if let Some(local_paths) = self.local_paths(cx) {
4557            if !local_paths.is_empty() {
4558                Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4559            } else {
4560                None
4561            }
4562        } else {
4563            None
4564        };
4565
4566        if let Some(location) = location {
4567            let center_group = build_serialized_pane_group(&self.center.root, window, cx);
4568            let docks = build_serialized_docks(self, window, cx);
4569            let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
4570            let serialized_workspace = SerializedWorkspace {
4571                id: database_id,
4572                location,
4573                center_group,
4574                window_bounds,
4575                display: Default::default(),
4576                docks,
4577                centered_layout: self.centered_layout,
4578                session_id: self.session_id.clone(),
4579                window_id: Some(window.window_handle().window_id().as_u64()),
4580            };
4581            return window.spawn(cx, |_| persistence::DB.save_workspace(serialized_workspace));
4582        }
4583        Task::ready(())
4584    }
4585
4586    async fn serialize_items(
4587        this: &WeakEntity<Self>,
4588        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4589        cx: &mut AsyncWindowContext,
4590    ) -> Result<()> {
4591        const CHUNK_SIZE: usize = 200;
4592
4593        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4594
4595        while let Some(items_received) = serializable_items.next().await {
4596            let unique_items =
4597                items_received
4598                    .into_iter()
4599                    .fold(HashMap::default(), |mut acc, item| {
4600                        acc.entry(item.item_id()).or_insert(item);
4601                        acc
4602                    });
4603
4604            // We use into_iter() here so that the references to the items are moved into
4605            // the tasks and not kept alive while we're sleeping.
4606            for (_, item) in unique_items.into_iter() {
4607                if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| {
4608                    item.serialize(workspace, false, window, cx)
4609                }) {
4610                    cx.background_executor()
4611                        .spawn(async move { task.await.log_err() })
4612                        .detach();
4613                }
4614            }
4615
4616            cx.background_executor()
4617                .timer(SERIALIZATION_THROTTLE_TIME)
4618                .await;
4619        }
4620
4621        Ok(())
4622    }
4623
4624    pub(crate) fn enqueue_item_serialization(
4625        &mut self,
4626        item: Box<dyn SerializableItemHandle>,
4627    ) -> Result<()> {
4628        self.serializable_items_tx
4629            .unbounded_send(item)
4630            .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4631    }
4632
4633    pub(crate) fn load_workspace(
4634        serialized_workspace: SerializedWorkspace,
4635        paths_to_open: Vec<Option<ProjectPath>>,
4636        window: &mut Window,
4637        cx: &mut Context<Workspace>,
4638    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4639        cx.spawn_in(window, |workspace, mut cx| async move {
4640            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4641
4642            let mut center_group = None;
4643            let mut center_items = None;
4644
4645            // Traverse the splits tree and add to things
4646            if let Some((group, active_pane, items)) = serialized_workspace
4647                .center_group
4648                .deserialize(
4649                    &project,
4650                    serialized_workspace.id,
4651                    workspace.clone(),
4652                    &mut cx,
4653                )
4654                .await
4655            {
4656                center_items = Some(items);
4657                center_group = Some((group, active_pane))
4658            }
4659
4660            let mut items_by_project_path = HashMap::default();
4661            let mut item_ids_by_kind = HashMap::default();
4662            let mut all_deserialized_items = Vec::default();
4663            cx.update(|_, cx| {
4664                for item in center_items.unwrap_or_default().into_iter().flatten() {
4665                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4666                        item_ids_by_kind
4667                            .entry(serializable_item_handle.serialized_item_kind())
4668                            .or_insert(Vec::new())
4669                            .push(item.item_id().as_u64() as ItemId);
4670                    }
4671
4672                    if let Some(project_path) = item.project_path(cx) {
4673                        items_by_project_path.insert(project_path, item.clone());
4674                    }
4675                    all_deserialized_items.push(item);
4676                }
4677            })?;
4678
4679            let opened_items = paths_to_open
4680                .into_iter()
4681                .map(|path_to_open| {
4682                    path_to_open
4683                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4684                })
4685                .collect::<Vec<_>>();
4686
4687            // Remove old panes from workspace panes list
4688            workspace.update_in(&mut cx, |workspace, window, cx| {
4689                if let Some((center_group, active_pane)) = center_group {
4690                    workspace.remove_panes(workspace.center.root.clone(), window, cx);
4691
4692                    // Swap workspace center group
4693                    workspace.center = PaneGroup::with_root(center_group);
4694                    if let Some(active_pane) = active_pane {
4695                        workspace.set_active_pane(&active_pane, window, cx);
4696                        cx.focus_self(window);
4697                    } else {
4698                        workspace.set_active_pane(&workspace.center.first_pane(), window, cx);
4699                    }
4700                }
4701
4702                let docks = serialized_workspace.docks;
4703
4704                for (dock, serialized_dock) in [
4705                    (&mut workspace.right_dock, docks.right),
4706                    (&mut workspace.left_dock, docks.left),
4707                    (&mut workspace.bottom_dock, docks.bottom),
4708                ]
4709                .iter_mut()
4710                {
4711                    dock.update(cx, |dock, cx| {
4712                        dock.serialized_dock = Some(serialized_dock.clone());
4713                        dock.restore_state(window, cx);
4714                    });
4715                }
4716
4717                cx.notify();
4718            })?;
4719
4720            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4721            // after loading the items, we might have different items and in order to avoid
4722            // the database filling up, we delete items that haven't been loaded now.
4723            //
4724            // The items that have been loaded, have been saved after they've been added to the workspace.
4725            let clean_up_tasks = workspace.update_in(&mut cx, |_, window, cx| {
4726                item_ids_by_kind
4727                    .into_iter()
4728                    .map(|(item_kind, loaded_items)| {
4729                        SerializableItemRegistry::cleanup(
4730                            item_kind,
4731                            serialized_workspace.id,
4732                            loaded_items,
4733                            window,
4734                            cx,
4735                        )
4736                        .log_err()
4737                    })
4738                    .collect::<Vec<_>>()
4739            })?;
4740
4741            futures::future::join_all(clean_up_tasks).await;
4742
4743            workspace
4744                .update_in(&mut cx, |workspace, window, cx| {
4745                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4746                    workspace.serialize_workspace_internal(window, cx).detach();
4747
4748                    // Ensure that we mark the window as edited if we did load dirty items
4749                    workspace.update_window_edited(window, cx);
4750                })
4751                .ok();
4752
4753            Ok(opened_items)
4754        })
4755    }
4756
4757    fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
4758        self.add_workspace_actions_listeners(div, window, cx)
4759            .on_action(cx.listener(Self::close_inactive_items_and_panes))
4760            .on_action(cx.listener(Self::close_all_items_and_panes))
4761            .on_action(cx.listener(Self::save_all))
4762            .on_action(cx.listener(Self::send_keystrokes))
4763            .on_action(cx.listener(Self::add_folder_to_project))
4764            .on_action(cx.listener(Self::follow_next_collaborator))
4765            .on_action(cx.listener(Self::close_window))
4766            .on_action(cx.listener(Self::activate_pane_at_index))
4767            .on_action(cx.listener(Self::move_item_to_pane_at_index))
4768            .on_action(cx.listener(Self::move_focused_panel_to_next_position))
4769            .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| {
4770                let pane = workspace.active_pane().clone();
4771                workspace.unfollow_in_pane(&pane, window, cx);
4772            }))
4773            .on_action(cx.listener(|workspace, action: &Save, window, cx| {
4774                workspace
4775                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
4776                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4777            }))
4778            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
4779                workspace
4780                    .save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
4781                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4782            }))
4783            .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
4784                workspace
4785                    .save_active_item(SaveIntent::SaveAs, window, cx)
4786                    .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
4787            }))
4788            .on_action(
4789                cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| {
4790                    workspace.activate_previous_pane(window, cx)
4791                }),
4792            )
4793            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4794                workspace.activate_next_pane(window, cx)
4795            }))
4796            .on_action(
4797                cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| {
4798                    workspace.activate_next_window(cx)
4799                }),
4800            )
4801            .on_action(
4802                cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| {
4803                    workspace.activate_previous_window(cx)
4804                }),
4805            )
4806            .on_action(
4807                cx.listener(|workspace, action: &ActivatePaneInDirection, window, cx| {
4808                    workspace.activate_pane_in_direction(action.0, window, cx)
4809                }),
4810            )
4811            .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| {
4812                workspace.activate_next_pane(window, cx)
4813            }))
4814            .on_action(
4815                cx.listener(|workspace, action: &ActivatePaneInDirection, window, cx| {
4816                    workspace.activate_pane_in_direction(action.0, window, cx)
4817                }),
4818            )
4819            .on_action(cx.listener(
4820                |workspace, action: &MoveItemToPaneInDirection, window, cx| {
4821                    workspace.move_item_to_pane_in_direction(action, window, cx)
4822                },
4823            ))
4824            .on_action(
4825                cx.listener(|workspace, action: &SwapPaneInDirection, _, cx| {
4826                    workspace.swap_pane_in_direction(action.0, cx)
4827                }),
4828            )
4829            .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| {
4830                this.toggle_dock(DockPosition::Left, window, cx);
4831            }))
4832            .on_action(cx.listener(
4833                |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| {
4834                    workspace.toggle_dock(DockPosition::Right, window, cx);
4835                },
4836            ))
4837            .on_action(cx.listener(
4838                |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| {
4839                    workspace.toggle_dock(DockPosition::Bottom, window, cx);
4840                },
4841            ))
4842            .on_action(
4843                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| {
4844                    workspace.close_all_docks(window, cx);
4845                }),
4846            )
4847            .on_action(cx.listener(
4848                |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
4849                    workspace.clear_all_notifications(cx);
4850                },
4851            ))
4852            .on_action(cx.listener(
4853                |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| {
4854                    workspace.reopen_closed_item(window, cx).detach();
4855                },
4856            ))
4857            .on_action(cx.listener(Workspace::toggle_centered_layout))
4858    }
4859
4860    #[cfg(any(test, feature = "test-support"))]
4861    pub fn test_new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
4862        use node_runtime::NodeRuntime;
4863        use session::Session;
4864
4865        let client = project.read(cx).client();
4866        let user_store = project.read(cx).user_store();
4867
4868        let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
4869        let session = cx.new(|cx| AppSession::new(Session::test(), cx));
4870        window.activate_window();
4871        let app_state = Arc::new(AppState {
4872            languages: project.read(cx).languages().clone(),
4873            workspace_store,
4874            client,
4875            user_store,
4876            fs: project.read(cx).fs().clone(),
4877            build_window_options: |_, _| Default::default(),
4878            node_runtime: NodeRuntime::unavailable(),
4879            session,
4880        });
4881        let workspace = Self::new(Default::default(), project, app_state, window, cx);
4882        workspace
4883            .active_pane
4884            .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)));
4885        workspace
4886    }
4887
4888    pub fn register_action<A: Action>(
4889        &mut self,
4890        callback: impl Fn(&mut Self, &A, &mut Window, &mut Context<Self>) + 'static,
4891    ) -> &mut Self {
4892        let callback = Arc::new(callback);
4893
4894        self.workspace_actions.push(Box::new(move |div, _, cx| {
4895            let callback = callback.clone();
4896            div.on_action(cx.listener(move |workspace, event, window, cx| {
4897                (callback.clone())(workspace, event, window, cx)
4898            }))
4899        }));
4900        self
4901    }
4902
4903    fn add_workspace_actions_listeners(
4904        &self,
4905        mut div: Div,
4906        window: &mut Window,
4907        cx: &mut Context<Self>,
4908    ) -> Div {
4909        for action in self.workspace_actions.iter() {
4910            div = (action)(div, window, cx)
4911        }
4912        div
4913    }
4914
4915    pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool {
4916        self.modal_layer.read(cx).has_active_modal()
4917    }
4918
4919    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &App) -> Option<Entity<V>> {
4920        self.modal_layer.read(cx).active_modal()
4921    }
4922
4923    pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
4924    where
4925        B: FnOnce(&mut Window, &mut Context<V>) -> V,
4926    {
4927        self.modal_layer.update(cx, |modal_layer, cx| {
4928            modal_layer.toggle_modal(window, cx, build)
4929        })
4930    }
4931
4932    pub fn toggle_centered_layout(
4933        &mut self,
4934        _: &ToggleCenteredLayout,
4935        _: &mut Window,
4936        cx: &mut Context<Self>,
4937    ) {
4938        self.centered_layout = !self.centered_layout;
4939        if let Some(database_id) = self.database_id() {
4940            cx.background_executor()
4941                .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4942                .detach_and_log_err(cx);
4943        }
4944        cx.notify();
4945    }
4946
4947    fn adjust_padding(padding: Option<f32>) -> f32 {
4948        padding
4949            .unwrap_or(Self::DEFAULT_PADDING)
4950            .clamp(0.0, Self::MAX_PADDING)
4951    }
4952
4953    fn render_dock(
4954        &self,
4955        position: DockPosition,
4956        dock: &Entity<Dock>,
4957        window: &mut Window,
4958        cx: &mut App,
4959    ) -> Option<Div> {
4960        if self.zoomed_position == Some(position) {
4961            return None;
4962        }
4963
4964        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4965            let pane = panel.pane(cx)?;
4966            let follower_states = &self.follower_states;
4967            leader_border_for_pane(follower_states, &pane, window, cx)
4968        });
4969
4970        Some(
4971            div()
4972                .flex()
4973                .flex_none()
4974                .overflow_hidden()
4975                .child(dock.clone())
4976                .children(leader_border),
4977        )
4978    }
4979
4980    pub fn for_window(window: &mut Window, _: &mut App) -> Option<Entity<Workspace>> {
4981        window.root().flatten()
4982    }
4983
4984    pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
4985        self.zoomed.as_ref()
4986    }
4987
4988    pub fn activate_next_window(&mut self, cx: &mut Context<Self>) {
4989        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
4990            return;
4991        };
4992        let windows = cx.windows();
4993        let Some(next_window) = windows
4994            .iter()
4995            .cycle()
4996            .skip_while(|window| window.window_id() != current_window_id)
4997            .nth(1)
4998        else {
4999            return;
5000        };
5001        next_window
5002            .update(cx, |_, window, _| window.activate_window())
5003            .ok();
5004    }
5005
5006    pub fn activate_previous_window(&mut self, cx: &mut Context<Self>) {
5007        let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else {
5008            return;
5009        };
5010        let windows = cx.windows();
5011        let Some(prev_window) = windows
5012            .iter()
5013            .rev()
5014            .cycle()
5015            .skip_while(|window| window.window_id() != current_window_id)
5016            .nth(1)
5017        else {
5018            return;
5019        };
5020        prev_window
5021            .update(cx, |_, window, _| window.activate_window())
5022            .ok();
5023    }
5024}
5025
5026fn leader_border_for_pane(
5027    follower_states: &HashMap<PeerId, FollowerState>,
5028    pane: &Entity<Pane>,
5029    _: &Window,
5030    cx: &App,
5031) -> Option<Div> {
5032    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
5033        if state.pane() == pane {
5034            Some((*leader_id, state))
5035        } else {
5036            None
5037        }
5038    })?;
5039
5040    let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
5041    let leader = room.remote_participant_for_peer_id(leader_id)?;
5042
5043    let mut leader_color = cx
5044        .theme()
5045        .players()
5046        .color_for_participant(leader.participant_index.0)
5047        .cursor;
5048    leader_color.fade_out(0.3);
5049    Some(
5050        div()
5051            .absolute()
5052            .size_full()
5053            .left_0()
5054            .top_0()
5055            .border_2()
5056            .border_color(leader_color),
5057    )
5058}
5059
5060fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
5061    ZED_WINDOW_POSITION
5062        .zip(*ZED_WINDOW_SIZE)
5063        .map(|(position, size)| Bounds {
5064            origin: position,
5065            size,
5066        })
5067}
5068
5069fn open_items(
5070    serialized_workspace: Option<SerializedWorkspace>,
5071    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
5072    window: &mut Window,
5073    cx: &mut Context<Workspace>,
5074) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
5075    let restored_items = serialized_workspace.map(|serialized_workspace| {
5076        Workspace::load_workspace(
5077            serialized_workspace,
5078            project_paths_to_open
5079                .iter()
5080                .map(|(_, project_path)| project_path)
5081                .cloned()
5082                .collect(),
5083            window,
5084            cx,
5085        )
5086    });
5087
5088    cx.spawn_in(window, |workspace, mut cx| async move {
5089        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
5090
5091        if let Some(restored_items) = restored_items {
5092            let restored_items = restored_items.await?;
5093
5094            let restored_project_paths = restored_items
5095                .iter()
5096                .filter_map(|item| {
5097                    cx.update(|_, cx| item.as_ref()?.project_path(cx))
5098                        .ok()
5099                        .flatten()
5100                })
5101                .collect::<HashSet<_>>();
5102
5103            for restored_item in restored_items {
5104                opened_items.push(restored_item.map(Ok));
5105            }
5106
5107            project_paths_to_open
5108                .iter_mut()
5109                .for_each(|(_, project_path)| {
5110                    if let Some(project_path_to_open) = project_path {
5111                        if restored_project_paths.contains(project_path_to_open) {
5112                            *project_path = None;
5113                        }
5114                    }
5115                });
5116        } else {
5117            for _ in 0..project_paths_to_open.len() {
5118                opened_items.push(None);
5119            }
5120        }
5121        assert!(opened_items.len() == project_paths_to_open.len());
5122
5123        let tasks =
5124            project_paths_to_open
5125                .into_iter()
5126                .enumerate()
5127                .map(|(ix, (abs_path, project_path))| {
5128                    let workspace = workspace.clone();
5129                    cx.spawn(|mut cx| async move {
5130                        let file_project_path = project_path?;
5131                        let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
5132                            workspace.project().update(cx, |project, cx| {
5133                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
5134                            })
5135                        });
5136
5137                        // We only want to open file paths here. If one of the items
5138                        // here is a directory, it was already opened further above
5139                        // with a `find_or_create_worktree`.
5140                        if let Ok(task) = abs_path_task {
5141                            if task.await.map_or(true, |p| p.is_file()) {
5142                                return Some((
5143                                    ix,
5144                                    workspace
5145                                        .update_in(&mut cx, |workspace, window, cx| {
5146                                            workspace.open_path(
5147                                                file_project_path,
5148                                                None,
5149                                                true,
5150                                                window,
5151                                                cx,
5152                                            )
5153                                        })
5154                                        .log_err()?
5155                                        .await,
5156                                ));
5157                            }
5158                        }
5159                        None
5160                    })
5161                });
5162
5163        let tasks = tasks.collect::<Vec<_>>();
5164
5165        let tasks = futures::future::join_all(tasks);
5166        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
5167            opened_items[ix] = Some(path_open_result);
5168        }
5169
5170        Ok(opened_items)
5171    })
5172}
5173
5174enum ActivateInDirectionTarget {
5175    Pane(Entity<Pane>),
5176    Dock(Entity<Dock>),
5177}
5178
5179fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncApp) {
5180    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";
5181
5182    workspace
5183        .update(cx, |workspace, _, cx| {
5184            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
5185                struct DatabaseFailedNotification;
5186
5187                workspace.show_notification(
5188                    NotificationId::unique::<DatabaseFailedNotification>(),
5189                    cx,
5190                    |cx| {
5191                        cx.new(|_| {
5192                            MessageNotification::new("Failed to load the database file.")
5193                                .with_click_message("File an issue")
5194                                .on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5195                        })
5196                    },
5197                );
5198            }
5199        })
5200        .log_err();
5201}
5202
5203impl Focusable for Workspace {
5204    fn focus_handle(&self, cx: &App) -> FocusHandle {
5205        self.active_pane.focus_handle(cx)
5206    }
5207}
5208
5209#[derive(Clone)]
5210struct DraggedDock(DockPosition);
5211
5212impl Render for DraggedDock {
5213    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5214        gpui::Empty
5215    }
5216}
5217
5218impl Render for Workspace {
5219    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5220        let mut context = KeyContext::new_with_defaults();
5221        context.add("Workspace");
5222        context.set("keyboard_layout", cx.keyboard_layout().clone());
5223        let centered_layout = self.centered_layout
5224            && self.center.panes().len() == 1
5225            && self.active_item(cx).is_some();
5226        let render_padding = |size| {
5227            (size > 0.0).then(|| {
5228                div()
5229                    .h_full()
5230                    .w(relative(size))
5231                    .bg(cx.theme().colors().editor_background)
5232                    .border_color(cx.theme().colors().pane_group_border)
5233            })
5234        };
5235        let paddings = if centered_layout {
5236            let settings = WorkspaceSettings::get_global(cx).centered_layout;
5237            (
5238                render_padding(Self::adjust_padding(settings.left_padding)),
5239                render_padding(Self::adjust_padding(settings.right_padding)),
5240            )
5241        } else {
5242            (None, None)
5243        };
5244        let ui_font = theme::setup_ui_font(window, cx);
5245
5246        let theme = cx.theme().clone();
5247        let colors = theme.colors();
5248
5249        client_side_decorations(
5250            self.actions(div(), window, cx)
5251                .key_context(context)
5252                .relative()
5253                .size_full()
5254                .flex()
5255                .flex_col()
5256                .font(ui_font)
5257                .gap_0()
5258                .justify_start()
5259                .items_start()
5260                .text_color(colors.text)
5261                .overflow_hidden()
5262                .children(self.titlebar_item.clone())
5263                .child(
5264                    div()
5265                        .size_full()
5266                        .relative()
5267                        .flex_1()
5268                        .flex()
5269                        .flex_col()
5270                        .child(
5271                            div()
5272                                .id("workspace")
5273                                .bg(colors.background)
5274                                .relative()
5275                                .flex_1()
5276                                .w_full()
5277                                .flex()
5278                                .flex_col()
5279                                .overflow_hidden()
5280                                .border_t_1()
5281                                .border_b_1()
5282                                .border_color(colors.border)
5283                                .child({
5284                                    let this = cx.entity().clone();
5285                                    canvas(
5286                                        move |bounds, window, cx| {
5287                                            this.update(cx, |this, cx| {
5288                                                let bounds_changed = this.bounds != bounds;
5289                                                this.bounds = bounds;
5290
5291                                                if bounds_changed {
5292                                                    this.left_dock.update(cx, |dock, cx| {
5293                                                        dock.clamp_panel_size(
5294                                                            bounds.size.width,
5295                                                            window,
5296                                                            cx,
5297                                                        )
5298                                                    });
5299
5300                                                    this.right_dock.update(cx, |dock, cx| {
5301                                                        dock.clamp_panel_size(
5302                                                            bounds.size.width,
5303                                                            window,
5304                                                            cx,
5305                                                        )
5306                                                    });
5307
5308                                                    this.bottom_dock.update(cx, |dock, cx| {
5309                                                        dock.clamp_panel_size(
5310                                                            bounds.size.height,
5311                                                            window,
5312                                                            cx,
5313                                                        )
5314                                                    });
5315                                                }
5316                                            })
5317                                        },
5318                                        |_, _, _, _| {},
5319                                    )
5320                                    .absolute()
5321                                    .size_full()
5322                                })
5323                                .when(self.zoomed.is_none(), |this| {
5324                                    this.on_drag_move(cx.listener(
5325                                        move |workspace,
5326                                              e: &DragMoveEvent<DraggedDock>,
5327                                              window,
5328                                              cx| {
5329                                            if workspace.previous_dock_drag_coordinates
5330                                                != Some(e.event.position)
5331                                            {
5332                                                workspace.previous_dock_drag_coordinates =
5333                                                    Some(e.event.position);
5334                                                match e.drag(cx).0 {
5335                                                    DockPosition::Left => {
5336                                                        resize_left_dock(
5337                                                            e.event.position.x
5338                                                                - workspace.bounds.left(),
5339                                                            workspace,
5340                                                            window,
5341                                                            cx,
5342                                                        );
5343                                                    }
5344                                                    DockPosition::Right => {
5345                                                        resize_right_dock(
5346                                                            workspace.bounds.right()
5347                                                                - e.event.position.x,
5348                                                            workspace,
5349                                                            window,
5350                                                            cx,
5351                                                        );
5352                                                    }
5353                                                    DockPosition::Bottom => {
5354                                                        resize_bottom_dock(
5355                                                            workspace.bounds.bottom()
5356                                                                - e.event.position.y,
5357                                                            workspace,
5358                                                            window,
5359                                                            cx,
5360                                                        );
5361                                                    }
5362                                                };
5363                                                workspace.serialize_workspace(window, cx);
5364                                            }
5365                                        },
5366                                    ))
5367                                })
5368                                .child(
5369                                    div()
5370                                        .flex()
5371                                        .flex_row()
5372                                        .h_full()
5373                                        // Left Dock
5374                                        .children(self.render_dock(
5375                                            DockPosition::Left,
5376                                            &self.left_dock,
5377                                            window,
5378                                            cx,
5379                                        ))
5380                                        // Panes
5381                                        .child(
5382                                            div()
5383                                                .flex()
5384                                                .flex_col()
5385                                                .flex_1()
5386                                                .overflow_hidden()
5387                                                .child(
5388                                                    h_flex()
5389                                                        .flex_1()
5390                                                        .when_some(paddings.0, |this, p| {
5391                                                            this.child(p.border_r_1())
5392                                                        })
5393                                                        .child(self.center.render(
5394                                                            &self.project,
5395                                                            &self.follower_states,
5396                                                            self.active_call(),
5397                                                            &self.active_pane,
5398                                                            self.zoomed.as_ref(),
5399                                                            &self.app_state,
5400                                                            window,
5401                                                            cx,
5402                                                        ))
5403                                                        .when_some(paddings.1, |this, p| {
5404                                                            this.child(p.border_l_1())
5405                                                        }),
5406                                                )
5407                                                .children(self.render_dock(
5408                                                    DockPosition::Bottom,
5409                                                    &self.bottom_dock,
5410                                                    window,
5411                                                    cx,
5412                                                )),
5413                                        )
5414                                        // Right Dock
5415                                        .children(self.render_dock(
5416                                            DockPosition::Right,
5417                                            &self.right_dock,
5418                                            window,
5419                                            cx,
5420                                        )),
5421                                )
5422                                .children(self.zoomed.as_ref().and_then(|view| {
5423                                    let zoomed_view = view.upgrade()?;
5424                                    let div = div()
5425                                        .occlude()
5426                                        .absolute()
5427                                        .overflow_hidden()
5428                                        .border_color(colors.border)
5429                                        .bg(colors.background)
5430                                        .child(zoomed_view)
5431                                        .inset_0()
5432                                        .shadow_lg();
5433
5434                                    Some(match self.zoomed_position {
5435                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
5436                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
5437                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5438                                        None => {
5439                                            div.top_2().bottom_2().left_2().right_2().border_1()
5440                                        }
5441                                    })
5442                                }))
5443                                .children(self.render_notifications(window, cx)),
5444                        )
5445                        .child(self.status_bar.clone())
5446                        .child(self.modal_layer.clone()),
5447                ),
5448            window,
5449            cx,
5450        )
5451    }
5452}
5453
5454fn resize_bottom_dock(
5455    new_size: Pixels,
5456    workspace: &mut Workspace,
5457    window: &mut Window,
5458    cx: &mut App,
5459) {
5460    let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5461    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5462        bottom_dock.resize_active_panel(Some(size), window, cx);
5463    });
5464}
5465
5466fn resize_right_dock(
5467    new_size: Pixels,
5468    workspace: &mut Workspace,
5469    window: &mut Window,
5470    cx: &mut App,
5471) {
5472    let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5473    workspace.right_dock.update(cx, |right_dock, cx| {
5474        right_dock.resize_active_panel(Some(size), window, cx);
5475    });
5476}
5477
5478fn resize_left_dock(
5479    new_size: Pixels,
5480    workspace: &mut Workspace,
5481    window: &mut Window,
5482    cx: &mut App,
5483) {
5484    let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5485
5486    workspace.left_dock.update(cx, |left_dock, cx| {
5487        left_dock.resize_active_panel(Some(size), window, cx);
5488    });
5489}
5490
5491impl WorkspaceStore {
5492    pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5493        Self {
5494            workspaces: Default::default(),
5495            _subscriptions: vec![
5496                client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5497                client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5498            ],
5499            client,
5500        }
5501    }
5502
5503    pub fn update_followers(
5504        &self,
5505        project_id: Option<u64>,
5506        update: proto::update_followers::Variant,
5507        cx: &App,
5508    ) -> Option<()> {
5509        let active_call = ActiveCall::try_global(cx)?;
5510        let room_id = active_call.read(cx).room()?.read(cx).id();
5511        self.client
5512            .send(proto::UpdateFollowers {
5513                room_id,
5514                project_id,
5515                variant: Some(update),
5516            })
5517            .log_err()
5518    }
5519
5520    pub async fn handle_follow(
5521        this: Entity<Self>,
5522        envelope: TypedEnvelope<proto::Follow>,
5523        mut cx: AsyncApp,
5524    ) -> Result<proto::FollowResponse> {
5525        this.update(&mut cx, |this, cx| {
5526            let follower = Follower {
5527                project_id: envelope.payload.project_id,
5528                peer_id: envelope.original_sender_id()?,
5529            };
5530
5531            let mut response = proto::FollowResponse::default();
5532            this.workspaces.retain(|workspace| {
5533                workspace
5534                    .update(cx, |workspace, window, cx| {
5535                        let handler_response =
5536                            workspace.handle_follow(follower.project_id, window, cx);
5537                        if let Some(active_view) = handler_response.active_view.clone() {
5538                            if workspace.project.read(cx).remote_id() == follower.project_id {
5539                                response.active_view = Some(active_view)
5540                            }
5541                        }
5542                    })
5543                    .is_ok()
5544            });
5545
5546            Ok(response)
5547        })?
5548    }
5549
5550    async fn handle_update_followers(
5551        this: Entity<Self>,
5552        envelope: TypedEnvelope<proto::UpdateFollowers>,
5553        mut cx: AsyncApp,
5554    ) -> Result<()> {
5555        let leader_id = envelope.original_sender_id()?;
5556        let update = envelope.payload;
5557
5558        this.update(&mut cx, |this, cx| {
5559            this.workspaces.retain(|workspace| {
5560                workspace
5561                    .update(cx, |workspace, window, cx| {
5562                        let project_id = workspace.project.read(cx).remote_id();
5563                        if update.project_id != project_id && update.project_id.is_some() {
5564                            return;
5565                        }
5566                        workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5567                    })
5568                    .is_ok()
5569            });
5570            Ok(())
5571        })?
5572    }
5573}
5574
5575impl ViewId {
5576    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5577        Ok(Self {
5578            creator: message
5579                .creator
5580                .ok_or_else(|| anyhow!("creator is missing"))?,
5581            id: message.id,
5582        })
5583    }
5584
5585    pub(crate) fn to_proto(self) -> proto::ViewId {
5586        proto::ViewId {
5587            creator: Some(self.creator),
5588            id: self.id,
5589        }
5590    }
5591}
5592
5593impl FollowerState {
5594    fn pane(&self) -> &Entity<Pane> {
5595        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5596    }
5597}
5598
5599pub trait WorkspaceHandle {
5600    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5601}
5602
5603impl WorkspaceHandle for Entity<Workspace> {
5604    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5605        self.read(cx)
5606            .worktrees(cx)
5607            .flat_map(|worktree| {
5608                let worktree_id = worktree.read(cx).id();
5609                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5610                    worktree_id,
5611                    path: f.path.clone(),
5612                })
5613            })
5614            .collect::<Vec<_>>()
5615    }
5616}
5617
5618impl std::fmt::Debug for OpenPaths {
5619    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5620        f.debug_struct("OpenPaths")
5621            .field("paths", &self.paths)
5622            .finish()
5623    }
5624}
5625
5626pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5627    DB.last_workspace().await.log_err().flatten()
5628}
5629
5630pub fn last_session_workspace_locations(
5631    last_session_id: &str,
5632    last_session_window_stack: Option<Vec<WindowId>>,
5633) -> Option<Vec<SerializedWorkspaceLocation>> {
5634    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5635        .log_err()
5636}
5637
5638actions!(collab, [OpenChannelNotes]);
5639actions!(zed, [OpenLog]);
5640
5641async fn join_channel_internal(
5642    channel_id: ChannelId,
5643    app_state: &Arc<AppState>,
5644    requesting_window: Option<WindowHandle<Workspace>>,
5645    active_call: &Entity<ActiveCall>,
5646    cx: &mut AsyncApp,
5647) -> Result<bool> {
5648    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5649        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5650            return (false, None);
5651        };
5652
5653        let already_in_channel = room.channel_id() == Some(channel_id);
5654        let should_prompt = room.is_sharing_project()
5655            && !room.remote_participants().is_empty()
5656            && !already_in_channel;
5657        let open_room = if already_in_channel {
5658            active_call.room().cloned()
5659        } else {
5660            None
5661        };
5662        (should_prompt, open_room)
5663    })?;
5664
5665    if let Some(room) = open_room {
5666        let task = room.update(cx, |room, cx| {
5667            if let Some((project, host)) = room.most_active_project(cx) {
5668                return Some(join_in_room_project(project, host, app_state.clone(), cx));
5669            }
5670
5671            None
5672        })?;
5673        if let Some(task) = task {
5674            task.await?;
5675        }
5676        return anyhow::Ok(true);
5677    }
5678
5679    if should_prompt {
5680        if let Some(workspace) = requesting_window {
5681            let answer = workspace
5682                .update(cx, |_, window, cx| {
5683                    window.prompt(
5684                        PromptLevel::Warning,
5685                        "Do you want to switch channels?",
5686                        Some("Leaving this call will unshare your current project."),
5687                        &["Yes, Join Channel", "Cancel"],
5688                        cx,
5689                    )
5690                })?
5691                .await;
5692
5693            if answer == Ok(1) {
5694                return Ok(false);
5695            }
5696        } else {
5697            return Ok(false); // unreachable!() hopefully
5698        }
5699    }
5700
5701    let client = cx.update(|cx| active_call.read(cx).client())?;
5702
5703    let mut client_status = client.status();
5704
5705    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5706    'outer: loop {
5707        let Some(status) = client_status.recv().await else {
5708            return Err(anyhow!("error connecting"));
5709        };
5710
5711        match status {
5712            Status::Connecting
5713            | Status::Authenticating
5714            | Status::Reconnecting
5715            | Status::Reauthenticating => continue,
5716            Status::Connected { .. } => break 'outer,
5717            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5718            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5719            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5720                return Err(ErrorCode::Disconnected.into());
5721            }
5722        }
5723    }
5724
5725    let room = active_call
5726        .update(cx, |active_call, cx| {
5727            active_call.join_channel(channel_id, cx)
5728        })?
5729        .await?;
5730
5731    let Some(room) = room else {
5732        return anyhow::Ok(true);
5733    };
5734
5735    room.update(cx, |room, _| room.room_update_completed())?
5736        .await;
5737
5738    let task = room.update(cx, |room, cx| {
5739        if let Some((project, host)) = room.most_active_project(cx) {
5740            return Some(join_in_room_project(project, host, app_state.clone(), cx));
5741        }
5742
5743        // If you are the first to join a channel, see if you should share your project.
5744        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5745            if let Some(workspace) = requesting_window {
5746                let project = workspace.update(cx, |workspace, _, cx| {
5747                    let project = workspace.project.read(cx);
5748
5749                    if !CallSettings::get_global(cx).share_on_join {
5750                        return None;
5751                    }
5752
5753                    if (project.is_local() || project.is_via_ssh())
5754                        && project.visible_worktrees(cx).any(|tree| {
5755                            tree.read(cx)
5756                                .root_entry()
5757                                .map_or(false, |entry| entry.is_dir())
5758                        })
5759                    {
5760                        Some(workspace.project.clone())
5761                    } else {
5762                        None
5763                    }
5764                });
5765                if let Ok(Some(project)) = project {
5766                    return Some(cx.spawn(|room, mut cx| async move {
5767                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5768                            .await?;
5769                        Ok(())
5770                    }));
5771                }
5772            }
5773        }
5774
5775        None
5776    })?;
5777    if let Some(task) = task {
5778        task.await?;
5779        return anyhow::Ok(true);
5780    }
5781    anyhow::Ok(false)
5782}
5783
5784pub fn join_channel(
5785    channel_id: ChannelId,
5786    app_state: Arc<AppState>,
5787    requesting_window: Option<WindowHandle<Workspace>>,
5788    cx: &mut App,
5789) -> Task<Result<()>> {
5790    let active_call = ActiveCall::global(cx);
5791    cx.spawn(|mut cx| async move {
5792        let result = join_channel_internal(
5793            channel_id,
5794            &app_state,
5795            requesting_window,
5796            &active_call,
5797            &mut cx,
5798        )
5799            .await;
5800
5801        // join channel succeeded, and opened a window
5802        if matches!(result, Ok(true)) {
5803            return anyhow::Ok(());
5804        }
5805
5806        // find an existing workspace to focus and show call controls
5807        let mut active_window =
5808            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5809        if active_window.is_none() {
5810            // no open workspaces, make one to show the error in (blergh)
5811            let (window_handle, _) = cx
5812                .update(|cx| {
5813                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5814                })?
5815                .await?;
5816
5817            if result.is_ok() {
5818                cx.update(|cx| {
5819                    cx.dispatch_action(&OpenChannelNotes);
5820                }).log_err();
5821            }
5822
5823            active_window = Some(window_handle);
5824        }
5825
5826        if let Err(err) = result {
5827            log::error!("failed to join channel: {}", err);
5828            if let Some(active_window) = active_window {
5829                active_window
5830                    .update(&mut cx, |_, window, cx| {
5831                        let detail: SharedString = match err.error_code() {
5832                            ErrorCode::SignedOut => {
5833                                "Please sign in to continue.".into()
5834                            }
5835                            ErrorCode::UpgradeRequired => {
5836                                "Your are running an unsupported version of Zed. Please update to continue.".into()
5837                            }
5838                            ErrorCode::NoSuchChannel => {
5839                                "No matching channel was found. Please check the link and try again.".into()
5840                            }
5841                            ErrorCode::Forbidden => {
5842                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5843                            }
5844                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5845                            _ => format!("{}\n\nPlease try again.", err).into(),
5846                        };
5847                        window.prompt(
5848                            PromptLevel::Critical,
5849                            "Failed to join channel",
5850                            Some(&detail),
5851                            &["Ok"],
5852                        cx)
5853                    })?
5854                    .await
5855                    .ok();
5856            }
5857        }
5858
5859        // return ok, we showed the error to the user.
5860        anyhow::Ok(())
5861    })
5862}
5863
5864pub async fn get_any_active_workspace(
5865    app_state: Arc<AppState>,
5866    mut cx: AsyncApp,
5867) -> anyhow::Result<WindowHandle<Workspace>> {
5868    // find an existing workspace to focus and show call controls
5869    let active_window = activate_any_workspace_window(&mut cx);
5870    if active_window.is_none() {
5871        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5872            .await?;
5873    }
5874    activate_any_workspace_window(&mut cx).context("could not open zed")
5875}
5876
5877fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
5878    cx.update(|cx| {
5879        if let Some(workspace_window) = cx
5880            .active_window()
5881            .and_then(|window| window.downcast::<Workspace>())
5882        {
5883            return Some(workspace_window);
5884        }
5885
5886        for window in cx.windows() {
5887            if let Some(workspace_window) = window.downcast::<Workspace>() {
5888                workspace_window
5889                    .update(cx, |_, window, _| window.activate_window())
5890                    .ok();
5891                return Some(workspace_window);
5892            }
5893        }
5894        None
5895    })
5896    .ok()
5897    .flatten()
5898}
5899
5900pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
5901    cx.windows()
5902        .into_iter()
5903        .filter_map(|window| window.downcast::<Workspace>())
5904        .filter(|workspace| {
5905            workspace
5906                .read(cx)
5907                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5908        })
5909        .collect()
5910}
5911
5912#[derive(Default)]
5913pub struct OpenOptions {
5914    pub open_new_workspace: Option<bool>,
5915    pub replace_window: Option<WindowHandle<Workspace>>,
5916    pub env: Option<HashMap<String, String>>,
5917}
5918
5919#[allow(clippy::type_complexity)]
5920pub fn open_paths(
5921    abs_paths: &[PathBuf],
5922    app_state: Arc<AppState>,
5923    open_options: OpenOptions,
5924    cx: &mut App,
5925) -> Task<
5926    anyhow::Result<(
5927        WindowHandle<Workspace>,
5928        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5929    )>,
5930> {
5931    let abs_paths = abs_paths.to_vec();
5932    let mut existing = None;
5933    let mut best_match = None;
5934    let mut open_visible = OpenVisible::All;
5935
5936    if open_options.open_new_workspace != Some(true) {
5937        for window in local_workspace_windows(cx) {
5938            if let Ok(workspace) = window.read(cx) {
5939                let m = workspace
5940                    .project
5941                    .read(cx)
5942                    .visibility_for_paths(&abs_paths, cx);
5943                if m > best_match {
5944                    existing = Some(window);
5945                    best_match = m;
5946                } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5947                    existing = Some(window)
5948                }
5949            }
5950        }
5951    }
5952
5953    cx.spawn(move |mut cx| async move {
5954        if open_options.open_new_workspace.is_none() && existing.is_none() {
5955            let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5956            if futures::future::join_all(all_files)
5957                .await
5958                .into_iter()
5959                .filter_map(|result| result.ok().flatten())
5960                .all(|file| !file.is_dir)
5961            {
5962                cx.update(|cx| {
5963                    if let Some(window) = cx
5964                        .active_window()
5965                        .and_then(|window| window.downcast::<Workspace>())
5966                    {
5967                        if let Ok(workspace) = window.read(cx) {
5968                            let project = workspace.project().read(cx);
5969                            if project.is_local() && !project.is_via_collab() {
5970                                existing = Some(window);
5971                                open_visible = OpenVisible::None;
5972                                return;
5973                            }
5974                        }
5975                    }
5976                    for window in local_workspace_windows(cx) {
5977                        if let Ok(workspace) = window.read(cx) {
5978                            let project = workspace.project().read(cx);
5979                            if project.is_via_collab() {
5980                                continue;
5981                            }
5982                            existing = Some(window);
5983                            open_visible = OpenVisible::None;
5984                            break;
5985                        }
5986                    }
5987                })?;
5988            }
5989        }
5990
5991        if let Some(existing) = existing {
5992            let open_task = existing
5993                .update(&mut cx, |workspace, window, cx| {
5994                    window.activate_window();
5995                    workspace.open_paths(abs_paths, open_visible, None, window, cx)
5996                })?
5997                .await;
5998
5999            _ = existing.update(&mut cx, |workspace, _, cx| {
6000                for item in open_task.iter().flatten() {
6001                    if let Err(e) = item {
6002                        workspace.show_error(&e, cx);
6003                    }
6004                }
6005            });
6006
6007            Ok((existing, open_task))
6008        } else {
6009            cx.update(move |cx| {
6010                Workspace::new_local(
6011                    abs_paths,
6012                    app_state.clone(),
6013                    open_options.replace_window,
6014                    open_options.env,
6015                    cx,
6016                )
6017            })?
6018            .await
6019        }
6020    })
6021}
6022
6023pub fn open_new(
6024    open_options: OpenOptions,
6025    app_state: Arc<AppState>,
6026    cx: &mut App,
6027    init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6028) -> Task<anyhow::Result<()>> {
6029    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6030    cx.spawn(|mut cx| async move {
6031        let (workspace, opened_paths) = task.await?;
6032        workspace.update(&mut cx, |workspace, window, cx| {
6033            if opened_paths.is_empty() {
6034                init(workspace, window, cx)
6035            }
6036        })?;
6037        Ok(())
6038    })
6039}
6040
6041pub fn create_and_open_local_file(
6042    path: &'static Path,
6043    window: &mut Window,
6044    cx: &mut Context<Workspace>,
6045    default_content: impl 'static + Send + FnOnce() -> Rope,
6046) -> Task<Result<Box<dyn ItemHandle>>> {
6047    cx.spawn_in(window, |workspace, mut cx| async move {
6048        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6049        if !fs.is_file(path).await {
6050            fs.create_file(path, Default::default()).await?;
6051            fs.save(path, &default_content(), Default::default())
6052                .await?;
6053        }
6054
6055        let mut items = workspace
6056            .update_in(&mut cx, |workspace, window, cx| {
6057                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6058                    workspace.open_paths(
6059                        vec![path.to_path_buf()],
6060                        OpenVisible::None,
6061                        None,
6062                        window,
6063                        cx,
6064                    )
6065                })
6066            })?
6067            .await?
6068            .await;
6069
6070        let item = items.pop().flatten();
6071        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6072    })
6073}
6074
6075pub fn open_ssh_project(
6076    window: WindowHandle<Workspace>,
6077    connection_options: SshConnectionOptions,
6078    cancel_rx: oneshot::Receiver<()>,
6079    delegate: Arc<dyn SshClientDelegate>,
6080    app_state: Arc<AppState>,
6081    paths: Vec<PathBuf>,
6082    cx: &mut App,
6083) -> Task<Result<()>> {
6084    cx.spawn(|mut cx| async move {
6085        let (serialized_ssh_project, workspace_id, serialized_workspace) =
6086            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6087
6088        let session = match cx
6089            .update(|cx| {
6090                remote::SshRemoteClient::new(
6091                    ConnectionIdentifier::Workspace(workspace_id.0),
6092                    connection_options,
6093                    cancel_rx,
6094                    delegate,
6095                    cx,
6096                )
6097            })?
6098            .await?
6099        {
6100            Some(result) => result,
6101            None => return Ok(()),
6102        };
6103
6104        let project = cx.update(|cx| {
6105            project::Project::ssh(
6106                session,
6107                app_state.client.clone(),
6108                app_state.node_runtime.clone(),
6109                app_state.user_store.clone(),
6110                app_state.languages.clone(),
6111                app_state.fs.clone(),
6112                cx,
6113            )
6114        })?;
6115
6116        let toolchains = DB.toolchains(workspace_id).await?;
6117        for (toolchain, worktree_id) in toolchains {
6118            project
6119                .update(&mut cx, |this, cx| {
6120                    this.activate_toolchain(worktree_id, toolchain, cx)
6121                })?
6122                .await;
6123        }
6124        let mut project_paths_to_open = vec![];
6125        let mut project_path_errors = vec![];
6126
6127        for path in paths {
6128            let result = cx
6129                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6130                .await;
6131            match result {
6132                Ok((_, project_path)) => {
6133                    project_paths_to_open.push((path.clone(), Some(project_path)));
6134                }
6135                Err(error) => {
6136                    project_path_errors.push(error);
6137                }
6138            };
6139        }
6140
6141        if project_paths_to_open.is_empty() {
6142            return Err(project_path_errors
6143                .pop()
6144                .unwrap_or_else(|| anyhow!("no paths given")));
6145        }
6146
6147        cx.update_window(window.into(), |_, window, cx| {
6148            window.replace_root(cx, |window, cx| {
6149                let mut workspace =
6150                    Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6151
6152                workspace
6153                    .client()
6154                    .telemetry()
6155                    .report_app_event("open ssh project".to_string());
6156
6157                workspace.set_serialized_ssh_project(serialized_ssh_project);
6158                workspace
6159            });
6160        })?;
6161
6162        window
6163            .update(&mut cx, |_, window, cx| {
6164                window.activate_window();
6165
6166                open_items(serialized_workspace, project_paths_to_open, window, cx)
6167            })?
6168            .await?;
6169
6170        window.update(&mut cx, |workspace, _, cx| {
6171            for error in project_path_errors {
6172                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6173                    if let Some(path) = error.error_tag("path") {
6174                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6175                    }
6176                } else {
6177                    workspace.show_error(&error, cx)
6178                }
6179            }
6180        })
6181    })
6182}
6183
6184fn serialize_ssh_project(
6185    connection_options: SshConnectionOptions,
6186    paths: Vec<PathBuf>,
6187    cx: &AsyncApp,
6188) -> Task<
6189    Result<(
6190        SerializedSshProject,
6191        WorkspaceId,
6192        Option<SerializedWorkspace>,
6193    )>,
6194> {
6195    cx.background_executor().spawn(async move {
6196        let serialized_ssh_project = persistence::DB
6197            .get_or_create_ssh_project(
6198                connection_options.host.clone(),
6199                connection_options.port,
6200                paths
6201                    .iter()
6202                    .map(|path| path.to_string_lossy().to_string())
6203                    .collect::<Vec<_>>(),
6204                connection_options.username.clone(),
6205            )
6206            .await?;
6207
6208        let serialized_workspace =
6209            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6210
6211        let workspace_id = if let Some(workspace_id) =
6212            serialized_workspace.as_ref().map(|workspace| workspace.id)
6213        {
6214            workspace_id
6215        } else {
6216            persistence::DB.next_id().await?
6217        };
6218
6219        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6220    })
6221}
6222
6223pub fn join_in_room_project(
6224    project_id: u64,
6225    follow_user_id: u64,
6226    app_state: Arc<AppState>,
6227    cx: &mut App,
6228) -> Task<Result<()>> {
6229    let windows = cx.windows();
6230    cx.spawn(|mut cx| async move {
6231        let existing_workspace = windows.into_iter().find_map(|window_handle| {
6232            window_handle
6233                .downcast::<Workspace>()
6234                .and_then(|window_handle| {
6235                    window_handle
6236                        .update(&mut cx, |workspace, _window, cx| {
6237                            if workspace.project().read(cx).remote_id() == Some(project_id) {
6238                                Some(window_handle)
6239                            } else {
6240                                None
6241                            }
6242                        })
6243                        .unwrap_or(None)
6244                })
6245        });
6246
6247        let workspace = if let Some(existing_workspace) = existing_workspace {
6248            existing_workspace
6249        } else {
6250            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6251            let room = active_call
6252                .read_with(&cx, |call, _| call.room().cloned())?
6253                .ok_or_else(|| anyhow!("not in a call"))?;
6254            let project = room
6255                .update(&mut cx, |room, cx| {
6256                    room.join_project(
6257                        project_id,
6258                        app_state.languages.clone(),
6259                        app_state.fs.clone(),
6260                        cx,
6261                    )
6262                })?
6263                .await?;
6264
6265            let window_bounds_override = window_bounds_env_override();
6266            cx.update(|cx| {
6267                let mut options = (app_state.build_window_options)(None, cx);
6268                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6269                cx.open_window(options, |window, cx| {
6270                    cx.new(|cx| {
6271                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6272                    })
6273                })
6274            })??
6275        };
6276
6277        workspace.update(&mut cx, |workspace, window, cx| {
6278            cx.activate(true);
6279            window.activate_window();
6280
6281            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6282                let follow_peer_id = room
6283                    .read(cx)
6284                    .remote_participants()
6285                    .iter()
6286                    .find(|(_, participant)| participant.user.id == follow_user_id)
6287                    .map(|(_, p)| p.peer_id)
6288                    .or_else(|| {
6289                        // If we couldn't follow the given user, follow the host instead.
6290                        let collaborator = workspace
6291                            .project()
6292                            .read(cx)
6293                            .collaborators()
6294                            .values()
6295                            .find(|collaborator| collaborator.is_host)?;
6296                        Some(collaborator.peer_id)
6297                    });
6298
6299                if let Some(follow_peer_id) = follow_peer_id {
6300                    workspace.follow(follow_peer_id, window, cx);
6301                }
6302            }
6303        })?;
6304
6305        anyhow::Ok(())
6306    })
6307}
6308
6309pub fn reload(reload: &Reload, cx: &mut App) {
6310    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6311    let mut workspace_windows = cx
6312        .windows()
6313        .into_iter()
6314        .filter_map(|window| window.downcast::<Workspace>())
6315        .collect::<Vec<_>>();
6316
6317    // If multiple windows have unsaved changes, and need a save prompt,
6318    // prompt in the active window before switching to a different window.
6319    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6320
6321    let mut prompt = None;
6322    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6323        prompt = window
6324            .update(cx, |_, window, cx| {
6325                window.prompt(
6326                    PromptLevel::Info,
6327                    "Are you sure you want to restart?",
6328                    None,
6329                    &["Restart", "Cancel"],
6330                    cx,
6331                )
6332            })
6333            .ok();
6334    }
6335
6336    let binary_path = reload.binary_path.clone();
6337    cx.spawn(|mut cx| async move {
6338        if let Some(prompt) = prompt {
6339            let answer = prompt.await?;
6340            if answer != 0 {
6341                return Ok(());
6342            }
6343        }
6344
6345        // If the user cancels any save prompt, then keep the app open.
6346        for window in workspace_windows {
6347            if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6348                workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6349            }) {
6350                if !should_close.await? {
6351                    return Ok(());
6352                }
6353            }
6354        }
6355
6356        cx.update(|cx| cx.restart(binary_path))
6357    })
6358    .detach_and_log_err(cx);
6359}
6360
6361fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6362    let mut parts = value.split(',');
6363    let x: usize = parts.next()?.parse().ok()?;
6364    let y: usize = parts.next()?.parse().ok()?;
6365    Some(point(px(x as f32), px(y as f32)))
6366}
6367
6368fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6369    let mut parts = value.split(',');
6370    let width: usize = parts.next()?.parse().ok()?;
6371    let height: usize = parts.next()?.parse().ok()?;
6372    Some(size(px(width as f32), px(height as f32)))
6373}
6374
6375pub fn client_side_decorations(
6376    element: impl IntoElement,
6377    window: &mut Window,
6378    cx: &mut App,
6379) -> Stateful<Div> {
6380    const BORDER_SIZE: Pixels = px(1.0);
6381    let decorations = window.window_decorations();
6382
6383    if matches!(decorations, Decorations::Client { .. }) {
6384        window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6385    }
6386
6387    struct GlobalResizeEdge(ResizeEdge);
6388    impl Global for GlobalResizeEdge {}
6389
6390    div()
6391        .id("window-backdrop")
6392        .bg(transparent_black())
6393        .map(|div| match decorations {
6394            Decorations::Server => div,
6395            Decorations::Client { tiling, .. } => div
6396                .when(!(tiling.top || tiling.right), |div| {
6397                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6398                })
6399                .when(!(tiling.top || tiling.left), |div| {
6400                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6401                })
6402                .when(!(tiling.bottom || tiling.right), |div| {
6403                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6404                })
6405                .when(!(tiling.bottom || tiling.left), |div| {
6406                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6407                })
6408                .when(!tiling.top, |div| {
6409                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6410                })
6411                .when(!tiling.bottom, |div| {
6412                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6413                })
6414                .when(!tiling.left, |div| {
6415                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6416                })
6417                .when(!tiling.right, |div| {
6418                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6419                })
6420                .on_mouse_move(move |e, window, cx| {
6421                    let size = window.window_bounds().get_bounds().size;
6422                    let pos = e.position;
6423
6424                    let new_edge =
6425                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6426
6427                    let edge = cx.try_global::<GlobalResizeEdge>();
6428                    if new_edge != edge.map(|edge| edge.0) {
6429                        window
6430                            .window_handle()
6431                            .update(cx, |workspace, _, cx| {
6432                                cx.notify(workspace.entity_id());
6433                            })
6434                            .ok();
6435                    }
6436                })
6437                .on_mouse_down(MouseButton::Left, move |e, window, _| {
6438                    let size = window.window_bounds().get_bounds().size;
6439                    let pos = e.position;
6440
6441                    let edge = match resize_edge(
6442                        pos,
6443                        theme::CLIENT_SIDE_DECORATION_SHADOW,
6444                        size,
6445                        tiling,
6446                    ) {
6447                        Some(value) => value,
6448                        None => return,
6449                    };
6450
6451                    window.start_window_resize(edge);
6452                }),
6453        })
6454        .size_full()
6455        .child(
6456            div()
6457                .cursor(CursorStyle::Arrow)
6458                .map(|div| match decorations {
6459                    Decorations::Server => div,
6460                    Decorations::Client { tiling } => div
6461                        .border_color(cx.theme().colors().border)
6462                        .when(!(tiling.top || tiling.right), |div| {
6463                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6464                        })
6465                        .when(!(tiling.top || tiling.left), |div| {
6466                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6467                        })
6468                        .when(!(tiling.bottom || tiling.right), |div| {
6469                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6470                        })
6471                        .when(!(tiling.bottom || tiling.left), |div| {
6472                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6473                        })
6474                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6475                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6476                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6477                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6478                        .when(!tiling.is_tiled(), |div| {
6479                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
6480                                color: Hsla {
6481                                    h: 0.,
6482                                    s: 0.,
6483                                    l: 0.,
6484                                    a: 0.4,
6485                                },
6486                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6487                                spread_radius: px(0.),
6488                                offset: point(px(0.0), px(0.0)),
6489                            }])
6490                        }),
6491                })
6492                .on_mouse_move(|_e, _, cx| {
6493                    cx.stop_propagation();
6494                })
6495                .size_full()
6496                .child(element),
6497        )
6498        .map(|div| match decorations {
6499            Decorations::Server => div,
6500            Decorations::Client { tiling, .. } => div.child(
6501                canvas(
6502                    |_bounds, window, _| {
6503                        window.insert_hitbox(
6504                            Bounds::new(
6505                                point(px(0.0), px(0.0)),
6506                                window.window_bounds().get_bounds().size,
6507                            ),
6508                            false,
6509                        )
6510                    },
6511                    move |_bounds, hitbox, window, cx| {
6512                        let mouse = window.mouse_position();
6513                        let size = window.window_bounds().get_bounds().size;
6514                        let Some(edge) =
6515                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6516                        else {
6517                            return;
6518                        };
6519                        cx.set_global(GlobalResizeEdge(edge));
6520                        window.set_cursor_style(
6521                            match edge {
6522                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6523                                ResizeEdge::Left | ResizeEdge::Right => {
6524                                    CursorStyle::ResizeLeftRight
6525                                }
6526                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6527                                    CursorStyle::ResizeUpLeftDownRight
6528                                }
6529                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6530                                    CursorStyle::ResizeUpRightDownLeft
6531                                }
6532                            },
6533                            &hitbox,
6534                        );
6535                    },
6536                )
6537                .size_full()
6538                .absolute(),
6539            ),
6540        })
6541}
6542
6543fn resize_edge(
6544    pos: Point<Pixels>,
6545    shadow_size: Pixels,
6546    window_size: Size<Pixels>,
6547    tiling: Tiling,
6548) -> Option<ResizeEdge> {
6549    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6550    if bounds.contains(&pos) {
6551        return None;
6552    }
6553
6554    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6555    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6556    if !tiling.top && top_left_bounds.contains(&pos) {
6557        return Some(ResizeEdge::TopLeft);
6558    }
6559
6560    let top_right_bounds = Bounds::new(
6561        Point::new(window_size.width - corner_size.width, px(0.)),
6562        corner_size,
6563    );
6564    if !tiling.top && top_right_bounds.contains(&pos) {
6565        return Some(ResizeEdge::TopRight);
6566    }
6567
6568    let bottom_left_bounds = Bounds::new(
6569        Point::new(px(0.), window_size.height - corner_size.height),
6570        corner_size,
6571    );
6572    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6573        return Some(ResizeEdge::BottomLeft);
6574    }
6575
6576    let bottom_right_bounds = Bounds::new(
6577        Point::new(
6578            window_size.width - corner_size.width,
6579            window_size.height - corner_size.height,
6580        ),
6581        corner_size,
6582    );
6583    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6584        return Some(ResizeEdge::BottomRight);
6585    }
6586
6587    if !tiling.top && pos.y < shadow_size {
6588        Some(ResizeEdge::Top)
6589    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6590        Some(ResizeEdge::Bottom)
6591    } else if !tiling.left && pos.x < shadow_size {
6592        Some(ResizeEdge::Left)
6593    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6594        Some(ResizeEdge::Right)
6595    } else {
6596        None
6597    }
6598}
6599
6600fn join_pane_into_active(
6601    active_pane: &Entity<Pane>,
6602    pane: &Entity<Pane>,
6603    window: &mut Window,
6604    cx: &mut App,
6605) {
6606    if pane == active_pane {
6607        return;
6608    } else if pane.read(cx).items_len() == 0 {
6609        pane.update(cx, |_, cx| {
6610            cx.emit(pane::Event::Remove {
6611                focus_on_pane: None,
6612            });
6613        })
6614    } else {
6615        move_all_items(pane, active_pane, window, cx);
6616    }
6617}
6618
6619fn move_all_items(
6620    from_pane: &Entity<Pane>,
6621    to_pane: &Entity<Pane>,
6622    window: &mut Window,
6623    cx: &mut App,
6624) {
6625    let destination_is_different = from_pane != to_pane;
6626    let mut moved_items = 0;
6627    for (item_ix, item_handle) in from_pane
6628        .read(cx)
6629        .items()
6630        .enumerate()
6631        .map(|(ix, item)| (ix, item.clone()))
6632        .collect::<Vec<_>>()
6633    {
6634        let ix = item_ix - moved_items;
6635        if destination_is_different {
6636            // Close item from previous pane
6637            from_pane.update(cx, |source, cx| {
6638                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6639            });
6640            moved_items += 1;
6641        }
6642
6643        // This automatically removes duplicate items in the pane
6644        to_pane.update(cx, |destination, cx| {
6645            destination.add_item(item_handle, true, true, None, window, cx);
6646            window.focus(&destination.focus_handle(cx))
6647        });
6648    }
6649}
6650
6651pub fn move_item(
6652    source: &Entity<Pane>,
6653    destination: &Entity<Pane>,
6654    item_id_to_move: EntityId,
6655    destination_index: usize,
6656    window: &mut Window,
6657    cx: &mut App,
6658) {
6659    let Some((item_ix, item_handle)) = source
6660        .read(cx)
6661        .items()
6662        .enumerate()
6663        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6664        .map(|(ix, item)| (ix, item.clone()))
6665    else {
6666        // Tab was closed during drag
6667        return;
6668    };
6669
6670    if source != destination {
6671        // Close item from previous pane
6672        source.update(cx, |source, cx| {
6673            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6674        });
6675    }
6676
6677    // This automatically removes duplicate items in the pane
6678    destination.update(cx, |destination, cx| {
6679        destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6680        window.focus(&destination.focus_handle(cx))
6681    });
6682}
6683
6684pub fn move_active_item(
6685    source: &Entity<Pane>,
6686    destination: &Entity<Pane>,
6687    focus_destination: bool,
6688    close_if_empty: bool,
6689    window: &mut Window,
6690    cx: &mut App,
6691) {
6692    if source == destination {
6693        return;
6694    }
6695    let Some(active_item) = source.read(cx).active_item() else {
6696        return;
6697    };
6698    source.update(cx, |source_pane, cx| {
6699        let item_id = active_item.item_id();
6700        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6701        destination.update(cx, |target_pane, cx| {
6702            target_pane.add_item(
6703                active_item,
6704                focus_destination,
6705                focus_destination,
6706                Some(target_pane.items_len()),
6707                window,
6708                cx,
6709            );
6710        });
6711    });
6712}
6713
6714#[cfg(test)]
6715mod tests {
6716    use std::{cell::RefCell, rc::Rc};
6717
6718    use super::*;
6719    use crate::{
6720        dock::{test::TestPanel, PanelEvent},
6721        item::{
6722            test::{TestItem, TestProjectItem},
6723            ItemEvent,
6724        },
6725    };
6726    use fs::FakeFs;
6727    use gpui::{
6728        px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6729        UpdateGlobal, VisualTestContext,
6730    };
6731    use project::{Project, ProjectEntryId};
6732    use serde_json::json;
6733    use settings::SettingsStore;
6734
6735    #[gpui::test]
6736    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6737        init_test(cx);
6738
6739        let fs = FakeFs::new(cx.executor());
6740        let project = Project::test(fs, [], cx).await;
6741        let (workspace, cx) =
6742            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6743
6744        // Adding an item with no ambiguity renders the tab without detail.
6745        let item1 = cx.new(|cx| {
6746            let mut item = TestItem::new(cx);
6747            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6748            item
6749        });
6750        workspace.update_in(cx, |workspace, window, cx| {
6751            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6752        });
6753        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6754
6755        // Adding an item that creates ambiguity increases the level of detail on
6756        // both tabs.
6757        let item2 = cx.new_window_entity(|_window, cx| {
6758            let mut item = TestItem::new(cx);
6759            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6760            item
6761        });
6762        workspace.update_in(cx, |workspace, window, cx| {
6763            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6764        });
6765        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6766        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6767
6768        // Adding an item that creates ambiguity increases the level of detail only
6769        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6770        // we stop at the highest detail available.
6771        let item3 = cx.new(|cx| {
6772            let mut item = TestItem::new(cx);
6773            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6774            item
6775        });
6776        workspace.update_in(cx, |workspace, window, cx| {
6777            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6778        });
6779        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6780        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6781        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6782    }
6783
6784    #[gpui::test]
6785    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6786        init_test(cx);
6787
6788        let fs = FakeFs::new(cx.executor());
6789        fs.insert_tree(
6790            "/root1",
6791            json!({
6792                "one.txt": "",
6793                "two.txt": "",
6794            }),
6795        )
6796        .await;
6797        fs.insert_tree(
6798            "/root2",
6799            json!({
6800                "three.txt": "",
6801            }),
6802        )
6803        .await;
6804
6805        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6806        let (workspace, cx) =
6807            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6808        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6809        let worktree_id = project.update(cx, |project, cx| {
6810            project.worktrees(cx).next().unwrap().read(cx).id()
6811        });
6812
6813        let item1 = cx.new(|cx| {
6814            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6815        });
6816        let item2 = cx.new(|cx| {
6817            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6818        });
6819
6820        // Add an item to an empty pane
6821        workspace.update_in(cx, |workspace, window, cx| {
6822            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6823        });
6824        project.update(cx, |project, cx| {
6825            assert_eq!(
6826                project.active_entry(),
6827                project
6828                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6829                    .map(|e| e.id)
6830            );
6831        });
6832        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6833
6834        // Add a second item to a non-empty pane
6835        workspace.update_in(cx, |workspace, window, cx| {
6836            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6837        });
6838        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6839        project.update(cx, |project, cx| {
6840            assert_eq!(
6841                project.active_entry(),
6842                project
6843                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6844                    .map(|e| e.id)
6845            );
6846        });
6847
6848        // Close the active item
6849        pane.update_in(cx, |pane, window, cx| {
6850            pane.close_active_item(&Default::default(), window, cx)
6851                .unwrap()
6852        })
6853        .await
6854        .unwrap();
6855        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6856        project.update(cx, |project, cx| {
6857            assert_eq!(
6858                project.active_entry(),
6859                project
6860                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6861                    .map(|e| e.id)
6862            );
6863        });
6864
6865        // Add a project folder
6866        project
6867            .update(cx, |project, cx| {
6868                project.find_or_create_worktree("root2", true, cx)
6869            })
6870            .await
6871            .unwrap();
6872        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6873
6874        // Remove a project folder
6875        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6876        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6877    }
6878
6879    #[gpui::test]
6880    async fn test_close_window(cx: &mut TestAppContext) {
6881        init_test(cx);
6882
6883        let fs = FakeFs::new(cx.executor());
6884        fs.insert_tree("/root", json!({ "one": "" })).await;
6885
6886        let project = Project::test(fs, ["root".as_ref()], cx).await;
6887        let (workspace, cx) =
6888            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6889
6890        // When there are no dirty items, there's nothing to do.
6891        let item1 = cx.new(TestItem::new);
6892        workspace.update_in(cx, |w, window, cx| {
6893            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6894        });
6895        let task = workspace.update_in(cx, |w, window, cx| {
6896            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6897        });
6898        assert!(task.await.unwrap());
6899
6900        // When there are dirty untitled items, prompt to save each one. If the user
6901        // cancels any prompt, then abort.
6902        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6903        let item3 = cx.new(|cx| {
6904            TestItem::new(cx)
6905                .with_dirty(true)
6906                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6907        });
6908        workspace.update_in(cx, |w, window, cx| {
6909            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6910            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6911        });
6912        let task = workspace.update_in(cx, |w, window, cx| {
6913            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6914        });
6915        cx.executor().run_until_parked();
6916        cx.simulate_prompt_answer(2); // cancel save all
6917        cx.executor().run_until_parked();
6918        cx.simulate_prompt_answer(2); // cancel save all
6919        cx.executor().run_until_parked();
6920        assert!(!cx.has_pending_prompt());
6921        assert!(!task.await.unwrap());
6922    }
6923
6924    #[gpui::test]
6925    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6926        init_test(cx);
6927
6928        // Register TestItem as a serializable item
6929        cx.update(|cx| {
6930            register_serializable_item::<TestItem>(cx);
6931        });
6932
6933        let fs = FakeFs::new(cx.executor());
6934        fs.insert_tree("/root", json!({ "one": "" })).await;
6935
6936        let project = Project::test(fs, ["root".as_ref()], cx).await;
6937        let (workspace, cx) =
6938            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6939
6940        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6941        let item1 = cx.new(|cx| {
6942            TestItem::new(cx)
6943                .with_dirty(true)
6944                .with_serialize(|| Some(Task::ready(Ok(()))))
6945        });
6946        let item2 = cx.new(|cx| {
6947            TestItem::new(cx)
6948                .with_dirty(true)
6949                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6950                .with_serialize(|| Some(Task::ready(Ok(()))))
6951        });
6952        workspace.update_in(cx, |w, window, cx| {
6953            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6954            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6955        });
6956        let task = workspace.update_in(cx, |w, window, cx| {
6957            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6958        });
6959        assert!(task.await.unwrap());
6960    }
6961
6962    #[gpui::test]
6963    async fn test_close_pane_items(cx: &mut TestAppContext) {
6964        init_test(cx);
6965
6966        let fs = FakeFs::new(cx.executor());
6967
6968        let project = Project::test(fs, None, cx).await;
6969        let (workspace, cx) =
6970            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6971
6972        let item1 = cx.new(|cx| {
6973            TestItem::new(cx)
6974                .with_dirty(true)
6975                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
6976        });
6977        let item2 = cx.new(|cx| {
6978            TestItem::new(cx)
6979                .with_dirty(true)
6980                .with_conflict(true)
6981                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
6982        });
6983        let item3 = cx.new(|cx| {
6984            TestItem::new(cx)
6985                .with_dirty(true)
6986                .with_conflict(true)
6987                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
6988        });
6989        let item4 = cx.new(|cx| {
6990            TestItem::new(cx).with_dirty(true).with_project_items(&[{
6991                let project_item = TestProjectItem::new_untitled(cx);
6992                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6993                project_item
6994            }])
6995        });
6996        let pane = workspace.update_in(cx, |workspace, window, cx| {
6997            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6998            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6999            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7000            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7001            workspace.active_pane().clone()
7002        });
7003
7004        let close_items = pane.update_in(cx, |pane, window, cx| {
7005            pane.activate_item(1, true, true, window, cx);
7006            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7007            let item1_id = item1.item_id();
7008            let item3_id = item3.item_id();
7009            let item4_id = item4.item_id();
7010            pane.close_items(window, cx, SaveIntent::Close, move |id| {
7011                [item1_id, item3_id, item4_id].contains(&id)
7012            })
7013        });
7014        cx.executor().run_until_parked();
7015
7016        assert!(cx.has_pending_prompt());
7017        // Ignore "Save all" prompt
7018        cx.simulate_prompt_answer(2);
7019        cx.executor().run_until_parked();
7020        // There's a prompt to save item 1.
7021        pane.update(cx, |pane, _| {
7022            assert_eq!(pane.items_len(), 4);
7023            assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
7024        });
7025        // Confirm saving item 1.
7026        cx.simulate_prompt_answer(0);
7027        cx.executor().run_until_parked();
7028
7029        // Item 1 is saved. There's a prompt to save item 3.
7030        pane.update(cx, |pane, cx| {
7031            assert_eq!(item1.read(cx).save_count, 1);
7032            assert_eq!(item1.read(cx).save_as_count, 0);
7033            assert_eq!(item1.read(cx).reload_count, 0);
7034            assert_eq!(pane.items_len(), 3);
7035            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7036        });
7037        assert!(cx.has_pending_prompt());
7038
7039        // Cancel saving item 3.
7040        cx.simulate_prompt_answer(1);
7041        cx.executor().run_until_parked();
7042
7043        // Item 3 is reloaded. There's a prompt to save item 4.
7044        pane.update(cx, |pane, cx| {
7045            assert_eq!(item3.read(cx).save_count, 0);
7046            assert_eq!(item3.read(cx).save_as_count, 0);
7047            assert_eq!(item3.read(cx).reload_count, 1);
7048            assert_eq!(pane.items_len(), 2);
7049            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7050        });
7051        assert!(cx.has_pending_prompt());
7052
7053        // Confirm saving item 4.
7054        cx.simulate_prompt_answer(0);
7055        cx.executor().run_until_parked();
7056
7057        // There's a prompt for a path for item 4.
7058        cx.simulate_new_path_selection(|_| Some(Default::default()));
7059        close_items.await.unwrap();
7060
7061        // The requested items are closed.
7062        pane.update(cx, |pane, cx| {
7063            assert_eq!(item4.read(cx).save_count, 0);
7064            assert_eq!(item4.read(cx).save_as_count, 1);
7065            assert_eq!(item4.read(cx).reload_count, 0);
7066            assert_eq!(pane.items_len(), 1);
7067            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7068        });
7069    }
7070
7071    #[gpui::test]
7072    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7073        init_test(cx);
7074
7075        let fs = FakeFs::new(cx.executor());
7076        let project = Project::test(fs, [], cx).await;
7077        let (workspace, cx) =
7078            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7079
7080        // Create several workspace items with single project entries, and two
7081        // workspace items with multiple project entries.
7082        let single_entry_items = (0..=4)
7083            .map(|project_entry_id| {
7084                cx.new(|cx| {
7085                    TestItem::new(cx)
7086                        .with_dirty(true)
7087                        .with_project_items(&[dirty_project_item(
7088                            project_entry_id,
7089                            &format!("{project_entry_id}.txt"),
7090                            cx,
7091                        )])
7092                })
7093            })
7094            .collect::<Vec<_>>();
7095        let item_2_3 = cx.new(|cx| {
7096            TestItem::new(cx)
7097                .with_dirty(true)
7098                .with_singleton(false)
7099                .with_project_items(&[
7100                    single_entry_items[2].read(cx).project_items[0].clone(),
7101                    single_entry_items[3].read(cx).project_items[0].clone(),
7102                ])
7103        });
7104        let item_3_4 = cx.new(|cx| {
7105            TestItem::new(cx)
7106                .with_dirty(true)
7107                .with_singleton(false)
7108                .with_project_items(&[
7109                    single_entry_items[3].read(cx).project_items[0].clone(),
7110                    single_entry_items[4].read(cx).project_items[0].clone(),
7111                ])
7112        });
7113
7114        // Create two panes that contain the following project entries:
7115        //   left pane:
7116        //     multi-entry items:   (2, 3)
7117        //     single-entry items:  0, 1, 2, 3, 4
7118        //   right pane:
7119        //     single-entry items:  1
7120        //     multi-entry items:   (3, 4)
7121        let left_pane = workspace.update_in(cx, |workspace, window, cx| {
7122            let left_pane = workspace.active_pane().clone();
7123            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7124            for item in single_entry_items {
7125                workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
7126            }
7127            left_pane.update(cx, |pane, cx| {
7128                pane.activate_item(2, true, true, window, cx);
7129            });
7130
7131            let right_pane = workspace
7132                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7133                .unwrap();
7134
7135            right_pane.update(cx, |pane, cx| {
7136                pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7137            });
7138
7139            left_pane
7140        });
7141
7142        cx.focus(&left_pane);
7143
7144        // When closing all of the items in the left pane, we should be prompted twice:
7145        // once for project entry 0, and once for project entry 2. Project entries 1,
7146        // 3, and 4 are all still open in the other paten. After those two
7147        // prompts, the task should complete.
7148
7149        let close = left_pane.update_in(cx, |pane, window, cx| {
7150            pane.close_all_items(&CloseAllItems::default(), window, cx)
7151                .unwrap()
7152        });
7153        cx.executor().run_until_parked();
7154
7155        // Discard "Save all" prompt
7156        cx.simulate_prompt_answer(2);
7157
7158        cx.executor().run_until_parked();
7159        left_pane.update(cx, |pane, cx| {
7160            assert_eq!(
7161                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7162                &[ProjectEntryId::from_proto(0)]
7163            );
7164        });
7165        cx.simulate_prompt_answer(0);
7166
7167        cx.executor().run_until_parked();
7168        left_pane.update(cx, |pane, cx| {
7169            assert_eq!(
7170                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7171                &[ProjectEntryId::from_proto(2)]
7172            );
7173        });
7174        cx.simulate_prompt_answer(0);
7175
7176        cx.executor().run_until_parked();
7177        close.await.unwrap();
7178        left_pane.update(cx, |pane, _| {
7179            assert_eq!(pane.items_len(), 0);
7180        });
7181    }
7182
7183    #[gpui::test]
7184    async fn test_autosave(cx: &mut gpui::TestAppContext) {
7185        init_test(cx);
7186
7187        let fs = FakeFs::new(cx.executor());
7188        let project = Project::test(fs, [], cx).await;
7189        let (workspace, cx) =
7190            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7191        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7192
7193        let item = cx.new(|cx| {
7194            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7195        });
7196        let item_id = item.entity_id();
7197        workspace.update_in(cx, |workspace, window, cx| {
7198            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7199        });
7200
7201        // Autosave on window change.
7202        item.update(cx, |item, cx| {
7203            SettingsStore::update_global(cx, |settings, cx| {
7204                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7205                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
7206                })
7207            });
7208            item.is_dirty = true;
7209        });
7210
7211        // Deactivating the window saves the file.
7212        cx.deactivate_window();
7213        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7214
7215        // Re-activating the window doesn't save the file.
7216        cx.update(|window, _| window.activate_window());
7217        cx.executor().run_until_parked();
7218        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7219
7220        // Autosave on focus change.
7221        item.update_in(cx, |item, window, cx| {
7222            cx.focus_self(window);
7223            SettingsStore::update_global(cx, |settings, cx| {
7224                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7225                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7226                })
7227            });
7228            item.is_dirty = true;
7229        });
7230
7231        // Blurring the item saves the file.
7232        item.update_in(cx, |_, window, _| window.blur());
7233        cx.executor().run_until_parked();
7234        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7235
7236        // Deactivating the window still saves the file.
7237        item.update_in(cx, |item, window, cx| {
7238            cx.focus_self(window);
7239            item.is_dirty = true;
7240        });
7241        cx.deactivate_window();
7242        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7243
7244        // Autosave after delay.
7245        item.update(cx, |item, cx| {
7246            SettingsStore::update_global(cx, |settings, cx| {
7247                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7248                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7249                })
7250            });
7251            item.is_dirty = true;
7252            cx.emit(ItemEvent::Edit);
7253        });
7254
7255        // Delay hasn't fully expired, so the file is still dirty and unsaved.
7256        cx.executor().advance_clock(Duration::from_millis(250));
7257        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7258
7259        // After delay expires, the file is saved.
7260        cx.executor().advance_clock(Duration::from_millis(250));
7261        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7262
7263        // Autosave on focus change, ensuring closing the tab counts as such.
7264        item.update(cx, |item, cx| {
7265            SettingsStore::update_global(cx, |settings, cx| {
7266                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7267                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7268                })
7269            });
7270            item.is_dirty = true;
7271            for project_item in &mut item.project_items {
7272                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7273            }
7274        });
7275
7276        pane.update_in(cx, |pane, window, cx| {
7277            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7278        })
7279        .await
7280        .unwrap();
7281        assert!(!cx.has_pending_prompt());
7282        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7283
7284        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7285        workspace.update_in(cx, |workspace, window, cx| {
7286            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7287        });
7288        item.update_in(cx, |item, window, cx| {
7289            item.project_items[0].update(cx, |item, _| {
7290                item.entry_id = None;
7291            });
7292            item.is_dirty = true;
7293            window.blur();
7294        });
7295        cx.run_until_parked();
7296        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7297
7298        // Ensure autosave is prevented for deleted files also when closing the buffer.
7299        let _close_items = pane.update_in(cx, |pane, window, cx| {
7300            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7301        });
7302        cx.run_until_parked();
7303        assert!(cx.has_pending_prompt());
7304        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7305    }
7306
7307    #[gpui::test]
7308    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7309        init_test(cx);
7310
7311        let fs = FakeFs::new(cx.executor());
7312
7313        let project = Project::test(fs, [], cx).await;
7314        let (workspace, cx) =
7315            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7316
7317        let item = cx.new(|cx| {
7318            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7319        });
7320        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7321        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7322        let toolbar_notify_count = Rc::new(RefCell::new(0));
7323
7324        workspace.update_in(cx, |workspace, window, cx| {
7325            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7326            let toolbar_notification_count = toolbar_notify_count.clone();
7327            cx.observe_in(&toolbar, window, move |_, _, _, _| {
7328                *toolbar_notification_count.borrow_mut() += 1
7329            })
7330            .detach();
7331        });
7332
7333        pane.update(cx, |pane, _| {
7334            assert!(!pane.can_navigate_backward());
7335            assert!(!pane.can_navigate_forward());
7336        });
7337
7338        item.update_in(cx, |item, _, cx| {
7339            item.set_state("one".to_string(), cx);
7340        });
7341
7342        // Toolbar must be notified to re-render the navigation buttons
7343        assert_eq!(*toolbar_notify_count.borrow(), 1);
7344
7345        pane.update(cx, |pane, _| {
7346            assert!(pane.can_navigate_backward());
7347            assert!(!pane.can_navigate_forward());
7348        });
7349
7350        workspace
7351            .update_in(cx, |workspace, window, cx| {
7352                workspace.go_back(pane.downgrade(), window, cx)
7353            })
7354            .await
7355            .unwrap();
7356
7357        assert_eq!(*toolbar_notify_count.borrow(), 2);
7358        pane.update(cx, |pane, _| {
7359            assert!(!pane.can_navigate_backward());
7360            assert!(pane.can_navigate_forward());
7361        });
7362    }
7363
7364    #[gpui::test]
7365    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7366        init_test(cx);
7367        let fs = FakeFs::new(cx.executor());
7368
7369        let project = Project::test(fs, [], cx).await;
7370        let (workspace, cx) =
7371            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7372
7373        let panel = workspace.update_in(cx, |workspace, window, cx| {
7374            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7375            workspace.add_panel(panel.clone(), window, cx);
7376
7377            workspace
7378                .right_dock()
7379                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7380
7381            panel
7382        });
7383
7384        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7385        pane.update_in(cx, |pane, window, cx| {
7386            let item = cx.new(TestItem::new);
7387            pane.add_item(Box::new(item), true, true, None, window, cx);
7388        });
7389
7390        // Transfer focus from center to panel
7391        workspace.update_in(cx, |workspace, window, cx| {
7392            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7393        });
7394
7395        workspace.update_in(cx, |workspace, window, cx| {
7396            assert!(workspace.right_dock().read(cx).is_open());
7397            assert!(!panel.is_zoomed(window, cx));
7398            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7399        });
7400
7401        // Transfer focus from panel to center
7402        workspace.update_in(cx, |workspace, window, cx| {
7403            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7404        });
7405
7406        workspace.update_in(cx, |workspace, window, cx| {
7407            assert!(workspace.right_dock().read(cx).is_open());
7408            assert!(!panel.is_zoomed(window, cx));
7409            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7410        });
7411
7412        // Close the dock
7413        workspace.update_in(cx, |workspace, window, cx| {
7414            workspace.toggle_dock(DockPosition::Right, 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        // Open the dock
7424        workspace.update_in(cx, |workspace, window, cx| {
7425            workspace.toggle_dock(DockPosition::Right, 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        // Focus and zoom panel
7435        panel.update_in(cx, |panel, window, cx| {
7436            cx.focus_self(window);
7437            panel.set_zoomed(true, window, cx)
7438        });
7439
7440        workspace.update_in(cx, |workspace, window, cx| {
7441            assert!(workspace.right_dock().read(cx).is_open());
7442            assert!(panel.is_zoomed(window, cx));
7443            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7444        });
7445
7446        // Transfer focus to the center closes the dock
7447        workspace.update_in(cx, |workspace, window, cx| {
7448            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7449        });
7450
7451        workspace.update_in(cx, |workspace, window, cx| {
7452            assert!(!workspace.right_dock().read(cx).is_open());
7453            assert!(panel.is_zoomed(window, cx));
7454            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7455        });
7456
7457        // Transferring focus back to the panel keeps it zoomed
7458        workspace.update_in(cx, |workspace, window, cx| {
7459            workspace.toggle_panel_focus::<TestPanel>(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        // Close the dock while it is zoomed
7469        workspace.update_in(cx, |workspace, window, cx| {
7470            workspace.toggle_dock(DockPosition::Right, 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!(workspace.zoomed.is_none());
7477            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7478        });
7479
7480        // Opening the dock, when it's zoomed, retains focus
7481        workspace.update_in(cx, |workspace, window, cx| {
7482            workspace.toggle_dock(DockPosition::Right, window, cx)
7483        });
7484
7485        workspace.update_in(cx, |workspace, window, cx| {
7486            assert!(workspace.right_dock().read(cx).is_open());
7487            assert!(panel.is_zoomed(window, cx));
7488            assert!(workspace.zoomed.is_some());
7489            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7490        });
7491
7492        // Unzoom and close the panel, zoom the active pane.
7493        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7494        workspace.update_in(cx, |workspace, window, cx| {
7495            workspace.toggle_dock(DockPosition::Right, window, cx)
7496        });
7497        pane.update_in(cx, |pane, window, cx| {
7498            pane.toggle_zoom(&Default::default(), window, cx)
7499        });
7500
7501        // Opening a dock unzooms the pane.
7502        workspace.update_in(cx, |workspace, window, cx| {
7503            workspace.toggle_dock(DockPosition::Right, window, cx)
7504        });
7505        workspace.update_in(cx, |workspace, window, cx| {
7506            let pane = pane.read(cx);
7507            assert!(!pane.is_zoomed());
7508            assert!(!pane.focus_handle(cx).is_focused(window));
7509            assert!(workspace.right_dock().read(cx).is_open());
7510            assert!(workspace.zoomed.is_none());
7511        });
7512    }
7513
7514    #[gpui::test]
7515    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7516        init_test(cx);
7517
7518        let fs = FakeFs::new(cx.executor());
7519
7520        let project = Project::test(fs, None, cx).await;
7521        let (workspace, cx) =
7522            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7523
7524        // Let's arrange the panes like this:
7525        //
7526        // +-----------------------+
7527        // |         top           |
7528        // +------+--------+-------+
7529        // | left | center | right |
7530        // +------+--------+-------+
7531        // |        bottom         |
7532        // +-----------------------+
7533
7534        let top_item = cx.new(|cx| {
7535            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7536        });
7537        let bottom_item = cx.new(|cx| {
7538            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7539        });
7540        let left_item = cx.new(|cx| {
7541            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7542        });
7543        let right_item = cx.new(|cx| {
7544            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7545        });
7546        let center_item = cx.new(|cx| {
7547            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7548        });
7549
7550        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7551            let top_pane_id = workspace.active_pane().entity_id();
7552            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7553            workspace.split_pane(
7554                workspace.active_pane().clone(),
7555                SplitDirection::Down,
7556                window,
7557                cx,
7558            );
7559            top_pane_id
7560        });
7561        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7562            let bottom_pane_id = workspace.active_pane().entity_id();
7563            workspace.add_item_to_active_pane(
7564                Box::new(bottom_item.clone()),
7565                None,
7566                false,
7567                window,
7568                cx,
7569            );
7570            workspace.split_pane(
7571                workspace.active_pane().clone(),
7572                SplitDirection::Up,
7573                window,
7574                cx,
7575            );
7576            bottom_pane_id
7577        });
7578        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7579            let left_pane_id = workspace.active_pane().entity_id();
7580            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7581            workspace.split_pane(
7582                workspace.active_pane().clone(),
7583                SplitDirection::Right,
7584                window,
7585                cx,
7586            );
7587            left_pane_id
7588        });
7589        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7590            let right_pane_id = workspace.active_pane().entity_id();
7591            workspace.add_item_to_active_pane(
7592                Box::new(right_item.clone()),
7593                None,
7594                false,
7595                window,
7596                cx,
7597            );
7598            workspace.split_pane(
7599                workspace.active_pane().clone(),
7600                SplitDirection::Left,
7601                window,
7602                cx,
7603            );
7604            right_pane_id
7605        });
7606        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7607            let center_pane_id = workspace.active_pane().entity_id();
7608            workspace.add_item_to_active_pane(
7609                Box::new(center_item.clone()),
7610                None,
7611                false,
7612                window,
7613                cx,
7614            );
7615            center_pane_id
7616        });
7617        cx.executor().run_until_parked();
7618
7619        workspace.update_in(cx, |workspace, window, cx| {
7620            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7621
7622            // Join into next from center pane into right
7623            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7624        });
7625
7626        workspace.update_in(cx, |workspace, window, cx| {
7627            let active_pane = workspace.active_pane();
7628            assert_eq!(right_pane_id, active_pane.entity_id());
7629            assert_eq!(2, active_pane.read(cx).items_len());
7630            let item_ids_in_pane =
7631                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7632            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7633            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7634
7635            // Join into next from right pane into bottom
7636            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7637        });
7638
7639        workspace.update_in(cx, |workspace, window, cx| {
7640            let active_pane = workspace.active_pane();
7641            assert_eq!(bottom_pane_id, active_pane.entity_id());
7642            assert_eq!(3, active_pane.read(cx).items_len());
7643            let item_ids_in_pane =
7644                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7645            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7646            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7647            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7648
7649            // Join into next from bottom pane into left
7650            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7651        });
7652
7653        workspace.update_in(cx, |workspace, window, cx| {
7654            let active_pane = workspace.active_pane();
7655            assert_eq!(left_pane_id, active_pane.entity_id());
7656            assert_eq!(4, active_pane.read(cx).items_len());
7657            let item_ids_in_pane =
7658                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7659            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7660            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7661            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7662            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7663
7664            // Join into next from left pane into top
7665            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7666        });
7667
7668        workspace.update_in(cx, |workspace, window, cx| {
7669            let active_pane = workspace.active_pane();
7670            assert_eq!(top_pane_id, active_pane.entity_id());
7671            assert_eq!(5, active_pane.read(cx).items_len());
7672            let item_ids_in_pane =
7673                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7674            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7675            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7676            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7677            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7678            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7679
7680            // Single pane left: no-op
7681            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7682        });
7683
7684        workspace.update(cx, |workspace, _cx| {
7685            let active_pane = workspace.active_pane();
7686            assert_eq!(top_pane_id, active_pane.entity_id());
7687        });
7688    }
7689
7690    fn add_an_item_to_active_pane(
7691        cx: &mut VisualTestContext,
7692        workspace: &Entity<Workspace>,
7693        item_id: u64,
7694    ) -> Entity<TestItem> {
7695        let item = cx.new(|cx| {
7696            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7697                item_id,
7698                "item{item_id}.txt",
7699                cx,
7700            )])
7701        });
7702        workspace.update_in(cx, |workspace, window, cx| {
7703            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7704        });
7705        return item;
7706    }
7707
7708    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7709        return workspace.update_in(cx, |workspace, window, cx| {
7710            let new_pane = workspace.split_pane(
7711                workspace.active_pane().clone(),
7712                SplitDirection::Right,
7713                window,
7714                cx,
7715            );
7716            new_pane
7717        });
7718    }
7719
7720    #[gpui::test]
7721    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7722        init_test(cx);
7723        let fs = FakeFs::new(cx.executor());
7724        let project = Project::test(fs, None, cx).await;
7725        let (workspace, cx) =
7726            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7727
7728        add_an_item_to_active_pane(cx, &workspace, 1);
7729        split_pane(cx, &workspace);
7730        add_an_item_to_active_pane(cx, &workspace, 2);
7731        split_pane(cx, &workspace); // empty pane
7732        split_pane(cx, &workspace);
7733        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7734
7735        cx.executor().run_until_parked();
7736
7737        workspace.update(cx, |workspace, cx| {
7738            let num_panes = workspace.panes().len();
7739            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7740            let active_item = workspace
7741                .active_pane()
7742                .read(cx)
7743                .active_item()
7744                .expect("item is in focus");
7745
7746            assert_eq!(num_panes, 4);
7747            assert_eq!(num_items_in_current_pane, 1);
7748            assert_eq!(active_item.item_id(), last_item.item_id());
7749        });
7750
7751        workspace.update_in(cx, |workspace, window, cx| {
7752            workspace.join_all_panes(window, cx);
7753        });
7754
7755        workspace.update(cx, |workspace, cx| {
7756            let num_panes = workspace.panes().len();
7757            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7758            let active_item = workspace
7759                .active_pane()
7760                .read(cx)
7761                .active_item()
7762                .expect("item is in focus");
7763
7764            assert_eq!(num_panes, 1);
7765            assert_eq!(num_items_in_current_pane, 3);
7766            assert_eq!(active_item.item_id(), last_item.item_id());
7767        });
7768    }
7769    struct TestModal(FocusHandle);
7770
7771    impl TestModal {
7772        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7773            Self(cx.focus_handle())
7774        }
7775    }
7776
7777    impl EventEmitter<DismissEvent> for TestModal {}
7778
7779    impl Focusable for TestModal {
7780        fn focus_handle(&self, _cx: &App) -> FocusHandle {
7781            self.0.clone()
7782        }
7783    }
7784
7785    impl ModalView for TestModal {}
7786
7787    impl Render for TestModal {
7788        fn render(
7789            &mut self,
7790            _window: &mut Window,
7791            _cx: &mut Context<TestModal>,
7792        ) -> impl IntoElement {
7793            div().track_focus(&self.0)
7794        }
7795    }
7796
7797    #[gpui::test]
7798    async fn test_panels(cx: &mut gpui::TestAppContext) {
7799        init_test(cx);
7800        let fs = FakeFs::new(cx.executor());
7801
7802        let project = Project::test(fs, [], cx).await;
7803        let (workspace, cx) =
7804            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7805
7806        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7807            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7808            workspace.add_panel(panel_1.clone(), window, cx);
7809            workspace.toggle_dock(DockPosition::Left, window, cx);
7810            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7811            workspace.add_panel(panel_2.clone(), window, cx);
7812            workspace.toggle_dock(DockPosition::Right, window, cx);
7813
7814            let left_dock = workspace.left_dock();
7815            assert_eq!(
7816                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7817                panel_1.panel_id()
7818            );
7819            assert_eq!(
7820                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7821                panel_1.size(window, cx)
7822            );
7823
7824            left_dock.update(cx, |left_dock, cx| {
7825                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7826            });
7827            assert_eq!(
7828                workspace
7829                    .right_dock()
7830                    .read(cx)
7831                    .visible_panel()
7832                    .unwrap()
7833                    .panel_id(),
7834                panel_2.panel_id(),
7835            );
7836
7837            (panel_1, panel_2)
7838        });
7839
7840        // Move panel_1 to the right
7841        panel_1.update_in(cx, |panel_1, window, cx| {
7842            panel_1.set_position(DockPosition::Right, window, cx)
7843        });
7844
7845        workspace.update_in(cx, |workspace, window, cx| {
7846            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7847            // Since it was the only panel on the left, the left dock should now be closed.
7848            assert!(!workspace.left_dock().read(cx).is_open());
7849            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7850            let right_dock = workspace.right_dock();
7851            assert_eq!(
7852                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7853                panel_1.panel_id()
7854            );
7855            assert_eq!(
7856                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7857                px(1337.)
7858            );
7859
7860            // Now we move panel_2 to the left
7861            panel_2.set_position(DockPosition::Left, window, cx);
7862        });
7863
7864        workspace.update(cx, |workspace, cx| {
7865            // Since panel_2 was not visible on the right, we don't open the left dock.
7866            assert!(!workspace.left_dock().read(cx).is_open());
7867            // And the right dock is unaffected in its displaying of panel_1
7868            assert!(workspace.right_dock().read(cx).is_open());
7869            assert_eq!(
7870                workspace
7871                    .right_dock()
7872                    .read(cx)
7873                    .visible_panel()
7874                    .unwrap()
7875                    .panel_id(),
7876                panel_1.panel_id(),
7877            );
7878        });
7879
7880        // Move panel_1 back to the left
7881        panel_1.update_in(cx, |panel_1, window, cx| {
7882            panel_1.set_position(DockPosition::Left, window, cx)
7883        });
7884
7885        workspace.update_in(cx, |workspace, window, cx| {
7886            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7887            let left_dock = workspace.left_dock();
7888            assert!(left_dock.read(cx).is_open());
7889            assert_eq!(
7890                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7891                panel_1.panel_id()
7892            );
7893            assert_eq!(
7894                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7895                px(1337.)
7896            );
7897            // And the right dock should be closed as it no longer has any panels.
7898            assert!(!workspace.right_dock().read(cx).is_open());
7899
7900            // Now we move panel_1 to the bottom
7901            panel_1.set_position(DockPosition::Bottom, window, cx);
7902        });
7903
7904        workspace.update_in(cx, |workspace, window, cx| {
7905            // Since panel_1 was visible on the left, we close the left dock.
7906            assert!(!workspace.left_dock().read(cx).is_open());
7907            // The bottom dock is sized based on the panel's default size,
7908            // since the panel orientation changed from vertical to horizontal.
7909            let bottom_dock = workspace.bottom_dock();
7910            assert_eq!(
7911                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
7912                panel_1.size(window, cx),
7913            );
7914            // Close bottom dock and move panel_1 back to the left.
7915            bottom_dock.update(cx, |bottom_dock, cx| {
7916                bottom_dock.set_open(false, window, cx)
7917            });
7918            panel_1.set_position(DockPosition::Left, window, cx);
7919        });
7920
7921        // Emit activated event on panel 1
7922        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7923
7924        // Now the left dock is open and panel_1 is active and focused.
7925        workspace.update_in(cx, |workspace, window, cx| {
7926            let left_dock = workspace.left_dock();
7927            assert!(left_dock.read(cx).is_open());
7928            assert_eq!(
7929                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7930                panel_1.panel_id(),
7931            );
7932            assert!(panel_1.focus_handle(cx).is_focused(window));
7933        });
7934
7935        // Emit closed event on panel 2, which is not active
7936        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7937
7938        // Wo don't close the left dock, because panel_2 wasn't the active panel
7939        workspace.update(cx, |workspace, cx| {
7940            let left_dock = workspace.left_dock();
7941            assert!(left_dock.read(cx).is_open());
7942            assert_eq!(
7943                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7944                panel_1.panel_id(),
7945            );
7946        });
7947
7948        // Emitting a ZoomIn event shows the panel as zoomed.
7949        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7950        workspace.update(cx, |workspace, _| {
7951            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7952            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7953        });
7954
7955        // Move panel to another dock while it is zoomed
7956        panel_1.update_in(cx, |panel, window, cx| {
7957            panel.set_position(DockPosition::Right, window, cx)
7958        });
7959        workspace.update(cx, |workspace, _| {
7960            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7961
7962            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7963        });
7964
7965        // This is a helper for getting a:
7966        // - valid focus on an element,
7967        // - that isn't a part of the panes and panels system of the Workspace,
7968        // - and doesn't trigger the 'on_focus_lost' API.
7969        let focus_other_view = {
7970            let workspace = workspace.clone();
7971            move |cx: &mut VisualTestContext| {
7972                workspace.update_in(cx, |workspace, window, cx| {
7973                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7974                        workspace.toggle_modal(window, cx, TestModal::new);
7975                        workspace.toggle_modal(window, cx, TestModal::new);
7976                    } else {
7977                        workspace.toggle_modal(window, cx, TestModal::new);
7978                    }
7979                })
7980            }
7981        };
7982
7983        // If focus is transferred to another view that's not a panel or another pane, we still show
7984        // the panel as zoomed.
7985        focus_other_view(cx);
7986        workspace.update(cx, |workspace, _| {
7987            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7988            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7989        });
7990
7991        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7992        workspace.update_in(cx, |_workspace, window, cx| {
7993            cx.focus_self(window);
7994        });
7995        workspace.update(cx, |workspace, _| {
7996            assert_eq!(workspace.zoomed, None);
7997            assert_eq!(workspace.zoomed_position, None);
7998        });
7999
8000        // If focus is transferred again to another view that's not a panel or a pane, we won't
8001        // show the panel as zoomed because it wasn't zoomed before.
8002        focus_other_view(cx);
8003        workspace.update(cx, |workspace, _| {
8004            assert_eq!(workspace.zoomed, None);
8005            assert_eq!(workspace.zoomed_position, None);
8006        });
8007
8008        // When the panel is activated, it is zoomed again.
8009        cx.dispatch_action(ToggleRightDock);
8010        workspace.update(cx, |workspace, _| {
8011            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8012            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8013        });
8014
8015        // Emitting a ZoomOut event unzooms the panel.
8016        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8017        workspace.update(cx, |workspace, _| {
8018            assert_eq!(workspace.zoomed, None);
8019            assert_eq!(workspace.zoomed_position, None);
8020        });
8021
8022        // Emit closed event on panel 1, which is active
8023        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8024
8025        // Now the left dock is closed, because panel_1 was the active panel
8026        workspace.update(cx, |workspace, cx| {
8027            let right_dock = workspace.right_dock();
8028            assert!(!right_dock.read(cx).is_open());
8029        });
8030    }
8031
8032    #[gpui::test]
8033    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8034        init_test(cx);
8035
8036        let fs = FakeFs::new(cx.background_executor.clone());
8037        let project = Project::test(fs, [], cx).await;
8038        let (workspace, cx) =
8039            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8040        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8041
8042        let dirty_regular_buffer = cx.new(|cx| {
8043            TestItem::new(cx)
8044                .with_dirty(true)
8045                .with_label("1.txt")
8046                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8047        });
8048        let dirty_regular_buffer_2 = cx.new(|cx| {
8049            TestItem::new(cx)
8050                .with_dirty(true)
8051                .with_label("2.txt")
8052                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8053        });
8054        let dirty_multi_buffer_with_both = cx.new(|cx| {
8055            TestItem::new(cx)
8056                .with_dirty(true)
8057                .with_singleton(false)
8058                .with_label("Fake Project Search")
8059                .with_project_items(&[
8060                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8061                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8062                ])
8063        });
8064        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8065        workspace.update_in(cx, |workspace, window, cx| {
8066            workspace.add_item(
8067                pane.clone(),
8068                Box::new(dirty_regular_buffer.clone()),
8069                None,
8070                false,
8071                false,
8072                window,
8073                cx,
8074            );
8075            workspace.add_item(
8076                pane.clone(),
8077                Box::new(dirty_regular_buffer_2.clone()),
8078                None,
8079                false,
8080                false,
8081                window,
8082                cx,
8083            );
8084            workspace.add_item(
8085                pane.clone(),
8086                Box::new(dirty_multi_buffer_with_both.clone()),
8087                None,
8088                false,
8089                false,
8090                window,
8091                cx,
8092            );
8093        });
8094
8095        pane.update_in(cx, |pane, window, cx| {
8096            pane.activate_item(2, true, true, window, cx);
8097            assert_eq!(
8098                pane.active_item().unwrap().item_id(),
8099                multi_buffer_with_both_files_id,
8100                "Should select the multi buffer in the pane"
8101            );
8102        });
8103        let close_all_but_multi_buffer_task = pane
8104            .update_in(cx, |pane, window, cx| {
8105                pane.close_inactive_items(
8106                    &CloseInactiveItems {
8107                        save_intent: Some(SaveIntent::Save),
8108                        close_pinned: true,
8109                    },
8110                    window,
8111                    cx,
8112                )
8113            })
8114            .expect("should have inactive files to close");
8115        cx.background_executor.run_until_parked();
8116        assert!(
8117            !cx.has_pending_prompt(),
8118            "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
8119        );
8120        close_all_but_multi_buffer_task
8121            .await
8122            .expect("Closing all buffers but the multi buffer failed");
8123        pane.update(cx, |pane, cx| {
8124            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8125            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8126            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8127            assert_eq!(pane.items_len(), 1);
8128            assert_eq!(
8129                pane.active_item().unwrap().item_id(),
8130                multi_buffer_with_both_files_id,
8131                "Should have only the multi buffer left in the pane"
8132            );
8133            assert!(
8134                dirty_multi_buffer_with_both.read(cx).is_dirty,
8135                "The multi buffer containing the unsaved buffer should still be dirty"
8136            );
8137        });
8138
8139        let close_multi_buffer_task = pane
8140            .update_in(cx, |pane, window, cx| {
8141                pane.close_active_item(
8142                    &CloseActiveItem {
8143                        save_intent: Some(SaveIntent::Close),
8144                    },
8145                    window,
8146                    cx,
8147                )
8148            })
8149            .expect("should have the multi buffer to close");
8150        cx.background_executor.run_until_parked();
8151        assert!(
8152            cx.has_pending_prompt(),
8153            "Dirty multi buffer should prompt a save dialog"
8154        );
8155        cx.simulate_prompt_answer(0);
8156        cx.background_executor.run_until_parked();
8157        close_multi_buffer_task
8158            .await
8159            .expect("Closing the multi buffer failed");
8160        pane.update(cx, |pane, cx| {
8161            assert_eq!(
8162                dirty_multi_buffer_with_both.read(cx).save_count,
8163                1,
8164                "Multi buffer item should get be saved"
8165            );
8166            // Test impl does not save inner items, so we do not assert them
8167            assert_eq!(
8168                pane.items_len(),
8169                0,
8170                "No more items should be left in the pane"
8171            );
8172            assert!(pane.active_item().is_none());
8173        });
8174    }
8175
8176    #[gpui::test]
8177    async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
8178        cx: &mut TestAppContext,
8179    ) {
8180        init_test(cx);
8181
8182        let fs = FakeFs::new(cx.background_executor.clone());
8183        let project = Project::test(fs, [], cx).await;
8184        let (workspace, cx) =
8185            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8186        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8187
8188        let dirty_regular_buffer = cx.new(|cx| {
8189            TestItem::new(cx)
8190                .with_dirty(true)
8191                .with_label("1.txt")
8192                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8193        });
8194        let dirty_regular_buffer_2 = cx.new(|cx| {
8195            TestItem::new(cx)
8196                .with_dirty(true)
8197                .with_label("2.txt")
8198                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8199        });
8200        let clear_regular_buffer = cx.new(|cx| {
8201            TestItem::new(cx)
8202                .with_label("3.txt")
8203                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8204        });
8205
8206        let dirty_multi_buffer_with_both = cx.new(|cx| {
8207            TestItem::new(cx)
8208                .with_dirty(true)
8209                .with_singleton(false)
8210                .with_label("Fake Project Search")
8211                .with_project_items(&[
8212                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8213                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8214                    clear_regular_buffer.read(cx).project_items[0].clone(),
8215                ])
8216        });
8217        workspace.update_in(cx, |workspace, window, cx| {
8218            workspace.add_item(
8219                pane.clone(),
8220                Box::new(dirty_regular_buffer.clone()),
8221                None,
8222                false,
8223                false,
8224                window,
8225                cx,
8226            );
8227            workspace.add_item(
8228                pane.clone(),
8229                Box::new(dirty_multi_buffer_with_both.clone()),
8230                None,
8231                false,
8232                false,
8233                window,
8234                cx,
8235            );
8236        });
8237
8238        pane.update_in(cx, |pane, window, cx| {
8239            pane.activate_item(0, true, true, window, cx);
8240            assert_eq!(
8241                pane.active_item().unwrap().item_id(),
8242                dirty_regular_buffer.item_id(),
8243                "Should select the dirty singleton buffer in the pane"
8244            );
8245        });
8246        let close_singleton_buffer_task = pane
8247            .update_in(cx, |pane, window, cx| {
8248                pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8249            })
8250            .expect("should have active singleton buffer to close");
8251        cx.background_executor.run_until_parked();
8252        assert!(
8253            !cx.has_pending_prompt(),
8254            "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
8255        );
8256
8257        close_singleton_buffer_task
8258            .await
8259            .expect("Should not fail closing the singleton buffer");
8260        pane.update(cx, |pane, cx| {
8261            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8262            assert_eq!(
8263                dirty_multi_buffer_with_both.read(cx).save_count,
8264                0,
8265                "Multi buffer itself should not be saved"
8266            );
8267            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8268            assert_eq!(
8269                pane.items_len(),
8270                1,
8271                "A dirty multi buffer should be present in the pane"
8272            );
8273            assert_eq!(
8274                pane.active_item().unwrap().item_id(),
8275                dirty_multi_buffer_with_both.item_id(),
8276                "Should activate the only remaining item in the pane"
8277            );
8278        });
8279    }
8280
8281    #[gpui::test]
8282    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8283        cx: &mut TestAppContext,
8284    ) {
8285        init_test(cx);
8286
8287        let fs = FakeFs::new(cx.background_executor.clone());
8288        let project = Project::test(fs, [], cx).await;
8289        let (workspace, cx) =
8290            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8291        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8292
8293        let dirty_regular_buffer = cx.new(|cx| {
8294            TestItem::new(cx)
8295                .with_dirty(true)
8296                .with_label("1.txt")
8297                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8298        });
8299        let dirty_regular_buffer_2 = cx.new(|cx| {
8300            TestItem::new(cx)
8301                .with_dirty(true)
8302                .with_label("2.txt")
8303                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8304        });
8305        let clear_regular_buffer = cx.new(|cx| {
8306            TestItem::new(cx)
8307                .with_label("3.txt")
8308                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8309        });
8310
8311        let dirty_multi_buffer_with_both = cx.new(|cx| {
8312            TestItem::new(cx)
8313                .with_dirty(true)
8314                .with_singleton(false)
8315                .with_label("Fake Project Search")
8316                .with_project_items(&[
8317                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8318                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8319                    clear_regular_buffer.read(cx).project_items[0].clone(),
8320                ])
8321        });
8322        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8323        workspace.update_in(cx, |workspace, window, cx| {
8324            workspace.add_item(
8325                pane.clone(),
8326                Box::new(dirty_regular_buffer.clone()),
8327                None,
8328                false,
8329                false,
8330                window,
8331                cx,
8332            );
8333            workspace.add_item(
8334                pane.clone(),
8335                Box::new(dirty_multi_buffer_with_both.clone()),
8336                None,
8337                false,
8338                false,
8339                window,
8340                cx,
8341            );
8342        });
8343
8344        pane.update_in(cx, |pane, window, cx| {
8345            pane.activate_item(1, true, true, window, cx);
8346            assert_eq!(
8347                pane.active_item().unwrap().item_id(),
8348                multi_buffer_with_both_files_id,
8349                "Should select the multi buffer in the pane"
8350            );
8351        });
8352        let _close_multi_buffer_task = pane
8353            .update_in(cx, |pane, window, cx| {
8354                pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8355            })
8356            .expect("should have active multi buffer to close");
8357        cx.background_executor.run_until_parked();
8358        assert!(
8359            cx.has_pending_prompt(),
8360            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8361        );
8362    }
8363
8364    #[gpui::test]
8365    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8366        cx: &mut TestAppContext,
8367    ) {
8368        init_test(cx);
8369
8370        let fs = FakeFs::new(cx.background_executor.clone());
8371        let project = Project::test(fs, [], cx).await;
8372        let (workspace, cx) =
8373            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8374        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8375
8376        let dirty_regular_buffer = cx.new(|cx| {
8377            TestItem::new(cx)
8378                .with_dirty(true)
8379                .with_label("1.txt")
8380                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8381        });
8382        let dirty_regular_buffer_2 = cx.new(|cx| {
8383            TestItem::new(cx)
8384                .with_dirty(true)
8385                .with_label("2.txt")
8386                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8387        });
8388        let clear_regular_buffer = cx.new(|cx| {
8389            TestItem::new(cx)
8390                .with_label("3.txt")
8391                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8392        });
8393
8394        let dirty_multi_buffer = cx.new(|cx| {
8395            TestItem::new(cx)
8396                .with_dirty(true)
8397                .with_singleton(false)
8398                .with_label("Fake Project Search")
8399                .with_project_items(&[
8400                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8401                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8402                    clear_regular_buffer.read(cx).project_items[0].clone(),
8403                ])
8404        });
8405        workspace.update_in(cx, |workspace, window, cx| {
8406            workspace.add_item(
8407                pane.clone(),
8408                Box::new(dirty_regular_buffer.clone()),
8409                None,
8410                false,
8411                false,
8412                window,
8413                cx,
8414            );
8415            workspace.add_item(
8416                pane.clone(),
8417                Box::new(dirty_regular_buffer_2.clone()),
8418                None,
8419                false,
8420                false,
8421                window,
8422                cx,
8423            );
8424            workspace.add_item(
8425                pane.clone(),
8426                Box::new(dirty_multi_buffer.clone()),
8427                None,
8428                false,
8429                false,
8430                window,
8431                cx,
8432            );
8433        });
8434
8435        pane.update_in(cx, |pane, window, cx| {
8436            pane.activate_item(2, true, true, window, cx);
8437            assert_eq!(
8438                pane.active_item().unwrap().item_id(),
8439                dirty_multi_buffer.item_id(),
8440                "Should select the multi buffer in the pane"
8441            );
8442        });
8443        let close_multi_buffer_task = pane
8444            .update_in(cx, |pane, window, cx| {
8445                pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8446            })
8447            .expect("should have active multi buffer to close");
8448        cx.background_executor.run_until_parked();
8449        assert!(
8450            !cx.has_pending_prompt(),
8451            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8452        );
8453        close_multi_buffer_task
8454            .await
8455            .expect("Closing multi buffer failed");
8456        pane.update(cx, |pane, cx| {
8457            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8458            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8459            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8460            assert_eq!(
8461                pane.items()
8462                    .map(|item| item.item_id())
8463                    .sorted()
8464                    .collect::<Vec<_>>(),
8465                vec![
8466                    dirty_regular_buffer.item_id(),
8467                    dirty_regular_buffer_2.item_id(),
8468                ],
8469                "Should have no multi buffer left in the pane"
8470            );
8471            assert!(dirty_regular_buffer.read(cx).is_dirty);
8472            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8473        });
8474    }
8475
8476    #[gpui::test]
8477    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8478        init_test(cx);
8479        let fs = FakeFs::new(cx.executor());
8480        let project = Project::test(fs, [], cx).await;
8481        let (workspace, cx) =
8482            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8483
8484        // Add a new panel to the right dock, opening the dock and setting the
8485        // focus to the new panel.
8486        let panel = workspace.update_in(cx, |workspace, window, cx| {
8487            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8488            workspace.add_panel(panel.clone(), window, cx);
8489
8490            workspace
8491                .right_dock()
8492                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8493
8494            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8495
8496            panel
8497        });
8498
8499        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8500        // panel to the next valid position which, in this case, is the left
8501        // dock.
8502        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8503        workspace.update(cx, |workspace, cx| {
8504            assert!(workspace.left_dock().read(cx).is_open());
8505            assert_eq!(panel.read(cx).position, DockPosition::Left);
8506        });
8507
8508        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8509        // panel to the next valid position which, in this case, is the bottom
8510        // dock.
8511        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8512        workspace.update(cx, |workspace, cx| {
8513            assert!(workspace.bottom_dock().read(cx).is_open());
8514            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8515        });
8516
8517        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8518        // around moving the panel to its initial position, the right dock.
8519        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8520        workspace.update(cx, |workspace, cx| {
8521            assert!(workspace.right_dock().read(cx).is_open());
8522            assert_eq!(panel.read(cx).position, DockPosition::Right);
8523        });
8524
8525        // Remove focus from the panel, ensuring that, if the panel is not
8526        // focused, the `MoveFocusedPanelToNextPosition` action does not update
8527        // the panel's position, so the panel is still in the right dock.
8528        workspace.update_in(cx, |workspace, window, cx| {
8529            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8530        });
8531
8532        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8533        workspace.update(cx, |workspace, cx| {
8534            assert!(workspace.right_dock().read(cx).is_open());
8535            assert_eq!(panel.read(cx).position, DockPosition::Right);
8536        });
8537    }
8538
8539    mod register_project_item_tests {
8540
8541        use super::*;
8542
8543        // View
8544        struct TestPngItemView {
8545            focus_handle: FocusHandle,
8546        }
8547        // Model
8548        struct TestPngItem {}
8549
8550        impl project::ProjectItem for TestPngItem {
8551            fn try_open(
8552                _project: &Entity<Project>,
8553                path: &ProjectPath,
8554                cx: &mut App,
8555            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8556                if path.path.extension().unwrap() == "png" {
8557                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8558                } else {
8559                    None
8560                }
8561            }
8562
8563            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8564                None
8565            }
8566
8567            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8568                None
8569            }
8570
8571            fn is_dirty(&self) -> bool {
8572                false
8573            }
8574        }
8575
8576        impl Item for TestPngItemView {
8577            type Event = ();
8578        }
8579        impl EventEmitter<()> for TestPngItemView {}
8580        impl Focusable for TestPngItemView {
8581            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8582                self.focus_handle.clone()
8583            }
8584        }
8585
8586        impl Render for TestPngItemView {
8587            fn render(
8588                &mut self,
8589                _window: &mut Window,
8590                _cx: &mut Context<Self>,
8591            ) -> impl IntoElement {
8592                Empty
8593            }
8594        }
8595
8596        impl ProjectItem for TestPngItemView {
8597            type Item = TestPngItem;
8598
8599            fn for_project_item(
8600                _project: Entity<Project>,
8601                _item: Entity<Self::Item>,
8602                _: &mut Window,
8603                cx: &mut Context<Self>,
8604            ) -> Self
8605            where
8606                Self: Sized,
8607            {
8608                Self {
8609                    focus_handle: cx.focus_handle(),
8610                }
8611            }
8612        }
8613
8614        // View
8615        struct TestIpynbItemView {
8616            focus_handle: FocusHandle,
8617        }
8618        // Model
8619        struct TestIpynbItem {}
8620
8621        impl project::ProjectItem for TestIpynbItem {
8622            fn try_open(
8623                _project: &Entity<Project>,
8624                path: &ProjectPath,
8625                cx: &mut App,
8626            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8627                if path.path.extension().unwrap() == "ipynb" {
8628                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8629                } else {
8630                    None
8631                }
8632            }
8633
8634            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8635                None
8636            }
8637
8638            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8639                None
8640            }
8641
8642            fn is_dirty(&self) -> bool {
8643                false
8644            }
8645        }
8646
8647        impl Item for TestIpynbItemView {
8648            type Event = ();
8649        }
8650        impl EventEmitter<()> for TestIpynbItemView {}
8651        impl Focusable for TestIpynbItemView {
8652            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8653                self.focus_handle.clone()
8654            }
8655        }
8656
8657        impl Render for TestIpynbItemView {
8658            fn render(
8659                &mut self,
8660                _window: &mut Window,
8661                _cx: &mut Context<Self>,
8662            ) -> impl IntoElement {
8663                Empty
8664            }
8665        }
8666
8667        impl ProjectItem for TestIpynbItemView {
8668            type Item = TestIpynbItem;
8669
8670            fn for_project_item(
8671                _project: Entity<Project>,
8672                _item: Entity<Self::Item>,
8673                _: &mut Window,
8674                cx: &mut Context<Self>,
8675            ) -> Self
8676            where
8677                Self: Sized,
8678            {
8679                Self {
8680                    focus_handle: cx.focus_handle(),
8681                }
8682            }
8683        }
8684
8685        struct TestAlternatePngItemView {
8686            focus_handle: FocusHandle,
8687        }
8688
8689        impl Item for TestAlternatePngItemView {
8690            type Event = ();
8691        }
8692
8693        impl EventEmitter<()> for TestAlternatePngItemView {}
8694        impl Focusable for TestAlternatePngItemView {
8695            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8696                self.focus_handle.clone()
8697            }
8698        }
8699
8700        impl Render for TestAlternatePngItemView {
8701            fn render(
8702                &mut self,
8703                _window: &mut Window,
8704                _cx: &mut Context<Self>,
8705            ) -> impl IntoElement {
8706                Empty
8707            }
8708        }
8709
8710        impl ProjectItem for TestAlternatePngItemView {
8711            type Item = TestPngItem;
8712
8713            fn for_project_item(
8714                _project: Entity<Project>,
8715                _item: Entity<Self::Item>,
8716                _: &mut Window,
8717                cx: &mut Context<Self>,
8718            ) -> Self
8719            where
8720                Self: Sized,
8721            {
8722                Self {
8723                    focus_handle: cx.focus_handle(),
8724                }
8725            }
8726        }
8727
8728        #[gpui::test]
8729        async fn test_register_project_item(cx: &mut TestAppContext) {
8730            init_test(cx);
8731
8732            cx.update(|cx| {
8733                register_project_item::<TestPngItemView>(cx);
8734                register_project_item::<TestIpynbItemView>(cx);
8735            });
8736
8737            let fs = FakeFs::new(cx.executor());
8738            fs.insert_tree(
8739                "/root1",
8740                json!({
8741                    "one.png": "BINARYDATAHERE",
8742                    "two.ipynb": "{ totally a notebook }",
8743                    "three.txt": "editing text, sure why not?"
8744                }),
8745            )
8746            .await;
8747
8748            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8749            let (workspace, cx) =
8750                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8751
8752            let worktree_id = project.update(cx, |project, cx| {
8753                project.worktrees(cx).next().unwrap().read(cx).id()
8754            });
8755
8756            let handle = workspace
8757                .update_in(cx, |workspace, window, cx| {
8758                    let project_path = (worktree_id, "one.png");
8759                    workspace.open_path(project_path, None, true, window, cx)
8760                })
8761                .await
8762                .unwrap();
8763
8764            // Now we can check if the handle we got back errored or not
8765            assert_eq!(
8766                handle.to_any().entity_type(),
8767                TypeId::of::<TestPngItemView>()
8768            );
8769
8770            let handle = workspace
8771                .update_in(cx, |workspace, window, cx| {
8772                    let project_path = (worktree_id, "two.ipynb");
8773                    workspace.open_path(project_path, None, true, window, cx)
8774                })
8775                .await
8776                .unwrap();
8777
8778            assert_eq!(
8779                handle.to_any().entity_type(),
8780                TypeId::of::<TestIpynbItemView>()
8781            );
8782
8783            let handle = workspace
8784                .update_in(cx, |workspace, window, cx| {
8785                    let project_path = (worktree_id, "three.txt");
8786                    workspace.open_path(project_path, None, true, window, cx)
8787                })
8788                .await;
8789            assert!(handle.is_err());
8790        }
8791
8792        #[gpui::test]
8793        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8794            init_test(cx);
8795
8796            cx.update(|cx| {
8797                register_project_item::<TestPngItemView>(cx);
8798                register_project_item::<TestAlternatePngItemView>(cx);
8799            });
8800
8801            let fs = FakeFs::new(cx.executor());
8802            fs.insert_tree(
8803                "/root1",
8804                json!({
8805                    "one.png": "BINARYDATAHERE",
8806                    "two.ipynb": "{ totally a notebook }",
8807                    "three.txt": "editing text, sure why not?"
8808                }),
8809            )
8810            .await;
8811            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8812            let (workspace, cx) =
8813                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8814            let worktree_id = project.update(cx, |project, cx| {
8815                project.worktrees(cx).next().unwrap().read(cx).id()
8816            });
8817
8818            let handle = workspace
8819                .update_in(cx, |workspace, window, cx| {
8820                    let project_path = (worktree_id, "one.png");
8821                    workspace.open_path(project_path, None, true, window, cx)
8822                })
8823                .await
8824                .unwrap();
8825
8826            // This _must_ be the second item registered
8827            assert_eq!(
8828                handle.to_any().entity_type(),
8829                TypeId::of::<TestAlternatePngItemView>()
8830            );
8831
8832            let handle = workspace
8833                .update_in(cx, |workspace, window, cx| {
8834                    let project_path = (worktree_id, "three.txt");
8835                    workspace.open_path(project_path, None, true, window, cx)
8836                })
8837                .await;
8838            assert!(handle.is_err());
8839        }
8840    }
8841
8842    pub fn init_test(cx: &mut TestAppContext) {
8843        cx.update(|cx| {
8844            let settings_store = SettingsStore::test(cx);
8845            cx.set_global(settings_store);
8846            theme::init(theme::LoadThemes::JustBase, cx);
8847            language::init(cx);
8848            crate::init_settings(cx);
8849            Project::init_settings(cx);
8850        });
8851    }
8852
8853    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8854        let item = TestProjectItem::new(id, path, cx);
8855        item.update(cx, |item, _| {
8856            item.is_dirty = true;
8857        });
8858        item
8859    }
8860}