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