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