workspace.rs

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