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