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    cancel_rx: oneshot::Receiver<()>,
5538    delegate: Arc<dyn SshClientDelegate>,
5539    app_state: Arc<AppState>,
5540    paths: Vec<PathBuf>,
5541    cx: &mut AppContext,
5542) -> Task<Result<()>> {
5543    let release_channel = ReleaseChannel::global(cx);
5544
5545    cx.spawn(|mut cx| async move {
5546        let (serialized_ssh_project, workspace_id, serialized_workspace) =
5547            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5548
5549        let identifier_prefix = match release_channel {
5550            ReleaseChannel::Stable => None,
5551            _ => Some(format!("{}-", release_channel.dev_name())),
5552        };
5553        let unique_identifier = format!(
5554            "{}workspace-{}",
5555            identifier_prefix.unwrap_or_default(),
5556            workspace_id.0
5557        );
5558
5559        let session = match cx
5560            .update(|cx| {
5561                remote::SshRemoteClient::new(
5562                    unique_identifier,
5563                    connection_options,
5564                    cancel_rx,
5565                    delegate,
5566                    cx,
5567                )
5568            })?
5569            .await?
5570        {
5571            Some(result) => result,
5572            None => return Ok(()),
5573        };
5574
5575        let project = cx.update(|cx| {
5576            project::Project::ssh(
5577                session,
5578                app_state.client.clone(),
5579                app_state.node_runtime.clone(),
5580                app_state.user_store.clone(),
5581                app_state.languages.clone(),
5582                app_state.fs.clone(),
5583                cx,
5584            )
5585        })?;
5586
5587        let mut project_paths_to_open = vec![];
5588        let mut project_path_errors = vec![];
5589
5590        for path in paths {
5591            let result = cx
5592                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5593                .await;
5594            match result {
5595                Ok((_, project_path)) => {
5596                    project_paths_to_open.push((path.clone(), Some(project_path)));
5597                }
5598                Err(error) => {
5599                    project_path_errors.push(error);
5600                }
5601            };
5602        }
5603
5604        if project_paths_to_open.is_empty() {
5605            return Err(project_path_errors
5606                .pop()
5607                .unwrap_or_else(|| anyhow!("no paths given")));
5608        }
5609
5610        cx.update_window(window.into(), |_, cx| {
5611            cx.replace_root_view(|cx| {
5612                let mut workspace =
5613                    Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5614
5615                workspace
5616                    .client()
5617                    .telemetry()
5618                    .report_app_event("open ssh project".to_string());
5619
5620                workspace.set_serialized_ssh_project(serialized_ssh_project);
5621                workspace
5622            });
5623        })?;
5624
5625        window
5626            .update(&mut cx, |_, cx| {
5627                cx.activate_window();
5628
5629                open_items(serialized_workspace, project_paths_to_open, app_state, cx)
5630            })?
5631            .await?;
5632
5633        window.update(&mut cx, |workspace, cx| {
5634            for error in project_path_errors {
5635                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5636                    if let Some(path) = error.error_tag("path") {
5637                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5638                    }
5639                } else {
5640                    workspace.show_error(&error, cx)
5641                }
5642            }
5643        })
5644    })
5645}
5646
5647fn serialize_ssh_project(
5648    connection_options: SshConnectionOptions,
5649    paths: Vec<PathBuf>,
5650    cx: &AsyncAppContext,
5651) -> Task<
5652    Result<(
5653        SerializedSshProject,
5654        WorkspaceId,
5655        Option<SerializedWorkspace>,
5656    )>,
5657> {
5658    cx.background_executor().spawn(async move {
5659        let serialized_ssh_project = persistence::DB
5660            .get_or_create_ssh_project(
5661                connection_options.host.clone(),
5662                connection_options.port,
5663                paths
5664                    .iter()
5665                    .map(|path| path.to_string_lossy().to_string())
5666                    .collect::<Vec<_>>(),
5667                connection_options.username.clone(),
5668            )
5669            .await?;
5670
5671        let serialized_workspace =
5672            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5673
5674        let workspace_id = if let Some(workspace_id) =
5675            serialized_workspace.as_ref().map(|workspace| workspace.id)
5676        {
5677            workspace_id
5678        } else {
5679            persistence::DB.next_id().await?
5680        };
5681
5682        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5683    })
5684}
5685
5686pub fn join_dev_server_project(
5687    dev_server_project_id: DevServerProjectId,
5688    project_id: ProjectId,
5689    app_state: Arc<AppState>,
5690    window_to_replace: Option<WindowHandle<Workspace>>,
5691    cx: &mut AppContext,
5692) -> Task<Result<WindowHandle<Workspace>>> {
5693    let windows = cx.windows();
5694    cx.spawn(|mut cx| async move {
5695        let existing_workspace = windows.into_iter().find_map(|window| {
5696            window.downcast::<Workspace>().and_then(|window| {
5697                window
5698                    .update(&mut cx, |workspace, cx| {
5699                        if workspace.project().read(cx).remote_id() == Some(project_id.0) {
5700                            Some(window)
5701                        } else {
5702                            None
5703                        }
5704                    })
5705                    .unwrap_or(None)
5706            })
5707        });
5708
5709        let serialized_workspace: Option<SerializedWorkspace> =
5710            persistence::DB.workspace_for_dev_server_project(dev_server_project_id);
5711
5712        let workspace = if let Some(existing_workspace) = existing_workspace {
5713            existing_workspace
5714        } else {
5715            let project = Project::remote(
5716                project_id.0,
5717                app_state.client.clone(),
5718                app_state.user_store.clone(),
5719                app_state.languages.clone(),
5720                app_state.fs.clone(),
5721                cx.clone(),
5722            )
5723            .await?;
5724
5725            let workspace_id = if let Some(ref serialized_workspace) = serialized_workspace {
5726                serialized_workspace.id
5727            } else {
5728                persistence::DB.next_id().await?
5729            };
5730
5731            if let Some(window_to_replace) = window_to_replace {
5732                cx.update_window(window_to_replace.into(), |_, cx| {
5733                    cx.replace_root_view(|cx| {
5734                        Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5735                    });
5736                })?;
5737                window_to_replace
5738            } else {
5739                let window_bounds_override = window_bounds_env_override();
5740                cx.update(|cx| {
5741                    let mut options = (app_state.build_window_options)(None, cx);
5742                    options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5743                    cx.open_window(options, |cx| {
5744                        cx.new_view(|cx| {
5745                            Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
5746                        })
5747                    })
5748                })??
5749            }
5750        };
5751
5752        workspace
5753            .update(&mut cx, |_, cx| {
5754                cx.activate(true);
5755                cx.activate_window();
5756                open_items(serialized_workspace, vec![], app_state, cx)
5757            })?
5758            .await?;
5759
5760        anyhow::Ok(workspace)
5761    })
5762}
5763
5764pub fn join_in_room_project(
5765    project_id: u64,
5766    follow_user_id: u64,
5767    app_state: Arc<AppState>,
5768    cx: &mut AppContext,
5769) -> Task<Result<()>> {
5770    let windows = cx.windows();
5771    cx.spawn(|mut cx| async move {
5772        let existing_workspace = windows.into_iter().find_map(|window| {
5773            window.downcast::<Workspace>().and_then(|window| {
5774                window
5775                    .update(&mut cx, |workspace, cx| {
5776                        if workspace.project().read(cx).remote_id() == Some(project_id) {
5777                            Some(window)
5778                        } else {
5779                            None
5780                        }
5781                    })
5782                    .unwrap_or(None)
5783            })
5784        });
5785
5786        let workspace = if let Some(existing_workspace) = existing_workspace {
5787            existing_workspace
5788        } else {
5789            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5790            let room = active_call
5791                .read_with(&cx, |call, _| call.room().cloned())?
5792                .ok_or_else(|| anyhow!("not in a call"))?;
5793            let project = room
5794                .update(&mut cx, |room, cx| {
5795                    room.join_project(
5796                        project_id,
5797                        app_state.languages.clone(),
5798                        app_state.fs.clone(),
5799                        cx,
5800                    )
5801                })?
5802                .await?;
5803
5804            let window_bounds_override = window_bounds_env_override();
5805            cx.update(|cx| {
5806                let mut options = (app_state.build_window_options)(None, cx);
5807                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5808                cx.open_window(options, |cx| {
5809                    cx.new_view(|cx| {
5810                        Workspace::new(Default::default(), project, app_state.clone(), cx)
5811                    })
5812                })
5813            })??
5814        };
5815
5816        workspace.update(&mut cx, |workspace, cx| {
5817            cx.activate(true);
5818            cx.activate_window();
5819
5820            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5821                let follow_peer_id = room
5822                    .read(cx)
5823                    .remote_participants()
5824                    .iter()
5825                    .find(|(_, participant)| participant.user.id == follow_user_id)
5826                    .map(|(_, p)| p.peer_id)
5827                    .or_else(|| {
5828                        // If we couldn't follow the given user, follow the host instead.
5829                        let collaborator = workspace
5830                            .project()
5831                            .read(cx)
5832                            .collaborators()
5833                            .values()
5834                            .find(|collaborator| collaborator.replica_id == 0)?;
5835                        Some(collaborator.peer_id)
5836                    });
5837
5838                if let Some(follow_peer_id) = follow_peer_id {
5839                    workspace.follow(follow_peer_id, cx);
5840                }
5841            }
5842        })?;
5843
5844        anyhow::Ok(())
5845    })
5846}
5847
5848pub fn reload(reload: &Reload, cx: &mut AppContext) {
5849    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5850    let mut workspace_windows = cx
5851        .windows()
5852        .into_iter()
5853        .filter_map(|window| window.downcast::<Workspace>())
5854        .collect::<Vec<_>>();
5855
5856    // If multiple windows have unsaved changes, and need a save prompt,
5857    // prompt in the active window before switching to a different window.
5858    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5859
5860    let mut prompt = None;
5861    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5862        prompt = window
5863            .update(cx, |_, cx| {
5864                cx.prompt(
5865                    PromptLevel::Info,
5866                    "Are you sure you want to restart?",
5867                    None,
5868                    &["Restart", "Cancel"],
5869                )
5870            })
5871            .ok();
5872    }
5873
5874    let binary_path = reload.binary_path.clone();
5875    cx.spawn(|mut cx| async move {
5876        if let Some(prompt) = prompt {
5877            let answer = prompt.await?;
5878            if answer != 0 {
5879                return Ok(());
5880            }
5881        }
5882
5883        // If the user cancels any save prompt, then keep the app open.
5884        for window in workspace_windows {
5885            if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5886                workspace.prepare_to_close(CloseIntent::Quit, cx)
5887            }) {
5888                if !should_close.await? {
5889                    return Ok(());
5890                }
5891            }
5892        }
5893
5894        cx.update(|cx| cx.restart(binary_path))
5895    })
5896    .detach_and_log_err(cx);
5897}
5898
5899fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5900    let mut parts = value.split(',');
5901    let x: usize = parts.next()?.parse().ok()?;
5902    let y: usize = parts.next()?.parse().ok()?;
5903    Some(point(px(x as f32), px(y as f32)))
5904}
5905
5906fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5907    let mut parts = value.split(',');
5908    let width: usize = parts.next()?.parse().ok()?;
5909    let height: usize = parts.next()?.parse().ok()?;
5910    Some(size(px(width as f32), px(height as f32)))
5911}
5912
5913pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5914    const BORDER_SIZE: Pixels = px(1.0);
5915    let decorations = cx.window_decorations();
5916
5917    if matches!(decorations, Decorations::Client { .. }) {
5918        cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5919    }
5920
5921    struct GlobalResizeEdge(ResizeEdge);
5922    impl Global for GlobalResizeEdge {}
5923
5924    div()
5925        .id("window-backdrop")
5926        .bg(transparent_black())
5927        .map(|div| match decorations {
5928            Decorations::Server => div,
5929            Decorations::Client { tiling, .. } => div
5930                .when(!(tiling.top || tiling.right), |div| {
5931                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5932                })
5933                .when(!(tiling.top || tiling.left), |div| {
5934                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5935                })
5936                .when(!(tiling.bottom || tiling.right), |div| {
5937                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5938                })
5939                .when(!(tiling.bottom || tiling.left), |div| {
5940                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5941                })
5942                .when(!tiling.top, |div| {
5943                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
5944                })
5945                .when(!tiling.bottom, |div| {
5946                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
5947                })
5948                .when(!tiling.left, |div| {
5949                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
5950                })
5951                .when(!tiling.right, |div| {
5952                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
5953                })
5954                .on_mouse_move(move |e, cx| {
5955                    let size = cx.window_bounds().get_bounds().size;
5956                    let pos = e.position;
5957
5958                    let new_edge =
5959                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
5960
5961                    let edge = cx.try_global::<GlobalResizeEdge>();
5962                    if new_edge != edge.map(|edge| edge.0) {
5963                        cx.window_handle()
5964                            .update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
5965                            .ok();
5966                    }
5967                })
5968                .on_mouse_down(MouseButton::Left, move |e, cx| {
5969                    let size = cx.window_bounds().get_bounds().size;
5970                    let pos = e.position;
5971
5972                    let edge = match resize_edge(
5973                        pos,
5974                        theme::CLIENT_SIDE_DECORATION_SHADOW,
5975                        size,
5976                        tiling,
5977                    ) {
5978                        Some(value) => value,
5979                        None => return,
5980                    };
5981
5982                    cx.start_window_resize(edge);
5983                }),
5984        })
5985        .size_full()
5986        .child(
5987            div()
5988                .cursor(CursorStyle::Arrow)
5989                .map(|div| match decorations {
5990                    Decorations::Server => div,
5991                    Decorations::Client { tiling } => div
5992                        .border_color(cx.theme().colors().border)
5993                        .when(!(tiling.top || tiling.right), |div| {
5994                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5995                        })
5996                        .when(!(tiling.top || tiling.left), |div| {
5997                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5998                        })
5999                        .when(!(tiling.bottom || tiling.right), |div| {
6000                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6001                        })
6002                        .when(!(tiling.bottom || tiling.left), |div| {
6003                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6004                        })
6005                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6006                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6007                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6008                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6009                        .when(!tiling.is_tiled(), |div| {
6010                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
6011                                color: Hsla {
6012                                    h: 0.,
6013                                    s: 0.,
6014                                    l: 0.,
6015                                    a: 0.4,
6016                                },
6017                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6018                                spread_radius: px(0.),
6019                                offset: point(px(0.0), px(0.0)),
6020                            }])
6021                        }),
6022                })
6023                .on_mouse_move(|_e, cx| {
6024                    cx.stop_propagation();
6025                })
6026                .size_full()
6027                .child(element),
6028        )
6029        .map(|div| match decorations {
6030            Decorations::Server => div,
6031            Decorations::Client { tiling, .. } => div.child(
6032                canvas(
6033                    |_bounds, cx| {
6034                        cx.insert_hitbox(
6035                            Bounds::new(
6036                                point(px(0.0), px(0.0)),
6037                                cx.window_bounds().get_bounds().size,
6038                            ),
6039                            false,
6040                        )
6041                    },
6042                    move |_bounds, hitbox, cx| {
6043                        let mouse = cx.mouse_position();
6044                        let size = cx.window_bounds().get_bounds().size;
6045                        let Some(edge) =
6046                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6047                        else {
6048                            return;
6049                        };
6050                        cx.set_global(GlobalResizeEdge(edge));
6051                        cx.set_cursor_style(
6052                            match edge {
6053                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6054                                ResizeEdge::Left | ResizeEdge::Right => {
6055                                    CursorStyle::ResizeLeftRight
6056                                }
6057                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6058                                    CursorStyle::ResizeUpLeftDownRight
6059                                }
6060                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6061                                    CursorStyle::ResizeUpRightDownLeft
6062                                }
6063                            },
6064                            &hitbox,
6065                        );
6066                    },
6067                )
6068                .size_full()
6069                .absolute(),
6070            ),
6071        })
6072}
6073
6074fn resize_edge(
6075    pos: Point<Pixels>,
6076    shadow_size: Pixels,
6077    window_size: Size<Pixels>,
6078    tiling: Tiling,
6079) -> Option<ResizeEdge> {
6080    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6081    if bounds.contains(&pos) {
6082        return None;
6083    }
6084
6085    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6086    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6087    if !tiling.top && top_left_bounds.contains(&pos) {
6088        return Some(ResizeEdge::TopLeft);
6089    }
6090
6091    let top_right_bounds = Bounds::new(
6092        Point::new(window_size.width - corner_size.width, px(0.)),
6093        corner_size,
6094    );
6095    if !tiling.top && top_right_bounds.contains(&pos) {
6096        return Some(ResizeEdge::TopRight);
6097    }
6098
6099    let bottom_left_bounds = Bounds::new(
6100        Point::new(px(0.), window_size.height - corner_size.height),
6101        corner_size,
6102    );
6103    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6104        return Some(ResizeEdge::BottomLeft);
6105    }
6106
6107    let bottom_right_bounds = Bounds::new(
6108        Point::new(
6109            window_size.width - corner_size.width,
6110            window_size.height - corner_size.height,
6111        ),
6112        corner_size,
6113    );
6114    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6115        return Some(ResizeEdge::BottomRight);
6116    }
6117
6118    if !tiling.top && pos.y < shadow_size {
6119        Some(ResizeEdge::Top)
6120    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6121        Some(ResizeEdge::Bottom)
6122    } else if !tiling.left && pos.x < shadow_size {
6123        Some(ResizeEdge::Left)
6124    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6125        Some(ResizeEdge::Right)
6126    } else {
6127        None
6128    }
6129}
6130
6131fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6132    if pane == active_pane {
6133        return;
6134    } else if pane.read(cx).items_len() == 0 {
6135        pane.update(cx, |_, cx| {
6136            cx.emit(pane::Event::Remove {
6137                focus_on_pane: None,
6138            });
6139        })
6140    } else {
6141        move_all_items(pane, active_pane, cx);
6142    }
6143}
6144
6145fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6146    let destination_is_different = from_pane != to_pane;
6147    let mut moved_items = 0;
6148    for (item_ix, item_handle) in from_pane
6149        .read(cx)
6150        .items()
6151        .enumerate()
6152        .map(|(ix, item)| (ix, item.clone()))
6153        .collect::<Vec<_>>()
6154    {
6155        let ix = item_ix - moved_items;
6156        if destination_is_different {
6157            // Close item from previous pane
6158            from_pane.update(cx, |source, cx| {
6159                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6160            });
6161            moved_items += 1;
6162        }
6163
6164        // This automatically removes duplicate items in the pane
6165        to_pane.update(cx, |destination, cx| {
6166            destination.add_item(item_handle, true, true, None, cx);
6167            destination.focus(cx)
6168        });
6169    }
6170}
6171
6172pub fn move_item(
6173    source: &View<Pane>,
6174    destination: &View<Pane>,
6175    item_id_to_move: EntityId,
6176    destination_index: usize,
6177    cx: &mut WindowContext<'_>,
6178) {
6179    let Some((item_ix, item_handle)) = source
6180        .read(cx)
6181        .items()
6182        .enumerate()
6183        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6184        .map(|(ix, item)| (ix, item.clone()))
6185    else {
6186        // Tab was closed during drag
6187        return;
6188    };
6189
6190    if source != destination {
6191        // Close item from previous pane
6192        source.update(cx, |source, cx| {
6193            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6194        });
6195    }
6196
6197    // This automatically removes duplicate items in the pane
6198    destination.update(cx, |destination, cx| {
6199        destination.add_item(item_handle, true, true, Some(destination_index), cx);
6200        destination.focus(cx)
6201    });
6202}
6203
6204#[cfg(test)]
6205mod tests {
6206    use std::{cell::RefCell, rc::Rc};
6207
6208    use super::*;
6209    use crate::{
6210        dock::{test::TestPanel, PanelEvent},
6211        item::{
6212            test::{TestItem, TestProjectItem},
6213            ItemEvent,
6214        },
6215    };
6216    use fs::FakeFs;
6217    use gpui::{
6218        px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6219        UpdateGlobal, VisualTestContext,
6220    };
6221    use project::{Project, ProjectEntryId};
6222    use serde_json::json;
6223    use settings::SettingsStore;
6224
6225    #[gpui::test]
6226    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6227        init_test(cx);
6228
6229        let fs = FakeFs::new(cx.executor());
6230        let project = Project::test(fs, [], cx).await;
6231        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6232
6233        // Adding an item with no ambiguity renders the tab without detail.
6234        let item1 = cx.new_view(|cx| {
6235            let mut item = TestItem::new(cx);
6236            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6237            item
6238        });
6239        workspace.update(cx, |workspace, cx| {
6240            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6241        });
6242        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6243
6244        // Adding an item that creates ambiguity increases the level of detail on
6245        // both tabs.
6246        let item2 = cx.new_view(|cx| {
6247            let mut item = TestItem::new(cx);
6248            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6249            item
6250        });
6251        workspace.update(cx, |workspace, cx| {
6252            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6253        });
6254        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6255        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6256
6257        // Adding an item that creates ambiguity increases the level of detail only
6258        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6259        // we stop at the highest detail available.
6260        let item3 = cx.new_view(|cx| {
6261            let mut item = TestItem::new(cx);
6262            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6263            item
6264        });
6265        workspace.update(cx, |workspace, cx| {
6266            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6267        });
6268        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6269        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6270        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6271    }
6272
6273    #[gpui::test]
6274    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6275        init_test(cx);
6276
6277        let fs = FakeFs::new(cx.executor());
6278        fs.insert_tree(
6279            "/root1",
6280            json!({
6281                "one.txt": "",
6282                "two.txt": "",
6283            }),
6284        )
6285        .await;
6286        fs.insert_tree(
6287            "/root2",
6288            json!({
6289                "three.txt": "",
6290            }),
6291        )
6292        .await;
6293
6294        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6295        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6296        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6297        let worktree_id = project.update(cx, |project, cx| {
6298            project.worktrees(cx).next().unwrap().read(cx).id()
6299        });
6300
6301        let item1 = cx.new_view(|cx| {
6302            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6303        });
6304        let item2 = cx.new_view(|cx| {
6305            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6306        });
6307
6308        // Add an item to an empty pane
6309        workspace.update(cx, |workspace, cx| {
6310            workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6311        });
6312        project.update(cx, |project, cx| {
6313            assert_eq!(
6314                project.active_entry(),
6315                project
6316                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6317                    .map(|e| e.id)
6318            );
6319        });
6320        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
6321
6322        // Add a second item to a non-empty pane
6323        workspace.update(cx, |workspace, cx| {
6324            workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6325        });
6326        assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
6327        project.update(cx, |project, cx| {
6328            assert_eq!(
6329                project.active_entry(),
6330                project
6331                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6332                    .map(|e| e.id)
6333            );
6334        });
6335
6336        // Close the active item
6337        pane.update(cx, |pane, cx| {
6338            pane.close_active_item(&Default::default(), cx).unwrap()
6339        })
6340        .await
6341        .unwrap();
6342        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
6343        project.update(cx, |project, cx| {
6344            assert_eq!(
6345                project.active_entry(),
6346                project
6347                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6348                    .map(|e| e.id)
6349            );
6350        });
6351
6352        // Add a project folder
6353        project
6354            .update(cx, |project, cx| {
6355                project.find_or_create_worktree("root2", true, cx)
6356            })
6357            .await
6358            .unwrap();
6359        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
6360
6361        // Remove a project folder
6362        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6363        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
6364    }
6365
6366    #[gpui::test]
6367    async fn test_close_window(cx: &mut TestAppContext) {
6368        init_test(cx);
6369
6370        let fs = FakeFs::new(cx.executor());
6371        fs.insert_tree("/root", json!({ "one": "" })).await;
6372
6373        let project = Project::test(fs, ["root".as_ref()], cx).await;
6374        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6375
6376        // When there are no dirty items, there's nothing to do.
6377        let item1 = cx.new_view(TestItem::new);
6378        workspace.update(cx, |w, cx| {
6379            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6380        });
6381        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6382        assert!(task.await.unwrap());
6383
6384        // When there are dirty untitled items, prompt to save each one. If the user
6385        // cancels any prompt, then abort.
6386        let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6387        let item3 = cx.new_view(|cx| {
6388            TestItem::new(cx)
6389                .with_dirty(true)
6390                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6391        });
6392        workspace.update(cx, |w, cx| {
6393            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6394            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6395        });
6396        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6397        cx.executor().run_until_parked();
6398        cx.simulate_prompt_answer(2); // cancel save all
6399        cx.executor().run_until_parked();
6400        cx.simulate_prompt_answer(2); // cancel save all
6401        cx.executor().run_until_parked();
6402        assert!(!cx.has_pending_prompt());
6403        assert!(!task.await.unwrap());
6404    }
6405
6406    #[gpui::test]
6407    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6408        init_test(cx);
6409
6410        // Register TestItem as a serializable item
6411        cx.update(|cx| {
6412            register_serializable_item::<TestItem>(cx);
6413        });
6414
6415        let fs = FakeFs::new(cx.executor());
6416        fs.insert_tree("/root", json!({ "one": "" })).await;
6417
6418        let project = Project::test(fs, ["root".as_ref()], cx).await;
6419        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6420
6421        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6422        let item1 = cx.new_view(|cx| {
6423            TestItem::new(cx)
6424                .with_dirty(true)
6425                .with_serialize(|| Some(Task::ready(Ok(()))))
6426        });
6427        let item2 = cx.new_view(|cx| {
6428            TestItem::new(cx)
6429                .with_dirty(true)
6430                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6431                .with_serialize(|| Some(Task::ready(Ok(()))))
6432        });
6433        workspace.update(cx, |w, cx| {
6434            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6435            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6436        });
6437        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6438        assert!(task.await.unwrap());
6439    }
6440
6441    #[gpui::test]
6442    async fn test_close_pane_items(cx: &mut TestAppContext) {
6443        init_test(cx);
6444
6445        let fs = FakeFs::new(cx.executor());
6446
6447        let project = Project::test(fs, None, cx).await;
6448        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6449
6450        let item1 = cx.new_view(|cx| {
6451            TestItem::new(cx)
6452                .with_dirty(true)
6453                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6454        });
6455        let item2 = cx.new_view(|cx| {
6456            TestItem::new(cx)
6457                .with_dirty(true)
6458                .with_conflict(true)
6459                .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
6460        });
6461        let item3 = cx.new_view(|cx| {
6462            TestItem::new(cx)
6463                .with_dirty(true)
6464                .with_conflict(true)
6465                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
6466        });
6467        let item4 = cx.new_view(|cx| {
6468            TestItem::new(cx)
6469                .with_dirty(true)
6470                .with_project_items(&[TestProjectItem::new_untitled(cx)])
6471        });
6472        let pane = workspace.update(cx, |workspace, cx| {
6473            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6474            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6475            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6476            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6477            workspace.active_pane().clone()
6478        });
6479
6480        let close_items = pane.update(cx, |pane, cx| {
6481            pane.activate_item(1, true, true, cx);
6482            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6483            let item1_id = item1.item_id();
6484            let item3_id = item3.item_id();
6485            let item4_id = item4.item_id();
6486            pane.close_items(cx, SaveIntent::Close, move |id| {
6487                [item1_id, item3_id, item4_id].contains(&id)
6488            })
6489        });
6490        cx.executor().run_until_parked();
6491
6492        assert!(cx.has_pending_prompt());
6493        // Ignore "Save all" prompt
6494        cx.simulate_prompt_answer(2);
6495        cx.executor().run_until_parked();
6496        // There's a prompt to save item 1.
6497        pane.update(cx, |pane, _| {
6498            assert_eq!(pane.items_len(), 4);
6499            assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6500        });
6501        // Confirm saving item 1.
6502        cx.simulate_prompt_answer(0);
6503        cx.executor().run_until_parked();
6504
6505        // Item 1 is saved. There's a prompt to save item 3.
6506        pane.update(cx, |pane, cx| {
6507            assert_eq!(item1.read(cx).save_count, 1);
6508            assert_eq!(item1.read(cx).save_as_count, 0);
6509            assert_eq!(item1.read(cx).reload_count, 0);
6510            assert_eq!(pane.items_len(), 3);
6511            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6512        });
6513        assert!(cx.has_pending_prompt());
6514
6515        // Cancel saving item 3.
6516        cx.simulate_prompt_answer(1);
6517        cx.executor().run_until_parked();
6518
6519        // Item 3 is reloaded. There's a prompt to save item 4.
6520        pane.update(cx, |pane, cx| {
6521            assert_eq!(item3.read(cx).save_count, 0);
6522            assert_eq!(item3.read(cx).save_as_count, 0);
6523            assert_eq!(item3.read(cx).reload_count, 1);
6524            assert_eq!(pane.items_len(), 2);
6525            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6526        });
6527        assert!(cx.has_pending_prompt());
6528
6529        // Confirm saving item 4.
6530        cx.simulate_prompt_answer(0);
6531        cx.executor().run_until_parked();
6532
6533        // There's a prompt for a path for item 4.
6534        cx.simulate_new_path_selection(|_| Some(Default::default()));
6535        close_items.await.unwrap();
6536
6537        // The requested items are closed.
6538        pane.update(cx, |pane, cx| {
6539            assert_eq!(item4.read(cx).save_count, 0);
6540            assert_eq!(item4.read(cx).save_as_count, 1);
6541            assert_eq!(item4.read(cx).reload_count, 0);
6542            assert_eq!(pane.items_len(), 1);
6543            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6544        });
6545    }
6546
6547    #[gpui::test]
6548    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6549        init_test(cx);
6550
6551        let fs = FakeFs::new(cx.executor());
6552        let project = Project::test(fs, [], cx).await;
6553        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6554
6555        // Create several workspace items with single project entries, and two
6556        // workspace items with multiple project entries.
6557        let single_entry_items = (0..=4)
6558            .map(|project_entry_id| {
6559                cx.new_view(|cx| {
6560                    TestItem::new(cx)
6561                        .with_dirty(true)
6562                        .with_project_items(&[TestProjectItem::new(
6563                            project_entry_id,
6564                            &format!("{project_entry_id}.txt"),
6565                            cx,
6566                        )])
6567                })
6568            })
6569            .collect::<Vec<_>>();
6570        let item_2_3 = cx.new_view(|cx| {
6571            TestItem::new(cx)
6572                .with_dirty(true)
6573                .with_singleton(false)
6574                .with_project_items(&[
6575                    single_entry_items[2].read(cx).project_items[0].clone(),
6576                    single_entry_items[3].read(cx).project_items[0].clone(),
6577                ])
6578        });
6579        let item_3_4 = cx.new_view(|cx| {
6580            TestItem::new(cx)
6581                .with_dirty(true)
6582                .with_singleton(false)
6583                .with_project_items(&[
6584                    single_entry_items[3].read(cx).project_items[0].clone(),
6585                    single_entry_items[4].read(cx).project_items[0].clone(),
6586                ])
6587        });
6588
6589        // Create two panes that contain the following project entries:
6590        //   left pane:
6591        //     multi-entry items:   (2, 3)
6592        //     single-entry items:  0, 1, 2, 3, 4
6593        //   right pane:
6594        //     single-entry items:  1
6595        //     multi-entry items:   (3, 4)
6596        let left_pane = workspace.update(cx, |workspace, cx| {
6597            let left_pane = workspace.active_pane().clone();
6598            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6599            for item in single_entry_items {
6600                workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6601            }
6602            left_pane.update(cx, |pane, cx| {
6603                pane.activate_item(2, true, true, cx);
6604            });
6605
6606            let right_pane = workspace
6607                .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6608                .unwrap();
6609
6610            right_pane.update(cx, |pane, cx| {
6611                pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6612            });
6613
6614            left_pane
6615        });
6616
6617        cx.focus_view(&left_pane);
6618
6619        // When closing all of the items in the left pane, we should be prompted twice:
6620        // once for project entry 0, and once for project entry 2. Project entries 1,
6621        // 3, and 4 are all still open in the other paten. After those two
6622        // prompts, the task should complete.
6623
6624        let close = left_pane.update(cx, |pane, cx| {
6625            pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6626        });
6627        cx.executor().run_until_parked();
6628
6629        // Discard "Save all" prompt
6630        cx.simulate_prompt_answer(2);
6631
6632        cx.executor().run_until_parked();
6633        left_pane.update(cx, |pane, cx| {
6634            assert_eq!(
6635                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6636                &[ProjectEntryId::from_proto(0)]
6637            );
6638        });
6639        cx.simulate_prompt_answer(0);
6640
6641        cx.executor().run_until_parked();
6642        left_pane.update(cx, |pane, cx| {
6643            assert_eq!(
6644                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6645                &[ProjectEntryId::from_proto(2)]
6646            );
6647        });
6648        cx.simulate_prompt_answer(0);
6649
6650        cx.executor().run_until_parked();
6651        close.await.unwrap();
6652        left_pane.update(cx, |pane, _| {
6653            assert_eq!(pane.items_len(), 0);
6654        });
6655    }
6656
6657    #[gpui::test]
6658    async fn test_autosave(cx: &mut gpui::TestAppContext) {
6659        init_test(cx);
6660
6661        let fs = FakeFs::new(cx.executor());
6662        let project = Project::test(fs, [], cx).await;
6663        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6664        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6665
6666        let item = cx.new_view(|cx| {
6667            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6668        });
6669        let item_id = item.entity_id();
6670        workspace.update(cx, |workspace, cx| {
6671            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6672        });
6673
6674        // Autosave on window change.
6675        item.update(cx, |item, cx| {
6676            SettingsStore::update_global(cx, |settings, cx| {
6677                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6678                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
6679                })
6680            });
6681            item.is_dirty = true;
6682        });
6683
6684        // Deactivating the window saves the file.
6685        cx.deactivate_window();
6686        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6687
6688        // Re-activating the window doesn't save the file.
6689        cx.update(|cx| cx.activate_window());
6690        cx.executor().run_until_parked();
6691        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6692
6693        // Autosave on focus change.
6694        item.update(cx, |item, cx| {
6695            cx.focus_self();
6696            SettingsStore::update_global(cx, |settings, cx| {
6697                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6698                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6699                })
6700            });
6701            item.is_dirty = true;
6702        });
6703
6704        // Blurring the item saves the file.
6705        item.update(cx, |_, cx| cx.blur());
6706        cx.executor().run_until_parked();
6707        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6708
6709        // Deactivating the window still saves the file.
6710        item.update(cx, |item, cx| {
6711            cx.focus_self();
6712            item.is_dirty = true;
6713        });
6714        cx.deactivate_window();
6715        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6716
6717        // Autosave after delay.
6718        item.update(cx, |item, cx| {
6719            SettingsStore::update_global(cx, |settings, cx| {
6720                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6721                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6722                })
6723            });
6724            item.is_dirty = true;
6725            cx.emit(ItemEvent::Edit);
6726        });
6727
6728        // Delay hasn't fully expired, so the file is still dirty and unsaved.
6729        cx.executor().advance_clock(Duration::from_millis(250));
6730        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6731
6732        // After delay expires, the file is saved.
6733        cx.executor().advance_clock(Duration::from_millis(250));
6734        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6735
6736        // Autosave on focus change, ensuring closing the tab counts as such.
6737        item.update(cx, |item, cx| {
6738            SettingsStore::update_global(cx, |settings, cx| {
6739                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6740                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6741                })
6742            });
6743            item.is_dirty = true;
6744        });
6745
6746        pane.update(cx, |pane, cx| {
6747            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6748        })
6749        .await
6750        .unwrap();
6751        assert!(!cx.has_pending_prompt());
6752        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6753
6754        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6755        workspace.update(cx, |workspace, cx| {
6756            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6757        });
6758        item.update(cx, |item, cx| {
6759            item.project_items[0].update(cx, |item, _| {
6760                item.entry_id = None;
6761            });
6762            item.is_dirty = true;
6763            cx.blur();
6764        });
6765        cx.run_until_parked();
6766        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6767
6768        // Ensure autosave is prevented for deleted files also when closing the buffer.
6769        let _close_items = pane.update(cx, |pane, cx| {
6770            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6771        });
6772        cx.run_until_parked();
6773        assert!(cx.has_pending_prompt());
6774        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6775    }
6776
6777    #[gpui::test]
6778    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6779        init_test(cx);
6780
6781        let fs = FakeFs::new(cx.executor());
6782
6783        let project = Project::test(fs, [], cx).await;
6784        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6785
6786        let item = cx.new_view(|cx| {
6787            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6788        });
6789        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6790        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6791        let toolbar_notify_count = Rc::new(RefCell::new(0));
6792
6793        workspace.update(cx, |workspace, cx| {
6794            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6795            let toolbar_notification_count = toolbar_notify_count.clone();
6796            cx.observe(&toolbar, move |_, _, _| {
6797                *toolbar_notification_count.borrow_mut() += 1
6798            })
6799            .detach();
6800        });
6801
6802        pane.update(cx, |pane, _| {
6803            assert!(!pane.can_navigate_backward());
6804            assert!(!pane.can_navigate_forward());
6805        });
6806
6807        item.update(cx, |item, cx| {
6808            item.set_state("one".to_string(), cx);
6809        });
6810
6811        // Toolbar must be notified to re-render the navigation buttons
6812        assert_eq!(*toolbar_notify_count.borrow(), 1);
6813
6814        pane.update(cx, |pane, _| {
6815            assert!(pane.can_navigate_backward());
6816            assert!(!pane.can_navigate_forward());
6817        });
6818
6819        workspace
6820            .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6821            .await
6822            .unwrap();
6823
6824        assert_eq!(*toolbar_notify_count.borrow(), 2);
6825        pane.update(cx, |pane, _| {
6826            assert!(!pane.can_navigate_backward());
6827            assert!(pane.can_navigate_forward());
6828        });
6829    }
6830
6831    #[gpui::test]
6832    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6833        init_test(cx);
6834        let fs = FakeFs::new(cx.executor());
6835
6836        let project = Project::test(fs, [], cx).await;
6837        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6838
6839        let panel = workspace.update(cx, |workspace, cx| {
6840            let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6841            workspace.add_panel(panel.clone(), cx);
6842
6843            workspace
6844                .right_dock()
6845                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6846
6847            panel
6848        });
6849
6850        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6851        pane.update(cx, |pane, cx| {
6852            let item = cx.new_view(TestItem::new);
6853            pane.add_item(Box::new(item), true, true, None, cx);
6854        });
6855
6856        // Transfer focus from center to panel
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        // Transfer focus from panel to center
6868        workspace.update(cx, |workspace, cx| {
6869            workspace.toggle_panel_focus::<TestPanel>(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        // Close 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        // Open the dock
6890        workspace.update(cx, |workspace, cx| {
6891            workspace.toggle_dock(DockPosition::Right, cx);
6892        });
6893
6894        workspace.update(cx, |workspace, cx| {
6895            assert!(workspace.right_dock().read(cx).is_open());
6896            assert!(!panel.is_zoomed(cx));
6897            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6898        });
6899
6900        // Focus and zoom panel
6901        panel.update(cx, |panel, cx| {
6902            cx.focus_self();
6903            panel.set_zoomed(true, 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        // Transfer focus to the center closes the dock
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        // Transferring focus back to the panel keeps it zoomed
6924        workspace.update(cx, |workspace, cx| {
6925            workspace.toggle_panel_focus::<TestPanel>(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!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6932        });
6933
6934        // Close the dock while it is zoomed
6935        workspace.update(cx, |workspace, cx| {
6936            workspace.toggle_dock(DockPosition::Right, cx)
6937        });
6938
6939        workspace.update(cx, |workspace, cx| {
6940            assert!(!workspace.right_dock().read(cx).is_open());
6941            assert!(panel.is_zoomed(cx));
6942            assert!(workspace.zoomed.is_none());
6943            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6944        });
6945
6946        // Opening the dock, when it's zoomed, retains focus
6947        workspace.update(cx, |workspace, cx| {
6948            workspace.toggle_dock(DockPosition::Right, cx)
6949        });
6950
6951        workspace.update(cx, |workspace, cx| {
6952            assert!(workspace.right_dock().read(cx).is_open());
6953            assert!(panel.is_zoomed(cx));
6954            assert!(workspace.zoomed.is_some());
6955            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6956        });
6957
6958        // Unzoom and close the panel, zoom the active pane.
6959        panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
6960        workspace.update(cx, |workspace, cx| {
6961            workspace.toggle_dock(DockPosition::Right, cx)
6962        });
6963        pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
6964
6965        // Opening a dock unzooms the pane.
6966        workspace.update(cx, |workspace, cx| {
6967            workspace.toggle_dock(DockPosition::Right, cx)
6968        });
6969        workspace.update(cx, |workspace, cx| {
6970            let pane = pane.read(cx);
6971            assert!(!pane.is_zoomed());
6972            assert!(!pane.focus_handle(cx).is_focused(cx));
6973            assert!(workspace.right_dock().read(cx).is_open());
6974            assert!(workspace.zoomed.is_none());
6975        });
6976    }
6977
6978    #[gpui::test]
6979    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
6980        init_test(cx);
6981
6982        let fs = FakeFs::new(cx.executor());
6983
6984        let project = Project::test(fs, None, cx).await;
6985        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6986
6987        // Let's arrange the panes like this:
6988        //
6989        // +-----------------------+
6990        // |         top           |
6991        // +------+--------+-------+
6992        // | left | center | right |
6993        // +------+--------+-------+
6994        // |        bottom         |
6995        // +-----------------------+
6996
6997        let top_item = cx.new_view(|cx| {
6998            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
6999        });
7000        let bottom_item = cx.new_view(|cx| {
7001            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7002        });
7003        let left_item = cx.new_view(|cx| {
7004            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7005        });
7006        let right_item = cx.new_view(|cx| {
7007            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7008        });
7009        let center_item = cx.new_view(|cx| {
7010            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7011        });
7012
7013        let top_pane_id = workspace.update(cx, |workspace, cx| {
7014            let top_pane_id = workspace.active_pane().entity_id();
7015            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
7016            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
7017            top_pane_id
7018        });
7019        let bottom_pane_id = workspace.update(cx, |workspace, cx| {
7020            let bottom_pane_id = workspace.active_pane().entity_id();
7021            workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
7022            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
7023            bottom_pane_id
7024        });
7025        let left_pane_id = workspace.update(cx, |workspace, cx| {
7026            let left_pane_id = workspace.active_pane().entity_id();
7027            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
7028            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7029            left_pane_id
7030        });
7031        let right_pane_id = workspace.update(cx, |workspace, cx| {
7032            let right_pane_id = workspace.active_pane().entity_id();
7033            workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
7034            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
7035            right_pane_id
7036        });
7037        let center_pane_id = workspace.update(cx, |workspace, cx| {
7038            let center_pane_id = workspace.active_pane().entity_id();
7039            workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
7040            center_pane_id
7041        });
7042        cx.executor().run_until_parked();
7043
7044        workspace.update(cx, |workspace, cx| {
7045            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7046
7047            // Join into next from center pane into right
7048            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7049        });
7050
7051        workspace.update(cx, |workspace, cx| {
7052            let active_pane = workspace.active_pane();
7053            assert_eq!(right_pane_id, active_pane.entity_id());
7054            assert_eq!(2, active_pane.read(cx).items_len());
7055            let item_ids_in_pane =
7056                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7057            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7058            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7059
7060            // Join into next from right pane into bottom
7061            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7062        });
7063
7064        workspace.update(cx, |workspace, cx| {
7065            let active_pane = workspace.active_pane();
7066            assert_eq!(bottom_pane_id, active_pane.entity_id());
7067            assert_eq!(3, active_pane.read(cx).items_len());
7068            let item_ids_in_pane =
7069                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7070            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7071            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7072            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7073
7074            // Join into next from bottom pane into left
7075            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7076        });
7077
7078        workspace.update(cx, |workspace, cx| {
7079            let active_pane = workspace.active_pane();
7080            assert_eq!(left_pane_id, active_pane.entity_id());
7081            assert_eq!(4, active_pane.read(cx).items_len());
7082            let item_ids_in_pane =
7083                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7084            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7085            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7086            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7087            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7088
7089            // Join into next from left pane into top
7090            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7091        });
7092
7093        workspace.update(cx, |workspace, cx| {
7094            let active_pane = workspace.active_pane();
7095            assert_eq!(top_pane_id, active_pane.entity_id());
7096            assert_eq!(5, active_pane.read(cx).items_len());
7097            let item_ids_in_pane =
7098                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7099            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7100            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7101            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7102            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7103            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7104
7105            // Single pane left: no-op
7106            workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7107        });
7108
7109        workspace.update(cx, |workspace, _cx| {
7110            let active_pane = workspace.active_pane();
7111            assert_eq!(top_pane_id, active_pane.entity_id());
7112        });
7113    }
7114
7115    fn add_an_item_to_active_pane(
7116        cx: &mut VisualTestContext,
7117        workspace: &View<Workspace>,
7118        item_id: u64,
7119    ) -> View<TestItem> {
7120        let item = cx.new_view(|cx| {
7121            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7122                item_id,
7123                "item{item_id}.txt",
7124                cx,
7125            )])
7126        });
7127        workspace.update(cx, |workspace, cx| {
7128            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7129        });
7130        return item;
7131    }
7132
7133    fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7134        return workspace.update(cx, |workspace, cx| {
7135            let new_pane =
7136                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7137            new_pane
7138        });
7139    }
7140
7141    #[gpui::test]
7142    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7143        init_test(cx);
7144        let fs = FakeFs::new(cx.executor());
7145        let project = Project::test(fs, None, cx).await;
7146        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7147
7148        add_an_item_to_active_pane(cx, &workspace, 1);
7149        split_pane(cx, &workspace);
7150        add_an_item_to_active_pane(cx, &workspace, 2);
7151        split_pane(cx, &workspace); // empty pane
7152        split_pane(cx, &workspace);
7153        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7154
7155        cx.executor().run_until_parked();
7156
7157        workspace.update(cx, |workspace, cx| {
7158            let num_panes = workspace.panes().len();
7159            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7160            let active_item = workspace
7161                .active_pane()
7162                .read(cx)
7163                .active_item()
7164                .expect("item is in focus");
7165
7166            assert_eq!(num_panes, 4);
7167            assert_eq!(num_items_in_current_pane, 1);
7168            assert_eq!(active_item.item_id(), last_item.item_id());
7169        });
7170
7171        workspace.update(cx, |workspace, cx| {
7172            workspace.join_all_panes(cx);
7173        });
7174
7175        workspace.update(cx, |workspace, cx| {
7176            let num_panes = workspace.panes().len();
7177            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7178            let active_item = workspace
7179                .active_pane()
7180                .read(cx)
7181                .active_item()
7182                .expect("item is in focus");
7183
7184            assert_eq!(num_panes, 1);
7185            assert_eq!(num_items_in_current_pane, 3);
7186            assert_eq!(active_item.item_id(), last_item.item_id());
7187        });
7188    }
7189    struct TestModal(FocusHandle);
7190
7191    impl TestModal {
7192        fn new(cx: &mut ViewContext<Self>) -> Self {
7193            Self(cx.focus_handle())
7194        }
7195    }
7196
7197    impl EventEmitter<DismissEvent> for TestModal {}
7198
7199    impl FocusableView for TestModal {
7200        fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7201            self.0.clone()
7202        }
7203    }
7204
7205    impl ModalView for TestModal {}
7206
7207    impl Render for TestModal {
7208        fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7209            div().track_focus(&self.0)
7210        }
7211    }
7212
7213    #[gpui::test]
7214    async fn test_panels(cx: &mut gpui::TestAppContext) {
7215        init_test(cx);
7216        let fs = FakeFs::new(cx.executor());
7217
7218        let project = Project::test(fs, [], cx).await;
7219        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7220
7221        let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7222            let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7223            workspace.add_panel(panel_1.clone(), cx);
7224            workspace
7225                .left_dock()
7226                .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
7227            let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7228            workspace.add_panel(panel_2.clone(), cx);
7229            workspace
7230                .right_dock()
7231                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
7232
7233            let left_dock = workspace.left_dock();
7234            assert_eq!(
7235                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7236                panel_1.panel_id()
7237            );
7238            assert_eq!(
7239                left_dock.read(cx).active_panel_size(cx).unwrap(),
7240                panel_1.size(cx)
7241            );
7242
7243            left_dock.update(cx, |left_dock, cx| {
7244                left_dock.resize_active_panel(Some(px(1337.)), cx)
7245            });
7246            assert_eq!(
7247                workspace
7248                    .right_dock()
7249                    .read(cx)
7250                    .visible_panel()
7251                    .unwrap()
7252                    .panel_id(),
7253                panel_2.panel_id(),
7254            );
7255
7256            (panel_1, panel_2)
7257        });
7258
7259        // Move panel_1 to the right
7260        panel_1.update(cx, |panel_1, cx| {
7261            panel_1.set_position(DockPosition::Right, cx)
7262        });
7263
7264        workspace.update(cx, |workspace, cx| {
7265            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7266            // Since it was the only panel on the left, the left dock should now be closed.
7267            assert!(!workspace.left_dock().read(cx).is_open());
7268            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7269            let right_dock = workspace.right_dock();
7270            assert_eq!(
7271                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7272                panel_1.panel_id()
7273            );
7274            assert_eq!(
7275                right_dock.read(cx).active_panel_size(cx).unwrap(),
7276                px(1337.)
7277            );
7278
7279            // Now we move panel_2 to the left
7280            panel_2.set_position(DockPosition::Left, cx);
7281        });
7282
7283        workspace.update(cx, |workspace, cx| {
7284            // Since panel_2 was not visible on the right, we don't open the left dock.
7285            assert!(!workspace.left_dock().read(cx).is_open());
7286            // And the right dock is unaffected in its displaying of panel_1
7287            assert!(workspace.right_dock().read(cx).is_open());
7288            assert_eq!(
7289                workspace
7290                    .right_dock()
7291                    .read(cx)
7292                    .visible_panel()
7293                    .unwrap()
7294                    .panel_id(),
7295                panel_1.panel_id(),
7296            );
7297        });
7298
7299        // Move panel_1 back to the left
7300        panel_1.update(cx, |panel_1, cx| {
7301            panel_1.set_position(DockPosition::Left, cx)
7302        });
7303
7304        workspace.update(cx, |workspace, cx| {
7305            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7306            let left_dock = workspace.left_dock();
7307            assert!(left_dock.read(cx).is_open());
7308            assert_eq!(
7309                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7310                panel_1.panel_id()
7311            );
7312            assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7313            // And the right dock should be closed as it no longer has any panels.
7314            assert!(!workspace.right_dock().read(cx).is_open());
7315
7316            // Now we move panel_1 to the bottom
7317            panel_1.set_position(DockPosition::Bottom, cx);
7318        });
7319
7320        workspace.update(cx, |workspace, cx| {
7321            // Since panel_1 was visible on the left, we close the left dock.
7322            assert!(!workspace.left_dock().read(cx).is_open());
7323            // The bottom dock is sized based on the panel's default size,
7324            // since the panel orientation changed from vertical to horizontal.
7325            let bottom_dock = workspace.bottom_dock();
7326            assert_eq!(
7327                bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7328                panel_1.size(cx),
7329            );
7330            // Close bottom dock and move panel_1 back to the left.
7331            bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7332            panel_1.set_position(DockPosition::Left, cx);
7333        });
7334
7335        // Emit activated event on panel 1
7336        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7337
7338        // Now the left dock is open and panel_1 is active and focused.
7339        workspace.update(cx, |workspace, cx| {
7340            let left_dock = workspace.left_dock();
7341            assert!(left_dock.read(cx).is_open());
7342            assert_eq!(
7343                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7344                panel_1.panel_id(),
7345            );
7346            assert!(panel_1.focus_handle(cx).is_focused(cx));
7347        });
7348
7349        // Emit closed event on panel 2, which is not active
7350        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7351
7352        // Wo don't close the left dock, because panel_2 wasn't the active panel
7353        workspace.update(cx, |workspace, cx| {
7354            let left_dock = workspace.left_dock();
7355            assert!(left_dock.read(cx).is_open());
7356            assert_eq!(
7357                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7358                panel_1.panel_id(),
7359            );
7360        });
7361
7362        // Emitting a ZoomIn event shows the panel as zoomed.
7363        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7364        workspace.update(cx, |workspace, _| {
7365            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7366            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7367        });
7368
7369        // Move panel to another dock while it is zoomed
7370        panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7371        workspace.update(cx, |workspace, _| {
7372            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7373
7374            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7375        });
7376
7377        // This is a helper for getting a:
7378        // - valid focus on an element,
7379        // - that isn't a part of the panes and panels system of the Workspace,
7380        // - and doesn't trigger the 'on_focus_lost' API.
7381        let focus_other_view = {
7382            let workspace = workspace.clone();
7383            move |cx: &mut VisualTestContext| {
7384                workspace.update(cx, |workspace, cx| {
7385                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7386                        workspace.toggle_modal(cx, TestModal::new);
7387                        workspace.toggle_modal(cx, TestModal::new);
7388                    } else {
7389                        workspace.toggle_modal(cx, TestModal::new);
7390                    }
7391                })
7392            }
7393        };
7394
7395        // If focus is transferred to another view that's not a panel or another pane, we still show
7396        // the panel as zoomed.
7397        focus_other_view(cx);
7398        workspace.update(cx, |workspace, _| {
7399            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7400            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7401        });
7402
7403        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7404        workspace.update(cx, |_, cx| cx.focus_self());
7405        workspace.update(cx, |workspace, _| {
7406            assert_eq!(workspace.zoomed, None);
7407            assert_eq!(workspace.zoomed_position, None);
7408        });
7409
7410        // If focus is transferred again to another view that's not a panel or a pane, we won't
7411        // show the panel as zoomed because it wasn't zoomed before.
7412        focus_other_view(cx);
7413        workspace.update(cx, |workspace, _| {
7414            assert_eq!(workspace.zoomed, None);
7415            assert_eq!(workspace.zoomed_position, None);
7416        });
7417
7418        // When the panel is activated, it is zoomed again.
7419        cx.dispatch_action(ToggleRightDock);
7420        workspace.update(cx, |workspace, _| {
7421            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7422            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7423        });
7424
7425        // Emitting a ZoomOut event unzooms the panel.
7426        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7427        workspace.update(cx, |workspace, _| {
7428            assert_eq!(workspace.zoomed, None);
7429            assert_eq!(workspace.zoomed_position, None);
7430        });
7431
7432        // Emit closed event on panel 1, which is active
7433        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7434
7435        // Now the left dock is closed, because panel_1 was the active panel
7436        workspace.update(cx, |workspace, cx| {
7437            let right_dock = workspace.right_dock();
7438            assert!(!right_dock.read(cx).is_open());
7439        });
7440    }
7441
7442    mod register_project_item_tests {
7443        use ui::Context as _;
7444
7445        use super::*;
7446
7447        // View
7448        struct TestPngItemView {
7449            focus_handle: FocusHandle,
7450        }
7451        // Model
7452        struct TestPngItem {}
7453
7454        impl project::Item for TestPngItem {
7455            fn try_open(
7456                _project: &Model<Project>,
7457                path: &ProjectPath,
7458                cx: &mut AppContext,
7459            ) -> Option<Task<gpui::Result<Model<Self>>>> {
7460                if path.path.extension().unwrap() == "png" {
7461                    Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
7462                } else {
7463                    None
7464                }
7465            }
7466
7467            fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7468                None
7469            }
7470
7471            fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7472                None
7473            }
7474        }
7475
7476        impl Item for TestPngItemView {
7477            type Event = ();
7478        }
7479        impl EventEmitter<()> for TestPngItemView {}
7480        impl FocusableView for TestPngItemView {
7481            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7482                self.focus_handle.clone()
7483            }
7484        }
7485
7486        impl Render for TestPngItemView {
7487            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7488                Empty
7489            }
7490        }
7491
7492        impl ProjectItem for TestPngItemView {
7493            type Item = TestPngItem;
7494
7495            fn for_project_item(
7496                _project: Model<Project>,
7497                _item: Model<Self::Item>,
7498                cx: &mut ViewContext<Self>,
7499            ) -> Self
7500            where
7501                Self: Sized,
7502            {
7503                Self {
7504                    focus_handle: cx.focus_handle(),
7505                }
7506            }
7507        }
7508
7509        // View
7510        struct TestIpynbItemView {
7511            focus_handle: FocusHandle,
7512        }
7513        // Model
7514        struct TestIpynbItem {}
7515
7516        impl project::Item for TestIpynbItem {
7517            fn try_open(
7518                _project: &Model<Project>,
7519                path: &ProjectPath,
7520                cx: &mut AppContext,
7521            ) -> Option<Task<gpui::Result<Model<Self>>>> {
7522                if path.path.extension().unwrap() == "ipynb" {
7523                    Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
7524                } else {
7525                    None
7526                }
7527            }
7528
7529            fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7530                None
7531            }
7532
7533            fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7534                None
7535            }
7536        }
7537
7538        impl Item for TestIpynbItemView {
7539            type Event = ();
7540        }
7541        impl EventEmitter<()> for TestIpynbItemView {}
7542        impl FocusableView for TestIpynbItemView {
7543            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7544                self.focus_handle.clone()
7545            }
7546        }
7547
7548        impl Render for TestIpynbItemView {
7549            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7550                Empty
7551            }
7552        }
7553
7554        impl ProjectItem for TestIpynbItemView {
7555            type Item = TestIpynbItem;
7556
7557            fn for_project_item(
7558                _project: Model<Project>,
7559                _item: Model<Self::Item>,
7560                cx: &mut ViewContext<Self>,
7561            ) -> Self
7562            where
7563                Self: Sized,
7564            {
7565                Self {
7566                    focus_handle: cx.focus_handle(),
7567                }
7568            }
7569        }
7570
7571        struct TestAlternatePngItemView {
7572            focus_handle: FocusHandle,
7573        }
7574
7575        impl Item for TestAlternatePngItemView {
7576            type Event = ();
7577        }
7578
7579        impl EventEmitter<()> for TestAlternatePngItemView {}
7580        impl FocusableView for TestAlternatePngItemView {
7581            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7582                self.focus_handle.clone()
7583            }
7584        }
7585
7586        impl Render for TestAlternatePngItemView {
7587            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7588                Empty
7589            }
7590        }
7591
7592        impl ProjectItem for TestAlternatePngItemView {
7593            type Item = TestPngItem;
7594
7595            fn for_project_item(
7596                _project: Model<Project>,
7597                _item: Model<Self::Item>,
7598                cx: &mut ViewContext<Self>,
7599            ) -> Self
7600            where
7601                Self: Sized,
7602            {
7603                Self {
7604                    focus_handle: cx.focus_handle(),
7605                }
7606            }
7607        }
7608
7609        #[gpui::test]
7610        async fn test_register_project_item(cx: &mut TestAppContext) {
7611            init_test(cx);
7612
7613            cx.update(|cx| {
7614                register_project_item::<TestPngItemView>(cx);
7615                register_project_item::<TestIpynbItemView>(cx);
7616            });
7617
7618            let fs = FakeFs::new(cx.executor());
7619            fs.insert_tree(
7620                "/root1",
7621                json!({
7622                    "one.png": "BINARYDATAHERE",
7623                    "two.ipynb": "{ totally a notebook }",
7624                    "three.txt": "editing text, sure why not?"
7625                }),
7626            )
7627            .await;
7628
7629            let project = Project::test(fs, ["root1".as_ref()], cx).await;
7630            let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7631
7632            let worktree_id = project.update(cx, |project, cx| {
7633                project.worktrees(cx).next().unwrap().read(cx).id()
7634            });
7635
7636            let handle = workspace
7637                .update(cx, |workspace, cx| {
7638                    let project_path = (worktree_id, "one.png");
7639                    workspace.open_path(project_path, None, true, cx)
7640                })
7641                .await
7642                .unwrap();
7643
7644            // Now we can check if the handle we got back errored or not
7645            assert_eq!(
7646                handle.to_any().entity_type(),
7647                TypeId::of::<TestPngItemView>()
7648            );
7649
7650            let handle = workspace
7651                .update(cx, |workspace, cx| {
7652                    let project_path = (worktree_id, "two.ipynb");
7653                    workspace.open_path(project_path, None, true, cx)
7654                })
7655                .await
7656                .unwrap();
7657
7658            assert_eq!(
7659                handle.to_any().entity_type(),
7660                TypeId::of::<TestIpynbItemView>()
7661            );
7662
7663            let handle = workspace
7664                .update(cx, |workspace, cx| {
7665                    let project_path = (worktree_id, "three.txt");
7666                    workspace.open_path(project_path, None, true, cx)
7667                })
7668                .await;
7669            assert!(handle.is_err());
7670        }
7671
7672        #[gpui::test]
7673        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
7674            init_test(cx);
7675
7676            cx.update(|cx| {
7677                register_project_item::<TestPngItemView>(cx);
7678                register_project_item::<TestAlternatePngItemView>(cx);
7679            });
7680
7681            let fs = FakeFs::new(cx.executor());
7682            fs.insert_tree(
7683                "/root1",
7684                json!({
7685                    "one.png": "BINARYDATAHERE",
7686                    "two.ipynb": "{ totally a notebook }",
7687                    "three.txt": "editing text, sure why not?"
7688                }),
7689            )
7690            .await;
7691
7692            let project = Project::test(fs, ["root1".as_ref()], cx).await;
7693            let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7694
7695            let worktree_id = project.update(cx, |project, cx| {
7696                project.worktrees(cx).next().unwrap().read(cx).id()
7697            });
7698
7699            let handle = workspace
7700                .update(cx, |workspace, cx| {
7701                    let project_path = (worktree_id, "one.png");
7702                    workspace.open_path(project_path, None, true, cx)
7703                })
7704                .await
7705                .unwrap();
7706
7707            // This _must_ be the second item registered
7708            assert_eq!(
7709                handle.to_any().entity_type(),
7710                TypeId::of::<TestAlternatePngItemView>()
7711            );
7712
7713            let handle = workspace
7714                .update(cx, |workspace, cx| {
7715                    let project_path = (worktree_id, "three.txt");
7716                    workspace.open_path(project_path, None, true, cx)
7717                })
7718                .await;
7719            assert!(handle.is_err());
7720        }
7721    }
7722
7723    pub fn init_test(cx: &mut TestAppContext) {
7724        cx.update(|cx| {
7725            let settings_store = SettingsStore::test(cx);
7726            cx.set_global(settings_store);
7727            theme::init(theme::LoadThemes::JustBase, cx);
7728            language::init(cx);
7729            crate::init_settings(cx);
7730            Project::init_settings(cx);
7731        });
7732    }
7733}