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                                .with_click_message("File an issue")
5211                                .on_click(|_window, cx| cx.open_url(REPORT_ISSUE_URL))
5212                        })
5213                    },
5214                );
5215            }
5216        })
5217        .log_err();
5218}
5219
5220impl Focusable for Workspace {
5221    fn focus_handle(&self, cx: &App) -> FocusHandle {
5222        self.active_pane.focus_handle(cx)
5223    }
5224}
5225
5226#[derive(Clone)]
5227struct DraggedDock(DockPosition);
5228
5229impl Render for DraggedDock {
5230    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5231        gpui::Empty
5232    }
5233}
5234
5235impl Render for Workspace {
5236    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5237        let mut context = KeyContext::new_with_defaults();
5238        context.add("Workspace");
5239        context.set("keyboard_layout", cx.keyboard_layout().clone());
5240        let centered_layout = self.centered_layout
5241            && self.center.panes().len() == 1
5242            && self.active_item(cx).is_some();
5243        let render_padding = |size| {
5244            (size > 0.0).then(|| {
5245                div()
5246                    .h_full()
5247                    .w(relative(size))
5248                    .bg(cx.theme().colors().editor_background)
5249                    .border_color(cx.theme().colors().pane_group_border)
5250            })
5251        };
5252        let paddings = if centered_layout {
5253            let settings = WorkspaceSettings::get_global(cx).centered_layout;
5254            (
5255                render_padding(Self::adjust_padding(settings.left_padding)),
5256                render_padding(Self::adjust_padding(settings.right_padding)),
5257            )
5258        } else {
5259            (None, None)
5260        };
5261        let ui_font = theme::setup_ui_font(window, cx);
5262
5263        let theme = cx.theme().clone();
5264        let colors = theme.colors();
5265
5266        client_side_decorations(
5267            self.actions(div(), window, cx)
5268                .key_context(context)
5269                .relative()
5270                .size_full()
5271                .flex()
5272                .flex_col()
5273                .font(ui_font)
5274                .gap_0()
5275                .justify_start()
5276                .items_start()
5277                .text_color(colors.text)
5278                .overflow_hidden()
5279                .children(self.titlebar_item.clone())
5280                .child(
5281                    div()
5282                        .size_full()
5283                        .relative()
5284                        .flex_1()
5285                        .flex()
5286                        .flex_col()
5287                        .child(
5288                            div()
5289                                .id("workspace")
5290                                .bg(colors.background)
5291                                .relative()
5292                                .flex_1()
5293                                .w_full()
5294                                .flex()
5295                                .flex_col()
5296                                .overflow_hidden()
5297                                .border_t_1()
5298                                .border_b_1()
5299                                .border_color(colors.border)
5300                                .child({
5301                                    let this = cx.entity().clone();
5302                                    canvas(
5303                                        move |bounds, window, cx| {
5304                                            this.update(cx, |this, cx| {
5305                                                let bounds_changed = this.bounds != bounds;
5306                                                this.bounds = bounds;
5307
5308                                                if bounds_changed {
5309                                                    this.left_dock.update(cx, |dock, cx| {
5310                                                        dock.clamp_panel_size(
5311                                                            bounds.size.width,
5312                                                            window,
5313                                                            cx,
5314                                                        )
5315                                                    });
5316
5317                                                    this.right_dock.update(cx, |dock, cx| {
5318                                                        dock.clamp_panel_size(
5319                                                            bounds.size.width,
5320                                                            window,
5321                                                            cx,
5322                                                        )
5323                                                    });
5324
5325                                                    this.bottom_dock.update(cx, |dock, cx| {
5326                                                        dock.clamp_panel_size(
5327                                                            bounds.size.height,
5328                                                            window,
5329                                                            cx,
5330                                                        )
5331                                                    });
5332                                                }
5333                                            })
5334                                        },
5335                                        |_, _, _, _| {},
5336                                    )
5337                                    .absolute()
5338                                    .size_full()
5339                                })
5340                                .when(self.zoomed.is_none(), |this| {
5341                                    this.on_drag_move(cx.listener(
5342                                        move |workspace,
5343                                              e: &DragMoveEvent<DraggedDock>,
5344                                              window,
5345                                              cx| {
5346                                            if workspace.previous_dock_drag_coordinates
5347                                                != Some(e.event.position)
5348                                            {
5349                                                workspace.previous_dock_drag_coordinates =
5350                                                    Some(e.event.position);
5351                                                match e.drag(cx).0 {
5352                                                    DockPosition::Left => {
5353                                                        resize_left_dock(
5354                                                            e.event.position.x
5355                                                                - workspace.bounds.left(),
5356                                                            workspace,
5357                                                            window,
5358                                                            cx,
5359                                                        );
5360                                                    }
5361                                                    DockPosition::Right => {
5362                                                        resize_right_dock(
5363                                                            workspace.bounds.right()
5364                                                                - e.event.position.x,
5365                                                            workspace,
5366                                                            window,
5367                                                            cx,
5368                                                        );
5369                                                    }
5370                                                    DockPosition::Bottom => {
5371                                                        resize_bottom_dock(
5372                                                            workspace.bounds.bottom()
5373                                                                - e.event.position.y,
5374                                                            workspace,
5375                                                            window,
5376                                                            cx,
5377                                                        );
5378                                                    }
5379                                                };
5380                                                workspace.serialize_workspace(window, cx);
5381                                            }
5382                                        },
5383                                    ))
5384                                })
5385                                .child(
5386                                    div()
5387                                        .flex()
5388                                        .flex_row()
5389                                        .h_full()
5390                                        // Left Dock
5391                                        .children(self.render_dock(
5392                                            DockPosition::Left,
5393                                            &self.left_dock,
5394                                            window,
5395                                            cx,
5396                                        ))
5397                                        // Panes
5398                                        .child(
5399                                            div()
5400                                                .flex()
5401                                                .flex_col()
5402                                                .flex_1()
5403                                                .overflow_hidden()
5404                                                .child(
5405                                                    h_flex()
5406                                                        .flex_1()
5407                                                        .when_some(paddings.0, |this, p| {
5408                                                            this.child(p.border_r_1())
5409                                                        })
5410                                                        .child(self.center.render(
5411                                                            &self.project,
5412                                                            &self.follower_states,
5413                                                            self.active_call(),
5414                                                            &self.active_pane,
5415                                                            self.zoomed.as_ref(),
5416                                                            &self.app_state,
5417                                                            window,
5418                                                            cx,
5419                                                        ))
5420                                                        .when_some(paddings.1, |this, p| {
5421                                                            this.child(p.border_l_1())
5422                                                        }),
5423                                                )
5424                                                .children(self.render_dock(
5425                                                    DockPosition::Bottom,
5426                                                    &self.bottom_dock,
5427                                                    window,
5428                                                    cx,
5429                                                )),
5430                                        )
5431                                        // Right Dock
5432                                        .children(self.render_dock(
5433                                            DockPosition::Right,
5434                                            &self.right_dock,
5435                                            window,
5436                                            cx,
5437                                        )),
5438                                )
5439                                .children(self.zoomed.as_ref().and_then(|view| {
5440                                    let zoomed_view = view.upgrade()?;
5441                                    let div = div()
5442                                        .occlude()
5443                                        .absolute()
5444                                        .overflow_hidden()
5445                                        .border_color(colors.border)
5446                                        .bg(colors.background)
5447                                        .child(zoomed_view)
5448                                        .inset_0()
5449                                        .shadow_lg();
5450
5451                                    Some(match self.zoomed_position {
5452                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
5453                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
5454                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5455                                        None => {
5456                                            div.top_2().bottom_2().left_2().right_2().border_1()
5457                                        }
5458                                    })
5459                                }))
5460                                .children(self.render_notifications(window, cx)),
5461                        )
5462                        .child(self.status_bar.clone())
5463                        .child(self.modal_layer.clone()),
5464                ),
5465            window,
5466            cx,
5467        )
5468    }
5469}
5470
5471fn resize_bottom_dock(
5472    new_size: Pixels,
5473    workspace: &mut Workspace,
5474    window: &mut Window,
5475    cx: &mut App,
5476) {
5477    let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5478    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5479        bottom_dock.resize_active_panel(Some(size), window, cx);
5480    });
5481}
5482
5483fn resize_right_dock(
5484    new_size: Pixels,
5485    workspace: &mut Workspace,
5486    window: &mut Window,
5487    cx: &mut App,
5488) {
5489    let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5490    workspace.right_dock.update(cx, |right_dock, cx| {
5491        right_dock.resize_active_panel(Some(size), window, cx);
5492    });
5493}
5494
5495fn resize_left_dock(
5496    new_size: Pixels,
5497    workspace: &mut Workspace,
5498    window: &mut Window,
5499    cx: &mut App,
5500) {
5501    let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5502
5503    workspace.left_dock.update(cx, |left_dock, cx| {
5504        left_dock.resize_active_panel(Some(size), window, cx);
5505    });
5506}
5507
5508impl WorkspaceStore {
5509    pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
5510        Self {
5511            workspaces: Default::default(),
5512            _subscriptions: vec![
5513                client.add_request_handler(cx.weak_entity(), Self::handle_follow),
5514                client.add_message_handler(cx.weak_entity(), Self::handle_update_followers),
5515            ],
5516            client,
5517        }
5518    }
5519
5520    pub fn update_followers(
5521        &self,
5522        project_id: Option<u64>,
5523        update: proto::update_followers::Variant,
5524        cx: &App,
5525    ) -> Option<()> {
5526        let active_call = ActiveCall::try_global(cx)?;
5527        let room_id = active_call.read(cx).room()?.read(cx).id();
5528        self.client
5529            .send(proto::UpdateFollowers {
5530                room_id,
5531                project_id,
5532                variant: Some(update),
5533            })
5534            .log_err()
5535    }
5536
5537    pub async fn handle_follow(
5538        this: Entity<Self>,
5539        envelope: TypedEnvelope<proto::Follow>,
5540        mut cx: AsyncApp,
5541    ) -> Result<proto::FollowResponse> {
5542        this.update(&mut cx, |this, cx| {
5543            let follower = Follower {
5544                project_id: envelope.payload.project_id,
5545                peer_id: envelope.original_sender_id()?,
5546            };
5547
5548            let mut response = proto::FollowResponse::default();
5549            this.workspaces.retain(|workspace| {
5550                workspace
5551                    .update(cx, |workspace, window, cx| {
5552                        let handler_response =
5553                            workspace.handle_follow(follower.project_id, window, cx);
5554                        if let Some(active_view) = handler_response.active_view.clone() {
5555                            if workspace.project.read(cx).remote_id() == follower.project_id {
5556                                response.active_view = Some(active_view)
5557                            }
5558                        }
5559                    })
5560                    .is_ok()
5561            });
5562
5563            Ok(response)
5564        })?
5565    }
5566
5567    async fn handle_update_followers(
5568        this: Entity<Self>,
5569        envelope: TypedEnvelope<proto::UpdateFollowers>,
5570        mut cx: AsyncApp,
5571    ) -> Result<()> {
5572        let leader_id = envelope.original_sender_id()?;
5573        let update = envelope.payload;
5574
5575        this.update(&mut cx, |this, cx| {
5576            this.workspaces.retain(|workspace| {
5577                workspace
5578                    .update(cx, |workspace, window, cx| {
5579                        let project_id = workspace.project.read(cx).remote_id();
5580                        if update.project_id != project_id && update.project_id.is_some() {
5581                            return;
5582                        }
5583                        workspace.handle_update_followers(leader_id, update.clone(), window, cx);
5584                    })
5585                    .is_ok()
5586            });
5587            Ok(())
5588        })?
5589    }
5590}
5591
5592impl ViewId {
5593    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5594        Ok(Self {
5595            creator: message
5596                .creator
5597                .ok_or_else(|| anyhow!("creator is missing"))?,
5598            id: message.id,
5599        })
5600    }
5601
5602    pub(crate) fn to_proto(self) -> proto::ViewId {
5603        proto::ViewId {
5604            creator: Some(self.creator),
5605            id: self.id,
5606        }
5607    }
5608}
5609
5610impl FollowerState {
5611    fn pane(&self) -> &Entity<Pane> {
5612        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5613    }
5614}
5615
5616pub trait WorkspaceHandle {
5617    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath>;
5618}
5619
5620impl WorkspaceHandle for Entity<Workspace> {
5621    fn file_project_paths(&self, cx: &App) -> Vec<ProjectPath> {
5622        self.read(cx)
5623            .worktrees(cx)
5624            .flat_map(|worktree| {
5625                let worktree_id = worktree.read(cx).id();
5626                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5627                    worktree_id,
5628                    path: f.path.clone(),
5629                })
5630            })
5631            .collect::<Vec<_>>()
5632    }
5633}
5634
5635impl std::fmt::Debug for OpenPaths {
5636    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5637        f.debug_struct("OpenPaths")
5638            .field("paths", &self.paths)
5639            .finish()
5640    }
5641}
5642
5643pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5644    DB.last_workspace().await.log_err().flatten()
5645}
5646
5647pub fn last_session_workspace_locations(
5648    last_session_id: &str,
5649    last_session_window_stack: Option<Vec<WindowId>>,
5650) -> Option<Vec<SerializedWorkspaceLocation>> {
5651    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5652        .log_err()
5653}
5654
5655actions!(collab, [OpenChannelNotes]);
5656actions!(zed, [OpenLog]);
5657
5658async fn join_channel_internal(
5659    channel_id: ChannelId,
5660    app_state: &Arc<AppState>,
5661    requesting_window: Option<WindowHandle<Workspace>>,
5662    active_call: &Entity<ActiveCall>,
5663    cx: &mut AsyncApp,
5664) -> Result<bool> {
5665    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5666        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5667            return (false, None);
5668        };
5669
5670        let already_in_channel = room.channel_id() == Some(channel_id);
5671        let should_prompt = room.is_sharing_project()
5672            && !room.remote_participants().is_empty()
5673            && !already_in_channel;
5674        let open_room = if already_in_channel {
5675            active_call.room().cloned()
5676        } else {
5677            None
5678        };
5679        (should_prompt, open_room)
5680    })?;
5681
5682    if let Some(room) = open_room {
5683        let task = room.update(cx, |room, cx| {
5684            if let Some((project, host)) = room.most_active_project(cx) {
5685                return Some(join_in_room_project(project, host, app_state.clone(), cx));
5686            }
5687
5688            None
5689        })?;
5690        if let Some(task) = task {
5691            task.await?;
5692        }
5693        return anyhow::Ok(true);
5694    }
5695
5696    if should_prompt {
5697        if let Some(workspace) = requesting_window {
5698            let answer = workspace
5699                .update(cx, |_, window, cx| {
5700                    window.prompt(
5701                        PromptLevel::Warning,
5702                        "Do you want to switch channels?",
5703                        Some("Leaving this call will unshare your current project."),
5704                        &["Yes, Join Channel", "Cancel"],
5705                        cx,
5706                    )
5707                })?
5708                .await;
5709
5710            if answer == Ok(1) {
5711                return Ok(false);
5712            }
5713        } else {
5714            return Ok(false); // unreachable!() hopefully
5715        }
5716    }
5717
5718    let client = cx.update(|cx| active_call.read(cx).client())?;
5719
5720    let mut client_status = client.status();
5721
5722    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5723    'outer: loop {
5724        let Some(status) = client_status.recv().await else {
5725            return Err(anyhow!("error connecting"));
5726        };
5727
5728        match status {
5729            Status::Connecting
5730            | Status::Authenticating
5731            | Status::Reconnecting
5732            | Status::Reauthenticating => continue,
5733            Status::Connected { .. } => break 'outer,
5734            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5735            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5736            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5737                return Err(ErrorCode::Disconnected.into());
5738            }
5739        }
5740    }
5741
5742    let room = active_call
5743        .update(cx, |active_call, cx| {
5744            active_call.join_channel(channel_id, cx)
5745        })?
5746        .await?;
5747
5748    let Some(room) = room else {
5749        return anyhow::Ok(true);
5750    };
5751
5752    room.update(cx, |room, _| room.room_update_completed())?
5753        .await;
5754
5755    let task = room.update(cx, |room, cx| {
5756        if let Some((project, host)) = room.most_active_project(cx) {
5757            return Some(join_in_room_project(project, host, app_state.clone(), cx));
5758        }
5759
5760        // If you are the first to join a channel, see if you should share your project.
5761        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5762            if let Some(workspace) = requesting_window {
5763                let project = workspace.update(cx, |workspace, _, cx| {
5764                    let project = workspace.project.read(cx);
5765
5766                    if !CallSettings::get_global(cx).share_on_join {
5767                        return None;
5768                    }
5769
5770                    if (project.is_local() || project.is_via_ssh())
5771                        && project.visible_worktrees(cx).any(|tree| {
5772                            tree.read(cx)
5773                                .root_entry()
5774                                .map_or(false, |entry| entry.is_dir())
5775                        })
5776                    {
5777                        Some(workspace.project.clone())
5778                    } else {
5779                        None
5780                    }
5781                });
5782                if let Ok(Some(project)) = project {
5783                    return Some(cx.spawn(|room, mut cx| async move {
5784                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5785                            .await?;
5786                        Ok(())
5787                    }));
5788                }
5789            }
5790        }
5791
5792        None
5793    })?;
5794    if let Some(task) = task {
5795        task.await?;
5796        return anyhow::Ok(true);
5797    }
5798    anyhow::Ok(false)
5799}
5800
5801pub fn join_channel(
5802    channel_id: ChannelId,
5803    app_state: Arc<AppState>,
5804    requesting_window: Option<WindowHandle<Workspace>>,
5805    cx: &mut App,
5806) -> Task<Result<()>> {
5807    let active_call = ActiveCall::global(cx);
5808    cx.spawn(|mut cx| async move {
5809        let result = join_channel_internal(
5810            channel_id,
5811            &app_state,
5812            requesting_window,
5813            &active_call,
5814            &mut cx,
5815        )
5816            .await;
5817
5818        // join channel succeeded, and opened a window
5819        if matches!(result, Ok(true)) {
5820            return anyhow::Ok(());
5821        }
5822
5823        // find an existing workspace to focus and show call controls
5824        let mut active_window =
5825            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5826        if active_window.is_none() {
5827            // no open workspaces, make one to show the error in (blergh)
5828            let (window_handle, _) = cx
5829                .update(|cx| {
5830                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5831                })?
5832                .await?;
5833
5834            if result.is_ok() {
5835                cx.update(|cx| {
5836                    cx.dispatch_action(&OpenChannelNotes);
5837                }).log_err();
5838            }
5839
5840            active_window = Some(window_handle);
5841        }
5842
5843        if let Err(err) = result {
5844            log::error!("failed to join channel: {}", err);
5845            if let Some(active_window) = active_window {
5846                active_window
5847                    .update(&mut cx, |_, window, cx| {
5848                        let detail: SharedString = match err.error_code() {
5849                            ErrorCode::SignedOut => {
5850                                "Please sign in to continue.".into()
5851                            }
5852                            ErrorCode::UpgradeRequired => {
5853                                "Your are running an unsupported version of Zed. Please update to continue.".into()
5854                            }
5855                            ErrorCode::NoSuchChannel => {
5856                                "No matching channel was found. Please check the link and try again.".into()
5857                            }
5858                            ErrorCode::Forbidden => {
5859                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5860                            }
5861                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5862                            _ => format!("{}\n\nPlease try again.", err).into(),
5863                        };
5864                        window.prompt(
5865                            PromptLevel::Critical,
5866                            "Failed to join channel",
5867                            Some(&detail),
5868                            &["Ok"],
5869                        cx)
5870                    })?
5871                    .await
5872                    .ok();
5873            }
5874        }
5875
5876        // return ok, we showed the error to the user.
5877        anyhow::Ok(())
5878    })
5879}
5880
5881pub async fn get_any_active_workspace(
5882    app_state: Arc<AppState>,
5883    mut cx: AsyncApp,
5884) -> anyhow::Result<WindowHandle<Workspace>> {
5885    // find an existing workspace to focus and show call controls
5886    let active_window = activate_any_workspace_window(&mut cx);
5887    if active_window.is_none() {
5888        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5889            .await?;
5890    }
5891    activate_any_workspace_window(&mut cx).context("could not open zed")
5892}
5893
5894fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Workspace>> {
5895    cx.update(|cx| {
5896        if let Some(workspace_window) = cx
5897            .active_window()
5898            .and_then(|window| window.downcast::<Workspace>())
5899        {
5900            return Some(workspace_window);
5901        }
5902
5903        for window in cx.windows() {
5904            if let Some(workspace_window) = window.downcast::<Workspace>() {
5905                workspace_window
5906                    .update(cx, |_, window, _| window.activate_window())
5907                    .ok();
5908                return Some(workspace_window);
5909            }
5910        }
5911        None
5912    })
5913    .ok()
5914    .flatten()
5915}
5916
5917pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<Workspace>> {
5918    cx.windows()
5919        .into_iter()
5920        .filter_map(|window| window.downcast::<Workspace>())
5921        .filter(|workspace| {
5922            workspace
5923                .read(cx)
5924                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5925        })
5926        .collect()
5927}
5928
5929#[derive(Default)]
5930pub struct OpenOptions {
5931    pub open_new_workspace: Option<bool>,
5932    pub replace_window: Option<WindowHandle<Workspace>>,
5933    pub env: Option<HashMap<String, String>>,
5934}
5935
5936#[allow(clippy::type_complexity)]
5937pub fn open_paths(
5938    abs_paths: &[PathBuf],
5939    app_state: Arc<AppState>,
5940    open_options: OpenOptions,
5941    cx: &mut App,
5942) -> Task<
5943    anyhow::Result<(
5944        WindowHandle<Workspace>,
5945        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5946    )>,
5947> {
5948    let abs_paths = abs_paths.to_vec();
5949    let mut existing = None;
5950    let mut best_match = None;
5951    let mut open_visible = OpenVisible::All;
5952
5953    if open_options.open_new_workspace != Some(true) {
5954        for window in local_workspace_windows(cx) {
5955            if let Ok(workspace) = window.read(cx) {
5956                let m = workspace
5957                    .project
5958                    .read(cx)
5959                    .visibility_for_paths(&abs_paths, cx);
5960                if m > best_match {
5961                    existing = Some(window);
5962                    best_match = m;
5963                } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5964                    existing = Some(window)
5965                }
5966            }
5967        }
5968    }
5969
5970    cx.spawn(move |mut cx| async move {
5971        if open_options.open_new_workspace.is_none() && existing.is_none() {
5972            let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5973            if futures::future::join_all(all_files)
5974                .await
5975                .into_iter()
5976                .filter_map(|result| result.ok().flatten())
5977                .all(|file| !file.is_dir)
5978            {
5979                cx.update(|cx| {
5980                    if let Some(window) = cx
5981                        .active_window()
5982                        .and_then(|window| window.downcast::<Workspace>())
5983                    {
5984                        if let Ok(workspace) = window.read(cx) {
5985                            let project = workspace.project().read(cx);
5986                            if project.is_local() && !project.is_via_collab() {
5987                                existing = Some(window);
5988                                open_visible = OpenVisible::None;
5989                                return;
5990                            }
5991                        }
5992                    }
5993                    for window in local_workspace_windows(cx) {
5994                        if let Ok(workspace) = window.read(cx) {
5995                            let project = workspace.project().read(cx);
5996                            if project.is_via_collab() {
5997                                continue;
5998                            }
5999                            existing = Some(window);
6000                            open_visible = OpenVisible::None;
6001                            break;
6002                        }
6003                    }
6004                })?;
6005            }
6006        }
6007
6008        if let Some(existing) = existing {
6009            let open_task = existing
6010                .update(&mut cx, |workspace, window, cx| {
6011                    window.activate_window();
6012                    workspace.open_paths(abs_paths, open_visible, None, window, cx)
6013                })?
6014                .await;
6015
6016            _ = existing.update(&mut cx, |workspace, _, cx| {
6017                for item in open_task.iter().flatten() {
6018                    if let Err(e) = item {
6019                        workspace.show_error(&e, cx);
6020                    }
6021                }
6022            });
6023
6024            Ok((existing, open_task))
6025        } else {
6026            cx.update(move |cx| {
6027                Workspace::new_local(
6028                    abs_paths,
6029                    app_state.clone(),
6030                    open_options.replace_window,
6031                    open_options.env,
6032                    cx,
6033                )
6034            })?
6035            .await
6036        }
6037    })
6038}
6039
6040pub fn open_new(
6041    open_options: OpenOptions,
6042    app_state: Arc<AppState>,
6043    cx: &mut App,
6044    init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
6045) -> Task<anyhow::Result<()>> {
6046    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
6047    cx.spawn(|mut cx| async move {
6048        let (workspace, opened_paths) = task.await?;
6049        workspace.update(&mut cx, |workspace, window, cx| {
6050            if opened_paths.is_empty() {
6051                init(workspace, window, cx)
6052            }
6053        })?;
6054        Ok(())
6055    })
6056}
6057
6058pub fn create_and_open_local_file(
6059    path: &'static Path,
6060    window: &mut Window,
6061    cx: &mut Context<Workspace>,
6062    default_content: impl 'static + Send + FnOnce() -> Rope,
6063) -> Task<Result<Box<dyn ItemHandle>>> {
6064    cx.spawn_in(window, |workspace, mut cx| async move {
6065        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
6066        if !fs.is_file(path).await {
6067            fs.create_file(path, Default::default()).await?;
6068            fs.save(path, &default_content(), Default::default())
6069                .await?;
6070        }
6071
6072        let mut items = workspace
6073            .update_in(&mut cx, |workspace, window, cx| {
6074                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
6075                    workspace.open_paths(
6076                        vec![path.to_path_buf()],
6077                        OpenVisible::None,
6078                        None,
6079                        window,
6080                        cx,
6081                    )
6082                })
6083            })?
6084            .await?
6085            .await;
6086
6087        let item = items.pop().flatten();
6088        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
6089    })
6090}
6091
6092pub fn open_ssh_project(
6093    window: WindowHandle<Workspace>,
6094    connection_options: SshConnectionOptions,
6095    cancel_rx: oneshot::Receiver<()>,
6096    delegate: Arc<dyn SshClientDelegate>,
6097    app_state: Arc<AppState>,
6098    paths: Vec<PathBuf>,
6099    cx: &mut App,
6100) -> Task<Result<()>> {
6101    cx.spawn(|mut cx| async move {
6102        let (serialized_ssh_project, workspace_id, serialized_workspace) =
6103            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
6104
6105        let session = match cx
6106            .update(|cx| {
6107                remote::SshRemoteClient::new(
6108                    ConnectionIdentifier::Workspace(workspace_id.0),
6109                    connection_options,
6110                    cancel_rx,
6111                    delegate,
6112                    cx,
6113                )
6114            })?
6115            .await?
6116        {
6117            Some(result) => result,
6118            None => return Ok(()),
6119        };
6120
6121        let project = cx.update(|cx| {
6122            project::Project::ssh(
6123                session,
6124                app_state.client.clone(),
6125                app_state.node_runtime.clone(),
6126                app_state.user_store.clone(),
6127                app_state.languages.clone(),
6128                app_state.fs.clone(),
6129                cx,
6130            )
6131        })?;
6132
6133        let toolchains = DB.toolchains(workspace_id).await?;
6134        for (toolchain, worktree_id) in toolchains {
6135            project
6136                .update(&mut cx, |this, cx| {
6137                    this.activate_toolchain(worktree_id, toolchain, cx)
6138                })?
6139                .await;
6140        }
6141        let mut project_paths_to_open = vec![];
6142        let mut project_path_errors = vec![];
6143
6144        for path in paths {
6145            let result = cx
6146                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
6147                .await;
6148            match result {
6149                Ok((_, project_path)) => {
6150                    project_paths_to_open.push((path.clone(), Some(project_path)));
6151                }
6152                Err(error) => {
6153                    project_path_errors.push(error);
6154                }
6155            };
6156        }
6157
6158        if project_paths_to_open.is_empty() {
6159            return Err(project_path_errors
6160                .pop()
6161                .unwrap_or_else(|| anyhow!("no paths given")));
6162        }
6163
6164        cx.update_window(window.into(), |_, window, cx| {
6165            window.replace_root(cx, |window, cx| {
6166                telemetry::event!("SSH Project Opened");
6167
6168                let mut workspace =
6169                    Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
6170                workspace.set_serialized_ssh_project(serialized_ssh_project);
6171                workspace
6172            });
6173        })?;
6174
6175        window
6176            .update(&mut cx, |_, window, cx| {
6177                window.activate_window();
6178
6179                open_items(serialized_workspace, project_paths_to_open, window, cx)
6180            })?
6181            .await?;
6182
6183        window.update(&mut cx, |workspace, _, cx| {
6184            for error in project_path_errors {
6185                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
6186                    if let Some(path) = error.error_tag("path") {
6187                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
6188                    }
6189                } else {
6190                    workspace.show_error(&error, cx)
6191                }
6192            }
6193        })
6194    })
6195}
6196
6197fn serialize_ssh_project(
6198    connection_options: SshConnectionOptions,
6199    paths: Vec<PathBuf>,
6200    cx: &AsyncApp,
6201) -> Task<
6202    Result<(
6203        SerializedSshProject,
6204        WorkspaceId,
6205        Option<SerializedWorkspace>,
6206    )>,
6207> {
6208    cx.background_executor().spawn(async move {
6209        let serialized_ssh_project = persistence::DB
6210            .get_or_create_ssh_project(
6211                connection_options.host.clone(),
6212                connection_options.port,
6213                paths
6214                    .iter()
6215                    .map(|path| path.to_string_lossy().to_string())
6216                    .collect::<Vec<_>>(),
6217                connection_options.username.clone(),
6218            )
6219            .await?;
6220
6221        let serialized_workspace =
6222            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
6223
6224        let workspace_id = if let Some(workspace_id) =
6225            serialized_workspace.as_ref().map(|workspace| workspace.id)
6226        {
6227            workspace_id
6228        } else {
6229            persistence::DB.next_id().await?
6230        };
6231
6232        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
6233    })
6234}
6235
6236pub fn join_in_room_project(
6237    project_id: u64,
6238    follow_user_id: u64,
6239    app_state: Arc<AppState>,
6240    cx: &mut App,
6241) -> Task<Result<()>> {
6242    let windows = cx.windows();
6243    cx.spawn(|mut cx| async move {
6244        let existing_workspace = windows.into_iter().find_map(|window_handle| {
6245            window_handle
6246                .downcast::<Workspace>()
6247                .and_then(|window_handle| {
6248                    window_handle
6249                        .update(&mut cx, |workspace, _window, cx| {
6250                            if workspace.project().read(cx).remote_id() == Some(project_id) {
6251                                Some(window_handle)
6252                            } else {
6253                                None
6254                            }
6255                        })
6256                        .unwrap_or(None)
6257                })
6258        });
6259
6260        let workspace = if let Some(existing_workspace) = existing_workspace {
6261            existing_workspace
6262        } else {
6263            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
6264            let room = active_call
6265                .read_with(&cx, |call, _| call.room().cloned())?
6266                .ok_or_else(|| anyhow!("not in a call"))?;
6267            let project = room
6268                .update(&mut cx, |room, cx| {
6269                    room.join_project(
6270                        project_id,
6271                        app_state.languages.clone(),
6272                        app_state.fs.clone(),
6273                        cx,
6274                    )
6275                })?
6276                .await?;
6277
6278            let window_bounds_override = window_bounds_env_override();
6279            cx.update(|cx| {
6280                let mut options = (app_state.build_window_options)(None, cx);
6281                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
6282                cx.open_window(options, |window, cx| {
6283                    cx.new(|cx| {
6284                        Workspace::new(Default::default(), project, app_state.clone(), window, cx)
6285                    })
6286                })
6287            })??
6288        };
6289
6290        workspace.update(&mut cx, |workspace, window, cx| {
6291            cx.activate(true);
6292            window.activate_window();
6293
6294            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
6295                let follow_peer_id = room
6296                    .read(cx)
6297                    .remote_participants()
6298                    .iter()
6299                    .find(|(_, participant)| participant.user.id == follow_user_id)
6300                    .map(|(_, p)| p.peer_id)
6301                    .or_else(|| {
6302                        // If we couldn't follow the given user, follow the host instead.
6303                        let collaborator = workspace
6304                            .project()
6305                            .read(cx)
6306                            .collaborators()
6307                            .values()
6308                            .find(|collaborator| collaborator.is_host)?;
6309                        Some(collaborator.peer_id)
6310                    });
6311
6312                if let Some(follow_peer_id) = follow_peer_id {
6313                    workspace.follow(follow_peer_id, window, cx);
6314                }
6315            }
6316        })?;
6317
6318        anyhow::Ok(())
6319    })
6320}
6321
6322pub fn reload(reload: &Reload, cx: &mut App) {
6323    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
6324    let mut workspace_windows = cx
6325        .windows()
6326        .into_iter()
6327        .filter_map(|window| window.downcast::<Workspace>())
6328        .collect::<Vec<_>>();
6329
6330    // If multiple windows have unsaved changes, and need a save prompt,
6331    // prompt in the active window before switching to a different window.
6332    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
6333
6334    let mut prompt = None;
6335    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
6336        prompt = window
6337            .update(cx, |_, window, cx| {
6338                window.prompt(
6339                    PromptLevel::Info,
6340                    "Are you sure you want to restart?",
6341                    None,
6342                    &["Restart", "Cancel"],
6343                    cx,
6344                )
6345            })
6346            .ok();
6347    }
6348
6349    let binary_path = reload.binary_path.clone();
6350    cx.spawn(|mut cx| async move {
6351        if let Some(prompt) = prompt {
6352            let answer = prompt.await?;
6353            if answer != 0 {
6354                return Ok(());
6355            }
6356        }
6357
6358        // If the user cancels any save prompt, then keep the app open.
6359        for window in workspace_windows {
6360            if let Ok(should_close) = window.update(&mut cx, |workspace, window, cx| {
6361                workspace.prepare_to_close(CloseIntent::Quit, window, cx)
6362            }) {
6363                if !should_close.await? {
6364                    return Ok(());
6365                }
6366            }
6367        }
6368
6369        cx.update(|cx| cx.restart(binary_path))
6370    })
6371    .detach_and_log_err(cx);
6372}
6373
6374fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
6375    let mut parts = value.split(',');
6376    let x: usize = parts.next()?.parse().ok()?;
6377    let y: usize = parts.next()?.parse().ok()?;
6378    Some(point(px(x as f32), px(y as f32)))
6379}
6380
6381fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
6382    let mut parts = value.split(',');
6383    let width: usize = parts.next()?.parse().ok()?;
6384    let height: usize = parts.next()?.parse().ok()?;
6385    Some(size(px(width as f32), px(height as f32)))
6386}
6387
6388pub fn client_side_decorations(
6389    element: impl IntoElement,
6390    window: &mut Window,
6391    cx: &mut App,
6392) -> Stateful<Div> {
6393    const BORDER_SIZE: Pixels = px(1.0);
6394    let decorations = window.window_decorations();
6395
6396    if matches!(decorations, Decorations::Client { .. }) {
6397        window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
6398    }
6399
6400    struct GlobalResizeEdge(ResizeEdge);
6401    impl Global for GlobalResizeEdge {}
6402
6403    div()
6404        .id("window-backdrop")
6405        .bg(transparent_black())
6406        .map(|div| match decorations {
6407            Decorations::Server => div,
6408            Decorations::Client { tiling, .. } => div
6409                .when(!(tiling.top || tiling.right), |div| {
6410                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6411                })
6412                .when(!(tiling.top || tiling.left), |div| {
6413                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6414                })
6415                .when(!(tiling.bottom || tiling.right), |div| {
6416                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6417                })
6418                .when(!(tiling.bottom || tiling.left), |div| {
6419                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6420                })
6421                .when(!tiling.top, |div| {
6422                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
6423                })
6424                .when(!tiling.bottom, |div| {
6425                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
6426                })
6427                .when(!tiling.left, |div| {
6428                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
6429                })
6430                .when(!tiling.right, |div| {
6431                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
6432                })
6433                .on_mouse_move(move |e, window, cx| {
6434                    let size = window.window_bounds().get_bounds().size;
6435                    let pos = e.position;
6436
6437                    let new_edge =
6438                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
6439
6440                    let edge = cx.try_global::<GlobalResizeEdge>();
6441                    if new_edge != edge.map(|edge| edge.0) {
6442                        window
6443                            .window_handle()
6444                            .update(cx, |workspace, _, cx| {
6445                                cx.notify(workspace.entity_id());
6446                            })
6447                            .ok();
6448                    }
6449                })
6450                .on_mouse_down(MouseButton::Left, move |e, window, _| {
6451                    let size = window.window_bounds().get_bounds().size;
6452                    let pos = e.position;
6453
6454                    let edge = match resize_edge(
6455                        pos,
6456                        theme::CLIENT_SIDE_DECORATION_SHADOW,
6457                        size,
6458                        tiling,
6459                    ) {
6460                        Some(value) => value,
6461                        None => return,
6462                    };
6463
6464                    window.start_window_resize(edge);
6465                }),
6466        })
6467        .size_full()
6468        .child(
6469            div()
6470                .cursor(CursorStyle::Arrow)
6471                .map(|div| match decorations {
6472                    Decorations::Server => div,
6473                    Decorations::Client { tiling } => div
6474                        .border_color(cx.theme().colors().border)
6475                        .when(!(tiling.top || tiling.right), |div| {
6476                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6477                        })
6478                        .when(!(tiling.top || tiling.left), |div| {
6479                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6480                        })
6481                        .when(!(tiling.bottom || tiling.right), |div| {
6482                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6483                        })
6484                        .when(!(tiling.bottom || tiling.left), |div| {
6485                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6486                        })
6487                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6488                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6489                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6490                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6491                        .when(!tiling.is_tiled(), |div| {
6492                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
6493                                color: Hsla {
6494                                    h: 0.,
6495                                    s: 0.,
6496                                    l: 0.,
6497                                    a: 0.4,
6498                                },
6499                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6500                                spread_radius: px(0.),
6501                                offset: point(px(0.0), px(0.0)),
6502                            }])
6503                        }),
6504                })
6505                .on_mouse_move(|_e, _, cx| {
6506                    cx.stop_propagation();
6507                })
6508                .size_full()
6509                .child(element),
6510        )
6511        .map(|div| match decorations {
6512            Decorations::Server => div,
6513            Decorations::Client { tiling, .. } => div.child(
6514                canvas(
6515                    |_bounds, window, _| {
6516                        window.insert_hitbox(
6517                            Bounds::new(
6518                                point(px(0.0), px(0.0)),
6519                                window.window_bounds().get_bounds().size,
6520                            ),
6521                            false,
6522                        )
6523                    },
6524                    move |_bounds, hitbox, window, cx| {
6525                        let mouse = window.mouse_position();
6526                        let size = window.window_bounds().get_bounds().size;
6527                        let Some(edge) =
6528                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6529                        else {
6530                            return;
6531                        };
6532                        cx.set_global(GlobalResizeEdge(edge));
6533                        window.set_cursor_style(
6534                            match edge {
6535                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6536                                ResizeEdge::Left | ResizeEdge::Right => {
6537                                    CursorStyle::ResizeLeftRight
6538                                }
6539                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6540                                    CursorStyle::ResizeUpLeftDownRight
6541                                }
6542                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6543                                    CursorStyle::ResizeUpRightDownLeft
6544                                }
6545                            },
6546                            &hitbox,
6547                        );
6548                    },
6549                )
6550                .size_full()
6551                .absolute(),
6552            ),
6553        })
6554}
6555
6556fn resize_edge(
6557    pos: Point<Pixels>,
6558    shadow_size: Pixels,
6559    window_size: Size<Pixels>,
6560    tiling: Tiling,
6561) -> Option<ResizeEdge> {
6562    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6563    if bounds.contains(&pos) {
6564        return None;
6565    }
6566
6567    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6568    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6569    if !tiling.top && top_left_bounds.contains(&pos) {
6570        return Some(ResizeEdge::TopLeft);
6571    }
6572
6573    let top_right_bounds = Bounds::new(
6574        Point::new(window_size.width - corner_size.width, px(0.)),
6575        corner_size,
6576    );
6577    if !tiling.top && top_right_bounds.contains(&pos) {
6578        return Some(ResizeEdge::TopRight);
6579    }
6580
6581    let bottom_left_bounds = Bounds::new(
6582        Point::new(px(0.), window_size.height - corner_size.height),
6583        corner_size,
6584    );
6585    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6586        return Some(ResizeEdge::BottomLeft);
6587    }
6588
6589    let bottom_right_bounds = Bounds::new(
6590        Point::new(
6591            window_size.width - corner_size.width,
6592            window_size.height - corner_size.height,
6593        ),
6594        corner_size,
6595    );
6596    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6597        return Some(ResizeEdge::BottomRight);
6598    }
6599
6600    if !tiling.top && pos.y < shadow_size {
6601        Some(ResizeEdge::Top)
6602    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6603        Some(ResizeEdge::Bottom)
6604    } else if !tiling.left && pos.x < shadow_size {
6605        Some(ResizeEdge::Left)
6606    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6607        Some(ResizeEdge::Right)
6608    } else {
6609        None
6610    }
6611}
6612
6613fn join_pane_into_active(
6614    active_pane: &Entity<Pane>,
6615    pane: &Entity<Pane>,
6616    window: &mut Window,
6617    cx: &mut App,
6618) {
6619    if pane == active_pane {
6620        return;
6621    } else if pane.read(cx).items_len() == 0 {
6622        pane.update(cx, |_, cx| {
6623            cx.emit(pane::Event::Remove {
6624                focus_on_pane: None,
6625            });
6626        })
6627    } else {
6628        move_all_items(pane, active_pane, window, cx);
6629    }
6630}
6631
6632fn move_all_items(
6633    from_pane: &Entity<Pane>,
6634    to_pane: &Entity<Pane>,
6635    window: &mut Window,
6636    cx: &mut App,
6637) {
6638    let destination_is_different = from_pane != to_pane;
6639    let mut moved_items = 0;
6640    for (item_ix, item_handle) in from_pane
6641        .read(cx)
6642        .items()
6643        .enumerate()
6644        .map(|(ix, item)| (ix, item.clone()))
6645        .collect::<Vec<_>>()
6646    {
6647        let ix = item_ix - moved_items;
6648        if destination_is_different {
6649            // Close item from previous pane
6650            from_pane.update(cx, |source, cx| {
6651                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx);
6652            });
6653            moved_items += 1;
6654        }
6655
6656        // This automatically removes duplicate items in the pane
6657        to_pane.update(cx, |destination, cx| {
6658            destination.add_item(item_handle, true, true, None, window, cx);
6659            window.focus(&destination.focus_handle(cx))
6660        });
6661    }
6662}
6663
6664pub fn move_item(
6665    source: &Entity<Pane>,
6666    destination: &Entity<Pane>,
6667    item_id_to_move: EntityId,
6668    destination_index: usize,
6669    window: &mut Window,
6670    cx: &mut App,
6671) {
6672    let Some((item_ix, item_handle)) = source
6673        .read(cx)
6674        .items()
6675        .enumerate()
6676        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6677        .map(|(ix, item)| (ix, item.clone()))
6678    else {
6679        // Tab was closed during drag
6680        return;
6681    };
6682
6683    if source != destination {
6684        // Close item from previous pane
6685        source.update(cx, |source, cx| {
6686            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx);
6687        });
6688    }
6689
6690    // This automatically removes duplicate items in the pane
6691    destination.update(cx, |destination, cx| {
6692        destination.add_item(item_handle, true, true, Some(destination_index), window, cx);
6693        window.focus(&destination.focus_handle(cx))
6694    });
6695}
6696
6697pub fn move_active_item(
6698    source: &Entity<Pane>,
6699    destination: &Entity<Pane>,
6700    focus_destination: bool,
6701    close_if_empty: bool,
6702    window: &mut Window,
6703    cx: &mut App,
6704) {
6705    if source == destination {
6706        return;
6707    }
6708    let Some(active_item) = source.read(cx).active_item() else {
6709        return;
6710    };
6711    source.update(cx, |source_pane, cx| {
6712        let item_id = active_item.item_id();
6713        source_pane.remove_item(item_id, false, close_if_empty, window, cx);
6714        destination.update(cx, |target_pane, cx| {
6715            target_pane.add_item(
6716                active_item,
6717                focus_destination,
6718                focus_destination,
6719                Some(target_pane.items_len()),
6720                window,
6721                cx,
6722            );
6723        });
6724    });
6725}
6726
6727#[cfg(test)]
6728mod tests {
6729    use std::{cell::RefCell, rc::Rc};
6730
6731    use super::*;
6732    use crate::{
6733        dock::{test::TestPanel, PanelEvent},
6734        item::{
6735            test::{TestItem, TestProjectItem},
6736            ItemEvent,
6737        },
6738    };
6739    use fs::FakeFs;
6740    use gpui::{
6741        px, DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext,
6742        UpdateGlobal, VisualTestContext,
6743    };
6744    use project::{Project, ProjectEntryId};
6745    use serde_json::json;
6746    use settings::SettingsStore;
6747
6748    #[gpui::test]
6749    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6750        init_test(cx);
6751
6752        let fs = FakeFs::new(cx.executor());
6753        let project = Project::test(fs, [], cx).await;
6754        let (workspace, cx) =
6755            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6756
6757        // Adding an item with no ambiguity renders the tab without detail.
6758        let item1 = cx.new(|cx| {
6759            let mut item = TestItem::new(cx);
6760            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6761            item
6762        });
6763        workspace.update_in(cx, |workspace, window, cx| {
6764            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6765        });
6766        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6767
6768        // Adding an item that creates ambiguity increases the level of detail on
6769        // both tabs.
6770        let item2 = cx.new_window_entity(|_window, cx| {
6771            let mut item = TestItem::new(cx);
6772            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6773            item
6774        });
6775        workspace.update_in(cx, |workspace, window, cx| {
6776            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6777        });
6778        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6779        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6780
6781        // Adding an item that creates ambiguity increases the level of detail only
6782        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6783        // we stop at the highest detail available.
6784        let item3 = cx.new(|cx| {
6785            let mut item = TestItem::new(cx);
6786            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6787            item
6788        });
6789        workspace.update_in(cx, |workspace, window, cx| {
6790            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6791        });
6792        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6793        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6794        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6795    }
6796
6797    #[gpui::test]
6798    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6799        init_test(cx);
6800
6801        let fs = FakeFs::new(cx.executor());
6802        fs.insert_tree(
6803            "/root1",
6804            json!({
6805                "one.txt": "",
6806                "two.txt": "",
6807            }),
6808        )
6809        .await;
6810        fs.insert_tree(
6811            "/root2",
6812            json!({
6813                "three.txt": "",
6814            }),
6815        )
6816        .await;
6817
6818        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6819        let (workspace, cx) =
6820            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6821        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6822        let worktree_id = project.update(cx, |project, cx| {
6823            project.worktrees(cx).next().unwrap().read(cx).id()
6824        });
6825
6826        let item1 = cx.new(|cx| {
6827            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6828        });
6829        let item2 = cx.new(|cx| {
6830            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6831        });
6832
6833        // Add an item to an empty pane
6834        workspace.update_in(cx, |workspace, window, cx| {
6835            workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx)
6836        });
6837        project.update(cx, |project, cx| {
6838            assert_eq!(
6839                project.active_entry(),
6840                project
6841                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6842                    .map(|e| e.id)
6843            );
6844        });
6845        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6846
6847        // Add a second item to a non-empty pane
6848        workspace.update_in(cx, |workspace, window, cx| {
6849            workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx)
6850        });
6851        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6852        project.update(cx, |project, cx| {
6853            assert_eq!(
6854                project.active_entry(),
6855                project
6856                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6857                    .map(|e| e.id)
6858            );
6859        });
6860
6861        // Close the active item
6862        pane.update_in(cx, |pane, window, cx| {
6863            pane.close_active_item(&Default::default(), window, cx)
6864                .unwrap()
6865        })
6866        .await
6867        .unwrap();
6868        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6869        project.update(cx, |project, cx| {
6870            assert_eq!(
6871                project.active_entry(),
6872                project
6873                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6874                    .map(|e| e.id)
6875            );
6876        });
6877
6878        // Add a project folder
6879        project
6880            .update(cx, |project, cx| {
6881                project.find_or_create_worktree("root2", true, cx)
6882            })
6883            .await
6884            .unwrap();
6885        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6886
6887        // Remove a project folder
6888        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6889        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6890    }
6891
6892    #[gpui::test]
6893    async fn test_close_window(cx: &mut TestAppContext) {
6894        init_test(cx);
6895
6896        let fs = FakeFs::new(cx.executor());
6897        fs.insert_tree("/root", json!({ "one": "" })).await;
6898
6899        let project = Project::test(fs, ["root".as_ref()], cx).await;
6900        let (workspace, cx) =
6901            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6902
6903        // When there are no dirty items, there's nothing to do.
6904        let item1 = cx.new(TestItem::new);
6905        workspace.update_in(cx, |w, window, cx| {
6906            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx)
6907        });
6908        let task = workspace.update_in(cx, |w, window, cx| {
6909            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6910        });
6911        assert!(task.await.unwrap());
6912
6913        // When there are dirty untitled items, prompt to save each one. If the user
6914        // cancels any prompt, then abort.
6915        let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true));
6916        let item3 = cx.new(|cx| {
6917            TestItem::new(cx)
6918                .with_dirty(true)
6919                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6920        });
6921        workspace.update_in(cx, |w, window, cx| {
6922            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6923            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
6924        });
6925        let task = workspace.update_in(cx, |w, window, cx| {
6926            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6927        });
6928        cx.executor().run_until_parked();
6929        cx.simulate_prompt_answer(2); // cancel save all
6930        cx.executor().run_until_parked();
6931        cx.simulate_prompt_answer(2); // cancel save all
6932        cx.executor().run_until_parked();
6933        assert!(!cx.has_pending_prompt());
6934        assert!(!task.await.unwrap());
6935    }
6936
6937    #[gpui::test]
6938    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6939        init_test(cx);
6940
6941        // Register TestItem as a serializable item
6942        cx.update(|cx| {
6943            register_serializable_item::<TestItem>(cx);
6944        });
6945
6946        let fs = FakeFs::new(cx.executor());
6947        fs.insert_tree("/root", json!({ "one": "" })).await;
6948
6949        let project = Project::test(fs, ["root".as_ref()], cx).await;
6950        let (workspace, cx) =
6951            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
6952
6953        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6954        let item1 = cx.new(|cx| {
6955            TestItem::new(cx)
6956                .with_dirty(true)
6957                .with_serialize(|| Some(Task::ready(Ok(()))))
6958        });
6959        let item2 = cx.new(|cx| {
6960            TestItem::new(cx)
6961                .with_dirty(true)
6962                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6963                .with_serialize(|| Some(Task::ready(Ok(()))))
6964        });
6965        workspace.update_in(cx, |w, window, cx| {
6966            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
6967            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
6968        });
6969        let task = workspace.update_in(cx, |w, window, cx| {
6970            w.prepare_to_close(CloseIntent::CloseWindow, window, cx)
6971        });
6972        assert!(task.await.unwrap());
6973    }
6974
6975    #[gpui::test]
6976    async fn test_close_pane_items(cx: &mut TestAppContext) {
6977        init_test(cx);
6978
6979        let fs = FakeFs::new(cx.executor());
6980
6981        let project = Project::test(fs, None, cx).await;
6982        let (workspace, cx) =
6983            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
6984
6985        let item1 = cx.new(|cx| {
6986            TestItem::new(cx)
6987                .with_dirty(true)
6988                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
6989        });
6990        let item2 = cx.new(|cx| {
6991            TestItem::new(cx)
6992                .with_dirty(true)
6993                .with_conflict(true)
6994                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
6995        });
6996        let item3 = cx.new(|cx| {
6997            TestItem::new(cx)
6998                .with_dirty(true)
6999                .with_conflict(true)
7000                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
7001        });
7002        let item4 = cx.new(|cx| {
7003            TestItem::new(cx).with_dirty(true).with_project_items(&[{
7004                let project_item = TestProjectItem::new_untitled(cx);
7005                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7006                project_item
7007            }])
7008        });
7009        let pane = workspace.update_in(cx, |workspace, window, cx| {
7010            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx);
7011            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx);
7012            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx);
7013            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx);
7014            workspace.active_pane().clone()
7015        });
7016
7017        let close_items = pane.update_in(cx, |pane, window, cx| {
7018            pane.activate_item(1, true, true, window, cx);
7019            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7020            let item1_id = item1.item_id();
7021            let item3_id = item3.item_id();
7022            let item4_id = item4.item_id();
7023            pane.close_items(window, cx, SaveIntent::Close, move |id| {
7024                [item1_id, item3_id, item4_id].contains(&id)
7025            })
7026        });
7027        cx.executor().run_until_parked();
7028
7029        assert!(cx.has_pending_prompt());
7030        // Ignore "Save all" prompt
7031        cx.simulate_prompt_answer(2);
7032        cx.executor().run_until_parked();
7033        // There's a prompt to save item 1.
7034        pane.update(cx, |pane, _| {
7035            assert_eq!(pane.items_len(), 4);
7036            assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
7037        });
7038        // Confirm saving item 1.
7039        cx.simulate_prompt_answer(0);
7040        cx.executor().run_until_parked();
7041
7042        // Item 1 is saved. There's a prompt to save item 3.
7043        pane.update(cx, |pane, cx| {
7044            assert_eq!(item1.read(cx).save_count, 1);
7045            assert_eq!(item1.read(cx).save_as_count, 0);
7046            assert_eq!(item1.read(cx).reload_count, 0);
7047            assert_eq!(pane.items_len(), 3);
7048            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
7049        });
7050        assert!(cx.has_pending_prompt());
7051
7052        // Cancel saving item 3.
7053        cx.simulate_prompt_answer(1);
7054        cx.executor().run_until_parked();
7055
7056        // Item 3 is reloaded. There's a prompt to save item 4.
7057        pane.update(cx, |pane, cx| {
7058            assert_eq!(item3.read(cx).save_count, 0);
7059            assert_eq!(item3.read(cx).save_as_count, 0);
7060            assert_eq!(item3.read(cx).reload_count, 1);
7061            assert_eq!(pane.items_len(), 2);
7062            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
7063        });
7064        assert!(cx.has_pending_prompt());
7065
7066        // Confirm saving item 4.
7067        cx.simulate_prompt_answer(0);
7068        cx.executor().run_until_parked();
7069
7070        // There's a prompt for a path for item 4.
7071        cx.simulate_new_path_selection(|_| Some(Default::default()));
7072        close_items.await.unwrap();
7073
7074        // The requested items are closed.
7075        pane.update(cx, |pane, cx| {
7076            assert_eq!(item4.read(cx).save_count, 0);
7077            assert_eq!(item4.read(cx).save_as_count, 1);
7078            assert_eq!(item4.read(cx).reload_count, 0);
7079            assert_eq!(pane.items_len(), 1);
7080            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
7081        });
7082    }
7083
7084    #[gpui::test]
7085    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
7086        init_test(cx);
7087
7088        let fs = FakeFs::new(cx.executor());
7089        let project = Project::test(fs, [], cx).await;
7090        let (workspace, cx) =
7091            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7092
7093        // Create several workspace items with single project entries, and two
7094        // workspace items with multiple project entries.
7095        let single_entry_items = (0..=4)
7096            .map(|project_entry_id| {
7097                cx.new(|cx| {
7098                    TestItem::new(cx)
7099                        .with_dirty(true)
7100                        .with_project_items(&[dirty_project_item(
7101                            project_entry_id,
7102                            &format!("{project_entry_id}.txt"),
7103                            cx,
7104                        )])
7105                })
7106            })
7107            .collect::<Vec<_>>();
7108        let item_2_3 = cx.new(|cx| {
7109            TestItem::new(cx)
7110                .with_dirty(true)
7111                .with_singleton(false)
7112                .with_project_items(&[
7113                    single_entry_items[2].read(cx).project_items[0].clone(),
7114                    single_entry_items[3].read(cx).project_items[0].clone(),
7115                ])
7116        });
7117        let item_3_4 = cx.new(|cx| {
7118            TestItem::new(cx)
7119                .with_dirty(true)
7120                .with_singleton(false)
7121                .with_project_items(&[
7122                    single_entry_items[3].read(cx).project_items[0].clone(),
7123                    single_entry_items[4].read(cx).project_items[0].clone(),
7124                ])
7125        });
7126
7127        // Create two panes that contain the following project entries:
7128        //   left pane:
7129        //     multi-entry items:   (2, 3)
7130        //     single-entry items:  0, 1, 2, 3, 4
7131        //   right pane:
7132        //     single-entry items:  1
7133        //     multi-entry items:   (3, 4)
7134        let left_pane = workspace.update_in(cx, |workspace, window, cx| {
7135            let left_pane = workspace.active_pane().clone();
7136            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx);
7137            for item in single_entry_items {
7138                workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
7139            }
7140            left_pane.update(cx, |pane, cx| {
7141                pane.activate_item(2, true, true, window, cx);
7142            });
7143
7144            let right_pane = workspace
7145                .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx)
7146                .unwrap();
7147
7148            right_pane.update(cx, |pane, cx| {
7149                pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx);
7150            });
7151
7152            left_pane
7153        });
7154
7155        cx.focus(&left_pane);
7156
7157        // When closing all of the items in the left pane, we should be prompted twice:
7158        // once for project entry 0, and once for project entry 2. Project entries 1,
7159        // 3, and 4 are all still open in the other paten. After those two
7160        // prompts, the task should complete.
7161
7162        let close = left_pane.update_in(cx, |pane, window, cx| {
7163            pane.close_all_items(&CloseAllItems::default(), window, cx)
7164                .unwrap()
7165        });
7166        cx.executor().run_until_parked();
7167
7168        // Discard "Save all" prompt
7169        cx.simulate_prompt_answer(2);
7170
7171        cx.executor().run_until_parked();
7172        left_pane.update(cx, |pane, cx| {
7173            assert_eq!(
7174                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7175                &[ProjectEntryId::from_proto(0)]
7176            );
7177        });
7178        cx.simulate_prompt_answer(0);
7179
7180        cx.executor().run_until_parked();
7181        left_pane.update(cx, |pane, cx| {
7182            assert_eq!(
7183                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
7184                &[ProjectEntryId::from_proto(2)]
7185            );
7186        });
7187        cx.simulate_prompt_answer(0);
7188
7189        cx.executor().run_until_parked();
7190        close.await.unwrap();
7191        left_pane.update(cx, |pane, _| {
7192            assert_eq!(pane.items_len(), 0);
7193        });
7194    }
7195
7196    #[gpui::test]
7197    async fn test_autosave(cx: &mut gpui::TestAppContext) {
7198        init_test(cx);
7199
7200        let fs = FakeFs::new(cx.executor());
7201        let project = Project::test(fs, [], cx).await;
7202        let (workspace, cx) =
7203            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7204        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7205
7206        let item = cx.new(|cx| {
7207            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7208        });
7209        let item_id = item.entity_id();
7210        workspace.update_in(cx, |workspace, window, cx| {
7211            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7212        });
7213
7214        // Autosave on window change.
7215        item.update(cx, |item, cx| {
7216            SettingsStore::update_global(cx, |settings, cx| {
7217                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7218                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
7219                })
7220            });
7221            item.is_dirty = true;
7222        });
7223
7224        // Deactivating the window saves the file.
7225        cx.deactivate_window();
7226        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7227
7228        // Re-activating the window doesn't save the file.
7229        cx.update(|window, _| window.activate_window());
7230        cx.executor().run_until_parked();
7231        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
7232
7233        // Autosave on focus change.
7234        item.update_in(cx, |item, window, cx| {
7235            cx.focus_self(window);
7236            SettingsStore::update_global(cx, |settings, cx| {
7237                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7238                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7239                })
7240            });
7241            item.is_dirty = true;
7242        });
7243
7244        // Blurring the item saves the file.
7245        item.update_in(cx, |_, window, _| window.blur());
7246        cx.executor().run_until_parked();
7247        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
7248
7249        // Deactivating the window still saves the file.
7250        item.update_in(cx, |item, window, cx| {
7251            cx.focus_self(window);
7252            item.is_dirty = true;
7253        });
7254        cx.deactivate_window();
7255        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7256
7257        // Autosave after delay.
7258        item.update(cx, |item, cx| {
7259            SettingsStore::update_global(cx, |settings, cx| {
7260                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7261                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
7262                })
7263            });
7264            item.is_dirty = true;
7265            cx.emit(ItemEvent::Edit);
7266        });
7267
7268        // Delay hasn't fully expired, so the file is still dirty and unsaved.
7269        cx.executor().advance_clock(Duration::from_millis(250));
7270        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
7271
7272        // After delay expires, the file is saved.
7273        cx.executor().advance_clock(Duration::from_millis(250));
7274        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
7275
7276        // Autosave on focus change, ensuring closing the tab counts as such.
7277        item.update(cx, |item, cx| {
7278            SettingsStore::update_global(cx, |settings, cx| {
7279                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
7280                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
7281                })
7282            });
7283            item.is_dirty = true;
7284            for project_item in &mut item.project_items {
7285                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
7286            }
7287        });
7288
7289        pane.update_in(cx, |pane, window, cx| {
7290            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7291        })
7292        .await
7293        .unwrap();
7294        assert!(!cx.has_pending_prompt());
7295        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7296
7297        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
7298        workspace.update_in(cx, |workspace, window, cx| {
7299            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7300        });
7301        item.update_in(cx, |item, window, cx| {
7302            item.project_items[0].update(cx, |item, _| {
7303                item.entry_id = None;
7304            });
7305            item.is_dirty = true;
7306            window.blur();
7307        });
7308        cx.run_until_parked();
7309        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7310
7311        // Ensure autosave is prevented for deleted files also when closing the buffer.
7312        let _close_items = pane.update_in(cx, |pane, window, cx| {
7313            pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id)
7314        });
7315        cx.run_until_parked();
7316        assert!(cx.has_pending_prompt());
7317        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
7318    }
7319
7320    #[gpui::test]
7321    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
7322        init_test(cx);
7323
7324        let fs = FakeFs::new(cx.executor());
7325
7326        let project = Project::test(fs, [], cx).await;
7327        let (workspace, cx) =
7328            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7329
7330        let item = cx.new(|cx| {
7331            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
7332        });
7333        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7334        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
7335        let toolbar_notify_count = Rc::new(RefCell::new(0));
7336
7337        workspace.update_in(cx, |workspace, window, cx| {
7338            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx);
7339            let toolbar_notification_count = toolbar_notify_count.clone();
7340            cx.observe_in(&toolbar, window, move |_, _, _, _| {
7341                *toolbar_notification_count.borrow_mut() += 1
7342            })
7343            .detach();
7344        });
7345
7346        pane.update(cx, |pane, _| {
7347            assert!(!pane.can_navigate_backward());
7348            assert!(!pane.can_navigate_forward());
7349        });
7350
7351        item.update_in(cx, |item, _, cx| {
7352            item.set_state("one".to_string(), cx);
7353        });
7354
7355        // Toolbar must be notified to re-render the navigation buttons
7356        assert_eq!(*toolbar_notify_count.borrow(), 1);
7357
7358        pane.update(cx, |pane, _| {
7359            assert!(pane.can_navigate_backward());
7360            assert!(!pane.can_navigate_forward());
7361        });
7362
7363        workspace
7364            .update_in(cx, |workspace, window, cx| {
7365                workspace.go_back(pane.downgrade(), window, cx)
7366            })
7367            .await
7368            .unwrap();
7369
7370        assert_eq!(*toolbar_notify_count.borrow(), 2);
7371        pane.update(cx, |pane, _| {
7372            assert!(!pane.can_navigate_backward());
7373            assert!(pane.can_navigate_forward());
7374        });
7375    }
7376
7377    #[gpui::test]
7378    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
7379        init_test(cx);
7380        let fs = FakeFs::new(cx.executor());
7381
7382        let project = Project::test(fs, [], cx).await;
7383        let (workspace, cx) =
7384            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7385
7386        let panel = workspace.update_in(cx, |workspace, window, cx| {
7387            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7388            workspace.add_panel(panel.clone(), window, cx);
7389
7390            workspace
7391                .right_dock()
7392                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
7393
7394            panel
7395        });
7396
7397        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7398        pane.update_in(cx, |pane, window, cx| {
7399            let item = cx.new(TestItem::new);
7400            pane.add_item(Box::new(item), true, true, None, window, cx);
7401        });
7402
7403        // Transfer focus from center to panel
7404        workspace.update_in(cx, |workspace, window, cx| {
7405            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7406        });
7407
7408        workspace.update_in(cx, |workspace, window, cx| {
7409            assert!(workspace.right_dock().read(cx).is_open());
7410            assert!(!panel.is_zoomed(window, cx));
7411            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7412        });
7413
7414        // Transfer focus from panel to center
7415        workspace.update_in(cx, |workspace, window, cx| {
7416            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7417        });
7418
7419        workspace.update_in(cx, |workspace, window, cx| {
7420            assert!(workspace.right_dock().read(cx).is_open());
7421            assert!(!panel.is_zoomed(window, cx));
7422            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7423        });
7424
7425        // Close the dock
7426        workspace.update_in(cx, |workspace, window, cx| {
7427            workspace.toggle_dock(DockPosition::Right, window, cx);
7428        });
7429
7430        workspace.update_in(cx, |workspace, window, cx| {
7431            assert!(!workspace.right_dock().read(cx).is_open());
7432            assert!(!panel.is_zoomed(window, cx));
7433            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7434        });
7435
7436        // Open the dock
7437        workspace.update_in(cx, |workspace, window, cx| {
7438            workspace.toggle_dock(DockPosition::Right, window, cx);
7439        });
7440
7441        workspace.update_in(cx, |workspace, window, cx| {
7442            assert!(workspace.right_dock().read(cx).is_open());
7443            assert!(!panel.is_zoomed(window, cx));
7444            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7445        });
7446
7447        // Focus and zoom panel
7448        panel.update_in(cx, |panel, window, cx| {
7449            cx.focus_self(window);
7450            panel.set_zoomed(true, window, cx)
7451        });
7452
7453        workspace.update_in(cx, |workspace, window, cx| {
7454            assert!(workspace.right_dock().read(cx).is_open());
7455            assert!(panel.is_zoomed(window, cx));
7456            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7457        });
7458
7459        // Transfer focus to the center closes the dock
7460        workspace.update_in(cx, |workspace, window, cx| {
7461            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7462        });
7463
7464        workspace.update_in(cx, |workspace, window, cx| {
7465            assert!(!workspace.right_dock().read(cx).is_open());
7466            assert!(panel.is_zoomed(window, cx));
7467            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7468        });
7469
7470        // Transferring focus back to the panel keeps it zoomed
7471        workspace.update_in(cx, |workspace, window, cx| {
7472            workspace.toggle_panel_focus::<TestPanel>(window, cx);
7473        });
7474
7475        workspace.update_in(cx, |workspace, window, cx| {
7476            assert!(workspace.right_dock().read(cx).is_open());
7477            assert!(panel.is_zoomed(window, cx));
7478            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7479        });
7480
7481        // Close the dock while it is zoomed
7482        workspace.update_in(cx, |workspace, window, cx| {
7483            workspace.toggle_dock(DockPosition::Right, window, cx)
7484        });
7485
7486        workspace.update_in(cx, |workspace, window, cx| {
7487            assert!(!workspace.right_dock().read(cx).is_open());
7488            assert!(panel.is_zoomed(window, cx));
7489            assert!(workspace.zoomed.is_none());
7490            assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7491        });
7492
7493        // Opening the dock, when it's zoomed, retains focus
7494        workspace.update_in(cx, |workspace, window, cx| {
7495            workspace.toggle_dock(DockPosition::Right, window, cx)
7496        });
7497
7498        workspace.update_in(cx, |workspace, window, cx| {
7499            assert!(workspace.right_dock().read(cx).is_open());
7500            assert!(panel.is_zoomed(window, cx));
7501            assert!(workspace.zoomed.is_some());
7502            assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx));
7503        });
7504
7505        // Unzoom and close the panel, zoom the active pane.
7506        panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx));
7507        workspace.update_in(cx, |workspace, window, cx| {
7508            workspace.toggle_dock(DockPosition::Right, window, cx)
7509        });
7510        pane.update_in(cx, |pane, window, cx| {
7511            pane.toggle_zoom(&Default::default(), window, cx)
7512        });
7513
7514        // Opening a dock unzooms the pane.
7515        workspace.update_in(cx, |workspace, window, cx| {
7516            workspace.toggle_dock(DockPosition::Right, window, cx)
7517        });
7518        workspace.update_in(cx, |workspace, window, cx| {
7519            let pane = pane.read(cx);
7520            assert!(!pane.is_zoomed());
7521            assert!(!pane.focus_handle(cx).is_focused(window));
7522            assert!(workspace.right_dock().read(cx).is_open());
7523            assert!(workspace.zoomed.is_none());
7524        });
7525    }
7526
7527    #[gpui::test]
7528    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7529        init_test(cx);
7530
7531        let fs = FakeFs::new(cx.executor());
7532
7533        let project = Project::test(fs, None, cx).await;
7534        let (workspace, cx) =
7535            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7536
7537        // Let's arrange the panes like this:
7538        //
7539        // +-----------------------+
7540        // |         top           |
7541        // +------+--------+-------+
7542        // | left | center | right |
7543        // +------+--------+-------+
7544        // |        bottom         |
7545        // +-----------------------+
7546
7547        let top_item = cx.new(|cx| {
7548            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7549        });
7550        let bottom_item = cx.new(|cx| {
7551            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7552        });
7553        let left_item = cx.new(|cx| {
7554            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7555        });
7556        let right_item = cx.new(|cx| {
7557            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7558        });
7559        let center_item = cx.new(|cx| {
7560            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7561        });
7562
7563        let top_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7564            let top_pane_id = workspace.active_pane().entity_id();
7565            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx);
7566            workspace.split_pane(
7567                workspace.active_pane().clone(),
7568                SplitDirection::Down,
7569                window,
7570                cx,
7571            );
7572            top_pane_id
7573        });
7574        let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7575            let bottom_pane_id = workspace.active_pane().entity_id();
7576            workspace.add_item_to_active_pane(
7577                Box::new(bottom_item.clone()),
7578                None,
7579                false,
7580                window,
7581                cx,
7582            );
7583            workspace.split_pane(
7584                workspace.active_pane().clone(),
7585                SplitDirection::Up,
7586                window,
7587                cx,
7588            );
7589            bottom_pane_id
7590        });
7591        let left_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7592            let left_pane_id = workspace.active_pane().entity_id();
7593            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx);
7594            workspace.split_pane(
7595                workspace.active_pane().clone(),
7596                SplitDirection::Right,
7597                window,
7598                cx,
7599            );
7600            left_pane_id
7601        });
7602        let right_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7603            let right_pane_id = workspace.active_pane().entity_id();
7604            workspace.add_item_to_active_pane(
7605                Box::new(right_item.clone()),
7606                None,
7607                false,
7608                window,
7609                cx,
7610            );
7611            workspace.split_pane(
7612                workspace.active_pane().clone(),
7613                SplitDirection::Left,
7614                window,
7615                cx,
7616            );
7617            right_pane_id
7618        });
7619        let center_pane_id = workspace.update_in(cx, |workspace, window, cx| {
7620            let center_pane_id = workspace.active_pane().entity_id();
7621            workspace.add_item_to_active_pane(
7622                Box::new(center_item.clone()),
7623                None,
7624                false,
7625                window,
7626                cx,
7627            );
7628            center_pane_id
7629        });
7630        cx.executor().run_until_parked();
7631
7632        workspace.update_in(cx, |workspace, window, cx| {
7633            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7634
7635            // Join into next from center pane into right
7636            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7637        });
7638
7639        workspace.update_in(cx, |workspace, window, cx| {
7640            let active_pane = workspace.active_pane();
7641            assert_eq!(right_pane_id, active_pane.entity_id());
7642            assert_eq!(2, active_pane.read(cx).items_len());
7643            let item_ids_in_pane =
7644                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7645            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7646            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7647
7648            // Join into next from right pane into bottom
7649            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7650        });
7651
7652        workspace.update_in(cx, |workspace, window, cx| {
7653            let active_pane = workspace.active_pane();
7654            assert_eq!(bottom_pane_id, active_pane.entity_id());
7655            assert_eq!(3, active_pane.read(cx).items_len());
7656            let item_ids_in_pane =
7657                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7658            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7659            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7660            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7661
7662            // Join into next from bottom pane into left
7663            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7664        });
7665
7666        workspace.update_in(cx, |workspace, window, cx| {
7667            let active_pane = workspace.active_pane();
7668            assert_eq!(left_pane_id, active_pane.entity_id());
7669            assert_eq!(4, active_pane.read(cx).items_len());
7670            let item_ids_in_pane =
7671                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7672            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7673            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7674            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7675            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7676
7677            // Join into next from left pane into top
7678            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx);
7679        });
7680
7681        workspace.update_in(cx, |workspace, window, cx| {
7682            let active_pane = workspace.active_pane();
7683            assert_eq!(top_pane_id, active_pane.entity_id());
7684            assert_eq!(5, active_pane.read(cx).items_len());
7685            let item_ids_in_pane =
7686                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7687            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7688            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7689            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7690            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7691            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7692
7693            // Single pane left: no-op
7694            workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx)
7695        });
7696
7697        workspace.update(cx, |workspace, _cx| {
7698            let active_pane = workspace.active_pane();
7699            assert_eq!(top_pane_id, active_pane.entity_id());
7700        });
7701    }
7702
7703    fn add_an_item_to_active_pane(
7704        cx: &mut VisualTestContext,
7705        workspace: &Entity<Workspace>,
7706        item_id: u64,
7707    ) -> Entity<TestItem> {
7708        let item = cx.new(|cx| {
7709            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7710                item_id,
7711                "item{item_id}.txt",
7712                cx,
7713            )])
7714        });
7715        workspace.update_in(cx, |workspace, window, cx| {
7716            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx);
7717        });
7718        return item;
7719    }
7720
7721    fn split_pane(cx: &mut VisualTestContext, workspace: &Entity<Workspace>) -> Entity<Pane> {
7722        return workspace.update_in(cx, |workspace, window, cx| {
7723            let new_pane = workspace.split_pane(
7724                workspace.active_pane().clone(),
7725                SplitDirection::Right,
7726                window,
7727                cx,
7728            );
7729            new_pane
7730        });
7731    }
7732
7733    #[gpui::test]
7734    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7735        init_test(cx);
7736        let fs = FakeFs::new(cx.executor());
7737        let project = Project::test(fs, None, cx).await;
7738        let (workspace, cx) =
7739            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7740
7741        add_an_item_to_active_pane(cx, &workspace, 1);
7742        split_pane(cx, &workspace);
7743        add_an_item_to_active_pane(cx, &workspace, 2);
7744        split_pane(cx, &workspace); // empty pane
7745        split_pane(cx, &workspace);
7746        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7747
7748        cx.executor().run_until_parked();
7749
7750        workspace.update(cx, |workspace, cx| {
7751            let num_panes = workspace.panes().len();
7752            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7753            let active_item = workspace
7754                .active_pane()
7755                .read(cx)
7756                .active_item()
7757                .expect("item is in focus");
7758
7759            assert_eq!(num_panes, 4);
7760            assert_eq!(num_items_in_current_pane, 1);
7761            assert_eq!(active_item.item_id(), last_item.item_id());
7762        });
7763
7764        workspace.update_in(cx, |workspace, window, cx| {
7765            workspace.join_all_panes(window, cx);
7766        });
7767
7768        workspace.update(cx, |workspace, cx| {
7769            let num_panes = workspace.panes().len();
7770            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7771            let active_item = workspace
7772                .active_pane()
7773                .read(cx)
7774                .active_item()
7775                .expect("item is in focus");
7776
7777            assert_eq!(num_panes, 1);
7778            assert_eq!(num_items_in_current_pane, 3);
7779            assert_eq!(active_item.item_id(), last_item.item_id());
7780        });
7781    }
7782    struct TestModal(FocusHandle);
7783
7784    impl TestModal {
7785        fn new(_: &mut Window, cx: &mut Context<Self>) -> Self {
7786            Self(cx.focus_handle())
7787        }
7788    }
7789
7790    impl EventEmitter<DismissEvent> for TestModal {}
7791
7792    impl Focusable for TestModal {
7793        fn focus_handle(&self, _cx: &App) -> FocusHandle {
7794            self.0.clone()
7795        }
7796    }
7797
7798    impl ModalView for TestModal {}
7799
7800    impl Render for TestModal {
7801        fn render(
7802            &mut self,
7803            _window: &mut Window,
7804            _cx: &mut Context<TestModal>,
7805        ) -> impl IntoElement {
7806            div().track_focus(&self.0)
7807        }
7808    }
7809
7810    #[gpui::test]
7811    async fn test_panels(cx: &mut gpui::TestAppContext) {
7812        init_test(cx);
7813        let fs = FakeFs::new(cx.executor());
7814
7815        let project = Project::test(fs, [], cx).await;
7816        let (workspace, cx) =
7817            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
7818
7819        let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| {
7820            let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
7821            workspace.add_panel(panel_1.clone(), window, cx);
7822            workspace.toggle_dock(DockPosition::Left, window, cx);
7823            let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
7824            workspace.add_panel(panel_2.clone(), window, cx);
7825            workspace.toggle_dock(DockPosition::Right, window, cx);
7826
7827            let left_dock = workspace.left_dock();
7828            assert_eq!(
7829                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7830                panel_1.panel_id()
7831            );
7832            assert_eq!(
7833                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7834                panel_1.size(window, cx)
7835            );
7836
7837            left_dock.update(cx, |left_dock, cx| {
7838                left_dock.resize_active_panel(Some(px(1337.)), window, cx)
7839            });
7840            assert_eq!(
7841                workspace
7842                    .right_dock()
7843                    .read(cx)
7844                    .visible_panel()
7845                    .unwrap()
7846                    .panel_id(),
7847                panel_2.panel_id(),
7848            );
7849
7850            (panel_1, panel_2)
7851        });
7852
7853        // Move panel_1 to the right
7854        panel_1.update_in(cx, |panel_1, window, cx| {
7855            panel_1.set_position(DockPosition::Right, window, cx)
7856        });
7857
7858        workspace.update_in(cx, |workspace, window, cx| {
7859            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7860            // Since it was the only panel on the left, the left dock should now be closed.
7861            assert!(!workspace.left_dock().read(cx).is_open());
7862            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7863            let right_dock = workspace.right_dock();
7864            assert_eq!(
7865                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7866                panel_1.panel_id()
7867            );
7868            assert_eq!(
7869                right_dock.read(cx).active_panel_size(window, cx).unwrap(),
7870                px(1337.)
7871            );
7872
7873            // Now we move panel_2 to the left
7874            panel_2.set_position(DockPosition::Left, window, cx);
7875        });
7876
7877        workspace.update(cx, |workspace, cx| {
7878            // Since panel_2 was not visible on the right, we don't open the left dock.
7879            assert!(!workspace.left_dock().read(cx).is_open());
7880            // And the right dock is unaffected in its displaying of panel_1
7881            assert!(workspace.right_dock().read(cx).is_open());
7882            assert_eq!(
7883                workspace
7884                    .right_dock()
7885                    .read(cx)
7886                    .visible_panel()
7887                    .unwrap()
7888                    .panel_id(),
7889                panel_1.panel_id(),
7890            );
7891        });
7892
7893        // Move panel_1 back to the left
7894        panel_1.update_in(cx, |panel_1, window, cx| {
7895            panel_1.set_position(DockPosition::Left, window, cx)
7896        });
7897
7898        workspace.update_in(cx, |workspace, window, cx| {
7899            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7900            let left_dock = workspace.left_dock();
7901            assert!(left_dock.read(cx).is_open());
7902            assert_eq!(
7903                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7904                panel_1.panel_id()
7905            );
7906            assert_eq!(
7907                left_dock.read(cx).active_panel_size(window, cx).unwrap(),
7908                px(1337.)
7909            );
7910            // And the right dock should be closed as it no longer has any panels.
7911            assert!(!workspace.right_dock().read(cx).is_open());
7912
7913            // Now we move panel_1 to the bottom
7914            panel_1.set_position(DockPosition::Bottom, window, cx);
7915        });
7916
7917        workspace.update_in(cx, |workspace, window, cx| {
7918            // Since panel_1 was visible on the left, we close the left dock.
7919            assert!(!workspace.left_dock().read(cx).is_open());
7920            // The bottom dock is sized based on the panel's default size,
7921            // since the panel orientation changed from vertical to horizontal.
7922            let bottom_dock = workspace.bottom_dock();
7923            assert_eq!(
7924                bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
7925                panel_1.size(window, cx),
7926            );
7927            // Close bottom dock and move panel_1 back to the left.
7928            bottom_dock.update(cx, |bottom_dock, cx| {
7929                bottom_dock.set_open(false, window, cx)
7930            });
7931            panel_1.set_position(DockPosition::Left, window, cx);
7932        });
7933
7934        // Emit activated event on panel 1
7935        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7936
7937        // Now the left dock is open and panel_1 is active and focused.
7938        workspace.update_in(cx, |workspace, window, cx| {
7939            let left_dock = workspace.left_dock();
7940            assert!(left_dock.read(cx).is_open());
7941            assert_eq!(
7942                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7943                panel_1.panel_id(),
7944            );
7945            assert!(panel_1.focus_handle(cx).is_focused(window));
7946        });
7947
7948        // Emit closed event on panel 2, which is not active
7949        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7950
7951        // Wo don't close the left dock, because panel_2 wasn't the active panel
7952        workspace.update(cx, |workspace, cx| {
7953            let left_dock = workspace.left_dock();
7954            assert!(left_dock.read(cx).is_open());
7955            assert_eq!(
7956                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7957                panel_1.panel_id(),
7958            );
7959        });
7960
7961        // Emitting a ZoomIn event shows the panel as zoomed.
7962        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7963        workspace.update(cx, |workspace, _| {
7964            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7965            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7966        });
7967
7968        // Move panel to another dock while it is zoomed
7969        panel_1.update_in(cx, |panel, window, cx| {
7970            panel.set_position(DockPosition::Right, window, cx)
7971        });
7972        workspace.update(cx, |workspace, _| {
7973            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7974
7975            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7976        });
7977
7978        // This is a helper for getting a:
7979        // - valid focus on an element,
7980        // - that isn't a part of the panes and panels system of the Workspace,
7981        // - and doesn't trigger the 'on_focus_lost' API.
7982        let focus_other_view = {
7983            let workspace = workspace.clone();
7984            move |cx: &mut VisualTestContext| {
7985                workspace.update_in(cx, |workspace, window, cx| {
7986                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7987                        workspace.toggle_modal(window, cx, TestModal::new);
7988                        workspace.toggle_modal(window, cx, TestModal::new);
7989                    } else {
7990                        workspace.toggle_modal(window, cx, TestModal::new);
7991                    }
7992                })
7993            }
7994        };
7995
7996        // If focus is transferred to another view that's not a panel or another pane, we still show
7997        // the panel as zoomed.
7998        focus_other_view(cx);
7999        workspace.update(cx, |workspace, _| {
8000            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8001            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8002        });
8003
8004        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
8005        workspace.update_in(cx, |_workspace, window, cx| {
8006            cx.focus_self(window);
8007        });
8008        workspace.update(cx, |workspace, _| {
8009            assert_eq!(workspace.zoomed, None);
8010            assert_eq!(workspace.zoomed_position, None);
8011        });
8012
8013        // If focus is transferred again to another view that's not a panel or a pane, we won't
8014        // show the panel as zoomed because it wasn't zoomed before.
8015        focus_other_view(cx);
8016        workspace.update(cx, |workspace, _| {
8017            assert_eq!(workspace.zoomed, None);
8018            assert_eq!(workspace.zoomed_position, None);
8019        });
8020
8021        // When the panel is activated, it is zoomed again.
8022        cx.dispatch_action(ToggleRightDock);
8023        workspace.update(cx, |workspace, _| {
8024            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
8025            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
8026        });
8027
8028        // Emitting a ZoomOut event unzooms the panel.
8029        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
8030        workspace.update(cx, |workspace, _| {
8031            assert_eq!(workspace.zoomed, None);
8032            assert_eq!(workspace.zoomed_position, None);
8033        });
8034
8035        // Emit closed event on panel 1, which is active
8036        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
8037
8038        // Now the left dock is closed, because panel_1 was the active panel
8039        workspace.update(cx, |workspace, cx| {
8040            let right_dock = workspace.right_dock();
8041            assert!(!right_dock.read(cx).is_open());
8042        });
8043    }
8044
8045    #[gpui::test]
8046    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
8047        init_test(cx);
8048
8049        let fs = FakeFs::new(cx.background_executor.clone());
8050        let project = Project::test(fs, [], cx).await;
8051        let (workspace, cx) =
8052            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8053        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8054
8055        let dirty_regular_buffer = cx.new(|cx| {
8056            TestItem::new(cx)
8057                .with_dirty(true)
8058                .with_label("1.txt")
8059                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8060        });
8061        let dirty_regular_buffer_2 = cx.new(|cx| {
8062            TestItem::new(cx)
8063                .with_dirty(true)
8064                .with_label("2.txt")
8065                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8066        });
8067        let dirty_multi_buffer_with_both = cx.new(|cx| {
8068            TestItem::new(cx)
8069                .with_dirty(true)
8070                .with_singleton(false)
8071                .with_label("Fake Project Search")
8072                .with_project_items(&[
8073                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8074                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8075                ])
8076        });
8077        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8078        workspace.update_in(cx, |workspace, window, cx| {
8079            workspace.add_item(
8080                pane.clone(),
8081                Box::new(dirty_regular_buffer.clone()),
8082                None,
8083                false,
8084                false,
8085                window,
8086                cx,
8087            );
8088            workspace.add_item(
8089                pane.clone(),
8090                Box::new(dirty_regular_buffer_2.clone()),
8091                None,
8092                false,
8093                false,
8094                window,
8095                cx,
8096            );
8097            workspace.add_item(
8098                pane.clone(),
8099                Box::new(dirty_multi_buffer_with_both.clone()),
8100                None,
8101                false,
8102                false,
8103                window,
8104                cx,
8105            );
8106        });
8107
8108        pane.update_in(cx, |pane, window, cx| {
8109            pane.activate_item(2, true, true, window, cx);
8110            assert_eq!(
8111                pane.active_item().unwrap().item_id(),
8112                multi_buffer_with_both_files_id,
8113                "Should select the multi buffer in the pane"
8114            );
8115        });
8116        let close_all_but_multi_buffer_task = pane
8117            .update_in(cx, |pane, window, cx| {
8118                pane.close_inactive_items(
8119                    &CloseInactiveItems {
8120                        save_intent: Some(SaveIntent::Save),
8121                        close_pinned: true,
8122                    },
8123                    window,
8124                    cx,
8125                )
8126            })
8127            .expect("should have inactive files to close");
8128        cx.background_executor.run_until_parked();
8129        assert!(
8130            !cx.has_pending_prompt(),
8131            "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
8132        );
8133        close_all_but_multi_buffer_task
8134            .await
8135            .expect("Closing all buffers but the multi buffer failed");
8136        pane.update(cx, |pane, cx| {
8137            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8138            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
8139            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8140            assert_eq!(pane.items_len(), 1);
8141            assert_eq!(
8142                pane.active_item().unwrap().item_id(),
8143                multi_buffer_with_both_files_id,
8144                "Should have only the multi buffer left in the pane"
8145            );
8146            assert!(
8147                dirty_multi_buffer_with_both.read(cx).is_dirty,
8148                "The multi buffer containing the unsaved buffer should still be dirty"
8149            );
8150        });
8151
8152        let close_multi_buffer_task = pane
8153            .update_in(cx, |pane, window, cx| {
8154                pane.close_active_item(
8155                    &CloseActiveItem {
8156                        save_intent: Some(SaveIntent::Close),
8157                    },
8158                    window,
8159                    cx,
8160                )
8161            })
8162            .expect("should have the multi buffer to close");
8163        cx.background_executor.run_until_parked();
8164        assert!(
8165            cx.has_pending_prompt(),
8166            "Dirty multi buffer should prompt a save dialog"
8167        );
8168        cx.simulate_prompt_answer(0);
8169        cx.background_executor.run_until_parked();
8170        close_multi_buffer_task
8171            .await
8172            .expect("Closing the multi buffer failed");
8173        pane.update(cx, |pane, cx| {
8174            assert_eq!(
8175                dirty_multi_buffer_with_both.read(cx).save_count,
8176                1,
8177                "Multi buffer item should get be saved"
8178            );
8179            // Test impl does not save inner items, so we do not assert them
8180            assert_eq!(
8181                pane.items_len(),
8182                0,
8183                "No more items should be left in the pane"
8184            );
8185            assert!(pane.active_item().is_none());
8186        });
8187    }
8188
8189    #[gpui::test]
8190    async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
8191        cx: &mut TestAppContext,
8192    ) {
8193        init_test(cx);
8194
8195        let fs = FakeFs::new(cx.background_executor.clone());
8196        let project = Project::test(fs, [], cx).await;
8197        let (workspace, cx) =
8198            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8199        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8200
8201        let dirty_regular_buffer = cx.new(|cx| {
8202            TestItem::new(cx)
8203                .with_dirty(true)
8204                .with_label("1.txt")
8205                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8206        });
8207        let dirty_regular_buffer_2 = cx.new(|cx| {
8208            TestItem::new(cx)
8209                .with_dirty(true)
8210                .with_label("2.txt")
8211                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8212        });
8213        let clear_regular_buffer = cx.new(|cx| {
8214            TestItem::new(cx)
8215                .with_label("3.txt")
8216                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8217        });
8218
8219        let dirty_multi_buffer_with_both = cx.new(|cx| {
8220            TestItem::new(cx)
8221                .with_dirty(true)
8222                .with_singleton(false)
8223                .with_label("Fake Project Search")
8224                .with_project_items(&[
8225                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8226                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8227                    clear_regular_buffer.read(cx).project_items[0].clone(),
8228                ])
8229        });
8230        workspace.update_in(cx, |workspace, window, cx| {
8231            workspace.add_item(
8232                pane.clone(),
8233                Box::new(dirty_regular_buffer.clone()),
8234                None,
8235                false,
8236                false,
8237                window,
8238                cx,
8239            );
8240            workspace.add_item(
8241                pane.clone(),
8242                Box::new(dirty_multi_buffer_with_both.clone()),
8243                None,
8244                false,
8245                false,
8246                window,
8247                cx,
8248            );
8249        });
8250
8251        pane.update_in(cx, |pane, window, cx| {
8252            pane.activate_item(0, true, true, window, cx);
8253            assert_eq!(
8254                pane.active_item().unwrap().item_id(),
8255                dirty_regular_buffer.item_id(),
8256                "Should select the dirty singleton buffer in the pane"
8257            );
8258        });
8259        let close_singleton_buffer_task = pane
8260            .update_in(cx, |pane, window, cx| {
8261                pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8262            })
8263            .expect("should have active singleton buffer to close");
8264        cx.background_executor.run_until_parked();
8265        assert!(
8266            !cx.has_pending_prompt(),
8267            "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
8268        );
8269
8270        close_singleton_buffer_task
8271            .await
8272            .expect("Should not fail closing the singleton buffer");
8273        pane.update(cx, |pane, cx| {
8274            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8275            assert_eq!(
8276                dirty_multi_buffer_with_both.read(cx).save_count,
8277                0,
8278                "Multi buffer itself should not be saved"
8279            );
8280            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8281            assert_eq!(
8282                pane.items_len(),
8283                1,
8284                "A dirty multi buffer should be present in the pane"
8285            );
8286            assert_eq!(
8287                pane.active_item().unwrap().item_id(),
8288                dirty_multi_buffer_with_both.item_id(),
8289                "Should activate the only remaining item in the pane"
8290            );
8291        });
8292    }
8293
8294    #[gpui::test]
8295    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
8296        cx: &mut TestAppContext,
8297    ) {
8298        init_test(cx);
8299
8300        let fs = FakeFs::new(cx.background_executor.clone());
8301        let project = Project::test(fs, [], cx).await;
8302        let (workspace, cx) =
8303            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8304        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8305
8306        let dirty_regular_buffer = cx.new(|cx| {
8307            TestItem::new(cx)
8308                .with_dirty(true)
8309                .with_label("1.txt")
8310                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8311        });
8312        let dirty_regular_buffer_2 = cx.new(|cx| {
8313            TestItem::new(cx)
8314                .with_dirty(true)
8315                .with_label("2.txt")
8316                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8317        });
8318        let clear_regular_buffer = cx.new(|cx| {
8319            TestItem::new(cx)
8320                .with_label("3.txt")
8321                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8322        });
8323
8324        let dirty_multi_buffer_with_both = cx.new(|cx| {
8325            TestItem::new(cx)
8326                .with_dirty(true)
8327                .with_singleton(false)
8328                .with_label("Fake Project Search")
8329                .with_project_items(&[
8330                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8331                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8332                    clear_regular_buffer.read(cx).project_items[0].clone(),
8333                ])
8334        });
8335        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
8336        workspace.update_in(cx, |workspace, window, cx| {
8337            workspace.add_item(
8338                pane.clone(),
8339                Box::new(dirty_regular_buffer.clone()),
8340                None,
8341                false,
8342                false,
8343                window,
8344                cx,
8345            );
8346            workspace.add_item(
8347                pane.clone(),
8348                Box::new(dirty_multi_buffer_with_both.clone()),
8349                None,
8350                false,
8351                false,
8352                window,
8353                cx,
8354            );
8355        });
8356
8357        pane.update_in(cx, |pane, window, cx| {
8358            pane.activate_item(1, true, true, window, cx);
8359            assert_eq!(
8360                pane.active_item().unwrap().item_id(),
8361                multi_buffer_with_both_files_id,
8362                "Should select the multi buffer in the pane"
8363            );
8364        });
8365        let _close_multi_buffer_task = pane
8366            .update_in(cx, |pane, window, cx| {
8367                pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8368            })
8369            .expect("should have active multi buffer to close");
8370        cx.background_executor.run_until_parked();
8371        assert!(
8372            cx.has_pending_prompt(),
8373            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
8374        );
8375    }
8376
8377    #[gpui::test]
8378    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
8379        cx: &mut TestAppContext,
8380    ) {
8381        init_test(cx);
8382
8383        let fs = FakeFs::new(cx.background_executor.clone());
8384        let project = Project::test(fs, [], cx).await;
8385        let (workspace, cx) =
8386            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8387        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
8388
8389        let dirty_regular_buffer = cx.new(|cx| {
8390            TestItem::new(cx)
8391                .with_dirty(true)
8392                .with_label("1.txt")
8393                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
8394        });
8395        let dirty_regular_buffer_2 = cx.new(|cx| {
8396            TestItem::new(cx)
8397                .with_dirty(true)
8398                .with_label("2.txt")
8399                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
8400        });
8401        let clear_regular_buffer = cx.new(|cx| {
8402            TestItem::new(cx)
8403                .with_label("3.txt")
8404                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
8405        });
8406
8407        let dirty_multi_buffer = cx.new(|cx| {
8408            TestItem::new(cx)
8409                .with_dirty(true)
8410                .with_singleton(false)
8411                .with_label("Fake Project Search")
8412                .with_project_items(&[
8413                    dirty_regular_buffer.read(cx).project_items[0].clone(),
8414                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
8415                    clear_regular_buffer.read(cx).project_items[0].clone(),
8416                ])
8417        });
8418        workspace.update_in(cx, |workspace, window, cx| {
8419            workspace.add_item(
8420                pane.clone(),
8421                Box::new(dirty_regular_buffer.clone()),
8422                None,
8423                false,
8424                false,
8425                window,
8426                cx,
8427            );
8428            workspace.add_item(
8429                pane.clone(),
8430                Box::new(dirty_regular_buffer_2.clone()),
8431                None,
8432                false,
8433                false,
8434                window,
8435                cx,
8436            );
8437            workspace.add_item(
8438                pane.clone(),
8439                Box::new(dirty_multi_buffer.clone()),
8440                None,
8441                false,
8442                false,
8443                window,
8444                cx,
8445            );
8446        });
8447
8448        pane.update_in(cx, |pane, window, cx| {
8449            pane.activate_item(2, true, true, window, cx);
8450            assert_eq!(
8451                pane.active_item().unwrap().item_id(),
8452                dirty_multi_buffer.item_id(),
8453                "Should select the multi buffer in the pane"
8454            );
8455        });
8456        let close_multi_buffer_task = pane
8457            .update_in(cx, |pane, window, cx| {
8458                pane.close_active_item(&CloseActiveItem { save_intent: None }, window, cx)
8459            })
8460            .expect("should have active multi buffer to close");
8461        cx.background_executor.run_until_parked();
8462        assert!(
8463            !cx.has_pending_prompt(),
8464            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
8465        );
8466        close_multi_buffer_task
8467            .await
8468            .expect("Closing multi buffer failed");
8469        pane.update(cx, |pane, cx| {
8470            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
8471            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
8472            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
8473            assert_eq!(
8474                pane.items()
8475                    .map(|item| item.item_id())
8476                    .sorted()
8477                    .collect::<Vec<_>>(),
8478                vec![
8479                    dirty_regular_buffer.item_id(),
8480                    dirty_regular_buffer_2.item_id(),
8481                ],
8482                "Should have no multi buffer left in the pane"
8483            );
8484            assert!(dirty_regular_buffer.read(cx).is_dirty);
8485            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
8486        });
8487    }
8488
8489    #[gpui::test]
8490    async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) {
8491        init_test(cx);
8492        let fs = FakeFs::new(cx.executor());
8493        let project = Project::test(fs, [], cx).await;
8494        let (workspace, cx) =
8495            cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
8496
8497        // Add a new panel to the right dock, opening the dock and setting the
8498        // focus to the new panel.
8499        let panel = workspace.update_in(cx, |workspace, window, cx| {
8500            let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
8501            workspace.add_panel(panel.clone(), window, cx);
8502
8503            workspace
8504                .right_dock()
8505                .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx));
8506
8507            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8508
8509            panel
8510        });
8511
8512        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8513        // panel to the next valid position which, in this case, is the left
8514        // dock.
8515        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8516        workspace.update(cx, |workspace, cx| {
8517            assert!(workspace.left_dock().read(cx).is_open());
8518            assert_eq!(panel.read(cx).position, DockPosition::Left);
8519        });
8520
8521        // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the
8522        // panel to the next valid position which, in this case, is the bottom
8523        // dock.
8524        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8525        workspace.update(cx, |workspace, cx| {
8526            assert!(workspace.bottom_dock().read(cx).is_open());
8527            assert_eq!(panel.read(cx).position, DockPosition::Bottom);
8528        });
8529
8530        // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time
8531        // around moving the panel to its initial position, the right dock.
8532        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8533        workspace.update(cx, |workspace, cx| {
8534            assert!(workspace.right_dock().read(cx).is_open());
8535            assert_eq!(panel.read(cx).position, DockPosition::Right);
8536        });
8537
8538        // Remove focus from the panel, ensuring that, if the panel is not
8539        // focused, the `MoveFocusedPanelToNextPosition` action does not update
8540        // the panel's position, so the panel is still in the right dock.
8541        workspace.update_in(cx, |workspace, window, cx| {
8542            workspace.toggle_panel_focus::<TestPanel>(window, cx);
8543        });
8544
8545        cx.dispatch_action(MoveFocusedPanelToNextPosition);
8546        workspace.update(cx, |workspace, cx| {
8547            assert!(workspace.right_dock().read(cx).is_open());
8548            assert_eq!(panel.read(cx).position, DockPosition::Right);
8549        });
8550    }
8551
8552    mod register_project_item_tests {
8553
8554        use super::*;
8555
8556        // View
8557        struct TestPngItemView {
8558            focus_handle: FocusHandle,
8559        }
8560        // Model
8561        struct TestPngItem {}
8562
8563        impl project::ProjectItem for TestPngItem {
8564            fn try_open(
8565                _project: &Entity<Project>,
8566                path: &ProjectPath,
8567                cx: &mut App,
8568            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8569                if path.path.extension().unwrap() == "png" {
8570                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestPngItem {}) }))
8571                } else {
8572                    None
8573                }
8574            }
8575
8576            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8577                None
8578            }
8579
8580            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8581                None
8582            }
8583
8584            fn is_dirty(&self) -> bool {
8585                false
8586            }
8587        }
8588
8589        impl Item for TestPngItemView {
8590            type Event = ();
8591        }
8592        impl EventEmitter<()> for TestPngItemView {}
8593        impl Focusable for TestPngItemView {
8594            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8595                self.focus_handle.clone()
8596            }
8597        }
8598
8599        impl Render for TestPngItemView {
8600            fn render(
8601                &mut self,
8602                _window: &mut Window,
8603                _cx: &mut Context<Self>,
8604            ) -> impl IntoElement {
8605                Empty
8606            }
8607        }
8608
8609        impl ProjectItem for TestPngItemView {
8610            type Item = TestPngItem;
8611
8612            fn for_project_item(
8613                _project: Entity<Project>,
8614                _item: Entity<Self::Item>,
8615                _: &mut Window,
8616                cx: &mut Context<Self>,
8617            ) -> Self
8618            where
8619                Self: Sized,
8620            {
8621                Self {
8622                    focus_handle: cx.focus_handle(),
8623                }
8624            }
8625        }
8626
8627        // View
8628        struct TestIpynbItemView {
8629            focus_handle: FocusHandle,
8630        }
8631        // Model
8632        struct TestIpynbItem {}
8633
8634        impl project::ProjectItem for TestIpynbItem {
8635            fn try_open(
8636                _project: &Entity<Project>,
8637                path: &ProjectPath,
8638                cx: &mut App,
8639            ) -> Option<Task<gpui::Result<Entity<Self>>>> {
8640                if path.path.extension().unwrap() == "ipynb" {
8641                    Some(cx.spawn(|mut cx| async move { cx.new(|_| TestIpynbItem {}) }))
8642                } else {
8643                    None
8644                }
8645            }
8646
8647            fn entry_id(&self, _: &App) -> Option<ProjectEntryId> {
8648                None
8649            }
8650
8651            fn project_path(&self, _: &App) -> Option<ProjectPath> {
8652                None
8653            }
8654
8655            fn is_dirty(&self) -> bool {
8656                false
8657            }
8658        }
8659
8660        impl Item for TestIpynbItemView {
8661            type Event = ();
8662        }
8663        impl EventEmitter<()> for TestIpynbItemView {}
8664        impl Focusable for TestIpynbItemView {
8665            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8666                self.focus_handle.clone()
8667            }
8668        }
8669
8670        impl Render for TestIpynbItemView {
8671            fn render(
8672                &mut self,
8673                _window: &mut Window,
8674                _cx: &mut Context<Self>,
8675            ) -> impl IntoElement {
8676                Empty
8677            }
8678        }
8679
8680        impl ProjectItem for TestIpynbItemView {
8681            type Item = TestIpynbItem;
8682
8683            fn for_project_item(
8684                _project: Entity<Project>,
8685                _item: Entity<Self::Item>,
8686                _: &mut Window,
8687                cx: &mut Context<Self>,
8688            ) -> Self
8689            where
8690                Self: Sized,
8691            {
8692                Self {
8693                    focus_handle: cx.focus_handle(),
8694                }
8695            }
8696        }
8697
8698        struct TestAlternatePngItemView {
8699            focus_handle: FocusHandle,
8700        }
8701
8702        impl Item for TestAlternatePngItemView {
8703            type Event = ();
8704        }
8705
8706        impl EventEmitter<()> for TestAlternatePngItemView {}
8707        impl Focusable for TestAlternatePngItemView {
8708            fn focus_handle(&self, _cx: &App) -> FocusHandle {
8709                self.focus_handle.clone()
8710            }
8711        }
8712
8713        impl Render for TestAlternatePngItemView {
8714            fn render(
8715                &mut self,
8716                _window: &mut Window,
8717                _cx: &mut Context<Self>,
8718            ) -> impl IntoElement {
8719                Empty
8720            }
8721        }
8722
8723        impl ProjectItem for TestAlternatePngItemView {
8724            type Item = TestPngItem;
8725
8726            fn for_project_item(
8727                _project: Entity<Project>,
8728                _item: Entity<Self::Item>,
8729                _: &mut Window,
8730                cx: &mut Context<Self>,
8731            ) -> Self
8732            where
8733                Self: Sized,
8734            {
8735                Self {
8736                    focus_handle: cx.focus_handle(),
8737                }
8738            }
8739        }
8740
8741        #[gpui::test]
8742        async fn test_register_project_item(cx: &mut TestAppContext) {
8743            init_test(cx);
8744
8745            cx.update(|cx| {
8746                register_project_item::<TestPngItemView>(cx);
8747                register_project_item::<TestIpynbItemView>(cx);
8748            });
8749
8750            let fs = FakeFs::new(cx.executor());
8751            fs.insert_tree(
8752                "/root1",
8753                json!({
8754                    "one.png": "BINARYDATAHERE",
8755                    "two.ipynb": "{ totally a notebook }",
8756                    "three.txt": "editing text, sure why not?"
8757                }),
8758            )
8759            .await;
8760
8761            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8762            let (workspace, cx) =
8763                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8764
8765            let worktree_id = project.update(cx, |project, cx| {
8766                project.worktrees(cx).next().unwrap().read(cx).id()
8767            });
8768
8769            let handle = workspace
8770                .update_in(cx, |workspace, window, cx| {
8771                    let project_path = (worktree_id, "one.png");
8772                    workspace.open_path(project_path, None, true, window, cx)
8773                })
8774                .await
8775                .unwrap();
8776
8777            // Now we can check if the handle we got back errored or not
8778            assert_eq!(
8779                handle.to_any().entity_type(),
8780                TypeId::of::<TestPngItemView>()
8781            );
8782
8783            let handle = workspace
8784                .update_in(cx, |workspace, window, cx| {
8785                    let project_path = (worktree_id, "two.ipynb");
8786                    workspace.open_path(project_path, None, true, window, cx)
8787                })
8788                .await
8789                .unwrap();
8790
8791            assert_eq!(
8792                handle.to_any().entity_type(),
8793                TypeId::of::<TestIpynbItemView>()
8794            );
8795
8796            let handle = workspace
8797                .update_in(cx, |workspace, window, cx| {
8798                    let project_path = (worktree_id, "three.txt");
8799                    workspace.open_path(project_path, None, true, window, cx)
8800                })
8801                .await;
8802            assert!(handle.is_err());
8803        }
8804
8805        #[gpui::test]
8806        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8807            init_test(cx);
8808
8809            cx.update(|cx| {
8810                register_project_item::<TestPngItemView>(cx);
8811                register_project_item::<TestAlternatePngItemView>(cx);
8812            });
8813
8814            let fs = FakeFs::new(cx.executor());
8815            fs.insert_tree(
8816                "/root1",
8817                json!({
8818                    "one.png": "BINARYDATAHERE",
8819                    "two.ipynb": "{ totally a notebook }",
8820                    "three.txt": "editing text, sure why not?"
8821                }),
8822            )
8823            .await;
8824            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8825            let (workspace, cx) =
8826                cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
8827            let worktree_id = project.update(cx, |project, cx| {
8828                project.worktrees(cx).next().unwrap().read(cx).id()
8829            });
8830
8831            let handle = workspace
8832                .update_in(cx, |workspace, window, cx| {
8833                    let project_path = (worktree_id, "one.png");
8834                    workspace.open_path(project_path, None, true, window, cx)
8835                })
8836                .await
8837                .unwrap();
8838
8839            // This _must_ be the second item registered
8840            assert_eq!(
8841                handle.to_any().entity_type(),
8842                TypeId::of::<TestAlternatePngItemView>()
8843            );
8844
8845            let handle = workspace
8846                .update_in(cx, |workspace, window, cx| {
8847                    let project_path = (worktree_id, "three.txt");
8848                    workspace.open_path(project_path, None, true, window, cx)
8849                })
8850                .await;
8851            assert!(handle.is_err());
8852        }
8853    }
8854
8855    pub fn init_test(cx: &mut TestAppContext) {
8856        cx.update(|cx| {
8857            let settings_store = SettingsStore::test(cx);
8858            cx.set_global(settings_store);
8859            theme::init(theme::LoadThemes::JustBase, cx);
8860            language::init(cx);
8861            crate::init_settings(cx);
8862            Project::init_settings(cx);
8863        });
8864    }
8865
8866    fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity<TestProjectItem> {
8867        let item = TestProjectItem::new(id, path, cx);
8868        item.update(cx, |item, _| {
8869            item.is_dirty = true;
8870        });
8871        item
8872    }
8873}