workspace.rs

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