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