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