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