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, 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, .. } => {
2062                self.open_path(project_path, None, true, cx)
2063            }
2064            ResolvedPath::AbsPath { path, .. } => self.open_abs_path(path, false, cx),
2065        }
2066    }
2067
2068    fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
2069        let project = self.project.read(cx);
2070        if project.is_via_collab() {
2071            self.show_error(
2072                &anyhow!("You cannot add folders to someone else's project"),
2073                cx,
2074            );
2075            return;
2076        }
2077        let paths = self.prompt_for_open_path(
2078            PathPromptOptions {
2079                files: false,
2080                directories: true,
2081                multiple: true,
2082            },
2083            DirectoryLister::Project(self.project.clone()),
2084            cx,
2085        );
2086        cx.spawn(|this, mut cx| async move {
2087            if let Some(paths) = paths.await.log_err().flatten() {
2088                let results = this
2089                    .update(&mut cx, |this, cx| {
2090                        this.open_paths(paths, OpenVisible::All, None, cx)
2091                    })?
2092                    .await;
2093                for result in results.into_iter().flatten() {
2094                    result.log_err();
2095                }
2096            }
2097            anyhow::Ok(())
2098        })
2099        .detach_and_log_err(cx);
2100    }
2101
2102    pub fn project_path_for_path(
2103        project: Model<Project>,
2104        abs_path: &Path,
2105        visible: bool,
2106        cx: &mut AppContext,
2107    ) -> Task<Result<(Model<Worktree>, ProjectPath)>> {
2108        let entry = project.update(cx, |project, cx| {
2109            project.find_or_create_worktree(abs_path, visible, cx)
2110        });
2111        cx.spawn(|mut cx| async move {
2112            let (worktree, path) = entry.await?;
2113            let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
2114            Ok((
2115                worktree,
2116                ProjectPath {
2117                    worktree_id,
2118                    path: path.into(),
2119                },
2120            ))
2121        })
2122    }
2123
2124    pub fn items<'a>(
2125        &'a self,
2126        cx: &'a AppContext,
2127    ) -> impl 'a + Iterator<Item = &'a Box<dyn ItemHandle>> {
2128        self.panes.iter().flat_map(|pane| pane.read(cx).items())
2129    }
2130
2131    pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
2132        self.items_of_type(cx).max_by_key(|item| item.item_id())
2133    }
2134
2135    pub fn items_of_type<'a, T: Item>(
2136        &'a self,
2137        cx: &'a AppContext,
2138    ) -> impl 'a + Iterator<Item = View<T>> {
2139        self.panes
2140            .iter()
2141            .flat_map(|pane| pane.read(cx).items_of_type())
2142    }
2143
2144    pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
2145        self.active_pane().read(cx).active_item()
2146    }
2147
2148    pub fn active_item_as<I: 'static>(&self, cx: &AppContext) -> Option<View<I>> {
2149        let item = self.active_item(cx)?;
2150        item.to_any().downcast::<I>().ok()
2151    }
2152
2153    fn active_project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
2154        self.active_item(cx).and_then(|item| item.project_path(cx))
2155    }
2156
2157    pub fn save_active_item(
2158        &mut self,
2159        save_intent: SaveIntent,
2160        cx: &mut WindowContext,
2161    ) -> Task<Result<()>> {
2162        let project = self.project.clone();
2163        let pane = self.active_pane();
2164        let item_ix = pane.read(cx).active_item_index();
2165        let item = pane.read(cx).active_item();
2166        let pane = pane.downgrade();
2167
2168        cx.spawn(|mut cx| async move {
2169            if let Some(item) = item {
2170                Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
2171                    .await
2172                    .map(|_| ())
2173            } else {
2174                Ok(())
2175            }
2176        })
2177    }
2178
2179    pub fn close_inactive_items_and_panes(
2180        &mut self,
2181        action: &CloseInactiveTabsAndPanes,
2182        cx: &mut ViewContext<Self>,
2183    ) {
2184        if let Some(task) =
2185            self.close_all_internal(true, action.save_intent.unwrap_or(SaveIntent::Close), cx)
2186        {
2187            task.detach_and_log_err(cx)
2188        }
2189    }
2190
2191    pub fn close_all_items_and_panes(
2192        &mut self,
2193        action: &CloseAllItemsAndPanes,
2194        cx: &mut ViewContext<Self>,
2195    ) {
2196        if let Some(task) =
2197            self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
2198        {
2199            task.detach_and_log_err(cx)
2200        }
2201    }
2202
2203    fn close_all_internal(
2204        &mut self,
2205        retain_active_pane: bool,
2206        save_intent: SaveIntent,
2207        cx: &mut ViewContext<Self>,
2208    ) -> Option<Task<Result<()>>> {
2209        let current_pane = self.active_pane();
2210
2211        let mut tasks = Vec::new();
2212
2213        if retain_active_pane {
2214            if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
2215                pane.close_inactive_items(&CloseInactiveItems { save_intent: None }, cx)
2216            }) {
2217                tasks.push(current_pane_close);
2218            };
2219        }
2220
2221        for pane in self.panes() {
2222            if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
2223                continue;
2224            }
2225
2226            if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
2227                pane.close_all_items(
2228                    &CloseAllItems {
2229                        save_intent: Some(save_intent),
2230                    },
2231                    cx,
2232                )
2233            }) {
2234                tasks.push(close_pane_items)
2235            }
2236        }
2237
2238        if tasks.is_empty() {
2239            None
2240        } else {
2241            Some(cx.spawn(|_, _| async move {
2242                for task in tasks {
2243                    task.await?
2244                }
2245                Ok(())
2246            }))
2247        }
2248    }
2249
2250    pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
2251        let dock = match dock_side {
2252            DockPosition::Left => &self.left_dock,
2253            DockPosition::Bottom => &self.bottom_dock,
2254            DockPosition::Right => &self.right_dock,
2255        };
2256        let mut focus_center = false;
2257        let mut reveal_dock = false;
2258        dock.update(cx, |dock, cx| {
2259            let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
2260            let was_visible = dock.is_open() && !other_is_zoomed;
2261            dock.set_open(!was_visible, cx);
2262
2263            if let Some(active_panel) = dock.active_panel() {
2264                if was_visible {
2265                    if active_panel.focus_handle(cx).contains_focused(cx) {
2266                        focus_center = true;
2267                    }
2268                } else {
2269                    let focus_handle = &active_panel.focus_handle(cx);
2270                    cx.focus(focus_handle);
2271                    reveal_dock = true;
2272                }
2273            }
2274        });
2275
2276        if reveal_dock {
2277            self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
2278        }
2279
2280        if focus_center {
2281            self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2282        }
2283
2284        cx.notify();
2285        self.serialize_workspace(cx);
2286    }
2287
2288    pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
2289        let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
2290
2291        for dock in docks {
2292            dock.update(cx, |dock, cx| {
2293                dock.set_open(false, cx);
2294            });
2295        }
2296
2297        cx.focus_self();
2298        cx.notify();
2299        self.serialize_workspace(cx);
2300    }
2301
2302    /// Transfer focus to the panel of the given type.
2303    pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
2304        let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
2305        panel.to_any().downcast().ok()
2306    }
2307
2308    /// Focus the panel of the given type if it isn't already focused. If it is
2309    /// already focused, then transfer focus back to the workspace center.
2310    pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2311        self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
2312            !panel.focus_handle(cx).contains_focused(cx)
2313        });
2314    }
2315
2316    pub fn activate_panel_for_proto_id(
2317        &mut self,
2318        panel_id: PanelId,
2319        cx: &mut ViewContext<Self>,
2320    ) -> Option<Arc<dyn PanelHandle>> {
2321        let mut panel = None;
2322        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2323            if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
2324                panel = dock.update(cx, |dock, cx| {
2325                    dock.activate_panel(panel_index, cx);
2326                    dock.set_open(true, cx);
2327                    dock.active_panel().cloned()
2328                });
2329                break;
2330            }
2331        }
2332
2333        if panel.is_some() {
2334            cx.notify();
2335            self.serialize_workspace(cx);
2336        }
2337
2338        panel
2339    }
2340
2341    /// Focus or unfocus the given panel type, depending on the given callback.
2342    fn focus_or_unfocus_panel<T: Panel>(
2343        &mut self,
2344        cx: &mut ViewContext<Self>,
2345        should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
2346    ) -> Option<Arc<dyn PanelHandle>> {
2347        let mut result_panel = None;
2348        let mut serialize = false;
2349        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2350            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2351                let mut focus_center = false;
2352                let panel = dock.update(cx, |dock, cx| {
2353                    dock.activate_panel(panel_index, cx);
2354
2355                    let panel = dock.active_panel().cloned();
2356                    if let Some(panel) = panel.as_ref() {
2357                        if should_focus(&**panel, cx) {
2358                            dock.set_open(true, cx);
2359                            panel.focus_handle(cx).focus(cx);
2360                        } else {
2361                            focus_center = true;
2362                        }
2363                    }
2364                    panel
2365                });
2366
2367                if focus_center {
2368                    self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2369                }
2370
2371                result_panel = panel;
2372                serialize = true;
2373                break;
2374            }
2375        }
2376
2377        if serialize {
2378            self.serialize_workspace(cx);
2379        }
2380
2381        cx.notify();
2382        result_panel
2383    }
2384
2385    /// Open the panel of the given type
2386    pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2387        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2388            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2389                dock.update(cx, |dock, cx| {
2390                    dock.activate_panel(panel_index, cx);
2391                    dock.set_open(true, cx);
2392                });
2393            }
2394        }
2395    }
2396
2397    pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
2398        [&self.left_dock, &self.bottom_dock, &self.right_dock]
2399            .iter()
2400            .find_map(|dock| dock.read(cx).panel::<T>())
2401    }
2402
2403    fn dismiss_zoomed_items_to_reveal(
2404        &mut self,
2405        dock_to_reveal: Option<DockPosition>,
2406        cx: &mut ViewContext<Self>,
2407    ) {
2408        // If a center pane is zoomed, unzoom it.
2409        for pane in &self.panes {
2410            if pane != &self.active_pane || dock_to_reveal.is_some() {
2411                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2412            }
2413        }
2414
2415        // If another dock is zoomed, hide it.
2416        let mut focus_center = false;
2417        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
2418            dock.update(cx, |dock, cx| {
2419                if Some(dock.position()) != dock_to_reveal {
2420                    if let Some(panel) = dock.active_panel() {
2421                        if panel.is_zoomed(cx) {
2422                            focus_center |= panel.focus_handle(cx).contains_focused(cx);
2423                            dock.set_open(false, cx);
2424                        }
2425                    }
2426                }
2427            });
2428        }
2429
2430        if focus_center {
2431            self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2432        }
2433
2434        if self.zoomed_position != dock_to_reveal {
2435            self.zoomed = None;
2436            self.zoomed_position = None;
2437            cx.emit(Event::ZoomChanged);
2438        }
2439
2440        cx.notify();
2441    }
2442
2443    fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2444        let pane = cx.new_view(|cx| {
2445            Pane::new(
2446                self.weak_handle(),
2447                self.project.clone(),
2448                self.pane_history_timestamp.clone(),
2449                None,
2450                NewFile.boxed_clone(),
2451                cx,
2452            )
2453        });
2454        cx.subscribe(&pane, Self::handle_pane_event).detach();
2455        self.panes.push(pane.clone());
2456        cx.focus_view(&pane);
2457        cx.emit(Event::PaneAdded(pane.clone()));
2458        pane
2459    }
2460
2461    pub fn add_item_to_center(
2462        &mut self,
2463        item: Box<dyn ItemHandle>,
2464        cx: &mut ViewContext<Self>,
2465    ) -> bool {
2466        if let Some(center_pane) = self.last_active_center_pane.clone() {
2467            if let Some(center_pane) = center_pane.upgrade() {
2468                center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
2469                true
2470            } else {
2471                false
2472            }
2473        } else {
2474            false
2475        }
2476    }
2477
2478    pub fn add_item_to_active_pane(
2479        &mut self,
2480        item: Box<dyn ItemHandle>,
2481        destination_index: Option<usize>,
2482        focus_item: bool,
2483        cx: &mut WindowContext,
2484    ) {
2485        self.add_item(
2486            self.active_pane.clone(),
2487            item,
2488            destination_index,
2489            false,
2490            focus_item,
2491            cx,
2492        )
2493    }
2494
2495    pub fn add_item(
2496        &mut self,
2497        pane: View<Pane>,
2498        item: Box<dyn ItemHandle>,
2499        destination_index: Option<usize>,
2500        activate_pane: bool,
2501        focus_item: bool,
2502        cx: &mut WindowContext,
2503    ) {
2504        if let Some(text) = item.telemetry_event_text(cx) {
2505            self.client()
2506                .telemetry()
2507                .report_app_event(format!("{}: open", text));
2508        }
2509
2510        pane.update(cx, |pane, cx| {
2511            pane.add_item(item, activate_pane, focus_item, destination_index, cx)
2512        });
2513    }
2514
2515    pub fn split_item(
2516        &mut self,
2517        split_direction: SplitDirection,
2518        item: Box<dyn ItemHandle>,
2519        cx: &mut ViewContext<Self>,
2520    ) {
2521        let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2522        self.add_item(new_pane, item, None, true, true, cx);
2523    }
2524
2525    pub fn open_abs_path(
2526        &mut self,
2527        abs_path: PathBuf,
2528        visible: bool,
2529        cx: &mut ViewContext<Self>,
2530    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2531        cx.spawn(|workspace, mut cx| async move {
2532            let open_paths_task_result = workspace
2533                .update(&mut cx, |workspace, cx| {
2534                    workspace.open_paths(
2535                        vec![abs_path.clone()],
2536                        if visible {
2537                            OpenVisible::All
2538                        } else {
2539                            OpenVisible::None
2540                        },
2541                        None,
2542                        cx,
2543                    )
2544                })
2545                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2546                .await;
2547            anyhow::ensure!(
2548                open_paths_task_result.len() == 1,
2549                "open abs path {abs_path:?} task returned incorrect number of results"
2550            );
2551            match open_paths_task_result
2552                .into_iter()
2553                .next()
2554                .expect("ensured single task result")
2555            {
2556                Some(open_result) => {
2557                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2558                }
2559                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2560            }
2561        })
2562    }
2563
2564    pub fn split_abs_path(
2565        &mut self,
2566        abs_path: PathBuf,
2567        visible: bool,
2568        cx: &mut ViewContext<Self>,
2569    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2570        let project_path_task =
2571            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2572        cx.spawn(|this, mut cx| async move {
2573            let (_, path) = project_path_task.await?;
2574            this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2575                .await
2576        })
2577    }
2578
2579    pub fn open_path(
2580        &mut self,
2581        path: impl Into<ProjectPath>,
2582        pane: Option<WeakView<Pane>>,
2583        focus_item: bool,
2584        cx: &mut WindowContext,
2585    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2586        self.open_path_preview(path, pane, focus_item, false, cx)
2587    }
2588
2589    pub fn open_path_preview(
2590        &mut self,
2591        path: impl Into<ProjectPath>,
2592        pane: Option<WeakView<Pane>>,
2593        focus_item: bool,
2594        allow_preview: bool,
2595        cx: &mut WindowContext,
2596    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2597        let pane = pane.unwrap_or_else(|| {
2598            self.last_active_center_pane.clone().unwrap_or_else(|| {
2599                self.panes
2600                    .first()
2601                    .expect("There must be an active pane")
2602                    .downgrade()
2603            })
2604        });
2605
2606        let task = self.load_path(path.into(), cx);
2607        cx.spawn(move |mut cx| async move {
2608            let (project_entry_id, build_item) = task.await?;
2609            pane.update(&mut cx, |pane, cx| {
2610                pane.open_item(project_entry_id, focus_item, allow_preview, cx, build_item)
2611            })
2612        })
2613    }
2614
2615    pub fn split_path(
2616        &mut self,
2617        path: impl Into<ProjectPath>,
2618        cx: &mut ViewContext<Self>,
2619    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2620        self.split_path_preview(path, false, cx)
2621    }
2622
2623    pub fn split_path_preview(
2624        &mut self,
2625        path: impl Into<ProjectPath>,
2626        allow_preview: bool,
2627        cx: &mut ViewContext<Self>,
2628    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2629        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2630            self.panes
2631                .first()
2632                .expect("There must be an active pane")
2633                .downgrade()
2634        });
2635
2636        if let Member::Pane(center_pane) = &self.center.root {
2637            if center_pane.read(cx).items_len() == 0 {
2638                return self.open_path(path, Some(pane), true, cx);
2639            }
2640        }
2641
2642        let task = self.load_path(path.into(), cx);
2643        cx.spawn(|this, mut cx| async move {
2644            let (project_entry_id, build_item) = task.await?;
2645            this.update(&mut cx, move |this, cx| -> Option<_> {
2646                let pane = pane.upgrade()?;
2647                let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
2648                new_pane.update(cx, |new_pane, cx| {
2649                    Some(new_pane.open_item(project_entry_id, true, allow_preview, cx, build_item))
2650                })
2651            })
2652            .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2653        })
2654    }
2655
2656    fn load_path(
2657        &mut self,
2658        path: ProjectPath,
2659        cx: &mut WindowContext,
2660    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2661        let project = self.project().clone();
2662        let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2663        let Some(open_project_item) = project_item_builders
2664            .iter()
2665            .rev()
2666            .find_map(|open_project_item| open_project_item(&project, &path, cx))
2667        else {
2668            return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2669        };
2670        open_project_item
2671    }
2672
2673    pub fn find_project_item<T>(
2674        &self,
2675        pane: &View<Pane>,
2676        project_item: &Model<T::Item>,
2677        cx: &AppContext,
2678    ) -> Option<View<T>>
2679    where
2680        T: ProjectItem,
2681    {
2682        use project::Item as _;
2683        let project_item = project_item.read(cx);
2684        let entry_id = project_item.entry_id(cx);
2685        let project_path = project_item.project_path(cx);
2686
2687        let mut item = None;
2688        if let Some(entry_id) = entry_id {
2689            item = pane.read(cx).item_for_entry(entry_id, cx);
2690        }
2691        if item.is_none() {
2692            if let Some(project_path) = project_path {
2693                item = pane.read(cx).item_for_path(project_path, cx);
2694            }
2695        }
2696
2697        item.and_then(|item| item.downcast::<T>())
2698    }
2699
2700    pub fn is_project_item_open<T>(
2701        &self,
2702        pane: &View<Pane>,
2703        project_item: &Model<T::Item>,
2704        cx: &AppContext,
2705    ) -> bool
2706    where
2707        T: ProjectItem,
2708    {
2709        self.find_project_item::<T>(pane, project_item, cx)
2710            .is_some()
2711    }
2712
2713    pub fn open_project_item<T>(
2714        &mut self,
2715        pane: View<Pane>,
2716        project_item: Model<T::Item>,
2717        activate_pane: bool,
2718        focus_item: bool,
2719        cx: &mut ViewContext<Self>,
2720    ) -> View<T>
2721    where
2722        T: ProjectItem,
2723    {
2724        if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
2725            self.activate_item(&item, activate_pane, focus_item, cx);
2726            return item;
2727        }
2728
2729        let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2730        let item_id = item.item_id();
2731        let mut destination_index = None;
2732        pane.update(cx, |pane, cx| {
2733            if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
2734                if let Some(preview_item_id) = pane.preview_item_id() {
2735                    if preview_item_id != item_id {
2736                        destination_index = pane.close_current_preview_item(cx);
2737                    }
2738                }
2739            }
2740            pane.set_preview_item_id(Some(item.item_id()), cx)
2741        });
2742
2743        self.add_item(
2744            pane,
2745            Box::new(item.clone()),
2746            destination_index,
2747            activate_pane,
2748            focus_item,
2749            cx,
2750        );
2751        item
2752    }
2753
2754    pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2755        if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2756            self.active_pane.update(cx, |pane, cx| {
2757                pane.add_item(Box::new(shared_screen), false, true, None, cx)
2758            });
2759        }
2760    }
2761
2762    pub fn activate_item(
2763        &mut self,
2764        item: &dyn ItemHandle,
2765        activate_pane: bool,
2766        focus_item: bool,
2767        cx: &mut WindowContext,
2768    ) -> bool {
2769        let result = self.panes.iter().find_map(|pane| {
2770            pane.read(cx)
2771                .index_for_item(item)
2772                .map(|ix| (pane.clone(), ix))
2773        });
2774        if let Some((pane, ix)) = result {
2775            pane.update(cx, |pane, cx| {
2776                pane.activate_item(ix, activate_pane, focus_item, cx)
2777            });
2778            true
2779        } else {
2780            false
2781        }
2782    }
2783
2784    fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2785        let panes = self.center.panes();
2786        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2787            cx.focus_view(&pane);
2788        } else {
2789            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2790        }
2791    }
2792
2793    pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2794        let panes = self.center.panes();
2795        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2796            let next_ix = (ix + 1) % panes.len();
2797            let next_pane = panes[next_ix].clone();
2798            cx.focus_view(&next_pane);
2799        }
2800    }
2801
2802    pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2803        let panes = self.center.panes();
2804        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2805            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2806            let prev_pane = panes[prev_ix].clone();
2807            cx.focus_view(&prev_pane);
2808        }
2809    }
2810
2811    pub fn activate_pane_in_direction(
2812        &mut self,
2813        direction: SplitDirection,
2814        cx: &mut WindowContext,
2815    ) {
2816        use ActivateInDirectionTarget as Target;
2817        enum Origin {
2818            LeftDock,
2819            RightDock,
2820            BottomDock,
2821            Center,
2822        }
2823
2824        let origin: Origin = [
2825            (&self.left_dock, Origin::LeftDock),
2826            (&self.right_dock, Origin::RightDock),
2827            (&self.bottom_dock, Origin::BottomDock),
2828        ]
2829        .into_iter()
2830        .find_map(|(dock, origin)| {
2831            if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2832                Some(origin)
2833            } else {
2834                None
2835            }
2836        })
2837        .unwrap_or(Origin::Center);
2838
2839        let get_last_active_pane = || {
2840            let pane = self
2841                .last_active_center_pane
2842                .clone()
2843                .unwrap_or_else(|| {
2844                    self.panes
2845                        .first()
2846                        .expect("There must be an active pane")
2847                        .downgrade()
2848                })
2849                .upgrade()?;
2850            (pane.read(cx).items_len() != 0).then_some(pane)
2851        };
2852
2853        let try_dock =
2854            |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2855
2856        let target = match (origin, direction) {
2857            // We're in the center, so we first try to go to a different pane,
2858            // otherwise try to go to a dock.
2859            (Origin::Center, direction) => {
2860                if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2861                    Some(Target::Pane(pane))
2862                } else {
2863                    match direction {
2864                        SplitDirection::Up => None,
2865                        SplitDirection::Down => try_dock(&self.bottom_dock),
2866                        SplitDirection::Left => try_dock(&self.left_dock),
2867                        SplitDirection::Right => try_dock(&self.right_dock),
2868                    }
2869                }
2870            }
2871
2872            (Origin::LeftDock, SplitDirection::Right) => {
2873                if let Some(last_active_pane) = get_last_active_pane() {
2874                    Some(Target::Pane(last_active_pane))
2875                } else {
2876                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2877                }
2878            }
2879
2880            (Origin::LeftDock, SplitDirection::Down)
2881            | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
2882
2883            (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
2884            (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
2885            (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
2886
2887            (Origin::RightDock, SplitDirection::Left) => {
2888                if let Some(last_active_pane) = get_last_active_pane() {
2889                    Some(Target::Pane(last_active_pane))
2890                } else {
2891                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
2892                }
2893            }
2894
2895            _ => None,
2896        };
2897
2898        match target {
2899            Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
2900            Some(ActivateInDirectionTarget::Dock(dock)) => {
2901                if let Some(panel) = dock.read(cx).active_panel() {
2902                    panel.focus_handle(cx).focus(cx);
2903                } else {
2904                    log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.read(cx).position());
2905                }
2906            }
2907            None => {}
2908        }
2909    }
2910
2911    pub fn find_pane_in_direction(
2912        &mut self,
2913        direction: SplitDirection,
2914        cx: &WindowContext,
2915    ) -> Option<View<Pane>> {
2916        let bounding_box = self.center.bounding_box_for_pane(&self.active_pane)?;
2917        let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
2918        let center = match cursor {
2919            Some(cursor) if bounding_box.contains(&cursor) => cursor,
2920            _ => bounding_box.center(),
2921        };
2922
2923        let distance_to_next = pane_group::HANDLE_HITBOX_SIZE;
2924
2925        let target = match direction {
2926            SplitDirection::Left => {
2927                Point::new(bounding_box.left() - distance_to_next.into(), center.y)
2928            }
2929            SplitDirection::Right => {
2930                Point::new(bounding_box.right() + distance_to_next.into(), center.y)
2931            }
2932            SplitDirection::Up => {
2933                Point::new(center.x, bounding_box.top() - distance_to_next.into())
2934            }
2935            SplitDirection::Down => {
2936                Point::new(center.x, bounding_box.bottom() + distance_to_next.into())
2937            }
2938        };
2939        self.center.pane_at_pixel_position(target).cloned()
2940    }
2941
2942    pub fn swap_pane_in_direction(
2943        &mut self,
2944        direction: SplitDirection,
2945        cx: &mut ViewContext<Self>,
2946    ) {
2947        if let Some(to) = self.find_pane_in_direction(direction, cx) {
2948            self.center.swap(&self.active_pane.clone(), &to);
2949            cx.notify();
2950        }
2951    }
2952
2953    fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
2954        // This is explicitly hoisted out of the following check for pane identity as
2955        // terminal panel panes are not registered as a center panes.
2956        self.status_bar.update(cx, |status_bar, cx| {
2957            status_bar.set_active_pane(&pane, cx);
2958        });
2959        if self.active_pane != pane {
2960            self.set_active_pane(&pane, cx);
2961        }
2962
2963        if self.last_active_center_pane.is_none() {
2964            self.last_active_center_pane = Some(pane.downgrade());
2965        }
2966
2967        self.dismiss_zoomed_items_to_reveal(None, cx);
2968        if pane.read(cx).is_zoomed() {
2969            self.zoomed = Some(pane.downgrade().into());
2970        } else {
2971            self.zoomed = None;
2972        }
2973        self.zoomed_position = None;
2974        cx.emit(Event::ZoomChanged);
2975        self.update_active_view_for_followers(cx);
2976        pane.model.update(cx, |pane, _| {
2977            pane.track_alternate_file_items();
2978        });
2979
2980        cx.notify();
2981    }
2982
2983    fn set_active_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) {
2984        self.active_pane = pane.clone();
2985        self.active_item_path_changed(cx);
2986        self.last_active_center_pane = Some(pane.downgrade());
2987    }
2988
2989    fn handle_panel_focused(&mut self, cx: &mut ViewContext<Self>) {
2990        self.update_active_view_for_followers(cx);
2991    }
2992
2993    fn handle_pane_event(
2994        &mut self,
2995        pane: View<Pane>,
2996        event: &pane::Event,
2997        cx: &mut ViewContext<Self>,
2998    ) {
2999        match event {
3000            pane::Event::AddItem { item } => {
3001                item.added_to_pane(self, pane, cx);
3002                cx.emit(Event::ItemAdded {
3003                    item: item.boxed_clone(),
3004                });
3005            }
3006            pane::Event::Split(direction) => {
3007                self.split_and_clone(pane, *direction, cx);
3008            }
3009            pane::Event::JoinIntoNext => self.join_pane_into_next(pane, cx),
3010            pane::Event::JoinAll => self.join_all_panes(cx),
3011            pane::Event::Remove { focus_on_pane } => {
3012                self.remove_pane(pane, focus_on_pane.clone(), cx)
3013            }
3014            pane::Event::ActivateItem { local } => {
3015                cx.on_next_frame(|_, cx| {
3016                    cx.invalidate_character_coordinates();
3017                });
3018
3019                pane.model.update(cx, |pane, _| {
3020                    pane.track_alternate_file_items();
3021                });
3022                if *local {
3023                    self.unfollow_in_pane(&pane, cx);
3024                }
3025                if &pane == self.active_pane() {
3026                    self.active_item_path_changed(cx);
3027                    self.update_active_view_for_followers(cx);
3028                }
3029            }
3030            pane::Event::UserSavedItem { item, save_intent } => cx.emit(Event::UserSavedItem {
3031                pane: pane.downgrade(),
3032                item: item.boxed_clone(),
3033                save_intent: *save_intent,
3034            }),
3035            pane::Event::ChangeItemTitle => {
3036                if pane == self.active_pane {
3037                    self.active_item_path_changed(cx);
3038                }
3039                self.update_window_edited(cx);
3040            }
3041            pane::Event::RemoveItem { .. } => {}
3042            pane::Event::RemovedItem { item_id } => {
3043                cx.emit(Event::ActiveItemChanged);
3044                self.update_window_edited(cx);
3045                if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3046                    if entry.get().entity_id() == pane.entity_id() {
3047                        entry.remove();
3048                    }
3049                }
3050            }
3051            pane::Event::Focus => {
3052                cx.on_next_frame(|_, cx| {
3053                    cx.invalidate_character_coordinates();
3054                });
3055                self.handle_pane_focused(pane.clone(), cx);
3056            }
3057            pane::Event::ZoomIn => {
3058                if pane == self.active_pane {
3059                    pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3060                    if pane.read(cx).has_focus(cx) {
3061                        self.zoomed = Some(pane.downgrade().into());
3062                        self.zoomed_position = None;
3063                        cx.emit(Event::ZoomChanged);
3064                    }
3065                    cx.notify();
3066                }
3067            }
3068            pane::Event::ZoomOut => {
3069                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3070                if self.zoomed_position.is_none() {
3071                    self.zoomed = None;
3072                    cx.emit(Event::ZoomChanged);
3073                }
3074                cx.notify();
3075            }
3076        }
3077
3078        self.serialize_workspace(cx);
3079    }
3080
3081    pub fn unfollow_in_pane(
3082        &mut self,
3083        pane: &View<Pane>,
3084        cx: &mut ViewContext<Workspace>,
3085    ) -> Option<PeerId> {
3086        let leader_id = self.leader_for_pane(pane)?;
3087        self.unfollow(leader_id, cx);
3088        Some(leader_id)
3089    }
3090
3091    pub fn split_pane(
3092        &mut self,
3093        pane_to_split: View<Pane>,
3094        split_direction: SplitDirection,
3095        cx: &mut ViewContext<Self>,
3096    ) -> View<Pane> {
3097        let new_pane = self.add_pane(cx);
3098        self.center
3099            .split(&pane_to_split, &new_pane, split_direction)
3100            .unwrap();
3101        cx.notify();
3102        new_pane
3103    }
3104
3105    pub fn split_and_clone(
3106        &mut self,
3107        pane: View<Pane>,
3108        direction: SplitDirection,
3109        cx: &mut ViewContext<Self>,
3110    ) -> Option<View<Pane>> {
3111        let item = pane.read(cx).active_item()?;
3112        let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
3113            let new_pane = self.add_pane(cx);
3114            new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
3115            self.center.split(&pane, &new_pane, direction).unwrap();
3116            Some(new_pane)
3117        } else {
3118            None
3119        };
3120        cx.notify();
3121        maybe_pane_handle
3122    }
3123
3124    pub fn split_pane_with_item(
3125        &mut self,
3126        pane_to_split: WeakView<Pane>,
3127        split_direction: SplitDirection,
3128        from: WeakView<Pane>,
3129        item_id_to_move: EntityId,
3130        cx: &mut ViewContext<Self>,
3131    ) {
3132        let Some(pane_to_split) = pane_to_split.upgrade() else {
3133            return;
3134        };
3135        let Some(from) = from.upgrade() else {
3136            return;
3137        };
3138
3139        let new_pane = self.add_pane(cx);
3140        move_item(&from, &new_pane, item_id_to_move, 0, cx);
3141        self.center
3142            .split(&pane_to_split, &new_pane, split_direction)
3143            .unwrap();
3144        cx.notify();
3145    }
3146
3147    pub fn split_pane_with_project_entry(
3148        &mut self,
3149        pane_to_split: WeakView<Pane>,
3150        split_direction: SplitDirection,
3151        project_entry: ProjectEntryId,
3152        cx: &mut ViewContext<Self>,
3153    ) -> Option<Task<Result<()>>> {
3154        let pane_to_split = pane_to_split.upgrade()?;
3155        let new_pane = self.add_pane(cx);
3156        self.center
3157            .split(&pane_to_split, &new_pane, split_direction)
3158            .unwrap();
3159
3160        let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3161        let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
3162        Some(cx.foreground_executor().spawn(async move {
3163            task.await?;
3164            Ok(())
3165        }))
3166    }
3167
3168    pub fn join_all_panes(&mut self, cx: &mut ViewContext<Self>) {
3169        let active_item = self.active_pane.read(cx).active_item();
3170        for pane in &self.panes {
3171            join_pane_into_active(&self.active_pane, pane, cx);
3172        }
3173        if let Some(active_item) = active_item {
3174            self.activate_item(active_item.as_ref(), true, true, cx);
3175        }
3176        cx.notify();
3177    }
3178
3179    pub fn join_pane_into_next(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3180        let next_pane = self
3181            .find_pane_in_direction(SplitDirection::Right, cx)
3182            .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3183            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3184            .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3185        let Some(next_pane) = next_pane else {
3186            return;
3187        };
3188        move_all_items(&pane, &next_pane, cx);
3189        cx.notify();
3190    }
3191
3192    fn remove_pane(
3193        &mut self,
3194        pane: View<Pane>,
3195        focus_on: Option<View<Pane>>,
3196        cx: &mut ViewContext<Self>,
3197    ) {
3198        if self.center.remove(&pane).unwrap() {
3199            self.force_remove_pane(&pane, &focus_on, cx);
3200            self.unfollow_in_pane(&pane, cx);
3201            self.last_leaders_by_pane.remove(&pane.downgrade());
3202            for removed_item in pane.read(cx).items() {
3203                self.panes_by_item.remove(&removed_item.item_id());
3204            }
3205
3206            cx.notify();
3207        } else {
3208            self.active_item_path_changed(cx);
3209        }
3210        cx.emit(Event::PaneRemoved);
3211    }
3212
3213    pub fn panes(&self) -> &[View<Pane>] {
3214        &self.panes
3215    }
3216
3217    pub fn active_pane(&self) -> &View<Pane> {
3218        &self.active_pane
3219    }
3220
3221    pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
3222        self.find_pane_in_direction(SplitDirection::Right, cx)
3223            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3224            .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
3225            .clone()
3226    }
3227
3228    pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
3229        let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3230        weak_pane.upgrade()
3231    }
3232
3233    fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
3234        self.follower_states.retain(|leader_id, state| {
3235            if *leader_id == peer_id {
3236                for item in state.items_by_leader_view_id.values() {
3237                    item.view.set_leader_peer_id(None, cx);
3238                }
3239                false
3240            } else {
3241                true
3242            }
3243        });
3244        cx.notify();
3245    }
3246
3247    pub fn start_following(
3248        &mut self,
3249        leader_id: PeerId,
3250        cx: &mut ViewContext<Self>,
3251    ) -> Option<Task<Result<()>>> {
3252        let pane = self.active_pane().clone();
3253
3254        self.last_leaders_by_pane
3255            .insert(pane.downgrade(), leader_id);
3256        self.unfollow(leader_id, cx);
3257        self.unfollow_in_pane(&pane, cx);
3258        self.follower_states.insert(
3259            leader_id,
3260            FollowerState {
3261                center_pane: pane.clone(),
3262                dock_pane: None,
3263                active_view_id: None,
3264                items_by_leader_view_id: Default::default(),
3265            },
3266        );
3267        cx.notify();
3268
3269        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3270        let project_id = self.project.read(cx).remote_id();
3271        let request = self.app_state.client.request(proto::Follow {
3272            room_id,
3273            project_id,
3274            leader_id: Some(leader_id),
3275        });
3276
3277        Some(cx.spawn(|this, mut cx| async move {
3278            let response = request.await?;
3279            this.update(&mut cx, |this, _| {
3280                let state = this
3281                    .follower_states
3282                    .get_mut(&leader_id)
3283                    .ok_or_else(|| anyhow!("following interrupted"))?;
3284                state.active_view_id = response
3285                    .active_view
3286                    .as_ref()
3287                    .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3288                Ok::<_, anyhow::Error>(())
3289            })??;
3290            if let Some(view) = response.active_view {
3291                Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3292            }
3293            this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
3294            Ok(())
3295        }))
3296    }
3297
3298    pub fn follow_next_collaborator(
3299        &mut self,
3300        _: &FollowNextCollaborator,
3301        cx: &mut ViewContext<Self>,
3302    ) {
3303        let collaborators = self.project.read(cx).collaborators();
3304        let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3305            let mut collaborators = collaborators.keys().copied();
3306            for peer_id in collaborators.by_ref() {
3307                if peer_id == leader_id {
3308                    break;
3309                }
3310            }
3311            collaborators.next()
3312        } else if let Some(last_leader_id) =
3313            self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3314        {
3315            if collaborators.contains_key(last_leader_id) {
3316                Some(*last_leader_id)
3317            } else {
3318                None
3319            }
3320        } else {
3321            None
3322        };
3323
3324        let pane = self.active_pane.clone();
3325        let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3326        else {
3327            return;
3328        };
3329        if self.unfollow_in_pane(&pane, cx) == Some(leader_id) {
3330            return;
3331        }
3332        if let Some(task) = self.start_following(leader_id, cx) {
3333            task.detach_and_log_err(cx)
3334        }
3335    }
3336
3337    pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
3338        let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3339            return;
3340        };
3341        let room = room.read(cx);
3342        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3343            return;
3344        };
3345
3346        let project = self.project.read(cx);
3347
3348        let other_project_id = match remote_participant.location {
3349            call::ParticipantLocation::External => None,
3350            call::ParticipantLocation::UnsharedProject => None,
3351            call::ParticipantLocation::SharedProject { project_id } => {
3352                if Some(project_id) == project.remote_id() {
3353                    None
3354                } else {
3355                    Some(project_id)
3356                }
3357            }
3358        };
3359
3360        // if they are active in another project, follow there.
3361        if let Some(project_id) = other_project_id {
3362            let app_state = self.app_state.clone();
3363            crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3364                .detach_and_log_err(cx);
3365        }
3366
3367        // if you're already following, find the right pane and focus it.
3368        if let Some(follower_state) = self.follower_states.get(&leader_id) {
3369            cx.focus_view(follower_state.pane());
3370            return;
3371        }
3372
3373        // Otherwise, follow.
3374        if let Some(task) = self.start_following(leader_id, cx) {
3375            task.detach_and_log_err(cx)
3376        }
3377    }
3378
3379    pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3380        cx.notify();
3381        let state = self.follower_states.remove(&leader_id)?;
3382        for (_, item) in state.items_by_leader_view_id {
3383            item.view.set_leader_peer_id(None, cx);
3384        }
3385
3386        let project_id = self.project.read(cx).remote_id();
3387        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3388        self.app_state
3389            .client
3390            .send(proto::Unfollow {
3391                room_id,
3392                project_id,
3393                leader_id: Some(leader_id),
3394            })
3395            .log_err();
3396
3397        Some(())
3398    }
3399
3400    pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3401        self.follower_states.contains_key(&peer_id)
3402    }
3403
3404    fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
3405        cx.emit(Event::ActiveItemChanged);
3406        let active_entry = self.active_project_path(cx);
3407        self.project
3408            .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3409
3410        self.update_window_title(cx);
3411    }
3412
3413    fn update_window_title(&mut self, cx: &mut WindowContext) {
3414        let project = self.project().read(cx);
3415        let mut title = String::new();
3416
3417        if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3418            let filename = path
3419                .path
3420                .file_name()
3421                .map(|s| s.to_string_lossy())
3422                .or_else(|| {
3423                    Some(Cow::Borrowed(
3424                        project
3425                            .worktree_for_id(path.worktree_id, cx)?
3426                            .read(cx)
3427                            .root_name(),
3428                    ))
3429                });
3430
3431            if let Some(filename) = filename {
3432                title.push_str(filename.as_ref());
3433                title.push_str("");
3434            }
3435        }
3436
3437        for (i, name) in project.worktree_root_names(cx).enumerate() {
3438            if i > 0 {
3439                title.push_str(", ");
3440            }
3441            title.push_str(name);
3442        }
3443
3444        if title.is_empty() {
3445            title = "empty project".to_string();
3446        }
3447
3448        if project.is_via_collab() {
3449            title.push_str("");
3450        } else if project.is_shared() {
3451            title.push_str("");
3452        }
3453
3454        cx.set_window_title(&title);
3455    }
3456
3457    fn update_window_edited(&mut self, cx: &mut WindowContext) {
3458        let is_edited = !self.project.read(cx).is_disconnected(cx)
3459            && self
3460                .items(cx)
3461                .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3462        if is_edited != self.window_edited {
3463            self.window_edited = is_edited;
3464            cx.set_window_edited(self.window_edited)
3465        }
3466    }
3467
3468    fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
3469        if self.notifications.is_empty() {
3470            None
3471        } else {
3472            Some(
3473                div()
3474                    .absolute()
3475                    .right_3()
3476                    .bottom_3()
3477                    .w_112()
3478                    .h_full()
3479                    .flex()
3480                    .flex_col()
3481                    .justify_end()
3482                    .gap_2()
3483                    .children(
3484                        self.notifications
3485                            .iter()
3486                            .map(|(_, notification)| notification.to_any()),
3487                    ),
3488            )
3489        }
3490    }
3491
3492    // RPC handlers
3493
3494    fn active_view_for_follower(
3495        &self,
3496        follower_project_id: Option<u64>,
3497        cx: &mut ViewContext<Self>,
3498    ) -> Option<proto::View> {
3499        let (item, panel_id) = self.active_item_for_followers(cx);
3500        let item = item?;
3501        let leader_id = self
3502            .pane_for(&*item)
3503            .and_then(|pane| self.leader_for_pane(&pane));
3504
3505        let item_handle = item.to_followable_item_handle(cx)?;
3506        let id = item_handle.remote_id(&self.app_state.client, cx)?;
3507        let variant = item_handle.to_state_proto(cx)?;
3508
3509        if item_handle.is_project_item(cx)
3510            && (follower_project_id.is_none()
3511                || follower_project_id != self.project.read(cx).remote_id())
3512        {
3513            return None;
3514        }
3515
3516        Some(proto::View {
3517            id: Some(id.to_proto()),
3518            leader_id,
3519            variant: Some(variant),
3520            panel_id: panel_id.map(|id| id as i32),
3521        })
3522    }
3523
3524    fn handle_follow(
3525        &mut self,
3526        follower_project_id: Option<u64>,
3527        cx: &mut ViewContext<Self>,
3528    ) -> proto::FollowResponse {
3529        let active_view = self.active_view_for_follower(follower_project_id, cx);
3530
3531        cx.notify();
3532        proto::FollowResponse {
3533            // TODO: Remove after version 0.145.x stabilizes.
3534            active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3535            views: active_view.iter().cloned().collect(),
3536            active_view,
3537        }
3538    }
3539
3540    fn handle_update_followers(
3541        &mut self,
3542        leader_id: PeerId,
3543        message: proto::UpdateFollowers,
3544        _cx: &mut ViewContext<Self>,
3545    ) {
3546        self.leader_updates_tx
3547            .unbounded_send((leader_id, message))
3548            .ok();
3549    }
3550
3551    async fn process_leader_update(
3552        this: &WeakView<Self>,
3553        leader_id: PeerId,
3554        update: proto::UpdateFollowers,
3555        cx: &mut AsyncWindowContext,
3556    ) -> Result<()> {
3557        match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3558            proto::update_followers::Variant::CreateView(view) => {
3559                let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3560                let should_add_view = this.update(cx, |this, _| {
3561                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3562                        anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3563                    } else {
3564                        anyhow::Ok(false)
3565                    }
3566                })??;
3567
3568                if should_add_view {
3569                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3570                }
3571            }
3572            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3573                let should_add_view = this.update(cx, |this, _| {
3574                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3575                        state.active_view_id = update_active_view
3576                            .view
3577                            .as_ref()
3578                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3579
3580                        if state.active_view_id.is_some_and(|view_id| {
3581                            !state.items_by_leader_view_id.contains_key(&view_id)
3582                        }) {
3583                            anyhow::Ok(true)
3584                        } else {
3585                            anyhow::Ok(false)
3586                        }
3587                    } else {
3588                        anyhow::Ok(false)
3589                    }
3590                })??;
3591
3592                if should_add_view {
3593                    if let Some(view) = update_active_view.view {
3594                        Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3595                    }
3596                }
3597            }
3598            proto::update_followers::Variant::UpdateView(update_view) => {
3599                let variant = update_view
3600                    .variant
3601                    .ok_or_else(|| anyhow!("missing update view variant"))?;
3602                let id = update_view
3603                    .id
3604                    .ok_or_else(|| anyhow!("missing update view id"))?;
3605                let mut tasks = Vec::new();
3606                this.update(cx, |this, cx| {
3607                    let project = this.project.clone();
3608                    if let Some(state) = this.follower_states.get(&leader_id) {
3609                        let view_id = ViewId::from_proto(id.clone())?;
3610                        if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3611                            tasks.push(item.view.apply_update_proto(&project, variant.clone(), cx));
3612                        }
3613                    }
3614                    anyhow::Ok(())
3615                })??;
3616                try_join_all(tasks).await.log_err();
3617            }
3618        }
3619        this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3620        Ok(())
3621    }
3622
3623    async fn add_view_from_leader(
3624        this: WeakView<Self>,
3625        leader_id: PeerId,
3626        view: &proto::View,
3627        cx: &mut AsyncWindowContext,
3628    ) -> Result<()> {
3629        let this = this.upgrade().context("workspace dropped")?;
3630
3631        let Some(id) = view.id.clone() else {
3632            return Err(anyhow!("no id for view"));
3633        };
3634        let id = ViewId::from_proto(id)?;
3635        let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
3636
3637        let pane = this.update(cx, |this, _cx| {
3638            let state = this
3639                .follower_states
3640                .get(&leader_id)
3641                .context("stopped following")?;
3642            anyhow::Ok(state.pane().clone())
3643        })??;
3644        let existing_item = pane.update(cx, |pane, cx| {
3645            let client = this.read(cx).client().clone();
3646            pane.items().find_map(|item| {
3647                let item = item.to_followable_item_handle(cx)?;
3648                if item.remote_id(&client, cx) == Some(id) {
3649                    Some(item)
3650                } else {
3651                    None
3652                }
3653            })
3654        })?;
3655        let item = if let Some(existing_item) = existing_item {
3656            existing_item
3657        } else {
3658            let variant = view.variant.clone();
3659            if variant.is_none() {
3660                Err(anyhow!("missing view variant"))?;
3661            }
3662
3663            let task = cx.update(|cx| {
3664                FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
3665            })?;
3666
3667            let Some(task) = task else {
3668                return Err(anyhow!(
3669                    "failed to construct view from leader (maybe from a different version of zed?)"
3670                ));
3671            };
3672
3673            let mut new_item = task.await?;
3674            pane.update(cx, |pane, cx| {
3675                let mut item_ix_to_remove = None;
3676                for (ix, item) in pane.items().enumerate() {
3677                    if let Some(item) = item.to_followable_item_handle(cx) {
3678                        match new_item.dedup(item.as_ref(), cx) {
3679                            Some(item::Dedup::KeepExisting) => {
3680                                new_item =
3681                                    item.boxed_clone().to_followable_item_handle(cx).unwrap();
3682                                break;
3683                            }
3684                            Some(item::Dedup::ReplaceExisting) => {
3685                                item_ix_to_remove = Some(ix);
3686                                break;
3687                            }
3688                            None => {}
3689                        }
3690                    }
3691                }
3692
3693                if let Some(ix) = item_ix_to_remove {
3694                    pane.remove_item(ix, false, false, cx);
3695                    pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx);
3696                }
3697            })?;
3698
3699            new_item
3700        };
3701
3702        this.update(cx, |this, cx| {
3703            let state = this.follower_states.get_mut(&leader_id)?;
3704            item.set_leader_peer_id(Some(leader_id), cx);
3705            state.items_by_leader_view_id.insert(
3706                id,
3707                FollowerView {
3708                    view: item,
3709                    location: panel_id,
3710                },
3711            );
3712
3713            Some(())
3714        })?;
3715
3716        Ok(())
3717    }
3718
3719    pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3720        let mut is_project_item = true;
3721        let mut update = proto::UpdateActiveView::default();
3722        if cx.is_window_active() {
3723            let (active_item, panel_id) = self.active_item_for_followers(cx);
3724
3725            if let Some(item) = active_item {
3726                if item.focus_handle(cx).contains_focused(cx) {
3727                    let leader_id = self
3728                        .pane_for(&*item)
3729                        .and_then(|pane| self.leader_for_pane(&pane));
3730
3731                    if let Some(item) = item.to_followable_item_handle(cx) {
3732                        let id = item
3733                            .remote_id(&self.app_state.client, cx)
3734                            .map(|id| id.to_proto());
3735
3736                        if let Some(id) = id.clone() {
3737                            if let Some(variant) = item.to_state_proto(cx) {
3738                                let view = Some(proto::View {
3739                                    id: Some(id.clone()),
3740                                    leader_id,
3741                                    variant: Some(variant),
3742                                    panel_id: panel_id.map(|id| id as i32),
3743                                });
3744
3745                                is_project_item = item.is_project_item(cx);
3746                                update = proto::UpdateActiveView {
3747                                    view,
3748                                    // TODO: Remove after version 0.145.x stabilizes.
3749                                    id: Some(id.clone()),
3750                                    leader_id,
3751                                };
3752                            }
3753                        };
3754                    }
3755                }
3756            }
3757        }
3758
3759        let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
3760        if active_view_id != self.last_active_view_id.as_ref() {
3761            self.last_active_view_id = active_view_id.cloned();
3762            self.update_followers(
3763                is_project_item,
3764                proto::update_followers::Variant::UpdateActiveView(update),
3765                cx,
3766            );
3767        }
3768    }
3769
3770    fn active_item_for_followers(
3771        &self,
3772        cx: &mut WindowContext,
3773    ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
3774        let mut active_item = None;
3775        let mut panel_id = None;
3776        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3777            if dock.focus_handle(cx).contains_focused(cx) {
3778                if let Some(panel) = dock.read(cx).active_panel() {
3779                    if let Some(pane) = panel.pane(cx) {
3780                        if let Some(item) = pane.read(cx).active_item() {
3781                            active_item = Some(item);
3782                            panel_id = panel.remote_id();
3783                            break;
3784                        }
3785                    }
3786                }
3787            }
3788        }
3789
3790        if active_item.is_none() {
3791            active_item = self.active_pane().read(cx).active_item();
3792        }
3793        (active_item, panel_id)
3794    }
3795
3796    fn update_followers(
3797        &self,
3798        project_only: bool,
3799        update: proto::update_followers::Variant,
3800        cx: &mut WindowContext,
3801    ) -> Option<()> {
3802        // If this update only applies to for followers in the current project,
3803        // then skip it unless this project is shared. If it applies to all
3804        // followers, regardless of project, then set `project_id` to none,
3805        // indicating that it goes to all followers.
3806        let project_id = if project_only {
3807            Some(self.project.read(cx).remote_id()?)
3808        } else {
3809            None
3810        };
3811        self.app_state().workspace_store.update(cx, |store, cx| {
3812            store.update_followers(project_id, update, cx)
3813        })
3814    }
3815
3816    pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3817        self.follower_states.iter().find_map(|(leader_id, state)| {
3818            if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
3819                Some(*leader_id)
3820            } else {
3821                None
3822            }
3823        })
3824    }
3825
3826    fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3827        cx.notify();
3828
3829        let call = self.active_call()?;
3830        let room = call.read(cx).room()?.read(cx);
3831        let participant = room.remote_participant_for_peer_id(leader_id)?;
3832
3833        let leader_in_this_app;
3834        let leader_in_this_project;
3835        match participant.location {
3836            call::ParticipantLocation::SharedProject { project_id } => {
3837                leader_in_this_app = true;
3838                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3839            }
3840            call::ParticipantLocation::UnsharedProject => {
3841                leader_in_this_app = true;
3842                leader_in_this_project = false;
3843            }
3844            call::ParticipantLocation::External => {
3845                leader_in_this_app = false;
3846                leader_in_this_project = false;
3847            }
3848        };
3849
3850        let state = self.follower_states.get(&leader_id)?;
3851        let mut item_to_activate = None;
3852        if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3853            if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3854                if leader_in_this_project || !item.view.is_project_item(cx) {
3855                    item_to_activate = Some((item.location, item.view.boxed_clone()));
3856                }
3857            }
3858        } else if let Some(shared_screen) =
3859            self.shared_screen_for_peer(leader_id, &state.center_pane, cx)
3860        {
3861            item_to_activate = Some((None, Box::new(shared_screen)));
3862        }
3863
3864        let (panel_id, item) = item_to_activate?;
3865
3866        let mut transfer_focus = state.center_pane.read(cx).has_focus(cx);
3867        let pane;
3868        if let Some(panel_id) = panel_id {
3869            pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?;
3870            let state = self.follower_states.get_mut(&leader_id)?;
3871            state.dock_pane = Some(pane.clone());
3872        } else {
3873            pane = state.center_pane.clone();
3874            let state = self.follower_states.get_mut(&leader_id)?;
3875            if let Some(dock_pane) = state.dock_pane.take() {
3876                transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx);
3877            }
3878        }
3879
3880        pane.update(cx, |pane, cx| {
3881            let focus_active_item = pane.has_focus(cx) || transfer_focus;
3882            if let Some(index) = pane.index_for_item(item.as_ref()) {
3883                pane.activate_item(index, false, false, cx);
3884            } else {
3885                pane.add_item(item.boxed_clone(), false, false, None, cx)
3886            }
3887
3888            if focus_active_item {
3889                pane.focus_active_item(cx)
3890            }
3891        });
3892
3893        None
3894    }
3895
3896    fn shared_screen_for_peer(
3897        &self,
3898        peer_id: PeerId,
3899        pane: &View<Pane>,
3900        cx: &mut WindowContext,
3901    ) -> Option<View<SharedScreen>> {
3902        let call = self.active_call()?;
3903        let room = call.read(cx).room()?.read(cx);
3904        let participant = room.remote_participant_for_peer_id(peer_id)?;
3905        let track = participant.video_tracks.values().next()?.clone();
3906        let user = participant.user.clone();
3907
3908        for item in pane.read(cx).items_of_type::<SharedScreen>() {
3909            if item.read(cx).peer_id == peer_id {
3910                return Some(item);
3911            }
3912        }
3913
3914        Some(cx.new_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
3915    }
3916
3917    pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3918        if cx.is_window_active() {
3919            self.update_active_view_for_followers(cx);
3920
3921            if let Some(database_id) = self.database_id {
3922                cx.background_executor()
3923                    .spawn(persistence::DB.update_timestamp(database_id))
3924                    .detach();
3925            }
3926        } else {
3927            for pane in &self.panes {
3928                pane.update(cx, |pane, cx| {
3929                    if let Some(item) = pane.active_item() {
3930                        item.workspace_deactivated(cx);
3931                    }
3932                    for item in pane.items() {
3933                        if matches!(
3934                            item.workspace_settings(cx).autosave,
3935                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3936                        ) {
3937                            Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
3938                                .detach_and_log_err(cx);
3939                        }
3940                    }
3941                });
3942            }
3943        }
3944    }
3945
3946    fn active_call(&self) -> Option<&Model<ActiveCall>> {
3947        self.active_call.as_ref().map(|(call, _)| call)
3948    }
3949
3950    fn on_active_call_event(
3951        &mut self,
3952        _: Model<ActiveCall>,
3953        event: &call::room::Event,
3954        cx: &mut ViewContext<Self>,
3955    ) {
3956        match event {
3957            call::room::Event::ParticipantLocationChanged { participant_id }
3958            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
3959                self.leader_updated(*participant_id, cx);
3960            }
3961            _ => {}
3962        }
3963    }
3964
3965    pub fn database_id(&self) -> Option<WorkspaceId> {
3966        self.database_id
3967    }
3968
3969    fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
3970        let project = self.project().read(cx);
3971
3972        if project.is_local() {
3973            Some(
3974                project
3975                    .visible_worktrees(cx)
3976                    .map(|worktree| worktree.read(cx).abs_path())
3977                    .collect::<Vec<_>>(),
3978            )
3979        } else {
3980            None
3981        }
3982    }
3983
3984    fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
3985        match member {
3986            Member::Axis(PaneAxis { members, .. }) => {
3987                for child in members.iter() {
3988                    self.remove_panes(child.clone(), cx)
3989                }
3990            }
3991            Member::Pane(pane) => {
3992                self.force_remove_pane(&pane, &None, cx);
3993            }
3994        }
3995    }
3996
3997    fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
3998        self.session_id.take();
3999        self.serialize_workspace_internal(cx)
4000    }
4001
4002    fn force_remove_pane(
4003        &mut self,
4004        pane: &View<Pane>,
4005        focus_on: &Option<View<Pane>>,
4006        cx: &mut ViewContext<Workspace>,
4007    ) {
4008        self.panes.retain(|p| p != pane);
4009        if let Some(focus_on) = focus_on {
4010            focus_on.update(cx, |pane, cx| pane.focus(cx));
4011        } else {
4012            self.panes
4013                .last()
4014                .unwrap()
4015                .update(cx, |pane, cx| pane.focus(cx));
4016        }
4017        if self.last_active_center_pane == Some(pane.downgrade()) {
4018            self.last_active_center_pane = None;
4019        }
4020        cx.notify();
4021    }
4022
4023    fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
4024        if self._schedule_serialize.is_none() {
4025            self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
4026                cx.background_executor()
4027                    .timer(Duration::from_millis(100))
4028                    .await;
4029                this.update(&mut cx, |this, cx| {
4030                    this.serialize_workspace_internal(cx).detach();
4031                    this._schedule_serialize.take();
4032                })
4033                .log_err();
4034            }));
4035        }
4036    }
4037
4038    fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
4039        let Some(database_id) = self.database_id() else {
4040            return Task::ready(());
4041        };
4042
4043        fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
4044            let (items, active, pinned_count) = {
4045                let pane = pane_handle.read(cx);
4046                let active_item_id = pane.active_item().map(|item| item.item_id());
4047                (
4048                    pane.items()
4049                        .filter_map(|handle| {
4050                            let handle = handle.to_serializable_item_handle(cx)?;
4051
4052                            Some(SerializedItem {
4053                                kind: Arc::from(handle.serialized_item_kind()),
4054                                item_id: handle.item_id().as_u64(),
4055                                active: Some(handle.item_id()) == active_item_id,
4056                                preview: pane.is_active_preview_item(handle.item_id()),
4057                            })
4058                        })
4059                        .collect::<Vec<_>>(),
4060                    pane.has_focus(cx),
4061                    pane.pinned_count(),
4062                )
4063            };
4064
4065            SerializedPane::new(items, active, pinned_count)
4066        }
4067
4068        fn build_serialized_pane_group(
4069            pane_group: &Member,
4070            cx: &WindowContext,
4071        ) -> SerializedPaneGroup {
4072            match pane_group {
4073                Member::Axis(PaneAxis {
4074                    axis,
4075                    members,
4076                    flexes,
4077                    bounding_boxes: _,
4078                }) => SerializedPaneGroup::Group {
4079                    axis: SerializedAxis(*axis),
4080                    children: members
4081                        .iter()
4082                        .map(|member| build_serialized_pane_group(member, cx))
4083                        .collect::<Vec<_>>(),
4084                    flexes: Some(flexes.lock().clone()),
4085                },
4086                Member::Pane(pane_handle) => {
4087                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
4088                }
4089            }
4090        }
4091
4092        fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
4093            let left_dock = this.left_dock.read(cx);
4094            let left_visible = left_dock.is_open();
4095            let left_active_panel = left_dock
4096                .visible_panel()
4097                .map(|panel| panel.persistent_name().to_string());
4098            let left_dock_zoom = left_dock
4099                .visible_panel()
4100                .map(|panel| panel.is_zoomed(cx))
4101                .unwrap_or(false);
4102
4103            let right_dock = this.right_dock.read(cx);
4104            let right_visible = right_dock.is_open();
4105            let right_active_panel = right_dock
4106                .visible_panel()
4107                .map(|panel| panel.persistent_name().to_string());
4108            let right_dock_zoom = right_dock
4109                .visible_panel()
4110                .map(|panel| panel.is_zoomed(cx))
4111                .unwrap_or(false);
4112
4113            let bottom_dock = this.bottom_dock.read(cx);
4114            let bottom_visible = bottom_dock.is_open();
4115            let bottom_active_panel = bottom_dock
4116                .visible_panel()
4117                .map(|panel| panel.persistent_name().to_string());
4118            let bottom_dock_zoom = bottom_dock
4119                .visible_panel()
4120                .map(|panel| panel.is_zoomed(cx))
4121                .unwrap_or(false);
4122
4123            DockStructure {
4124                left: DockData {
4125                    visible: left_visible,
4126                    active_panel: left_active_panel,
4127                    zoom: left_dock_zoom,
4128                },
4129                right: DockData {
4130                    visible: right_visible,
4131                    active_panel: right_active_panel,
4132                    zoom: right_dock_zoom,
4133                },
4134                bottom: DockData {
4135                    visible: bottom_visible,
4136                    active_panel: bottom_active_panel,
4137                    zoom: bottom_dock_zoom,
4138                },
4139            }
4140        }
4141
4142        let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4143            Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4144        } else if let Some(local_paths) = self.local_paths(cx) {
4145            if !local_paths.is_empty() {
4146                Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4147            } else {
4148                None
4149            }
4150        } else {
4151            None
4152        };
4153
4154        if let Some(location) = location {
4155            let center_group = build_serialized_pane_group(&self.center.root, cx);
4156            let docks = build_serialized_docks(self, cx);
4157            let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
4158            let serialized_workspace = SerializedWorkspace {
4159                id: database_id,
4160                location,
4161                center_group,
4162                window_bounds,
4163                display: Default::default(),
4164                docks,
4165                centered_layout: self.centered_layout,
4166                session_id: self.session_id.clone(),
4167                window_id: Some(cx.window_handle().window_id().as_u64()),
4168            };
4169            return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
4170        }
4171        Task::ready(())
4172    }
4173
4174    async fn serialize_items(
4175        this: &WeakView<Self>,
4176        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4177        cx: &mut AsyncWindowContext,
4178    ) -> Result<()> {
4179        const CHUNK_SIZE: usize = 200;
4180        const THROTTLE_TIME: Duration = Duration::from_millis(200);
4181
4182        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4183
4184        while let Some(items_received) = serializable_items.next().await {
4185            let unique_items =
4186                items_received
4187                    .into_iter()
4188                    .fold(HashMap::default(), |mut acc, item| {
4189                        acc.entry(item.item_id()).or_insert(item);
4190                        acc
4191                    });
4192
4193            // We use into_iter() here so that the references to the items are moved into
4194            // the tasks and not kept alive while we're sleeping.
4195            for (_, item) in unique_items.into_iter() {
4196                if let Ok(Some(task)) =
4197                    this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4198                {
4199                    cx.background_executor()
4200                        .spawn(async move { task.await.log_err() })
4201                        .detach();
4202                }
4203            }
4204
4205            cx.background_executor().timer(THROTTLE_TIME).await;
4206        }
4207
4208        Ok(())
4209    }
4210
4211    pub(crate) fn enqueue_item_serialization(
4212        &mut self,
4213        item: Box<dyn SerializableItemHandle>,
4214    ) -> Result<()> {
4215        self.serializable_items_tx
4216            .unbounded_send(item)
4217            .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4218    }
4219
4220    pub(crate) fn load_workspace(
4221        serialized_workspace: SerializedWorkspace,
4222        paths_to_open: Vec<Option<ProjectPath>>,
4223        cx: &mut ViewContext<Workspace>,
4224    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4225        cx.spawn(|workspace, mut cx| async move {
4226            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4227
4228            let mut center_group = None;
4229            let mut center_items = None;
4230
4231            // Traverse the splits tree and add to things
4232            if let Some((group, active_pane, items)) = serialized_workspace
4233                .center_group
4234                .deserialize(
4235                    &project,
4236                    serialized_workspace.id,
4237                    workspace.clone(),
4238                    &mut cx,
4239                )
4240                .await
4241            {
4242                center_items = Some(items);
4243                center_group = Some((group, active_pane))
4244            }
4245
4246            let mut items_by_project_path = HashMap::default();
4247            let mut item_ids_by_kind = HashMap::default();
4248            let mut all_deserialized_items = Vec::default();
4249            cx.update(|cx| {
4250                for item in center_items.unwrap_or_default().into_iter().flatten() {
4251                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4252                        item_ids_by_kind
4253                            .entry(serializable_item_handle.serialized_item_kind())
4254                            .or_insert(Vec::new())
4255                            .push(item.item_id().as_u64() as ItemId);
4256                    }
4257
4258                    if let Some(project_path) = item.project_path(cx) {
4259                        items_by_project_path.insert(project_path, item.clone());
4260                    }
4261                    all_deserialized_items.push(item);
4262                }
4263            })?;
4264
4265            let opened_items = paths_to_open
4266                .into_iter()
4267                .map(|path_to_open| {
4268                    path_to_open
4269                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4270                })
4271                .collect::<Vec<_>>();
4272
4273            // Remove old panes from workspace panes list
4274            workspace.update(&mut cx, |workspace, cx| {
4275                if let Some((center_group, active_pane)) = center_group {
4276                    workspace.remove_panes(workspace.center.root.clone(), cx);
4277
4278                    // Swap workspace center group
4279                    workspace.center = PaneGroup::with_root(center_group);
4280                    if let Some(active_pane) = active_pane {
4281                        workspace.set_active_pane(&active_pane, cx);
4282                        cx.focus_self();
4283                    } else {
4284                        workspace.set_active_pane(&workspace.center.first_pane(), cx);
4285                    }
4286                }
4287
4288                let docks = serialized_workspace.docks;
4289
4290                for (dock, serialized_dock) in [
4291                    (&mut workspace.right_dock, docks.right),
4292                    (&mut workspace.left_dock, docks.left),
4293                    (&mut workspace.bottom_dock, docks.bottom),
4294                ]
4295                .iter_mut()
4296                {
4297                    dock.update(cx, |dock, cx| {
4298                        dock.serialized_dock = Some(serialized_dock.clone());
4299                        dock.restore_state(cx);
4300                    });
4301                }
4302
4303                cx.notify();
4304            })?;
4305
4306            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4307            // after loading the items, we might have different items and in order to avoid
4308            // the database filling up, we delete items that haven't been loaded now.
4309            //
4310            // The items that have been loaded, have been saved after they've been added to the workspace.
4311            let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
4312                item_ids_by_kind
4313                    .into_iter()
4314                    .map(|(item_kind, loaded_items)| {
4315                        SerializableItemRegistry::cleanup(
4316                            item_kind,
4317                            serialized_workspace.id,
4318                            loaded_items,
4319                            cx,
4320                        )
4321                        .log_err()
4322                    })
4323                    .collect::<Vec<_>>()
4324            })?;
4325
4326            futures::future::join_all(clean_up_tasks).await;
4327
4328            workspace
4329                .update(&mut cx, |workspace, cx| {
4330                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4331                    workspace.serialize_workspace_internal(cx).detach();
4332
4333                    // Ensure that we mark the window as edited if we did load dirty items
4334                    workspace.update_window_edited(cx);
4335                })
4336                .ok();
4337
4338            Ok(opened_items)
4339        })
4340    }
4341
4342    fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4343        self.add_workspace_actions_listeners(div, cx)
4344            .on_action(cx.listener(Self::close_inactive_items_and_panes))
4345            .on_action(cx.listener(Self::close_all_items_and_panes))
4346            .on_action(cx.listener(Self::save_all))
4347            .on_action(cx.listener(Self::send_keystrokes))
4348            .on_action(cx.listener(Self::add_folder_to_project))
4349            .on_action(cx.listener(Self::follow_next_collaborator))
4350            .on_action(cx.listener(Self::close_window))
4351            .on_action(cx.listener(Self::activate_pane_at_index))
4352            .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
4353                let pane = workspace.active_pane().clone();
4354                workspace.unfollow_in_pane(&pane, cx);
4355            }))
4356            .on_action(cx.listener(|workspace, action: &Save, cx| {
4357                workspace
4358                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
4359                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4360            }))
4361            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
4362                workspace
4363                    .save_active_item(SaveIntent::SaveWithoutFormat, cx)
4364                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4365            }))
4366            .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
4367                workspace
4368                    .save_active_item(SaveIntent::SaveAs, cx)
4369                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4370            }))
4371            .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4372                workspace.activate_previous_pane(cx)
4373            }))
4374            .on_action(
4375                cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4376            )
4377            .on_action(
4378                cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4379                    workspace.activate_pane_in_direction(action.0, cx)
4380                }),
4381            )
4382            .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4383                workspace.swap_pane_in_direction(action.0, cx)
4384            }))
4385            .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4386                this.toggle_dock(DockPosition::Left, cx);
4387            }))
4388            .on_action(
4389                cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4390                    workspace.toggle_dock(DockPosition::Right, cx);
4391                }),
4392            )
4393            .on_action(
4394                cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4395                    workspace.toggle_dock(DockPosition::Bottom, cx);
4396                }),
4397            )
4398            .on_action(
4399                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4400                    workspace.close_all_docks(cx);
4401                }),
4402            )
4403            .on_action(
4404                cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4405                    workspace.clear_all_notifications(cx);
4406                }),
4407            )
4408            .on_action(
4409                cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4410                    workspace.reopen_closed_item(cx).detach();
4411                }),
4412            )
4413            .on_action(cx.listener(Workspace::toggle_centered_layout))
4414    }
4415
4416    #[cfg(any(test, feature = "test-support"))]
4417    pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4418        use node_runtime::NodeRuntime;
4419        use session::Session;
4420
4421        let client = project.read(cx).client();
4422        let user_store = project.read(cx).user_store();
4423
4424        let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4425        let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
4426        cx.activate_window();
4427        let app_state = Arc::new(AppState {
4428            languages: project.read(cx).languages().clone(),
4429            workspace_store,
4430            client,
4431            user_store,
4432            fs: project.read(cx).fs().clone(),
4433            build_window_options: |_, _| Default::default(),
4434            node_runtime: NodeRuntime::unavailable(),
4435            session,
4436        });
4437        let workspace = Self::new(Default::default(), project, app_state, cx);
4438        workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4439        workspace
4440    }
4441
4442    pub fn register_action<A: Action>(
4443        &mut self,
4444        callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4445    ) -> &mut Self {
4446        let callback = Arc::new(callback);
4447
4448        self.workspace_actions.push(Box::new(move |div, cx| {
4449            let callback = callback.clone();
4450            div.on_action(
4451                cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4452            )
4453        }));
4454        self
4455    }
4456
4457    fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
4458        for action in self.workspace_actions.iter() {
4459            div = (action)(div, cx)
4460        }
4461        div
4462    }
4463
4464    pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
4465        self.modal_layer.read(cx).has_active_modal()
4466    }
4467
4468    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &AppContext) -> Option<View<V>> {
4469        self.modal_layer.read(cx).active_modal()
4470    }
4471
4472    pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
4473    where
4474        B: FnOnce(&mut ViewContext<V>) -> V,
4475    {
4476        self.modal_layer
4477            .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
4478    }
4479
4480    pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
4481        self.centered_layout = !self.centered_layout;
4482        if let Some(database_id) = self.database_id() {
4483            cx.background_executor()
4484                .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4485                .detach_and_log_err(cx);
4486        }
4487        cx.notify();
4488    }
4489
4490    fn adjust_padding(padding: Option<f32>) -> f32 {
4491        padding
4492            .unwrap_or(Self::DEFAULT_PADDING)
4493            .clamp(0.0, Self::MAX_PADDING)
4494    }
4495
4496    fn render_dock(
4497        &self,
4498        position: DockPosition,
4499        dock: &View<Dock>,
4500        cx: &WindowContext,
4501    ) -> Option<Div> {
4502        if self.zoomed_position == Some(position) {
4503            return None;
4504        }
4505
4506        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4507            let pane = panel.pane(cx)?;
4508            let follower_states = &self.follower_states;
4509            leader_border_for_pane(follower_states, &pane, cx)
4510        });
4511
4512        Some(
4513            div()
4514                .flex()
4515                .flex_none()
4516                .overflow_hidden()
4517                .child(dock.clone())
4518                .children(leader_border),
4519        )
4520    }
4521}
4522
4523fn leader_border_for_pane(
4524    follower_states: &HashMap<PeerId, FollowerState>,
4525    pane: &View<Pane>,
4526    cx: &WindowContext,
4527) -> Option<Div> {
4528    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4529        if state.pane() == pane {
4530            Some((*leader_id, state))
4531        } else {
4532            None
4533        }
4534    })?;
4535
4536    let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4537    let leader = room.remote_participant_for_peer_id(leader_id)?;
4538
4539    let mut leader_color = cx
4540        .theme()
4541        .players()
4542        .color_for_participant(leader.participant_index.0)
4543        .cursor;
4544    leader_color.fade_out(0.3);
4545    Some(
4546        div()
4547            .absolute()
4548            .size_full()
4549            .left_0()
4550            .top_0()
4551            .border_2()
4552            .border_color(leader_color),
4553    )
4554}
4555
4556fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4557    ZED_WINDOW_POSITION
4558        .zip(*ZED_WINDOW_SIZE)
4559        .map(|(position, size)| Bounds {
4560            origin: position,
4561            size,
4562        })
4563}
4564
4565fn open_items(
4566    serialized_workspace: Option<SerializedWorkspace>,
4567    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4568    cx: &mut ViewContext<Workspace>,
4569) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4570    let restored_items = serialized_workspace.map(|serialized_workspace| {
4571        Workspace::load_workspace(
4572            serialized_workspace,
4573            project_paths_to_open
4574                .iter()
4575                .map(|(_, project_path)| project_path)
4576                .cloned()
4577                .collect(),
4578            cx,
4579        )
4580    });
4581
4582    cx.spawn(|workspace, mut cx| async move {
4583        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4584
4585        if let Some(restored_items) = restored_items {
4586            let restored_items = restored_items.await?;
4587
4588            let restored_project_paths = restored_items
4589                .iter()
4590                .filter_map(|item| {
4591                    cx.update(|cx| item.as_ref()?.project_path(cx))
4592                        .ok()
4593                        .flatten()
4594                })
4595                .collect::<HashSet<_>>();
4596
4597            for restored_item in restored_items {
4598                opened_items.push(restored_item.map(Ok));
4599            }
4600
4601            project_paths_to_open
4602                .iter_mut()
4603                .for_each(|(_, project_path)| {
4604                    if let Some(project_path_to_open) = project_path {
4605                        if restored_project_paths.contains(project_path_to_open) {
4606                            *project_path = None;
4607                        }
4608                    }
4609                });
4610        } else {
4611            for _ in 0..project_paths_to_open.len() {
4612                opened_items.push(None);
4613            }
4614        }
4615        assert!(opened_items.len() == project_paths_to_open.len());
4616
4617        let tasks =
4618            project_paths_to_open
4619                .into_iter()
4620                .enumerate()
4621                .map(|(ix, (abs_path, project_path))| {
4622                    let workspace = workspace.clone();
4623                    cx.spawn(|mut cx| async move {
4624                        let file_project_path = project_path?;
4625                        let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
4626                            workspace.project().update(cx, |project, cx| {
4627                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
4628                            })
4629                        });
4630
4631                        // We only want to open file paths here. If one of the items
4632                        // here is a directory, it was already opened further above
4633                        // with a `find_or_create_worktree`.
4634                        if let Ok(task) = abs_path_task {
4635                            if task.await.map_or(true, |p| p.is_file()) {
4636                                return Some((
4637                                    ix,
4638                                    workspace
4639                                        .update(&mut cx, |workspace, cx| {
4640                                            workspace.open_path(file_project_path, None, true, cx)
4641                                        })
4642                                        .log_err()?
4643                                        .await,
4644                                ));
4645                            }
4646                        }
4647                        None
4648                    })
4649                });
4650
4651        let tasks = tasks.collect::<Vec<_>>();
4652
4653        let tasks = futures::future::join_all(tasks);
4654        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4655            opened_items[ix] = Some(path_open_result);
4656        }
4657
4658        Ok(opened_items)
4659    })
4660}
4661
4662enum ActivateInDirectionTarget {
4663    Pane(View<Pane>),
4664    Dock(View<Dock>),
4665}
4666
4667fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4668    const REPORT_ISSUE_URL: &str = "https://github.com/zed-industries/zed/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
4669
4670    workspace
4671        .update(cx, |workspace, cx| {
4672            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4673                struct DatabaseFailedNotification;
4674
4675                workspace.show_notification_once(
4676                    NotificationId::unique::<DatabaseFailedNotification>(),
4677                    cx,
4678                    |cx| {
4679                        cx.new_view(|_| {
4680                            MessageNotification::new("Failed to load the database file.")
4681                                .with_click_message("File an issue")
4682                                .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4683                        })
4684                    },
4685                );
4686            }
4687        })
4688        .log_err();
4689}
4690
4691impl FocusableView for Workspace {
4692    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4693        self.active_pane.focus_handle(cx)
4694    }
4695}
4696
4697#[derive(Clone, Render)]
4698struct DraggedDock(DockPosition);
4699
4700impl Render for Workspace {
4701    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4702        let mut context = KeyContext::new_with_defaults();
4703        context.add("Workspace");
4704        let centered_layout = self.centered_layout
4705            && self.center.panes().len() == 1
4706            && self.active_item(cx).is_some();
4707        let render_padding = |size| {
4708            (size > 0.0).then(|| {
4709                div()
4710                    .h_full()
4711                    .w(relative(size))
4712                    .bg(cx.theme().colors().editor_background)
4713                    .border_color(cx.theme().colors().pane_group_border)
4714            })
4715        };
4716        let paddings = if centered_layout {
4717            let settings = WorkspaceSettings::get_global(cx).centered_layout;
4718            (
4719                render_padding(Self::adjust_padding(settings.left_padding)),
4720                render_padding(Self::adjust_padding(settings.right_padding)),
4721            )
4722        } else {
4723            (None, None)
4724        };
4725        let ui_font = theme::setup_ui_font(cx);
4726
4727        let theme = cx.theme().clone();
4728        let colors = theme.colors();
4729
4730        client_side_decorations(
4731            self.actions(div(), cx)
4732                .key_context(context)
4733                .relative()
4734                .size_full()
4735                .flex()
4736                .flex_col()
4737                .font(ui_font)
4738                .gap_0()
4739                .justify_start()
4740                .items_start()
4741                .text_color(colors.text)
4742                .overflow_hidden()
4743                .children(self.titlebar_item.clone())
4744                .child(
4745                    div()
4746                        .size_full()
4747                        .relative()
4748                        .flex_1()
4749                        .flex()
4750                        .flex_col()
4751                        .child(
4752                            div()
4753                                .id("workspace")
4754                                .bg(colors.background)
4755                                .relative()
4756                                .flex_1()
4757                                .w_full()
4758                                .flex()
4759                                .flex_col()
4760                                .overflow_hidden()
4761                                .border_t_1()
4762                                .border_b_1()
4763                                .border_color(colors.border)
4764                                .child({
4765                                    let this = cx.view().clone();
4766                                    canvas(
4767                                        move |bounds, cx| {
4768                                            this.update(cx, |this, _cx| this.bounds = bounds)
4769                                        },
4770                                        |_, _, _| {},
4771                                    )
4772                                    .absolute()
4773                                    .size_full()
4774                                })
4775                                .when(self.zoomed.is_none(), |this| {
4776                                    this.on_drag_move(cx.listener(
4777                                        |workspace, e: &DragMoveEvent<DraggedDock>, cx| {
4778                                            match e.drag(cx).0 {
4779                                                DockPosition::Left => {
4780                                                    let size = e.event.position.x
4781                                                        - workspace.bounds.left();
4782                                                    workspace.left_dock.update(
4783                                                        cx,
4784                                                        |left_dock, cx| {
4785                                                            left_dock.resize_active_panel(
4786                                                                Some(size),
4787                                                                cx,
4788                                                            );
4789                                                        },
4790                                                    );
4791                                                }
4792                                                DockPosition::Right => {
4793                                                    let size = workspace.bounds.right()
4794                                                        - e.event.position.x;
4795                                                    workspace.right_dock.update(
4796                                                        cx,
4797                                                        |right_dock, cx| {
4798                                                            right_dock.resize_active_panel(
4799                                                                Some(size),
4800                                                                cx,
4801                                                            );
4802                                                        },
4803                                                    );
4804                                                }
4805                                                DockPosition::Bottom => {
4806                                                    let size = workspace.bounds.bottom()
4807                                                        - e.event.position.y;
4808                                                    workspace.bottom_dock.update(
4809                                                        cx,
4810                                                        |bottom_dock, cx| {
4811                                                            bottom_dock.resize_active_panel(
4812                                                                Some(size),
4813                                                                cx,
4814                                                            );
4815                                                        },
4816                                                    );
4817                                                }
4818                                            }
4819                                        },
4820                                    ))
4821                                })
4822                                .child(
4823                                    div()
4824                                        .flex()
4825                                        .flex_row()
4826                                        .h_full()
4827                                        // Left Dock
4828                                        .children(self.render_dock(
4829                                            DockPosition::Left,
4830                                            &self.left_dock,
4831                                            cx,
4832                                        ))
4833                                        // Panes
4834                                        .child(
4835                                            div()
4836                                                .flex()
4837                                                .flex_col()
4838                                                .flex_1()
4839                                                .overflow_hidden()
4840                                                .child(
4841                                                    h_flex()
4842                                                        .flex_1()
4843                                                        .when_some(paddings.0, |this, p| {
4844                                                            this.child(p.border_r_1())
4845                                                        })
4846                                                        .child(self.center.render(
4847                                                            &self.project,
4848                                                            &self.follower_states,
4849                                                            self.active_call(),
4850                                                            &self.active_pane,
4851                                                            self.zoomed.as_ref(),
4852                                                            &self.app_state,
4853                                                            cx,
4854                                                        ))
4855                                                        .when_some(paddings.1, |this, p| {
4856                                                            this.child(p.border_l_1())
4857                                                        }),
4858                                                )
4859                                                .children(self.render_dock(
4860                                                    DockPosition::Bottom,
4861                                                    &self.bottom_dock,
4862                                                    cx,
4863                                                )),
4864                                        )
4865                                        // Right Dock
4866                                        .children(self.render_dock(
4867                                            DockPosition::Right,
4868                                            &self.right_dock,
4869                                            cx,
4870                                        )),
4871                                )
4872                                .children(self.zoomed.as_ref().and_then(|view| {
4873                                    let zoomed_view = view.upgrade()?;
4874                                    let div = div()
4875                                        .occlude()
4876                                        .absolute()
4877                                        .overflow_hidden()
4878                                        .border_color(colors.border)
4879                                        .bg(colors.background)
4880                                        .child(zoomed_view)
4881                                        .inset_0()
4882                                        .shadow_lg();
4883
4884                                    Some(match self.zoomed_position {
4885                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
4886                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
4887                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
4888                                        None => {
4889                                            div.top_2().bottom_2().left_2().right_2().border_1()
4890                                        }
4891                                    })
4892                                }))
4893                                .children(self.render_notifications(cx)),
4894                        )
4895                        .child(self.status_bar.clone())
4896                        .child(self.modal_layer.clone()),
4897                ),
4898            cx,
4899        )
4900    }
4901}
4902
4903impl WorkspaceStore {
4904    pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
4905        Self {
4906            workspaces: Default::default(),
4907            _subscriptions: vec![
4908                client.add_request_handler(cx.weak_model(), Self::handle_follow),
4909                client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
4910            ],
4911            client,
4912        }
4913    }
4914
4915    pub fn update_followers(
4916        &self,
4917        project_id: Option<u64>,
4918        update: proto::update_followers::Variant,
4919        cx: &AppContext,
4920    ) -> Option<()> {
4921        let active_call = ActiveCall::try_global(cx)?;
4922        let room_id = active_call.read(cx).room()?.read(cx).id();
4923        self.client
4924            .send(proto::UpdateFollowers {
4925                room_id,
4926                project_id,
4927                variant: Some(update),
4928            })
4929            .log_err()
4930    }
4931
4932    pub async fn handle_follow(
4933        this: Model<Self>,
4934        envelope: TypedEnvelope<proto::Follow>,
4935        mut cx: AsyncAppContext,
4936    ) -> Result<proto::FollowResponse> {
4937        this.update(&mut cx, |this, cx| {
4938            let follower = Follower {
4939                project_id: envelope.payload.project_id,
4940                peer_id: envelope.original_sender_id()?,
4941            };
4942
4943            let mut response = proto::FollowResponse::default();
4944            this.workspaces.retain(|workspace| {
4945                workspace
4946                    .update(cx, |workspace, cx| {
4947                        let handler_response = workspace.handle_follow(follower.project_id, cx);
4948                        if let Some(active_view) = handler_response.active_view.clone() {
4949                            if workspace.project.read(cx).remote_id() == follower.project_id {
4950                                response.active_view = Some(active_view)
4951                            }
4952                        }
4953                    })
4954                    .is_ok()
4955            });
4956
4957            Ok(response)
4958        })?
4959    }
4960
4961    async fn handle_update_followers(
4962        this: Model<Self>,
4963        envelope: TypedEnvelope<proto::UpdateFollowers>,
4964        mut cx: AsyncAppContext,
4965    ) -> Result<()> {
4966        let leader_id = envelope.original_sender_id()?;
4967        let update = envelope.payload;
4968
4969        this.update(&mut cx, |this, cx| {
4970            this.workspaces.retain(|workspace| {
4971                workspace
4972                    .update(cx, |workspace, cx| {
4973                        let project_id = workspace.project.read(cx).remote_id();
4974                        if update.project_id != project_id && update.project_id.is_some() {
4975                            return;
4976                        }
4977                        workspace.handle_update_followers(leader_id, update.clone(), cx);
4978                    })
4979                    .is_ok()
4980            });
4981            Ok(())
4982        })?
4983    }
4984}
4985
4986impl ViewId {
4987    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
4988        Ok(Self {
4989            creator: message
4990                .creator
4991                .ok_or_else(|| anyhow!("creator is missing"))?,
4992            id: message.id,
4993        })
4994    }
4995
4996    pub(crate) fn to_proto(self) -> proto::ViewId {
4997        proto::ViewId {
4998            creator: Some(self.creator),
4999            id: self.id,
5000        }
5001    }
5002}
5003
5004impl FollowerState {
5005    fn pane(&self) -> &View<Pane> {
5006        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5007    }
5008}
5009
5010pub trait WorkspaceHandle {
5011    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
5012}
5013
5014impl WorkspaceHandle for View<Workspace> {
5015    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
5016        self.read(cx)
5017            .worktrees(cx)
5018            .flat_map(|worktree| {
5019                let worktree_id = worktree.read(cx).id();
5020                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5021                    worktree_id,
5022                    path: f.path.clone(),
5023                })
5024            })
5025            .collect::<Vec<_>>()
5026    }
5027}
5028
5029impl std::fmt::Debug for OpenPaths {
5030    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5031        f.debug_struct("OpenPaths")
5032            .field("paths", &self.paths)
5033            .finish()
5034    }
5035}
5036
5037pub fn activate_workspace_for_project(
5038    cx: &mut AppContext,
5039    predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
5040) -> Option<WindowHandle<Workspace>> {
5041    for window in cx.windows() {
5042        let Some(workspace) = window.downcast::<Workspace>() else {
5043            continue;
5044        };
5045
5046        let predicate = workspace
5047            .update(cx, |workspace, cx| {
5048                let project = workspace.project.read(cx);
5049                if predicate(project, cx) {
5050                    cx.activate_window();
5051                    true
5052                } else {
5053                    false
5054                }
5055            })
5056            .log_err()
5057            .unwrap_or(false);
5058
5059        if predicate {
5060            return Some(workspace);
5061        }
5062    }
5063
5064    None
5065}
5066
5067pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5068    DB.last_workspace().await.log_err().flatten()
5069}
5070
5071pub fn last_session_workspace_locations(
5072    last_session_id: &str,
5073    last_session_window_stack: Option<Vec<WindowId>>,
5074) -> Option<Vec<SerializedWorkspaceLocation>> {
5075    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5076        .log_err()
5077}
5078
5079actions!(collab, [OpenChannelNotes]);
5080actions!(zed, [OpenLog]);
5081
5082async fn join_channel_internal(
5083    channel_id: ChannelId,
5084    app_state: &Arc<AppState>,
5085    requesting_window: Option<WindowHandle<Workspace>>,
5086    active_call: &Model<ActiveCall>,
5087    cx: &mut AsyncAppContext,
5088) -> Result<bool> {
5089    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5090        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5091            return (false, None);
5092        };
5093
5094        let already_in_channel = room.channel_id() == Some(channel_id);
5095        let should_prompt = room.is_sharing_project()
5096            && !room.remote_participants().is_empty()
5097            && !already_in_channel;
5098        let open_room = if already_in_channel {
5099            active_call.room().cloned()
5100        } else {
5101            None
5102        };
5103        (should_prompt, open_room)
5104    })?;
5105
5106    if let Some(room) = open_room {
5107        let task = room.update(cx, |room, cx| {
5108            if let Some((project, host)) = room.most_active_project(cx) {
5109                return Some(join_in_room_project(project, host, app_state.clone(), cx));
5110            }
5111
5112            None
5113        })?;
5114        if let Some(task) = task {
5115            task.await?;
5116        }
5117        return anyhow::Ok(true);
5118    }
5119
5120    if should_prompt {
5121        if let Some(workspace) = requesting_window {
5122            let answer = workspace
5123                .update(cx, |_, cx| {
5124                    cx.prompt(
5125                        PromptLevel::Warning,
5126                        "Do you want to switch channels?",
5127                        Some("Leaving this call will unshare your current project."),
5128                        &["Yes, Join Channel", "Cancel"],
5129                    )
5130                })?
5131                .await;
5132
5133            if answer == Ok(1) {
5134                return Ok(false);
5135            }
5136        } else {
5137            return Ok(false); // unreachable!() hopefully
5138        }
5139    }
5140
5141    let client = cx.update(|cx| active_call.read(cx).client())?;
5142
5143    let mut client_status = client.status();
5144
5145    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5146    'outer: loop {
5147        let Some(status) = client_status.recv().await else {
5148            return Err(anyhow!("error connecting"));
5149        };
5150
5151        match status {
5152            Status::Connecting
5153            | Status::Authenticating
5154            | Status::Reconnecting
5155            | Status::Reauthenticating => continue,
5156            Status::Connected { .. } => break 'outer,
5157            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5158            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5159            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5160                return Err(ErrorCode::Disconnected.into());
5161            }
5162        }
5163    }
5164
5165    let room = active_call
5166        .update(cx, |active_call, cx| {
5167            active_call.join_channel(channel_id, cx)
5168        })?
5169        .await?;
5170
5171    let Some(room) = room else {
5172        return anyhow::Ok(true);
5173    };
5174
5175    room.update(cx, |room, _| room.room_update_completed())?
5176        .await;
5177
5178    let task = room.update(cx, |room, cx| {
5179        if let Some((project, host)) = room.most_active_project(cx) {
5180            return Some(join_in_room_project(project, host, app_state.clone(), cx));
5181        }
5182
5183        // If you are the first to join a channel, see if you should share your project.
5184        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5185            if let Some(workspace) = requesting_window {
5186                let project = workspace.update(cx, |workspace, cx| {
5187                    let project = workspace.project.read(cx);
5188
5189                    if !CallSettings::get_global(cx).share_on_join {
5190                        return None;
5191                    }
5192
5193                    if (project.is_local() || project.is_via_ssh())
5194                        && project.visible_worktrees(cx).any(|tree| {
5195                            tree.read(cx)
5196                                .root_entry()
5197                                .map_or(false, |entry| entry.is_dir())
5198                        })
5199                    {
5200                        Some(workspace.project.clone())
5201                    } else {
5202                        None
5203                    }
5204                });
5205                if let Ok(Some(project)) = project {
5206                    return Some(cx.spawn(|room, mut cx| async move {
5207                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5208                            .await?;
5209                        Ok(())
5210                    }));
5211                }
5212            }
5213        }
5214
5215        None
5216    })?;
5217    if let Some(task) = task {
5218        task.await?;
5219        return anyhow::Ok(true);
5220    }
5221    anyhow::Ok(false)
5222}
5223
5224pub fn join_channel(
5225    channel_id: ChannelId,
5226    app_state: Arc<AppState>,
5227    requesting_window: Option<WindowHandle<Workspace>>,
5228    cx: &mut AppContext,
5229) -> Task<Result<()>> {
5230    let active_call = ActiveCall::global(cx);
5231    cx.spawn(|mut cx| async move {
5232        let result = join_channel_internal(
5233            channel_id,
5234            &app_state,
5235            requesting_window,
5236            &active_call,
5237            &mut cx,
5238        )
5239            .await;
5240
5241        // join channel succeeded, and opened a window
5242        if matches!(result, Ok(true)) {
5243            return anyhow::Ok(());
5244        }
5245
5246        // find an existing workspace to focus and show call controls
5247        let mut active_window =
5248            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5249        if active_window.is_none() {
5250            // no open workspaces, make one to show the error in (blergh)
5251            let (window_handle, _) = cx
5252                .update(|cx| {
5253                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5254                })?
5255                .await?;
5256
5257            if result.is_ok() {
5258                cx.update(|cx| {
5259                    cx.dispatch_action(&OpenChannelNotes);
5260                }).log_err();
5261            }
5262
5263            active_window = Some(window_handle);
5264        }
5265
5266        if let Err(err) = result {
5267            log::error!("failed to join channel: {}", err);
5268            if let Some(active_window) = active_window {
5269                active_window
5270                    .update(&mut cx, |_, cx| {
5271                        let detail: SharedString = match err.error_code() {
5272                            ErrorCode::SignedOut => {
5273                                "Please sign in to continue.".into()
5274                            }
5275                            ErrorCode::UpgradeRequired => {
5276                                "Your are running an unsupported version of Zed. Please update to continue.".into()
5277                            }
5278                            ErrorCode::NoSuchChannel => {
5279                                "No matching channel was found. Please check the link and try again.".into()
5280                            }
5281                            ErrorCode::Forbidden => {
5282                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5283                            }
5284                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5285                            _ => format!("{}\n\nPlease try again.", err).into(),
5286                        };
5287                        cx.prompt(
5288                            PromptLevel::Critical,
5289                            "Failed to join channel",
5290                            Some(&detail),
5291                            &["Ok"],
5292                        )
5293                    })?
5294                    .await
5295                    .ok();
5296            }
5297        }
5298
5299        // return ok, we showed the error to the user.
5300        anyhow::Ok(())
5301    })
5302}
5303
5304pub async fn get_any_active_workspace(
5305    app_state: Arc<AppState>,
5306    mut cx: AsyncAppContext,
5307) -> anyhow::Result<WindowHandle<Workspace>> {
5308    // find an existing workspace to focus and show call controls
5309    let active_window = activate_any_workspace_window(&mut cx);
5310    if active_window.is_none() {
5311        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5312            .await?;
5313    }
5314    activate_any_workspace_window(&mut cx).context("could not open zed")
5315}
5316
5317fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5318    cx.update(|cx| {
5319        if let Some(workspace_window) = cx
5320            .active_window()
5321            .and_then(|window| window.downcast::<Workspace>())
5322        {
5323            return Some(workspace_window);
5324        }
5325
5326        for window in cx.windows() {
5327            if let Some(workspace_window) = window.downcast::<Workspace>() {
5328                workspace_window
5329                    .update(cx, |_, cx| cx.activate_window())
5330                    .ok();
5331                return Some(workspace_window);
5332            }
5333        }
5334        None
5335    })
5336    .ok()
5337    .flatten()
5338}
5339
5340pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5341    cx.windows()
5342        .into_iter()
5343        .filter_map(|window| window.downcast::<Workspace>())
5344        .filter(|workspace| {
5345            workspace
5346                .read(cx)
5347                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5348        })
5349        .collect()
5350}
5351
5352#[derive(Default)]
5353pub struct OpenOptions {
5354    pub open_new_workspace: Option<bool>,
5355    pub replace_window: Option<WindowHandle<Workspace>>,
5356    pub env: Option<HashMap<String, String>>,
5357}
5358
5359#[allow(clippy::type_complexity)]
5360pub fn open_paths(
5361    abs_paths: &[PathBuf],
5362    app_state: Arc<AppState>,
5363    open_options: OpenOptions,
5364    cx: &mut AppContext,
5365) -> Task<
5366    anyhow::Result<(
5367        WindowHandle<Workspace>,
5368        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5369    )>,
5370> {
5371    let abs_paths = abs_paths.to_vec();
5372    let mut existing = None;
5373    let mut best_match = None;
5374    let mut open_visible = OpenVisible::All;
5375
5376    if open_options.open_new_workspace != Some(true) {
5377        for window in local_workspace_windows(cx) {
5378            if let Ok(workspace) = window.read(cx) {
5379                let m = workspace
5380                    .project
5381                    .read(cx)
5382                    .visibility_for_paths(&abs_paths, cx);
5383                if m > best_match {
5384                    existing = Some(window);
5385                    best_match = m;
5386                } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5387                    existing = Some(window)
5388                }
5389            }
5390        }
5391    }
5392
5393    cx.spawn(move |mut cx| async move {
5394        if open_options.open_new_workspace.is_none() && existing.is_none() {
5395            let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5396            if futures::future::join_all(all_files)
5397                .await
5398                .into_iter()
5399                .filter_map(|result| result.ok().flatten())
5400                .all(|file| !file.is_dir)
5401            {
5402                cx.update(|cx| {
5403                    for window in local_workspace_windows(cx) {
5404                        if let Ok(workspace) = window.read(cx) {
5405                            let project = workspace.project().read(cx);
5406                            if project.is_via_collab() {
5407                                continue;
5408                            }
5409                            existing = Some(window);
5410                            open_visible = OpenVisible::None;
5411                            break;
5412                        }
5413                    }
5414                })?;
5415            }
5416        }
5417
5418        if let Some(existing) = existing {
5419            Ok((
5420                existing,
5421                existing
5422                    .update(&mut cx, |workspace, cx| {
5423                        cx.activate_window();
5424                        workspace.open_paths(abs_paths, open_visible, None, cx)
5425                    })?
5426                    .await,
5427            ))
5428        } else {
5429            cx.update(move |cx| {
5430                Workspace::new_local(
5431                    abs_paths,
5432                    app_state.clone(),
5433                    open_options.replace_window,
5434                    open_options.env,
5435                    cx,
5436                )
5437            })?
5438            .await
5439        }
5440    })
5441}
5442
5443pub fn open_new(
5444    open_options: OpenOptions,
5445    app_state: Arc<AppState>,
5446    cx: &mut AppContext,
5447    init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5448) -> Task<anyhow::Result<()>> {
5449    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5450    cx.spawn(|mut cx| async move {
5451        let (workspace, opened_paths) = task.await?;
5452        workspace.update(&mut cx, |workspace, cx| {
5453            if opened_paths.is_empty() {
5454                init(workspace, cx)
5455            }
5456        })?;
5457        Ok(())
5458    })
5459}
5460
5461pub fn create_and_open_local_file(
5462    path: &'static Path,
5463    cx: &mut ViewContext<Workspace>,
5464    default_content: impl 'static + Send + FnOnce() -> Rope,
5465) -> Task<Result<Box<dyn ItemHandle>>> {
5466    cx.spawn(|workspace, mut cx| async move {
5467        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5468        if !fs.is_file(path).await {
5469            fs.create_file(path, Default::default()).await?;
5470            fs.save(path, &default_content(), Default::default())
5471                .await?;
5472        }
5473
5474        let mut items = workspace
5475            .update(&mut cx, |workspace, cx| {
5476                workspace.with_local_workspace(cx, |workspace, cx| {
5477                    workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5478                })
5479            })?
5480            .await?
5481            .await;
5482
5483        let item = items.pop().flatten();
5484        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5485    })
5486}
5487
5488pub fn open_ssh_project(
5489    window: WindowHandle<Workspace>,
5490    connection_options: SshConnectionOptions,
5491    cancel_rx: oneshot::Receiver<()>,
5492    delegate: Arc<dyn SshClientDelegate>,
5493    app_state: Arc<AppState>,
5494    paths: Vec<PathBuf>,
5495    cx: &mut AppContext,
5496) -> Task<Result<()>> {
5497    let release_channel = ReleaseChannel::global(cx);
5498
5499    cx.spawn(|mut cx| async move {
5500        let (serialized_ssh_project, workspace_id, serialized_workspace) =
5501            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5502
5503        let identifier_prefix = match release_channel {
5504            ReleaseChannel::Stable => None,
5505            _ => Some(format!("{}-", release_channel.dev_name())),
5506        };
5507        let unique_identifier = format!(
5508            "{}workspace-{}",
5509            identifier_prefix.unwrap_or_default(),
5510            workspace_id.0
5511        );
5512
5513        let session = match cx
5514            .update(|cx| {
5515                remote::SshRemoteClient::new(
5516                    unique_identifier,
5517                    connection_options,
5518                    cancel_rx,
5519                    delegate,
5520                    cx,
5521                )
5522            })?
5523            .await?
5524        {
5525            Some(result) => result,
5526            None => return Ok(()),
5527        };
5528
5529        let project = cx.update(|cx| {
5530            project::Project::ssh(
5531                session,
5532                app_state.client.clone(),
5533                app_state.node_runtime.clone(),
5534                app_state.user_store.clone(),
5535                app_state.languages.clone(),
5536                app_state.fs.clone(),
5537                cx,
5538            )
5539        })?;
5540
5541        let toolchains = DB.toolchains(workspace_id).await?;
5542        for (toolchain, worktree_id) in toolchains {
5543            project
5544                .update(&mut cx, |this, cx| {
5545                    this.activate_toolchain(worktree_id, toolchain, cx)
5546                })?
5547                .await;
5548        }
5549        let mut project_paths_to_open = vec![];
5550        let mut project_path_errors = vec![];
5551
5552        for path in paths {
5553            let result = cx
5554                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5555                .await;
5556            match result {
5557                Ok((_, project_path)) => {
5558                    project_paths_to_open.push((path.clone(), Some(project_path)));
5559                }
5560                Err(error) => {
5561                    project_path_errors.push(error);
5562                }
5563            };
5564        }
5565
5566        if project_paths_to_open.is_empty() {
5567            return Err(project_path_errors
5568                .pop()
5569                .unwrap_or_else(|| anyhow!("no paths given")));
5570        }
5571
5572        cx.update_window(window.into(), |_, cx| {
5573            cx.replace_root_view(|cx| {
5574                let mut workspace =
5575                    Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5576
5577                workspace
5578                    .client()
5579                    .telemetry()
5580                    .report_app_event("open ssh project".to_string());
5581
5582                workspace.set_serialized_ssh_project(serialized_ssh_project);
5583                workspace
5584            });
5585        })?;
5586
5587        window
5588            .update(&mut cx, |_, cx| {
5589                cx.activate_window();
5590
5591                open_items(serialized_workspace, project_paths_to_open, cx)
5592            })?
5593            .await?;
5594
5595        window.update(&mut cx, |workspace, cx| {
5596            for error in project_path_errors {
5597                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5598                    if let Some(path) = error.error_tag("path") {
5599                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5600                    }
5601                } else {
5602                    workspace.show_error(&error, cx)
5603                }
5604            }
5605        })
5606    })
5607}
5608
5609fn serialize_ssh_project(
5610    connection_options: SshConnectionOptions,
5611    paths: Vec<PathBuf>,
5612    cx: &AsyncAppContext,
5613) -> Task<
5614    Result<(
5615        SerializedSshProject,
5616        WorkspaceId,
5617        Option<SerializedWorkspace>,
5618    )>,
5619> {
5620    cx.background_executor().spawn(async move {
5621        let serialized_ssh_project = persistence::DB
5622            .get_or_create_ssh_project(
5623                connection_options.host.clone(),
5624                connection_options.port,
5625                paths
5626                    .iter()
5627                    .map(|path| path.to_string_lossy().to_string())
5628                    .collect::<Vec<_>>(),
5629                connection_options.username.clone(),
5630            )
5631            .await?;
5632
5633        let serialized_workspace =
5634            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5635
5636        let workspace_id = if let Some(workspace_id) =
5637            serialized_workspace.as_ref().map(|workspace| workspace.id)
5638        {
5639            workspace_id
5640        } else {
5641            persistence::DB.next_id().await?
5642        };
5643
5644        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5645    })
5646}
5647
5648pub fn join_in_room_project(
5649    project_id: u64,
5650    follow_user_id: u64,
5651    app_state: Arc<AppState>,
5652    cx: &mut AppContext,
5653) -> Task<Result<()>> {
5654    let windows = cx.windows();
5655    cx.spawn(|mut cx| async move {
5656        let existing_workspace = windows.into_iter().find_map(|window| {
5657            window.downcast::<Workspace>().and_then(|window| {
5658                window
5659                    .update(&mut cx, |workspace, cx| {
5660                        if workspace.project().read(cx).remote_id() == Some(project_id) {
5661                            Some(window)
5662                        } else {
5663                            None
5664                        }
5665                    })
5666                    .unwrap_or(None)
5667            })
5668        });
5669
5670        let workspace = if let Some(existing_workspace) = existing_workspace {
5671            existing_workspace
5672        } else {
5673            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5674            let room = active_call
5675                .read_with(&cx, |call, _| call.room().cloned())?
5676                .ok_or_else(|| anyhow!("not in a call"))?;
5677            let project = room
5678                .update(&mut cx, |room, cx| {
5679                    room.join_project(
5680                        project_id,
5681                        app_state.languages.clone(),
5682                        app_state.fs.clone(),
5683                        cx,
5684                    )
5685                })?
5686                .await?;
5687
5688            let window_bounds_override = window_bounds_env_override();
5689            cx.update(|cx| {
5690                let mut options = (app_state.build_window_options)(None, cx);
5691                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5692                cx.open_window(options, |cx| {
5693                    cx.new_view(|cx| {
5694                        Workspace::new(Default::default(), project, app_state.clone(), cx)
5695                    })
5696                })
5697            })??
5698        };
5699
5700        workspace.update(&mut cx, |workspace, cx| {
5701            cx.activate(true);
5702            cx.activate_window();
5703
5704            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5705                let follow_peer_id = room
5706                    .read(cx)
5707                    .remote_participants()
5708                    .iter()
5709                    .find(|(_, participant)| participant.user.id == follow_user_id)
5710                    .map(|(_, p)| p.peer_id)
5711                    .or_else(|| {
5712                        // If we couldn't follow the given user, follow the host instead.
5713                        let collaborator = workspace
5714                            .project()
5715                            .read(cx)
5716                            .collaborators()
5717                            .values()
5718                            .find(|collaborator| collaborator.is_host)?;
5719                        Some(collaborator.peer_id)
5720                    });
5721
5722                if let Some(follow_peer_id) = follow_peer_id {
5723                    workspace.follow(follow_peer_id, cx);
5724                }
5725            }
5726        })?;
5727
5728        anyhow::Ok(())
5729    })
5730}
5731
5732pub fn reload(reload: &Reload, cx: &mut AppContext) {
5733    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5734    let mut workspace_windows = cx
5735        .windows()
5736        .into_iter()
5737        .filter_map(|window| window.downcast::<Workspace>())
5738        .collect::<Vec<_>>();
5739
5740    // If multiple windows have unsaved changes, and need a save prompt,
5741    // prompt in the active window before switching to a different window.
5742    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5743
5744    let mut prompt = None;
5745    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5746        prompt = window
5747            .update(cx, |_, cx| {
5748                cx.prompt(
5749                    PromptLevel::Info,
5750                    "Are you sure you want to restart?",
5751                    None,
5752                    &["Restart", "Cancel"],
5753                )
5754            })
5755            .ok();
5756    }
5757
5758    let binary_path = reload.binary_path.clone();
5759    cx.spawn(|mut cx| async move {
5760        if let Some(prompt) = prompt {
5761            let answer = prompt.await?;
5762            if answer != 0 {
5763                return Ok(());
5764            }
5765        }
5766
5767        // If the user cancels any save prompt, then keep the app open.
5768        for window in workspace_windows {
5769            if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5770                workspace.prepare_to_close(CloseIntent::Quit, cx)
5771            }) {
5772                if !should_close.await? {
5773                    return Ok(());
5774                }
5775            }
5776        }
5777
5778        cx.update(|cx| cx.restart(binary_path))
5779    })
5780    .detach_and_log_err(cx);
5781}
5782
5783fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5784    let mut parts = value.split(',');
5785    let x: usize = parts.next()?.parse().ok()?;
5786    let y: usize = parts.next()?.parse().ok()?;
5787    Some(point(px(x as f32), px(y as f32)))
5788}
5789
5790fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5791    let mut parts = value.split(',');
5792    let width: usize = parts.next()?.parse().ok()?;
5793    let height: usize = parts.next()?.parse().ok()?;
5794    Some(size(px(width as f32), px(height as f32)))
5795}
5796
5797pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5798    const BORDER_SIZE: Pixels = px(1.0);
5799    let decorations = cx.window_decorations();
5800
5801    if matches!(decorations, Decorations::Client { .. }) {
5802        cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5803    }
5804
5805    struct GlobalResizeEdge(ResizeEdge);
5806    impl Global for GlobalResizeEdge {}
5807
5808    div()
5809        .id("window-backdrop")
5810        .bg(transparent_black())
5811        .map(|div| match decorations {
5812            Decorations::Server => div,
5813            Decorations::Client { tiling, .. } => div
5814                .when(!(tiling.top || tiling.right), |div| {
5815                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5816                })
5817                .when(!(tiling.top || tiling.left), |div| {
5818                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5819                })
5820                .when(!(tiling.bottom || tiling.right), |div| {
5821                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5822                })
5823                .when(!(tiling.bottom || tiling.left), |div| {
5824                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5825                })
5826                .when(!tiling.top, |div| {
5827                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
5828                })
5829                .when(!tiling.bottom, |div| {
5830                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
5831                })
5832                .when(!tiling.left, |div| {
5833                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
5834                })
5835                .when(!tiling.right, |div| {
5836                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
5837                })
5838                .on_mouse_move(move |e, cx| {
5839                    let size = cx.window_bounds().get_bounds().size;
5840                    let pos = e.position;
5841
5842                    let new_edge =
5843                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
5844
5845                    let edge = cx.try_global::<GlobalResizeEdge>();
5846                    if new_edge != edge.map(|edge| edge.0) {
5847                        cx.window_handle()
5848                            .update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
5849                            .ok();
5850                    }
5851                })
5852                .on_mouse_down(MouseButton::Left, move |e, cx| {
5853                    let size = cx.window_bounds().get_bounds().size;
5854                    let pos = e.position;
5855
5856                    let edge = match resize_edge(
5857                        pos,
5858                        theme::CLIENT_SIDE_DECORATION_SHADOW,
5859                        size,
5860                        tiling,
5861                    ) {
5862                        Some(value) => value,
5863                        None => return,
5864                    };
5865
5866                    cx.start_window_resize(edge);
5867                }),
5868        })
5869        .size_full()
5870        .child(
5871            div()
5872                .cursor(CursorStyle::Arrow)
5873                .map(|div| match decorations {
5874                    Decorations::Server => div,
5875                    Decorations::Client { tiling } => div
5876                        .border_color(cx.theme().colors().border)
5877                        .when(!(tiling.top || tiling.right), |div| {
5878                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5879                        })
5880                        .when(!(tiling.top || tiling.left), |div| {
5881                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5882                        })
5883                        .when(!(tiling.bottom || tiling.right), |div| {
5884                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5885                        })
5886                        .when(!(tiling.bottom || tiling.left), |div| {
5887                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5888                        })
5889                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
5890                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
5891                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
5892                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
5893                        .when(!tiling.is_tiled(), |div| {
5894                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
5895                                color: Hsla {
5896                                    h: 0.,
5897                                    s: 0.,
5898                                    l: 0.,
5899                                    a: 0.4,
5900                                },
5901                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
5902                                spread_radius: px(0.),
5903                                offset: point(px(0.0), px(0.0)),
5904                            }])
5905                        }),
5906                })
5907                .on_mouse_move(|_e, cx| {
5908                    cx.stop_propagation();
5909                })
5910                .size_full()
5911                .child(element),
5912        )
5913        .map(|div| match decorations {
5914            Decorations::Server => div,
5915            Decorations::Client { tiling, .. } => div.child(
5916                canvas(
5917                    |_bounds, cx| {
5918                        cx.insert_hitbox(
5919                            Bounds::new(
5920                                point(px(0.0), px(0.0)),
5921                                cx.window_bounds().get_bounds().size,
5922                            ),
5923                            false,
5924                        )
5925                    },
5926                    move |_bounds, hitbox, cx| {
5927                        let mouse = cx.mouse_position();
5928                        let size = cx.window_bounds().get_bounds().size;
5929                        let Some(edge) =
5930                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
5931                        else {
5932                            return;
5933                        };
5934                        cx.set_global(GlobalResizeEdge(edge));
5935                        cx.set_cursor_style(
5936                            match edge {
5937                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
5938                                ResizeEdge::Left | ResizeEdge::Right => {
5939                                    CursorStyle::ResizeLeftRight
5940                                }
5941                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
5942                                    CursorStyle::ResizeUpLeftDownRight
5943                                }
5944                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
5945                                    CursorStyle::ResizeUpRightDownLeft
5946                                }
5947                            },
5948                            &hitbox,
5949                        );
5950                    },
5951                )
5952                .size_full()
5953                .absolute(),
5954            ),
5955        })
5956}
5957
5958fn resize_edge(
5959    pos: Point<Pixels>,
5960    shadow_size: Pixels,
5961    window_size: Size<Pixels>,
5962    tiling: Tiling,
5963) -> Option<ResizeEdge> {
5964    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
5965    if bounds.contains(&pos) {
5966        return None;
5967    }
5968
5969    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
5970    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
5971    if !tiling.top && top_left_bounds.contains(&pos) {
5972        return Some(ResizeEdge::TopLeft);
5973    }
5974
5975    let top_right_bounds = Bounds::new(
5976        Point::new(window_size.width - corner_size.width, px(0.)),
5977        corner_size,
5978    );
5979    if !tiling.top && top_right_bounds.contains(&pos) {
5980        return Some(ResizeEdge::TopRight);
5981    }
5982
5983    let bottom_left_bounds = Bounds::new(
5984        Point::new(px(0.), window_size.height - corner_size.height),
5985        corner_size,
5986    );
5987    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
5988        return Some(ResizeEdge::BottomLeft);
5989    }
5990
5991    let bottom_right_bounds = Bounds::new(
5992        Point::new(
5993            window_size.width - corner_size.width,
5994            window_size.height - corner_size.height,
5995        ),
5996        corner_size,
5997    );
5998    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
5999        return Some(ResizeEdge::BottomRight);
6000    }
6001
6002    if !tiling.top && pos.y < shadow_size {
6003        Some(ResizeEdge::Top)
6004    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6005        Some(ResizeEdge::Bottom)
6006    } else if !tiling.left && pos.x < shadow_size {
6007        Some(ResizeEdge::Left)
6008    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6009        Some(ResizeEdge::Right)
6010    } else {
6011        None
6012    }
6013}
6014
6015fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6016    if pane == active_pane {
6017        return;
6018    } else if pane.read(cx).items_len() == 0 {
6019        pane.update(cx, |_, cx| {
6020            cx.emit(pane::Event::Remove {
6021                focus_on_pane: None,
6022            });
6023        })
6024    } else {
6025        move_all_items(pane, active_pane, cx);
6026    }
6027}
6028
6029fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6030    let destination_is_different = from_pane != to_pane;
6031    let mut moved_items = 0;
6032    for (item_ix, item_handle) in from_pane
6033        .read(cx)
6034        .items()
6035        .enumerate()
6036        .map(|(ix, item)| (ix, item.clone()))
6037        .collect::<Vec<_>>()
6038    {
6039        let ix = item_ix - moved_items;
6040        if destination_is_different {
6041            // Close item from previous pane
6042            from_pane.update(cx, |source, cx| {
6043                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6044            });
6045            moved_items += 1;
6046        }
6047
6048        // This automatically removes duplicate items in the pane
6049        to_pane.update(cx, |destination, cx| {
6050            destination.add_item(item_handle, true, true, None, cx);
6051            destination.focus(cx)
6052        });
6053    }
6054}
6055
6056pub fn move_item(
6057    source: &View<Pane>,
6058    destination: &View<Pane>,
6059    item_id_to_move: EntityId,
6060    destination_index: usize,
6061    cx: &mut WindowContext<'_>,
6062) {
6063    let Some((item_ix, item_handle)) = source
6064        .read(cx)
6065        .items()
6066        .enumerate()
6067        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6068        .map(|(ix, item)| (ix, item.clone()))
6069    else {
6070        // Tab was closed during drag
6071        return;
6072    };
6073
6074    if source != destination {
6075        // Close item from previous pane
6076        source.update(cx, |source, cx| {
6077            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6078        });
6079    }
6080
6081    // This automatically removes duplicate items in the pane
6082    destination.update(cx, |destination, cx| {
6083        destination.add_item(item_handle, true, true, Some(destination_index), cx);
6084        destination.focus(cx)
6085    });
6086}
6087
6088#[cfg(test)]
6089mod tests {
6090    use std::{cell::RefCell, rc::Rc};
6091
6092    use super::*;
6093    use crate::{
6094        dock::{test::TestPanel, PanelEvent},
6095        item::{
6096            test::{TestItem, TestProjectItem},
6097            ItemEvent,
6098        },
6099    };
6100    use fs::FakeFs;
6101    use gpui::{
6102        px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6103        UpdateGlobal, VisualTestContext,
6104    };
6105    use project::{Project, ProjectEntryId};
6106    use serde_json::json;
6107    use settings::SettingsStore;
6108
6109    #[gpui::test]
6110    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6111        init_test(cx);
6112
6113        let fs = FakeFs::new(cx.executor());
6114        let project = Project::test(fs, [], cx).await;
6115        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6116
6117        // Adding an item with no ambiguity renders the tab without detail.
6118        let item1 = cx.new_view(|cx| {
6119            let mut item = TestItem::new(cx);
6120            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6121            item
6122        });
6123        workspace.update(cx, |workspace, cx| {
6124            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6125        });
6126        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6127
6128        // Adding an item that creates ambiguity increases the level of detail on
6129        // both tabs.
6130        let item2 = cx.new_view(|cx| {
6131            let mut item = TestItem::new(cx);
6132            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6133            item
6134        });
6135        workspace.update(cx, |workspace, cx| {
6136            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6137        });
6138        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6139        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6140
6141        // Adding an item that creates ambiguity increases the level of detail only
6142        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6143        // we stop at the highest detail available.
6144        let item3 = cx.new_view(|cx| {
6145            let mut item = TestItem::new(cx);
6146            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6147            item
6148        });
6149        workspace.update(cx, |workspace, cx| {
6150            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6151        });
6152        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6153        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6154        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6155    }
6156
6157    #[gpui::test]
6158    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6159        init_test(cx);
6160
6161        let fs = FakeFs::new(cx.executor());
6162        fs.insert_tree(
6163            "/root1",
6164            json!({
6165                "one.txt": "",
6166                "two.txt": "",
6167            }),
6168        )
6169        .await;
6170        fs.insert_tree(
6171            "/root2",
6172            json!({
6173                "three.txt": "",
6174            }),
6175        )
6176        .await;
6177
6178        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6179        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6180        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6181        let worktree_id = project.update(cx, |project, cx| {
6182            project.worktrees(cx).next().unwrap().read(cx).id()
6183        });
6184
6185        let item1 = cx.new_view(|cx| {
6186            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6187        });
6188        let item2 = cx.new_view(|cx| {
6189            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6190        });
6191
6192        // Add an item to an empty pane
6193        workspace.update(cx, |workspace, cx| {
6194            workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6195        });
6196        project.update(cx, |project, cx| {
6197            assert_eq!(
6198                project.active_entry(),
6199                project
6200                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6201                    .map(|e| e.id)
6202            );
6203        });
6204        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
6205
6206        // Add a second item to a non-empty pane
6207        workspace.update(cx, |workspace, cx| {
6208            workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6209        });
6210        assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
6211        project.update(cx, |project, cx| {
6212            assert_eq!(
6213                project.active_entry(),
6214                project
6215                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6216                    .map(|e| e.id)
6217            );
6218        });
6219
6220        // Close the active item
6221        pane.update(cx, |pane, cx| {
6222            pane.close_active_item(&Default::default(), cx).unwrap()
6223        })
6224        .await
6225        .unwrap();
6226        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
6227        project.update(cx, |project, cx| {
6228            assert_eq!(
6229                project.active_entry(),
6230                project
6231                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6232                    .map(|e| e.id)
6233            );
6234        });
6235
6236        // Add a project folder
6237        project
6238            .update(cx, |project, cx| {
6239                project.find_or_create_worktree("root2", true, cx)
6240            })
6241            .await
6242            .unwrap();
6243        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
6244
6245        // Remove a project folder
6246        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6247        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
6248    }
6249
6250    #[gpui::test]
6251    async fn test_close_window(cx: &mut TestAppContext) {
6252        init_test(cx);
6253
6254        let fs = FakeFs::new(cx.executor());
6255        fs.insert_tree("/root", json!({ "one": "" })).await;
6256
6257        let project = Project::test(fs, ["root".as_ref()], cx).await;
6258        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6259
6260        // When there are no dirty items, there's nothing to do.
6261        let item1 = cx.new_view(TestItem::new);
6262        workspace.update(cx, |w, cx| {
6263            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6264        });
6265        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6266        assert!(task.await.unwrap());
6267
6268        // When there are dirty untitled items, prompt to save each one. If the user
6269        // cancels any prompt, then abort.
6270        let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6271        let item3 = cx.new_view(|cx| {
6272            TestItem::new(cx)
6273                .with_dirty(true)
6274                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6275        });
6276        workspace.update(cx, |w, cx| {
6277            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6278            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6279        });
6280        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6281        cx.executor().run_until_parked();
6282        cx.simulate_prompt_answer(2); // cancel save all
6283        cx.executor().run_until_parked();
6284        cx.simulate_prompt_answer(2); // cancel save all
6285        cx.executor().run_until_parked();
6286        assert!(!cx.has_pending_prompt());
6287        assert!(!task.await.unwrap());
6288    }
6289
6290    #[gpui::test]
6291    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6292        init_test(cx);
6293
6294        // Register TestItem as a serializable item
6295        cx.update(|cx| {
6296            register_serializable_item::<TestItem>(cx);
6297        });
6298
6299        let fs = FakeFs::new(cx.executor());
6300        fs.insert_tree("/root", json!({ "one": "" })).await;
6301
6302        let project = Project::test(fs, ["root".as_ref()], cx).await;
6303        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6304
6305        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6306        let item1 = cx.new_view(|cx| {
6307            TestItem::new(cx)
6308                .with_dirty(true)
6309                .with_serialize(|| Some(Task::ready(Ok(()))))
6310        });
6311        let item2 = cx.new_view(|cx| {
6312            TestItem::new(cx)
6313                .with_dirty(true)
6314                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6315                .with_serialize(|| Some(Task::ready(Ok(()))))
6316        });
6317        workspace.update(cx, |w, cx| {
6318            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6319            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6320        });
6321        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6322        assert!(task.await.unwrap());
6323    }
6324
6325    #[gpui::test]
6326    async fn test_close_pane_items(cx: &mut TestAppContext) {
6327        init_test(cx);
6328
6329        let fs = FakeFs::new(cx.executor());
6330
6331        let project = Project::test(fs, None, cx).await;
6332        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6333
6334        let item1 = cx.new_view(|cx| {
6335            TestItem::new(cx)
6336                .with_dirty(true)
6337                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6338        });
6339        let item2 = cx.new_view(|cx| {
6340            TestItem::new(cx)
6341                .with_dirty(true)
6342                .with_conflict(true)
6343                .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
6344        });
6345        let item3 = cx.new_view(|cx| {
6346            TestItem::new(cx)
6347                .with_dirty(true)
6348                .with_conflict(true)
6349                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
6350        });
6351        let item4 = cx.new_view(|cx| {
6352            TestItem::new(cx)
6353                .with_dirty(true)
6354                .with_project_items(&[TestProjectItem::new_untitled(cx)])
6355        });
6356        let pane = workspace.update(cx, |workspace, cx| {
6357            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6358            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6359            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6360            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6361            workspace.active_pane().clone()
6362        });
6363
6364        let close_items = pane.update(cx, |pane, cx| {
6365            pane.activate_item(1, true, true, cx);
6366            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6367            let item1_id = item1.item_id();
6368            let item3_id = item3.item_id();
6369            let item4_id = item4.item_id();
6370            pane.close_items(cx, SaveIntent::Close, move |id| {
6371                [item1_id, item3_id, item4_id].contains(&id)
6372            })
6373        });
6374        cx.executor().run_until_parked();
6375
6376        assert!(cx.has_pending_prompt());
6377        // Ignore "Save all" prompt
6378        cx.simulate_prompt_answer(2);
6379        cx.executor().run_until_parked();
6380        // There's a prompt to save item 1.
6381        pane.update(cx, |pane, _| {
6382            assert_eq!(pane.items_len(), 4);
6383            assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6384        });
6385        // Confirm saving item 1.
6386        cx.simulate_prompt_answer(0);
6387        cx.executor().run_until_parked();
6388
6389        // Item 1 is saved. There's a prompt to save item 3.
6390        pane.update(cx, |pane, cx| {
6391            assert_eq!(item1.read(cx).save_count, 1);
6392            assert_eq!(item1.read(cx).save_as_count, 0);
6393            assert_eq!(item1.read(cx).reload_count, 0);
6394            assert_eq!(pane.items_len(), 3);
6395            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6396        });
6397        assert!(cx.has_pending_prompt());
6398
6399        // Cancel saving item 3.
6400        cx.simulate_prompt_answer(1);
6401        cx.executor().run_until_parked();
6402
6403        // Item 3 is reloaded. There's a prompt to save item 4.
6404        pane.update(cx, |pane, cx| {
6405            assert_eq!(item3.read(cx).save_count, 0);
6406            assert_eq!(item3.read(cx).save_as_count, 0);
6407            assert_eq!(item3.read(cx).reload_count, 1);
6408            assert_eq!(pane.items_len(), 2);
6409            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6410        });
6411        assert!(cx.has_pending_prompt());
6412
6413        // Confirm saving item 4.
6414        cx.simulate_prompt_answer(0);
6415        cx.executor().run_until_parked();
6416
6417        // There's a prompt for a path for item 4.
6418        cx.simulate_new_path_selection(|_| Some(Default::default()));
6419        close_items.await.unwrap();
6420
6421        // The requested items are closed.
6422        pane.update(cx, |pane, cx| {
6423            assert_eq!(item4.read(cx).save_count, 0);
6424            assert_eq!(item4.read(cx).save_as_count, 1);
6425            assert_eq!(item4.read(cx).reload_count, 0);
6426            assert_eq!(pane.items_len(), 1);
6427            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6428        });
6429    }
6430
6431    #[gpui::test]
6432    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6433        init_test(cx);
6434
6435        let fs = FakeFs::new(cx.executor());
6436        let project = Project::test(fs, [], cx).await;
6437        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6438
6439        // Create several workspace items with single project entries, and two
6440        // workspace items with multiple project entries.
6441        let single_entry_items = (0..=4)
6442            .map(|project_entry_id| {
6443                cx.new_view(|cx| {
6444                    TestItem::new(cx)
6445                        .with_dirty(true)
6446                        .with_project_items(&[TestProjectItem::new(
6447                            project_entry_id,
6448                            &format!("{project_entry_id}.txt"),
6449                            cx,
6450                        )])
6451                })
6452            })
6453            .collect::<Vec<_>>();
6454        let item_2_3 = cx.new_view(|cx| {
6455            TestItem::new(cx)
6456                .with_dirty(true)
6457                .with_singleton(false)
6458                .with_project_items(&[
6459                    single_entry_items[2].read(cx).project_items[0].clone(),
6460                    single_entry_items[3].read(cx).project_items[0].clone(),
6461                ])
6462        });
6463        let item_3_4 = cx.new_view(|cx| {
6464            TestItem::new(cx)
6465                .with_dirty(true)
6466                .with_singleton(false)
6467                .with_project_items(&[
6468                    single_entry_items[3].read(cx).project_items[0].clone(),
6469                    single_entry_items[4].read(cx).project_items[0].clone(),
6470                ])
6471        });
6472
6473        // Create two panes that contain the following project entries:
6474        //   left pane:
6475        //     multi-entry items:   (2, 3)
6476        //     single-entry items:  0, 1, 2, 3, 4
6477        //   right pane:
6478        //     single-entry items:  1
6479        //     multi-entry items:   (3, 4)
6480        let left_pane = workspace.update(cx, |workspace, cx| {
6481            let left_pane = workspace.active_pane().clone();
6482            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6483            for item in single_entry_items {
6484                workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6485            }
6486            left_pane.update(cx, |pane, cx| {
6487                pane.activate_item(2, true, true, cx);
6488            });
6489
6490            let right_pane = workspace
6491                .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6492                .unwrap();
6493
6494            right_pane.update(cx, |pane, cx| {
6495                pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6496            });
6497
6498            left_pane
6499        });
6500
6501        cx.focus_view(&left_pane);
6502
6503        // When closing all of the items in the left pane, we should be prompted twice:
6504        // once for project entry 0, and once for project entry 2. Project entries 1,
6505        // 3, and 4 are all still open in the other paten. After those two
6506        // prompts, the task should complete.
6507
6508        let close = left_pane.update(cx, |pane, cx| {
6509            pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6510        });
6511        cx.executor().run_until_parked();
6512
6513        // Discard "Save all" prompt
6514        cx.simulate_prompt_answer(2);
6515
6516        cx.executor().run_until_parked();
6517        left_pane.update(cx, |pane, cx| {
6518            assert_eq!(
6519                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6520                &[ProjectEntryId::from_proto(0)]
6521            );
6522        });
6523        cx.simulate_prompt_answer(0);
6524
6525        cx.executor().run_until_parked();
6526        left_pane.update(cx, |pane, cx| {
6527            assert_eq!(
6528                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6529                &[ProjectEntryId::from_proto(2)]
6530            );
6531        });
6532        cx.simulate_prompt_answer(0);
6533
6534        cx.executor().run_until_parked();
6535        close.await.unwrap();
6536        left_pane.update(cx, |pane, _| {
6537            assert_eq!(pane.items_len(), 0);
6538        });
6539    }
6540
6541    #[gpui::test]
6542    async fn test_autosave(cx: &mut gpui::TestAppContext) {
6543        init_test(cx);
6544
6545        let fs = FakeFs::new(cx.executor());
6546        let project = Project::test(fs, [], cx).await;
6547        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6548        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6549
6550        let item = cx.new_view(|cx| {
6551            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6552        });
6553        let item_id = item.entity_id();
6554        workspace.update(cx, |workspace, cx| {
6555            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6556        });
6557
6558        // Autosave on window change.
6559        item.update(cx, |item, cx| {
6560            SettingsStore::update_global(cx, |settings, cx| {
6561                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6562                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
6563                })
6564            });
6565            item.is_dirty = true;
6566        });
6567
6568        // Deactivating the window saves the file.
6569        cx.deactivate_window();
6570        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6571
6572        // Re-activating the window doesn't save the file.
6573        cx.update(|cx| cx.activate_window());
6574        cx.executor().run_until_parked();
6575        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6576
6577        // Autosave on focus change.
6578        item.update(cx, |item, cx| {
6579            cx.focus_self();
6580            SettingsStore::update_global(cx, |settings, cx| {
6581                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6582                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6583                })
6584            });
6585            item.is_dirty = true;
6586        });
6587
6588        // Blurring the item saves the file.
6589        item.update(cx, |_, cx| cx.blur());
6590        cx.executor().run_until_parked();
6591        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6592
6593        // Deactivating the window still saves the file.
6594        item.update(cx, |item, cx| {
6595            cx.focus_self();
6596            item.is_dirty = true;
6597        });
6598        cx.deactivate_window();
6599        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6600
6601        // Autosave after delay.
6602        item.update(cx, |item, cx| {
6603            SettingsStore::update_global(cx, |settings, cx| {
6604                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6605                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6606                })
6607            });
6608            item.is_dirty = true;
6609            cx.emit(ItemEvent::Edit);
6610        });
6611
6612        // Delay hasn't fully expired, so the file is still dirty and unsaved.
6613        cx.executor().advance_clock(Duration::from_millis(250));
6614        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6615
6616        // After delay expires, the file is saved.
6617        cx.executor().advance_clock(Duration::from_millis(250));
6618        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6619
6620        // Autosave on focus change, ensuring closing the tab counts as such.
6621        item.update(cx, |item, cx| {
6622            SettingsStore::update_global(cx, |settings, cx| {
6623                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6624                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6625                })
6626            });
6627            item.is_dirty = true;
6628        });
6629
6630        pane.update(cx, |pane, cx| {
6631            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6632        })
6633        .await
6634        .unwrap();
6635        assert!(!cx.has_pending_prompt());
6636        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6637
6638        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6639        workspace.update(cx, |workspace, cx| {
6640            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6641        });
6642        item.update(cx, |item, cx| {
6643            item.project_items[0].update(cx, |item, _| {
6644                item.entry_id = None;
6645            });
6646            item.is_dirty = true;
6647            cx.blur();
6648        });
6649        cx.run_until_parked();
6650        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6651
6652        // Ensure autosave is prevented for deleted files also when closing the buffer.
6653        let _close_items = pane.update(cx, |pane, cx| {
6654            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6655        });
6656        cx.run_until_parked();
6657        assert!(cx.has_pending_prompt());
6658        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6659    }
6660
6661    #[gpui::test]
6662    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6663        init_test(cx);
6664
6665        let fs = FakeFs::new(cx.executor());
6666
6667        let project = Project::test(fs, [], cx).await;
6668        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6669
6670        let item = cx.new_view(|cx| {
6671            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6672        });
6673        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6674        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6675        let toolbar_notify_count = Rc::new(RefCell::new(0));
6676
6677        workspace.update(cx, |workspace, cx| {
6678            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6679            let toolbar_notification_count = toolbar_notify_count.clone();
6680            cx.observe(&toolbar, move |_, _, _| {
6681                *toolbar_notification_count.borrow_mut() += 1
6682            })
6683            .detach();
6684        });
6685
6686        pane.update(cx, |pane, _| {
6687            assert!(!pane.can_navigate_backward());
6688            assert!(!pane.can_navigate_forward());
6689        });
6690
6691        item.update(cx, |item, cx| {
6692            item.set_state("one".to_string(), cx);
6693        });
6694
6695        // Toolbar must be notified to re-render the navigation buttons
6696        assert_eq!(*toolbar_notify_count.borrow(), 1);
6697
6698        pane.update(cx, |pane, _| {
6699            assert!(pane.can_navigate_backward());
6700            assert!(!pane.can_navigate_forward());
6701        });
6702
6703        workspace
6704            .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6705            .await
6706            .unwrap();
6707
6708        assert_eq!(*toolbar_notify_count.borrow(), 2);
6709        pane.update(cx, |pane, _| {
6710            assert!(!pane.can_navigate_backward());
6711            assert!(pane.can_navigate_forward());
6712        });
6713    }
6714
6715    #[gpui::test]
6716    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6717        init_test(cx);
6718        let fs = FakeFs::new(cx.executor());
6719
6720        let project = Project::test(fs, [], cx).await;
6721        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6722
6723        let panel = workspace.update(cx, |workspace, cx| {
6724            let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6725            workspace.add_panel(panel.clone(), cx);
6726
6727            workspace
6728                .right_dock()
6729                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6730
6731            panel
6732        });
6733
6734        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6735        pane.update(cx, |pane, cx| {
6736            let item = cx.new_view(TestItem::new);
6737            pane.add_item(Box::new(item), true, true, None, cx);
6738        });
6739
6740        // Transfer focus from center to panel
6741        workspace.update(cx, |workspace, cx| {
6742            workspace.toggle_panel_focus::<TestPanel>(cx);
6743        });
6744
6745        workspace.update(cx, |workspace, cx| {
6746            assert!(workspace.right_dock().read(cx).is_open());
6747            assert!(!panel.is_zoomed(cx));
6748            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6749        });
6750
6751        // Transfer focus from panel to center
6752        workspace.update(cx, |workspace, cx| {
6753            workspace.toggle_panel_focus::<TestPanel>(cx);
6754        });
6755
6756        workspace.update(cx, |workspace, cx| {
6757            assert!(workspace.right_dock().read(cx).is_open());
6758            assert!(!panel.is_zoomed(cx));
6759            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6760        });
6761
6762        // Close the dock
6763        workspace.update(cx, |workspace, cx| {
6764            workspace.toggle_dock(DockPosition::Right, cx);
6765        });
6766
6767        workspace.update(cx, |workspace, cx| {
6768            assert!(!workspace.right_dock().read(cx).is_open());
6769            assert!(!panel.is_zoomed(cx));
6770            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6771        });
6772
6773        // Open the dock
6774        workspace.update(cx, |workspace, cx| {
6775            workspace.toggle_dock(DockPosition::Right, cx);
6776        });
6777
6778        workspace.update(cx, |workspace, cx| {
6779            assert!(workspace.right_dock().read(cx).is_open());
6780            assert!(!panel.is_zoomed(cx));
6781            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6782        });
6783
6784        // Focus and zoom panel
6785        panel.update(cx, |panel, cx| {
6786            cx.focus_self();
6787            panel.set_zoomed(true, cx)
6788        });
6789
6790        workspace.update(cx, |workspace, cx| {
6791            assert!(workspace.right_dock().read(cx).is_open());
6792            assert!(panel.is_zoomed(cx));
6793            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6794        });
6795
6796        // Transfer focus to the center closes the dock
6797        workspace.update(cx, |workspace, cx| {
6798            workspace.toggle_panel_focus::<TestPanel>(cx);
6799        });
6800
6801        workspace.update(cx, |workspace, cx| {
6802            assert!(!workspace.right_dock().read(cx).is_open());
6803            assert!(panel.is_zoomed(cx));
6804            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6805        });
6806
6807        // Transferring focus back to the panel keeps it zoomed
6808        workspace.update(cx, |workspace, cx| {
6809            workspace.toggle_panel_focus::<TestPanel>(cx);
6810        });
6811
6812        workspace.update(cx, |workspace, cx| {
6813            assert!(workspace.right_dock().read(cx).is_open());
6814            assert!(panel.is_zoomed(cx));
6815            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6816        });
6817
6818        // Close the dock while it is zoomed
6819        workspace.update(cx, |workspace, cx| {
6820            workspace.toggle_dock(DockPosition::Right, cx)
6821        });
6822
6823        workspace.update(cx, |workspace, cx| {
6824            assert!(!workspace.right_dock().read(cx).is_open());
6825            assert!(panel.is_zoomed(cx));
6826            assert!(workspace.zoomed.is_none());
6827            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6828        });
6829
6830        // Opening the dock, when it's zoomed, retains focus
6831        workspace.update(cx, |workspace, cx| {
6832            workspace.toggle_dock(DockPosition::Right, cx)
6833        });
6834
6835        workspace.update(cx, |workspace, cx| {
6836            assert!(workspace.right_dock().read(cx).is_open());
6837            assert!(panel.is_zoomed(cx));
6838            assert!(workspace.zoomed.is_some());
6839            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6840        });
6841
6842        // Unzoom and close the panel, zoom the active pane.
6843        panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
6844        workspace.update(cx, |workspace, cx| {
6845            workspace.toggle_dock(DockPosition::Right, cx)
6846        });
6847        pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
6848
6849        // Opening a dock unzooms the pane.
6850        workspace.update(cx, |workspace, cx| {
6851            workspace.toggle_dock(DockPosition::Right, cx)
6852        });
6853        workspace.update(cx, |workspace, cx| {
6854            let pane = pane.read(cx);
6855            assert!(!pane.is_zoomed());
6856            assert!(!pane.focus_handle(cx).is_focused(cx));
6857            assert!(workspace.right_dock().read(cx).is_open());
6858            assert!(workspace.zoomed.is_none());
6859        });
6860    }
6861
6862    #[gpui::test]
6863    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
6864        init_test(cx);
6865
6866        let fs = FakeFs::new(cx.executor());
6867
6868        let project = Project::test(fs, None, cx).await;
6869        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6870
6871        // Let's arrange the panes like this:
6872        //
6873        // +-----------------------+
6874        // |         top           |
6875        // +------+--------+-------+
6876        // | left | center | right |
6877        // +------+--------+-------+
6878        // |        bottom         |
6879        // +-----------------------+
6880
6881        let top_item = cx.new_view(|cx| {
6882            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
6883        });
6884        let bottom_item = cx.new_view(|cx| {
6885            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
6886        });
6887        let left_item = cx.new_view(|cx| {
6888            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
6889        });
6890        let right_item = cx.new_view(|cx| {
6891            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
6892        });
6893        let center_item = cx.new_view(|cx| {
6894            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
6895        });
6896
6897        let top_pane_id = workspace.update(cx, |workspace, cx| {
6898            let top_pane_id = workspace.active_pane().entity_id();
6899            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
6900            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
6901            top_pane_id
6902        });
6903        let bottom_pane_id = workspace.update(cx, |workspace, cx| {
6904            let bottom_pane_id = workspace.active_pane().entity_id();
6905            workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
6906            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
6907            bottom_pane_id
6908        });
6909        let left_pane_id = workspace.update(cx, |workspace, cx| {
6910            let left_pane_id = workspace.active_pane().entity_id();
6911            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
6912            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6913            left_pane_id
6914        });
6915        let right_pane_id = workspace.update(cx, |workspace, cx| {
6916            let right_pane_id = workspace.active_pane().entity_id();
6917            workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
6918            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
6919            right_pane_id
6920        });
6921        let center_pane_id = workspace.update(cx, |workspace, cx| {
6922            let center_pane_id = workspace.active_pane().entity_id();
6923            workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
6924            center_pane_id
6925        });
6926        cx.executor().run_until_parked();
6927
6928        workspace.update(cx, |workspace, cx| {
6929            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
6930
6931            // Join into next from center pane into right
6932            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6933        });
6934
6935        workspace.update(cx, |workspace, cx| {
6936            let active_pane = workspace.active_pane();
6937            assert_eq!(right_pane_id, active_pane.entity_id());
6938            assert_eq!(2, active_pane.read(cx).items_len());
6939            let item_ids_in_pane =
6940                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6941            assert!(item_ids_in_pane.contains(&center_item.item_id()));
6942            assert!(item_ids_in_pane.contains(&right_item.item_id()));
6943
6944            // Join into next from right pane into bottom
6945            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6946        });
6947
6948        workspace.update(cx, |workspace, cx| {
6949            let active_pane = workspace.active_pane();
6950            assert_eq!(bottom_pane_id, active_pane.entity_id());
6951            assert_eq!(3, active_pane.read(cx).items_len());
6952            let item_ids_in_pane =
6953                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6954            assert!(item_ids_in_pane.contains(&center_item.item_id()));
6955            assert!(item_ids_in_pane.contains(&right_item.item_id()));
6956            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6957
6958            // Join into next from bottom pane into left
6959            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6960        });
6961
6962        workspace.update(cx, |workspace, cx| {
6963            let active_pane = workspace.active_pane();
6964            assert_eq!(left_pane_id, active_pane.entity_id());
6965            assert_eq!(4, active_pane.read(cx).items_len());
6966            let item_ids_in_pane =
6967                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6968            assert!(item_ids_in_pane.contains(&center_item.item_id()));
6969            assert!(item_ids_in_pane.contains(&right_item.item_id()));
6970            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6971            assert!(item_ids_in_pane.contains(&left_item.item_id()));
6972
6973            // Join into next from left pane into top
6974            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6975        });
6976
6977        workspace.update(cx, |workspace, cx| {
6978            let active_pane = workspace.active_pane();
6979            assert_eq!(top_pane_id, active_pane.entity_id());
6980            assert_eq!(5, active_pane.read(cx).items_len());
6981            let item_ids_in_pane =
6982                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6983            assert!(item_ids_in_pane.contains(&center_item.item_id()));
6984            assert!(item_ids_in_pane.contains(&right_item.item_id()));
6985            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6986            assert!(item_ids_in_pane.contains(&left_item.item_id()));
6987            assert!(item_ids_in_pane.contains(&top_item.item_id()));
6988
6989            // Single pane left: no-op
6990            workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
6991        });
6992
6993        workspace.update(cx, |workspace, _cx| {
6994            let active_pane = workspace.active_pane();
6995            assert_eq!(top_pane_id, active_pane.entity_id());
6996        });
6997    }
6998
6999    fn add_an_item_to_active_pane(
7000        cx: &mut VisualTestContext,
7001        workspace: &View<Workspace>,
7002        item_id: u64,
7003    ) -> View<TestItem> {
7004        let item = cx.new_view(|cx| {
7005            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7006                item_id,
7007                "item{item_id}.txt",
7008                cx,
7009            )])
7010        });
7011        workspace.update(cx, |workspace, cx| {
7012            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7013        });
7014        return item;
7015    }
7016
7017    fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7018        return workspace.update(cx, |workspace, cx| {
7019            let new_pane =
7020                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7021            new_pane
7022        });
7023    }
7024
7025    #[gpui::test]
7026    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7027        init_test(cx);
7028        let fs = FakeFs::new(cx.executor());
7029        let project = Project::test(fs, None, cx).await;
7030        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7031
7032        add_an_item_to_active_pane(cx, &workspace, 1);
7033        split_pane(cx, &workspace);
7034        add_an_item_to_active_pane(cx, &workspace, 2);
7035        split_pane(cx, &workspace); // empty pane
7036        split_pane(cx, &workspace);
7037        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7038
7039        cx.executor().run_until_parked();
7040
7041        workspace.update(cx, |workspace, cx| {
7042            let num_panes = workspace.panes().len();
7043            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7044            let active_item = workspace
7045                .active_pane()
7046                .read(cx)
7047                .active_item()
7048                .expect("item is in focus");
7049
7050            assert_eq!(num_panes, 4);
7051            assert_eq!(num_items_in_current_pane, 1);
7052            assert_eq!(active_item.item_id(), last_item.item_id());
7053        });
7054
7055        workspace.update(cx, |workspace, cx| {
7056            workspace.join_all_panes(cx);
7057        });
7058
7059        workspace.update(cx, |workspace, cx| {
7060            let num_panes = workspace.panes().len();
7061            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7062            let active_item = workspace
7063                .active_pane()
7064                .read(cx)
7065                .active_item()
7066                .expect("item is in focus");
7067
7068            assert_eq!(num_panes, 1);
7069            assert_eq!(num_items_in_current_pane, 3);
7070            assert_eq!(active_item.item_id(), last_item.item_id());
7071        });
7072    }
7073    struct TestModal(FocusHandle);
7074
7075    impl TestModal {
7076        fn new(cx: &mut ViewContext<Self>) -> Self {
7077            Self(cx.focus_handle())
7078        }
7079    }
7080
7081    impl EventEmitter<DismissEvent> for TestModal {}
7082
7083    impl FocusableView for TestModal {
7084        fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7085            self.0.clone()
7086        }
7087    }
7088
7089    impl ModalView for TestModal {}
7090
7091    impl Render for TestModal {
7092        fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7093            div().track_focus(&self.0)
7094        }
7095    }
7096
7097    #[gpui::test]
7098    async fn test_panels(cx: &mut gpui::TestAppContext) {
7099        init_test(cx);
7100        let fs = FakeFs::new(cx.executor());
7101
7102        let project = Project::test(fs, [], cx).await;
7103        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7104
7105        let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7106            let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7107            workspace.add_panel(panel_1.clone(), cx);
7108            workspace
7109                .left_dock()
7110                .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
7111            let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7112            workspace.add_panel(panel_2.clone(), cx);
7113            workspace
7114                .right_dock()
7115                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
7116
7117            let left_dock = workspace.left_dock();
7118            assert_eq!(
7119                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7120                panel_1.panel_id()
7121            );
7122            assert_eq!(
7123                left_dock.read(cx).active_panel_size(cx).unwrap(),
7124                panel_1.size(cx)
7125            );
7126
7127            left_dock.update(cx, |left_dock, cx| {
7128                left_dock.resize_active_panel(Some(px(1337.)), cx)
7129            });
7130            assert_eq!(
7131                workspace
7132                    .right_dock()
7133                    .read(cx)
7134                    .visible_panel()
7135                    .unwrap()
7136                    .panel_id(),
7137                panel_2.panel_id(),
7138            );
7139
7140            (panel_1, panel_2)
7141        });
7142
7143        // Move panel_1 to the right
7144        panel_1.update(cx, |panel_1, cx| {
7145            panel_1.set_position(DockPosition::Right, cx)
7146        });
7147
7148        workspace.update(cx, |workspace, cx| {
7149            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7150            // Since it was the only panel on the left, the left dock should now be closed.
7151            assert!(!workspace.left_dock().read(cx).is_open());
7152            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7153            let right_dock = workspace.right_dock();
7154            assert_eq!(
7155                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7156                panel_1.panel_id()
7157            );
7158            assert_eq!(
7159                right_dock.read(cx).active_panel_size(cx).unwrap(),
7160                px(1337.)
7161            );
7162
7163            // Now we move panel_2 to the left
7164            panel_2.set_position(DockPosition::Left, cx);
7165        });
7166
7167        workspace.update(cx, |workspace, cx| {
7168            // Since panel_2 was not visible on the right, we don't open the left dock.
7169            assert!(!workspace.left_dock().read(cx).is_open());
7170            // And the right dock is unaffected in its displaying of panel_1
7171            assert!(workspace.right_dock().read(cx).is_open());
7172            assert_eq!(
7173                workspace
7174                    .right_dock()
7175                    .read(cx)
7176                    .visible_panel()
7177                    .unwrap()
7178                    .panel_id(),
7179                panel_1.panel_id(),
7180            );
7181        });
7182
7183        // Move panel_1 back to the left
7184        panel_1.update(cx, |panel_1, cx| {
7185            panel_1.set_position(DockPosition::Left, cx)
7186        });
7187
7188        workspace.update(cx, |workspace, cx| {
7189            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7190            let left_dock = workspace.left_dock();
7191            assert!(left_dock.read(cx).is_open());
7192            assert_eq!(
7193                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7194                panel_1.panel_id()
7195            );
7196            assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7197            // And the right dock should be closed as it no longer has any panels.
7198            assert!(!workspace.right_dock().read(cx).is_open());
7199
7200            // Now we move panel_1 to the bottom
7201            panel_1.set_position(DockPosition::Bottom, cx);
7202        });
7203
7204        workspace.update(cx, |workspace, cx| {
7205            // Since panel_1 was visible on the left, we close the left dock.
7206            assert!(!workspace.left_dock().read(cx).is_open());
7207            // The bottom dock is sized based on the panel's default size,
7208            // since the panel orientation changed from vertical to horizontal.
7209            let bottom_dock = workspace.bottom_dock();
7210            assert_eq!(
7211                bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7212                panel_1.size(cx),
7213            );
7214            // Close bottom dock and move panel_1 back to the left.
7215            bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7216            panel_1.set_position(DockPosition::Left, cx);
7217        });
7218
7219        // Emit activated event on panel 1
7220        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7221
7222        // Now the left dock is open and panel_1 is active and focused.
7223        workspace.update(cx, |workspace, cx| {
7224            let left_dock = workspace.left_dock();
7225            assert!(left_dock.read(cx).is_open());
7226            assert_eq!(
7227                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7228                panel_1.panel_id(),
7229            );
7230            assert!(panel_1.focus_handle(cx).is_focused(cx));
7231        });
7232
7233        // Emit closed event on panel 2, which is not active
7234        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7235
7236        // Wo don't close the left dock, because panel_2 wasn't the active panel
7237        workspace.update(cx, |workspace, cx| {
7238            let left_dock = workspace.left_dock();
7239            assert!(left_dock.read(cx).is_open());
7240            assert_eq!(
7241                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7242                panel_1.panel_id(),
7243            );
7244        });
7245
7246        // Emitting a ZoomIn event shows the panel as zoomed.
7247        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7248        workspace.update(cx, |workspace, _| {
7249            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7250            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7251        });
7252
7253        // Move panel to another dock while it is zoomed
7254        panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7255        workspace.update(cx, |workspace, _| {
7256            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7257
7258            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7259        });
7260
7261        // This is a helper for getting a:
7262        // - valid focus on an element,
7263        // - that isn't a part of the panes and panels system of the Workspace,
7264        // - and doesn't trigger the 'on_focus_lost' API.
7265        let focus_other_view = {
7266            let workspace = workspace.clone();
7267            move |cx: &mut VisualTestContext| {
7268                workspace.update(cx, |workspace, cx| {
7269                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7270                        workspace.toggle_modal(cx, TestModal::new);
7271                        workspace.toggle_modal(cx, TestModal::new);
7272                    } else {
7273                        workspace.toggle_modal(cx, TestModal::new);
7274                    }
7275                })
7276            }
7277        };
7278
7279        // If focus is transferred to another view that's not a panel or another pane, we still show
7280        // the panel as zoomed.
7281        focus_other_view(cx);
7282        workspace.update(cx, |workspace, _| {
7283            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7284            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7285        });
7286
7287        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7288        workspace.update(cx, |_, cx| cx.focus_self());
7289        workspace.update(cx, |workspace, _| {
7290            assert_eq!(workspace.zoomed, None);
7291            assert_eq!(workspace.zoomed_position, None);
7292        });
7293
7294        // If focus is transferred again to another view that's not a panel or a pane, we won't
7295        // show the panel as zoomed because it wasn't zoomed before.
7296        focus_other_view(cx);
7297        workspace.update(cx, |workspace, _| {
7298            assert_eq!(workspace.zoomed, None);
7299            assert_eq!(workspace.zoomed_position, None);
7300        });
7301
7302        // When the panel is activated, it is zoomed again.
7303        cx.dispatch_action(ToggleRightDock);
7304        workspace.update(cx, |workspace, _| {
7305            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7306            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7307        });
7308
7309        // Emitting a ZoomOut event unzooms the panel.
7310        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7311        workspace.update(cx, |workspace, _| {
7312            assert_eq!(workspace.zoomed, None);
7313            assert_eq!(workspace.zoomed_position, None);
7314        });
7315
7316        // Emit closed event on panel 1, which is active
7317        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7318
7319        // Now the left dock is closed, because panel_1 was the active panel
7320        workspace.update(cx, |workspace, cx| {
7321            let right_dock = workspace.right_dock();
7322            assert!(!right_dock.read(cx).is_open());
7323        });
7324    }
7325
7326    mod register_project_item_tests {
7327        use ui::Context as _;
7328
7329        use super::*;
7330
7331        // View
7332        struct TestPngItemView {
7333            focus_handle: FocusHandle,
7334        }
7335        // Model
7336        struct TestPngItem {}
7337
7338        impl project::Item for TestPngItem {
7339            fn try_open(
7340                _project: &Model<Project>,
7341                path: &ProjectPath,
7342                cx: &mut AppContext,
7343            ) -> Option<Task<gpui::Result<Model<Self>>>> {
7344                if path.path.extension().unwrap() == "png" {
7345                    Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
7346                } else {
7347                    None
7348                }
7349            }
7350
7351            fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7352                None
7353            }
7354
7355            fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7356                None
7357            }
7358        }
7359
7360        impl Item for TestPngItemView {
7361            type Event = ();
7362        }
7363        impl EventEmitter<()> for TestPngItemView {}
7364        impl FocusableView for TestPngItemView {
7365            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7366                self.focus_handle.clone()
7367            }
7368        }
7369
7370        impl Render for TestPngItemView {
7371            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7372                Empty
7373            }
7374        }
7375
7376        impl ProjectItem for TestPngItemView {
7377            type Item = TestPngItem;
7378
7379            fn for_project_item(
7380                _project: Model<Project>,
7381                _item: Model<Self::Item>,
7382                cx: &mut ViewContext<Self>,
7383            ) -> Self
7384            where
7385                Self: Sized,
7386            {
7387                Self {
7388                    focus_handle: cx.focus_handle(),
7389                }
7390            }
7391        }
7392
7393        // View
7394        struct TestIpynbItemView {
7395            focus_handle: FocusHandle,
7396        }
7397        // Model
7398        struct TestIpynbItem {}
7399
7400        impl project::Item for TestIpynbItem {
7401            fn try_open(
7402                _project: &Model<Project>,
7403                path: &ProjectPath,
7404                cx: &mut AppContext,
7405            ) -> Option<Task<gpui::Result<Model<Self>>>> {
7406                if path.path.extension().unwrap() == "ipynb" {
7407                    Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
7408                } else {
7409                    None
7410                }
7411            }
7412
7413            fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7414                None
7415            }
7416
7417            fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7418                None
7419            }
7420        }
7421
7422        impl Item for TestIpynbItemView {
7423            type Event = ();
7424        }
7425        impl EventEmitter<()> for TestIpynbItemView {}
7426        impl FocusableView for TestIpynbItemView {
7427            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7428                self.focus_handle.clone()
7429            }
7430        }
7431
7432        impl Render for TestIpynbItemView {
7433            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7434                Empty
7435            }
7436        }
7437
7438        impl ProjectItem for TestIpynbItemView {
7439            type Item = TestIpynbItem;
7440
7441            fn for_project_item(
7442                _project: Model<Project>,
7443                _item: Model<Self::Item>,
7444                cx: &mut ViewContext<Self>,
7445            ) -> Self
7446            where
7447                Self: Sized,
7448            {
7449                Self {
7450                    focus_handle: cx.focus_handle(),
7451                }
7452            }
7453        }
7454
7455        struct TestAlternatePngItemView {
7456            focus_handle: FocusHandle,
7457        }
7458
7459        impl Item for TestAlternatePngItemView {
7460            type Event = ();
7461        }
7462
7463        impl EventEmitter<()> for TestAlternatePngItemView {}
7464        impl FocusableView for TestAlternatePngItemView {
7465            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7466                self.focus_handle.clone()
7467            }
7468        }
7469
7470        impl Render for TestAlternatePngItemView {
7471            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7472                Empty
7473            }
7474        }
7475
7476        impl ProjectItem for TestAlternatePngItemView {
7477            type Item = TestPngItem;
7478
7479            fn for_project_item(
7480                _project: Model<Project>,
7481                _item: Model<Self::Item>,
7482                cx: &mut ViewContext<Self>,
7483            ) -> Self
7484            where
7485                Self: Sized,
7486            {
7487                Self {
7488                    focus_handle: cx.focus_handle(),
7489                }
7490            }
7491        }
7492
7493        #[gpui::test]
7494        async fn test_register_project_item(cx: &mut TestAppContext) {
7495            init_test(cx);
7496
7497            cx.update(|cx| {
7498                register_project_item::<TestPngItemView>(cx);
7499                register_project_item::<TestIpynbItemView>(cx);
7500            });
7501
7502            let fs = FakeFs::new(cx.executor());
7503            fs.insert_tree(
7504                "/root1",
7505                json!({
7506                    "one.png": "BINARYDATAHERE",
7507                    "two.ipynb": "{ totally a notebook }",
7508                    "three.txt": "editing text, sure why not?"
7509                }),
7510            )
7511            .await;
7512
7513            let project = Project::test(fs, ["root1".as_ref()], cx).await;
7514            let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7515
7516            let worktree_id = project.update(cx, |project, cx| {
7517                project.worktrees(cx).next().unwrap().read(cx).id()
7518            });
7519
7520            let handle = workspace
7521                .update(cx, |workspace, cx| {
7522                    let project_path = (worktree_id, "one.png");
7523                    workspace.open_path(project_path, None, true, cx)
7524                })
7525                .await
7526                .unwrap();
7527
7528            // Now we can check if the handle we got back errored or not
7529            assert_eq!(
7530                handle.to_any().entity_type(),
7531                TypeId::of::<TestPngItemView>()
7532            );
7533
7534            let handle = workspace
7535                .update(cx, |workspace, cx| {
7536                    let project_path = (worktree_id, "two.ipynb");
7537                    workspace.open_path(project_path, None, true, cx)
7538                })
7539                .await
7540                .unwrap();
7541
7542            assert_eq!(
7543                handle.to_any().entity_type(),
7544                TypeId::of::<TestIpynbItemView>()
7545            );
7546
7547            let handle = workspace
7548                .update(cx, |workspace, cx| {
7549                    let project_path = (worktree_id, "three.txt");
7550                    workspace.open_path(project_path, None, true, cx)
7551                })
7552                .await;
7553            assert!(handle.is_err());
7554        }
7555
7556        #[gpui::test]
7557        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
7558            init_test(cx);
7559
7560            cx.update(|cx| {
7561                register_project_item::<TestPngItemView>(cx);
7562                register_project_item::<TestAlternatePngItemView>(cx);
7563            });
7564
7565            let fs = FakeFs::new(cx.executor());
7566            fs.insert_tree(
7567                "/root1",
7568                json!({
7569                    "one.png": "BINARYDATAHERE",
7570                    "two.ipynb": "{ totally a notebook }",
7571                    "three.txt": "editing text, sure why not?"
7572                }),
7573            )
7574            .await;
7575
7576            let project = Project::test(fs, ["root1".as_ref()], cx).await;
7577            let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7578
7579            let worktree_id = project.update(cx, |project, cx| {
7580                project.worktrees(cx).next().unwrap().read(cx).id()
7581            });
7582
7583            let handle = workspace
7584                .update(cx, |workspace, cx| {
7585                    let project_path = (worktree_id, "one.png");
7586                    workspace.open_path(project_path, None, true, cx)
7587                })
7588                .await
7589                .unwrap();
7590
7591            // This _must_ be the second item registered
7592            assert_eq!(
7593                handle.to_any().entity_type(),
7594                TypeId::of::<TestAlternatePngItemView>()
7595            );
7596
7597            let handle = workspace
7598                .update(cx, |workspace, cx| {
7599                    let project_path = (worktree_id, "three.txt");
7600                    workspace.open_path(project_path, None, true, cx)
7601                })
7602                .await;
7603            assert!(handle.is_err());
7604        }
7605    }
7606
7607    pub fn init_test(cx: &mut TestAppContext) {
7608        cx.update(|cx| {
7609            let settings_store = SettingsStore::test(cx);
7610            cx.set_global(settings_store);
7611            theme::init(theme::LoadThemes::JustBase, cx);
7612            language::init(cx);
7613            crate::init_settings(cx);
7614            Project::init_settings(cx);
7615        });
7616    }
7617}