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