workspace.rs

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