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