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