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