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