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