workspace.rs

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