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