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