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