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