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_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_to_remove = Some((ix, item.item_id()));
3737                                break;
3738                            }
3739                            None => {}
3740                        }
3741                    }
3742                }
3743
3744                if let Some((ix, id)) = item_to_remove {
3745                    pane.remove_item(id, 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    #[cfg(target_os = "windows")]
3948    fn shared_screen_for_peer(
3949        &self,
3950        _peer_id: PeerId,
3951        _pane: &View<Pane>,
3952        _cx: &mut WindowContext,
3953    ) -> Option<View<SharedScreen>> {
3954        None
3955    }
3956
3957    #[cfg(not(target_os = "windows"))]
3958    fn shared_screen_for_peer(
3959        &self,
3960        peer_id: PeerId,
3961        pane: &View<Pane>,
3962        cx: &mut WindowContext,
3963    ) -> Option<View<SharedScreen>> {
3964        let call = self.active_call()?;
3965        let room = call.read(cx).room()?.read(cx);
3966        let participant = room.remote_participant_for_peer_id(peer_id)?;
3967        let track = participant.video_tracks.values().next()?.clone();
3968        let user = participant.user.clone();
3969
3970        for item in pane.read(cx).items_of_type::<SharedScreen>() {
3971            if item.read(cx).peer_id == peer_id {
3972                return Some(item);
3973            }
3974        }
3975
3976        Some(cx.new_view(|cx| SharedScreen::new(track, peer_id, user.clone(), cx)))
3977    }
3978
3979    pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
3980        if cx.is_window_active() {
3981            self.update_active_view_for_followers(cx);
3982
3983            if let Some(database_id) = self.database_id {
3984                cx.background_executor()
3985                    .spawn(persistence::DB.update_timestamp(database_id))
3986                    .detach();
3987            }
3988        } else {
3989            for pane in &self.panes {
3990                pane.update(cx, |pane, cx| {
3991                    if let Some(item) = pane.active_item() {
3992                        item.workspace_deactivated(cx);
3993                    }
3994                    for item in pane.items() {
3995                        if matches!(
3996                            item.workspace_settings(cx).autosave,
3997                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
3998                        ) {
3999                            Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
4000                                .detach_and_log_err(cx);
4001                        }
4002                    }
4003                });
4004            }
4005        }
4006    }
4007
4008    fn active_call(&self) -> Option<&Model<ActiveCall>> {
4009        self.active_call.as_ref().map(|(call, _)| call)
4010    }
4011
4012    fn on_active_call_event(
4013        &mut self,
4014        _: Model<ActiveCall>,
4015        event: &call::room::Event,
4016        cx: &mut ViewContext<Self>,
4017    ) {
4018        match event {
4019            call::room::Event::ParticipantLocationChanged { participant_id }
4020            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4021                self.leader_updated(*participant_id, cx);
4022            }
4023            _ => {}
4024        }
4025    }
4026
4027    pub fn database_id(&self) -> Option<WorkspaceId> {
4028        self.database_id
4029    }
4030
4031    fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
4032        let project = self.project().read(cx);
4033
4034        if project.is_local() {
4035            Some(
4036                project
4037                    .visible_worktrees(cx)
4038                    .map(|worktree| worktree.read(cx).abs_path())
4039                    .collect::<Vec<_>>(),
4040            )
4041        } else {
4042            None
4043        }
4044    }
4045
4046    fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
4047        match member {
4048            Member::Axis(PaneAxis { members, .. }) => {
4049                for child in members.iter() {
4050                    self.remove_panes(child.clone(), cx)
4051                }
4052            }
4053            Member::Pane(pane) => {
4054                self.force_remove_pane(&pane, &None, cx);
4055            }
4056        }
4057    }
4058
4059    fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
4060        self.session_id.take();
4061        self.serialize_workspace_internal(cx)
4062    }
4063
4064    fn force_remove_pane(
4065        &mut self,
4066        pane: &View<Pane>,
4067        focus_on: &Option<View<Pane>>,
4068        cx: &mut ViewContext<Workspace>,
4069    ) {
4070        self.panes.retain(|p| p != pane);
4071        if let Some(focus_on) = focus_on {
4072            focus_on.update(cx, |pane, cx| pane.focus(cx));
4073        } else {
4074            self.panes
4075                .last()
4076                .unwrap()
4077                .update(cx, |pane, cx| pane.focus(cx));
4078        }
4079        if self.last_active_center_pane == Some(pane.downgrade()) {
4080            self.last_active_center_pane = None;
4081        }
4082        cx.notify();
4083    }
4084
4085    fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
4086        if self._schedule_serialize.is_none() {
4087            self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
4088                cx.background_executor()
4089                    .timer(Duration::from_millis(100))
4090                    .await;
4091                this.update(&mut cx, |this, cx| {
4092                    this.serialize_workspace_internal(cx).detach();
4093                    this._schedule_serialize.take();
4094                })
4095                .log_err();
4096            }));
4097        }
4098    }
4099
4100    fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
4101        let Some(database_id) = self.database_id() else {
4102            return Task::ready(());
4103        };
4104
4105        fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
4106            let (items, active, pinned_count) = {
4107                let pane = pane_handle.read(cx);
4108                let active_item_id = pane.active_item().map(|item| item.item_id());
4109                (
4110                    pane.items()
4111                        .filter_map(|handle| {
4112                            let handle = handle.to_serializable_item_handle(cx)?;
4113
4114                            Some(SerializedItem {
4115                                kind: Arc::from(handle.serialized_item_kind()),
4116                                item_id: handle.item_id().as_u64(),
4117                                active: Some(handle.item_id()) == active_item_id,
4118                                preview: pane.is_active_preview_item(handle.item_id()),
4119                            })
4120                        })
4121                        .collect::<Vec<_>>(),
4122                    pane.has_focus(cx),
4123                    pane.pinned_count(),
4124                )
4125            };
4126
4127            SerializedPane::new(items, active, pinned_count)
4128        }
4129
4130        fn build_serialized_pane_group(
4131            pane_group: &Member,
4132            cx: &WindowContext,
4133        ) -> SerializedPaneGroup {
4134            match pane_group {
4135                Member::Axis(PaneAxis {
4136                    axis,
4137                    members,
4138                    flexes,
4139                    bounding_boxes: _,
4140                }) => SerializedPaneGroup::Group {
4141                    axis: SerializedAxis(*axis),
4142                    children: members
4143                        .iter()
4144                        .map(|member| build_serialized_pane_group(member, cx))
4145                        .collect::<Vec<_>>(),
4146                    flexes: Some(flexes.lock().clone()),
4147                },
4148                Member::Pane(pane_handle) => {
4149                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
4150                }
4151            }
4152        }
4153
4154        fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
4155            let left_dock = this.left_dock.read(cx);
4156            let left_visible = left_dock.is_open();
4157            let left_active_panel = left_dock
4158                .active_panel()
4159                .map(|panel| panel.persistent_name().to_string());
4160            let left_dock_zoom = left_dock
4161                .active_panel()
4162                .map(|panel| panel.is_zoomed(cx))
4163                .unwrap_or(false);
4164
4165            let right_dock = this.right_dock.read(cx);
4166            let right_visible = right_dock.is_open();
4167            let right_active_panel = right_dock
4168                .active_panel()
4169                .map(|panel| panel.persistent_name().to_string());
4170            let right_dock_zoom = right_dock
4171                .active_panel()
4172                .map(|panel| panel.is_zoomed(cx))
4173                .unwrap_or(false);
4174
4175            let bottom_dock = this.bottom_dock.read(cx);
4176            let bottom_visible = bottom_dock.is_open();
4177            let bottom_active_panel = bottom_dock
4178                .active_panel()
4179                .map(|panel| panel.persistent_name().to_string());
4180            let bottom_dock_zoom = bottom_dock
4181                .active_panel()
4182                .map(|panel| panel.is_zoomed(cx))
4183                .unwrap_or(false);
4184
4185            DockStructure {
4186                left: DockData {
4187                    visible: left_visible,
4188                    active_panel: left_active_panel,
4189                    zoom: left_dock_zoom,
4190                },
4191                right: DockData {
4192                    visible: right_visible,
4193                    active_panel: right_active_panel,
4194                    zoom: right_dock_zoom,
4195                },
4196                bottom: DockData {
4197                    visible: bottom_visible,
4198                    active_panel: bottom_active_panel,
4199                    zoom: bottom_dock_zoom,
4200                },
4201            }
4202        }
4203
4204        let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4205            Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4206        } else if let Some(local_paths) = self.local_paths(cx) {
4207            if !local_paths.is_empty() {
4208                Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4209            } else {
4210                None
4211            }
4212        } else {
4213            None
4214        };
4215
4216        if let Some(location) = location {
4217            let center_group = build_serialized_pane_group(&self.center.root, cx);
4218            let docks = build_serialized_docks(self, cx);
4219            let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
4220            let serialized_workspace = SerializedWorkspace {
4221                id: database_id,
4222                location,
4223                center_group,
4224                window_bounds,
4225                display: Default::default(),
4226                docks,
4227                centered_layout: self.centered_layout,
4228                session_id: self.session_id.clone(),
4229                window_id: Some(cx.window_handle().window_id().as_u64()),
4230            };
4231            return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
4232        }
4233        Task::ready(())
4234    }
4235
4236    async fn serialize_items(
4237        this: &WeakView<Self>,
4238        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4239        cx: &mut AsyncWindowContext,
4240    ) -> Result<()> {
4241        const CHUNK_SIZE: usize = 200;
4242        const THROTTLE_TIME: Duration = Duration::from_millis(200);
4243
4244        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4245
4246        while let Some(items_received) = serializable_items.next().await {
4247            let unique_items =
4248                items_received
4249                    .into_iter()
4250                    .fold(HashMap::default(), |mut acc, item| {
4251                        acc.entry(item.item_id()).or_insert(item);
4252                        acc
4253                    });
4254
4255            // We use into_iter() here so that the references to the items are moved into
4256            // the tasks and not kept alive while we're sleeping.
4257            for (_, item) in unique_items.into_iter() {
4258                if let Ok(Some(task)) =
4259                    this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4260                {
4261                    cx.background_executor()
4262                        .spawn(async move { task.await.log_err() })
4263                        .detach();
4264                }
4265            }
4266
4267            cx.background_executor().timer(THROTTLE_TIME).await;
4268        }
4269
4270        Ok(())
4271    }
4272
4273    pub(crate) fn enqueue_item_serialization(
4274        &mut self,
4275        item: Box<dyn SerializableItemHandle>,
4276    ) -> Result<()> {
4277        self.serializable_items_tx
4278            .unbounded_send(item)
4279            .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4280    }
4281
4282    pub(crate) fn load_workspace(
4283        serialized_workspace: SerializedWorkspace,
4284        paths_to_open: Vec<Option<ProjectPath>>,
4285        cx: &mut ViewContext<Workspace>,
4286    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4287        cx.spawn(|workspace, mut cx| async move {
4288            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4289
4290            let mut center_group = None;
4291            let mut center_items = None;
4292
4293            // Traverse the splits tree and add to things
4294            if let Some((group, active_pane, items)) = serialized_workspace
4295                .center_group
4296                .deserialize(
4297                    &project,
4298                    serialized_workspace.id,
4299                    workspace.clone(),
4300                    &mut cx,
4301                )
4302                .await
4303            {
4304                center_items = Some(items);
4305                center_group = Some((group, active_pane))
4306            }
4307
4308            let mut items_by_project_path = HashMap::default();
4309            let mut item_ids_by_kind = HashMap::default();
4310            let mut all_deserialized_items = Vec::default();
4311            cx.update(|cx| {
4312                for item in center_items.unwrap_or_default().into_iter().flatten() {
4313                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4314                        item_ids_by_kind
4315                            .entry(serializable_item_handle.serialized_item_kind())
4316                            .or_insert(Vec::new())
4317                            .push(item.item_id().as_u64() as ItemId);
4318                    }
4319
4320                    if let Some(project_path) = item.project_path(cx) {
4321                        items_by_project_path.insert(project_path, item.clone());
4322                    }
4323                    all_deserialized_items.push(item);
4324                }
4325            })?;
4326
4327            let opened_items = paths_to_open
4328                .into_iter()
4329                .map(|path_to_open| {
4330                    path_to_open
4331                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4332                })
4333                .collect::<Vec<_>>();
4334
4335            // Remove old panes from workspace panes list
4336            workspace.update(&mut cx, |workspace, cx| {
4337                if let Some((center_group, active_pane)) = center_group {
4338                    workspace.remove_panes(workspace.center.root.clone(), cx);
4339
4340                    // Swap workspace center group
4341                    workspace.center = PaneGroup::with_root(center_group);
4342                    if let Some(active_pane) = active_pane {
4343                        workspace.set_active_pane(&active_pane, cx);
4344                        cx.focus_self();
4345                    } else {
4346                        workspace.set_active_pane(&workspace.center.first_pane(), cx);
4347                    }
4348                }
4349
4350                let docks = serialized_workspace.docks;
4351
4352                for (dock, serialized_dock) in [
4353                    (&mut workspace.right_dock, docks.right),
4354                    (&mut workspace.left_dock, docks.left),
4355                    (&mut workspace.bottom_dock, docks.bottom),
4356                ]
4357                .iter_mut()
4358                {
4359                    dock.update(cx, |dock, cx| {
4360                        dock.serialized_dock = Some(serialized_dock.clone());
4361                        dock.restore_state(cx);
4362                    });
4363                }
4364
4365                cx.notify();
4366            })?;
4367
4368            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4369            // after loading the items, we might have different items and in order to avoid
4370            // the database filling up, we delete items that haven't been loaded now.
4371            //
4372            // The items that have been loaded, have been saved after they've been added to the workspace.
4373            let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
4374                item_ids_by_kind
4375                    .into_iter()
4376                    .map(|(item_kind, loaded_items)| {
4377                        SerializableItemRegistry::cleanup(
4378                            item_kind,
4379                            serialized_workspace.id,
4380                            loaded_items,
4381                            cx,
4382                        )
4383                        .log_err()
4384                    })
4385                    .collect::<Vec<_>>()
4386            })?;
4387
4388            futures::future::join_all(clean_up_tasks).await;
4389
4390            workspace
4391                .update(&mut cx, |workspace, cx| {
4392                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4393                    workspace.serialize_workspace_internal(cx).detach();
4394
4395                    // Ensure that we mark the window as edited if we did load dirty items
4396                    workspace.update_window_edited(cx);
4397                })
4398                .ok();
4399
4400            Ok(opened_items)
4401        })
4402    }
4403
4404    fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4405        self.add_workspace_actions_listeners(div, cx)
4406            .on_action(cx.listener(Self::close_inactive_items_and_panes))
4407            .on_action(cx.listener(Self::close_all_items_and_panes))
4408            .on_action(cx.listener(Self::save_all))
4409            .on_action(cx.listener(Self::send_keystrokes))
4410            .on_action(cx.listener(Self::add_folder_to_project))
4411            .on_action(cx.listener(Self::follow_next_collaborator))
4412            .on_action(cx.listener(Self::close_window))
4413            .on_action(cx.listener(Self::activate_pane_at_index))
4414            .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
4415                let pane = workspace.active_pane().clone();
4416                workspace.unfollow_in_pane(&pane, cx);
4417            }))
4418            .on_action(cx.listener(|workspace, action: &Save, cx| {
4419                workspace
4420                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
4421                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4422            }))
4423            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
4424                workspace
4425                    .save_active_item(SaveIntent::SaveWithoutFormat, cx)
4426                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4427            }))
4428            .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
4429                workspace
4430                    .save_active_item(SaveIntent::SaveAs, cx)
4431                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4432            }))
4433            .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4434                workspace.activate_previous_pane(cx)
4435            }))
4436            .on_action(
4437                cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4438            )
4439            .on_action(
4440                cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4441                    workspace.activate_pane_in_direction(action.0, cx)
4442                }),
4443            )
4444            .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4445                workspace.swap_pane_in_direction(action.0, cx)
4446            }))
4447            .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4448                this.toggle_dock(DockPosition::Left, cx);
4449            }))
4450            .on_action(
4451                cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4452                    workspace.toggle_dock(DockPosition::Right, cx);
4453                }),
4454            )
4455            .on_action(
4456                cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4457                    workspace.toggle_dock(DockPosition::Bottom, cx);
4458                }),
4459            )
4460            .on_action(
4461                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4462                    workspace.close_all_docks(cx);
4463                }),
4464            )
4465            .on_action(
4466                cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4467                    workspace.clear_all_notifications(cx);
4468                }),
4469            )
4470            .on_action(
4471                cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4472                    workspace.reopen_closed_item(cx).detach();
4473                }),
4474            )
4475            .on_action(cx.listener(Workspace::toggle_centered_layout))
4476    }
4477
4478    #[cfg(any(test, feature = "test-support"))]
4479    pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4480        use node_runtime::NodeRuntime;
4481        use session::Session;
4482
4483        let client = project.read(cx).client();
4484        let user_store = project.read(cx).user_store();
4485
4486        let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4487        let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
4488        cx.activate_window();
4489        let app_state = Arc::new(AppState {
4490            languages: project.read(cx).languages().clone(),
4491            workspace_store,
4492            client,
4493            user_store,
4494            fs: project.read(cx).fs().clone(),
4495            build_window_options: |_, _| Default::default(),
4496            node_runtime: NodeRuntime::unavailable(),
4497            session,
4498        });
4499        let workspace = Self::new(Default::default(), project, app_state, cx);
4500        workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4501        workspace
4502    }
4503
4504    pub fn register_action<A: Action>(
4505        &mut self,
4506        callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4507    ) -> &mut Self {
4508        let callback = Arc::new(callback);
4509
4510        self.workspace_actions.push(Box::new(move |div, cx| {
4511            let callback = callback.clone();
4512            div.on_action(
4513                cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4514            )
4515        }));
4516        self
4517    }
4518
4519    fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
4520        for action in self.workspace_actions.iter() {
4521            div = (action)(div, cx)
4522        }
4523        div
4524    }
4525
4526    pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool {
4527        self.modal_layer.read(cx).has_active_modal()
4528    }
4529
4530    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &AppContext) -> Option<View<V>> {
4531        self.modal_layer.read(cx).active_modal()
4532    }
4533
4534    pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
4535    where
4536        B: FnOnce(&mut ViewContext<V>) -> V,
4537    {
4538        self.modal_layer
4539            .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
4540    }
4541
4542    pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
4543        self.centered_layout = !self.centered_layout;
4544        if let Some(database_id) = self.database_id() {
4545            cx.background_executor()
4546                .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4547                .detach_and_log_err(cx);
4548        }
4549        cx.notify();
4550    }
4551
4552    fn adjust_padding(padding: Option<f32>) -> f32 {
4553        padding
4554            .unwrap_or(Self::DEFAULT_PADDING)
4555            .clamp(0.0, Self::MAX_PADDING)
4556    }
4557
4558    fn render_dock(
4559        &self,
4560        position: DockPosition,
4561        dock: &View<Dock>,
4562        cx: &WindowContext,
4563    ) -> Option<Div> {
4564        if self.zoomed_position == Some(position) {
4565            return None;
4566        }
4567
4568        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4569            let pane = panel.pane(cx)?;
4570            let follower_states = &self.follower_states;
4571            leader_border_for_pane(follower_states, &pane, cx)
4572        });
4573
4574        Some(
4575            div()
4576                .flex()
4577                .flex_none()
4578                .overflow_hidden()
4579                .child(dock.clone())
4580                .children(leader_border),
4581        )
4582    }
4583
4584    pub fn for_window(cx: &mut WindowContext) -> Option<View<Workspace>> {
4585        let window = cx.window_handle().downcast::<Workspace>()?;
4586        cx.read_window(&window, |workspace, _| workspace).ok()
4587    }
4588
4589    pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
4590        self.zoomed.as_ref()
4591    }
4592}
4593
4594fn leader_border_for_pane(
4595    follower_states: &HashMap<PeerId, FollowerState>,
4596    pane: &View<Pane>,
4597    cx: &WindowContext,
4598) -> Option<Div> {
4599    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4600        if state.pane() == pane {
4601            Some((*leader_id, state))
4602        } else {
4603            None
4604        }
4605    })?;
4606
4607    let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4608    let leader = room.remote_participant_for_peer_id(leader_id)?;
4609
4610    let mut leader_color = cx
4611        .theme()
4612        .players()
4613        .color_for_participant(leader.participant_index.0)
4614        .cursor;
4615    leader_color.fade_out(0.3);
4616    Some(
4617        div()
4618            .absolute()
4619            .size_full()
4620            .left_0()
4621            .top_0()
4622            .border_2()
4623            .border_color(leader_color),
4624    )
4625}
4626
4627fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4628    ZED_WINDOW_POSITION
4629        .zip(*ZED_WINDOW_SIZE)
4630        .map(|(position, size)| Bounds {
4631            origin: position,
4632            size,
4633        })
4634}
4635
4636fn open_items(
4637    serialized_workspace: Option<SerializedWorkspace>,
4638    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4639    cx: &mut ViewContext<Workspace>,
4640) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4641    let restored_items = serialized_workspace.map(|serialized_workspace| {
4642        Workspace::load_workspace(
4643            serialized_workspace,
4644            project_paths_to_open
4645                .iter()
4646                .map(|(_, project_path)| project_path)
4647                .cloned()
4648                .collect(),
4649            cx,
4650        )
4651    });
4652
4653    cx.spawn(|workspace, mut cx| async move {
4654        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4655
4656        if let Some(restored_items) = restored_items {
4657            let restored_items = restored_items.await?;
4658
4659            let restored_project_paths = restored_items
4660                .iter()
4661                .filter_map(|item| {
4662                    cx.update(|cx| item.as_ref()?.project_path(cx))
4663                        .ok()
4664                        .flatten()
4665                })
4666                .collect::<HashSet<_>>();
4667
4668            for restored_item in restored_items {
4669                opened_items.push(restored_item.map(Ok));
4670            }
4671
4672            project_paths_to_open
4673                .iter_mut()
4674                .for_each(|(_, project_path)| {
4675                    if let Some(project_path_to_open) = project_path {
4676                        if restored_project_paths.contains(project_path_to_open) {
4677                            *project_path = None;
4678                        }
4679                    }
4680                });
4681        } else {
4682            for _ in 0..project_paths_to_open.len() {
4683                opened_items.push(None);
4684            }
4685        }
4686        assert!(opened_items.len() == project_paths_to_open.len());
4687
4688        let tasks =
4689            project_paths_to_open
4690                .into_iter()
4691                .enumerate()
4692                .map(|(ix, (abs_path, project_path))| {
4693                    let workspace = workspace.clone();
4694                    cx.spawn(|mut cx| async move {
4695                        let file_project_path = project_path?;
4696                        let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
4697                            workspace.project().update(cx, |project, cx| {
4698                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
4699                            })
4700                        });
4701
4702                        // We only want to open file paths here. If one of the items
4703                        // here is a directory, it was already opened further above
4704                        // with a `find_or_create_worktree`.
4705                        if let Ok(task) = abs_path_task {
4706                            if task.await.map_or(true, |p| p.is_file()) {
4707                                return Some((
4708                                    ix,
4709                                    workspace
4710                                        .update(&mut cx, |workspace, cx| {
4711                                            workspace.open_path(file_project_path, None, true, cx)
4712                                        })
4713                                        .log_err()?
4714                                        .await,
4715                                ));
4716                            }
4717                        }
4718                        None
4719                    })
4720                });
4721
4722        let tasks = tasks.collect::<Vec<_>>();
4723
4724        let tasks = futures::future::join_all(tasks);
4725        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4726            opened_items[ix] = Some(path_open_result);
4727        }
4728
4729        Ok(opened_items)
4730    })
4731}
4732
4733enum ActivateInDirectionTarget {
4734    Pane(View<Pane>),
4735    Dock(View<Dock>),
4736}
4737
4738fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4739    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";
4740
4741    workspace
4742        .update(cx, |workspace, cx| {
4743            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4744                struct DatabaseFailedNotification;
4745
4746                workspace.show_notification_once(
4747                    NotificationId::unique::<DatabaseFailedNotification>(),
4748                    cx,
4749                    |cx| {
4750                        cx.new_view(|_| {
4751                            MessageNotification::new("Failed to load the database file.")
4752                                .with_click_message("File an issue")
4753                                .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4754                        })
4755                    },
4756                );
4757            }
4758        })
4759        .log_err();
4760}
4761
4762impl FocusableView for Workspace {
4763    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4764        self.active_pane.focus_handle(cx)
4765    }
4766}
4767
4768#[derive(Clone, Render)]
4769struct DraggedDock(DockPosition);
4770
4771impl Render for Workspace {
4772    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4773        let mut context = KeyContext::new_with_defaults();
4774        context.add("Workspace");
4775        context.set("keyboard_layout", cx.keyboard_layout().clone());
4776        let centered_layout = self.centered_layout
4777            && self.center.panes().len() == 1
4778            && self.active_item(cx).is_some();
4779        let render_padding = |size| {
4780            (size > 0.0).then(|| {
4781                div()
4782                    .h_full()
4783                    .w(relative(size))
4784                    .bg(cx.theme().colors().editor_background)
4785                    .border_color(cx.theme().colors().pane_group_border)
4786            })
4787        };
4788        let paddings = if centered_layout {
4789            let settings = WorkspaceSettings::get_global(cx).centered_layout;
4790            (
4791                render_padding(Self::adjust_padding(settings.left_padding)),
4792                render_padding(Self::adjust_padding(settings.right_padding)),
4793            )
4794        } else {
4795            (None, None)
4796        };
4797        let ui_font = theme::setup_ui_font(cx);
4798
4799        let theme = cx.theme().clone();
4800        let colors = theme.colors();
4801
4802        client_side_decorations(
4803            self.actions(div(), cx)
4804                .key_context(context)
4805                .relative()
4806                .size_full()
4807                .flex()
4808                .flex_col()
4809                .font(ui_font)
4810                .gap_0()
4811                .justify_start()
4812                .items_start()
4813                .text_color(colors.text)
4814                .overflow_hidden()
4815                .children(self.titlebar_item.clone())
4816                .child(
4817                    div()
4818                        .size_full()
4819                        .relative()
4820                        .flex_1()
4821                        .flex()
4822                        .flex_col()
4823                        .child(
4824                            div()
4825                                .id("workspace")
4826                                .bg(colors.background)
4827                                .relative()
4828                                .flex_1()
4829                                .w_full()
4830                                .flex()
4831                                .flex_col()
4832                                .overflow_hidden()
4833                                .border_t_1()
4834                                .border_b_1()
4835                                .border_color(colors.border)
4836                                .child({
4837                                    let this = cx.view().clone();
4838                                    canvas(
4839                                        move |bounds, cx| {
4840                                            this.update(cx, |this, cx| {
4841                                                let bounds_changed = this.bounds != bounds;
4842                                                this.bounds = bounds;
4843
4844                                                if bounds_changed {
4845                                                    this.left_dock.update(cx, |dock, cx| {
4846                                                        dock.clamp_panel_size(bounds.size.width, cx)
4847                                                    });
4848
4849                                                    this.right_dock.update(cx, |dock, cx| {
4850                                                        dock.clamp_panel_size(bounds.size.width, cx)
4851                                                    });
4852
4853                                                    this.bottom_dock.update(cx, |dock, cx| {
4854                                                        dock.clamp_panel_size(
4855                                                            bounds.size.height,
4856                                                            cx,
4857                                                        )
4858                                                    });
4859                                                }
4860                                            })
4861                                        },
4862                                        |_, _, _| {},
4863                                    )
4864                                    .absolute()
4865                                    .size_full()
4866                                })
4867                                .when(self.zoomed.is_none(), |this| {
4868                                    this.on_drag_move(cx.listener(
4869                                        |workspace, e: &DragMoveEvent<DraggedDock>, cx| {
4870                                            match e.drag(cx).0 {
4871                                                DockPosition::Left => {
4872                                                    resize_left_dock(
4873                                                        e.event.position.x
4874                                                            - workspace.bounds.left(),
4875                                                        workspace,
4876                                                        cx,
4877                                                    );
4878                                                }
4879                                                DockPosition::Right => {
4880                                                    resize_right_dock(
4881                                                        workspace.bounds.right()
4882                                                            - e.event.position.x,
4883                                                        workspace,
4884                                                        cx,
4885                                                    );
4886                                                }
4887                                                DockPosition::Bottom => {
4888                                                    resize_bottom_dock(
4889                                                        workspace.bounds.bottom()
4890                                                            - e.event.position.y,
4891                                                        workspace,
4892                                                        cx,
4893                                                    );
4894                                                }
4895                                            }
4896                                        },
4897                                    ))
4898                                })
4899                                .child(
4900                                    div()
4901                                        .flex()
4902                                        .flex_row()
4903                                        .h_full()
4904                                        // Left Dock
4905                                        .children(self.render_dock(
4906                                            DockPosition::Left,
4907                                            &self.left_dock,
4908                                            cx,
4909                                        ))
4910                                        // Panes
4911                                        .child(
4912                                            div()
4913                                                .flex()
4914                                                .flex_col()
4915                                                .flex_1()
4916                                                .overflow_hidden()
4917                                                .child(
4918                                                    h_flex()
4919                                                        .flex_1()
4920                                                        .when_some(paddings.0, |this, p| {
4921                                                            this.child(p.border_r_1())
4922                                                        })
4923                                                        .child(self.center.render(
4924                                                            &self.project,
4925                                                            &self.follower_states,
4926                                                            self.active_call(),
4927                                                            &self.active_pane,
4928                                                            self.zoomed.as_ref(),
4929                                                            &self.app_state,
4930                                                            cx,
4931                                                        ))
4932                                                        .when_some(paddings.1, |this, p| {
4933                                                            this.child(p.border_l_1())
4934                                                        }),
4935                                                )
4936                                                .children(self.render_dock(
4937                                                    DockPosition::Bottom,
4938                                                    &self.bottom_dock,
4939                                                    cx,
4940                                                )),
4941                                        )
4942                                        // Right Dock
4943                                        .children(self.render_dock(
4944                                            DockPosition::Right,
4945                                            &self.right_dock,
4946                                            cx,
4947                                        )),
4948                                )
4949                                .children(self.zoomed.as_ref().and_then(|view| {
4950                                    let zoomed_view = view.upgrade()?;
4951                                    let div = div()
4952                                        .occlude()
4953                                        .absolute()
4954                                        .overflow_hidden()
4955                                        .border_color(colors.border)
4956                                        .bg(colors.background)
4957                                        .child(zoomed_view)
4958                                        .inset_0()
4959                                        .shadow_lg();
4960
4961                                    Some(match self.zoomed_position {
4962                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
4963                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
4964                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
4965                                        None => {
4966                                            div.top_2().bottom_2().left_2().right_2().border_1()
4967                                        }
4968                                    })
4969                                }))
4970                                .children(self.render_notifications(cx)),
4971                        )
4972                        .child(self.status_bar.clone())
4973                        .child(self.modal_layer.clone()),
4974                ),
4975            cx,
4976        )
4977    }
4978}
4979
4980fn resize_bottom_dock(
4981    new_size: Pixels,
4982    workspace: &mut Workspace,
4983    cx: &mut ViewContext<'_, Workspace>,
4984) {
4985    let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
4986    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
4987        bottom_dock.resize_active_panel(Some(size), cx);
4988    });
4989}
4990
4991fn resize_right_dock(
4992    new_size: Pixels,
4993    workspace: &mut Workspace,
4994    cx: &mut ViewContext<'_, Workspace>,
4995) {
4996    let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
4997    workspace.right_dock.update(cx, |right_dock, cx| {
4998        right_dock.resize_active_panel(Some(size), cx);
4999    });
5000}
5001
5002fn resize_left_dock(
5003    new_size: Pixels,
5004    workspace: &mut Workspace,
5005    cx: &mut ViewContext<'_, Workspace>,
5006) {
5007    let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5008
5009    workspace.left_dock.update(cx, |left_dock, cx| {
5010        left_dock.resize_active_panel(Some(size), cx);
5011    });
5012}
5013
5014impl WorkspaceStore {
5015    pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
5016        Self {
5017            workspaces: Default::default(),
5018            _subscriptions: vec![
5019                client.add_request_handler(cx.weak_model(), Self::handle_follow),
5020                client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
5021            ],
5022            client,
5023        }
5024    }
5025
5026    pub fn update_followers(
5027        &self,
5028        project_id: Option<u64>,
5029        update: proto::update_followers::Variant,
5030        cx: &AppContext,
5031    ) -> Option<()> {
5032        let active_call = ActiveCall::try_global(cx)?;
5033        let room_id = active_call.read(cx).room()?.read(cx).id();
5034        self.client
5035            .send(proto::UpdateFollowers {
5036                room_id,
5037                project_id,
5038                variant: Some(update),
5039            })
5040            .log_err()
5041    }
5042
5043    pub async fn handle_follow(
5044        this: Model<Self>,
5045        envelope: TypedEnvelope<proto::Follow>,
5046        mut cx: AsyncAppContext,
5047    ) -> Result<proto::FollowResponse> {
5048        this.update(&mut cx, |this, cx| {
5049            let follower = Follower {
5050                project_id: envelope.payload.project_id,
5051                peer_id: envelope.original_sender_id()?,
5052            };
5053
5054            let mut response = proto::FollowResponse::default();
5055            this.workspaces.retain(|workspace| {
5056                workspace
5057                    .update(cx, |workspace, cx| {
5058                        let handler_response = workspace.handle_follow(follower.project_id, cx);
5059                        if let Some(active_view) = handler_response.active_view.clone() {
5060                            if workspace.project.read(cx).remote_id() == follower.project_id {
5061                                response.active_view = Some(active_view)
5062                            }
5063                        }
5064                    })
5065                    .is_ok()
5066            });
5067
5068            Ok(response)
5069        })?
5070    }
5071
5072    async fn handle_update_followers(
5073        this: Model<Self>,
5074        envelope: TypedEnvelope<proto::UpdateFollowers>,
5075        mut cx: AsyncAppContext,
5076    ) -> Result<()> {
5077        let leader_id = envelope.original_sender_id()?;
5078        let update = envelope.payload;
5079
5080        this.update(&mut cx, |this, cx| {
5081            this.workspaces.retain(|workspace| {
5082                workspace
5083                    .update(cx, |workspace, cx| {
5084                        let project_id = workspace.project.read(cx).remote_id();
5085                        if update.project_id != project_id && update.project_id.is_some() {
5086                            return;
5087                        }
5088                        workspace.handle_update_followers(leader_id, update.clone(), cx);
5089                    })
5090                    .is_ok()
5091            });
5092            Ok(())
5093        })?
5094    }
5095}
5096
5097impl ViewId {
5098    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5099        Ok(Self {
5100            creator: message
5101                .creator
5102                .ok_or_else(|| anyhow!("creator is missing"))?,
5103            id: message.id,
5104        })
5105    }
5106
5107    pub(crate) fn to_proto(self) -> proto::ViewId {
5108        proto::ViewId {
5109            creator: Some(self.creator),
5110            id: self.id,
5111        }
5112    }
5113}
5114
5115impl FollowerState {
5116    fn pane(&self) -> &View<Pane> {
5117        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5118    }
5119}
5120
5121pub trait WorkspaceHandle {
5122    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
5123}
5124
5125impl WorkspaceHandle for View<Workspace> {
5126    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
5127        self.read(cx)
5128            .worktrees(cx)
5129            .flat_map(|worktree| {
5130                let worktree_id = worktree.read(cx).id();
5131                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5132                    worktree_id,
5133                    path: f.path.clone(),
5134                })
5135            })
5136            .collect::<Vec<_>>()
5137    }
5138}
5139
5140impl std::fmt::Debug for OpenPaths {
5141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5142        f.debug_struct("OpenPaths")
5143            .field("paths", &self.paths)
5144            .finish()
5145    }
5146}
5147
5148pub fn activate_workspace_for_project(
5149    cx: &mut AppContext,
5150    predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
5151) -> Option<WindowHandle<Workspace>> {
5152    for window in cx.windows() {
5153        let Some(workspace) = window.downcast::<Workspace>() else {
5154            continue;
5155        };
5156
5157        let predicate = workspace
5158            .update(cx, |workspace, cx| {
5159                let project = workspace.project.read(cx);
5160                if predicate(project, cx) {
5161                    cx.activate_window();
5162                    true
5163                } else {
5164                    false
5165                }
5166            })
5167            .log_err()
5168            .unwrap_or(false);
5169
5170        if predicate {
5171            return Some(workspace);
5172        }
5173    }
5174
5175    None
5176}
5177
5178pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5179    DB.last_workspace().await.log_err().flatten()
5180}
5181
5182pub fn last_session_workspace_locations(
5183    last_session_id: &str,
5184    last_session_window_stack: Option<Vec<WindowId>>,
5185) -> Option<Vec<SerializedWorkspaceLocation>> {
5186    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5187        .log_err()
5188}
5189
5190actions!(collab, [OpenChannelNotes]);
5191actions!(zed, [OpenLog]);
5192
5193async fn join_channel_internal(
5194    channel_id: ChannelId,
5195    app_state: &Arc<AppState>,
5196    requesting_window: Option<WindowHandle<Workspace>>,
5197    active_call: &Model<ActiveCall>,
5198    cx: &mut AsyncAppContext,
5199) -> Result<bool> {
5200    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5201        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5202            return (false, None);
5203        };
5204
5205        let already_in_channel = room.channel_id() == Some(channel_id);
5206        let should_prompt = room.is_sharing_project()
5207            && !room.remote_participants().is_empty()
5208            && !already_in_channel;
5209        let open_room = if already_in_channel {
5210            active_call.room().cloned()
5211        } else {
5212            None
5213        };
5214        (should_prompt, open_room)
5215    })?;
5216
5217    if let Some(room) = open_room {
5218        let task = room.update(cx, |room, cx| {
5219            if let Some((project, host)) = room.most_active_project(cx) {
5220                return Some(join_in_room_project(project, host, app_state.clone(), cx));
5221            }
5222
5223            None
5224        })?;
5225        if let Some(task) = task {
5226            task.await?;
5227        }
5228        return anyhow::Ok(true);
5229    }
5230
5231    if should_prompt {
5232        if let Some(workspace) = requesting_window {
5233            let answer = workspace
5234                .update(cx, |_, cx| {
5235                    cx.prompt(
5236                        PromptLevel::Warning,
5237                        "Do you want to switch channels?",
5238                        Some("Leaving this call will unshare your current project."),
5239                        &["Yes, Join Channel", "Cancel"],
5240                    )
5241                })?
5242                .await;
5243
5244            if answer == Ok(1) {
5245                return Ok(false);
5246            }
5247        } else {
5248            return Ok(false); // unreachable!() hopefully
5249        }
5250    }
5251
5252    let client = cx.update(|cx| active_call.read(cx).client())?;
5253
5254    let mut client_status = client.status();
5255
5256    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5257    'outer: loop {
5258        let Some(status) = client_status.recv().await else {
5259            return Err(anyhow!("error connecting"));
5260        };
5261
5262        match status {
5263            Status::Connecting
5264            | Status::Authenticating
5265            | Status::Reconnecting
5266            | Status::Reauthenticating => continue,
5267            Status::Connected { .. } => break 'outer,
5268            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5269            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5270            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5271                return Err(ErrorCode::Disconnected.into());
5272            }
5273        }
5274    }
5275
5276    let room = active_call
5277        .update(cx, |active_call, cx| {
5278            active_call.join_channel(channel_id, cx)
5279        })?
5280        .await?;
5281
5282    let Some(room) = room else {
5283        return anyhow::Ok(true);
5284    };
5285
5286    room.update(cx, |room, _| room.room_update_completed())?
5287        .await;
5288
5289    let task = room.update(cx, |room, cx| {
5290        if let Some((project, host)) = room.most_active_project(cx) {
5291            return Some(join_in_room_project(project, host, app_state.clone(), cx));
5292        }
5293
5294        // If you are the first to join a channel, see if you should share your project.
5295        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5296            if let Some(workspace) = requesting_window {
5297                let project = workspace.update(cx, |workspace, cx| {
5298                    let project = workspace.project.read(cx);
5299
5300                    if !CallSettings::get_global(cx).share_on_join {
5301                        return None;
5302                    }
5303
5304                    if (project.is_local() || project.is_via_ssh())
5305                        && project.visible_worktrees(cx).any(|tree| {
5306                            tree.read(cx)
5307                                .root_entry()
5308                                .map_or(false, |entry| entry.is_dir())
5309                        })
5310                    {
5311                        Some(workspace.project.clone())
5312                    } else {
5313                        None
5314                    }
5315                });
5316                if let Ok(Some(project)) = project {
5317                    return Some(cx.spawn(|room, mut cx| async move {
5318                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5319                            .await?;
5320                        Ok(())
5321                    }));
5322                }
5323            }
5324        }
5325
5326        None
5327    })?;
5328    if let Some(task) = task {
5329        task.await?;
5330        return anyhow::Ok(true);
5331    }
5332    anyhow::Ok(false)
5333}
5334
5335pub fn join_channel(
5336    channel_id: ChannelId,
5337    app_state: Arc<AppState>,
5338    requesting_window: Option<WindowHandle<Workspace>>,
5339    cx: &mut AppContext,
5340) -> Task<Result<()>> {
5341    let active_call = ActiveCall::global(cx);
5342    cx.spawn(|mut cx| async move {
5343        let result = join_channel_internal(
5344            channel_id,
5345            &app_state,
5346            requesting_window,
5347            &active_call,
5348            &mut cx,
5349        )
5350            .await;
5351
5352        // join channel succeeded, and opened a window
5353        if matches!(result, Ok(true)) {
5354            return anyhow::Ok(());
5355        }
5356
5357        // find an existing workspace to focus and show call controls
5358        let mut active_window =
5359            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5360        if active_window.is_none() {
5361            // no open workspaces, make one to show the error in (blergh)
5362            let (window_handle, _) = cx
5363                .update(|cx| {
5364                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5365                })?
5366                .await?;
5367
5368            if result.is_ok() {
5369                cx.update(|cx| {
5370                    cx.dispatch_action(&OpenChannelNotes);
5371                }).log_err();
5372            }
5373
5374            active_window = Some(window_handle);
5375        }
5376
5377        if let Err(err) = result {
5378            log::error!("failed to join channel: {}", err);
5379            if let Some(active_window) = active_window {
5380                active_window
5381                    .update(&mut cx, |_, cx| {
5382                        let detail: SharedString = match err.error_code() {
5383                            ErrorCode::SignedOut => {
5384                                "Please sign in to continue.".into()
5385                            }
5386                            ErrorCode::UpgradeRequired => {
5387                                "Your are running an unsupported version of Zed. Please update to continue.".into()
5388                            }
5389                            ErrorCode::NoSuchChannel => {
5390                                "No matching channel was found. Please check the link and try again.".into()
5391                            }
5392                            ErrorCode::Forbidden => {
5393                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5394                            }
5395                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5396                            _ => format!("{}\n\nPlease try again.", err).into(),
5397                        };
5398                        cx.prompt(
5399                            PromptLevel::Critical,
5400                            "Failed to join channel",
5401                            Some(&detail),
5402                            &["Ok"],
5403                        )
5404                    })?
5405                    .await
5406                    .ok();
5407            }
5408        }
5409
5410        // return ok, we showed the error to the user.
5411        anyhow::Ok(())
5412    })
5413}
5414
5415pub async fn get_any_active_workspace(
5416    app_state: Arc<AppState>,
5417    mut cx: AsyncAppContext,
5418) -> anyhow::Result<WindowHandle<Workspace>> {
5419    // find an existing workspace to focus and show call controls
5420    let active_window = activate_any_workspace_window(&mut cx);
5421    if active_window.is_none() {
5422        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5423            .await?;
5424    }
5425    activate_any_workspace_window(&mut cx).context("could not open zed")
5426}
5427
5428fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5429    cx.update(|cx| {
5430        if let Some(workspace_window) = cx
5431            .active_window()
5432            .and_then(|window| window.downcast::<Workspace>())
5433        {
5434            return Some(workspace_window);
5435        }
5436
5437        for window in cx.windows() {
5438            if let Some(workspace_window) = window.downcast::<Workspace>() {
5439                workspace_window
5440                    .update(cx, |_, cx| cx.activate_window())
5441                    .ok();
5442                return Some(workspace_window);
5443            }
5444        }
5445        None
5446    })
5447    .ok()
5448    .flatten()
5449}
5450
5451pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5452    cx.windows()
5453        .into_iter()
5454        .filter_map(|window| window.downcast::<Workspace>())
5455        .filter(|workspace| {
5456            workspace
5457                .read(cx)
5458                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5459        })
5460        .collect()
5461}
5462
5463#[derive(Default)]
5464pub struct OpenOptions {
5465    pub open_new_workspace: Option<bool>,
5466    pub replace_window: Option<WindowHandle<Workspace>>,
5467    pub env: Option<HashMap<String, String>>,
5468}
5469
5470#[allow(clippy::type_complexity)]
5471pub fn open_paths(
5472    abs_paths: &[PathBuf],
5473    app_state: Arc<AppState>,
5474    open_options: OpenOptions,
5475    cx: &mut AppContext,
5476) -> Task<
5477    anyhow::Result<(
5478        WindowHandle<Workspace>,
5479        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5480    )>,
5481> {
5482    let abs_paths = abs_paths.to_vec();
5483    let mut existing = None;
5484    let mut best_match = None;
5485    let mut open_visible = OpenVisible::All;
5486
5487    if open_options.open_new_workspace != Some(true) {
5488        for window in local_workspace_windows(cx) {
5489            if let Ok(workspace) = window.read(cx) {
5490                let m = workspace
5491                    .project
5492                    .read(cx)
5493                    .visibility_for_paths(&abs_paths, cx);
5494                if m > best_match {
5495                    existing = Some(window);
5496                    best_match = m;
5497                } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5498                    existing = Some(window)
5499                }
5500            }
5501        }
5502    }
5503
5504    cx.spawn(move |mut cx| async move {
5505        if open_options.open_new_workspace.is_none() && existing.is_none() {
5506            let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5507            if futures::future::join_all(all_files)
5508                .await
5509                .into_iter()
5510                .filter_map(|result| result.ok().flatten())
5511                .all(|file| !file.is_dir)
5512            {
5513                cx.update(|cx| {
5514                    for window in local_workspace_windows(cx) {
5515                        if let Ok(workspace) = window.read(cx) {
5516                            let project = workspace.project().read(cx);
5517                            if project.is_via_collab() {
5518                                continue;
5519                            }
5520                            existing = Some(window);
5521                            open_visible = OpenVisible::None;
5522                            break;
5523                        }
5524                    }
5525                })?;
5526            }
5527        }
5528
5529        if let Some(existing) = existing {
5530            Ok((
5531                existing,
5532                existing
5533                    .update(&mut cx, |workspace, cx| {
5534                        cx.activate_window();
5535                        workspace.open_paths(abs_paths, open_visible, None, cx)
5536                    })?
5537                    .await,
5538            ))
5539        } else {
5540            cx.update(move |cx| {
5541                Workspace::new_local(
5542                    abs_paths,
5543                    app_state.clone(),
5544                    open_options.replace_window,
5545                    open_options.env,
5546                    cx,
5547                )
5548            })?
5549            .await
5550        }
5551    })
5552}
5553
5554pub fn open_new(
5555    open_options: OpenOptions,
5556    app_state: Arc<AppState>,
5557    cx: &mut AppContext,
5558    init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5559) -> Task<anyhow::Result<()>> {
5560    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5561    cx.spawn(|mut cx| async move {
5562        let (workspace, opened_paths) = task.await?;
5563        workspace.update(&mut cx, |workspace, cx| {
5564            if opened_paths.is_empty() {
5565                init(workspace, cx)
5566            }
5567        })?;
5568        Ok(())
5569    })
5570}
5571
5572pub fn create_and_open_local_file(
5573    path: &'static Path,
5574    cx: &mut ViewContext<Workspace>,
5575    default_content: impl 'static + Send + FnOnce() -> Rope,
5576) -> Task<Result<Box<dyn ItemHandle>>> {
5577    cx.spawn(|workspace, mut cx| async move {
5578        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5579        if !fs.is_file(path).await {
5580            fs.create_file(path, Default::default()).await?;
5581            fs.save(path, &default_content(), Default::default())
5582                .await?;
5583        }
5584
5585        let mut items = workspace
5586            .update(&mut cx, |workspace, cx| {
5587                workspace.with_local_workspace(cx, |workspace, cx| {
5588                    workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5589                })
5590            })?
5591            .await?
5592            .await;
5593
5594        let item = items.pop().flatten();
5595        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5596    })
5597}
5598
5599pub fn open_ssh_project(
5600    window: WindowHandle<Workspace>,
5601    connection_options: SshConnectionOptions,
5602    cancel_rx: oneshot::Receiver<()>,
5603    delegate: Arc<dyn SshClientDelegate>,
5604    app_state: Arc<AppState>,
5605    paths: Vec<PathBuf>,
5606    cx: &mut AppContext,
5607) -> Task<Result<()>> {
5608    cx.spawn(|mut cx| async move {
5609        let (serialized_ssh_project, workspace_id, serialized_workspace) =
5610            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5611
5612        let session = match cx
5613            .update(|cx| {
5614                remote::SshRemoteClient::new(
5615                    ConnectionIdentifier::Workspace(workspace_id.0),
5616                    connection_options,
5617                    cancel_rx,
5618                    delegate,
5619                    cx,
5620                )
5621            })?
5622            .await?
5623        {
5624            Some(result) => result,
5625            None => return Ok(()),
5626        };
5627
5628        let project = cx.update(|cx| {
5629            project::Project::ssh(
5630                session,
5631                app_state.client.clone(),
5632                app_state.node_runtime.clone(),
5633                app_state.user_store.clone(),
5634                app_state.languages.clone(),
5635                app_state.fs.clone(),
5636                cx,
5637            )
5638        })?;
5639
5640        let toolchains = DB.toolchains(workspace_id).await?;
5641        for (toolchain, worktree_id) in toolchains {
5642            project
5643                .update(&mut cx, |this, cx| {
5644                    this.activate_toolchain(worktree_id, toolchain, cx)
5645                })?
5646                .await;
5647        }
5648        let mut project_paths_to_open = vec![];
5649        let mut project_path_errors = vec![];
5650
5651        for path in paths {
5652            let result = cx
5653                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5654                .await;
5655            match result {
5656                Ok((_, project_path)) => {
5657                    project_paths_to_open.push((path.clone(), Some(project_path)));
5658                }
5659                Err(error) => {
5660                    project_path_errors.push(error);
5661                }
5662            };
5663        }
5664
5665        if project_paths_to_open.is_empty() {
5666            return Err(project_path_errors
5667                .pop()
5668                .unwrap_or_else(|| anyhow!("no paths given")));
5669        }
5670
5671        cx.update_window(window.into(), |_, cx| {
5672            cx.replace_root_view(|cx| {
5673                let mut workspace =
5674                    Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5675
5676                workspace
5677                    .client()
5678                    .telemetry()
5679                    .report_app_event("open ssh project".to_string());
5680
5681                workspace.set_serialized_ssh_project(serialized_ssh_project);
5682                workspace
5683            });
5684        })?;
5685
5686        window
5687            .update(&mut cx, |_, cx| {
5688                cx.activate_window();
5689
5690                open_items(serialized_workspace, project_paths_to_open, cx)
5691            })?
5692            .await?;
5693
5694        window.update(&mut cx, |workspace, cx| {
5695            for error in project_path_errors {
5696                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5697                    if let Some(path) = error.error_tag("path") {
5698                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5699                    }
5700                } else {
5701                    workspace.show_error(&error, cx)
5702                }
5703            }
5704        })
5705    })
5706}
5707
5708fn serialize_ssh_project(
5709    connection_options: SshConnectionOptions,
5710    paths: Vec<PathBuf>,
5711    cx: &AsyncAppContext,
5712) -> Task<
5713    Result<(
5714        SerializedSshProject,
5715        WorkspaceId,
5716        Option<SerializedWorkspace>,
5717    )>,
5718> {
5719    cx.background_executor().spawn(async move {
5720        let serialized_ssh_project = persistence::DB
5721            .get_or_create_ssh_project(
5722                connection_options.host.clone(),
5723                connection_options.port,
5724                paths
5725                    .iter()
5726                    .map(|path| path.to_string_lossy().to_string())
5727                    .collect::<Vec<_>>(),
5728                connection_options.username.clone(),
5729            )
5730            .await?;
5731
5732        let serialized_workspace =
5733            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5734
5735        let workspace_id = if let Some(workspace_id) =
5736            serialized_workspace.as_ref().map(|workspace| workspace.id)
5737        {
5738            workspace_id
5739        } else {
5740            persistence::DB.next_id().await?
5741        };
5742
5743        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5744    })
5745}
5746
5747pub fn join_in_room_project(
5748    project_id: u64,
5749    follow_user_id: u64,
5750    app_state: Arc<AppState>,
5751    cx: &mut AppContext,
5752) -> Task<Result<()>> {
5753    let windows = cx.windows();
5754    cx.spawn(|mut cx| async move {
5755        let existing_workspace = windows.into_iter().find_map(|window| {
5756            window.downcast::<Workspace>().and_then(|window| {
5757                window
5758                    .update(&mut cx, |workspace, cx| {
5759                        if workspace.project().read(cx).remote_id() == Some(project_id) {
5760                            Some(window)
5761                        } else {
5762                            None
5763                        }
5764                    })
5765                    .unwrap_or(None)
5766            })
5767        });
5768
5769        let workspace = if let Some(existing_workspace) = existing_workspace {
5770            existing_workspace
5771        } else {
5772            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5773            let room = active_call
5774                .read_with(&cx, |call, _| call.room().cloned())?
5775                .ok_or_else(|| anyhow!("not in a call"))?;
5776            let project = room
5777                .update(&mut cx, |room, cx| {
5778                    room.join_project(
5779                        project_id,
5780                        app_state.languages.clone(),
5781                        app_state.fs.clone(),
5782                        cx,
5783                    )
5784                })?
5785                .await?;
5786
5787            let window_bounds_override = window_bounds_env_override();
5788            cx.update(|cx| {
5789                let mut options = (app_state.build_window_options)(None, cx);
5790                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5791                cx.open_window(options, |cx| {
5792                    cx.new_view(|cx| {
5793                        Workspace::new(Default::default(), project, app_state.clone(), cx)
5794                    })
5795                })
5796            })??
5797        };
5798
5799        workspace.update(&mut cx, |workspace, cx| {
5800            cx.activate(true);
5801            cx.activate_window();
5802
5803            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5804                let follow_peer_id = room
5805                    .read(cx)
5806                    .remote_participants()
5807                    .iter()
5808                    .find(|(_, participant)| participant.user.id == follow_user_id)
5809                    .map(|(_, p)| p.peer_id)
5810                    .or_else(|| {
5811                        // If we couldn't follow the given user, follow the host instead.
5812                        let collaborator = workspace
5813                            .project()
5814                            .read(cx)
5815                            .collaborators()
5816                            .values()
5817                            .find(|collaborator| collaborator.is_host)?;
5818                        Some(collaborator.peer_id)
5819                    });
5820
5821                if let Some(follow_peer_id) = follow_peer_id {
5822                    workspace.follow(follow_peer_id, cx);
5823                }
5824            }
5825        })?;
5826
5827        anyhow::Ok(())
5828    })
5829}
5830
5831pub fn reload(reload: &Reload, cx: &mut AppContext) {
5832    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5833    let mut workspace_windows = cx
5834        .windows()
5835        .into_iter()
5836        .filter_map(|window| window.downcast::<Workspace>())
5837        .collect::<Vec<_>>();
5838
5839    // If multiple windows have unsaved changes, and need a save prompt,
5840    // prompt in the active window before switching to a different window.
5841    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5842
5843    let mut prompt = None;
5844    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5845        prompt = window
5846            .update(cx, |_, cx| {
5847                cx.prompt(
5848                    PromptLevel::Info,
5849                    "Are you sure you want to restart?",
5850                    None,
5851                    &["Restart", "Cancel"],
5852                )
5853            })
5854            .ok();
5855    }
5856
5857    let binary_path = reload.binary_path.clone();
5858    cx.spawn(|mut cx| async move {
5859        if let Some(prompt) = prompt {
5860            let answer = prompt.await?;
5861            if answer != 0 {
5862                return Ok(());
5863            }
5864        }
5865
5866        // If the user cancels any save prompt, then keep the app open.
5867        for window in workspace_windows {
5868            if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5869                workspace.prepare_to_close(CloseIntent::Quit, cx)
5870            }) {
5871                if !should_close.await? {
5872                    return Ok(());
5873                }
5874            }
5875        }
5876
5877        cx.update(|cx| cx.restart(binary_path))
5878    })
5879    .detach_and_log_err(cx);
5880}
5881
5882fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5883    let mut parts = value.split(',');
5884    let x: usize = parts.next()?.parse().ok()?;
5885    let y: usize = parts.next()?.parse().ok()?;
5886    Some(point(px(x as f32), px(y as f32)))
5887}
5888
5889fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5890    let mut parts = value.split(',');
5891    let width: usize = parts.next()?.parse().ok()?;
5892    let height: usize = parts.next()?.parse().ok()?;
5893    Some(size(px(width as f32), px(height as f32)))
5894}
5895
5896pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5897    const BORDER_SIZE: Pixels = px(1.0);
5898    let decorations = cx.window_decorations();
5899
5900    if matches!(decorations, Decorations::Client { .. }) {
5901        cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5902    }
5903
5904    struct GlobalResizeEdge(ResizeEdge);
5905    impl Global for GlobalResizeEdge {}
5906
5907    div()
5908        .id("window-backdrop")
5909        .bg(transparent_black())
5910        .map(|div| match decorations {
5911            Decorations::Server => div,
5912            Decorations::Client { tiling, .. } => div
5913                .when(!(tiling.top || tiling.right), |div| {
5914                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5915                })
5916                .when(!(tiling.top || tiling.left), |div| {
5917                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5918                })
5919                .when(!(tiling.bottom || tiling.right), |div| {
5920                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5921                })
5922                .when(!(tiling.bottom || tiling.left), |div| {
5923                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5924                })
5925                .when(!tiling.top, |div| {
5926                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
5927                })
5928                .when(!tiling.bottom, |div| {
5929                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
5930                })
5931                .when(!tiling.left, |div| {
5932                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
5933                })
5934                .when(!tiling.right, |div| {
5935                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
5936                })
5937                .on_mouse_move(move |e, cx| {
5938                    let size = cx.window_bounds().get_bounds().size;
5939                    let pos = e.position;
5940
5941                    let new_edge =
5942                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
5943
5944                    let edge = cx.try_global::<GlobalResizeEdge>();
5945                    if new_edge != edge.map(|edge| edge.0) {
5946                        cx.window_handle()
5947                            .update(cx, |workspace, cx| cx.notify(Some(workspace.entity_id())))
5948                            .ok();
5949                    }
5950                })
5951                .on_mouse_down(MouseButton::Left, move |e, cx| {
5952                    let size = cx.window_bounds().get_bounds().size;
5953                    let pos = e.position;
5954
5955                    let edge = match resize_edge(
5956                        pos,
5957                        theme::CLIENT_SIDE_DECORATION_SHADOW,
5958                        size,
5959                        tiling,
5960                    ) {
5961                        Some(value) => value,
5962                        None => return,
5963                    };
5964
5965                    cx.start_window_resize(edge);
5966                }),
5967        })
5968        .size_full()
5969        .child(
5970            div()
5971                .cursor(CursorStyle::Arrow)
5972                .map(|div| match decorations {
5973                    Decorations::Server => div,
5974                    Decorations::Client { tiling } => div
5975                        .border_color(cx.theme().colors().border)
5976                        .when(!(tiling.top || tiling.right), |div| {
5977                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5978                        })
5979                        .when(!(tiling.top || tiling.left), |div| {
5980                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5981                        })
5982                        .when(!(tiling.bottom || tiling.right), |div| {
5983                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5984                        })
5985                        .when(!(tiling.bottom || tiling.left), |div| {
5986                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5987                        })
5988                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
5989                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
5990                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
5991                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
5992                        .when(!tiling.is_tiled(), |div| {
5993                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
5994                                color: Hsla {
5995                                    h: 0.,
5996                                    s: 0.,
5997                                    l: 0.,
5998                                    a: 0.4,
5999                                },
6000                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6001                                spread_radius: px(0.),
6002                                offset: point(px(0.0), px(0.0)),
6003                            }])
6004                        }),
6005                })
6006                .on_mouse_move(|_e, cx| {
6007                    cx.stop_propagation();
6008                })
6009                .size_full()
6010                .child(element),
6011        )
6012        .map(|div| match decorations {
6013            Decorations::Server => div,
6014            Decorations::Client { tiling, .. } => div.child(
6015                canvas(
6016                    |_bounds, cx| {
6017                        cx.insert_hitbox(
6018                            Bounds::new(
6019                                point(px(0.0), px(0.0)),
6020                                cx.window_bounds().get_bounds().size,
6021                            ),
6022                            false,
6023                        )
6024                    },
6025                    move |_bounds, hitbox, cx| {
6026                        let mouse = cx.mouse_position();
6027                        let size = cx.window_bounds().get_bounds().size;
6028                        let Some(edge) =
6029                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6030                        else {
6031                            return;
6032                        };
6033                        cx.set_global(GlobalResizeEdge(edge));
6034                        cx.set_cursor_style(
6035                            match edge {
6036                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6037                                ResizeEdge::Left | ResizeEdge::Right => {
6038                                    CursorStyle::ResizeLeftRight
6039                                }
6040                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6041                                    CursorStyle::ResizeUpLeftDownRight
6042                                }
6043                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6044                                    CursorStyle::ResizeUpRightDownLeft
6045                                }
6046                            },
6047                            &hitbox,
6048                        );
6049                    },
6050                )
6051                .size_full()
6052                .absolute(),
6053            ),
6054        })
6055}
6056
6057fn resize_edge(
6058    pos: Point<Pixels>,
6059    shadow_size: Pixels,
6060    window_size: Size<Pixels>,
6061    tiling: Tiling,
6062) -> Option<ResizeEdge> {
6063    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6064    if bounds.contains(&pos) {
6065        return None;
6066    }
6067
6068    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6069    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6070    if !tiling.top && top_left_bounds.contains(&pos) {
6071        return Some(ResizeEdge::TopLeft);
6072    }
6073
6074    let top_right_bounds = Bounds::new(
6075        Point::new(window_size.width - corner_size.width, px(0.)),
6076        corner_size,
6077    );
6078    if !tiling.top && top_right_bounds.contains(&pos) {
6079        return Some(ResizeEdge::TopRight);
6080    }
6081
6082    let bottom_left_bounds = Bounds::new(
6083        Point::new(px(0.), window_size.height - corner_size.height),
6084        corner_size,
6085    );
6086    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6087        return Some(ResizeEdge::BottomLeft);
6088    }
6089
6090    let bottom_right_bounds = Bounds::new(
6091        Point::new(
6092            window_size.width - corner_size.width,
6093            window_size.height - corner_size.height,
6094        ),
6095        corner_size,
6096    );
6097    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6098        return Some(ResizeEdge::BottomRight);
6099    }
6100
6101    if !tiling.top && pos.y < shadow_size {
6102        Some(ResizeEdge::Top)
6103    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6104        Some(ResizeEdge::Bottom)
6105    } else if !tiling.left && pos.x < shadow_size {
6106        Some(ResizeEdge::Left)
6107    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6108        Some(ResizeEdge::Right)
6109    } else {
6110        None
6111    }
6112}
6113
6114fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6115    if pane == active_pane {
6116        return;
6117    } else if pane.read(cx).items_len() == 0 {
6118        pane.update(cx, |_, cx| {
6119            cx.emit(pane::Event::Remove {
6120                focus_on_pane: None,
6121            });
6122        })
6123    } else {
6124        move_all_items(pane, active_pane, cx);
6125    }
6126}
6127
6128fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6129    let destination_is_different = from_pane != to_pane;
6130    let mut moved_items = 0;
6131    for (item_ix, item_handle) in from_pane
6132        .read(cx)
6133        .items()
6134        .enumerate()
6135        .map(|(ix, item)| (ix, item.clone()))
6136        .collect::<Vec<_>>()
6137    {
6138        let ix = item_ix - moved_items;
6139        if destination_is_different {
6140            // Close item from previous pane
6141            from_pane.update(cx, |source, cx| {
6142                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6143            });
6144            moved_items += 1;
6145        }
6146
6147        // This automatically removes duplicate items in the pane
6148        to_pane.update(cx, |destination, cx| {
6149            destination.add_item(item_handle, true, true, None, cx);
6150            destination.focus(cx)
6151        });
6152    }
6153}
6154
6155pub fn move_item(
6156    source: &View<Pane>,
6157    destination: &View<Pane>,
6158    item_id_to_move: EntityId,
6159    destination_index: usize,
6160    cx: &mut WindowContext<'_>,
6161) {
6162    let Some((item_ix, item_handle)) = source
6163        .read(cx)
6164        .items()
6165        .enumerate()
6166        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6167        .map(|(ix, item)| (ix, item.clone()))
6168    else {
6169        // Tab was closed during drag
6170        return;
6171    };
6172
6173    if source != destination {
6174        // Close item from previous pane
6175        source.update(cx, |source, cx| {
6176            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6177        });
6178    }
6179
6180    // This automatically removes duplicate items in the pane
6181    destination.update(cx, |destination, cx| {
6182        destination.add_item(item_handle, true, true, Some(destination_index), cx);
6183        destination.focus(cx)
6184    });
6185}
6186
6187#[cfg(test)]
6188mod tests {
6189    use std::{cell::RefCell, rc::Rc};
6190
6191    use super::*;
6192    use crate::{
6193        dock::{test::TestPanel, PanelEvent},
6194        item::{
6195            test::{TestItem, TestProjectItem},
6196            ItemEvent,
6197        },
6198    };
6199    use fs::FakeFs;
6200    use gpui::{
6201        px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6202        UpdateGlobal, VisualTestContext,
6203    };
6204    use project::{Project, ProjectEntryId};
6205    use serde_json::json;
6206    use settings::SettingsStore;
6207
6208    #[gpui::test]
6209    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6210        init_test(cx);
6211
6212        let fs = FakeFs::new(cx.executor());
6213        let project = Project::test(fs, [], cx).await;
6214        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6215
6216        // Adding an item with no ambiguity renders the tab without detail.
6217        let item1 = cx.new_view(|cx| {
6218            let mut item = TestItem::new(cx);
6219            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6220            item
6221        });
6222        workspace.update(cx, |workspace, cx| {
6223            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6224        });
6225        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6226
6227        // Adding an item that creates ambiguity increases the level of detail on
6228        // both tabs.
6229        let item2 = cx.new_view(|cx| {
6230            let mut item = TestItem::new(cx);
6231            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6232            item
6233        });
6234        workspace.update(cx, |workspace, cx| {
6235            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6236        });
6237        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6238        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6239
6240        // Adding an item that creates ambiguity increases the level of detail only
6241        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6242        // we stop at the highest detail available.
6243        let item3 = cx.new_view(|cx| {
6244            let mut item = TestItem::new(cx);
6245            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6246            item
6247        });
6248        workspace.update(cx, |workspace, cx| {
6249            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6250        });
6251        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6252        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6253        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6254    }
6255
6256    #[gpui::test]
6257    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6258        init_test(cx);
6259
6260        let fs = FakeFs::new(cx.executor());
6261        fs.insert_tree(
6262            "/root1",
6263            json!({
6264                "one.txt": "",
6265                "two.txt": "",
6266            }),
6267        )
6268        .await;
6269        fs.insert_tree(
6270            "/root2",
6271            json!({
6272                "three.txt": "",
6273            }),
6274        )
6275        .await;
6276
6277        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6278        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6279        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6280        let worktree_id = project.update(cx, |project, cx| {
6281            project.worktrees(cx).next().unwrap().read(cx).id()
6282        });
6283
6284        let item1 = cx.new_view(|cx| {
6285            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6286        });
6287        let item2 = cx.new_view(|cx| {
6288            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6289        });
6290
6291        // Add an item to an empty pane
6292        workspace.update(cx, |workspace, cx| {
6293            workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6294        });
6295        project.update(cx, |project, cx| {
6296            assert_eq!(
6297                project.active_entry(),
6298                project
6299                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6300                    .map(|e| e.id)
6301            );
6302        });
6303        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6304
6305        // Add a second item to a non-empty pane
6306        workspace.update(cx, |workspace, cx| {
6307            workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6308        });
6309        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6310        project.update(cx, |project, cx| {
6311            assert_eq!(
6312                project.active_entry(),
6313                project
6314                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6315                    .map(|e| e.id)
6316            );
6317        });
6318
6319        // Close the active item
6320        pane.update(cx, |pane, cx| {
6321            pane.close_active_item(&Default::default(), cx).unwrap()
6322        })
6323        .await
6324        .unwrap();
6325        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6326        project.update(cx, |project, cx| {
6327            assert_eq!(
6328                project.active_entry(),
6329                project
6330                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6331                    .map(|e| e.id)
6332            );
6333        });
6334
6335        // Add a project folder
6336        project
6337            .update(cx, |project, cx| {
6338                project.find_or_create_worktree("root2", true, cx)
6339            })
6340            .await
6341            .unwrap();
6342        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6343
6344        // Remove a project folder
6345        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6346        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6347    }
6348
6349    #[gpui::test]
6350    async fn test_close_window(cx: &mut TestAppContext) {
6351        init_test(cx);
6352
6353        let fs = FakeFs::new(cx.executor());
6354        fs.insert_tree("/root", json!({ "one": "" })).await;
6355
6356        let project = Project::test(fs, ["root".as_ref()], cx).await;
6357        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6358
6359        // When there are no dirty items, there's nothing to do.
6360        let item1 = cx.new_view(TestItem::new);
6361        workspace.update(cx, |w, cx| {
6362            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6363        });
6364        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6365        assert!(task.await.unwrap());
6366
6367        // When there are dirty untitled items, prompt to save each one. If the user
6368        // cancels any prompt, then abort.
6369        let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6370        let item3 = cx.new_view(|cx| {
6371            TestItem::new(cx)
6372                .with_dirty(true)
6373                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6374        });
6375        workspace.update(cx, |w, cx| {
6376            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6377            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6378        });
6379        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6380        cx.executor().run_until_parked();
6381        cx.simulate_prompt_answer(2); // cancel save all
6382        cx.executor().run_until_parked();
6383        cx.simulate_prompt_answer(2); // cancel save all
6384        cx.executor().run_until_parked();
6385        assert!(!cx.has_pending_prompt());
6386        assert!(!task.await.unwrap());
6387    }
6388
6389    #[gpui::test]
6390    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6391        init_test(cx);
6392
6393        // Register TestItem as a serializable item
6394        cx.update(|cx| {
6395            register_serializable_item::<TestItem>(cx);
6396        });
6397
6398        let fs = FakeFs::new(cx.executor());
6399        fs.insert_tree("/root", json!({ "one": "" })).await;
6400
6401        let project = Project::test(fs, ["root".as_ref()], cx).await;
6402        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6403
6404        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6405        let item1 = cx.new_view(|cx| {
6406            TestItem::new(cx)
6407                .with_dirty(true)
6408                .with_serialize(|| Some(Task::ready(Ok(()))))
6409        });
6410        let item2 = cx.new_view(|cx| {
6411            TestItem::new(cx)
6412                .with_dirty(true)
6413                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6414                .with_serialize(|| Some(Task::ready(Ok(()))))
6415        });
6416        workspace.update(cx, |w, cx| {
6417            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6418            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6419        });
6420        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6421        assert!(task.await.unwrap());
6422    }
6423
6424    #[gpui::test]
6425    async fn test_close_pane_items(cx: &mut TestAppContext) {
6426        init_test(cx);
6427
6428        let fs = FakeFs::new(cx.executor());
6429
6430        let project = Project::test(fs, None, cx).await;
6431        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6432
6433        let item1 = cx.new_view(|cx| {
6434            TestItem::new(cx)
6435                .with_dirty(true)
6436                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
6437        });
6438        let item2 = cx.new_view(|cx| {
6439            TestItem::new(cx)
6440                .with_dirty(true)
6441                .with_conflict(true)
6442                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
6443        });
6444        let item3 = cx.new_view(|cx| {
6445            TestItem::new(cx)
6446                .with_dirty(true)
6447                .with_conflict(true)
6448                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
6449        });
6450        let item4 = cx.new_view(|cx| {
6451            TestItem::new(cx).with_dirty(true).with_project_items(&[{
6452                let project_item = TestProjectItem::new_untitled(cx);
6453                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6454                project_item
6455            }])
6456        });
6457        let pane = workspace.update(cx, |workspace, cx| {
6458            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6459            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6460            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6461            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6462            workspace.active_pane().clone()
6463        });
6464
6465        let close_items = pane.update(cx, |pane, cx| {
6466            pane.activate_item(1, true, true, cx);
6467            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6468            let item1_id = item1.item_id();
6469            let item3_id = item3.item_id();
6470            let item4_id = item4.item_id();
6471            pane.close_items(cx, SaveIntent::Close, move |id| {
6472                [item1_id, item3_id, item4_id].contains(&id)
6473            })
6474        });
6475        cx.executor().run_until_parked();
6476
6477        assert!(cx.has_pending_prompt());
6478        // Ignore "Save all" prompt
6479        cx.simulate_prompt_answer(2);
6480        cx.executor().run_until_parked();
6481        // There's a prompt to save item 1.
6482        pane.update(cx, |pane, _| {
6483            assert_eq!(pane.items_len(), 4);
6484            assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6485        });
6486        // Confirm saving item 1.
6487        cx.simulate_prompt_answer(0);
6488        cx.executor().run_until_parked();
6489
6490        // Item 1 is saved. There's a prompt to save item 3.
6491        pane.update(cx, |pane, cx| {
6492            assert_eq!(item1.read(cx).save_count, 1);
6493            assert_eq!(item1.read(cx).save_as_count, 0);
6494            assert_eq!(item1.read(cx).reload_count, 0);
6495            assert_eq!(pane.items_len(), 3);
6496            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6497        });
6498        assert!(cx.has_pending_prompt());
6499
6500        // Cancel saving item 3.
6501        cx.simulate_prompt_answer(1);
6502        cx.executor().run_until_parked();
6503
6504        // Item 3 is reloaded. There's a prompt to save item 4.
6505        pane.update(cx, |pane, cx| {
6506            assert_eq!(item3.read(cx).save_count, 0);
6507            assert_eq!(item3.read(cx).save_as_count, 0);
6508            assert_eq!(item3.read(cx).reload_count, 1);
6509            assert_eq!(pane.items_len(), 2);
6510            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6511        });
6512        assert!(cx.has_pending_prompt());
6513
6514        // Confirm saving item 4.
6515        cx.simulate_prompt_answer(0);
6516        cx.executor().run_until_parked();
6517
6518        // There's a prompt for a path for item 4.
6519        cx.simulate_new_path_selection(|_| Some(Default::default()));
6520        close_items.await.unwrap();
6521
6522        // The requested items are closed.
6523        pane.update(cx, |pane, cx| {
6524            assert_eq!(item4.read(cx).save_count, 0);
6525            assert_eq!(item4.read(cx).save_as_count, 1);
6526            assert_eq!(item4.read(cx).reload_count, 0);
6527            assert_eq!(pane.items_len(), 1);
6528            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6529        });
6530    }
6531
6532    #[gpui::test]
6533    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6534        init_test(cx);
6535
6536        let fs = FakeFs::new(cx.executor());
6537        let project = Project::test(fs, [], cx).await;
6538        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6539
6540        // Create several workspace items with single project entries, and two
6541        // workspace items with multiple project entries.
6542        let single_entry_items = (0..=4)
6543            .map(|project_entry_id| {
6544                cx.new_view(|cx| {
6545                    TestItem::new(cx)
6546                        .with_dirty(true)
6547                        .with_project_items(&[dirty_project_item(
6548                            project_entry_id,
6549                            &format!("{project_entry_id}.txt"),
6550                            cx,
6551                        )])
6552                })
6553            })
6554            .collect::<Vec<_>>();
6555        let item_2_3 = cx.new_view(|cx| {
6556            TestItem::new(cx)
6557                .with_dirty(true)
6558                .with_singleton(false)
6559                .with_project_items(&[
6560                    single_entry_items[2].read(cx).project_items[0].clone(),
6561                    single_entry_items[3].read(cx).project_items[0].clone(),
6562                ])
6563        });
6564        let item_3_4 = cx.new_view(|cx| {
6565            TestItem::new(cx)
6566                .with_dirty(true)
6567                .with_singleton(false)
6568                .with_project_items(&[
6569                    single_entry_items[3].read(cx).project_items[0].clone(),
6570                    single_entry_items[4].read(cx).project_items[0].clone(),
6571                ])
6572        });
6573
6574        // Create two panes that contain the following project entries:
6575        //   left pane:
6576        //     multi-entry items:   (2, 3)
6577        //     single-entry items:  0, 1, 2, 3, 4
6578        //   right pane:
6579        //     single-entry items:  1
6580        //     multi-entry items:   (3, 4)
6581        let left_pane = workspace.update(cx, |workspace, cx| {
6582            let left_pane = workspace.active_pane().clone();
6583            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6584            for item in single_entry_items {
6585                workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6586            }
6587            left_pane.update(cx, |pane, cx| {
6588                pane.activate_item(2, true, true, cx);
6589            });
6590
6591            let right_pane = workspace
6592                .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6593                .unwrap();
6594
6595            right_pane.update(cx, |pane, cx| {
6596                pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6597            });
6598
6599            left_pane
6600        });
6601
6602        cx.focus_view(&left_pane);
6603
6604        // When closing all of the items in the left pane, we should be prompted twice:
6605        // once for project entry 0, and once for project entry 2. Project entries 1,
6606        // 3, and 4 are all still open in the other paten. After those two
6607        // prompts, the task should complete.
6608
6609        let close = left_pane.update(cx, |pane, cx| {
6610            pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6611        });
6612        cx.executor().run_until_parked();
6613
6614        // Discard "Save all" prompt
6615        cx.simulate_prompt_answer(2);
6616
6617        cx.executor().run_until_parked();
6618        left_pane.update(cx, |pane, cx| {
6619            assert_eq!(
6620                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6621                &[ProjectEntryId::from_proto(0)]
6622            );
6623        });
6624        cx.simulate_prompt_answer(0);
6625
6626        cx.executor().run_until_parked();
6627        left_pane.update(cx, |pane, cx| {
6628            assert_eq!(
6629                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6630                &[ProjectEntryId::from_proto(2)]
6631            );
6632        });
6633        cx.simulate_prompt_answer(0);
6634
6635        cx.executor().run_until_parked();
6636        close.await.unwrap();
6637        left_pane.update(cx, |pane, _| {
6638            assert_eq!(pane.items_len(), 0);
6639        });
6640    }
6641
6642    #[gpui::test]
6643    async fn test_autosave(cx: &mut gpui::TestAppContext) {
6644        init_test(cx);
6645
6646        let fs = FakeFs::new(cx.executor());
6647        let project = Project::test(fs, [], cx).await;
6648        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6649        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6650
6651        let item = cx.new_view(|cx| {
6652            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6653        });
6654        let item_id = item.entity_id();
6655        workspace.update(cx, |workspace, cx| {
6656            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6657        });
6658
6659        // Autosave on window change.
6660        item.update(cx, |item, cx| {
6661            SettingsStore::update_global(cx, |settings, cx| {
6662                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6663                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
6664                })
6665            });
6666            item.is_dirty = true;
6667        });
6668
6669        // Deactivating the window saves the file.
6670        cx.deactivate_window();
6671        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6672
6673        // Re-activating the window doesn't save the file.
6674        cx.update(|cx| cx.activate_window());
6675        cx.executor().run_until_parked();
6676        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6677
6678        // Autosave on focus change.
6679        item.update(cx, |item, cx| {
6680            cx.focus_self();
6681            SettingsStore::update_global(cx, |settings, cx| {
6682                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6683                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6684                })
6685            });
6686            item.is_dirty = true;
6687        });
6688
6689        // Blurring the item saves the file.
6690        item.update(cx, |_, cx| cx.blur());
6691        cx.executor().run_until_parked();
6692        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6693
6694        // Deactivating the window still saves the file.
6695        item.update(cx, |item, cx| {
6696            cx.focus_self();
6697            item.is_dirty = true;
6698        });
6699        cx.deactivate_window();
6700        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6701
6702        // Autosave after delay.
6703        item.update(cx, |item, cx| {
6704            SettingsStore::update_global(cx, |settings, cx| {
6705                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6706                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6707                })
6708            });
6709            item.is_dirty = true;
6710            cx.emit(ItemEvent::Edit);
6711        });
6712
6713        // Delay hasn't fully expired, so the file is still dirty and unsaved.
6714        cx.executor().advance_clock(Duration::from_millis(250));
6715        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6716
6717        // After delay expires, the file is saved.
6718        cx.executor().advance_clock(Duration::from_millis(250));
6719        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6720
6721        // Autosave on focus change, ensuring closing the tab counts as such.
6722        item.update(cx, |item, cx| {
6723            SettingsStore::update_global(cx, |settings, cx| {
6724                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6725                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6726                })
6727            });
6728            item.is_dirty = true;
6729            for project_item in &mut item.project_items {
6730                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6731            }
6732        });
6733
6734        pane.update(cx, |pane, cx| {
6735            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6736        })
6737        .await
6738        .unwrap();
6739        assert!(!cx.has_pending_prompt());
6740        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6741
6742        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6743        workspace.update(cx, |workspace, cx| {
6744            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6745        });
6746        item.update(cx, |item, cx| {
6747            item.project_items[0].update(cx, |item, _| {
6748                item.entry_id = None;
6749            });
6750            item.is_dirty = true;
6751            cx.blur();
6752        });
6753        cx.run_until_parked();
6754        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6755
6756        // Ensure autosave is prevented for deleted files also when closing the buffer.
6757        let _close_items = pane.update(cx, |pane, cx| {
6758            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6759        });
6760        cx.run_until_parked();
6761        assert!(cx.has_pending_prompt());
6762        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6763    }
6764
6765    #[gpui::test]
6766    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6767        init_test(cx);
6768
6769        let fs = FakeFs::new(cx.executor());
6770
6771        let project = Project::test(fs, [], cx).await;
6772        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6773
6774        let item = cx.new_view(|cx| {
6775            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6776        });
6777        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6778        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6779        let toolbar_notify_count = Rc::new(RefCell::new(0));
6780
6781        workspace.update(cx, |workspace, cx| {
6782            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6783            let toolbar_notification_count = toolbar_notify_count.clone();
6784            cx.observe(&toolbar, move |_, _, _| {
6785                *toolbar_notification_count.borrow_mut() += 1
6786            })
6787            .detach();
6788        });
6789
6790        pane.update(cx, |pane, _| {
6791            assert!(!pane.can_navigate_backward());
6792            assert!(!pane.can_navigate_forward());
6793        });
6794
6795        item.update(cx, |item, cx| {
6796            item.set_state("one".to_string(), cx);
6797        });
6798
6799        // Toolbar must be notified to re-render the navigation buttons
6800        assert_eq!(*toolbar_notify_count.borrow(), 1);
6801
6802        pane.update(cx, |pane, _| {
6803            assert!(pane.can_navigate_backward());
6804            assert!(!pane.can_navigate_forward());
6805        });
6806
6807        workspace
6808            .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6809            .await
6810            .unwrap();
6811
6812        assert_eq!(*toolbar_notify_count.borrow(), 2);
6813        pane.update(cx, |pane, _| {
6814            assert!(!pane.can_navigate_backward());
6815            assert!(pane.can_navigate_forward());
6816        });
6817    }
6818
6819    #[gpui::test]
6820    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6821        init_test(cx);
6822        let fs = FakeFs::new(cx.executor());
6823
6824        let project = Project::test(fs, [], cx).await;
6825        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6826
6827        let panel = workspace.update(cx, |workspace, cx| {
6828            let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6829            workspace.add_panel(panel.clone(), cx);
6830
6831            workspace
6832                .right_dock()
6833                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6834
6835            panel
6836        });
6837
6838        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6839        pane.update(cx, |pane, cx| {
6840            let item = cx.new_view(TestItem::new);
6841            pane.add_item(Box::new(item), true, true, None, cx);
6842        });
6843
6844        // Transfer focus from center to panel
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        // Transfer focus from panel to center
6856        workspace.update(cx, |workspace, cx| {
6857            workspace.toggle_panel_focus::<TestPanel>(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        // Close 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        // Open the dock
6878        workspace.update(cx, |workspace, cx| {
6879            workspace.toggle_dock(DockPosition::Right, cx);
6880        });
6881
6882        workspace.update(cx, |workspace, cx| {
6883            assert!(workspace.right_dock().read(cx).is_open());
6884            assert!(!panel.is_zoomed(cx));
6885            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6886        });
6887
6888        // Focus and zoom panel
6889        panel.update(cx, |panel, cx| {
6890            cx.focus_self();
6891            panel.set_zoomed(true, 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        // Transfer focus to the center closes the dock
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        // Transferring focus back to the panel keeps it zoomed
6912        workspace.update(cx, |workspace, cx| {
6913            workspace.toggle_panel_focus::<TestPanel>(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!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6920        });
6921
6922        // Close the dock while it is zoomed
6923        workspace.update(cx, |workspace, cx| {
6924            workspace.toggle_dock(DockPosition::Right, cx)
6925        });
6926
6927        workspace.update(cx, |workspace, cx| {
6928            assert!(!workspace.right_dock().read(cx).is_open());
6929            assert!(panel.is_zoomed(cx));
6930            assert!(workspace.zoomed.is_none());
6931            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6932        });
6933
6934        // Opening the dock, when it's zoomed, retains focus
6935        workspace.update(cx, |workspace, cx| {
6936            workspace.toggle_dock(DockPosition::Right, cx)
6937        });
6938
6939        workspace.update(cx, |workspace, cx| {
6940            assert!(workspace.right_dock().read(cx).is_open());
6941            assert!(panel.is_zoomed(cx));
6942            assert!(workspace.zoomed.is_some());
6943            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6944        });
6945
6946        // Unzoom and close the panel, zoom the active pane.
6947        panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
6948        workspace.update(cx, |workspace, cx| {
6949            workspace.toggle_dock(DockPosition::Right, cx)
6950        });
6951        pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
6952
6953        // Opening a dock unzooms the pane.
6954        workspace.update(cx, |workspace, cx| {
6955            workspace.toggle_dock(DockPosition::Right, cx)
6956        });
6957        workspace.update(cx, |workspace, cx| {
6958            let pane = pane.read(cx);
6959            assert!(!pane.is_zoomed());
6960            assert!(!pane.focus_handle(cx).is_focused(cx));
6961            assert!(workspace.right_dock().read(cx).is_open());
6962            assert!(workspace.zoomed.is_none());
6963        });
6964    }
6965
6966    #[gpui::test]
6967    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
6968        init_test(cx);
6969
6970        let fs = FakeFs::new(cx.executor());
6971
6972        let project = Project::test(fs, None, cx).await;
6973        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6974
6975        // Let's arrange the panes like this:
6976        //
6977        // +-----------------------+
6978        // |         top           |
6979        // +------+--------+-------+
6980        // | left | center | right |
6981        // +------+--------+-------+
6982        // |        bottom         |
6983        // +-----------------------+
6984
6985        let top_item = cx.new_view(|cx| {
6986            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
6987        });
6988        let bottom_item = cx.new_view(|cx| {
6989            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
6990        });
6991        let left_item = cx.new_view(|cx| {
6992            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
6993        });
6994        let right_item = cx.new_view(|cx| {
6995            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
6996        });
6997        let center_item = cx.new_view(|cx| {
6998            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
6999        });
7000
7001        let top_pane_id = workspace.update(cx, |workspace, cx| {
7002            let top_pane_id = workspace.active_pane().entity_id();
7003            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
7004            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
7005            top_pane_id
7006        });
7007        let bottom_pane_id = workspace.update(cx, |workspace, cx| {
7008            let bottom_pane_id = workspace.active_pane().entity_id();
7009            workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
7010            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
7011            bottom_pane_id
7012        });
7013        let left_pane_id = workspace.update(cx, |workspace, cx| {
7014            let left_pane_id = workspace.active_pane().entity_id();
7015            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
7016            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7017            left_pane_id
7018        });
7019        let right_pane_id = workspace.update(cx, |workspace, cx| {
7020            let right_pane_id = workspace.active_pane().entity_id();
7021            workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
7022            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
7023            right_pane_id
7024        });
7025        let center_pane_id = workspace.update(cx, |workspace, cx| {
7026            let center_pane_id = workspace.active_pane().entity_id();
7027            workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
7028            center_pane_id
7029        });
7030        cx.executor().run_until_parked();
7031
7032        workspace.update(cx, |workspace, cx| {
7033            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7034
7035            // Join into next from center pane into right
7036            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7037        });
7038
7039        workspace.update(cx, |workspace, cx| {
7040            let active_pane = workspace.active_pane();
7041            assert_eq!(right_pane_id, active_pane.entity_id());
7042            assert_eq!(2, active_pane.read(cx).items_len());
7043            let item_ids_in_pane =
7044                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7045            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7046            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7047
7048            // Join into next from right pane into bottom
7049            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7050        });
7051
7052        workspace.update(cx, |workspace, cx| {
7053            let active_pane = workspace.active_pane();
7054            assert_eq!(bottom_pane_id, active_pane.entity_id());
7055            assert_eq!(3, active_pane.read(cx).items_len());
7056            let item_ids_in_pane =
7057                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7058            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7059            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7060            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7061
7062            // Join into next from bottom pane into left
7063            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7064        });
7065
7066        workspace.update(cx, |workspace, cx| {
7067            let active_pane = workspace.active_pane();
7068            assert_eq!(left_pane_id, active_pane.entity_id());
7069            assert_eq!(4, active_pane.read(cx).items_len());
7070            let item_ids_in_pane =
7071                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7072            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7073            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7074            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7075            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7076
7077            // Join into next from left pane into top
7078            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7079        });
7080
7081        workspace.update(cx, |workspace, cx| {
7082            let active_pane = workspace.active_pane();
7083            assert_eq!(top_pane_id, active_pane.entity_id());
7084            assert_eq!(5, active_pane.read(cx).items_len());
7085            let item_ids_in_pane =
7086                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7087            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7088            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7089            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7090            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7091            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7092
7093            // Single pane left: no-op
7094            workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7095        });
7096
7097        workspace.update(cx, |workspace, _cx| {
7098            let active_pane = workspace.active_pane();
7099            assert_eq!(top_pane_id, active_pane.entity_id());
7100        });
7101    }
7102
7103    fn add_an_item_to_active_pane(
7104        cx: &mut VisualTestContext,
7105        workspace: &View<Workspace>,
7106        item_id: u64,
7107    ) -> View<TestItem> {
7108        let item = cx.new_view(|cx| {
7109            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7110                item_id,
7111                "item{item_id}.txt",
7112                cx,
7113            )])
7114        });
7115        workspace.update(cx, |workspace, cx| {
7116            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7117        });
7118        return item;
7119    }
7120
7121    fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7122        return workspace.update(cx, |workspace, cx| {
7123            let new_pane =
7124                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7125            new_pane
7126        });
7127    }
7128
7129    #[gpui::test]
7130    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7131        init_test(cx);
7132        let fs = FakeFs::new(cx.executor());
7133        let project = Project::test(fs, None, cx).await;
7134        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7135
7136        add_an_item_to_active_pane(cx, &workspace, 1);
7137        split_pane(cx, &workspace);
7138        add_an_item_to_active_pane(cx, &workspace, 2);
7139        split_pane(cx, &workspace); // empty pane
7140        split_pane(cx, &workspace);
7141        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7142
7143        cx.executor().run_until_parked();
7144
7145        workspace.update(cx, |workspace, cx| {
7146            let num_panes = workspace.panes().len();
7147            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7148            let active_item = workspace
7149                .active_pane()
7150                .read(cx)
7151                .active_item()
7152                .expect("item is in focus");
7153
7154            assert_eq!(num_panes, 4);
7155            assert_eq!(num_items_in_current_pane, 1);
7156            assert_eq!(active_item.item_id(), last_item.item_id());
7157        });
7158
7159        workspace.update(cx, |workspace, cx| {
7160            workspace.join_all_panes(cx);
7161        });
7162
7163        workspace.update(cx, |workspace, cx| {
7164            let num_panes = workspace.panes().len();
7165            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7166            let active_item = workspace
7167                .active_pane()
7168                .read(cx)
7169                .active_item()
7170                .expect("item is in focus");
7171
7172            assert_eq!(num_panes, 1);
7173            assert_eq!(num_items_in_current_pane, 3);
7174            assert_eq!(active_item.item_id(), last_item.item_id());
7175        });
7176    }
7177    struct TestModal(FocusHandle);
7178
7179    impl TestModal {
7180        fn new(cx: &mut ViewContext<Self>) -> Self {
7181            Self(cx.focus_handle())
7182        }
7183    }
7184
7185    impl EventEmitter<DismissEvent> for TestModal {}
7186
7187    impl FocusableView for TestModal {
7188        fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7189            self.0.clone()
7190        }
7191    }
7192
7193    impl ModalView for TestModal {}
7194
7195    impl Render for TestModal {
7196        fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7197            div().track_focus(&self.0)
7198        }
7199    }
7200
7201    #[gpui::test]
7202    async fn test_panels(cx: &mut gpui::TestAppContext) {
7203        init_test(cx);
7204        let fs = FakeFs::new(cx.executor());
7205
7206        let project = Project::test(fs, [], cx).await;
7207        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7208
7209        let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7210            let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7211            workspace.add_panel(panel_1.clone(), cx);
7212            workspace
7213                .left_dock()
7214                .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
7215            let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7216            workspace.add_panel(panel_2.clone(), cx);
7217            workspace
7218                .right_dock()
7219                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
7220
7221            let left_dock = workspace.left_dock();
7222            assert_eq!(
7223                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7224                panel_1.panel_id()
7225            );
7226            assert_eq!(
7227                left_dock.read(cx).active_panel_size(cx).unwrap(),
7228                panel_1.size(cx)
7229            );
7230
7231            left_dock.update(cx, |left_dock, cx| {
7232                left_dock.resize_active_panel(Some(px(1337.)), cx)
7233            });
7234            assert_eq!(
7235                workspace
7236                    .right_dock()
7237                    .read(cx)
7238                    .visible_panel()
7239                    .unwrap()
7240                    .panel_id(),
7241                panel_2.panel_id(),
7242            );
7243
7244            (panel_1, panel_2)
7245        });
7246
7247        // Move panel_1 to the right
7248        panel_1.update(cx, |panel_1, cx| {
7249            panel_1.set_position(DockPosition::Right, cx)
7250        });
7251
7252        workspace.update(cx, |workspace, cx| {
7253            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7254            // Since it was the only panel on the left, the left dock should now be closed.
7255            assert!(!workspace.left_dock().read(cx).is_open());
7256            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7257            let right_dock = workspace.right_dock();
7258            assert_eq!(
7259                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7260                panel_1.panel_id()
7261            );
7262            assert_eq!(
7263                right_dock.read(cx).active_panel_size(cx).unwrap(),
7264                px(1337.)
7265            );
7266
7267            // Now we move panel_2 to the left
7268            panel_2.set_position(DockPosition::Left, cx);
7269        });
7270
7271        workspace.update(cx, |workspace, cx| {
7272            // Since panel_2 was not visible on the right, we don't open the left dock.
7273            assert!(!workspace.left_dock().read(cx).is_open());
7274            // And the right dock is unaffected in its displaying of panel_1
7275            assert!(workspace.right_dock().read(cx).is_open());
7276            assert_eq!(
7277                workspace
7278                    .right_dock()
7279                    .read(cx)
7280                    .visible_panel()
7281                    .unwrap()
7282                    .panel_id(),
7283                panel_1.panel_id(),
7284            );
7285        });
7286
7287        // Move panel_1 back to the left
7288        panel_1.update(cx, |panel_1, cx| {
7289            panel_1.set_position(DockPosition::Left, cx)
7290        });
7291
7292        workspace.update(cx, |workspace, cx| {
7293            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7294            let left_dock = workspace.left_dock();
7295            assert!(left_dock.read(cx).is_open());
7296            assert_eq!(
7297                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7298                panel_1.panel_id()
7299            );
7300            assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7301            // And the right dock should be closed as it no longer has any panels.
7302            assert!(!workspace.right_dock().read(cx).is_open());
7303
7304            // Now we move panel_1 to the bottom
7305            panel_1.set_position(DockPosition::Bottom, cx);
7306        });
7307
7308        workspace.update(cx, |workspace, cx| {
7309            // Since panel_1 was visible on the left, we close the left dock.
7310            assert!(!workspace.left_dock().read(cx).is_open());
7311            // The bottom dock is sized based on the panel's default size,
7312            // since the panel orientation changed from vertical to horizontal.
7313            let bottom_dock = workspace.bottom_dock();
7314            assert_eq!(
7315                bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7316                panel_1.size(cx),
7317            );
7318            // Close bottom dock and move panel_1 back to the left.
7319            bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7320            panel_1.set_position(DockPosition::Left, cx);
7321        });
7322
7323        // Emit activated event on panel 1
7324        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7325
7326        // Now the left dock is open and panel_1 is active and focused.
7327        workspace.update(cx, |workspace, cx| {
7328            let left_dock = workspace.left_dock();
7329            assert!(left_dock.read(cx).is_open());
7330            assert_eq!(
7331                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7332                panel_1.panel_id(),
7333            );
7334            assert!(panel_1.focus_handle(cx).is_focused(cx));
7335        });
7336
7337        // Emit closed event on panel 2, which is not active
7338        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7339
7340        // Wo don't close the left dock, because panel_2 wasn't the active panel
7341        workspace.update(cx, |workspace, cx| {
7342            let left_dock = workspace.left_dock();
7343            assert!(left_dock.read(cx).is_open());
7344            assert_eq!(
7345                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7346                panel_1.panel_id(),
7347            );
7348        });
7349
7350        // Emitting a ZoomIn event shows the panel as zoomed.
7351        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7352        workspace.update(cx, |workspace, _| {
7353            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7354            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7355        });
7356
7357        // Move panel to another dock while it is zoomed
7358        panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7359        workspace.update(cx, |workspace, _| {
7360            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7361
7362            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7363        });
7364
7365        // This is a helper for getting a:
7366        // - valid focus on an element,
7367        // - that isn't a part of the panes and panels system of the Workspace,
7368        // - and doesn't trigger the 'on_focus_lost' API.
7369        let focus_other_view = {
7370            let workspace = workspace.clone();
7371            move |cx: &mut VisualTestContext| {
7372                workspace.update(cx, |workspace, cx| {
7373                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7374                        workspace.toggle_modal(cx, TestModal::new);
7375                        workspace.toggle_modal(cx, TestModal::new);
7376                    } else {
7377                        workspace.toggle_modal(cx, TestModal::new);
7378                    }
7379                })
7380            }
7381        };
7382
7383        // If focus is transferred to another view that's not a panel or another pane, we still show
7384        // the panel as zoomed.
7385        focus_other_view(cx);
7386        workspace.update(cx, |workspace, _| {
7387            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7388            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7389        });
7390
7391        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7392        workspace.update(cx, |_, cx| cx.focus_self());
7393        workspace.update(cx, |workspace, _| {
7394            assert_eq!(workspace.zoomed, None);
7395            assert_eq!(workspace.zoomed_position, None);
7396        });
7397
7398        // If focus is transferred again to another view that's not a panel or a pane, we won't
7399        // show the panel as zoomed because it wasn't zoomed before.
7400        focus_other_view(cx);
7401        workspace.update(cx, |workspace, _| {
7402            assert_eq!(workspace.zoomed, None);
7403            assert_eq!(workspace.zoomed_position, None);
7404        });
7405
7406        // When the panel is activated, it is zoomed again.
7407        cx.dispatch_action(ToggleRightDock);
7408        workspace.update(cx, |workspace, _| {
7409            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7410            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7411        });
7412
7413        // Emitting a ZoomOut event unzooms the panel.
7414        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7415        workspace.update(cx, |workspace, _| {
7416            assert_eq!(workspace.zoomed, None);
7417            assert_eq!(workspace.zoomed_position, None);
7418        });
7419
7420        // Emit closed event on panel 1, which is active
7421        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7422
7423        // Now the left dock is closed, because panel_1 was the active panel
7424        workspace.update(cx, |workspace, cx| {
7425            let right_dock = workspace.right_dock();
7426            assert!(!right_dock.read(cx).is_open());
7427        });
7428    }
7429
7430    #[gpui::test]
7431    async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) {
7432        init_test(cx);
7433
7434        let fs = FakeFs::new(cx.background_executor.clone());
7435        let project = Project::test(fs, [], cx).await;
7436        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7437        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7438
7439        let dirty_regular_buffer = cx.new_view(|cx| {
7440            TestItem::new(cx)
7441                .with_dirty(true)
7442                .with_label("1.txt")
7443                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7444        });
7445        let dirty_regular_buffer_2 = cx.new_view(|cx| {
7446            TestItem::new(cx)
7447                .with_dirty(true)
7448                .with_label("2.txt")
7449                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7450        });
7451        let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7452            TestItem::new(cx)
7453                .with_dirty(true)
7454                .with_singleton(false)
7455                .with_label("Fake Project Search")
7456                .with_project_items(&[
7457                    dirty_regular_buffer.read(cx).project_items[0].clone(),
7458                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7459                ])
7460        });
7461        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7462        workspace.update(cx, |workspace, cx| {
7463            workspace.add_item(
7464                pane.clone(),
7465                Box::new(dirty_regular_buffer.clone()),
7466                None,
7467                false,
7468                false,
7469                cx,
7470            );
7471            workspace.add_item(
7472                pane.clone(),
7473                Box::new(dirty_regular_buffer_2.clone()),
7474                None,
7475                false,
7476                false,
7477                cx,
7478            );
7479            workspace.add_item(
7480                pane.clone(),
7481                Box::new(dirty_multi_buffer_with_both.clone()),
7482                None,
7483                false,
7484                false,
7485                cx,
7486            );
7487        });
7488
7489        pane.update(cx, |pane, cx| {
7490            pane.activate_item(2, true, true, cx);
7491            assert_eq!(
7492                pane.active_item().unwrap().item_id(),
7493                multi_buffer_with_both_files_id,
7494                "Should select the multi buffer in the pane"
7495            );
7496        });
7497        let close_all_but_multi_buffer_task = pane
7498            .update(cx, |pane, cx| {
7499                pane.close_inactive_items(
7500                    &CloseInactiveItems {
7501                        save_intent: Some(SaveIntent::Save),
7502                        close_pinned: true,
7503                    },
7504                    cx,
7505                )
7506            })
7507            .expect("should have inactive files to close");
7508        cx.background_executor.run_until_parked();
7509        assert!(
7510            !cx.has_pending_prompt(),
7511            "Multi buffer still has the unsaved buffer inside, so no save prompt should be shown"
7512        );
7513        close_all_but_multi_buffer_task
7514            .await
7515            .expect("Closing all buffers but the multi buffer failed");
7516        pane.update(cx, |pane, cx| {
7517            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7518            assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0);
7519            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7520            assert_eq!(pane.items_len(), 1);
7521            assert_eq!(
7522                pane.active_item().unwrap().item_id(),
7523                multi_buffer_with_both_files_id,
7524                "Should have only the multi buffer left in the pane"
7525            );
7526            assert!(
7527                dirty_multi_buffer_with_both.read(cx).is_dirty,
7528                "The multi buffer containing the unsaved buffer should still be dirty"
7529            );
7530        });
7531
7532        let close_multi_buffer_task = pane
7533            .update(cx, |pane, cx| {
7534                pane.close_active_item(
7535                    &CloseActiveItem {
7536                        save_intent: Some(SaveIntent::Close),
7537                    },
7538                    cx,
7539                )
7540            })
7541            .expect("should have the multi buffer to close");
7542        cx.background_executor.run_until_parked();
7543        assert!(
7544            cx.has_pending_prompt(),
7545            "Dirty multi buffer should prompt a save dialog"
7546        );
7547        cx.simulate_prompt_answer(0);
7548        cx.background_executor.run_until_parked();
7549        close_multi_buffer_task
7550            .await
7551            .expect("Closing the multi buffer failed");
7552        pane.update(cx, |pane, cx| {
7553            assert_eq!(
7554                dirty_multi_buffer_with_both.read(cx).save_count,
7555                1,
7556                "Multi buffer item should get be saved"
7557            );
7558            // Test impl does not save inner items, so we do not assert them
7559            assert_eq!(
7560                pane.items_len(),
7561                0,
7562                "No more items should be left in the pane"
7563            );
7564            assert!(pane.active_item().is_none());
7565        });
7566    }
7567
7568    #[gpui::test]
7569    async fn test_no_save_prompt_when_dirty_singleton_buffer_closed_with_a_multi_buffer_containing_it_present_in_the_pane(
7570        cx: &mut TestAppContext,
7571    ) {
7572        init_test(cx);
7573
7574        let fs = FakeFs::new(cx.background_executor.clone());
7575        let project = Project::test(fs, [], cx).await;
7576        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7577        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7578
7579        let dirty_regular_buffer = cx.new_view(|cx| {
7580            TestItem::new(cx)
7581                .with_dirty(true)
7582                .with_label("1.txt")
7583                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7584        });
7585        let dirty_regular_buffer_2 = cx.new_view(|cx| {
7586            TestItem::new(cx)
7587                .with_dirty(true)
7588                .with_label("2.txt")
7589                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7590        });
7591        let clear_regular_buffer = cx.new_view(|cx| {
7592            TestItem::new(cx)
7593                .with_label("3.txt")
7594                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7595        });
7596
7597        let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7598            TestItem::new(cx)
7599                .with_dirty(true)
7600                .with_singleton(false)
7601                .with_label("Fake Project Search")
7602                .with_project_items(&[
7603                    dirty_regular_buffer.read(cx).project_items[0].clone(),
7604                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7605                    clear_regular_buffer.read(cx).project_items[0].clone(),
7606                ])
7607        });
7608        workspace.update(cx, |workspace, cx| {
7609            workspace.add_item(
7610                pane.clone(),
7611                Box::new(dirty_regular_buffer.clone()),
7612                None,
7613                false,
7614                false,
7615                cx,
7616            );
7617            workspace.add_item(
7618                pane.clone(),
7619                Box::new(dirty_multi_buffer_with_both.clone()),
7620                None,
7621                false,
7622                false,
7623                cx,
7624            );
7625        });
7626
7627        pane.update(cx, |pane, cx| {
7628            pane.activate_item(0, true, true, cx);
7629            assert_eq!(
7630                pane.active_item().unwrap().item_id(),
7631                dirty_regular_buffer.item_id(),
7632                "Should select the dirty singleton buffer in the pane"
7633            );
7634        });
7635        let close_singleton_buffer_task = pane
7636            .update(cx, |pane, cx| {
7637                pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7638            })
7639            .expect("should have active singleton buffer to close");
7640        cx.background_executor.run_until_parked();
7641        assert!(
7642            !cx.has_pending_prompt(),
7643            "Multi buffer is still in the pane and has the unsaved buffer inside, so no save prompt should be shown"
7644        );
7645
7646        close_singleton_buffer_task
7647            .await
7648            .expect("Should not fail closing the singleton buffer");
7649        pane.update(cx, |pane, cx| {
7650            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7651            assert_eq!(
7652                dirty_multi_buffer_with_both.read(cx).save_count,
7653                0,
7654                "Multi buffer itself should not be saved"
7655            );
7656            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7657            assert_eq!(
7658                pane.items_len(),
7659                1,
7660                "A dirty multi buffer should be present in the pane"
7661            );
7662            assert_eq!(
7663                pane.active_item().unwrap().item_id(),
7664                dirty_multi_buffer_with_both.item_id(),
7665                "Should activate the only remaining item in the pane"
7666            );
7667        });
7668    }
7669
7670    #[gpui::test]
7671    async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane(
7672        cx: &mut TestAppContext,
7673    ) {
7674        init_test(cx);
7675
7676        let fs = FakeFs::new(cx.background_executor.clone());
7677        let project = Project::test(fs, [], cx).await;
7678        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7679        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7680
7681        let dirty_regular_buffer = cx.new_view(|cx| {
7682            TestItem::new(cx)
7683                .with_dirty(true)
7684                .with_label("1.txt")
7685                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7686        });
7687        let dirty_regular_buffer_2 = cx.new_view(|cx| {
7688            TestItem::new(cx)
7689                .with_dirty(true)
7690                .with_label("2.txt")
7691                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7692        });
7693        let clear_regular_buffer = cx.new_view(|cx| {
7694            TestItem::new(cx)
7695                .with_label("3.txt")
7696                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7697        });
7698
7699        let dirty_multi_buffer_with_both = cx.new_view(|cx| {
7700            TestItem::new(cx)
7701                .with_dirty(true)
7702                .with_singleton(false)
7703                .with_label("Fake Project Search")
7704                .with_project_items(&[
7705                    dirty_regular_buffer.read(cx).project_items[0].clone(),
7706                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7707                    clear_regular_buffer.read(cx).project_items[0].clone(),
7708                ])
7709        });
7710        let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id();
7711        workspace.update(cx, |workspace, cx| {
7712            workspace.add_item(
7713                pane.clone(),
7714                Box::new(dirty_regular_buffer.clone()),
7715                None,
7716                false,
7717                false,
7718                cx,
7719            );
7720            workspace.add_item(
7721                pane.clone(),
7722                Box::new(dirty_multi_buffer_with_both.clone()),
7723                None,
7724                false,
7725                false,
7726                cx,
7727            );
7728        });
7729
7730        pane.update(cx, |pane, cx| {
7731            pane.activate_item(1, true, true, cx);
7732            assert_eq!(
7733                pane.active_item().unwrap().item_id(),
7734                multi_buffer_with_both_files_id,
7735                "Should select the multi buffer in the pane"
7736            );
7737        });
7738        let _close_multi_buffer_task = pane
7739            .update(cx, |pane, cx| {
7740                pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7741            })
7742            .expect("should have active multi buffer to close");
7743        cx.background_executor.run_until_parked();
7744        assert!(
7745            cx.has_pending_prompt(),
7746            "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown"
7747        );
7748    }
7749
7750    #[gpui::test]
7751    async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane(
7752        cx: &mut TestAppContext,
7753    ) {
7754        init_test(cx);
7755
7756        let fs = FakeFs::new(cx.background_executor.clone());
7757        let project = Project::test(fs, [], cx).await;
7758        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7759        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
7760
7761        let dirty_regular_buffer = cx.new_view(|cx| {
7762            TestItem::new(cx)
7763                .with_dirty(true)
7764                .with_label("1.txt")
7765                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
7766        });
7767        let dirty_regular_buffer_2 = cx.new_view(|cx| {
7768            TestItem::new(cx)
7769                .with_dirty(true)
7770                .with_label("2.txt")
7771                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
7772        });
7773        let clear_regular_buffer = cx.new_view(|cx| {
7774            TestItem::new(cx)
7775                .with_label("3.txt")
7776                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
7777        });
7778
7779        let dirty_multi_buffer = cx.new_view(|cx| {
7780            TestItem::new(cx)
7781                .with_dirty(true)
7782                .with_singleton(false)
7783                .with_label("Fake Project Search")
7784                .with_project_items(&[
7785                    dirty_regular_buffer.read(cx).project_items[0].clone(),
7786                    dirty_regular_buffer_2.read(cx).project_items[0].clone(),
7787                    clear_regular_buffer.read(cx).project_items[0].clone(),
7788                ])
7789        });
7790        workspace.update(cx, |workspace, cx| {
7791            workspace.add_item(
7792                pane.clone(),
7793                Box::new(dirty_regular_buffer.clone()),
7794                None,
7795                false,
7796                false,
7797                cx,
7798            );
7799            workspace.add_item(
7800                pane.clone(),
7801                Box::new(dirty_regular_buffer_2.clone()),
7802                None,
7803                false,
7804                false,
7805                cx,
7806            );
7807            workspace.add_item(
7808                pane.clone(),
7809                Box::new(dirty_multi_buffer.clone()),
7810                None,
7811                false,
7812                false,
7813                cx,
7814            );
7815        });
7816
7817        pane.update(cx, |pane, cx| {
7818            pane.activate_item(2, true, true, cx);
7819            assert_eq!(
7820                pane.active_item().unwrap().item_id(),
7821                dirty_multi_buffer.item_id(),
7822                "Should select the multi buffer in the pane"
7823            );
7824        });
7825        let close_multi_buffer_task = pane
7826            .update(cx, |pane, cx| {
7827                pane.close_active_item(&CloseActiveItem { save_intent: None }, cx)
7828            })
7829            .expect("should have active multi buffer to close");
7830        cx.background_executor.run_until_parked();
7831        assert!(
7832            !cx.has_pending_prompt(),
7833            "All dirty items from the multi buffer are in the pane still, no save prompts should be shown"
7834        );
7835        close_multi_buffer_task
7836            .await
7837            .expect("Closing multi buffer failed");
7838        pane.update(cx, |pane, cx| {
7839            assert_eq!(dirty_regular_buffer.read(cx).save_count, 0);
7840            assert_eq!(dirty_multi_buffer.read(cx).save_count, 0);
7841            assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0);
7842            assert_eq!(
7843                pane.items()
7844                    .map(|item| item.item_id())
7845                    .sorted()
7846                    .collect::<Vec<_>>(),
7847                vec![
7848                    dirty_regular_buffer.item_id(),
7849                    dirty_regular_buffer_2.item_id(),
7850                ],
7851                "Should have no multi buffer left in the pane"
7852            );
7853            assert!(dirty_regular_buffer.read(cx).is_dirty);
7854            assert!(dirty_regular_buffer_2.read(cx).is_dirty);
7855        });
7856    }
7857
7858    mod register_project_item_tests {
7859        use ui::Context as _;
7860
7861        use super::*;
7862
7863        // View
7864        struct TestPngItemView {
7865            focus_handle: FocusHandle,
7866        }
7867        // Model
7868        struct TestPngItem {}
7869
7870        impl project::ProjectItem for TestPngItem {
7871            fn try_open(
7872                _project: &Model<Project>,
7873                path: &ProjectPath,
7874                cx: &mut AppContext,
7875            ) -> Option<Task<gpui::Result<Model<Self>>>> {
7876                if path.path.extension().unwrap() == "png" {
7877                    Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
7878                } else {
7879                    None
7880                }
7881            }
7882
7883            fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7884                None
7885            }
7886
7887            fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7888                None
7889            }
7890
7891            fn is_dirty(&self) -> bool {
7892                false
7893            }
7894        }
7895
7896        impl Item for TestPngItemView {
7897            type Event = ();
7898        }
7899        impl EventEmitter<()> for TestPngItemView {}
7900        impl FocusableView for TestPngItemView {
7901            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7902                self.focus_handle.clone()
7903            }
7904        }
7905
7906        impl Render for TestPngItemView {
7907            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7908                Empty
7909            }
7910        }
7911
7912        impl ProjectItem for TestPngItemView {
7913            type Item = TestPngItem;
7914
7915            fn for_project_item(
7916                _project: Model<Project>,
7917                _item: Model<Self::Item>,
7918                cx: &mut ViewContext<Self>,
7919            ) -> Self
7920            where
7921                Self: Sized,
7922            {
7923                Self {
7924                    focus_handle: cx.focus_handle(),
7925                }
7926            }
7927        }
7928
7929        // View
7930        struct TestIpynbItemView {
7931            focus_handle: FocusHandle,
7932        }
7933        // Model
7934        struct TestIpynbItem {}
7935
7936        impl project::ProjectItem for TestIpynbItem {
7937            fn try_open(
7938                _project: &Model<Project>,
7939                path: &ProjectPath,
7940                cx: &mut AppContext,
7941            ) -> Option<Task<gpui::Result<Model<Self>>>> {
7942                if path.path.extension().unwrap() == "ipynb" {
7943                    Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
7944                } else {
7945                    None
7946                }
7947            }
7948
7949            fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7950                None
7951            }
7952
7953            fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7954                None
7955            }
7956
7957            fn is_dirty(&self) -> bool {
7958                false
7959            }
7960        }
7961
7962        impl Item for TestIpynbItemView {
7963            type Event = ();
7964        }
7965        impl EventEmitter<()> for TestIpynbItemView {}
7966        impl FocusableView for TestIpynbItemView {
7967            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7968                self.focus_handle.clone()
7969            }
7970        }
7971
7972        impl Render for TestIpynbItemView {
7973            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7974                Empty
7975            }
7976        }
7977
7978        impl ProjectItem for TestIpynbItemView {
7979            type Item = TestIpynbItem;
7980
7981            fn for_project_item(
7982                _project: Model<Project>,
7983                _item: Model<Self::Item>,
7984                cx: &mut ViewContext<Self>,
7985            ) -> Self
7986            where
7987                Self: Sized,
7988            {
7989                Self {
7990                    focus_handle: cx.focus_handle(),
7991                }
7992            }
7993        }
7994
7995        struct TestAlternatePngItemView {
7996            focus_handle: FocusHandle,
7997        }
7998
7999        impl Item for TestAlternatePngItemView {
8000            type Event = ();
8001        }
8002
8003        impl EventEmitter<()> for TestAlternatePngItemView {}
8004        impl FocusableView for TestAlternatePngItemView {
8005            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
8006                self.focus_handle.clone()
8007            }
8008        }
8009
8010        impl Render for TestAlternatePngItemView {
8011            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
8012                Empty
8013            }
8014        }
8015
8016        impl ProjectItem for TestAlternatePngItemView {
8017            type Item = TestPngItem;
8018
8019            fn for_project_item(
8020                _project: Model<Project>,
8021                _item: Model<Self::Item>,
8022                cx: &mut ViewContext<Self>,
8023            ) -> Self
8024            where
8025                Self: Sized,
8026            {
8027                Self {
8028                    focus_handle: cx.focus_handle(),
8029                }
8030            }
8031        }
8032
8033        #[gpui::test]
8034        async fn test_register_project_item(cx: &mut TestAppContext) {
8035            init_test(cx);
8036
8037            cx.update(|cx| {
8038                register_project_item::<TestPngItemView>(cx);
8039                register_project_item::<TestIpynbItemView>(cx);
8040            });
8041
8042            let fs = FakeFs::new(cx.executor());
8043            fs.insert_tree(
8044                "/root1",
8045                json!({
8046                    "one.png": "BINARYDATAHERE",
8047                    "two.ipynb": "{ totally a notebook }",
8048                    "three.txt": "editing text, sure why not?"
8049                }),
8050            )
8051            .await;
8052
8053            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8054            let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8055
8056            let worktree_id = project.update(cx, |project, cx| {
8057                project.worktrees(cx).next().unwrap().read(cx).id()
8058            });
8059
8060            let handle = workspace
8061                .update(cx, |workspace, cx| {
8062                    let project_path = (worktree_id, "one.png");
8063                    workspace.open_path(project_path, None, true, cx)
8064                })
8065                .await
8066                .unwrap();
8067
8068            // Now we can check if the handle we got back errored or not
8069            assert_eq!(
8070                handle.to_any().entity_type(),
8071                TypeId::of::<TestPngItemView>()
8072            );
8073
8074            let handle = workspace
8075                .update(cx, |workspace, cx| {
8076                    let project_path = (worktree_id, "two.ipynb");
8077                    workspace.open_path(project_path, None, true, cx)
8078                })
8079                .await
8080                .unwrap();
8081
8082            assert_eq!(
8083                handle.to_any().entity_type(),
8084                TypeId::of::<TestIpynbItemView>()
8085            );
8086
8087            let handle = workspace
8088                .update(cx, |workspace, cx| {
8089                    let project_path = (worktree_id, "three.txt");
8090                    workspace.open_path(project_path, None, true, cx)
8091                })
8092                .await;
8093            assert!(handle.is_err());
8094        }
8095
8096        #[gpui::test]
8097        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
8098            init_test(cx);
8099
8100            cx.update(|cx| {
8101                register_project_item::<TestPngItemView>(cx);
8102                register_project_item::<TestAlternatePngItemView>(cx);
8103            });
8104
8105            let fs = FakeFs::new(cx.executor());
8106            fs.insert_tree(
8107                "/root1",
8108                json!({
8109                    "one.png": "BINARYDATAHERE",
8110                    "two.ipynb": "{ totally a notebook }",
8111                    "three.txt": "editing text, sure why not?"
8112                }),
8113            )
8114            .await;
8115
8116            let project = Project::test(fs, ["root1".as_ref()], cx).await;
8117            let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
8118
8119            let worktree_id = project.update(cx, |project, cx| {
8120                project.worktrees(cx).next().unwrap().read(cx).id()
8121            });
8122
8123            let handle = workspace
8124                .update(cx, |workspace, cx| {
8125                    let project_path = (worktree_id, "one.png");
8126                    workspace.open_path(project_path, None, true, cx)
8127                })
8128                .await
8129                .unwrap();
8130
8131            // This _must_ be the second item registered
8132            assert_eq!(
8133                handle.to_any().entity_type(),
8134                TypeId::of::<TestAlternatePngItemView>()
8135            );
8136
8137            let handle = workspace
8138                .update(cx, |workspace, cx| {
8139                    let project_path = (worktree_id, "three.txt");
8140                    workspace.open_path(project_path, None, true, cx)
8141                })
8142                .await;
8143            assert!(handle.is_err());
8144        }
8145    }
8146
8147    pub fn init_test(cx: &mut TestAppContext) {
8148        cx.update(|cx| {
8149            let settings_store = SettingsStore::test(cx);
8150            cx.set_global(settings_store);
8151            theme::init(theme::LoadThemes::JustBase, cx);
8152            language::init(cx);
8153            crate::init_settings(cx);
8154            Project::init_settings(cx);
8155        });
8156    }
8157
8158    fn dirty_project_item(id: u64, path: &str, cx: &mut AppContext) -> Model<TestProjectItem> {
8159        let item = TestProjectItem::new(id, path, cx);
8160        item.update(cx, |item, _| {
8161            item.is_dirty = true;
8162        });
8163        item
8164    }
8165}