workspace.rs

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