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