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