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