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