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