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