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