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