workspace.rs

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