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