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 let Some(active_panel) = dock.active_panel() {
2301                if was_visible {
2302                    if active_panel.focus_handle(cx).contains_focused(cx) {
2303                        focus_center = true;
2304                    }
2305                } else {
2306                    let focus_handle = &active_panel.focus_handle(cx);
2307                    cx.focus(focus_handle);
2308                    reveal_dock = true;
2309                }
2310            }
2311        });
2312
2313        if reveal_dock {
2314            self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
2315        }
2316
2317        if focus_center {
2318            self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2319        }
2320
2321        cx.notify();
2322        self.serialize_workspace(cx);
2323    }
2324
2325    pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
2326        let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
2327
2328        for dock in docks {
2329            dock.update(cx, |dock, cx| {
2330                dock.set_open(false, cx);
2331            });
2332        }
2333
2334        cx.focus_self();
2335        cx.notify();
2336        self.serialize_workspace(cx);
2337    }
2338
2339    /// Transfer focus to the panel of the given type.
2340    pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<View<T>> {
2341        let panel = self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?;
2342        panel.to_any().downcast().ok()
2343    }
2344
2345    /// Focus the panel of the given type if it isn't already focused. If it is
2346    /// already focused, then transfer focus back to the workspace center.
2347    pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2348        self.focus_or_unfocus_panel::<T>(cx, |panel, cx| {
2349            !panel.focus_handle(cx).contains_focused(cx)
2350        });
2351    }
2352
2353    pub fn activate_panel_for_proto_id(
2354        &mut self,
2355        panel_id: PanelId,
2356        cx: &mut ViewContext<Self>,
2357    ) -> Option<Arc<dyn PanelHandle>> {
2358        let mut panel = None;
2359        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2360            if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) {
2361                panel = dock.update(cx, |dock, cx| {
2362                    dock.activate_panel(panel_index, cx);
2363                    dock.set_open(true, cx);
2364                    dock.active_panel().cloned()
2365                });
2366                break;
2367            }
2368        }
2369
2370        if panel.is_some() {
2371            cx.notify();
2372            self.serialize_workspace(cx);
2373        }
2374
2375        panel
2376    }
2377
2378    /// Focus or unfocus the given panel type, depending on the given callback.
2379    fn focus_or_unfocus_panel<T: Panel>(
2380        &mut self,
2381        cx: &mut ViewContext<Self>,
2382        should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
2383    ) -> Option<Arc<dyn PanelHandle>> {
2384        let mut result_panel = None;
2385        let mut serialize = false;
2386        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2387            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2388                let mut focus_center = false;
2389                let panel = dock.update(cx, |dock, cx| {
2390                    dock.activate_panel(panel_index, cx);
2391
2392                    let panel = dock.active_panel().cloned();
2393                    if let Some(panel) = panel.as_ref() {
2394                        if should_focus(&**panel, cx) {
2395                            dock.set_open(true, cx);
2396                            panel.focus_handle(cx).focus(cx);
2397                        } else {
2398                            focus_center = true;
2399                        }
2400                    }
2401                    panel
2402                });
2403
2404                if focus_center {
2405                    self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2406                }
2407
2408                result_panel = panel;
2409                serialize = true;
2410                break;
2411            }
2412        }
2413
2414        if serialize {
2415            self.serialize_workspace(cx);
2416        }
2417
2418        cx.notify();
2419        result_panel
2420    }
2421
2422    /// Open the panel of the given type
2423    pub fn open_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
2424        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
2425            if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
2426                dock.update(cx, |dock, cx| {
2427                    dock.activate_panel(panel_index, cx);
2428                    dock.set_open(true, cx);
2429                });
2430            }
2431        }
2432    }
2433
2434    pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
2435        [&self.left_dock, &self.bottom_dock, &self.right_dock]
2436            .iter()
2437            .find_map(|dock| dock.read(cx).panel::<T>())
2438    }
2439
2440    fn dismiss_zoomed_items_to_reveal(
2441        &mut self,
2442        dock_to_reveal: Option<DockPosition>,
2443        cx: &mut ViewContext<Self>,
2444    ) {
2445        // If a center pane is zoomed, unzoom it.
2446        for pane in &self.panes {
2447            if pane != &self.active_pane || dock_to_reveal.is_some() {
2448                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
2449            }
2450        }
2451
2452        // If another dock is zoomed, hide it.
2453        let mut focus_center = false;
2454        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
2455            dock.update(cx, |dock, cx| {
2456                if Some(dock.position()) != dock_to_reveal {
2457                    if let Some(panel) = dock.active_panel() {
2458                        if panel.is_zoomed(cx) {
2459                            focus_center |= panel.focus_handle(cx).contains_focused(cx);
2460                            dock.set_open(false, cx);
2461                        }
2462                    }
2463                }
2464            });
2465        }
2466
2467        if focus_center {
2468            self.active_pane.update(cx, |pane, cx| pane.focus(cx))
2469        }
2470
2471        if self.zoomed_position != dock_to_reveal {
2472            self.zoomed = None;
2473            self.zoomed_position = None;
2474            cx.emit(Event::ZoomChanged);
2475        }
2476
2477        cx.notify();
2478    }
2479
2480    fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
2481        let pane = cx.new_view(|cx| {
2482            let mut pane = Pane::new(
2483                self.weak_handle(),
2484                self.project.clone(),
2485                self.pane_history_timestamp.clone(),
2486                None,
2487                NewFile.boxed_clone(),
2488                cx,
2489            );
2490            pane.set_can_split(Some(Arc::new(|_, _, _| true)));
2491            pane
2492        });
2493        cx.subscribe(&pane, Self::handle_pane_event).detach();
2494        self.panes.push(pane.clone());
2495        cx.focus_view(&pane);
2496        cx.emit(Event::PaneAdded(pane.clone()));
2497        pane
2498    }
2499
2500    pub fn add_item_to_center(
2501        &mut self,
2502        item: Box<dyn ItemHandle>,
2503        cx: &mut ViewContext<Self>,
2504    ) -> bool {
2505        if let Some(center_pane) = self.last_active_center_pane.clone() {
2506            if let Some(center_pane) = center_pane.upgrade() {
2507                center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
2508                true
2509            } else {
2510                false
2511            }
2512        } else {
2513            false
2514        }
2515    }
2516
2517    pub fn add_item_to_active_pane(
2518        &mut self,
2519        item: Box<dyn ItemHandle>,
2520        destination_index: Option<usize>,
2521        focus_item: bool,
2522        cx: &mut WindowContext,
2523    ) {
2524        self.add_item(
2525            self.active_pane.clone(),
2526            item,
2527            destination_index,
2528            false,
2529            focus_item,
2530            cx,
2531        )
2532    }
2533
2534    pub fn add_item(
2535        &mut self,
2536        pane: View<Pane>,
2537        item: Box<dyn ItemHandle>,
2538        destination_index: Option<usize>,
2539        activate_pane: bool,
2540        focus_item: bool,
2541        cx: &mut WindowContext,
2542    ) {
2543        if let Some(text) = item.telemetry_event_text(cx) {
2544            self.client()
2545                .telemetry()
2546                .report_app_event(format!("{}: open", text));
2547        }
2548
2549        pane.update(cx, |pane, cx| {
2550            pane.add_item(item, activate_pane, focus_item, destination_index, cx)
2551        });
2552    }
2553
2554    pub fn split_item(
2555        &mut self,
2556        split_direction: SplitDirection,
2557        item: Box<dyn ItemHandle>,
2558        cx: &mut ViewContext<Self>,
2559    ) {
2560        let new_pane = self.split_pane(self.active_pane.clone(), split_direction, cx);
2561        self.add_item(new_pane, item, None, true, true, cx);
2562    }
2563
2564    pub fn open_abs_path(
2565        &mut self,
2566        abs_path: PathBuf,
2567        visible: bool,
2568        cx: &mut ViewContext<Self>,
2569    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2570        cx.spawn(|workspace, mut cx| async move {
2571            let open_paths_task_result = workspace
2572                .update(&mut cx, |workspace, cx| {
2573                    workspace.open_paths(
2574                        vec![abs_path.clone()],
2575                        if visible {
2576                            OpenVisible::All
2577                        } else {
2578                            OpenVisible::None
2579                        },
2580                        None,
2581                        cx,
2582                    )
2583                })
2584                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
2585                .await;
2586            anyhow::ensure!(
2587                open_paths_task_result.len() == 1,
2588                "open abs path {abs_path:?} task returned incorrect number of results"
2589            );
2590            match open_paths_task_result
2591                .into_iter()
2592                .next()
2593                .expect("ensured single task result")
2594            {
2595                Some(open_result) => {
2596                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
2597                }
2598                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
2599            }
2600        })
2601    }
2602
2603    pub fn split_abs_path(
2604        &mut self,
2605        abs_path: PathBuf,
2606        visible: bool,
2607        cx: &mut ViewContext<Self>,
2608    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
2609        let project_path_task =
2610            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
2611        cx.spawn(|this, mut cx| async move {
2612            let (_, path) = project_path_task.await?;
2613            this.update(&mut cx, |this, cx| this.split_path(path, cx))?
2614                .await
2615        })
2616    }
2617
2618    pub fn open_path(
2619        &mut self,
2620        path: impl Into<ProjectPath>,
2621        pane: Option<WeakView<Pane>>,
2622        focus_item: bool,
2623        cx: &mut WindowContext,
2624    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2625        self.open_path_preview(path, pane, focus_item, false, cx)
2626    }
2627
2628    pub fn open_path_preview(
2629        &mut self,
2630        path: impl Into<ProjectPath>,
2631        pane: Option<WeakView<Pane>>,
2632        focus_item: bool,
2633        allow_preview: bool,
2634        cx: &mut WindowContext,
2635    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2636        let pane = pane.unwrap_or_else(|| {
2637            self.last_active_center_pane.clone().unwrap_or_else(|| {
2638                self.panes
2639                    .first()
2640                    .expect("There must be an active pane")
2641                    .downgrade()
2642            })
2643        });
2644
2645        let task = self.load_path(path.into(), cx);
2646        cx.spawn(move |mut cx| async move {
2647            let (project_entry_id, build_item) = task.await?;
2648            pane.update(&mut cx, |pane, cx| {
2649                pane.open_item(
2650                    project_entry_id,
2651                    focus_item,
2652                    allow_preview,
2653                    None,
2654                    cx,
2655                    build_item,
2656                )
2657            })
2658        })
2659    }
2660
2661    pub fn split_path(
2662        &mut self,
2663        path: impl Into<ProjectPath>,
2664        cx: &mut ViewContext<Self>,
2665    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2666        self.split_path_preview(path, false, None, cx)
2667    }
2668
2669    pub fn split_path_preview(
2670        &mut self,
2671        path: impl Into<ProjectPath>,
2672        allow_preview: bool,
2673        split_direction: Option<SplitDirection>,
2674        cx: &mut ViewContext<Self>,
2675    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
2676        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
2677            self.panes
2678                .first()
2679                .expect("There must be an active pane")
2680                .downgrade()
2681        });
2682
2683        if let Member::Pane(center_pane) = &self.center.root {
2684            if center_pane.read(cx).items_len() == 0 {
2685                return self.open_path(path, Some(pane), true, cx);
2686            }
2687        }
2688
2689        let task = self.load_path(path.into(), cx);
2690        cx.spawn(|this, mut cx| async move {
2691            let (project_entry_id, build_item) = task.await?;
2692            this.update(&mut cx, move |this, cx| -> Option<_> {
2693                let pane = pane.upgrade()?;
2694                let new_pane =
2695                    this.split_pane(pane, split_direction.unwrap_or(SplitDirection::Right), cx);
2696                new_pane.update(cx, |new_pane, cx| {
2697                    Some(new_pane.open_item(
2698                        project_entry_id,
2699                        true,
2700                        allow_preview,
2701                        None,
2702                        cx,
2703                        build_item,
2704                    ))
2705                })
2706            })
2707            .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
2708        })
2709    }
2710
2711    fn load_path(
2712        &mut self,
2713        path: ProjectPath,
2714        cx: &mut WindowContext,
2715    ) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
2716        let project = self.project().clone();
2717        let project_item_builders = cx.default_global::<ProjectItemOpeners>().clone();
2718        let Some(open_project_item) = project_item_builders
2719            .iter()
2720            .rev()
2721            .find_map(|open_project_item| open_project_item(&project, &path, cx))
2722        else {
2723            return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
2724        };
2725        open_project_item
2726    }
2727
2728    pub fn find_project_item<T>(
2729        &self,
2730        pane: &View<Pane>,
2731        project_item: &Model<T::Item>,
2732        cx: &AppContext,
2733    ) -> Option<View<T>>
2734    where
2735        T: ProjectItem,
2736    {
2737        use project::ProjectItem as _;
2738        let project_item = project_item.read(cx);
2739        let entry_id = project_item.entry_id(cx);
2740        let project_path = project_item.project_path(cx);
2741
2742        let mut item = None;
2743        if let Some(entry_id) = entry_id {
2744            item = pane.read(cx).item_for_entry(entry_id, cx);
2745        }
2746        if item.is_none() {
2747            if let Some(project_path) = project_path {
2748                item = pane.read(cx).item_for_path(project_path, cx);
2749            }
2750        }
2751
2752        item.and_then(|item| item.downcast::<T>())
2753    }
2754
2755    pub fn is_project_item_open<T>(
2756        &self,
2757        pane: &View<Pane>,
2758        project_item: &Model<T::Item>,
2759        cx: &AppContext,
2760    ) -> bool
2761    where
2762        T: ProjectItem,
2763    {
2764        self.find_project_item::<T>(pane, project_item, cx)
2765            .is_some()
2766    }
2767
2768    pub fn open_project_item<T>(
2769        &mut self,
2770        pane: View<Pane>,
2771        project_item: Model<T::Item>,
2772        activate_pane: bool,
2773        focus_item: bool,
2774        cx: &mut ViewContext<Self>,
2775    ) -> View<T>
2776    where
2777        T: ProjectItem,
2778    {
2779        if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
2780            self.activate_item(&item, activate_pane, focus_item, cx);
2781            return item;
2782        }
2783
2784        let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx));
2785        let item_id = item.item_id();
2786        let mut destination_index = None;
2787        pane.update(cx, |pane, cx| {
2788            if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
2789                if let Some(preview_item_id) = pane.preview_item_id() {
2790                    if preview_item_id != item_id {
2791                        destination_index = pane.close_current_preview_item(cx);
2792                    }
2793                }
2794            }
2795            pane.set_preview_item_id(Some(item.item_id()), cx)
2796        });
2797
2798        self.add_item(
2799            pane,
2800            Box::new(item.clone()),
2801            destination_index,
2802            activate_pane,
2803            focus_item,
2804            cx,
2805        );
2806        item
2807    }
2808
2809    pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
2810        if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
2811            self.active_pane.update(cx, |pane, cx| {
2812                pane.add_item(Box::new(shared_screen), false, true, None, cx)
2813            });
2814        }
2815    }
2816
2817    pub fn activate_item(
2818        &mut self,
2819        item: &dyn ItemHandle,
2820        activate_pane: bool,
2821        focus_item: bool,
2822        cx: &mut WindowContext,
2823    ) -> bool {
2824        let result = self.panes.iter().find_map(|pane| {
2825            pane.read(cx)
2826                .index_for_item(item)
2827                .map(|ix| (pane.clone(), ix))
2828        });
2829        if let Some((pane, ix)) = result {
2830            pane.update(cx, |pane, cx| {
2831                pane.activate_item(ix, activate_pane, focus_item, cx)
2832            });
2833            true
2834        } else {
2835            false
2836        }
2837    }
2838
2839    fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
2840        let panes = self.center.panes();
2841        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
2842            cx.focus_view(&pane);
2843        } else {
2844            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
2845        }
2846    }
2847
2848    fn move_item_to_pane_at_index(&mut self, action: &MoveItemToPane, cx: &mut ViewContext<Self>) {
2849        let Some(&target_pane) = self.center.panes().get(action.destination) else {
2850            return;
2851        };
2852        move_active_item(&self.active_pane, target_pane, action.focus, true, cx);
2853    }
2854
2855    pub fn activate_next_pane(&mut self, cx: &mut WindowContext) {
2856        let panes = self.center.panes();
2857        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2858            let next_ix = (ix + 1) % panes.len();
2859            let next_pane = panes[next_ix].clone();
2860            cx.focus_view(&next_pane);
2861        }
2862    }
2863
2864    pub fn activate_previous_pane(&mut self, cx: &mut WindowContext) {
2865        let panes = self.center.panes();
2866        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
2867            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
2868            let prev_pane = panes[prev_ix].clone();
2869            cx.focus_view(&prev_pane);
2870        }
2871    }
2872
2873    pub fn activate_pane_in_direction(
2874        &mut self,
2875        direction: SplitDirection,
2876        cx: &mut WindowContext,
2877    ) {
2878        use ActivateInDirectionTarget as Target;
2879        enum Origin {
2880            LeftDock,
2881            RightDock,
2882            BottomDock,
2883            Center,
2884        }
2885
2886        let origin: Origin = [
2887            (&self.left_dock, Origin::LeftDock),
2888            (&self.right_dock, Origin::RightDock),
2889            (&self.bottom_dock, Origin::BottomDock),
2890        ]
2891        .into_iter()
2892        .find_map(|(dock, origin)| {
2893            if dock.focus_handle(cx).contains_focused(cx) && dock.read(cx).is_open() {
2894                Some(origin)
2895            } else {
2896                None
2897            }
2898        })
2899        .unwrap_or(Origin::Center);
2900
2901        let get_last_active_pane = || {
2902            let pane = self
2903                .last_active_center_pane
2904                .clone()
2905                .unwrap_or_else(|| {
2906                    self.panes
2907                        .first()
2908                        .expect("There must be an active pane")
2909                        .downgrade()
2910                })
2911                .upgrade()?;
2912            (pane.read(cx).items_len() != 0).then_some(pane)
2913        };
2914
2915        let try_dock =
2916            |dock: &View<Dock>| dock.read(cx).is_open().then(|| Target::Dock(dock.clone()));
2917
2918        let target = match (origin, direction) {
2919            // We're in the center, so we first try to go to a different pane,
2920            // otherwise try to go to a dock.
2921            (Origin::Center, direction) => {
2922                if let Some(pane) = self.find_pane_in_direction(direction, cx) {
2923                    Some(Target::Pane(pane))
2924                } else {
2925                    match direction {
2926                        SplitDirection::Up => None,
2927                        SplitDirection::Down => try_dock(&self.bottom_dock),
2928                        SplitDirection::Left => try_dock(&self.left_dock),
2929                        SplitDirection::Right => try_dock(&self.right_dock),
2930                    }
2931                }
2932            }
2933
2934            (Origin::LeftDock, SplitDirection::Right) => {
2935                if let Some(last_active_pane) = get_last_active_pane() {
2936                    Some(Target::Pane(last_active_pane))
2937                } else {
2938                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock))
2939                }
2940            }
2941
2942            (Origin::LeftDock, SplitDirection::Down)
2943            | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock),
2944
2945            (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane),
2946            (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock),
2947            (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock),
2948
2949            (Origin::RightDock, SplitDirection::Left) => {
2950                if let Some(last_active_pane) = get_last_active_pane() {
2951                    Some(Target::Pane(last_active_pane))
2952                } else {
2953                    try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock))
2954                }
2955            }
2956
2957            _ => None,
2958        };
2959
2960        match target {
2961            Some(ActivateInDirectionTarget::Pane(pane)) => cx.focus_view(&pane),
2962            Some(ActivateInDirectionTarget::Dock(dock)) => {
2963                dock.update(cx, |dock, cx| {
2964                    if let Some(panel) = dock.active_panel() {
2965                        panel.focus_handle(cx).focus(cx);
2966                    } else {
2967                        log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position());
2968                    }
2969                });
2970            }
2971            None => {}
2972        }
2973    }
2974
2975    pub fn move_item_to_pane_in_direction(
2976        &mut self,
2977        action: &MoveItemToPaneInDirection,
2978        cx: &mut WindowContext,
2979    ) {
2980        if let Some(destination) = self.find_pane_in_direction(action.direction, cx) {
2981            move_active_item(&self.active_pane, &destination, action.focus, true, cx);
2982        }
2983    }
2984
2985    pub fn bounding_box_for_pane(&self, pane: &View<Pane>) -> Option<Bounds<Pixels>> {
2986        self.center.bounding_box_for_pane(pane)
2987    }
2988
2989    pub fn find_pane_in_direction(
2990        &mut self,
2991        direction: SplitDirection,
2992        cx: &WindowContext,
2993    ) -> Option<View<Pane>> {
2994        self.center
2995            .find_pane_in_direction(&self.active_pane, direction, cx)
2996            .cloned()
2997    }
2998
2999    pub fn swap_pane_in_direction(
3000        &mut self,
3001        direction: SplitDirection,
3002        cx: &mut ViewContext<Self>,
3003    ) {
3004        if let Some(to) = self.find_pane_in_direction(direction, cx) {
3005            self.center.swap(&self.active_pane, &to);
3006            cx.notify();
3007        }
3008    }
3009
3010    pub fn resize_pane(&mut self, axis: gpui::Axis, amount: Pixels, cx: &mut ViewContext<Self>) {
3011        self.center
3012            .resize(&self.active_pane, axis, amount, &self.bounds);
3013        cx.notify();
3014    }
3015
3016    pub fn reset_pane_sizes(&mut self, cx: &mut ViewContext<Self>) {
3017        self.center.reset_pane_sizes();
3018        cx.notify();
3019    }
3020
3021    fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3022        // This is explicitly hoisted out of the following check for pane identity as
3023        // terminal panel panes are not registered as a center panes.
3024        self.status_bar.update(cx, |status_bar, cx| {
3025            status_bar.set_active_pane(&pane, cx);
3026        });
3027        if self.active_pane != pane {
3028            self.set_active_pane(&pane, cx);
3029        }
3030
3031        if self.last_active_center_pane.is_none() {
3032            self.last_active_center_pane = Some(pane.downgrade());
3033        }
3034
3035        self.dismiss_zoomed_items_to_reveal(None, cx);
3036        if pane.read(cx).is_zoomed() {
3037            self.zoomed = Some(pane.downgrade().into());
3038        } else {
3039            self.zoomed = None;
3040        }
3041        self.zoomed_position = None;
3042        cx.emit(Event::ZoomChanged);
3043        self.update_active_view_for_followers(cx);
3044        pane.model.update(cx, |pane, _| {
3045            pane.track_alternate_file_items();
3046        });
3047
3048        cx.notify();
3049    }
3050
3051    fn set_active_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) {
3052        self.active_pane = pane.clone();
3053        self.active_item_path_changed(cx);
3054        self.last_active_center_pane = Some(pane.downgrade());
3055    }
3056
3057    fn handle_panel_focused(&mut self, cx: &mut ViewContext<Self>) {
3058        self.update_active_view_for_followers(cx);
3059    }
3060
3061    fn handle_pane_event(
3062        &mut self,
3063        pane: View<Pane>,
3064        event: &pane::Event,
3065        cx: &mut ViewContext<Self>,
3066    ) {
3067        match event {
3068            pane::Event::AddItem { item } => {
3069                item.added_to_pane(self, pane, cx);
3070                cx.emit(Event::ItemAdded {
3071                    item: item.boxed_clone(),
3072                });
3073            }
3074            pane::Event::Split(direction) => {
3075                self.split_and_clone(pane, *direction, cx);
3076            }
3077            pane::Event::JoinIntoNext => self.join_pane_into_next(pane, cx),
3078            pane::Event::JoinAll => self.join_all_panes(cx),
3079            pane::Event::Remove { focus_on_pane } => {
3080                self.remove_pane(pane, focus_on_pane.clone(), cx)
3081            }
3082            pane::Event::ActivateItem { local } => {
3083                cx.on_next_frame(|_, cx| {
3084                    cx.invalidate_character_coordinates();
3085                });
3086
3087                pane.model.update(cx, |pane, _| {
3088                    pane.track_alternate_file_items();
3089                });
3090                if *local {
3091                    self.unfollow_in_pane(&pane, cx);
3092                }
3093                if &pane == self.active_pane() {
3094                    self.active_item_path_changed(cx);
3095                    self.update_active_view_for_followers(cx);
3096                }
3097            }
3098            pane::Event::UserSavedItem { item, save_intent } => cx.emit(Event::UserSavedItem {
3099                pane: pane.downgrade(),
3100                item: item.boxed_clone(),
3101                save_intent: *save_intent,
3102            }),
3103            pane::Event::ChangeItemTitle => {
3104                if pane == self.active_pane {
3105                    self.active_item_path_changed(cx);
3106                }
3107                self.update_window_edited(cx);
3108            }
3109            pane::Event::RemoveItem { .. } => {}
3110            pane::Event::RemovedItem { item_id } => {
3111                cx.emit(Event::ActiveItemChanged);
3112                self.update_window_edited(cx);
3113                if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(*item_id) {
3114                    if entry.get().entity_id() == pane.entity_id() {
3115                        entry.remove();
3116                    }
3117                }
3118            }
3119            pane::Event::Focus => {
3120                cx.on_next_frame(|_, cx| {
3121                    cx.invalidate_character_coordinates();
3122                });
3123                self.handle_pane_focused(pane.clone(), cx);
3124            }
3125            pane::Event::ZoomIn => {
3126                if pane == self.active_pane {
3127                    pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
3128                    if pane.read(cx).has_focus(cx) {
3129                        self.zoomed = Some(pane.downgrade().into());
3130                        self.zoomed_position = None;
3131                        cx.emit(Event::ZoomChanged);
3132                    }
3133                    cx.notify();
3134                }
3135            }
3136            pane::Event::ZoomOut => {
3137                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
3138                if self.zoomed_position.is_none() {
3139                    self.zoomed = None;
3140                    cx.emit(Event::ZoomChanged);
3141                }
3142                cx.notify();
3143            }
3144        }
3145
3146        self.serialize_workspace(cx);
3147    }
3148
3149    pub fn unfollow_in_pane(
3150        &mut self,
3151        pane: &View<Pane>,
3152        cx: &mut ViewContext<Workspace>,
3153    ) -> Option<PeerId> {
3154        let leader_id = self.leader_for_pane(pane)?;
3155        self.unfollow(leader_id, cx);
3156        Some(leader_id)
3157    }
3158
3159    pub fn split_pane(
3160        &mut self,
3161        pane_to_split: View<Pane>,
3162        split_direction: SplitDirection,
3163        cx: &mut ViewContext<Self>,
3164    ) -> View<Pane> {
3165        let new_pane = self.add_pane(cx);
3166        self.center
3167            .split(&pane_to_split, &new_pane, split_direction)
3168            .unwrap();
3169        cx.notify();
3170        new_pane
3171    }
3172
3173    pub fn split_and_clone(
3174        &mut self,
3175        pane: View<Pane>,
3176        direction: SplitDirection,
3177        cx: &mut ViewContext<Self>,
3178    ) -> Option<View<Pane>> {
3179        let item = pane.read(cx).active_item()?;
3180        let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) {
3181            let new_pane = self.add_pane(cx);
3182            new_pane.update(cx, |pane, cx| pane.add_item(clone, true, true, None, cx));
3183            self.center.split(&pane, &new_pane, direction).unwrap();
3184            Some(new_pane)
3185        } else {
3186            None
3187        };
3188        cx.notify();
3189        maybe_pane_handle
3190    }
3191
3192    pub fn split_pane_with_item(
3193        &mut self,
3194        pane_to_split: WeakView<Pane>,
3195        split_direction: SplitDirection,
3196        from: WeakView<Pane>,
3197        item_id_to_move: EntityId,
3198        cx: &mut ViewContext<Self>,
3199    ) {
3200        let Some(pane_to_split) = pane_to_split.upgrade() else {
3201            return;
3202        };
3203        let Some(from) = from.upgrade() else {
3204            return;
3205        };
3206
3207        let new_pane = self.add_pane(cx);
3208        move_item(&from, &new_pane, item_id_to_move, 0, cx);
3209        self.center
3210            .split(&pane_to_split, &new_pane, split_direction)
3211            .unwrap();
3212        cx.notify();
3213    }
3214
3215    pub fn split_pane_with_project_entry(
3216        &mut self,
3217        pane_to_split: WeakView<Pane>,
3218        split_direction: SplitDirection,
3219        project_entry: ProjectEntryId,
3220        cx: &mut ViewContext<Self>,
3221    ) -> Option<Task<Result<()>>> {
3222        let pane_to_split = pane_to_split.upgrade()?;
3223        let new_pane = self.add_pane(cx);
3224        self.center
3225            .split(&pane_to_split, &new_pane, split_direction)
3226            .unwrap();
3227
3228        let path = self.project.read(cx).path_for_entry(project_entry, cx)?;
3229        let task = self.open_path(path, Some(new_pane.downgrade()), true, cx);
3230        Some(cx.foreground_executor().spawn(async move {
3231            task.await?;
3232            Ok(())
3233        }))
3234    }
3235
3236    pub fn join_all_panes(&mut self, cx: &mut ViewContext<Self>) {
3237        let active_item = self.active_pane.read(cx).active_item();
3238        for pane in &self.panes {
3239            join_pane_into_active(&self.active_pane, pane, cx);
3240        }
3241        if let Some(active_item) = active_item {
3242            self.activate_item(active_item.as_ref(), true, true, cx);
3243        }
3244        cx.notify();
3245    }
3246
3247    pub fn join_pane_into_next(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
3248        let next_pane = self
3249            .find_pane_in_direction(SplitDirection::Right, cx)
3250            .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx))
3251            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3252            .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx));
3253        let Some(next_pane) = next_pane else {
3254            return;
3255        };
3256        move_all_items(&pane, &next_pane, cx);
3257        cx.notify();
3258    }
3259
3260    fn remove_pane(
3261        &mut self,
3262        pane: View<Pane>,
3263        focus_on: Option<View<Pane>>,
3264        cx: &mut ViewContext<Self>,
3265    ) {
3266        if self.center.remove(&pane).unwrap() {
3267            self.force_remove_pane(&pane, &focus_on, cx);
3268            self.unfollow_in_pane(&pane, cx);
3269            self.last_leaders_by_pane.remove(&pane.downgrade());
3270            for removed_item in pane.read(cx).items() {
3271                self.panes_by_item.remove(&removed_item.item_id());
3272            }
3273
3274            cx.notify();
3275        } else {
3276            self.active_item_path_changed(cx);
3277        }
3278        cx.emit(Event::PaneRemoved);
3279    }
3280
3281    pub fn panes(&self) -> &[View<Pane>] {
3282        &self.panes
3283    }
3284
3285    pub fn active_pane(&self) -> &View<Pane> {
3286        &self.active_pane
3287    }
3288
3289    pub fn focused_pane(&self, cx: &WindowContext) -> View<Pane> {
3290        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3291            if dock.focus_handle(cx).contains_focused(cx) {
3292                if let Some(pane) = dock
3293                    .read(cx)
3294                    .active_panel()
3295                    .and_then(|panel| panel.pane(cx))
3296                {
3297                    return pane;
3298                }
3299            }
3300        }
3301        self.active_pane().clone()
3302    }
3303
3304    pub fn adjacent_pane(&mut self, cx: &mut ViewContext<Self>) -> View<Pane> {
3305        self.find_pane_in_direction(SplitDirection::Right, cx)
3306            .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx))
3307            .unwrap_or_else(|| self.split_pane(self.active_pane.clone(), SplitDirection::Right, cx))
3308            .clone()
3309    }
3310
3311    pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
3312        let weak_pane = self.panes_by_item.get(&handle.item_id())?;
3313        weak_pane.upgrade()
3314    }
3315
3316    fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
3317        self.follower_states.retain(|leader_id, state| {
3318            if *leader_id == peer_id {
3319                for item in state.items_by_leader_view_id.values() {
3320                    item.view.set_leader_peer_id(None, cx);
3321                }
3322                false
3323            } else {
3324                true
3325            }
3326        });
3327        cx.notify();
3328    }
3329
3330    pub fn start_following(
3331        &mut self,
3332        leader_id: PeerId,
3333        cx: &mut ViewContext<Self>,
3334    ) -> Option<Task<Result<()>>> {
3335        let pane = self.active_pane().clone();
3336
3337        self.last_leaders_by_pane
3338            .insert(pane.downgrade(), leader_id);
3339        self.unfollow(leader_id, cx);
3340        self.unfollow_in_pane(&pane, cx);
3341        self.follower_states.insert(
3342            leader_id,
3343            FollowerState {
3344                center_pane: pane.clone(),
3345                dock_pane: None,
3346                active_view_id: None,
3347                items_by_leader_view_id: Default::default(),
3348            },
3349        );
3350        cx.notify();
3351
3352        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3353        let project_id = self.project.read(cx).remote_id();
3354        let request = self.app_state.client.request(proto::Follow {
3355            room_id,
3356            project_id,
3357            leader_id: Some(leader_id),
3358        });
3359
3360        Some(cx.spawn(|this, mut cx| async move {
3361            let response = request.await?;
3362            this.update(&mut cx, |this, _| {
3363                let state = this
3364                    .follower_states
3365                    .get_mut(&leader_id)
3366                    .ok_or_else(|| anyhow!("following interrupted"))?;
3367                state.active_view_id = response
3368                    .active_view
3369                    .as_ref()
3370                    .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3371                Ok::<_, anyhow::Error>(())
3372            })??;
3373            if let Some(view) = response.active_view {
3374                Self::add_view_from_leader(this.clone(), leader_id, &view, &mut cx).await?;
3375            }
3376            this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
3377            Ok(())
3378        }))
3379    }
3380
3381    pub fn follow_next_collaborator(
3382        &mut self,
3383        _: &FollowNextCollaborator,
3384        cx: &mut ViewContext<Self>,
3385    ) {
3386        let collaborators = self.project.read(cx).collaborators();
3387        let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
3388            let mut collaborators = collaborators.keys().copied();
3389            for peer_id in collaborators.by_ref() {
3390                if peer_id == leader_id {
3391                    break;
3392                }
3393            }
3394            collaborators.next()
3395        } else if let Some(last_leader_id) =
3396            self.last_leaders_by_pane.get(&self.active_pane.downgrade())
3397        {
3398            if collaborators.contains_key(last_leader_id) {
3399                Some(*last_leader_id)
3400            } else {
3401                None
3402            }
3403        } else {
3404            None
3405        };
3406
3407        let pane = self.active_pane.clone();
3408        let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
3409        else {
3410            return;
3411        };
3412        if self.unfollow_in_pane(&pane, cx) == Some(leader_id) {
3413            return;
3414        }
3415        if let Some(task) = self.start_following(leader_id, cx) {
3416            task.detach_and_log_err(cx)
3417        }
3418    }
3419
3420    pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) {
3421        let Some(room) = ActiveCall::global(cx).read(cx).room() else {
3422            return;
3423        };
3424        let room = room.read(cx);
3425        let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
3426            return;
3427        };
3428
3429        let project = self.project.read(cx);
3430
3431        let other_project_id = match remote_participant.location {
3432            call::ParticipantLocation::External => None,
3433            call::ParticipantLocation::UnsharedProject => None,
3434            call::ParticipantLocation::SharedProject { project_id } => {
3435                if Some(project_id) == project.remote_id() {
3436                    None
3437                } else {
3438                    Some(project_id)
3439                }
3440            }
3441        };
3442
3443        // if they are active in another project, follow there.
3444        if let Some(project_id) = other_project_id {
3445            let app_state = self.app_state.clone();
3446            crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx)
3447                .detach_and_log_err(cx);
3448        }
3449
3450        // if you're already following, find the right pane and focus it.
3451        if let Some(follower_state) = self.follower_states.get(&leader_id) {
3452            cx.focus_view(follower_state.pane());
3453            return;
3454        }
3455
3456        // Otherwise, follow.
3457        if let Some(task) = self.start_following(leader_id, cx) {
3458            task.detach_and_log_err(cx)
3459        }
3460    }
3461
3462    pub fn unfollow(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3463        cx.notify();
3464        let state = self.follower_states.remove(&leader_id)?;
3465        for (_, item) in state.items_by_leader_view_id {
3466            item.view.set_leader_peer_id(None, cx);
3467        }
3468
3469        let project_id = self.project.read(cx).remote_id();
3470        let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
3471        self.app_state
3472            .client
3473            .send(proto::Unfollow {
3474                room_id,
3475                project_id,
3476                leader_id: Some(leader_id),
3477            })
3478            .log_err();
3479
3480        Some(())
3481    }
3482
3483    pub fn is_being_followed(&self, peer_id: PeerId) -> bool {
3484        self.follower_states.contains_key(&peer_id)
3485    }
3486
3487    fn active_item_path_changed(&mut self, cx: &mut ViewContext<Self>) {
3488        cx.emit(Event::ActiveItemChanged);
3489        let active_entry = self.active_project_path(cx);
3490        self.project
3491            .update(cx, |project, cx| project.set_active_path(active_entry, cx));
3492
3493        self.update_window_title(cx);
3494    }
3495
3496    fn update_window_title(&mut self, cx: &mut WindowContext) {
3497        let project = self.project().read(cx);
3498        let mut title = String::new();
3499
3500        for (i, name) in project.worktree_root_names(cx).enumerate() {
3501            if i > 0 {
3502                title.push_str(", ");
3503            }
3504            title.push_str(name);
3505        }
3506
3507        if title.is_empty() {
3508            title = "empty project".to_string();
3509        }
3510
3511        if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) {
3512            let filename = path
3513                .path
3514                .file_name()
3515                .map(|s| s.to_string_lossy())
3516                .or_else(|| {
3517                    Some(Cow::Borrowed(
3518                        project
3519                            .worktree_for_id(path.worktree_id, cx)?
3520                            .read(cx)
3521                            .root_name(),
3522                    ))
3523                });
3524
3525            if let Some(filename) = filename {
3526                title.push_str("");
3527                title.push_str(filename.as_ref());
3528            }
3529        }
3530
3531        if project.is_via_collab() {
3532            title.push_str("");
3533        } else if project.is_shared() {
3534            title.push_str("");
3535        }
3536
3537        cx.set_window_title(&title);
3538    }
3539
3540    fn update_window_edited(&mut self, cx: &mut WindowContext) {
3541        let is_edited = !self.project.read(cx).is_disconnected(cx)
3542            && self
3543                .items(cx)
3544                .any(|item| item.has_conflict(cx) || item.is_dirty(cx));
3545        if is_edited != self.window_edited {
3546            self.window_edited = is_edited;
3547            cx.set_window_edited(self.window_edited)
3548        }
3549    }
3550
3551    fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
3552        if self.notifications.is_empty() {
3553            None
3554        } else {
3555            Some(
3556                div()
3557                    .absolute()
3558                    .right_3()
3559                    .bottom_3()
3560                    .w_112()
3561                    .h_full()
3562                    .flex()
3563                    .flex_col()
3564                    .justify_end()
3565                    .gap_2()
3566                    .children(
3567                        self.notifications
3568                            .iter()
3569                            .map(|(_, notification)| notification.to_any()),
3570                    ),
3571            )
3572        }
3573    }
3574
3575    // RPC handlers
3576
3577    fn active_view_for_follower(
3578        &self,
3579        follower_project_id: Option<u64>,
3580        cx: &mut ViewContext<Self>,
3581    ) -> Option<proto::View> {
3582        let (item, panel_id) = self.active_item_for_followers(cx);
3583        let item = item?;
3584        let leader_id = self
3585            .pane_for(&*item)
3586            .and_then(|pane| self.leader_for_pane(&pane));
3587
3588        let item_handle = item.to_followable_item_handle(cx)?;
3589        let id = item_handle.remote_id(&self.app_state.client, cx)?;
3590        let variant = item_handle.to_state_proto(cx)?;
3591
3592        if item_handle.is_project_item(cx)
3593            && (follower_project_id.is_none()
3594                || follower_project_id != self.project.read(cx).remote_id())
3595        {
3596            return None;
3597        }
3598
3599        Some(proto::View {
3600            id: Some(id.to_proto()),
3601            leader_id,
3602            variant: Some(variant),
3603            panel_id: panel_id.map(|id| id as i32),
3604        })
3605    }
3606
3607    fn handle_follow(
3608        &mut self,
3609        follower_project_id: Option<u64>,
3610        cx: &mut ViewContext<Self>,
3611    ) -> proto::FollowResponse {
3612        let active_view = self.active_view_for_follower(follower_project_id, cx);
3613
3614        cx.notify();
3615        proto::FollowResponse {
3616            // TODO: Remove after version 0.145.x stabilizes.
3617            active_view_id: active_view.as_ref().and_then(|view| view.id.clone()),
3618            views: active_view.iter().cloned().collect(),
3619            active_view,
3620        }
3621    }
3622
3623    fn handle_update_followers(
3624        &mut self,
3625        leader_id: PeerId,
3626        message: proto::UpdateFollowers,
3627        _cx: &mut ViewContext<Self>,
3628    ) {
3629        self.leader_updates_tx
3630            .unbounded_send((leader_id, message))
3631            .ok();
3632    }
3633
3634    async fn process_leader_update(
3635        this: &WeakView<Self>,
3636        leader_id: PeerId,
3637        update: proto::UpdateFollowers,
3638        cx: &mut AsyncWindowContext,
3639    ) -> Result<()> {
3640        match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
3641            proto::update_followers::Variant::CreateView(view) => {
3642                let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?;
3643                let should_add_view = this.update(cx, |this, _| {
3644                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3645                        anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id))
3646                    } else {
3647                        anyhow::Ok(false)
3648                    }
3649                })??;
3650
3651                if should_add_view {
3652                    Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3653                }
3654            }
3655            proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
3656                let should_add_view = this.update(cx, |this, _| {
3657                    if let Some(state) = this.follower_states.get_mut(&leader_id) {
3658                        state.active_view_id = update_active_view
3659                            .view
3660                            .as_ref()
3661                            .and_then(|view| ViewId::from_proto(view.id.clone()?).ok());
3662
3663                        if state.active_view_id.is_some_and(|view_id| {
3664                            !state.items_by_leader_view_id.contains_key(&view_id)
3665                        }) {
3666                            anyhow::Ok(true)
3667                        } else {
3668                            anyhow::Ok(false)
3669                        }
3670                    } else {
3671                        anyhow::Ok(false)
3672                    }
3673                })??;
3674
3675                if should_add_view {
3676                    if let Some(view) = update_active_view.view {
3677                        Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await?
3678                    }
3679                }
3680            }
3681            proto::update_followers::Variant::UpdateView(update_view) => {
3682                let variant = update_view
3683                    .variant
3684                    .ok_or_else(|| anyhow!("missing update view variant"))?;
3685                let id = update_view
3686                    .id
3687                    .ok_or_else(|| anyhow!("missing update view id"))?;
3688                let mut tasks = Vec::new();
3689                this.update(cx, |this, cx| {
3690                    let project = this.project.clone();
3691                    if let Some(state) = this.follower_states.get(&leader_id) {
3692                        let view_id = ViewId::from_proto(id.clone())?;
3693                        if let Some(item) = state.items_by_leader_view_id.get(&view_id) {
3694                            tasks.push(item.view.apply_update_proto(&project, variant.clone(), cx));
3695                        }
3696                    }
3697                    anyhow::Ok(())
3698                })??;
3699                try_join_all(tasks).await.log_err();
3700            }
3701        }
3702        this.update(cx, |this, cx| this.leader_updated(leader_id, cx))?;
3703        Ok(())
3704    }
3705
3706    async fn add_view_from_leader(
3707        this: WeakView<Self>,
3708        leader_id: PeerId,
3709        view: &proto::View,
3710        cx: &mut AsyncWindowContext,
3711    ) -> Result<()> {
3712        let this = this.upgrade().context("workspace dropped")?;
3713
3714        let Some(id) = view.id.clone() else {
3715            return Err(anyhow!("no id for view"));
3716        };
3717        let id = ViewId::from_proto(id)?;
3718        let panel_id = view.panel_id.and_then(proto::PanelId::from_i32);
3719
3720        let pane = this.update(cx, |this, _cx| {
3721            let state = this
3722                .follower_states
3723                .get(&leader_id)
3724                .context("stopped following")?;
3725            anyhow::Ok(state.pane().clone())
3726        })??;
3727        let existing_item = pane.update(cx, |pane, cx| {
3728            let client = this.read(cx).client().clone();
3729            pane.items().find_map(|item| {
3730                let item = item.to_followable_item_handle(cx)?;
3731                if item.remote_id(&client, cx) == Some(id) {
3732                    Some(item)
3733                } else {
3734                    None
3735                }
3736            })
3737        })?;
3738        let item = if let Some(existing_item) = existing_item {
3739            existing_item
3740        } else {
3741            let variant = view.variant.clone();
3742            if variant.is_none() {
3743                Err(anyhow!("missing view variant"))?;
3744            }
3745
3746            let task = cx.update(|cx| {
3747                FollowableViewRegistry::from_state_proto(this.clone(), id, variant, cx)
3748            })?;
3749
3750            let Some(task) = task else {
3751                return Err(anyhow!(
3752                    "failed to construct view from leader (maybe from a different version of zed?)"
3753                ));
3754            };
3755
3756            let mut new_item = task.await?;
3757            pane.update(cx, |pane, cx| {
3758                let mut item_to_remove = None;
3759                for (ix, item) in pane.items().enumerate() {
3760                    if let Some(item) = item.to_followable_item_handle(cx) {
3761                        match new_item.dedup(item.as_ref(), cx) {
3762                            Some(item::Dedup::KeepExisting) => {
3763                                new_item =
3764                                    item.boxed_clone().to_followable_item_handle(cx).unwrap();
3765                                break;
3766                            }
3767                            Some(item::Dedup::ReplaceExisting) => {
3768                                item_to_remove = Some((ix, item.item_id()));
3769                                break;
3770                            }
3771                            None => {}
3772                        }
3773                    }
3774                }
3775
3776                if let Some((ix, id)) = item_to_remove {
3777                    pane.remove_item(id, false, false, cx);
3778                    pane.add_item(new_item.boxed_clone(), false, false, Some(ix), cx);
3779                }
3780            })?;
3781
3782            new_item
3783        };
3784
3785        this.update(cx, |this, cx| {
3786            let state = this.follower_states.get_mut(&leader_id)?;
3787            item.set_leader_peer_id(Some(leader_id), cx);
3788            state.items_by_leader_view_id.insert(
3789                id,
3790                FollowerView {
3791                    view: item,
3792                    location: panel_id,
3793                },
3794            );
3795
3796            Some(())
3797        })?;
3798
3799        Ok(())
3800    }
3801
3802    pub fn update_active_view_for_followers(&mut self, cx: &mut WindowContext) {
3803        let mut is_project_item = true;
3804        let mut update = proto::UpdateActiveView::default();
3805        if cx.is_window_active() {
3806            let (active_item, panel_id) = self.active_item_for_followers(cx);
3807
3808            if let Some(item) = active_item {
3809                if item.focus_handle(cx).contains_focused(cx) {
3810                    let leader_id = self
3811                        .pane_for(&*item)
3812                        .and_then(|pane| self.leader_for_pane(&pane));
3813
3814                    if let Some(item) = item.to_followable_item_handle(cx) {
3815                        let id = item
3816                            .remote_id(&self.app_state.client, cx)
3817                            .map(|id| id.to_proto());
3818
3819                        if let Some(id) = id.clone() {
3820                            if let Some(variant) = item.to_state_proto(cx) {
3821                                let view = Some(proto::View {
3822                                    id: Some(id.clone()),
3823                                    leader_id,
3824                                    variant: Some(variant),
3825                                    panel_id: panel_id.map(|id| id as i32),
3826                                });
3827
3828                                is_project_item = item.is_project_item(cx);
3829                                update = proto::UpdateActiveView {
3830                                    view,
3831                                    // TODO: Remove after version 0.145.x stabilizes.
3832                                    id: Some(id.clone()),
3833                                    leader_id,
3834                                };
3835                            }
3836                        };
3837                    }
3838                }
3839            }
3840        }
3841
3842        let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref());
3843        if active_view_id != self.last_active_view_id.as_ref() {
3844            self.last_active_view_id = active_view_id.cloned();
3845            self.update_followers(
3846                is_project_item,
3847                proto::update_followers::Variant::UpdateActiveView(update),
3848                cx,
3849            );
3850        }
3851    }
3852
3853    fn active_item_for_followers(
3854        &self,
3855        cx: &mut WindowContext,
3856    ) -> (Option<Box<dyn ItemHandle>>, Option<proto::PanelId>) {
3857        let mut active_item = None;
3858        let mut panel_id = None;
3859        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
3860            if dock.focus_handle(cx).contains_focused(cx) {
3861                if let Some(panel) = dock.read(cx).active_panel() {
3862                    if let Some(pane) = panel.pane(cx) {
3863                        if let Some(item) = pane.read(cx).active_item() {
3864                            active_item = Some(item);
3865                            panel_id = panel.remote_id();
3866                            break;
3867                        }
3868                    }
3869                }
3870            }
3871        }
3872
3873        if active_item.is_none() {
3874            active_item = self.active_pane().read(cx).active_item();
3875        }
3876        (active_item, panel_id)
3877    }
3878
3879    fn update_followers(
3880        &self,
3881        project_only: bool,
3882        update: proto::update_followers::Variant,
3883        cx: &mut WindowContext,
3884    ) -> Option<()> {
3885        // If this update only applies to for followers in the current project,
3886        // then skip it unless this project is shared. If it applies to all
3887        // followers, regardless of project, then set `project_id` to none,
3888        // indicating that it goes to all followers.
3889        let project_id = if project_only {
3890            Some(self.project.read(cx).remote_id()?)
3891        } else {
3892            None
3893        };
3894        self.app_state().workspace_store.update(cx, |store, cx| {
3895            store.update_followers(project_id, update, cx)
3896        })
3897    }
3898
3899    pub fn leader_for_pane(&self, pane: &View<Pane>) -> Option<PeerId> {
3900        self.follower_states.iter().find_map(|(leader_id, state)| {
3901            if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) {
3902                Some(*leader_id)
3903            } else {
3904                None
3905            }
3906        })
3907    }
3908
3909    fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
3910        cx.notify();
3911
3912        let call = self.active_call()?;
3913        let room = call.read(cx).room()?.read(cx);
3914        let participant = room.remote_participant_for_peer_id(leader_id)?;
3915
3916        let leader_in_this_app;
3917        let leader_in_this_project;
3918        match participant.location {
3919            call::ParticipantLocation::SharedProject { project_id } => {
3920                leader_in_this_app = true;
3921                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
3922            }
3923            call::ParticipantLocation::UnsharedProject => {
3924                leader_in_this_app = true;
3925                leader_in_this_project = false;
3926            }
3927            call::ParticipantLocation::External => {
3928                leader_in_this_app = false;
3929                leader_in_this_project = false;
3930            }
3931        };
3932
3933        let state = self.follower_states.get(&leader_id)?;
3934        let mut item_to_activate = None;
3935        if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) {
3936            if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) {
3937                if leader_in_this_project || !item.view.is_project_item(cx) {
3938                    item_to_activate = Some((item.location, item.view.boxed_clone()));
3939                }
3940            }
3941        } else if let Some(shared_screen) =
3942            self.shared_screen_for_peer(leader_id, &state.center_pane, cx)
3943        {
3944            item_to_activate = Some((None, Box::new(shared_screen)));
3945        }
3946
3947        let (panel_id, item) = item_to_activate?;
3948
3949        let mut transfer_focus = state.center_pane.read(cx).has_focus(cx);
3950        let pane;
3951        if let Some(panel_id) = panel_id {
3952            pane = self.activate_panel_for_proto_id(panel_id, cx)?.pane(cx)?;
3953            let state = self.follower_states.get_mut(&leader_id)?;
3954            state.dock_pane = Some(pane.clone());
3955        } else {
3956            pane = state.center_pane.clone();
3957            let state = self.follower_states.get_mut(&leader_id)?;
3958            if let Some(dock_pane) = state.dock_pane.take() {
3959                transfer_focus |= dock_pane.focus_handle(cx).contains_focused(cx);
3960            }
3961        }
3962
3963        pane.update(cx, |pane, cx| {
3964            let focus_active_item = pane.has_focus(cx) || transfer_focus;
3965            if let Some(index) = pane.index_for_item(item.as_ref()) {
3966                pane.activate_item(index, false, false, cx);
3967            } else {
3968                pane.add_item(item.boxed_clone(), false, false, None, cx)
3969            }
3970
3971            if focus_active_item {
3972                pane.focus_active_item(cx)
3973            }
3974        });
3975
3976        None
3977    }
3978
3979    #[cfg(target_os = "windows")]
3980    fn shared_screen_for_peer(
3981        &self,
3982        _peer_id: PeerId,
3983        _pane: &View<Pane>,
3984        _cx: &mut WindowContext,
3985    ) -> Option<View<SharedScreen>> {
3986        None
3987    }
3988
3989    #[cfg(not(target_os = "windows"))]
3990    fn shared_screen_for_peer(
3991        &self,
3992        peer_id: PeerId,
3993        pane: &View<Pane>,
3994        cx: &mut WindowContext,
3995    ) -> Option<View<SharedScreen>> {
3996        let call = self.active_call()?;
3997        let room = call.read(cx).room()?.read(cx);
3998        let participant = room.remote_participant_for_peer_id(peer_id)?;
3999        let track = participant.video_tracks.values().next()?.clone();
4000        let user = participant.user.clone();
4001
4002        for item in pane.read(cx).items_of_type::<SharedScreen>() {
4003            if item.read(cx).peer_id == peer_id {
4004                return Some(item);
4005            }
4006        }
4007
4008        Some(cx.new_view(|cx| SharedScreen::new(track, peer_id, user.clone(), cx)))
4009    }
4010
4011    pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
4012        if cx.is_window_active() {
4013            self.update_active_view_for_followers(cx);
4014
4015            if let Some(database_id) = self.database_id {
4016                cx.background_executor()
4017                    .spawn(persistence::DB.update_timestamp(database_id))
4018                    .detach();
4019            }
4020        } else {
4021            for pane in &self.panes {
4022                pane.update(cx, |pane, cx| {
4023                    if let Some(item) = pane.active_item() {
4024                        item.workspace_deactivated(cx);
4025                    }
4026                    for item in pane.items() {
4027                        if matches!(
4028                            item.workspace_settings(cx).autosave,
4029                            AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange
4030                        ) {
4031                            Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
4032                                .detach_and_log_err(cx);
4033                        }
4034                    }
4035                });
4036            }
4037        }
4038    }
4039
4040    fn active_call(&self) -> Option<&Model<ActiveCall>> {
4041        self.active_call.as_ref().map(|(call, _)| call)
4042    }
4043
4044    fn on_active_call_event(
4045        &mut self,
4046        _: Model<ActiveCall>,
4047        event: &call::room::Event,
4048        cx: &mut ViewContext<Self>,
4049    ) {
4050        match event {
4051            call::room::Event::ParticipantLocationChanged { participant_id }
4052            | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
4053                self.leader_updated(*participant_id, cx);
4054            }
4055            _ => {}
4056        }
4057    }
4058
4059    pub fn database_id(&self) -> Option<WorkspaceId> {
4060        self.database_id
4061    }
4062
4063    fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
4064        let project = self.project().read(cx);
4065
4066        if project.is_local() {
4067            Some(
4068                project
4069                    .visible_worktrees(cx)
4070                    .map(|worktree| worktree.read(cx).abs_path())
4071                    .collect::<Vec<_>>(),
4072            )
4073        } else {
4074            None
4075        }
4076    }
4077
4078    fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
4079        match member {
4080            Member::Axis(PaneAxis { members, .. }) => {
4081                for child in members.iter() {
4082                    self.remove_panes(child.clone(), cx)
4083                }
4084            }
4085            Member::Pane(pane) => {
4086                self.force_remove_pane(&pane, &None, cx);
4087            }
4088        }
4089    }
4090
4091    fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
4092        self.session_id.take();
4093        self.serialize_workspace_internal(cx)
4094    }
4095
4096    fn force_remove_pane(
4097        &mut self,
4098        pane: &View<Pane>,
4099        focus_on: &Option<View<Pane>>,
4100        cx: &mut ViewContext<Workspace>,
4101    ) {
4102        self.panes.retain(|p| p != pane);
4103        if let Some(focus_on) = focus_on {
4104            focus_on.update(cx, |pane, cx| pane.focus(cx));
4105        } else {
4106            self.panes
4107                .last()
4108                .unwrap()
4109                .update(cx, |pane, cx| pane.focus(cx));
4110        }
4111        if self.last_active_center_pane == Some(pane.downgrade()) {
4112            self.last_active_center_pane = None;
4113        }
4114        cx.notify();
4115    }
4116
4117    fn serialize_workspace(&mut self, cx: &mut ViewContext<Self>) {
4118        if self._schedule_serialize.is_none() {
4119            self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move {
4120                cx.background_executor()
4121                    .timer(Duration::from_millis(100))
4122                    .await;
4123                this.update(&mut cx, |this, cx| {
4124                    this.serialize_workspace_internal(cx).detach();
4125                    this._schedule_serialize.take();
4126                })
4127                .log_err();
4128            }));
4129        }
4130    }
4131
4132    fn serialize_workspace_internal(&self, cx: &mut WindowContext) -> Task<()> {
4133        let Some(database_id) = self.database_id() else {
4134            return Task::ready(());
4135        };
4136
4137        fn serialize_pane_handle(pane_handle: &View<Pane>, cx: &WindowContext) -> SerializedPane {
4138            let (items, active, pinned_count) = {
4139                let pane = pane_handle.read(cx);
4140                let active_item_id = pane.active_item().map(|item| item.item_id());
4141                (
4142                    pane.items()
4143                        .filter_map(|handle| {
4144                            let handle = handle.to_serializable_item_handle(cx)?;
4145
4146                            Some(SerializedItem {
4147                                kind: Arc::from(handle.serialized_item_kind()),
4148                                item_id: handle.item_id().as_u64(),
4149                                active: Some(handle.item_id()) == active_item_id,
4150                                preview: pane.is_active_preview_item(handle.item_id()),
4151                            })
4152                        })
4153                        .collect::<Vec<_>>(),
4154                    pane.has_focus(cx),
4155                    pane.pinned_count(),
4156                )
4157            };
4158
4159            SerializedPane::new(items, active, pinned_count)
4160        }
4161
4162        fn build_serialized_pane_group(
4163            pane_group: &Member,
4164            cx: &WindowContext,
4165        ) -> SerializedPaneGroup {
4166            match pane_group {
4167                Member::Axis(PaneAxis {
4168                    axis,
4169                    members,
4170                    flexes,
4171                    bounding_boxes: _,
4172                }) => SerializedPaneGroup::Group {
4173                    axis: SerializedAxis(*axis),
4174                    children: members
4175                        .iter()
4176                        .map(|member| build_serialized_pane_group(member, cx))
4177                        .collect::<Vec<_>>(),
4178                    flexes: Some(flexes.lock().clone()),
4179                },
4180                Member::Pane(pane_handle) => {
4181                    SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, cx))
4182                }
4183            }
4184        }
4185
4186        fn build_serialized_docks(this: &Workspace, cx: &mut WindowContext) -> DockStructure {
4187            let left_dock = this.left_dock.read(cx);
4188            let left_visible = left_dock.is_open();
4189            let left_active_panel = left_dock
4190                .active_panel()
4191                .map(|panel| panel.persistent_name().to_string());
4192            let left_dock_zoom = left_dock
4193                .active_panel()
4194                .map(|panel| panel.is_zoomed(cx))
4195                .unwrap_or(false);
4196
4197            let right_dock = this.right_dock.read(cx);
4198            let right_visible = right_dock.is_open();
4199            let right_active_panel = right_dock
4200                .active_panel()
4201                .map(|panel| panel.persistent_name().to_string());
4202            let right_dock_zoom = right_dock
4203                .active_panel()
4204                .map(|panel| panel.is_zoomed(cx))
4205                .unwrap_or(false);
4206
4207            let bottom_dock = this.bottom_dock.read(cx);
4208            let bottom_visible = bottom_dock.is_open();
4209            let bottom_active_panel = bottom_dock
4210                .active_panel()
4211                .map(|panel| panel.persistent_name().to_string());
4212            let bottom_dock_zoom = bottom_dock
4213                .active_panel()
4214                .map(|panel| panel.is_zoomed(cx))
4215                .unwrap_or(false);
4216
4217            DockStructure {
4218                left: DockData {
4219                    visible: left_visible,
4220                    active_panel: left_active_panel,
4221                    zoom: left_dock_zoom,
4222                },
4223                right: DockData {
4224                    visible: right_visible,
4225                    active_panel: right_active_panel,
4226                    zoom: right_dock_zoom,
4227                },
4228                bottom: DockData {
4229                    visible: bottom_visible,
4230                    active_panel: bottom_active_panel,
4231                    zoom: bottom_dock_zoom,
4232                },
4233            }
4234        }
4235
4236        let location = if let Some(ssh_project) = &self.serialized_ssh_project {
4237            Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
4238        } else if let Some(local_paths) = self.local_paths(cx) {
4239            if !local_paths.is_empty() {
4240                Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
4241            } else {
4242                None
4243            }
4244        } else {
4245            None
4246        };
4247
4248        if let Some(location) = location {
4249            let center_group = build_serialized_pane_group(&self.center.root, cx);
4250            let docks = build_serialized_docks(self, cx);
4251            let window_bounds = Some(SerializedWindowBounds(cx.window_bounds()));
4252            let serialized_workspace = SerializedWorkspace {
4253                id: database_id,
4254                location,
4255                center_group,
4256                window_bounds,
4257                display: Default::default(),
4258                docks,
4259                centered_layout: self.centered_layout,
4260                session_id: self.session_id.clone(),
4261                window_id: Some(cx.window_handle().window_id().as_u64()),
4262            };
4263            return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
4264        }
4265        Task::ready(())
4266    }
4267
4268    async fn serialize_items(
4269        this: &WeakView<Self>,
4270        items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
4271        cx: &mut AsyncWindowContext,
4272    ) -> Result<()> {
4273        const CHUNK_SIZE: usize = 200;
4274        const THROTTLE_TIME: Duration = Duration::from_millis(200);
4275
4276        let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
4277
4278        while let Some(items_received) = serializable_items.next().await {
4279            let unique_items =
4280                items_received
4281                    .into_iter()
4282                    .fold(HashMap::default(), |mut acc, item| {
4283                        acc.entry(item.item_id()).or_insert(item);
4284                        acc
4285                    });
4286
4287            // We use into_iter() here so that the references to the items are moved into
4288            // the tasks and not kept alive while we're sleeping.
4289            for (_, item) in unique_items.into_iter() {
4290                if let Ok(Some(task)) =
4291                    this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
4292                {
4293                    cx.background_executor()
4294                        .spawn(async move { task.await.log_err() })
4295                        .detach();
4296                }
4297            }
4298
4299            cx.background_executor().timer(THROTTLE_TIME).await;
4300        }
4301
4302        Ok(())
4303    }
4304
4305    pub(crate) fn enqueue_item_serialization(
4306        &mut self,
4307        item: Box<dyn SerializableItemHandle>,
4308    ) -> Result<()> {
4309        self.serializable_items_tx
4310            .unbounded_send(item)
4311            .map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
4312    }
4313
4314    pub(crate) fn load_workspace(
4315        serialized_workspace: SerializedWorkspace,
4316        paths_to_open: Vec<Option<ProjectPath>>,
4317        cx: &mut ViewContext<Workspace>,
4318    ) -> Task<Result<Vec<Option<Box<dyn ItemHandle>>>>> {
4319        cx.spawn(|workspace, mut cx| async move {
4320            let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
4321
4322            let mut center_group = None;
4323            let mut center_items = None;
4324
4325            // Traverse the splits tree and add to things
4326            if let Some((group, active_pane, items)) = serialized_workspace
4327                .center_group
4328                .deserialize(
4329                    &project,
4330                    serialized_workspace.id,
4331                    workspace.clone(),
4332                    &mut cx,
4333                )
4334                .await
4335            {
4336                center_items = Some(items);
4337                center_group = Some((group, active_pane))
4338            }
4339
4340            let mut items_by_project_path = HashMap::default();
4341            let mut item_ids_by_kind = HashMap::default();
4342            let mut all_deserialized_items = Vec::default();
4343            cx.update(|cx| {
4344                for item in center_items.unwrap_or_default().into_iter().flatten() {
4345                    if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
4346                        item_ids_by_kind
4347                            .entry(serializable_item_handle.serialized_item_kind())
4348                            .or_insert(Vec::new())
4349                            .push(item.item_id().as_u64() as ItemId);
4350                    }
4351
4352                    if let Some(project_path) = item.project_path(cx) {
4353                        items_by_project_path.insert(project_path, item.clone());
4354                    }
4355                    all_deserialized_items.push(item);
4356                }
4357            })?;
4358
4359            let opened_items = paths_to_open
4360                .into_iter()
4361                .map(|path_to_open| {
4362                    path_to_open
4363                        .and_then(|path_to_open| items_by_project_path.remove(&path_to_open))
4364                })
4365                .collect::<Vec<_>>();
4366
4367            // Remove old panes from workspace panes list
4368            workspace.update(&mut cx, |workspace, cx| {
4369                if let Some((center_group, active_pane)) = center_group {
4370                    workspace.remove_panes(workspace.center.root.clone(), cx);
4371
4372                    // Swap workspace center group
4373                    workspace.center = PaneGroup::with_root(center_group);
4374                    if let Some(active_pane) = active_pane {
4375                        workspace.set_active_pane(&active_pane, cx);
4376                        cx.focus_self();
4377                    } else {
4378                        workspace.set_active_pane(&workspace.center.first_pane(), cx);
4379                    }
4380                }
4381
4382                let docks = serialized_workspace.docks;
4383
4384                for (dock, serialized_dock) in [
4385                    (&mut workspace.right_dock, docks.right),
4386                    (&mut workspace.left_dock, docks.left),
4387                    (&mut workspace.bottom_dock, docks.bottom),
4388                ]
4389                .iter_mut()
4390                {
4391                    dock.update(cx, |dock, cx| {
4392                        dock.serialized_dock = Some(serialized_dock.clone());
4393                        dock.restore_state(cx);
4394                    });
4395                }
4396
4397                cx.notify();
4398            })?;
4399
4400            // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
4401            // after loading the items, we might have different items and in order to avoid
4402            // the database filling up, we delete items that haven't been loaded now.
4403            //
4404            // The items that have been loaded, have been saved after they've been added to the workspace.
4405            let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
4406                item_ids_by_kind
4407                    .into_iter()
4408                    .map(|(item_kind, loaded_items)| {
4409                        SerializableItemRegistry::cleanup(
4410                            item_kind,
4411                            serialized_workspace.id,
4412                            loaded_items,
4413                            cx,
4414                        )
4415                        .log_err()
4416                    })
4417                    .collect::<Vec<_>>()
4418            })?;
4419
4420            futures::future::join_all(clean_up_tasks).await;
4421
4422            workspace
4423                .update(&mut cx, |workspace, cx| {
4424                    // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
4425                    workspace.serialize_workspace_internal(cx).detach();
4426
4427                    // Ensure that we mark the window as edited if we did load dirty items
4428                    workspace.update_window_edited(cx);
4429                })
4430                .ok();
4431
4432            Ok(opened_items)
4433        })
4434    }
4435
4436    fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
4437        self.add_workspace_actions_listeners(div, cx)
4438            .on_action(cx.listener(Self::close_inactive_items_and_panes))
4439            .on_action(cx.listener(Self::close_all_items_and_panes))
4440            .on_action(cx.listener(Self::save_all))
4441            .on_action(cx.listener(Self::send_keystrokes))
4442            .on_action(cx.listener(Self::add_folder_to_project))
4443            .on_action(cx.listener(Self::follow_next_collaborator))
4444            .on_action(cx.listener(Self::close_window))
4445            .on_action(cx.listener(Self::activate_pane_at_index))
4446            .on_action(cx.listener(Self::move_item_to_pane_at_index))
4447            .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
4448                let pane = workspace.active_pane().clone();
4449                workspace.unfollow_in_pane(&pane, cx);
4450            }))
4451            .on_action(cx.listener(|workspace, action: &Save, cx| {
4452                workspace
4453                    .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
4454                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4455            }))
4456            .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, cx| {
4457                workspace
4458                    .save_active_item(SaveIntent::SaveWithoutFormat, cx)
4459                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4460            }))
4461            .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
4462                workspace
4463                    .save_active_item(SaveIntent::SaveAs, cx)
4464                    .detach_and_prompt_err("Failed to save", cx, |_, _| None);
4465            }))
4466            .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
4467                workspace.activate_previous_pane(cx)
4468            }))
4469            .on_action(
4470                cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
4471            )
4472            .on_action(
4473                cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
4474                    workspace.activate_pane_in_direction(action.0, cx)
4475                }),
4476            )
4477            .on_action(
4478                cx.listener(|workspace, action: &MoveItemToPaneInDirection, cx| {
4479                    workspace.move_item_to_pane_in_direction(action, cx)
4480                }),
4481            )
4482            .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
4483                workspace.swap_pane_in_direction(action.0, cx)
4484            }))
4485            .on_action(cx.listener(|this, _: &ToggleLeftDock, cx| {
4486                this.toggle_dock(DockPosition::Left, cx);
4487            }))
4488            .on_action(
4489                cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
4490                    workspace.toggle_dock(DockPosition::Right, cx);
4491                }),
4492            )
4493            .on_action(
4494                cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
4495                    workspace.toggle_dock(DockPosition::Bottom, cx);
4496                }),
4497            )
4498            .on_action(
4499                cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
4500                    workspace.close_all_docks(cx);
4501                }),
4502            )
4503            .on_action(
4504                cx.listener(|workspace: &mut Workspace, _: &ClearAllNotifications, cx| {
4505                    workspace.clear_all_notifications(cx);
4506                }),
4507            )
4508            .on_action(
4509                cx.listener(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
4510                    workspace.reopen_closed_item(cx).detach();
4511                }),
4512            )
4513            .on_action(cx.listener(Workspace::toggle_centered_layout))
4514    }
4515
4516    #[cfg(any(test, feature = "test-support"))]
4517    pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
4518        use node_runtime::NodeRuntime;
4519        use session::Session;
4520
4521        let client = project.read(cx).client();
4522        let user_store = project.read(cx).user_store();
4523
4524        let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
4525        let session = cx.new_model(|cx| AppSession::new(Session::test(), cx));
4526        cx.activate_window();
4527        let app_state = Arc::new(AppState {
4528            languages: project.read(cx).languages().clone(),
4529            workspace_store,
4530            client,
4531            user_store,
4532            fs: project.read(cx).fs().clone(),
4533            build_window_options: |_, _| Default::default(),
4534            node_runtime: NodeRuntime::unavailable(),
4535            session,
4536        });
4537        let workspace = Self::new(Default::default(), project, app_state, cx);
4538        workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
4539        workspace
4540    }
4541
4542    pub fn register_action<A: Action>(
4543        &mut self,
4544        callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
4545    ) -> &mut Self {
4546        let callback = Arc::new(callback);
4547
4548        self.workspace_actions.push(Box::new(move |div, cx| {
4549            let callback = callback.clone();
4550            div.on_action(
4551                cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
4552            )
4553        }));
4554        self
4555    }
4556
4557    fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
4558        for action in self.workspace_actions.iter() {
4559            div = (action)(div, cx)
4560        }
4561        div
4562    }
4563
4564    pub fn has_active_modal(&self, cx: &WindowContext) -> bool {
4565        self.modal_layer.read(cx).has_active_modal()
4566    }
4567
4568    pub fn active_modal<V: ManagedView + 'static>(&self, cx: &AppContext) -> Option<View<V>> {
4569        self.modal_layer.read(cx).active_modal()
4570    }
4571
4572    pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut WindowContext, build: B)
4573    where
4574        B: FnOnce(&mut ViewContext<V>) -> V,
4575    {
4576        self.modal_layer
4577            .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
4578    }
4579
4580    pub fn toggle_centered_layout(&mut self, _: &ToggleCenteredLayout, cx: &mut ViewContext<Self>) {
4581        self.centered_layout = !self.centered_layout;
4582        if let Some(database_id) = self.database_id() {
4583            cx.background_executor()
4584                .spawn(DB.set_centered_layout(database_id, self.centered_layout))
4585                .detach_and_log_err(cx);
4586        }
4587        cx.notify();
4588    }
4589
4590    fn adjust_padding(padding: Option<f32>) -> f32 {
4591        padding
4592            .unwrap_or(Self::DEFAULT_PADDING)
4593            .clamp(0.0, Self::MAX_PADDING)
4594    }
4595
4596    fn render_dock(
4597        &self,
4598        position: DockPosition,
4599        dock: &View<Dock>,
4600        cx: &WindowContext,
4601    ) -> Option<Div> {
4602        if self.zoomed_position == Some(position) {
4603            return None;
4604        }
4605
4606        let leader_border = dock.read(cx).active_panel().and_then(|panel| {
4607            let pane = panel.pane(cx)?;
4608            let follower_states = &self.follower_states;
4609            leader_border_for_pane(follower_states, &pane, cx)
4610        });
4611
4612        Some(
4613            div()
4614                .flex()
4615                .flex_none()
4616                .overflow_hidden()
4617                .child(dock.clone())
4618                .children(leader_border),
4619        )
4620    }
4621
4622    pub fn for_window(cx: &mut WindowContext) -> Option<View<Workspace>> {
4623        let window = cx.window_handle().downcast::<Workspace>()?;
4624        cx.read_window(&window, |workspace, _| workspace).ok()
4625    }
4626
4627    pub fn zoomed_item(&self) -> Option<&AnyWeakView> {
4628        self.zoomed.as_ref()
4629    }
4630}
4631
4632fn leader_border_for_pane(
4633    follower_states: &HashMap<PeerId, FollowerState>,
4634    pane: &View<Pane>,
4635    cx: &WindowContext,
4636) -> Option<Div> {
4637    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4638        if state.pane() == pane {
4639            Some((*leader_id, state))
4640        } else {
4641            None
4642        }
4643    })?;
4644
4645    let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4646    let leader = room.remote_participant_for_peer_id(leader_id)?;
4647
4648    let mut leader_color = cx
4649        .theme()
4650        .players()
4651        .color_for_participant(leader.participant_index.0)
4652        .cursor;
4653    leader_color.fade_out(0.3);
4654    Some(
4655        div()
4656            .absolute()
4657            .size_full()
4658            .left_0()
4659            .top_0()
4660            .border_2()
4661            .border_color(leader_color),
4662    )
4663}
4664
4665fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4666    ZED_WINDOW_POSITION
4667        .zip(*ZED_WINDOW_SIZE)
4668        .map(|(position, size)| Bounds {
4669            origin: position,
4670            size,
4671        })
4672}
4673
4674fn open_items(
4675    serialized_workspace: Option<SerializedWorkspace>,
4676    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4677    cx: &mut ViewContext<Workspace>,
4678) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4679    let restored_items = serialized_workspace.map(|serialized_workspace| {
4680        Workspace::load_workspace(
4681            serialized_workspace,
4682            project_paths_to_open
4683                .iter()
4684                .map(|(_, project_path)| project_path)
4685                .cloned()
4686                .collect(),
4687            cx,
4688        )
4689    });
4690
4691    cx.spawn(|workspace, mut cx| async move {
4692        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4693
4694        if let Some(restored_items) = restored_items {
4695            let restored_items = restored_items.await?;
4696
4697            let restored_project_paths = restored_items
4698                .iter()
4699                .filter_map(|item| {
4700                    cx.update(|cx| item.as_ref()?.project_path(cx))
4701                        .ok()
4702                        .flatten()
4703                })
4704                .collect::<HashSet<_>>();
4705
4706            for restored_item in restored_items {
4707                opened_items.push(restored_item.map(Ok));
4708            }
4709
4710            project_paths_to_open
4711                .iter_mut()
4712                .for_each(|(_, project_path)| {
4713                    if let Some(project_path_to_open) = project_path {
4714                        if restored_project_paths.contains(project_path_to_open) {
4715                            *project_path = None;
4716                        }
4717                    }
4718                });
4719        } else {
4720            for _ in 0..project_paths_to_open.len() {
4721                opened_items.push(None);
4722            }
4723        }
4724        assert!(opened_items.len() == project_paths_to_open.len());
4725
4726        let tasks =
4727            project_paths_to_open
4728                .into_iter()
4729                .enumerate()
4730                .map(|(ix, (abs_path, project_path))| {
4731                    let workspace = workspace.clone();
4732                    cx.spawn(|mut cx| async move {
4733                        let file_project_path = project_path?;
4734                        let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
4735                            workspace.project().update(cx, |project, cx| {
4736                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
4737                            })
4738                        });
4739
4740                        // We only want to open file paths here. If one of the items
4741                        // here is a directory, it was already opened further above
4742                        // with a `find_or_create_worktree`.
4743                        if let Ok(task) = abs_path_task {
4744                            if task.await.map_or(true, |p| p.is_file()) {
4745                                return Some((
4746                                    ix,
4747                                    workspace
4748                                        .update(&mut cx, |workspace, cx| {
4749                                            workspace.open_path(file_project_path, None, true, cx)
4750                                        })
4751                                        .log_err()?
4752                                        .await,
4753                                ));
4754                            }
4755                        }
4756                        None
4757                    })
4758                });
4759
4760        let tasks = tasks.collect::<Vec<_>>();
4761
4762        let tasks = futures::future::join_all(tasks);
4763        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4764            opened_items[ix] = Some(path_open_result);
4765        }
4766
4767        Ok(opened_items)
4768    })
4769}
4770
4771enum ActivateInDirectionTarget {
4772    Pane(View<Pane>),
4773    Dock(View<Dock>),
4774}
4775
4776fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4777    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";
4778
4779    workspace
4780        .update(cx, |workspace, cx| {
4781            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4782                struct DatabaseFailedNotification;
4783
4784                workspace.show_notification_once(
4785                    NotificationId::unique::<DatabaseFailedNotification>(),
4786                    cx,
4787                    |cx| {
4788                        cx.new_view(|_| {
4789                            MessageNotification::new("Failed to load the database file.")
4790                                .with_click_message("File an issue")
4791                                .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4792                        })
4793                    },
4794                );
4795            }
4796        })
4797        .log_err();
4798}
4799
4800impl FocusableView for Workspace {
4801    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4802        self.active_pane.focus_handle(cx)
4803    }
4804}
4805
4806#[derive(Clone, Render)]
4807struct DraggedDock(DockPosition);
4808
4809impl Render for Workspace {
4810    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4811        let mut context = KeyContext::new_with_defaults();
4812        context.add("Workspace");
4813        context.set("keyboard_layout", cx.keyboard_layout().clone());
4814        let centered_layout = self.centered_layout
4815            && self.center.panes().len() == 1
4816            && self.active_item(cx).is_some();
4817        let render_padding = |size| {
4818            (size > 0.0).then(|| {
4819                div()
4820                    .h_full()
4821                    .w(relative(size))
4822                    .bg(cx.theme().colors().editor_background)
4823                    .border_color(cx.theme().colors().pane_group_border)
4824            })
4825        };
4826        let paddings = if centered_layout {
4827            let settings = WorkspaceSettings::get_global(cx).centered_layout;
4828            (
4829                render_padding(Self::adjust_padding(settings.left_padding)),
4830                render_padding(Self::adjust_padding(settings.right_padding)),
4831            )
4832        } else {
4833            (None, None)
4834        };
4835        let ui_font = theme::setup_ui_font(cx);
4836
4837        let theme = cx.theme().clone();
4838        let colors = theme.colors();
4839
4840        client_side_decorations(
4841            self.actions(div(), cx)
4842                .key_context(context)
4843                .relative()
4844                .size_full()
4845                .flex()
4846                .flex_col()
4847                .font(ui_font)
4848                .gap_0()
4849                .justify_start()
4850                .items_start()
4851                .text_color(colors.text)
4852                .overflow_hidden()
4853                .children(self.titlebar_item.clone())
4854                .child(
4855                    div()
4856                        .size_full()
4857                        .relative()
4858                        .flex_1()
4859                        .flex()
4860                        .flex_col()
4861                        .child(
4862                            div()
4863                                .id("workspace")
4864                                .bg(colors.background)
4865                                .relative()
4866                                .flex_1()
4867                                .w_full()
4868                                .flex()
4869                                .flex_col()
4870                                .overflow_hidden()
4871                                .border_t_1()
4872                                .border_b_1()
4873                                .border_color(colors.border)
4874                                .child({
4875                                    let this = cx.view().clone();
4876                                    canvas(
4877                                        move |bounds, cx| {
4878                                            this.update(cx, |this, cx| {
4879                                                let bounds_changed = this.bounds != bounds;
4880                                                this.bounds = bounds;
4881
4882                                                if bounds_changed {
4883                                                    this.left_dock.update(cx, |dock, cx| {
4884                                                        dock.clamp_panel_size(bounds.size.width, cx)
4885                                                    });
4886
4887                                                    this.right_dock.update(cx, |dock, cx| {
4888                                                        dock.clamp_panel_size(bounds.size.width, cx)
4889                                                    });
4890
4891                                                    this.bottom_dock.update(cx, |dock, cx| {
4892                                                        dock.clamp_panel_size(
4893                                                            bounds.size.height,
4894                                                            cx,
4895                                                        )
4896                                                    });
4897                                                }
4898                                            })
4899                                        },
4900                                        |_, _, _| {},
4901                                    )
4902                                    .absolute()
4903                                    .size_full()
4904                                })
4905                                .when(self.zoomed.is_none(), |this| {
4906                                    this.on_drag_move(cx.listener(
4907                                        |workspace, e: &DragMoveEvent<DraggedDock>, cx| {
4908                                            match e.drag(cx).0 {
4909                                                DockPosition::Left => {
4910                                                    resize_left_dock(
4911                                                        e.event.position.x
4912                                                            - workspace.bounds.left(),
4913                                                        workspace,
4914                                                        cx,
4915                                                    );
4916                                                }
4917                                                DockPosition::Right => {
4918                                                    resize_right_dock(
4919                                                        workspace.bounds.right()
4920                                                            - e.event.position.x,
4921                                                        workspace,
4922                                                        cx,
4923                                                    );
4924                                                }
4925                                                DockPosition::Bottom => {
4926                                                    resize_bottom_dock(
4927                                                        workspace.bounds.bottom()
4928                                                            - e.event.position.y,
4929                                                        workspace,
4930                                                        cx,
4931                                                    );
4932                                                }
4933                                            }
4934                                        },
4935                                    ))
4936                                })
4937                                .child(
4938                                    div()
4939                                        .flex()
4940                                        .flex_row()
4941                                        .h_full()
4942                                        // Left Dock
4943                                        .children(self.render_dock(
4944                                            DockPosition::Left,
4945                                            &self.left_dock,
4946                                            cx,
4947                                        ))
4948                                        // Panes
4949                                        .child(
4950                                            div()
4951                                                .flex()
4952                                                .flex_col()
4953                                                .flex_1()
4954                                                .overflow_hidden()
4955                                                .child(
4956                                                    h_flex()
4957                                                        .flex_1()
4958                                                        .when_some(paddings.0, |this, p| {
4959                                                            this.child(p.border_r_1())
4960                                                        })
4961                                                        .child(self.center.render(
4962                                                            &self.project,
4963                                                            &self.follower_states,
4964                                                            self.active_call(),
4965                                                            &self.active_pane,
4966                                                            self.zoomed.as_ref(),
4967                                                            &self.app_state,
4968                                                            cx,
4969                                                        ))
4970                                                        .when_some(paddings.1, |this, p| {
4971                                                            this.child(p.border_l_1())
4972                                                        }),
4973                                                )
4974                                                .children(self.render_dock(
4975                                                    DockPosition::Bottom,
4976                                                    &self.bottom_dock,
4977                                                    cx,
4978                                                )),
4979                                        )
4980                                        // Right Dock
4981                                        .children(self.render_dock(
4982                                            DockPosition::Right,
4983                                            &self.right_dock,
4984                                            cx,
4985                                        )),
4986                                )
4987                                .children(self.zoomed.as_ref().and_then(|view| {
4988                                    let zoomed_view = view.upgrade()?;
4989                                    let div = div()
4990                                        .occlude()
4991                                        .absolute()
4992                                        .overflow_hidden()
4993                                        .border_color(colors.border)
4994                                        .bg(colors.background)
4995                                        .child(zoomed_view)
4996                                        .inset_0()
4997                                        .shadow_lg();
4998
4999                                    Some(match self.zoomed_position {
5000                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
5001                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
5002                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
5003                                        None => {
5004                                            div.top_2().bottom_2().left_2().right_2().border_1()
5005                                        }
5006                                    })
5007                                }))
5008                                .children(self.render_notifications(cx)),
5009                        )
5010                        .child(self.status_bar.clone())
5011                        .child(self.modal_layer.clone()),
5012                ),
5013            cx,
5014        )
5015    }
5016}
5017
5018fn resize_bottom_dock(
5019    new_size: Pixels,
5020    workspace: &mut Workspace,
5021    cx: &mut ViewContext<Workspace>,
5022) {
5023    let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
5024    workspace.bottom_dock.update(cx, |bottom_dock, cx| {
5025        bottom_dock.resize_active_panel(Some(size), cx);
5026    });
5027}
5028
5029fn resize_right_dock(new_size: Pixels, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
5030    let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
5031    workspace.right_dock.update(cx, |right_dock, cx| {
5032        right_dock.resize_active_panel(Some(size), cx);
5033    });
5034}
5035
5036fn resize_left_dock(new_size: Pixels, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
5037    let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
5038
5039    workspace.left_dock.update(cx, |left_dock, cx| {
5040        left_dock.resize_active_panel(Some(size), cx);
5041    });
5042}
5043
5044impl WorkspaceStore {
5045    pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
5046        Self {
5047            workspaces: Default::default(),
5048            _subscriptions: vec![
5049                client.add_request_handler(cx.weak_model(), Self::handle_follow),
5050                client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
5051            ],
5052            client,
5053        }
5054    }
5055
5056    pub fn update_followers(
5057        &self,
5058        project_id: Option<u64>,
5059        update: proto::update_followers::Variant,
5060        cx: &AppContext,
5061    ) -> Option<()> {
5062        let active_call = ActiveCall::try_global(cx)?;
5063        let room_id = active_call.read(cx).room()?.read(cx).id();
5064        self.client
5065            .send(proto::UpdateFollowers {
5066                room_id,
5067                project_id,
5068                variant: Some(update),
5069            })
5070            .log_err()
5071    }
5072
5073    pub async fn handle_follow(
5074        this: Model<Self>,
5075        envelope: TypedEnvelope<proto::Follow>,
5076        mut cx: AsyncAppContext,
5077    ) -> Result<proto::FollowResponse> {
5078        this.update(&mut cx, |this, cx| {
5079            let follower = Follower {
5080                project_id: envelope.payload.project_id,
5081                peer_id: envelope.original_sender_id()?,
5082            };
5083
5084            let mut response = proto::FollowResponse::default();
5085            this.workspaces.retain(|workspace| {
5086                workspace
5087                    .update(cx, |workspace, cx| {
5088                        let handler_response = workspace.handle_follow(follower.project_id, cx);
5089                        if let Some(active_view) = handler_response.active_view.clone() {
5090                            if workspace.project.read(cx).remote_id() == follower.project_id {
5091                                response.active_view = Some(active_view)
5092                            }
5093                        }
5094                    })
5095                    .is_ok()
5096            });
5097
5098            Ok(response)
5099        })?
5100    }
5101
5102    async fn handle_update_followers(
5103        this: Model<Self>,
5104        envelope: TypedEnvelope<proto::UpdateFollowers>,
5105        mut cx: AsyncAppContext,
5106    ) -> Result<()> {
5107        let leader_id = envelope.original_sender_id()?;
5108        let update = envelope.payload;
5109
5110        this.update(&mut cx, |this, cx| {
5111            this.workspaces.retain(|workspace| {
5112                workspace
5113                    .update(cx, |workspace, cx| {
5114                        let project_id = workspace.project.read(cx).remote_id();
5115                        if update.project_id != project_id && update.project_id.is_some() {
5116                            return;
5117                        }
5118                        workspace.handle_update_followers(leader_id, update.clone(), cx);
5119                    })
5120                    .is_ok()
5121            });
5122            Ok(())
5123        })?
5124    }
5125}
5126
5127impl ViewId {
5128    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5129        Ok(Self {
5130            creator: message
5131                .creator
5132                .ok_or_else(|| anyhow!("creator is missing"))?,
5133            id: message.id,
5134        })
5135    }
5136
5137    pub(crate) fn to_proto(self) -> proto::ViewId {
5138        proto::ViewId {
5139            creator: Some(self.creator),
5140            id: self.id,
5141        }
5142    }
5143}
5144
5145impl FollowerState {
5146    fn pane(&self) -> &View<Pane> {
5147        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5148    }
5149}
5150
5151pub trait WorkspaceHandle {
5152    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
5153}
5154
5155impl WorkspaceHandle for View<Workspace> {
5156    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
5157        self.read(cx)
5158            .worktrees(cx)
5159            .flat_map(|worktree| {
5160                let worktree_id = worktree.read(cx).id();
5161                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5162                    worktree_id,
5163                    path: f.path.clone(),
5164                })
5165            })
5166            .collect::<Vec<_>>()
5167    }
5168}
5169
5170impl std::fmt::Debug for OpenPaths {
5171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5172        f.debug_struct("OpenPaths")
5173            .field("paths", &self.paths)
5174            .finish()
5175    }
5176}
5177
5178pub fn activate_workspace_for_project(
5179    cx: &mut AppContext,
5180    predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
5181) -> Option<WindowHandle<Workspace>> {
5182    for window in cx.windows() {
5183        let Some(workspace) = window.downcast::<Workspace>() else {
5184            continue;
5185        };
5186
5187        let predicate = workspace
5188            .update(cx, |workspace, cx| {
5189                let project = workspace.project.read(cx);
5190                if predicate(project, cx) {
5191                    cx.activate_window();
5192                    true
5193                } else {
5194                    false
5195                }
5196            })
5197            .log_err()
5198            .unwrap_or(false);
5199
5200        if predicate {
5201            return Some(workspace);
5202        }
5203    }
5204
5205    None
5206}
5207
5208pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5209    DB.last_workspace().await.log_err().flatten()
5210}
5211
5212pub fn last_session_workspace_locations(
5213    last_session_id: &str,
5214    last_session_window_stack: Option<Vec<WindowId>>,
5215) -> Option<Vec<SerializedWorkspaceLocation>> {
5216    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5217        .log_err()
5218}
5219
5220actions!(collab, [OpenChannelNotes]);
5221actions!(zed, [OpenLog]);
5222
5223async fn join_channel_internal(
5224    channel_id: ChannelId,
5225    app_state: &Arc<AppState>,
5226    requesting_window: Option<WindowHandle<Workspace>>,
5227    active_call: &Model<ActiveCall>,
5228    cx: &mut AsyncAppContext,
5229) -> Result<bool> {
5230    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5231        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5232            return (false, None);
5233        };
5234
5235        let already_in_channel = room.channel_id() == Some(channel_id);
5236        let should_prompt = room.is_sharing_project()
5237            && !room.remote_participants().is_empty()
5238            && !already_in_channel;
5239        let open_room = if already_in_channel {
5240            active_call.room().cloned()
5241        } else {
5242            None
5243        };
5244        (should_prompt, open_room)
5245    })?;
5246
5247    if let Some(room) = open_room {
5248        let task = room.update(cx, |room, cx| {
5249            if let Some((project, host)) = room.most_active_project(cx) {
5250                return Some(join_in_room_project(project, host, app_state.clone(), cx));
5251            }
5252
5253            None
5254        })?;
5255        if let Some(task) = task {
5256            task.await?;
5257        }
5258        return anyhow::Ok(true);
5259    }
5260
5261    if should_prompt {
5262        if let Some(workspace) = requesting_window {
5263            let answer = workspace
5264                .update(cx, |_, cx| {
5265                    cx.prompt(
5266                        PromptLevel::Warning,
5267                        "Do you want to switch channels?",
5268                        Some("Leaving this call will unshare your current project."),
5269                        &["Yes, Join Channel", "Cancel"],
5270                    )
5271                })?
5272                .await;
5273
5274            if answer == Ok(1) {
5275                return Ok(false);
5276            }
5277        } else {
5278            return Ok(false); // unreachable!() hopefully
5279        }
5280    }
5281
5282    let client = cx.update(|cx| active_call.read(cx).client())?;
5283
5284    let mut client_status = client.status();
5285
5286    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5287    'outer: loop {
5288        let Some(status) = client_status.recv().await else {
5289            return Err(anyhow!("error connecting"));
5290        };
5291
5292        match status {
5293            Status::Connecting
5294            | Status::Authenticating
5295            | Status::Reconnecting
5296            | Status::Reauthenticating => continue,
5297            Status::Connected { .. } => break 'outer,
5298            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5299            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5300            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5301                return Err(ErrorCode::Disconnected.into());
5302            }
5303        }
5304    }
5305
5306    let room = active_call
5307        .update(cx, |active_call, cx| {
5308            active_call.join_channel(channel_id, cx)
5309        })?
5310        .await?;
5311
5312    let Some(room) = room else {
5313        return anyhow::Ok(true);
5314    };
5315
5316    room.update(cx, |room, _| room.room_update_completed())?
5317        .await;
5318
5319    let task = room.update(cx, |room, cx| {
5320        if let Some((project, host)) = room.most_active_project(cx) {
5321            return Some(join_in_room_project(project, host, app_state.clone(), cx));
5322        }
5323
5324        // If you are the first to join a channel, see if you should share your project.
5325        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5326            if let Some(workspace) = requesting_window {
5327                let project = workspace.update(cx, |workspace, cx| {
5328                    let project = workspace.project.read(cx);
5329
5330                    if !CallSettings::get_global(cx).share_on_join {
5331                        return None;
5332                    }
5333
5334                    if (project.is_local() || project.is_via_ssh())
5335                        && project.visible_worktrees(cx).any(|tree| {
5336                            tree.read(cx)
5337                                .root_entry()
5338                                .map_or(false, |entry| entry.is_dir())
5339                        })
5340                    {
5341                        Some(workspace.project.clone())
5342                    } else {
5343                        None
5344                    }
5345                });
5346                if let Ok(Some(project)) = project {
5347                    return Some(cx.spawn(|room, mut cx| async move {
5348                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5349                            .await?;
5350                        Ok(())
5351                    }));
5352                }
5353            }
5354        }
5355
5356        None
5357    })?;
5358    if let Some(task) = task {
5359        task.await?;
5360        return anyhow::Ok(true);
5361    }
5362    anyhow::Ok(false)
5363}
5364
5365pub fn join_channel(
5366    channel_id: ChannelId,
5367    app_state: Arc<AppState>,
5368    requesting_window: Option<WindowHandle<Workspace>>,
5369    cx: &mut AppContext,
5370) -> Task<Result<()>> {
5371    let active_call = ActiveCall::global(cx);
5372    cx.spawn(|mut cx| async move {
5373        let result = join_channel_internal(
5374            channel_id,
5375            &app_state,
5376            requesting_window,
5377            &active_call,
5378            &mut cx,
5379        )
5380            .await;
5381
5382        // join channel succeeded, and opened a window
5383        if matches!(result, Ok(true)) {
5384            return anyhow::Ok(());
5385        }
5386
5387        // find an existing workspace to focus and show call controls
5388        let mut active_window =
5389            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5390        if active_window.is_none() {
5391            // no open workspaces, make one to show the error in (blergh)
5392            let (window_handle, _) = cx
5393                .update(|cx| {
5394                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5395                })?
5396                .await?;
5397
5398            if result.is_ok() {
5399                cx.update(|cx| {
5400                    cx.dispatch_action(&OpenChannelNotes);
5401                }).log_err();
5402            }
5403
5404            active_window = Some(window_handle);
5405        }
5406
5407        if let Err(err) = result {
5408            log::error!("failed to join channel: {}", err);
5409            if let Some(active_window) = active_window {
5410                active_window
5411                    .update(&mut cx, |_, cx| {
5412                        let detail: SharedString = match err.error_code() {
5413                            ErrorCode::SignedOut => {
5414                                "Please sign in to continue.".into()
5415                            }
5416                            ErrorCode::UpgradeRequired => {
5417                                "Your are running an unsupported version of Zed. Please update to continue.".into()
5418                            }
5419                            ErrorCode::NoSuchChannel => {
5420                                "No matching channel was found. Please check the link and try again.".into()
5421                            }
5422                            ErrorCode::Forbidden => {
5423                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5424                            }
5425                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5426                            _ => format!("{}\n\nPlease try again.", err).into(),
5427                        };
5428                        cx.prompt(
5429                            PromptLevel::Critical,
5430                            "Failed to join channel",
5431                            Some(&detail),
5432                            &["Ok"],
5433                        )
5434                    })?
5435                    .await
5436                    .ok();
5437            }
5438        }
5439
5440        // return ok, we showed the error to the user.
5441        anyhow::Ok(())
5442    })
5443}
5444
5445pub async fn get_any_active_workspace(
5446    app_state: Arc<AppState>,
5447    mut cx: AsyncAppContext,
5448) -> anyhow::Result<WindowHandle<Workspace>> {
5449    // find an existing workspace to focus and show call controls
5450    let active_window = activate_any_workspace_window(&mut cx);
5451    if active_window.is_none() {
5452        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5453            .await?;
5454    }
5455    activate_any_workspace_window(&mut cx).context("could not open zed")
5456}
5457
5458fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5459    cx.update(|cx| {
5460        if let Some(workspace_window) = cx
5461            .active_window()
5462            .and_then(|window| window.downcast::<Workspace>())
5463        {
5464            return Some(workspace_window);
5465        }
5466
5467        for window in cx.windows() {
5468            if let Some(workspace_window) = window.downcast::<Workspace>() {
5469                workspace_window
5470                    .update(cx, |_, cx| cx.activate_window())
5471                    .ok();
5472                return Some(workspace_window);
5473            }
5474        }
5475        None
5476    })
5477    .ok()
5478    .flatten()
5479}
5480
5481pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5482    cx.windows()
5483        .into_iter()
5484        .filter_map(|window| window.downcast::<Workspace>())
5485        .filter(|workspace| {
5486            workspace
5487                .read(cx)
5488                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5489        })
5490        .collect()
5491}
5492
5493#[derive(Default)]
5494pub struct OpenOptions {
5495    pub open_new_workspace: Option<bool>,
5496    pub replace_window: Option<WindowHandle<Workspace>>,
5497    pub env: Option<HashMap<String, String>>,
5498}
5499
5500#[allow(clippy::type_complexity)]
5501pub fn open_paths(
5502    abs_paths: &[PathBuf],
5503    app_state: Arc<AppState>,
5504    open_options: OpenOptions,
5505    cx: &mut AppContext,
5506) -> Task<
5507    anyhow::Result<(
5508        WindowHandle<Workspace>,
5509        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5510    )>,
5511> {
5512    let abs_paths = abs_paths.to_vec();
5513    let mut existing = None;
5514    let mut best_match = None;
5515    let mut open_visible = OpenVisible::All;
5516
5517    if open_options.open_new_workspace != Some(true) {
5518        for window in local_workspace_windows(cx) {
5519            if let Ok(workspace) = window.read(cx) {
5520                let m = workspace
5521                    .project
5522                    .read(cx)
5523                    .visibility_for_paths(&abs_paths, cx);
5524                if m > best_match {
5525                    existing = Some(window);
5526                    best_match = m;
5527                } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5528                    existing = Some(window)
5529                }
5530            }
5531        }
5532    }
5533
5534    cx.spawn(move |mut cx| async move {
5535        if open_options.open_new_workspace.is_none() && existing.is_none() {
5536            let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5537            if futures::future::join_all(all_files)
5538                .await
5539                .into_iter()
5540                .filter_map(|result| result.ok().flatten())
5541                .all(|file| !file.is_dir)
5542            {
5543                cx.update(|cx| {
5544                    for window in local_workspace_windows(cx) {
5545                        if let Ok(workspace) = window.read(cx) {
5546                            let project = workspace.project().read(cx);
5547                            if project.is_via_collab() {
5548                                continue;
5549                            }
5550                            existing = Some(window);
5551                            open_visible = OpenVisible::None;
5552                            break;
5553                        }
5554                    }
5555                })?;
5556            }
5557        }
5558
5559        if let Some(existing) = existing {
5560            Ok((
5561                existing,
5562                existing
5563                    .update(&mut cx, |workspace, cx| {
5564                        cx.activate_window();
5565                        workspace.open_paths(abs_paths, open_visible, None, cx)
5566                    })?
5567                    .await,
5568            ))
5569        } else {
5570            cx.update(move |cx| {
5571                Workspace::new_local(
5572                    abs_paths,
5573                    app_state.clone(),
5574                    open_options.replace_window,
5575                    open_options.env,
5576                    cx,
5577                )
5578            })?
5579            .await
5580        }
5581    })
5582}
5583
5584pub fn open_new(
5585    open_options: OpenOptions,
5586    app_state: Arc<AppState>,
5587    cx: &mut AppContext,
5588    init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5589) -> Task<anyhow::Result<()>> {
5590    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5591    cx.spawn(|mut cx| async move {
5592        let (workspace, opened_paths) = task.await?;
5593        workspace.update(&mut cx, |workspace, cx| {
5594            if opened_paths.is_empty() {
5595                init(workspace, cx)
5596            }
5597        })?;
5598        Ok(())
5599    })
5600}
5601
5602pub fn create_and_open_local_file(
5603    path: &'static Path,
5604    cx: &mut ViewContext<Workspace>,
5605    default_content: impl 'static + Send + FnOnce() -> Rope,
5606) -> Task<Result<Box<dyn ItemHandle>>> {
5607    cx.spawn(|workspace, mut cx| async move {
5608        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5609        if !fs.is_file(path).await {
5610            fs.create_file(path, Default::default()).await?;
5611            fs.save(path, &default_content(), Default::default())
5612                .await?;
5613        }
5614
5615        let mut items = workspace
5616            .update(&mut cx, |workspace, cx| {
5617                workspace.with_local_workspace(cx, |workspace, cx| {
5618                    workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5619                })
5620            })?
5621            .await?
5622            .await;
5623
5624        let item = items.pop().flatten();
5625        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5626    })
5627}
5628
5629pub fn open_ssh_project(
5630    window: WindowHandle<Workspace>,
5631    connection_options: SshConnectionOptions,
5632    cancel_rx: oneshot::Receiver<()>,
5633    delegate: Arc<dyn SshClientDelegate>,
5634    app_state: Arc<AppState>,
5635    paths: Vec<PathBuf>,
5636    cx: &mut AppContext,
5637) -> Task<Result<()>> {
5638    cx.spawn(|mut cx| async move {
5639        let (serialized_ssh_project, workspace_id, serialized_workspace) =
5640            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5641
5642        let session = match cx
5643            .update(|cx| {
5644                remote::SshRemoteClient::new(
5645                    ConnectionIdentifier::Workspace(workspace_id.0),
5646                    connection_options,
5647                    cancel_rx,
5648                    delegate,
5649                    cx,
5650                )
5651            })?
5652            .await?
5653        {
5654            Some(result) => result,
5655            None => return Ok(()),
5656        };
5657
5658        let project = cx.update(|cx| {
5659            project::Project::ssh(
5660                session,
5661                app_state.client.clone(),
5662                app_state.node_runtime.clone(),
5663                app_state.user_store.clone(),
5664                app_state.languages.clone(),
5665                app_state.fs.clone(),
5666                cx,
5667            )
5668        })?;
5669
5670        let toolchains = DB.toolchains(workspace_id).await?;
5671        for (toolchain, worktree_id) in toolchains {
5672            project
5673                .update(&mut cx, |this, cx| {
5674                    this.activate_toolchain(worktree_id, toolchain, cx)
5675                })?
5676                .await;
5677        }
5678        let mut project_paths_to_open = vec![];
5679        let mut project_path_errors = vec![];
5680
5681        for path in paths {
5682            let result = cx
5683                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5684                .await;
5685            match result {
5686                Ok((_, project_path)) => {
5687                    project_paths_to_open.push((path.clone(), Some(project_path)));
5688                }
5689                Err(error) => {
5690                    project_path_errors.push(error);
5691                }
5692            };
5693        }
5694
5695        if project_paths_to_open.is_empty() {
5696            return Err(project_path_errors
5697                .pop()
5698                .unwrap_or_else(|| anyhow!("no paths given")));
5699        }
5700
5701        cx.update_window(window.into(), |_, cx| {
5702            cx.replace_root_view(|cx| {
5703                let mut workspace =
5704                    Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5705
5706                workspace
5707                    .client()
5708                    .telemetry()
5709                    .report_app_event("open ssh project".to_string());
5710
5711                workspace.set_serialized_ssh_project(serialized_ssh_project);
5712                workspace
5713            });
5714        })?;
5715
5716        window
5717            .update(&mut cx, |_, cx| {
5718                cx.activate_window();
5719
5720                open_items(serialized_workspace, project_paths_to_open, cx)
5721            })?
5722            .await?;
5723
5724        window.update(&mut cx, |workspace, cx| {
5725            for error in project_path_errors {
5726                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5727                    if let Some(path) = error.error_tag("path") {
5728                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5729                    }
5730                } else {
5731                    workspace.show_error(&error, cx)
5732                }
5733            }
5734        })
5735    })
5736}
5737
5738fn serialize_ssh_project(
5739    connection_options: SshConnectionOptions,
5740    paths: Vec<PathBuf>,
5741    cx: &AsyncAppContext,
5742) -> Task<
5743    Result<(
5744        SerializedSshProject,
5745        WorkspaceId,
5746        Option<SerializedWorkspace>,
5747    )>,
5748> {
5749    cx.background_executor().spawn(async move {
5750        let serialized_ssh_project = persistence::DB
5751            .get_or_create_ssh_project(
5752                connection_options.host.clone(),
5753                connection_options.port,
5754                paths
5755                    .iter()
5756                    .map(|path| path.to_string_lossy().to_string())
5757                    .collect::<Vec<_>>(),
5758                connection_options.username.clone(),
5759            )
5760            .await?;
5761
5762        let serialized_workspace =
5763            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5764
5765        let workspace_id = if let Some(workspace_id) =
5766            serialized_workspace.as_ref().map(|workspace| workspace.id)
5767        {
5768            workspace_id
5769        } else {
5770            persistence::DB.next_id().await?
5771        };
5772
5773        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5774    })
5775}
5776
5777pub fn join_in_room_project(
5778    project_id: u64,
5779    follow_user_id: u64,
5780    app_state: Arc<AppState>,
5781    cx: &mut AppContext,
5782) -> Task<Result<()>> {
5783    let windows = cx.windows();
5784    cx.spawn(|mut cx| async move {
5785        let existing_workspace = windows.into_iter().find_map(|window| {
5786            window.downcast::<Workspace>().and_then(|window| {
5787                window
5788                    .update(&mut cx, |workspace, cx| {
5789                        if workspace.project().read(cx).remote_id() == Some(project_id) {
5790                            Some(window)
5791                        } else {
5792                            None
5793                        }
5794                    })
5795                    .unwrap_or(None)
5796            })
5797        });
5798
5799        let workspace = if let Some(existing_workspace) = existing_workspace {
5800            existing_workspace
5801        } else {
5802            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5803            let room = active_call
5804                .read_with(&cx, |call, _| call.room().cloned())?
5805                .ok_or_else(|| anyhow!("not in a call"))?;
5806            let project = room
5807                .update(&mut cx, |room, cx| {
5808                    room.join_project(
5809                        project_id,
5810                        app_state.languages.clone(),
5811                        app_state.fs.clone(),
5812                        cx,
5813                    )
5814                })?
5815                .await?;
5816
5817            let window_bounds_override = window_bounds_env_override();
5818            cx.update(|cx| {
5819                let mut options = (app_state.build_window_options)(None, cx);
5820                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5821                cx.open_window(options, |cx| {
5822                    cx.new_view(|cx| {
5823                        Workspace::new(Default::default(), project, app_state.clone(), cx)
5824                    })
5825                })
5826            })??
5827        };
5828
5829        workspace.update(&mut cx, |workspace, cx| {
5830            cx.activate(true);
5831            cx.activate_window();
5832
5833            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5834                let follow_peer_id = room
5835                    .read(cx)
5836                    .remote_participants()
5837                    .iter()
5838                    .find(|(_, participant)| participant.user.id == follow_user_id)
5839                    .map(|(_, p)| p.peer_id)
5840                    .or_else(|| {
5841                        // If we couldn't follow the given user, follow the host instead.
5842                        let collaborator = workspace
5843                            .project()
5844                            .read(cx)
5845                            .collaborators()
5846                            .values()
5847                            .find(|collaborator| collaborator.is_host)?;
5848                        Some(collaborator.peer_id)
5849                    });
5850
5851                if let Some(follow_peer_id) = follow_peer_id {
5852                    workspace.follow(follow_peer_id, cx);
5853                }
5854            }
5855        })?;
5856
5857        anyhow::Ok(())
5858    })
5859}
5860
5861pub fn reload(reload: &Reload, cx: &mut AppContext) {
5862    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5863    let mut workspace_windows = cx
5864        .windows()
5865        .into_iter()
5866        .filter_map(|window| window.downcast::<Workspace>())
5867        .collect::<Vec<_>>();
5868
5869    // If multiple windows have unsaved changes, and need a save prompt,
5870    // prompt in the active window before switching to a different window.
5871    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5872
5873    let mut prompt = None;
5874    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5875        prompt = window
5876            .update(cx, |_, cx| {
5877                cx.prompt(
5878                    PromptLevel::Info,
5879                    "Are you sure you want to restart?",
5880                    None,
5881                    &["Restart", "Cancel"],
5882                )
5883            })
5884            .ok();
5885    }
5886
5887    let binary_path = reload.binary_path.clone();
5888    cx.spawn(|mut cx| async move {
5889        if let Some(prompt) = prompt {
5890            let answer = prompt.await?;
5891            if answer != 0 {
5892                return Ok(());
5893            }
5894        }
5895
5896        // If the user cancels any save prompt, then keep the app open.
5897        for window in workspace_windows {
5898            if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5899                workspace.prepare_to_close(CloseIntent::Quit, cx)
5900            }) {
5901                if !should_close.await? {
5902                    return Ok(());
5903                }
5904            }
5905        }
5906
5907        cx.update(|cx| cx.restart(binary_path))
5908    })
5909    .detach_and_log_err(cx);
5910}
5911
5912fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5913    let mut parts = value.split(',');
5914    let x: usize = parts.next()?.parse().ok()?;
5915    let y: usize = parts.next()?.parse().ok()?;
5916    Some(point(px(x as f32), px(y as f32)))
5917}
5918
5919fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5920    let mut parts = value.split(',');
5921    let width: usize = parts.next()?.parse().ok()?;
5922    let height: usize = parts.next()?.parse().ok()?;
5923    Some(size(px(width as f32), px(height as f32)))
5924}
5925
5926pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5927    const BORDER_SIZE: Pixels = px(1.0);
5928    let decorations = cx.window_decorations();
5929
5930    if matches!(decorations, Decorations::Client { .. }) {
5931        cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5932    }
5933
5934    struct GlobalResizeEdge(ResizeEdge);
5935    impl Global for GlobalResizeEdge {}
5936
5937    div()
5938        .id("window-backdrop")
5939        .bg(transparent_black())
5940        .map(|div| match decorations {
5941            Decorations::Server => div,
5942            Decorations::Client { tiling, .. } => div
5943                .when(!(tiling.top || tiling.right), |div| {
5944                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5945                })
5946                .when(!(tiling.top || tiling.left), |div| {
5947                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5948                })
5949                .when(!(tiling.bottom || tiling.right), |div| {
5950                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5951                })
5952                .when(!(tiling.bottom || tiling.left), |div| {
5953                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5954                })
5955                .when(!tiling.top, |div| {
5956                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
5957                })
5958                .when(!tiling.bottom, |div| {
5959                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
5960                })
5961                .when(!tiling.left, |div| {
5962                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
5963                })
5964                .when(!tiling.right, |div| {
5965                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
5966                })
5967                .on_mouse_move(move |e, cx| {
5968                    let size = cx.window_bounds().get_bounds().size;
5969                    let pos = e.position;
5970
5971                    let new_edge =
5972                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
5973
5974                    let edge = cx.try_global::<GlobalResizeEdge>();
5975                    if new_edge != edge.map(|edge| edge.0) {
5976                        cx.window_handle()
5977                            .update(cx, |workspace, cx| cx.notify(Some(workspace.entity_id())))
5978                            .ok();
5979                    }
5980                })
5981                .on_mouse_down(MouseButton::Left, move |e, cx| {
5982                    let size = cx.window_bounds().get_bounds().size;
5983                    let pos = e.position;
5984
5985                    let edge = match resize_edge(
5986                        pos,
5987                        theme::CLIENT_SIDE_DECORATION_SHADOW,
5988                        size,
5989                        tiling,
5990                    ) {
5991                        Some(value) => value,
5992                        None => return,
5993                    };
5994
5995                    cx.start_window_resize(edge);
5996                }),
5997        })
5998        .size_full()
5999        .child(
6000            div()
6001                .cursor(CursorStyle::Arrow)
6002                .map(|div| match decorations {
6003                    Decorations::Server => div,
6004                    Decorations::Client { tiling } => div
6005                        .border_color(cx.theme().colors().border)
6006                        .when(!(tiling.top || tiling.right), |div| {
6007                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6008                        })
6009                        .when(!(tiling.top || tiling.left), |div| {
6010                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6011                        })
6012                        .when(!(tiling.bottom || tiling.right), |div| {
6013                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6014                        })
6015                        .when(!(tiling.bottom || tiling.left), |div| {
6016                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
6017                        })
6018                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
6019                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
6020                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
6021                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
6022                        .when(!tiling.is_tiled(), |div| {
6023                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
6024                                color: Hsla {
6025                                    h: 0.,
6026                                    s: 0.,
6027                                    l: 0.,
6028                                    a: 0.4,
6029                                },
6030                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
6031                                spread_radius: px(0.),
6032                                offset: point(px(0.0), px(0.0)),
6033                            }])
6034                        }),
6035                })
6036                .on_mouse_move(|_e, cx| {
6037                    cx.stop_propagation();
6038                })
6039                .size_full()
6040                .child(element),
6041        )
6042        .map(|div| match decorations {
6043            Decorations::Server => div,
6044            Decorations::Client { tiling, .. } => div.child(
6045                canvas(
6046                    |_bounds, cx| {
6047                        cx.insert_hitbox(
6048                            Bounds::new(
6049                                point(px(0.0), px(0.0)),
6050                                cx.window_bounds().get_bounds().size,
6051                            ),
6052                            false,
6053                        )
6054                    },
6055                    move |_bounds, hitbox, cx| {
6056                        let mouse = cx.mouse_position();
6057                        let size = cx.window_bounds().get_bounds().size;
6058                        let Some(edge) =
6059                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
6060                        else {
6061                            return;
6062                        };
6063                        cx.set_global(GlobalResizeEdge(edge));
6064                        cx.set_cursor_style(
6065                            match edge {
6066                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
6067                                ResizeEdge::Left | ResizeEdge::Right => {
6068                                    CursorStyle::ResizeLeftRight
6069                                }
6070                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
6071                                    CursorStyle::ResizeUpLeftDownRight
6072                                }
6073                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
6074                                    CursorStyle::ResizeUpRightDownLeft
6075                                }
6076                            },
6077                            &hitbox,
6078                        );
6079                    },
6080                )
6081                .size_full()
6082                .absolute(),
6083            ),
6084        })
6085}
6086
6087fn resize_edge(
6088    pos: Point<Pixels>,
6089    shadow_size: Pixels,
6090    window_size: Size<Pixels>,
6091    tiling: Tiling,
6092) -> Option<ResizeEdge> {
6093    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
6094    if bounds.contains(&pos) {
6095        return None;
6096    }
6097
6098    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
6099    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
6100    if !tiling.top && top_left_bounds.contains(&pos) {
6101        return Some(ResizeEdge::TopLeft);
6102    }
6103
6104    let top_right_bounds = Bounds::new(
6105        Point::new(window_size.width - corner_size.width, px(0.)),
6106        corner_size,
6107    );
6108    if !tiling.top && top_right_bounds.contains(&pos) {
6109        return Some(ResizeEdge::TopRight);
6110    }
6111
6112    let bottom_left_bounds = Bounds::new(
6113        Point::new(px(0.), window_size.height - corner_size.height),
6114        corner_size,
6115    );
6116    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
6117        return Some(ResizeEdge::BottomLeft);
6118    }
6119
6120    let bottom_right_bounds = Bounds::new(
6121        Point::new(
6122            window_size.width - corner_size.width,
6123            window_size.height - corner_size.height,
6124        ),
6125        corner_size,
6126    );
6127    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6128        return Some(ResizeEdge::BottomRight);
6129    }
6130
6131    if !tiling.top && pos.y < shadow_size {
6132        Some(ResizeEdge::Top)
6133    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6134        Some(ResizeEdge::Bottom)
6135    } else if !tiling.left && pos.x < shadow_size {
6136        Some(ResizeEdge::Left)
6137    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6138        Some(ResizeEdge::Right)
6139    } else {
6140        None
6141    }
6142}
6143
6144fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext) {
6145    if pane == active_pane {
6146        return;
6147    } else if pane.read(cx).items_len() == 0 {
6148        pane.update(cx, |_, cx| {
6149            cx.emit(pane::Event::Remove {
6150                focus_on_pane: None,
6151            });
6152        })
6153    } else {
6154        move_all_items(pane, active_pane, cx);
6155    }
6156}
6157
6158fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext) {
6159    let destination_is_different = from_pane != to_pane;
6160    let mut moved_items = 0;
6161    for (item_ix, item_handle) in from_pane
6162        .read(cx)
6163        .items()
6164        .enumerate()
6165        .map(|(ix, item)| (ix, item.clone()))
6166        .collect::<Vec<_>>()
6167    {
6168        let ix = item_ix - moved_items;
6169        if destination_is_different {
6170            // Close item from previous pane
6171            from_pane.update(cx, |source, cx| {
6172                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6173            });
6174            moved_items += 1;
6175        }
6176
6177        // This automatically removes duplicate items in the pane
6178        to_pane.update(cx, |destination, cx| {
6179            destination.add_item(item_handle, true, true, None, cx);
6180            destination.focus(cx)
6181        });
6182    }
6183}
6184
6185pub fn move_item(
6186    source: &View<Pane>,
6187    destination: &View<Pane>,
6188    item_id_to_move: EntityId,
6189    destination_index: usize,
6190    cx: &mut WindowContext,
6191) {
6192    let Some((item_ix, item_handle)) = source
6193        .read(cx)
6194        .items()
6195        .enumerate()
6196        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6197        .map(|(ix, item)| (ix, item.clone()))
6198    else {
6199        // Tab was closed during drag
6200        return;
6201    };
6202
6203    if source != destination {
6204        // Close item from previous pane
6205        source.update(cx, |source, cx| {
6206            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6207        });
6208    }
6209
6210    // This automatically removes duplicate items in the pane
6211    destination.update(cx, |destination, cx| {
6212        destination.add_item(item_handle, true, true, Some(destination_index), cx);
6213        destination.focus(cx)
6214    });
6215}
6216
6217pub fn move_active_item(
6218    source: &View<Pane>,
6219    destination: &View<Pane>,
6220    focus_destination: bool,
6221    close_if_empty: bool,
6222    cx: &mut WindowContext,
6223) {
6224    if source == destination {
6225        return;
6226    }
6227    let Some(active_item) = source.read(cx).active_item() else {
6228        return;
6229    };
6230    source.update(cx, |source_pane, cx| {
6231        let item_id = active_item.item_id();
6232        source_pane.remove_item(item_id, false, close_if_empty, cx);
6233        destination.update(cx, |target_pane, cx| {
6234            target_pane.add_item(
6235                active_item,
6236                focus_destination,
6237                focus_destination,
6238                Some(target_pane.items_len()),
6239                cx,
6240            );
6241        });
6242    });
6243}
6244
6245#[cfg(test)]
6246mod tests {
6247    use std::{cell::RefCell, rc::Rc};
6248
6249    use super::*;
6250    use crate::{
6251        dock::{test::TestPanel, PanelEvent},
6252        item::{
6253            test::{TestItem, TestProjectItem},
6254            ItemEvent,
6255        },
6256    };
6257    use fs::FakeFs;
6258    use gpui::{
6259        px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6260        UpdateGlobal, VisualTestContext,
6261    };
6262    use project::{Project, ProjectEntryId};
6263    use serde_json::json;
6264    use settings::SettingsStore;
6265
6266    #[gpui::test]
6267    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6268        init_test(cx);
6269
6270        let fs = FakeFs::new(cx.executor());
6271        let project = Project::test(fs, [], cx).await;
6272        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6273
6274        // Adding an item with no ambiguity renders the tab without detail.
6275        let item1 = cx.new_view(|cx| {
6276            let mut item = TestItem::new(cx);
6277            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6278            item
6279        });
6280        workspace.update(cx, |workspace, cx| {
6281            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6282        });
6283        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6284
6285        // Adding an item that creates ambiguity increases the level of detail on
6286        // both tabs.
6287        let item2 = cx.new_view(|cx| {
6288            let mut item = TestItem::new(cx);
6289            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6290            item
6291        });
6292        workspace.update(cx, |workspace, cx| {
6293            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6294        });
6295        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6296        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6297
6298        // Adding an item that creates ambiguity increases the level of detail only
6299        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6300        // we stop at the highest detail available.
6301        let item3 = cx.new_view(|cx| {
6302            let mut item = TestItem::new(cx);
6303            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6304            item
6305        });
6306        workspace.update(cx, |workspace, cx| {
6307            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6308        });
6309        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6310        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6311        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6312    }
6313
6314    #[gpui::test]
6315    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6316        init_test(cx);
6317
6318        let fs = FakeFs::new(cx.executor());
6319        fs.insert_tree(
6320            "/root1",
6321            json!({
6322                "one.txt": "",
6323                "two.txt": "",
6324            }),
6325        )
6326        .await;
6327        fs.insert_tree(
6328            "/root2",
6329            json!({
6330                "three.txt": "",
6331            }),
6332        )
6333        .await;
6334
6335        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6336        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6337        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6338        let worktree_id = project.update(cx, |project, cx| {
6339            project.worktrees(cx).next().unwrap().read(cx).id()
6340        });
6341
6342        let item1 = cx.new_view(|cx| {
6343            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6344        });
6345        let item2 = cx.new_view(|cx| {
6346            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6347        });
6348
6349        // Add an item to an empty pane
6350        workspace.update(cx, |workspace, cx| {
6351            workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6352        });
6353        project.update(cx, |project, cx| {
6354            assert_eq!(
6355                project.active_entry(),
6356                project
6357                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6358                    .map(|e| e.id)
6359            );
6360        });
6361        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6362
6363        // Add a second item to a non-empty pane
6364        workspace.update(cx, |workspace, cx| {
6365            workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6366        });
6367        assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt"));
6368        project.update(cx, |project, cx| {
6369            assert_eq!(
6370                project.active_entry(),
6371                project
6372                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6373                    .map(|e| e.id)
6374            );
6375        });
6376
6377        // Close the active item
6378        pane.update(cx, |pane, cx| {
6379            pane.close_active_item(&Default::default(), cx).unwrap()
6380        })
6381        .await
6382        .unwrap();
6383        assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt"));
6384        project.update(cx, |project, cx| {
6385            assert_eq!(
6386                project.active_entry(),
6387                project
6388                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6389                    .map(|e| e.id)
6390            );
6391        });
6392
6393        // Add a project folder
6394        project
6395            .update(cx, |project, cx| {
6396                project.find_or_create_worktree("root2", true, cx)
6397            })
6398            .await
6399            .unwrap();
6400        assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt"));
6401
6402        // Remove a project folder
6403        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6404        assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt"));
6405    }
6406
6407    #[gpui::test]
6408    async fn test_close_window(cx: &mut TestAppContext) {
6409        init_test(cx);
6410
6411        let fs = FakeFs::new(cx.executor());
6412        fs.insert_tree("/root", json!({ "one": "" })).await;
6413
6414        let project = Project::test(fs, ["root".as_ref()], cx).await;
6415        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6416
6417        // When there are no dirty items, there's nothing to do.
6418        let item1 = cx.new_view(TestItem::new);
6419        workspace.update(cx, |w, cx| {
6420            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6421        });
6422        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6423        assert!(task.await.unwrap());
6424
6425        // When there are dirty untitled items, prompt to save each one. If the user
6426        // cancels any prompt, then abort.
6427        let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6428        let item3 = cx.new_view(|cx| {
6429            TestItem::new(cx)
6430                .with_dirty(true)
6431                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6432        });
6433        workspace.update(cx, |w, cx| {
6434            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6435            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6436        });
6437        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6438        cx.executor().run_until_parked();
6439        cx.simulate_prompt_answer(2); // cancel save all
6440        cx.executor().run_until_parked();
6441        cx.simulate_prompt_answer(2); // cancel save all
6442        cx.executor().run_until_parked();
6443        assert!(!cx.has_pending_prompt());
6444        assert!(!task.await.unwrap());
6445    }
6446
6447    #[gpui::test]
6448    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6449        init_test(cx);
6450
6451        // Register TestItem as a serializable item
6452        cx.update(|cx| {
6453            register_serializable_item::<TestItem>(cx);
6454        });
6455
6456        let fs = FakeFs::new(cx.executor());
6457        fs.insert_tree("/root", json!({ "one": "" })).await;
6458
6459        let project = Project::test(fs, ["root".as_ref()], cx).await;
6460        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6461
6462        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6463        let item1 = cx.new_view(|cx| {
6464            TestItem::new(cx)
6465                .with_dirty(true)
6466                .with_serialize(|| Some(Task::ready(Ok(()))))
6467        });
6468        let item2 = cx.new_view(|cx| {
6469            TestItem::new(cx)
6470                .with_dirty(true)
6471                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6472                .with_serialize(|| Some(Task::ready(Ok(()))))
6473        });
6474        workspace.update(cx, |w, cx| {
6475            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6476            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6477        });
6478        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6479        assert!(task.await.unwrap());
6480    }
6481
6482    #[gpui::test]
6483    async fn test_close_pane_items(cx: &mut TestAppContext) {
6484        init_test(cx);
6485
6486        let fs = FakeFs::new(cx.executor());
6487
6488        let project = Project::test(fs, None, cx).await;
6489        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6490
6491        let item1 = cx.new_view(|cx| {
6492            TestItem::new(cx)
6493                .with_dirty(true)
6494                .with_project_items(&[dirty_project_item(1, "1.txt", cx)])
6495        });
6496        let item2 = cx.new_view(|cx| {
6497            TestItem::new(cx)
6498                .with_dirty(true)
6499                .with_conflict(true)
6500                .with_project_items(&[dirty_project_item(2, "2.txt", cx)])
6501        });
6502        let item3 = cx.new_view(|cx| {
6503            TestItem::new(cx)
6504                .with_dirty(true)
6505                .with_conflict(true)
6506                .with_project_items(&[dirty_project_item(3, "3.txt", cx)])
6507        });
6508        let item4 = cx.new_view(|cx| {
6509            TestItem::new(cx).with_dirty(true).with_project_items(&[{
6510                let project_item = TestProjectItem::new_untitled(cx);
6511                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6512                project_item
6513            }])
6514        });
6515        let pane = workspace.update(cx, |workspace, cx| {
6516            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6517            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6518            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6519            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6520            workspace.active_pane().clone()
6521        });
6522
6523        let close_items = pane.update(cx, |pane, cx| {
6524            pane.activate_item(1, true, true, cx);
6525            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6526            let item1_id = item1.item_id();
6527            let item3_id = item3.item_id();
6528            let item4_id = item4.item_id();
6529            pane.close_items(cx, SaveIntent::Close, move |id| {
6530                [item1_id, item3_id, item4_id].contains(&id)
6531            })
6532        });
6533        cx.executor().run_until_parked();
6534
6535        assert!(cx.has_pending_prompt());
6536        // Ignore "Save all" prompt
6537        cx.simulate_prompt_answer(2);
6538        cx.executor().run_until_parked();
6539        // There's a prompt to save item 1.
6540        pane.update(cx, |pane, _| {
6541            assert_eq!(pane.items_len(), 4);
6542            assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6543        });
6544        // Confirm saving item 1.
6545        cx.simulate_prompt_answer(0);
6546        cx.executor().run_until_parked();
6547
6548        // Item 1 is saved. There's a prompt to save item 3.
6549        pane.update(cx, |pane, cx| {
6550            assert_eq!(item1.read(cx).save_count, 1);
6551            assert_eq!(item1.read(cx).save_as_count, 0);
6552            assert_eq!(item1.read(cx).reload_count, 0);
6553            assert_eq!(pane.items_len(), 3);
6554            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6555        });
6556        assert!(cx.has_pending_prompt());
6557
6558        // Cancel saving item 3.
6559        cx.simulate_prompt_answer(1);
6560        cx.executor().run_until_parked();
6561
6562        // Item 3 is reloaded. There's a prompt to save item 4.
6563        pane.update(cx, |pane, cx| {
6564            assert_eq!(item3.read(cx).save_count, 0);
6565            assert_eq!(item3.read(cx).save_as_count, 0);
6566            assert_eq!(item3.read(cx).reload_count, 1);
6567            assert_eq!(pane.items_len(), 2);
6568            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6569        });
6570        assert!(cx.has_pending_prompt());
6571
6572        // Confirm saving item 4.
6573        cx.simulate_prompt_answer(0);
6574        cx.executor().run_until_parked();
6575
6576        // There's a prompt for a path for item 4.
6577        cx.simulate_new_path_selection(|_| Some(Default::default()));
6578        close_items.await.unwrap();
6579
6580        // The requested items are closed.
6581        pane.update(cx, |pane, cx| {
6582            assert_eq!(item4.read(cx).save_count, 0);
6583            assert_eq!(item4.read(cx).save_as_count, 1);
6584            assert_eq!(item4.read(cx).reload_count, 0);
6585            assert_eq!(pane.items_len(), 1);
6586            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6587        });
6588    }
6589
6590    #[gpui::test]
6591    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6592        init_test(cx);
6593
6594        let fs = FakeFs::new(cx.executor());
6595        let project = Project::test(fs, [], cx).await;
6596        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6597
6598        // Create several workspace items with single project entries, and two
6599        // workspace items with multiple project entries.
6600        let single_entry_items = (0..=4)
6601            .map(|project_entry_id| {
6602                cx.new_view(|cx| {
6603                    TestItem::new(cx)
6604                        .with_dirty(true)
6605                        .with_project_items(&[dirty_project_item(
6606                            project_entry_id,
6607                            &format!("{project_entry_id}.txt"),
6608                            cx,
6609                        )])
6610                })
6611            })
6612            .collect::<Vec<_>>();
6613        let item_2_3 = cx.new_view(|cx| {
6614            TestItem::new(cx)
6615                .with_dirty(true)
6616                .with_singleton(false)
6617                .with_project_items(&[
6618                    single_entry_items[2].read(cx).project_items[0].clone(),
6619                    single_entry_items[3].read(cx).project_items[0].clone(),
6620                ])
6621        });
6622        let item_3_4 = cx.new_view(|cx| {
6623            TestItem::new(cx)
6624                .with_dirty(true)
6625                .with_singleton(false)
6626                .with_project_items(&[
6627                    single_entry_items[3].read(cx).project_items[0].clone(),
6628                    single_entry_items[4].read(cx).project_items[0].clone(),
6629                ])
6630        });
6631
6632        // Create two panes that contain the following project entries:
6633        //   left pane:
6634        //     multi-entry items:   (2, 3)
6635        //     single-entry items:  0, 1, 2, 3, 4
6636        //   right pane:
6637        //     single-entry items:  1
6638        //     multi-entry items:   (3, 4)
6639        let left_pane = workspace.update(cx, |workspace, cx| {
6640            let left_pane = workspace.active_pane().clone();
6641            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6642            for item in single_entry_items {
6643                workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6644            }
6645            left_pane.update(cx, |pane, cx| {
6646                pane.activate_item(2, true, true, cx);
6647            });
6648
6649            let right_pane = workspace
6650                .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6651                .unwrap();
6652
6653            right_pane.update(cx, |pane, cx| {
6654                pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6655            });
6656
6657            left_pane
6658        });
6659
6660        cx.focus_view(&left_pane);
6661
6662        // When closing all of the items in the left pane, we should be prompted twice:
6663        // once for project entry 0, and once for project entry 2. Project entries 1,
6664        // 3, and 4 are all still open in the other paten. After those two
6665        // prompts, the task should complete.
6666
6667        let close = left_pane.update(cx, |pane, cx| {
6668            pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6669        });
6670        cx.executor().run_until_parked();
6671
6672        // Discard "Save all" prompt
6673        cx.simulate_prompt_answer(2);
6674
6675        cx.executor().run_until_parked();
6676        left_pane.update(cx, |pane, cx| {
6677            assert_eq!(
6678                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6679                &[ProjectEntryId::from_proto(0)]
6680            );
6681        });
6682        cx.simulate_prompt_answer(0);
6683
6684        cx.executor().run_until_parked();
6685        left_pane.update(cx, |pane, cx| {
6686            assert_eq!(
6687                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6688                &[ProjectEntryId::from_proto(2)]
6689            );
6690        });
6691        cx.simulate_prompt_answer(0);
6692
6693        cx.executor().run_until_parked();
6694        close.await.unwrap();
6695        left_pane.update(cx, |pane, _| {
6696            assert_eq!(pane.items_len(), 0);
6697        });
6698    }
6699
6700    #[gpui::test]
6701    async fn test_autosave(cx: &mut gpui::TestAppContext) {
6702        init_test(cx);
6703
6704        let fs = FakeFs::new(cx.executor());
6705        let project = Project::test(fs, [], cx).await;
6706        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6707        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6708
6709        let item = cx.new_view(|cx| {
6710            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6711        });
6712        let item_id = item.entity_id();
6713        workspace.update(cx, |workspace, cx| {
6714            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6715        });
6716
6717        // Autosave on window change.
6718        item.update(cx, |item, cx| {
6719            SettingsStore::update_global(cx, |settings, cx| {
6720                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6721                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
6722                })
6723            });
6724            item.is_dirty = true;
6725        });
6726
6727        // Deactivating the window saves the file.
6728        cx.deactivate_window();
6729        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6730
6731        // Re-activating the window doesn't save the file.
6732        cx.update(|cx| cx.activate_window());
6733        cx.executor().run_until_parked();
6734        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6735
6736        // Autosave on focus change.
6737        item.update(cx, |item, cx| {
6738            cx.focus_self();
6739            SettingsStore::update_global(cx, |settings, cx| {
6740                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6741                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6742                })
6743            });
6744            item.is_dirty = true;
6745        });
6746
6747        // Blurring the item saves the file.
6748        item.update(cx, |_, cx| cx.blur());
6749        cx.executor().run_until_parked();
6750        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6751
6752        // Deactivating the window still saves the file.
6753        item.update(cx, |item, cx| {
6754            cx.focus_self();
6755            item.is_dirty = true;
6756        });
6757        cx.deactivate_window();
6758        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6759
6760        // Autosave after delay.
6761        item.update(cx, |item, cx| {
6762            SettingsStore::update_global(cx, |settings, cx| {
6763                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6764                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6765                })
6766            });
6767            item.is_dirty = true;
6768            cx.emit(ItemEvent::Edit);
6769        });
6770
6771        // Delay hasn't fully expired, so the file is still dirty and unsaved.
6772        cx.executor().advance_clock(Duration::from_millis(250));
6773        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6774
6775        // After delay expires, the file is saved.
6776        cx.executor().advance_clock(Duration::from_millis(250));
6777        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6778
6779        // Autosave on focus change, ensuring closing the tab counts as such.
6780        item.update(cx, |item, cx| {
6781            SettingsStore::update_global(cx, |settings, cx| {
6782                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6783                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6784                })
6785            });
6786            item.is_dirty = true;
6787            for project_item in &mut item.project_items {
6788                project_item.update(cx, |project_item, _| project_item.is_dirty = true);
6789            }
6790        });
6791
6792        pane.update(cx, |pane, cx| {
6793            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6794        })
6795        .await
6796        .unwrap();
6797        assert!(!cx.has_pending_prompt());
6798        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6799
6800        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6801        workspace.update(cx, |workspace, cx| {
6802            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6803        });
6804        item.update(cx, |item, cx| {
6805            item.project_items[0].update(cx, |item, _| {
6806                item.entry_id = None;
6807            });
6808            item.is_dirty = true;
6809            cx.blur();
6810        });
6811        cx.run_until_parked();
6812        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6813
6814        // Ensure autosave is prevented for deleted files also when closing the buffer.
6815        let _close_items = pane.update(cx, |pane, cx| {
6816            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6817        });
6818        cx.run_until_parked();
6819        assert!(cx.has_pending_prompt());
6820        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6821    }
6822
6823    #[gpui::test]
6824    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6825        init_test(cx);
6826
6827        let fs = FakeFs::new(cx.executor());
6828
6829        let project = Project::test(fs, [], cx).await;
6830        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6831
6832        let item = cx.new_view(|cx| {
6833            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6834        });
6835        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6836        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6837        let toolbar_notify_count = Rc::new(RefCell::new(0));
6838
6839        workspace.update(cx, |workspace, cx| {
6840            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6841            let toolbar_notification_count = toolbar_notify_count.clone();
6842            cx.observe(&toolbar, move |_, _, _| {
6843                *toolbar_notification_count.borrow_mut() += 1
6844            })
6845            .detach();
6846        });
6847
6848        pane.update(cx, |pane, _| {
6849            assert!(!pane.can_navigate_backward());
6850            assert!(!pane.can_navigate_forward());
6851        });
6852
6853        item.update(cx, |item, cx| {
6854            item.set_state("one".to_string(), cx);
6855        });
6856
6857        // Toolbar must be notified to re-render the navigation buttons
6858        assert_eq!(*toolbar_notify_count.borrow(), 1);
6859
6860        pane.update(cx, |pane, _| {
6861            assert!(pane.can_navigate_backward());
6862            assert!(!pane.can_navigate_forward());
6863        });
6864
6865        workspace
6866            .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6867            .await
6868            .unwrap();
6869
6870        assert_eq!(*toolbar_notify_count.borrow(), 2);
6871        pane.update(cx, |pane, _| {
6872            assert!(!pane.can_navigate_backward());
6873            assert!(pane.can_navigate_forward());
6874        });
6875    }
6876
6877    #[gpui::test]
6878    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6879        init_test(cx);
6880        let fs = FakeFs::new(cx.executor());
6881
6882        let project = Project::test(fs, [], cx).await;
6883        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6884
6885        let panel = workspace.update(cx, |workspace, cx| {
6886            let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6887            workspace.add_panel(panel.clone(), cx);
6888
6889            workspace
6890                .right_dock()
6891                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6892
6893            panel
6894        });
6895
6896        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6897        pane.update(cx, |pane, cx| {
6898            let item = cx.new_view(TestItem::new);
6899            pane.add_item(Box::new(item), true, true, None, cx);
6900        });
6901
6902        // Transfer focus from center to panel
6903        workspace.update(cx, |workspace, cx| {
6904            workspace.toggle_panel_focus::<TestPanel>(cx);
6905        });
6906
6907        workspace.update(cx, |workspace, cx| {
6908            assert!(workspace.right_dock().read(cx).is_open());
6909            assert!(!panel.is_zoomed(cx));
6910            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6911        });
6912
6913        // Transfer focus from panel to center
6914        workspace.update(cx, |workspace, cx| {
6915            workspace.toggle_panel_focus::<TestPanel>(cx);
6916        });
6917
6918        workspace.update(cx, |workspace, cx| {
6919            assert!(workspace.right_dock().read(cx).is_open());
6920            assert!(!panel.is_zoomed(cx));
6921            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6922        });
6923
6924        // Close the dock
6925        workspace.update(cx, |workspace, cx| {
6926            workspace.toggle_dock(DockPosition::Right, cx);
6927        });
6928
6929        workspace.update(cx, |workspace, cx| {
6930            assert!(!workspace.right_dock().read(cx).is_open());
6931            assert!(!panel.is_zoomed(cx));
6932            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6933        });
6934
6935        // Open the dock
6936        workspace.update(cx, |workspace, cx| {
6937            workspace.toggle_dock(DockPosition::Right, cx);
6938        });
6939
6940        workspace.update(cx, |workspace, cx| {
6941            assert!(workspace.right_dock().read(cx).is_open());
6942            assert!(!panel.is_zoomed(cx));
6943            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6944        });
6945
6946        // Focus and zoom panel
6947        panel.update(cx, |panel, cx| {
6948            cx.focus_self();
6949            panel.set_zoomed(true, cx)
6950        });
6951
6952        workspace.update(cx, |workspace, cx| {
6953            assert!(workspace.right_dock().read(cx).is_open());
6954            assert!(panel.is_zoomed(cx));
6955            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6956        });
6957
6958        // Transfer focus to the center closes the dock
6959        workspace.update(cx, |workspace, cx| {
6960            workspace.toggle_panel_focus::<TestPanel>(cx);
6961        });
6962
6963        workspace.update(cx, |workspace, cx| {
6964            assert!(!workspace.right_dock().read(cx).is_open());
6965            assert!(panel.is_zoomed(cx));
6966            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6967        });
6968
6969        // Transferring focus back to the panel keeps it zoomed
6970        workspace.update(cx, |workspace, cx| {
6971            workspace.toggle_panel_focus::<TestPanel>(cx);
6972        });
6973
6974        workspace.update(cx, |workspace, cx| {
6975            assert!(workspace.right_dock().read(cx).is_open());
6976            assert!(panel.is_zoomed(cx));
6977            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6978        });
6979
6980        // Close the dock while it is zoomed
6981        workspace.update(cx, |workspace, cx| {
6982            workspace.toggle_dock(DockPosition::Right, cx)
6983        });
6984
6985        workspace.update(cx, |workspace, cx| {
6986            assert!(!workspace.right_dock().read(cx).is_open());
6987            assert!(panel.is_zoomed(cx));
6988            assert!(workspace.zoomed.is_none());
6989            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6990        });
6991
6992        // Opening the dock, when it's zoomed, retains focus
6993        workspace.update(cx, |workspace, cx| {
6994            workspace.toggle_dock(DockPosition::Right, cx)
6995        });
6996
6997        workspace.update(cx, |workspace, cx| {
6998            assert!(workspace.right_dock().read(cx).is_open());
6999            assert!(panel.is_zoomed(cx));
7000            assert!(workspace.zoomed.is_some());
7001            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
7002        });
7003
7004        // Unzoom and close the panel, zoom the active pane.
7005        panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
7006        workspace.update(cx, |workspace, cx| {
7007            workspace.toggle_dock(DockPosition::Right, cx)
7008        });
7009        pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
7010
7011        // Opening a dock unzooms the pane.
7012        workspace.update(cx, |workspace, cx| {
7013            workspace.toggle_dock(DockPosition::Right, cx)
7014        });
7015        workspace.update(cx, |workspace, cx| {
7016            let pane = pane.read(cx);
7017            assert!(!pane.is_zoomed());
7018            assert!(!pane.focus_handle(cx).is_focused(cx));
7019            assert!(workspace.right_dock().read(cx).is_open());
7020            assert!(workspace.zoomed.is_none());
7021        });
7022    }
7023
7024    #[gpui::test]
7025    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
7026        init_test(cx);
7027
7028        let fs = FakeFs::new(cx.executor());
7029
7030        let project = Project::test(fs, None, cx).await;
7031        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7032
7033        // Let's arrange the panes like this:
7034        //
7035        // +-----------------------+
7036        // |         top           |
7037        // +------+--------+-------+
7038        // | left | center | right |
7039        // +------+--------+-------+
7040        // |        bottom         |
7041        // +-----------------------+
7042
7043        let top_item = cx.new_view(|cx| {
7044            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
7045        });
7046        let bottom_item = cx.new_view(|cx| {
7047            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
7048        });
7049        let left_item = cx.new_view(|cx| {
7050            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
7051        });
7052        let right_item = cx.new_view(|cx| {
7053            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
7054        });
7055        let center_item = cx.new_view(|cx| {
7056            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
7057        });
7058
7059        let top_pane_id = workspace.update(cx, |workspace, cx| {
7060            let top_pane_id = workspace.active_pane().entity_id();
7061            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
7062            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
7063            top_pane_id
7064        });
7065        let bottom_pane_id = workspace.update(cx, |workspace, cx| {
7066            let bottom_pane_id = workspace.active_pane().entity_id();
7067            workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
7068            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
7069            bottom_pane_id
7070        });
7071        let left_pane_id = workspace.update(cx, |workspace, cx| {
7072            let left_pane_id = workspace.active_pane().entity_id();
7073            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
7074            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7075            left_pane_id
7076        });
7077        let right_pane_id = workspace.update(cx, |workspace, cx| {
7078            let right_pane_id = workspace.active_pane().entity_id();
7079            workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
7080            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
7081            right_pane_id
7082        });
7083        let center_pane_id = workspace.update(cx, |workspace, cx| {
7084            let center_pane_id = workspace.active_pane().entity_id();
7085            workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
7086            center_pane_id
7087        });
7088        cx.executor().run_until_parked();
7089
7090        workspace.update(cx, |workspace, cx| {
7091            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
7092
7093            // Join into next from center pane into right
7094            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7095        });
7096
7097        workspace.update(cx, |workspace, cx| {
7098            let active_pane = workspace.active_pane();
7099            assert_eq!(right_pane_id, active_pane.entity_id());
7100            assert_eq!(2, active_pane.read(cx).items_len());
7101            let item_ids_in_pane =
7102                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7103            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7104            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7105
7106            // Join into next from right pane into bottom
7107            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7108        });
7109
7110        workspace.update(cx, |workspace, cx| {
7111            let active_pane = workspace.active_pane();
7112            assert_eq!(bottom_pane_id, active_pane.entity_id());
7113            assert_eq!(3, active_pane.read(cx).items_len());
7114            let item_ids_in_pane =
7115                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7116            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7117            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7118            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7119
7120            // Join into next from bottom pane into left
7121            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7122        });
7123
7124        workspace.update(cx, |workspace, cx| {
7125            let active_pane = workspace.active_pane();
7126            assert_eq!(left_pane_id, active_pane.entity_id());
7127            assert_eq!(4, active_pane.read(cx).items_len());
7128            let item_ids_in_pane =
7129                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7130            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7131            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7132            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7133            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7134
7135            // Join into next from left pane into top
7136            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
7137        });
7138
7139        workspace.update(cx, |workspace, cx| {
7140            let active_pane = workspace.active_pane();
7141            assert_eq!(top_pane_id, active_pane.entity_id());
7142            assert_eq!(5, active_pane.read(cx).items_len());
7143            let item_ids_in_pane =
7144                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
7145            assert!(item_ids_in_pane.contains(&center_item.item_id()));
7146            assert!(item_ids_in_pane.contains(&right_item.item_id()));
7147            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
7148            assert!(item_ids_in_pane.contains(&left_item.item_id()));
7149            assert!(item_ids_in_pane.contains(&top_item.item_id()));
7150
7151            // Single pane left: no-op
7152            workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7153        });
7154
7155        workspace.update(cx, |workspace, _cx| {
7156            let active_pane = workspace.active_pane();
7157            assert_eq!(top_pane_id, active_pane.entity_id());
7158        });
7159    }
7160
7161    fn add_an_item_to_active_pane(
7162        cx: &mut VisualTestContext,
7163        workspace: &View<Workspace>,
7164        item_id: u64,
7165    ) -> View<TestItem> {
7166        let item = cx.new_view(|cx| {
7167            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7168                item_id,
7169                "item{item_id}.txt",
7170                cx,
7171            )])
7172        });
7173        workspace.update(cx, |workspace, cx| {
7174            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7175        });
7176        return item;
7177    }
7178
7179    fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7180        return workspace.update(cx, |workspace, cx| {
7181            let new_pane =
7182                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7183            new_pane
7184        });
7185    }
7186
7187    #[gpui::test]
7188    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7189        init_test(cx);
7190        let fs = FakeFs::new(cx.executor());
7191        let project = Project::test(fs, None, cx).await;
7192        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7193
7194        add_an_item_to_active_pane(cx, &workspace, 1);
7195        split_pane(cx, &workspace);
7196        add_an_item_to_active_pane(cx, &workspace, 2);
7197        split_pane(cx, &workspace); // empty pane
7198        split_pane(cx, &workspace);
7199        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7200
7201        cx.executor().run_until_parked();
7202
7203        workspace.update(cx, |workspace, cx| {
7204            let num_panes = workspace.panes().len();
7205            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7206            let active_item = workspace
7207                .active_pane()
7208                .read(cx)
7209                .active_item()
7210                .expect("item is in focus");
7211
7212            assert_eq!(num_panes, 4);
7213            assert_eq!(num_items_in_current_pane, 1);
7214            assert_eq!(active_item.item_id(), last_item.item_id());
7215        });
7216
7217        workspace.update(cx, |workspace, cx| {
7218            workspace.join_all_panes(cx);
7219        });
7220
7221        workspace.update(cx, |workspace, cx| {
7222            let num_panes = workspace.panes().len();
7223            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7224            let active_item = workspace
7225                .active_pane()
7226                .read(cx)
7227                .active_item()
7228                .expect("item is in focus");
7229
7230            assert_eq!(num_panes, 1);
7231            assert_eq!(num_items_in_current_pane, 3);
7232            assert_eq!(active_item.item_id(), last_item.item_id());
7233        });
7234    }
7235    struct TestModal(FocusHandle);
7236
7237    impl TestModal {
7238        fn new(cx: &mut ViewContext<Self>) -> Self {
7239            Self(cx.focus_handle())
7240        }
7241    }
7242
7243    impl EventEmitter<DismissEvent> for TestModal {}
7244
7245    impl FocusableView for TestModal {
7246        fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7247            self.0.clone()
7248        }
7249    }
7250
7251    impl ModalView for TestModal {}
7252
7253    impl Render for TestModal {
7254        fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7255            div().track_focus(&self.0)
7256        }
7257    }
7258
7259    #[gpui::test]
7260    async fn test_panels(cx: &mut gpui::TestAppContext) {
7261        init_test(cx);
7262        let fs = FakeFs::new(cx.executor());
7263
7264        let project = Project::test(fs, [], cx).await;
7265        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7266
7267        let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7268            let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7269            workspace.add_panel(panel_1.clone(), cx);
7270            workspace
7271                .left_dock()
7272                .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
7273            let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7274            workspace.add_panel(panel_2.clone(), cx);
7275            workspace
7276                .right_dock()
7277                .update(cx, |right_dock, cx| right_dock.set_open(true, 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}