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
4545fn leader_border_for_pane(
4546    follower_states: &HashMap<PeerId, FollowerState>,
4547    pane: &View<Pane>,
4548    cx: &WindowContext,
4549) -> Option<Div> {
4550    let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| {
4551        if state.pane() == pane {
4552            Some((*leader_id, state))
4553        } else {
4554            None
4555        }
4556    })?;
4557
4558    let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx);
4559    let leader = room.remote_participant_for_peer_id(leader_id)?;
4560
4561    let mut leader_color = cx
4562        .theme()
4563        .players()
4564        .color_for_participant(leader.participant_index.0)
4565        .cursor;
4566    leader_color.fade_out(0.3);
4567    Some(
4568        div()
4569            .absolute()
4570            .size_full()
4571            .left_0()
4572            .top_0()
4573            .border_2()
4574            .border_color(leader_color),
4575    )
4576}
4577
4578fn window_bounds_env_override() -> Option<Bounds<Pixels>> {
4579    ZED_WINDOW_POSITION
4580        .zip(*ZED_WINDOW_SIZE)
4581        .map(|(position, size)| Bounds {
4582            origin: position,
4583            size,
4584        })
4585}
4586
4587fn open_items(
4588    serialized_workspace: Option<SerializedWorkspace>,
4589    mut project_paths_to_open: Vec<(PathBuf, Option<ProjectPath>)>,
4590    cx: &mut ViewContext<Workspace>,
4591) -> impl 'static + Future<Output = Result<Vec<Option<Result<Box<dyn ItemHandle>>>>>> {
4592    let restored_items = serialized_workspace.map(|serialized_workspace| {
4593        Workspace::load_workspace(
4594            serialized_workspace,
4595            project_paths_to_open
4596                .iter()
4597                .map(|(_, project_path)| project_path)
4598                .cloned()
4599                .collect(),
4600            cx,
4601        )
4602    });
4603
4604    cx.spawn(|workspace, mut cx| async move {
4605        let mut opened_items = Vec::with_capacity(project_paths_to_open.len());
4606
4607        if let Some(restored_items) = restored_items {
4608            let restored_items = restored_items.await?;
4609
4610            let restored_project_paths = restored_items
4611                .iter()
4612                .filter_map(|item| {
4613                    cx.update(|cx| item.as_ref()?.project_path(cx))
4614                        .ok()
4615                        .flatten()
4616                })
4617                .collect::<HashSet<_>>();
4618
4619            for restored_item in restored_items {
4620                opened_items.push(restored_item.map(Ok));
4621            }
4622
4623            project_paths_to_open
4624                .iter_mut()
4625                .for_each(|(_, project_path)| {
4626                    if let Some(project_path_to_open) = project_path {
4627                        if restored_project_paths.contains(project_path_to_open) {
4628                            *project_path = None;
4629                        }
4630                    }
4631                });
4632        } else {
4633            for _ in 0..project_paths_to_open.len() {
4634                opened_items.push(None);
4635            }
4636        }
4637        assert!(opened_items.len() == project_paths_to_open.len());
4638
4639        let tasks =
4640            project_paths_to_open
4641                .into_iter()
4642                .enumerate()
4643                .map(|(ix, (abs_path, project_path))| {
4644                    let workspace = workspace.clone();
4645                    cx.spawn(|mut cx| async move {
4646                        let file_project_path = project_path?;
4647                        let abs_path_task = workspace.update(&mut cx, |workspace, cx| {
4648                            workspace.project().update(cx, |project, cx| {
4649                                project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx)
4650                            })
4651                        });
4652
4653                        // We only want to open file paths here. If one of the items
4654                        // here is a directory, it was already opened further above
4655                        // with a `find_or_create_worktree`.
4656                        if let Ok(task) = abs_path_task {
4657                            if task.await.map_or(true, |p| p.is_file()) {
4658                                return Some((
4659                                    ix,
4660                                    workspace
4661                                        .update(&mut cx, |workspace, cx| {
4662                                            workspace.open_path(file_project_path, None, true, cx)
4663                                        })
4664                                        .log_err()?
4665                                        .await,
4666                                ));
4667                            }
4668                        }
4669                        None
4670                    })
4671                });
4672
4673        let tasks = tasks.collect::<Vec<_>>();
4674
4675        let tasks = futures::future::join_all(tasks);
4676        for (ix, path_open_result) in tasks.await.into_iter().flatten() {
4677            opened_items[ix] = Some(path_open_result);
4678        }
4679
4680        Ok(opened_items)
4681    })
4682}
4683
4684enum ActivateInDirectionTarget {
4685    Pane(View<Pane>),
4686    Dock(View<Dock>),
4687}
4688
4689fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
4690    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";
4691
4692    workspace
4693        .update(cx, |workspace, cx| {
4694            if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
4695                struct DatabaseFailedNotification;
4696
4697                workspace.show_notification_once(
4698                    NotificationId::unique::<DatabaseFailedNotification>(),
4699                    cx,
4700                    |cx| {
4701                        cx.new_view(|_| {
4702                            MessageNotification::new("Failed to load the database file.")
4703                                .with_click_message("File an issue")
4704                                .on_click(|cx| cx.open_url(REPORT_ISSUE_URL))
4705                        })
4706                    },
4707                );
4708            }
4709        })
4710        .log_err();
4711}
4712
4713impl FocusableView for Workspace {
4714    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4715        self.active_pane.focus_handle(cx)
4716    }
4717}
4718
4719#[derive(Clone, Render)]
4720struct DraggedDock(DockPosition);
4721
4722impl Render for Workspace {
4723    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4724        let mut context = KeyContext::new_with_defaults();
4725        context.add("Workspace");
4726        let centered_layout = self.centered_layout
4727            && self.center.panes().len() == 1
4728            && self.active_item(cx).is_some();
4729        let render_padding = |size| {
4730            (size > 0.0).then(|| {
4731                div()
4732                    .h_full()
4733                    .w(relative(size))
4734                    .bg(cx.theme().colors().editor_background)
4735                    .border_color(cx.theme().colors().pane_group_border)
4736            })
4737        };
4738        let paddings = if centered_layout {
4739            let settings = WorkspaceSettings::get_global(cx).centered_layout;
4740            (
4741                render_padding(Self::adjust_padding(settings.left_padding)),
4742                render_padding(Self::adjust_padding(settings.right_padding)),
4743            )
4744        } else {
4745            (None, None)
4746        };
4747        let ui_font = theme::setup_ui_font(cx);
4748
4749        let theme = cx.theme().clone();
4750        let colors = theme.colors();
4751
4752        client_side_decorations(
4753            self.actions(div(), cx)
4754                .key_context(context)
4755                .relative()
4756                .size_full()
4757                .flex()
4758                .flex_col()
4759                .font(ui_font)
4760                .gap_0()
4761                .justify_start()
4762                .items_start()
4763                .text_color(colors.text)
4764                .overflow_hidden()
4765                .children(self.titlebar_item.clone())
4766                .child(
4767                    div()
4768                        .size_full()
4769                        .relative()
4770                        .flex_1()
4771                        .flex()
4772                        .flex_col()
4773                        .child(
4774                            div()
4775                                .id("workspace")
4776                                .bg(colors.background)
4777                                .relative()
4778                                .flex_1()
4779                                .w_full()
4780                                .flex()
4781                                .flex_col()
4782                                .overflow_hidden()
4783                                .border_t_1()
4784                                .border_b_1()
4785                                .border_color(colors.border)
4786                                .child({
4787                                    let this = cx.view().clone();
4788                                    canvas(
4789                                        move |bounds, cx| {
4790                                            this.update(cx, |this, _cx| this.bounds = bounds)
4791                                        },
4792                                        |_, _, _| {},
4793                                    )
4794                                    .absolute()
4795                                    .size_full()
4796                                })
4797                                .when(self.zoomed.is_none(), |this| {
4798                                    this.on_drag_move(cx.listener(
4799                                        |workspace, e: &DragMoveEvent<DraggedDock>, cx| {
4800                                            match e.drag(cx).0 {
4801                                                DockPosition::Left => {
4802                                                    let size = e.event.position.x
4803                                                        - workspace.bounds.left();
4804                                                    workspace.left_dock.update(
4805                                                        cx,
4806                                                        |left_dock, cx| {
4807                                                            left_dock.resize_active_panel(
4808                                                                Some(size),
4809                                                                cx,
4810                                                            );
4811                                                        },
4812                                                    );
4813                                                }
4814                                                DockPosition::Right => {
4815                                                    let size = workspace.bounds.right()
4816                                                        - e.event.position.x;
4817                                                    workspace.right_dock.update(
4818                                                        cx,
4819                                                        |right_dock, cx| {
4820                                                            right_dock.resize_active_panel(
4821                                                                Some(size),
4822                                                                cx,
4823                                                            );
4824                                                        },
4825                                                    );
4826                                                }
4827                                                DockPosition::Bottom => {
4828                                                    let size = workspace.bounds.bottom()
4829                                                        - e.event.position.y;
4830                                                    workspace.bottom_dock.update(
4831                                                        cx,
4832                                                        |bottom_dock, cx| {
4833                                                            bottom_dock.resize_active_panel(
4834                                                                Some(size),
4835                                                                cx,
4836                                                            );
4837                                                        },
4838                                                    );
4839                                                }
4840                                            }
4841                                        },
4842                                    ))
4843                                })
4844                                .child(
4845                                    div()
4846                                        .flex()
4847                                        .flex_row()
4848                                        .h_full()
4849                                        // Left Dock
4850                                        .children(self.render_dock(
4851                                            DockPosition::Left,
4852                                            &self.left_dock,
4853                                            cx,
4854                                        ))
4855                                        // Panes
4856                                        .child(
4857                                            div()
4858                                                .flex()
4859                                                .flex_col()
4860                                                .flex_1()
4861                                                .overflow_hidden()
4862                                                .child(
4863                                                    h_flex()
4864                                                        .flex_1()
4865                                                        .when_some(paddings.0, |this, p| {
4866                                                            this.child(p.border_r_1())
4867                                                        })
4868                                                        .child(self.center.render(
4869                                                            &self.project,
4870                                                            &self.follower_states,
4871                                                            self.active_call(),
4872                                                            &self.active_pane,
4873                                                            self.zoomed.as_ref(),
4874                                                            &self.app_state,
4875                                                            cx,
4876                                                        ))
4877                                                        .when_some(paddings.1, |this, p| {
4878                                                            this.child(p.border_l_1())
4879                                                        }),
4880                                                )
4881                                                .children(self.render_dock(
4882                                                    DockPosition::Bottom,
4883                                                    &self.bottom_dock,
4884                                                    cx,
4885                                                )),
4886                                        )
4887                                        // Right Dock
4888                                        .children(self.render_dock(
4889                                            DockPosition::Right,
4890                                            &self.right_dock,
4891                                            cx,
4892                                        )),
4893                                )
4894                                .children(self.zoomed.as_ref().and_then(|view| {
4895                                    let zoomed_view = view.upgrade()?;
4896                                    let div = div()
4897                                        .occlude()
4898                                        .absolute()
4899                                        .overflow_hidden()
4900                                        .border_color(colors.border)
4901                                        .bg(colors.background)
4902                                        .child(zoomed_view)
4903                                        .inset_0()
4904                                        .shadow_lg();
4905
4906                                    Some(match self.zoomed_position {
4907                                        Some(DockPosition::Left) => div.right_2().border_r_1(),
4908                                        Some(DockPosition::Right) => div.left_2().border_l_1(),
4909                                        Some(DockPosition::Bottom) => div.top_2().border_t_1(),
4910                                        None => {
4911                                            div.top_2().bottom_2().left_2().right_2().border_1()
4912                                        }
4913                                    })
4914                                }))
4915                                .children(self.render_notifications(cx)),
4916                        )
4917                        .child(self.status_bar.clone())
4918                        .child(self.modal_layer.clone()),
4919                ),
4920            cx,
4921        )
4922    }
4923}
4924
4925impl WorkspaceStore {
4926    pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
4927        Self {
4928            workspaces: Default::default(),
4929            _subscriptions: vec![
4930                client.add_request_handler(cx.weak_model(), Self::handle_follow),
4931                client.add_message_handler(cx.weak_model(), Self::handle_update_followers),
4932            ],
4933            client,
4934        }
4935    }
4936
4937    pub fn update_followers(
4938        &self,
4939        project_id: Option<u64>,
4940        update: proto::update_followers::Variant,
4941        cx: &AppContext,
4942    ) -> Option<()> {
4943        let active_call = ActiveCall::try_global(cx)?;
4944        let room_id = active_call.read(cx).room()?.read(cx).id();
4945        self.client
4946            .send(proto::UpdateFollowers {
4947                room_id,
4948                project_id,
4949                variant: Some(update),
4950            })
4951            .log_err()
4952    }
4953
4954    pub async fn handle_follow(
4955        this: Model<Self>,
4956        envelope: TypedEnvelope<proto::Follow>,
4957        mut cx: AsyncAppContext,
4958    ) -> Result<proto::FollowResponse> {
4959        this.update(&mut cx, |this, cx| {
4960            let follower = Follower {
4961                project_id: envelope.payload.project_id,
4962                peer_id: envelope.original_sender_id()?,
4963            };
4964
4965            let mut response = proto::FollowResponse::default();
4966            this.workspaces.retain(|workspace| {
4967                workspace
4968                    .update(cx, |workspace, cx| {
4969                        let handler_response = workspace.handle_follow(follower.project_id, cx);
4970                        if let Some(active_view) = handler_response.active_view.clone() {
4971                            if workspace.project.read(cx).remote_id() == follower.project_id {
4972                                response.active_view = Some(active_view)
4973                            }
4974                        }
4975                    })
4976                    .is_ok()
4977            });
4978
4979            Ok(response)
4980        })?
4981    }
4982
4983    async fn handle_update_followers(
4984        this: Model<Self>,
4985        envelope: TypedEnvelope<proto::UpdateFollowers>,
4986        mut cx: AsyncAppContext,
4987    ) -> Result<()> {
4988        let leader_id = envelope.original_sender_id()?;
4989        let update = envelope.payload;
4990
4991        this.update(&mut cx, |this, cx| {
4992            this.workspaces.retain(|workspace| {
4993                workspace
4994                    .update(cx, |workspace, cx| {
4995                        let project_id = workspace.project.read(cx).remote_id();
4996                        if update.project_id != project_id && update.project_id.is_some() {
4997                            return;
4998                        }
4999                        workspace.handle_update_followers(leader_id, update.clone(), cx);
5000                    })
5001                    .is_ok()
5002            });
5003            Ok(())
5004        })?
5005    }
5006}
5007
5008impl ViewId {
5009    pub(crate) fn from_proto(message: proto::ViewId) -> Result<Self> {
5010        Ok(Self {
5011            creator: message
5012                .creator
5013                .ok_or_else(|| anyhow!("creator is missing"))?,
5014            id: message.id,
5015        })
5016    }
5017
5018    pub(crate) fn to_proto(self) -> proto::ViewId {
5019        proto::ViewId {
5020            creator: Some(self.creator),
5021            id: self.id,
5022        }
5023    }
5024}
5025
5026impl FollowerState {
5027    fn pane(&self) -> &View<Pane> {
5028        self.dock_pane.as_ref().unwrap_or(&self.center_pane)
5029    }
5030}
5031
5032pub trait WorkspaceHandle {
5033    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath>;
5034}
5035
5036impl WorkspaceHandle for View<Workspace> {
5037    fn file_project_paths(&self, cx: &AppContext) -> Vec<ProjectPath> {
5038        self.read(cx)
5039            .worktrees(cx)
5040            .flat_map(|worktree| {
5041                let worktree_id = worktree.read(cx).id();
5042                worktree.read(cx).files(true, 0).map(move |f| ProjectPath {
5043                    worktree_id,
5044                    path: f.path.clone(),
5045                })
5046            })
5047            .collect::<Vec<_>>()
5048    }
5049}
5050
5051impl std::fmt::Debug for OpenPaths {
5052    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5053        f.debug_struct("OpenPaths")
5054            .field("paths", &self.paths)
5055            .finish()
5056    }
5057}
5058
5059pub fn activate_workspace_for_project(
5060    cx: &mut AppContext,
5061    predicate: impl Fn(&Project, &AppContext) -> bool + Send + 'static,
5062) -> Option<WindowHandle<Workspace>> {
5063    for window in cx.windows() {
5064        let Some(workspace) = window.downcast::<Workspace>() else {
5065            continue;
5066        };
5067
5068        let predicate = workspace
5069            .update(cx, |workspace, cx| {
5070                let project = workspace.project.read(cx);
5071                if predicate(project, cx) {
5072                    cx.activate_window();
5073                    true
5074                } else {
5075                    false
5076                }
5077            })
5078            .log_err()
5079            .unwrap_or(false);
5080
5081        if predicate {
5082            return Some(workspace);
5083        }
5084    }
5085
5086    None
5087}
5088
5089pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
5090    DB.last_workspace().await.log_err().flatten()
5091}
5092
5093pub fn last_session_workspace_locations(
5094    last_session_id: &str,
5095    last_session_window_stack: Option<Vec<WindowId>>,
5096) -> Option<Vec<SerializedWorkspaceLocation>> {
5097    DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
5098        .log_err()
5099}
5100
5101actions!(collab, [OpenChannelNotes]);
5102actions!(zed, [OpenLog]);
5103
5104async fn join_channel_internal(
5105    channel_id: ChannelId,
5106    app_state: &Arc<AppState>,
5107    requesting_window: Option<WindowHandle<Workspace>>,
5108    active_call: &Model<ActiveCall>,
5109    cx: &mut AsyncAppContext,
5110) -> Result<bool> {
5111    let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| {
5112        let Some(room) = active_call.room().map(|room| room.read(cx)) else {
5113            return (false, None);
5114        };
5115
5116        let already_in_channel = room.channel_id() == Some(channel_id);
5117        let should_prompt = room.is_sharing_project()
5118            && !room.remote_participants().is_empty()
5119            && !already_in_channel;
5120        let open_room = if already_in_channel {
5121            active_call.room().cloned()
5122        } else {
5123            None
5124        };
5125        (should_prompt, open_room)
5126    })?;
5127
5128    if let Some(room) = open_room {
5129        let task = room.update(cx, |room, cx| {
5130            if let Some((project, host)) = room.most_active_project(cx) {
5131                return Some(join_in_room_project(project, host, app_state.clone(), cx));
5132            }
5133
5134            None
5135        })?;
5136        if let Some(task) = task {
5137            task.await?;
5138        }
5139        return anyhow::Ok(true);
5140    }
5141
5142    if should_prompt {
5143        if let Some(workspace) = requesting_window {
5144            let answer = workspace
5145                .update(cx, |_, cx| {
5146                    cx.prompt(
5147                        PromptLevel::Warning,
5148                        "Do you want to switch channels?",
5149                        Some("Leaving this call will unshare your current project."),
5150                        &["Yes, Join Channel", "Cancel"],
5151                    )
5152                })?
5153                .await;
5154
5155            if answer == Ok(1) {
5156                return Ok(false);
5157            }
5158        } else {
5159            return Ok(false); // unreachable!() hopefully
5160        }
5161    }
5162
5163    let client = cx.update(|cx| active_call.read(cx).client())?;
5164
5165    let mut client_status = client.status();
5166
5167    // this loop will terminate within client::CONNECTION_TIMEOUT seconds.
5168    'outer: loop {
5169        let Some(status) = client_status.recv().await else {
5170            return Err(anyhow!("error connecting"));
5171        };
5172
5173        match status {
5174            Status::Connecting
5175            | Status::Authenticating
5176            | Status::Reconnecting
5177            | Status::Reauthenticating => continue,
5178            Status::Connected { .. } => break 'outer,
5179            Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
5180            Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
5181            Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
5182                return Err(ErrorCode::Disconnected.into());
5183            }
5184        }
5185    }
5186
5187    let room = active_call
5188        .update(cx, |active_call, cx| {
5189            active_call.join_channel(channel_id, cx)
5190        })?
5191        .await?;
5192
5193    let Some(room) = room else {
5194        return anyhow::Ok(true);
5195    };
5196
5197    room.update(cx, |room, _| room.room_update_completed())?
5198        .await;
5199
5200    let task = room.update(cx, |room, cx| {
5201        if let Some((project, host)) = room.most_active_project(cx) {
5202            return Some(join_in_room_project(project, host, app_state.clone(), cx));
5203        }
5204
5205        // If you are the first to join a channel, see if you should share your project.
5206        if room.remote_participants().is_empty() && !room.local_participant_is_guest() {
5207            if let Some(workspace) = requesting_window {
5208                let project = workspace.update(cx, |workspace, cx| {
5209                    let project = workspace.project.read(cx);
5210
5211                    if !CallSettings::get_global(cx).share_on_join {
5212                        return None;
5213                    }
5214
5215                    if (project.is_local() || project.is_via_ssh())
5216                        && project.visible_worktrees(cx).any(|tree| {
5217                            tree.read(cx)
5218                                .root_entry()
5219                                .map_or(false, |entry| entry.is_dir())
5220                        })
5221                    {
5222                        Some(workspace.project.clone())
5223                    } else {
5224                        None
5225                    }
5226                });
5227                if let Ok(Some(project)) = project {
5228                    return Some(cx.spawn(|room, mut cx| async move {
5229                        room.update(&mut cx, |room, cx| room.share_project(project, cx))?
5230                            .await?;
5231                        Ok(())
5232                    }));
5233                }
5234            }
5235        }
5236
5237        None
5238    })?;
5239    if let Some(task) = task {
5240        task.await?;
5241        return anyhow::Ok(true);
5242    }
5243    anyhow::Ok(false)
5244}
5245
5246pub fn join_channel(
5247    channel_id: ChannelId,
5248    app_state: Arc<AppState>,
5249    requesting_window: Option<WindowHandle<Workspace>>,
5250    cx: &mut AppContext,
5251) -> Task<Result<()>> {
5252    let active_call = ActiveCall::global(cx);
5253    cx.spawn(|mut cx| async move {
5254        let result = join_channel_internal(
5255            channel_id,
5256            &app_state,
5257            requesting_window,
5258            &active_call,
5259            &mut cx,
5260        )
5261            .await;
5262
5263        // join channel succeeded, and opened a window
5264        if matches!(result, Ok(true)) {
5265            return anyhow::Ok(());
5266        }
5267
5268        // find an existing workspace to focus and show call controls
5269        let mut active_window =
5270            requesting_window.or_else(|| activate_any_workspace_window(&mut cx));
5271        if active_window.is_none() {
5272            // no open workspaces, make one to show the error in (blergh)
5273            let (window_handle, _) = cx
5274                .update(|cx| {
5275                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
5276                })?
5277                .await?;
5278
5279            if result.is_ok() {
5280                cx.update(|cx| {
5281                    cx.dispatch_action(&OpenChannelNotes);
5282                }).log_err();
5283            }
5284
5285            active_window = Some(window_handle);
5286        }
5287
5288        if let Err(err) = result {
5289            log::error!("failed to join channel: {}", err);
5290            if let Some(active_window) = active_window {
5291                active_window
5292                    .update(&mut cx, |_, cx| {
5293                        let detail: SharedString = match err.error_code() {
5294                            ErrorCode::SignedOut => {
5295                                "Please sign in to continue.".into()
5296                            }
5297                            ErrorCode::UpgradeRequired => {
5298                                "Your are running an unsupported version of Zed. Please update to continue.".into()
5299                            }
5300                            ErrorCode::NoSuchChannel => {
5301                                "No matching channel was found. Please check the link and try again.".into()
5302                            }
5303                            ErrorCode::Forbidden => {
5304                                "This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
5305                            }
5306                            ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
5307                            _ => format!("{}\n\nPlease try again.", err).into(),
5308                        };
5309                        cx.prompt(
5310                            PromptLevel::Critical,
5311                            "Failed to join channel",
5312                            Some(&detail),
5313                            &["Ok"],
5314                        )
5315                    })?
5316                    .await
5317                    .ok();
5318            }
5319        }
5320
5321        // return ok, we showed the error to the user.
5322        anyhow::Ok(())
5323    })
5324}
5325
5326pub async fn get_any_active_workspace(
5327    app_state: Arc<AppState>,
5328    mut cx: AsyncAppContext,
5329) -> anyhow::Result<WindowHandle<Workspace>> {
5330    // find an existing workspace to focus and show call controls
5331    let active_window = activate_any_workspace_window(&mut cx);
5332    if active_window.is_none() {
5333        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
5334            .await?;
5335    }
5336    activate_any_workspace_window(&mut cx).context("could not open zed")
5337}
5338
5339fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option<WindowHandle<Workspace>> {
5340    cx.update(|cx| {
5341        if let Some(workspace_window) = cx
5342            .active_window()
5343            .and_then(|window| window.downcast::<Workspace>())
5344        {
5345            return Some(workspace_window);
5346        }
5347
5348        for window in cx.windows() {
5349            if let Some(workspace_window) = window.downcast::<Workspace>() {
5350                workspace_window
5351                    .update(cx, |_, cx| cx.activate_window())
5352                    .ok();
5353                return Some(workspace_window);
5354            }
5355        }
5356        None
5357    })
5358    .ok()
5359    .flatten()
5360}
5361
5362pub fn local_workspace_windows(cx: &AppContext) -> Vec<WindowHandle<Workspace>> {
5363    cx.windows()
5364        .into_iter()
5365        .filter_map(|window| window.downcast::<Workspace>())
5366        .filter(|workspace| {
5367            workspace
5368                .read(cx)
5369                .is_ok_and(|workspace| workspace.project.read(cx).is_local())
5370        })
5371        .collect()
5372}
5373
5374#[derive(Default)]
5375pub struct OpenOptions {
5376    pub open_new_workspace: Option<bool>,
5377    pub replace_window: Option<WindowHandle<Workspace>>,
5378    pub env: Option<HashMap<String, String>>,
5379}
5380
5381#[allow(clippy::type_complexity)]
5382pub fn open_paths(
5383    abs_paths: &[PathBuf],
5384    app_state: Arc<AppState>,
5385    open_options: OpenOptions,
5386    cx: &mut AppContext,
5387) -> Task<
5388    anyhow::Result<(
5389        WindowHandle<Workspace>,
5390        Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
5391    )>,
5392> {
5393    let abs_paths = abs_paths.to_vec();
5394    let mut existing = None;
5395    let mut best_match = None;
5396    let mut open_visible = OpenVisible::All;
5397
5398    if open_options.open_new_workspace != Some(true) {
5399        for window in local_workspace_windows(cx) {
5400            if let Ok(workspace) = window.read(cx) {
5401                let m = workspace
5402                    .project
5403                    .read(cx)
5404                    .visibility_for_paths(&abs_paths, cx);
5405                if m > best_match {
5406                    existing = Some(window);
5407                    best_match = m;
5408                } else if best_match.is_none() && open_options.open_new_workspace == Some(false) {
5409                    existing = Some(window)
5410                }
5411            }
5412        }
5413    }
5414
5415    cx.spawn(move |mut cx| async move {
5416        if open_options.open_new_workspace.is_none() && existing.is_none() {
5417            let all_files = abs_paths.iter().map(|path| app_state.fs.metadata(path));
5418            if futures::future::join_all(all_files)
5419                .await
5420                .into_iter()
5421                .filter_map(|result| result.ok().flatten())
5422                .all(|file| !file.is_dir)
5423            {
5424                cx.update(|cx| {
5425                    for window in local_workspace_windows(cx) {
5426                        if let Ok(workspace) = window.read(cx) {
5427                            let project = workspace.project().read(cx);
5428                            if project.is_via_collab() {
5429                                continue;
5430                            }
5431                            existing = Some(window);
5432                            open_visible = OpenVisible::None;
5433                            break;
5434                        }
5435                    }
5436                })?;
5437            }
5438        }
5439
5440        if let Some(existing) = existing {
5441            Ok((
5442                existing,
5443                existing
5444                    .update(&mut cx, |workspace, cx| {
5445                        cx.activate_window();
5446                        workspace.open_paths(abs_paths, open_visible, None, cx)
5447                    })?
5448                    .await,
5449            ))
5450        } else {
5451            cx.update(move |cx| {
5452                Workspace::new_local(
5453                    abs_paths,
5454                    app_state.clone(),
5455                    open_options.replace_window,
5456                    open_options.env,
5457                    cx,
5458                )
5459            })?
5460            .await
5461        }
5462    })
5463}
5464
5465pub fn open_new(
5466    open_options: OpenOptions,
5467    app_state: Arc<AppState>,
5468    cx: &mut AppContext,
5469    init: impl FnOnce(&mut Workspace, &mut ViewContext<Workspace>) + 'static + Send,
5470) -> Task<anyhow::Result<()>> {
5471    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
5472    cx.spawn(|mut cx| async move {
5473        let (workspace, opened_paths) = task.await?;
5474        workspace.update(&mut cx, |workspace, cx| {
5475            if opened_paths.is_empty() {
5476                init(workspace, cx)
5477            }
5478        })?;
5479        Ok(())
5480    })
5481}
5482
5483pub fn create_and_open_local_file(
5484    path: &'static Path,
5485    cx: &mut ViewContext<Workspace>,
5486    default_content: impl 'static + Send + FnOnce() -> Rope,
5487) -> Task<Result<Box<dyn ItemHandle>>> {
5488    cx.spawn(|workspace, mut cx| async move {
5489        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
5490        if !fs.is_file(path).await {
5491            fs.create_file(path, Default::default()).await?;
5492            fs.save(path, &default_content(), Default::default())
5493                .await?;
5494        }
5495
5496        let mut items = workspace
5497            .update(&mut cx, |workspace, cx| {
5498                workspace.with_local_workspace(cx, |workspace, cx| {
5499                    workspace.open_paths(vec![path.to_path_buf()], OpenVisible::None, None, cx)
5500                })
5501            })?
5502            .await?
5503            .await;
5504
5505        let item = items.pop().flatten();
5506        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
5507    })
5508}
5509
5510pub fn open_ssh_project(
5511    window: WindowHandle<Workspace>,
5512    connection_options: SshConnectionOptions,
5513    cancel_rx: oneshot::Receiver<()>,
5514    delegate: Arc<dyn SshClientDelegate>,
5515    app_state: Arc<AppState>,
5516    paths: Vec<PathBuf>,
5517    cx: &mut AppContext,
5518) -> Task<Result<()>> {
5519    cx.spawn(|mut cx| async move {
5520        let (serialized_ssh_project, workspace_id, serialized_workspace) =
5521            serialize_ssh_project(connection_options.clone(), paths.clone(), &cx).await?;
5522
5523        let session = match cx
5524            .update(|cx| {
5525                remote::SshRemoteClient::new(
5526                    ConnectionIdentifier::Workspace(workspace_id.0),
5527                    connection_options,
5528                    cancel_rx,
5529                    delegate,
5530                    cx,
5531                )
5532            })?
5533            .await?
5534        {
5535            Some(result) => result,
5536            None => return Ok(()),
5537        };
5538
5539        let project = cx.update(|cx| {
5540            project::Project::ssh(
5541                session,
5542                app_state.client.clone(),
5543                app_state.node_runtime.clone(),
5544                app_state.user_store.clone(),
5545                app_state.languages.clone(),
5546                app_state.fs.clone(),
5547                cx,
5548            )
5549        })?;
5550
5551        let toolchains = DB.toolchains(workspace_id).await?;
5552        for (toolchain, worktree_id) in toolchains {
5553            project
5554                .update(&mut cx, |this, cx| {
5555                    this.activate_toolchain(worktree_id, toolchain, cx)
5556                })?
5557                .await;
5558        }
5559        let mut project_paths_to_open = vec![];
5560        let mut project_path_errors = vec![];
5561
5562        for path in paths {
5563            let result = cx
5564                .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))?
5565                .await;
5566            match result {
5567                Ok((_, project_path)) => {
5568                    project_paths_to_open.push((path.clone(), Some(project_path)));
5569                }
5570                Err(error) => {
5571                    project_path_errors.push(error);
5572                }
5573            };
5574        }
5575
5576        if project_paths_to_open.is_empty() {
5577            return Err(project_path_errors
5578                .pop()
5579                .unwrap_or_else(|| anyhow!("no paths given")));
5580        }
5581
5582        cx.update_window(window.into(), |_, cx| {
5583            cx.replace_root_view(|cx| {
5584                let mut workspace =
5585                    Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
5586
5587                workspace
5588                    .client()
5589                    .telemetry()
5590                    .report_app_event("open ssh project".to_string());
5591
5592                workspace.set_serialized_ssh_project(serialized_ssh_project);
5593                workspace
5594            });
5595        })?;
5596
5597        window
5598            .update(&mut cx, |_, cx| {
5599                cx.activate_window();
5600
5601                open_items(serialized_workspace, project_paths_to_open, cx)
5602            })?
5603            .await?;
5604
5605        window.update(&mut cx, |workspace, cx| {
5606            for error in project_path_errors {
5607                if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist {
5608                    if let Some(path) = error.error_tag("path") {
5609                        workspace.show_error(&anyhow!("'{path}' does not exist"), cx)
5610                    }
5611                } else {
5612                    workspace.show_error(&error, cx)
5613                }
5614            }
5615        })
5616    })
5617}
5618
5619fn serialize_ssh_project(
5620    connection_options: SshConnectionOptions,
5621    paths: Vec<PathBuf>,
5622    cx: &AsyncAppContext,
5623) -> Task<
5624    Result<(
5625        SerializedSshProject,
5626        WorkspaceId,
5627        Option<SerializedWorkspace>,
5628    )>,
5629> {
5630    cx.background_executor().spawn(async move {
5631        let serialized_ssh_project = persistence::DB
5632            .get_or_create_ssh_project(
5633                connection_options.host.clone(),
5634                connection_options.port,
5635                paths
5636                    .iter()
5637                    .map(|path| path.to_string_lossy().to_string())
5638                    .collect::<Vec<_>>(),
5639                connection_options.username.clone(),
5640            )
5641            .await?;
5642
5643        let serialized_workspace =
5644            persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
5645
5646        let workspace_id = if let Some(workspace_id) =
5647            serialized_workspace.as_ref().map(|workspace| workspace.id)
5648        {
5649            workspace_id
5650        } else {
5651            persistence::DB.next_id().await?
5652        };
5653
5654        Ok((serialized_ssh_project, workspace_id, serialized_workspace))
5655    })
5656}
5657
5658pub fn join_in_room_project(
5659    project_id: u64,
5660    follow_user_id: u64,
5661    app_state: Arc<AppState>,
5662    cx: &mut AppContext,
5663) -> Task<Result<()>> {
5664    let windows = cx.windows();
5665    cx.spawn(|mut cx| async move {
5666        let existing_workspace = windows.into_iter().find_map(|window| {
5667            window.downcast::<Workspace>().and_then(|window| {
5668                window
5669                    .update(&mut cx, |workspace, cx| {
5670                        if workspace.project().read(cx).remote_id() == Some(project_id) {
5671                            Some(window)
5672                        } else {
5673                            None
5674                        }
5675                    })
5676                    .unwrap_or(None)
5677            })
5678        });
5679
5680        let workspace = if let Some(existing_workspace) = existing_workspace {
5681            existing_workspace
5682        } else {
5683            let active_call = cx.update(|cx| ActiveCall::global(cx))?;
5684            let room = active_call
5685                .read_with(&cx, |call, _| call.room().cloned())?
5686                .ok_or_else(|| anyhow!("not in a call"))?;
5687            let project = room
5688                .update(&mut cx, |room, cx| {
5689                    room.join_project(
5690                        project_id,
5691                        app_state.languages.clone(),
5692                        app_state.fs.clone(),
5693                        cx,
5694                    )
5695                })?
5696                .await?;
5697
5698            let window_bounds_override = window_bounds_env_override();
5699            cx.update(|cx| {
5700                let mut options = (app_state.build_window_options)(None, cx);
5701                options.window_bounds = window_bounds_override.map(WindowBounds::Windowed);
5702                cx.open_window(options, |cx| {
5703                    cx.new_view(|cx| {
5704                        Workspace::new(Default::default(), project, app_state.clone(), cx)
5705                    })
5706                })
5707            })??
5708        };
5709
5710        workspace.update(&mut cx, |workspace, cx| {
5711            cx.activate(true);
5712            cx.activate_window();
5713
5714            if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
5715                let follow_peer_id = room
5716                    .read(cx)
5717                    .remote_participants()
5718                    .iter()
5719                    .find(|(_, participant)| participant.user.id == follow_user_id)
5720                    .map(|(_, p)| p.peer_id)
5721                    .or_else(|| {
5722                        // If we couldn't follow the given user, follow the host instead.
5723                        let collaborator = workspace
5724                            .project()
5725                            .read(cx)
5726                            .collaborators()
5727                            .values()
5728                            .find(|collaborator| collaborator.is_host)?;
5729                        Some(collaborator.peer_id)
5730                    });
5731
5732                if let Some(follow_peer_id) = follow_peer_id {
5733                    workspace.follow(follow_peer_id, cx);
5734                }
5735            }
5736        })?;
5737
5738        anyhow::Ok(())
5739    })
5740}
5741
5742pub fn reload(reload: &Reload, cx: &mut AppContext) {
5743    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
5744    let mut workspace_windows = cx
5745        .windows()
5746        .into_iter()
5747        .filter_map(|window| window.downcast::<Workspace>())
5748        .collect::<Vec<_>>();
5749
5750    // If multiple windows have unsaved changes, and need a save prompt,
5751    // prompt in the active window before switching to a different window.
5752    workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
5753
5754    let mut prompt = None;
5755    if let (true, Some(window)) = (should_confirm, workspace_windows.first()) {
5756        prompt = window
5757            .update(cx, |_, cx| {
5758                cx.prompt(
5759                    PromptLevel::Info,
5760                    "Are you sure you want to restart?",
5761                    None,
5762                    &["Restart", "Cancel"],
5763                )
5764            })
5765            .ok();
5766    }
5767
5768    let binary_path = reload.binary_path.clone();
5769    cx.spawn(|mut cx| async move {
5770        if let Some(prompt) = prompt {
5771            let answer = prompt.await?;
5772            if answer != 0 {
5773                return Ok(());
5774            }
5775        }
5776
5777        // If the user cancels any save prompt, then keep the app open.
5778        for window in workspace_windows {
5779            if let Ok(should_close) = window.update(&mut cx, |workspace, cx| {
5780                workspace.prepare_to_close(CloseIntent::Quit, cx)
5781            }) {
5782                if !should_close.await? {
5783                    return Ok(());
5784                }
5785            }
5786        }
5787
5788        cx.update(|cx| cx.restart(binary_path))
5789    })
5790    .detach_and_log_err(cx);
5791}
5792
5793fn parse_pixel_position_env_var(value: &str) -> Option<Point<Pixels>> {
5794    let mut parts = value.split(',');
5795    let x: usize = parts.next()?.parse().ok()?;
5796    let y: usize = parts.next()?.parse().ok()?;
5797    Some(point(px(x as f32), px(y as f32)))
5798}
5799
5800fn parse_pixel_size_env_var(value: &str) -> Option<Size<Pixels>> {
5801    let mut parts = value.split(',');
5802    let width: usize = parts.next()?.parse().ok()?;
5803    let height: usize = parts.next()?.parse().ok()?;
5804    Some(size(px(width as f32), px(height as f32)))
5805}
5806
5807pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
5808    const BORDER_SIZE: Pixels = px(1.0);
5809    let decorations = cx.window_decorations();
5810
5811    if matches!(decorations, Decorations::Client { .. }) {
5812        cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
5813    }
5814
5815    struct GlobalResizeEdge(ResizeEdge);
5816    impl Global for GlobalResizeEdge {}
5817
5818    div()
5819        .id("window-backdrop")
5820        .bg(transparent_black())
5821        .map(|div| match decorations {
5822            Decorations::Server => div,
5823            Decorations::Client { tiling, .. } => div
5824                .when(!(tiling.top || tiling.right), |div| {
5825                    div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5826                })
5827                .when(!(tiling.top || tiling.left), |div| {
5828                    div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5829                })
5830                .when(!(tiling.bottom || tiling.right), |div| {
5831                    div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5832                })
5833                .when(!(tiling.bottom || tiling.left), |div| {
5834                    div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5835                })
5836                .when(!tiling.top, |div| {
5837                    div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
5838                })
5839                .when(!tiling.bottom, |div| {
5840                    div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
5841                })
5842                .when(!tiling.left, |div| {
5843                    div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
5844                })
5845                .when(!tiling.right, |div| {
5846                    div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
5847                })
5848                .on_mouse_move(move |e, cx| {
5849                    let size = cx.window_bounds().get_bounds().size;
5850                    let pos = e.position;
5851
5852                    let new_edge =
5853                        resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
5854
5855                    let edge = cx.try_global::<GlobalResizeEdge>();
5856                    if new_edge != edge.map(|edge| edge.0) {
5857                        cx.window_handle()
5858                            .update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
5859                            .ok();
5860                    }
5861                })
5862                .on_mouse_down(MouseButton::Left, move |e, cx| {
5863                    let size = cx.window_bounds().get_bounds().size;
5864                    let pos = e.position;
5865
5866                    let edge = match resize_edge(
5867                        pos,
5868                        theme::CLIENT_SIDE_DECORATION_SHADOW,
5869                        size,
5870                        tiling,
5871                    ) {
5872                        Some(value) => value,
5873                        None => return,
5874                    };
5875
5876                    cx.start_window_resize(edge);
5877                }),
5878        })
5879        .size_full()
5880        .child(
5881            div()
5882                .cursor(CursorStyle::Arrow)
5883                .map(|div| match decorations {
5884                    Decorations::Server => div,
5885                    Decorations::Client { tiling } => div
5886                        .border_color(cx.theme().colors().border)
5887                        .when(!(tiling.top || tiling.right), |div| {
5888                            div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5889                        })
5890                        .when(!(tiling.top || tiling.left), |div| {
5891                            div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5892                        })
5893                        .when(!(tiling.bottom || tiling.right), |div| {
5894                            div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5895                        })
5896                        .when(!(tiling.bottom || tiling.left), |div| {
5897                            div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
5898                        })
5899                        .when(!tiling.top, |div| div.border_t(BORDER_SIZE))
5900                        .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
5901                        .when(!tiling.left, |div| div.border_l(BORDER_SIZE))
5902                        .when(!tiling.right, |div| div.border_r(BORDER_SIZE))
5903                        .when(!tiling.is_tiled(), |div| {
5904                            div.shadow(smallvec::smallvec![gpui::BoxShadow {
5905                                color: Hsla {
5906                                    h: 0.,
5907                                    s: 0.,
5908                                    l: 0.,
5909                                    a: 0.4,
5910                                },
5911                                blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
5912                                spread_radius: px(0.),
5913                                offset: point(px(0.0), px(0.0)),
5914                            }])
5915                        }),
5916                })
5917                .on_mouse_move(|_e, cx| {
5918                    cx.stop_propagation();
5919                })
5920                .size_full()
5921                .child(element),
5922        )
5923        .map(|div| match decorations {
5924            Decorations::Server => div,
5925            Decorations::Client { tiling, .. } => div.child(
5926                canvas(
5927                    |_bounds, cx| {
5928                        cx.insert_hitbox(
5929                            Bounds::new(
5930                                point(px(0.0), px(0.0)),
5931                                cx.window_bounds().get_bounds().size,
5932                            ),
5933                            false,
5934                        )
5935                    },
5936                    move |_bounds, hitbox, cx| {
5937                        let mouse = cx.mouse_position();
5938                        let size = cx.window_bounds().get_bounds().size;
5939                        let Some(edge) =
5940                            resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
5941                        else {
5942                            return;
5943                        };
5944                        cx.set_global(GlobalResizeEdge(edge));
5945                        cx.set_cursor_style(
5946                            match edge {
5947                                ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
5948                                ResizeEdge::Left | ResizeEdge::Right => {
5949                                    CursorStyle::ResizeLeftRight
5950                                }
5951                                ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
5952                                    CursorStyle::ResizeUpLeftDownRight
5953                                }
5954                                ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
5955                                    CursorStyle::ResizeUpRightDownLeft
5956                                }
5957                            },
5958                            &hitbox,
5959                        );
5960                    },
5961                )
5962                .size_full()
5963                .absolute(),
5964            ),
5965        })
5966}
5967
5968fn resize_edge(
5969    pos: Point<Pixels>,
5970    shadow_size: Pixels,
5971    window_size: Size<Pixels>,
5972    tiling: Tiling,
5973) -> Option<ResizeEdge> {
5974    let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
5975    if bounds.contains(&pos) {
5976        return None;
5977    }
5978
5979    let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
5980    let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
5981    if !tiling.top && top_left_bounds.contains(&pos) {
5982        return Some(ResizeEdge::TopLeft);
5983    }
5984
5985    let top_right_bounds = Bounds::new(
5986        Point::new(window_size.width - corner_size.width, px(0.)),
5987        corner_size,
5988    );
5989    if !tiling.top && top_right_bounds.contains(&pos) {
5990        return Some(ResizeEdge::TopRight);
5991    }
5992
5993    let bottom_left_bounds = Bounds::new(
5994        Point::new(px(0.), window_size.height - corner_size.height),
5995        corner_size,
5996    );
5997    if !tiling.bottom && bottom_left_bounds.contains(&pos) {
5998        return Some(ResizeEdge::BottomLeft);
5999    }
6000
6001    let bottom_right_bounds = Bounds::new(
6002        Point::new(
6003            window_size.width - corner_size.width,
6004            window_size.height - corner_size.height,
6005        ),
6006        corner_size,
6007    );
6008    if !tiling.bottom && bottom_right_bounds.contains(&pos) {
6009        return Some(ResizeEdge::BottomRight);
6010    }
6011
6012    if !tiling.top && pos.y < shadow_size {
6013        Some(ResizeEdge::Top)
6014    } else if !tiling.bottom && pos.y > window_size.height - shadow_size {
6015        Some(ResizeEdge::Bottom)
6016    } else if !tiling.left && pos.x < shadow_size {
6017        Some(ResizeEdge::Left)
6018    } else if !tiling.right && pos.x > window_size.width - shadow_size {
6019        Some(ResizeEdge::Right)
6020    } else {
6021        None
6022    }
6023}
6024
6025fn join_pane_into_active(active_pane: &View<Pane>, pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6026    if pane == active_pane {
6027        return;
6028    } else if pane.read(cx).items_len() == 0 {
6029        pane.update(cx, |_, cx| {
6030            cx.emit(pane::Event::Remove {
6031                focus_on_pane: None,
6032            });
6033        })
6034    } else {
6035        move_all_items(pane, active_pane, cx);
6036    }
6037}
6038
6039fn move_all_items(from_pane: &View<Pane>, to_pane: &View<Pane>, cx: &mut WindowContext<'_>) {
6040    let destination_is_different = from_pane != to_pane;
6041    let mut moved_items = 0;
6042    for (item_ix, item_handle) in from_pane
6043        .read(cx)
6044        .items()
6045        .enumerate()
6046        .map(|(ix, item)| (ix, item.clone()))
6047        .collect::<Vec<_>>()
6048    {
6049        let ix = item_ix - moved_items;
6050        if destination_is_different {
6051            // Close item from previous pane
6052            from_pane.update(cx, |source, cx| {
6053                source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), cx);
6054            });
6055            moved_items += 1;
6056        }
6057
6058        // This automatically removes duplicate items in the pane
6059        to_pane.update(cx, |destination, cx| {
6060            destination.add_item(item_handle, true, true, None, cx);
6061            destination.focus(cx)
6062        });
6063    }
6064}
6065
6066pub fn move_item(
6067    source: &View<Pane>,
6068    destination: &View<Pane>,
6069    item_id_to_move: EntityId,
6070    destination_index: usize,
6071    cx: &mut WindowContext<'_>,
6072) {
6073    let Some((item_ix, item_handle)) = source
6074        .read(cx)
6075        .items()
6076        .enumerate()
6077        .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move)
6078        .map(|(ix, item)| (ix, item.clone()))
6079    else {
6080        // Tab was closed during drag
6081        return;
6082    };
6083
6084    if source != destination {
6085        // Close item from previous pane
6086        source.update(cx, |source, cx| {
6087            source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), cx);
6088        });
6089    }
6090
6091    // This automatically removes duplicate items in the pane
6092    destination.update(cx, |destination, cx| {
6093        destination.add_item(item_handle, true, true, Some(destination_index), cx);
6094        destination.focus(cx)
6095    });
6096}
6097
6098#[cfg(test)]
6099mod tests {
6100    use std::{cell::RefCell, rc::Rc};
6101
6102    use super::*;
6103    use crate::{
6104        dock::{test::TestPanel, PanelEvent},
6105        item::{
6106            test::{TestItem, TestProjectItem},
6107            ItemEvent,
6108        },
6109    };
6110    use fs::FakeFs;
6111    use gpui::{
6112        px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext,
6113        UpdateGlobal, VisualTestContext,
6114    };
6115    use project::{Project, ProjectEntryId};
6116    use serde_json::json;
6117    use settings::SettingsStore;
6118
6119    #[gpui::test]
6120    async fn test_tab_disambiguation(cx: &mut TestAppContext) {
6121        init_test(cx);
6122
6123        let fs = FakeFs::new(cx.executor());
6124        let project = Project::test(fs, [], cx).await;
6125        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6126
6127        // Adding an item with no ambiguity renders the tab without detail.
6128        let item1 = cx.new_view(|cx| {
6129            let mut item = TestItem::new(cx);
6130            item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]);
6131            item
6132        });
6133        workspace.update(cx, |workspace, cx| {
6134            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6135        });
6136        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0)));
6137
6138        // Adding an item that creates ambiguity increases the level of detail on
6139        // both tabs.
6140        let item2 = cx.new_view(|cx| {
6141            let mut item = TestItem::new(cx);
6142            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6143            item
6144        });
6145        workspace.update(cx, |workspace, cx| {
6146            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6147        });
6148        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6149        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6150
6151        // Adding an item that creates ambiguity increases the level of detail only
6152        // on the ambiguous tabs. In this case, the ambiguity can't be resolved so
6153        // we stop at the highest detail available.
6154        let item3 = cx.new_view(|cx| {
6155            let mut item = TestItem::new(cx);
6156            item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]);
6157            item
6158        });
6159        workspace.update(cx, |workspace, cx| {
6160            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6161        });
6162        item1.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1)));
6163        item2.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6164        item3.update(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3)));
6165    }
6166
6167    #[gpui::test]
6168    async fn test_tracking_active_path(cx: &mut TestAppContext) {
6169        init_test(cx);
6170
6171        let fs = FakeFs::new(cx.executor());
6172        fs.insert_tree(
6173            "/root1",
6174            json!({
6175                "one.txt": "",
6176                "two.txt": "",
6177            }),
6178        )
6179        .await;
6180        fs.insert_tree(
6181            "/root2",
6182            json!({
6183                "three.txt": "",
6184            }),
6185        )
6186        .await;
6187
6188        let project = Project::test(fs, ["root1".as_ref()], cx).await;
6189        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6190        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6191        let worktree_id = project.update(cx, |project, cx| {
6192            project.worktrees(cx).next().unwrap().read(cx).id()
6193        });
6194
6195        let item1 = cx.new_view(|cx| {
6196            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
6197        });
6198        let item2 = cx.new_view(|cx| {
6199            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)])
6200        });
6201
6202        // Add an item to an empty pane
6203        workspace.update(cx, |workspace, cx| {
6204            workspace.add_item_to_active_pane(Box::new(item1), None, true, cx)
6205        });
6206        project.update(cx, |project, cx| {
6207            assert_eq!(
6208                project.active_entry(),
6209                project
6210                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6211                    .map(|e| e.id)
6212            );
6213        });
6214        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
6215
6216        // Add a second item to a non-empty pane
6217        workspace.update(cx, |workspace, cx| {
6218            workspace.add_item_to_active_pane(Box::new(item2), None, true, cx)
6219        });
6220        assert_eq!(cx.window_title().as_deref(), Some("two.txt — root1"));
6221        project.update(cx, |project, cx| {
6222            assert_eq!(
6223                project.active_entry(),
6224                project
6225                    .entry_for_path(&(worktree_id, "two.txt").into(), cx)
6226                    .map(|e| e.id)
6227            );
6228        });
6229
6230        // Close the active item
6231        pane.update(cx, |pane, cx| {
6232            pane.close_active_item(&Default::default(), cx).unwrap()
6233        })
6234        .await
6235        .unwrap();
6236        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1"));
6237        project.update(cx, |project, cx| {
6238            assert_eq!(
6239                project.active_entry(),
6240                project
6241                    .entry_for_path(&(worktree_id, "one.txt").into(), cx)
6242                    .map(|e| e.id)
6243            );
6244        });
6245
6246        // Add a project folder
6247        project
6248            .update(cx, |project, cx| {
6249                project.find_or_create_worktree("root2", true, cx)
6250            })
6251            .await
6252            .unwrap();
6253        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root1, root2"));
6254
6255        // Remove a project folder
6256        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
6257        assert_eq!(cx.window_title().as_deref(), Some("one.txt — root2"));
6258    }
6259
6260    #[gpui::test]
6261    async fn test_close_window(cx: &mut TestAppContext) {
6262        init_test(cx);
6263
6264        let fs = FakeFs::new(cx.executor());
6265        fs.insert_tree("/root", json!({ "one": "" })).await;
6266
6267        let project = Project::test(fs, ["root".as_ref()], cx).await;
6268        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6269
6270        // When there are no dirty items, there's nothing to do.
6271        let item1 = cx.new_view(TestItem::new);
6272        workspace.update(cx, |w, cx| {
6273            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx)
6274        });
6275        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6276        assert!(task.await.unwrap());
6277
6278        // When there are dirty untitled items, prompt to save each one. If the user
6279        // cancels any prompt, then abort.
6280        let item2 = cx.new_view(|cx| TestItem::new(cx).with_dirty(true));
6281        let item3 = cx.new_view(|cx| {
6282            TestItem::new(cx)
6283                .with_dirty(true)
6284                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6285        });
6286        workspace.update(cx, |w, cx| {
6287            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6288            w.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6289        });
6290        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6291        cx.executor().run_until_parked();
6292        cx.simulate_prompt_answer(2); // cancel save all
6293        cx.executor().run_until_parked();
6294        cx.simulate_prompt_answer(2); // cancel save all
6295        cx.executor().run_until_parked();
6296        assert!(!cx.has_pending_prompt());
6297        assert!(!task.await.unwrap());
6298    }
6299
6300    #[gpui::test]
6301    async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
6302        init_test(cx);
6303
6304        // Register TestItem as a serializable item
6305        cx.update(|cx| {
6306            register_serializable_item::<TestItem>(cx);
6307        });
6308
6309        let fs = FakeFs::new(cx.executor());
6310        fs.insert_tree("/root", json!({ "one": "" })).await;
6311
6312        let project = Project::test(fs, ["root".as_ref()], cx).await;
6313        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
6314
6315        // When there are dirty untitled items, but they can serialize, then there is no prompt.
6316        let item1 = cx.new_view(|cx| {
6317            TestItem::new(cx)
6318                .with_dirty(true)
6319                .with_serialize(|| Some(Task::ready(Ok(()))))
6320        });
6321        let item2 = cx.new_view(|cx| {
6322            TestItem::new(cx)
6323                .with_dirty(true)
6324                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6325                .with_serialize(|| Some(Task::ready(Ok(()))))
6326        });
6327        workspace.update(cx, |w, cx| {
6328            w.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6329            w.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6330        });
6331        let task = workspace.update(cx, |w, cx| w.prepare_to_close(CloseIntent::CloseWindow, cx));
6332        assert!(task.await.unwrap());
6333    }
6334
6335    #[gpui::test]
6336    async fn test_close_pane_items(cx: &mut TestAppContext) {
6337        init_test(cx);
6338
6339        let fs = FakeFs::new(cx.executor());
6340
6341        let project = Project::test(fs, None, cx).await;
6342        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6343
6344        let item1 = cx.new_view(|cx| {
6345            TestItem::new(cx)
6346                .with_dirty(true)
6347                .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6348        });
6349        let item2 = cx.new_view(|cx| {
6350            TestItem::new(cx)
6351                .with_dirty(true)
6352                .with_conflict(true)
6353                .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)])
6354        });
6355        let item3 = cx.new_view(|cx| {
6356            TestItem::new(cx)
6357                .with_dirty(true)
6358                .with_conflict(true)
6359                .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)])
6360        });
6361        let item4 = cx.new_view(|cx| {
6362            TestItem::new(cx)
6363                .with_dirty(true)
6364                .with_project_items(&[TestProjectItem::new_untitled(cx)])
6365        });
6366        let pane = workspace.update(cx, |workspace, cx| {
6367            workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, cx);
6368            workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, cx);
6369            workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, cx);
6370            workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, cx);
6371            workspace.active_pane().clone()
6372        });
6373
6374        let close_items = pane.update(cx, |pane, cx| {
6375            pane.activate_item(1, true, true, cx);
6376            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6377            let item1_id = item1.item_id();
6378            let item3_id = item3.item_id();
6379            let item4_id = item4.item_id();
6380            pane.close_items(cx, SaveIntent::Close, move |id| {
6381                [item1_id, item3_id, item4_id].contains(&id)
6382            })
6383        });
6384        cx.executor().run_until_parked();
6385
6386        assert!(cx.has_pending_prompt());
6387        // Ignore "Save all" prompt
6388        cx.simulate_prompt_answer(2);
6389        cx.executor().run_until_parked();
6390        // There's a prompt to save item 1.
6391        pane.update(cx, |pane, _| {
6392            assert_eq!(pane.items_len(), 4);
6393            assert_eq!(pane.active_item().unwrap().item_id(), item1.item_id());
6394        });
6395        // Confirm saving item 1.
6396        cx.simulate_prompt_answer(0);
6397        cx.executor().run_until_parked();
6398
6399        // Item 1 is saved. There's a prompt to save item 3.
6400        pane.update(cx, |pane, cx| {
6401            assert_eq!(item1.read(cx).save_count, 1);
6402            assert_eq!(item1.read(cx).save_as_count, 0);
6403            assert_eq!(item1.read(cx).reload_count, 0);
6404            assert_eq!(pane.items_len(), 3);
6405            assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id());
6406        });
6407        assert!(cx.has_pending_prompt());
6408
6409        // Cancel saving item 3.
6410        cx.simulate_prompt_answer(1);
6411        cx.executor().run_until_parked();
6412
6413        // Item 3 is reloaded. There's a prompt to save item 4.
6414        pane.update(cx, |pane, cx| {
6415            assert_eq!(item3.read(cx).save_count, 0);
6416            assert_eq!(item3.read(cx).save_as_count, 0);
6417            assert_eq!(item3.read(cx).reload_count, 1);
6418            assert_eq!(pane.items_len(), 2);
6419            assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id());
6420        });
6421        assert!(cx.has_pending_prompt());
6422
6423        // Confirm saving item 4.
6424        cx.simulate_prompt_answer(0);
6425        cx.executor().run_until_parked();
6426
6427        // There's a prompt for a path for item 4.
6428        cx.simulate_new_path_selection(|_| Some(Default::default()));
6429        close_items.await.unwrap();
6430
6431        // The requested items are closed.
6432        pane.update(cx, |pane, cx| {
6433            assert_eq!(item4.read(cx).save_count, 0);
6434            assert_eq!(item4.read(cx).save_as_count, 1);
6435            assert_eq!(item4.read(cx).reload_count, 0);
6436            assert_eq!(pane.items_len(), 1);
6437            assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id());
6438        });
6439    }
6440
6441    #[gpui::test]
6442    async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
6443        init_test(cx);
6444
6445        let fs = FakeFs::new(cx.executor());
6446        let project = Project::test(fs, [], cx).await;
6447        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6448
6449        // Create several workspace items with single project entries, and two
6450        // workspace items with multiple project entries.
6451        let single_entry_items = (0..=4)
6452            .map(|project_entry_id| {
6453                cx.new_view(|cx| {
6454                    TestItem::new(cx)
6455                        .with_dirty(true)
6456                        .with_project_items(&[TestProjectItem::new(
6457                            project_entry_id,
6458                            &format!("{project_entry_id}.txt"),
6459                            cx,
6460                        )])
6461                })
6462            })
6463            .collect::<Vec<_>>();
6464        let item_2_3 = cx.new_view(|cx| {
6465            TestItem::new(cx)
6466                .with_dirty(true)
6467                .with_singleton(false)
6468                .with_project_items(&[
6469                    single_entry_items[2].read(cx).project_items[0].clone(),
6470                    single_entry_items[3].read(cx).project_items[0].clone(),
6471                ])
6472        });
6473        let item_3_4 = cx.new_view(|cx| {
6474            TestItem::new(cx)
6475                .with_dirty(true)
6476                .with_singleton(false)
6477                .with_project_items(&[
6478                    single_entry_items[3].read(cx).project_items[0].clone(),
6479                    single_entry_items[4].read(cx).project_items[0].clone(),
6480                ])
6481        });
6482
6483        // Create two panes that contain the following project entries:
6484        //   left pane:
6485        //     multi-entry items:   (2, 3)
6486        //     single-entry items:  0, 1, 2, 3, 4
6487        //   right pane:
6488        //     single-entry items:  1
6489        //     multi-entry items:   (3, 4)
6490        let left_pane = workspace.update(cx, |workspace, cx| {
6491            let left_pane = workspace.active_pane().clone();
6492            workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, cx);
6493            for item in single_entry_items {
6494                workspace.add_item_to_active_pane(Box::new(item), None, true, cx);
6495            }
6496            left_pane.update(cx, |pane, cx| {
6497                pane.activate_item(2, true, true, cx);
6498            });
6499
6500            let right_pane = workspace
6501                .split_and_clone(left_pane.clone(), SplitDirection::Right, cx)
6502                .unwrap();
6503
6504            right_pane.update(cx, |pane, cx| {
6505                pane.add_item(Box::new(item_3_4.clone()), true, true, None, cx);
6506            });
6507
6508            left_pane
6509        });
6510
6511        cx.focus_view(&left_pane);
6512
6513        // When closing all of the items in the left pane, we should be prompted twice:
6514        // once for project entry 0, and once for project entry 2. Project entries 1,
6515        // 3, and 4 are all still open in the other paten. After those two
6516        // prompts, the task should complete.
6517
6518        let close = left_pane.update(cx, |pane, cx| {
6519            pane.close_all_items(&CloseAllItems::default(), cx).unwrap()
6520        });
6521        cx.executor().run_until_parked();
6522
6523        // Discard "Save all" prompt
6524        cx.simulate_prompt_answer(2);
6525
6526        cx.executor().run_until_parked();
6527        left_pane.update(cx, |pane, cx| {
6528            assert_eq!(
6529                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6530                &[ProjectEntryId::from_proto(0)]
6531            );
6532        });
6533        cx.simulate_prompt_answer(0);
6534
6535        cx.executor().run_until_parked();
6536        left_pane.update(cx, |pane, cx| {
6537            assert_eq!(
6538                pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
6539                &[ProjectEntryId::from_proto(2)]
6540            );
6541        });
6542        cx.simulate_prompt_answer(0);
6543
6544        cx.executor().run_until_parked();
6545        close.await.unwrap();
6546        left_pane.update(cx, |pane, _| {
6547            assert_eq!(pane.items_len(), 0);
6548        });
6549    }
6550
6551    #[gpui::test]
6552    async fn test_autosave(cx: &mut gpui::TestAppContext) {
6553        init_test(cx);
6554
6555        let fs = FakeFs::new(cx.executor());
6556        let project = Project::test(fs, [], cx).await;
6557        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6558        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6559
6560        let item = cx.new_view(|cx| {
6561            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6562        });
6563        let item_id = item.entity_id();
6564        workspace.update(cx, |workspace, cx| {
6565            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6566        });
6567
6568        // Autosave on window change.
6569        item.update(cx, |item, cx| {
6570            SettingsStore::update_global(cx, |settings, cx| {
6571                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6572                    settings.autosave = Some(AutosaveSetting::OnWindowChange);
6573                })
6574            });
6575            item.is_dirty = true;
6576        });
6577
6578        // Deactivating the window saves the file.
6579        cx.deactivate_window();
6580        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6581
6582        // Re-activating the window doesn't save the file.
6583        cx.update(|cx| cx.activate_window());
6584        cx.executor().run_until_parked();
6585        item.update(cx, |item, _| assert_eq!(item.save_count, 1));
6586
6587        // Autosave on focus change.
6588        item.update(cx, |item, cx| {
6589            cx.focus_self();
6590            SettingsStore::update_global(cx, |settings, cx| {
6591                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6592                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6593                })
6594            });
6595            item.is_dirty = true;
6596        });
6597
6598        // Blurring the item saves the file.
6599        item.update(cx, |_, cx| cx.blur());
6600        cx.executor().run_until_parked();
6601        item.update(cx, |item, _| assert_eq!(item.save_count, 2));
6602
6603        // Deactivating the window still saves the file.
6604        item.update(cx, |item, cx| {
6605            cx.focus_self();
6606            item.is_dirty = true;
6607        });
6608        cx.deactivate_window();
6609        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6610
6611        // Autosave after delay.
6612        item.update(cx, |item, cx| {
6613            SettingsStore::update_global(cx, |settings, cx| {
6614                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6615                    settings.autosave = Some(AutosaveSetting::AfterDelay { milliseconds: 500 });
6616                })
6617            });
6618            item.is_dirty = true;
6619            cx.emit(ItemEvent::Edit);
6620        });
6621
6622        // Delay hasn't fully expired, so the file is still dirty and unsaved.
6623        cx.executor().advance_clock(Duration::from_millis(250));
6624        item.update(cx, |item, _| assert_eq!(item.save_count, 3));
6625
6626        // After delay expires, the file is saved.
6627        cx.executor().advance_clock(Duration::from_millis(250));
6628        item.update(cx, |item, _| assert_eq!(item.save_count, 4));
6629
6630        // Autosave on focus change, ensuring closing the tab counts as such.
6631        item.update(cx, |item, cx| {
6632            SettingsStore::update_global(cx, |settings, cx| {
6633                settings.update_user_settings::<WorkspaceSettings>(cx, |settings| {
6634                    settings.autosave = Some(AutosaveSetting::OnFocusChange);
6635                })
6636            });
6637            item.is_dirty = true;
6638        });
6639
6640        pane.update(cx, |pane, cx| {
6641            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6642        })
6643        .await
6644        .unwrap();
6645        assert!(!cx.has_pending_prompt());
6646        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6647
6648        // Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
6649        workspace.update(cx, |workspace, cx| {
6650            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6651        });
6652        item.update(cx, |item, cx| {
6653            item.project_items[0].update(cx, |item, _| {
6654                item.entry_id = None;
6655            });
6656            item.is_dirty = true;
6657            cx.blur();
6658        });
6659        cx.run_until_parked();
6660        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6661
6662        // Ensure autosave is prevented for deleted files also when closing the buffer.
6663        let _close_items = pane.update(cx, |pane, cx| {
6664            pane.close_items(cx, SaveIntent::Close, move |id| id == item_id)
6665        });
6666        cx.run_until_parked();
6667        assert!(cx.has_pending_prompt());
6668        item.update(cx, |item, _| assert_eq!(item.save_count, 5));
6669    }
6670
6671    #[gpui::test]
6672    async fn test_pane_navigation(cx: &mut gpui::TestAppContext) {
6673        init_test(cx);
6674
6675        let fs = FakeFs::new(cx.executor());
6676
6677        let project = Project::test(fs, [], cx).await;
6678        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6679
6680        let item = cx.new_view(|cx| {
6681            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
6682        });
6683        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6684        let toolbar = pane.update(cx, |pane, _| pane.toolbar().clone());
6685        let toolbar_notify_count = Rc::new(RefCell::new(0));
6686
6687        workspace.update(cx, |workspace, cx| {
6688            workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, cx);
6689            let toolbar_notification_count = toolbar_notify_count.clone();
6690            cx.observe(&toolbar, move |_, _, _| {
6691                *toolbar_notification_count.borrow_mut() += 1
6692            })
6693            .detach();
6694        });
6695
6696        pane.update(cx, |pane, _| {
6697            assert!(!pane.can_navigate_backward());
6698            assert!(!pane.can_navigate_forward());
6699        });
6700
6701        item.update(cx, |item, cx| {
6702            item.set_state("one".to_string(), cx);
6703        });
6704
6705        // Toolbar must be notified to re-render the navigation buttons
6706        assert_eq!(*toolbar_notify_count.borrow(), 1);
6707
6708        pane.update(cx, |pane, _| {
6709            assert!(pane.can_navigate_backward());
6710            assert!(!pane.can_navigate_forward());
6711        });
6712
6713        workspace
6714            .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx))
6715            .await
6716            .unwrap();
6717
6718        assert_eq!(*toolbar_notify_count.borrow(), 2);
6719        pane.update(cx, |pane, _| {
6720            assert!(!pane.can_navigate_backward());
6721            assert!(pane.can_navigate_forward());
6722        });
6723    }
6724
6725    #[gpui::test]
6726    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
6727        init_test(cx);
6728        let fs = FakeFs::new(cx.executor());
6729
6730        let project = Project::test(fs, [], cx).await;
6731        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6732
6733        let panel = workspace.update(cx, |workspace, cx| {
6734            let panel = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
6735            workspace.add_panel(panel.clone(), cx);
6736
6737            workspace
6738                .right_dock()
6739                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
6740
6741            panel
6742        });
6743
6744        let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
6745        pane.update(cx, |pane, cx| {
6746            let item = cx.new_view(TestItem::new);
6747            pane.add_item(Box::new(item), true, true, None, cx);
6748        });
6749
6750        // Transfer focus from center to panel
6751        workspace.update(cx, |workspace, cx| {
6752            workspace.toggle_panel_focus::<TestPanel>(cx);
6753        });
6754
6755        workspace.update(cx, |workspace, cx| {
6756            assert!(workspace.right_dock().read(cx).is_open());
6757            assert!(!panel.is_zoomed(cx));
6758            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6759        });
6760
6761        // Transfer focus from panel to center
6762        workspace.update(cx, |workspace, cx| {
6763            workspace.toggle_panel_focus::<TestPanel>(cx);
6764        });
6765
6766        workspace.update(cx, |workspace, cx| {
6767            assert!(workspace.right_dock().read(cx).is_open());
6768            assert!(!panel.is_zoomed(cx));
6769            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6770        });
6771
6772        // Close the dock
6773        workspace.update(cx, |workspace, cx| {
6774            workspace.toggle_dock(DockPosition::Right, cx);
6775        });
6776
6777        workspace.update(cx, |workspace, cx| {
6778            assert!(!workspace.right_dock().read(cx).is_open());
6779            assert!(!panel.is_zoomed(cx));
6780            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6781        });
6782
6783        // Open the dock
6784        workspace.update(cx, |workspace, cx| {
6785            workspace.toggle_dock(DockPosition::Right, cx);
6786        });
6787
6788        workspace.update(cx, |workspace, cx| {
6789            assert!(workspace.right_dock().read(cx).is_open());
6790            assert!(!panel.is_zoomed(cx));
6791            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6792        });
6793
6794        // Focus and zoom panel
6795        panel.update(cx, |panel, cx| {
6796            cx.focus_self();
6797            panel.set_zoomed(true, cx)
6798        });
6799
6800        workspace.update(cx, |workspace, cx| {
6801            assert!(workspace.right_dock().read(cx).is_open());
6802            assert!(panel.is_zoomed(cx));
6803            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6804        });
6805
6806        // Transfer focus to the center closes the dock
6807        workspace.update(cx, |workspace, cx| {
6808            workspace.toggle_panel_focus::<TestPanel>(cx);
6809        });
6810
6811        workspace.update(cx, |workspace, cx| {
6812            assert!(!workspace.right_dock().read(cx).is_open());
6813            assert!(panel.is_zoomed(cx));
6814            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6815        });
6816
6817        // Transferring focus back to the panel keeps it zoomed
6818        workspace.update(cx, |workspace, cx| {
6819            workspace.toggle_panel_focus::<TestPanel>(cx);
6820        });
6821
6822        workspace.update(cx, |workspace, cx| {
6823            assert!(workspace.right_dock().read(cx).is_open());
6824            assert!(panel.is_zoomed(cx));
6825            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6826        });
6827
6828        // Close the dock while it is zoomed
6829        workspace.update(cx, |workspace, cx| {
6830            workspace.toggle_dock(DockPosition::Right, cx)
6831        });
6832
6833        workspace.update(cx, |workspace, cx| {
6834            assert!(!workspace.right_dock().read(cx).is_open());
6835            assert!(panel.is_zoomed(cx));
6836            assert!(workspace.zoomed.is_none());
6837            assert!(!panel.read(cx).focus_handle(cx).contains_focused(cx));
6838        });
6839
6840        // Opening the dock, when it's zoomed, retains focus
6841        workspace.update(cx, |workspace, cx| {
6842            workspace.toggle_dock(DockPosition::Right, cx)
6843        });
6844
6845        workspace.update(cx, |workspace, cx| {
6846            assert!(workspace.right_dock().read(cx).is_open());
6847            assert!(panel.is_zoomed(cx));
6848            assert!(workspace.zoomed.is_some());
6849            assert!(panel.read(cx).focus_handle(cx).contains_focused(cx));
6850        });
6851
6852        // Unzoom and close the panel, zoom the active pane.
6853        panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
6854        workspace.update(cx, |workspace, cx| {
6855            workspace.toggle_dock(DockPosition::Right, cx)
6856        });
6857        pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
6858
6859        // Opening a dock unzooms the pane.
6860        workspace.update(cx, |workspace, cx| {
6861            workspace.toggle_dock(DockPosition::Right, cx)
6862        });
6863        workspace.update(cx, |workspace, cx| {
6864            let pane = pane.read(cx);
6865            assert!(!pane.is_zoomed());
6866            assert!(!pane.focus_handle(cx).is_focused(cx));
6867            assert!(workspace.right_dock().read(cx).is_open());
6868            assert!(workspace.zoomed.is_none());
6869        });
6870    }
6871
6872    #[gpui::test]
6873    async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
6874        init_test(cx);
6875
6876        let fs = FakeFs::new(cx.executor());
6877
6878        let project = Project::test(fs, None, cx).await;
6879        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
6880
6881        // Let's arrange the panes like this:
6882        //
6883        // +-----------------------+
6884        // |         top           |
6885        // +------+--------+-------+
6886        // | left | center | right |
6887        // +------+--------+-------+
6888        // |        bottom         |
6889        // +-----------------------+
6890
6891        let top_item = cx.new_view(|cx| {
6892            TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)])
6893        });
6894        let bottom_item = cx.new_view(|cx| {
6895            TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)])
6896        });
6897        let left_item = cx.new_view(|cx| {
6898            TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)])
6899        });
6900        let right_item = cx.new_view(|cx| {
6901            TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)])
6902        });
6903        let center_item = cx.new_view(|cx| {
6904            TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)])
6905        });
6906
6907        let top_pane_id = workspace.update(cx, |workspace, cx| {
6908            let top_pane_id = workspace.active_pane().entity_id();
6909            workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, cx);
6910            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Down, cx);
6911            top_pane_id
6912        });
6913        let bottom_pane_id = workspace.update(cx, |workspace, cx| {
6914            let bottom_pane_id = workspace.active_pane().entity_id();
6915            workspace.add_item_to_active_pane(Box::new(bottom_item.clone()), None, false, cx);
6916            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Up, cx);
6917            bottom_pane_id
6918        });
6919        let left_pane_id = workspace.update(cx, |workspace, cx| {
6920            let left_pane_id = workspace.active_pane().entity_id();
6921            workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, cx);
6922            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6923            left_pane_id
6924        });
6925        let right_pane_id = workspace.update(cx, |workspace, cx| {
6926            let right_pane_id = workspace.active_pane().entity_id();
6927            workspace.add_item_to_active_pane(Box::new(right_item.clone()), None, false, cx);
6928            workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Left, cx);
6929            right_pane_id
6930        });
6931        let center_pane_id = workspace.update(cx, |workspace, cx| {
6932            let center_pane_id = workspace.active_pane().entity_id();
6933            workspace.add_item_to_active_pane(Box::new(center_item.clone()), None, false, cx);
6934            center_pane_id
6935        });
6936        cx.executor().run_until_parked();
6937
6938        workspace.update(cx, |workspace, cx| {
6939            assert_eq!(center_pane_id, workspace.active_pane().entity_id());
6940
6941            // Join into next from center pane into right
6942            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6943        });
6944
6945        workspace.update(cx, |workspace, cx| {
6946            let active_pane = workspace.active_pane();
6947            assert_eq!(right_pane_id, active_pane.entity_id());
6948            assert_eq!(2, active_pane.read(cx).items_len());
6949            let item_ids_in_pane =
6950                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6951            assert!(item_ids_in_pane.contains(&center_item.item_id()));
6952            assert!(item_ids_in_pane.contains(&right_item.item_id()));
6953
6954            // Join into next from right pane into bottom
6955            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6956        });
6957
6958        workspace.update(cx, |workspace, cx| {
6959            let active_pane = workspace.active_pane();
6960            assert_eq!(bottom_pane_id, active_pane.entity_id());
6961            assert_eq!(3, active_pane.read(cx).items_len());
6962            let item_ids_in_pane =
6963                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6964            assert!(item_ids_in_pane.contains(&center_item.item_id()));
6965            assert!(item_ids_in_pane.contains(&right_item.item_id()));
6966            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6967
6968            // Join into next from bottom pane into left
6969            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6970        });
6971
6972        workspace.update(cx, |workspace, cx| {
6973            let active_pane = workspace.active_pane();
6974            assert_eq!(left_pane_id, active_pane.entity_id());
6975            assert_eq!(4, active_pane.read(cx).items_len());
6976            let item_ids_in_pane =
6977                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6978            assert!(item_ids_in_pane.contains(&center_item.item_id()));
6979            assert!(item_ids_in_pane.contains(&right_item.item_id()));
6980            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6981            assert!(item_ids_in_pane.contains(&left_item.item_id()));
6982
6983            // Join into next from left pane into top
6984            workspace.join_pane_into_next(workspace.active_pane().clone(), cx);
6985        });
6986
6987        workspace.update(cx, |workspace, cx| {
6988            let active_pane = workspace.active_pane();
6989            assert_eq!(top_pane_id, active_pane.entity_id());
6990            assert_eq!(5, active_pane.read(cx).items_len());
6991            let item_ids_in_pane =
6992                HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id()));
6993            assert!(item_ids_in_pane.contains(&center_item.item_id()));
6994            assert!(item_ids_in_pane.contains(&right_item.item_id()));
6995            assert!(item_ids_in_pane.contains(&bottom_item.item_id()));
6996            assert!(item_ids_in_pane.contains(&left_item.item_id()));
6997            assert!(item_ids_in_pane.contains(&top_item.item_id()));
6998
6999            // Single pane left: no-op
7000            workspace.join_pane_into_next(workspace.active_pane().clone(), cx)
7001        });
7002
7003        workspace.update(cx, |workspace, _cx| {
7004            let active_pane = workspace.active_pane();
7005            assert_eq!(top_pane_id, active_pane.entity_id());
7006        });
7007    }
7008
7009    fn add_an_item_to_active_pane(
7010        cx: &mut VisualTestContext,
7011        workspace: &View<Workspace>,
7012        item_id: u64,
7013    ) -> View<TestItem> {
7014        let item = cx.new_view(|cx| {
7015            TestItem::new(cx).with_project_items(&[TestProjectItem::new(
7016                item_id,
7017                "item{item_id}.txt",
7018                cx,
7019            )])
7020        });
7021        workspace.update(cx, |workspace, cx| {
7022            workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, cx);
7023        });
7024        return item;
7025    }
7026
7027    fn split_pane(cx: &mut VisualTestContext, workspace: &View<Workspace>) -> View<Pane> {
7028        return workspace.update(cx, |workspace, cx| {
7029            let new_pane =
7030                workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
7031            new_pane
7032        });
7033    }
7034
7035    #[gpui::test]
7036    async fn test_join_all_panes(cx: &mut gpui::TestAppContext) {
7037        init_test(cx);
7038        let fs = FakeFs::new(cx.executor());
7039        let project = Project::test(fs, None, cx).await;
7040        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7041
7042        add_an_item_to_active_pane(cx, &workspace, 1);
7043        split_pane(cx, &workspace);
7044        add_an_item_to_active_pane(cx, &workspace, 2);
7045        split_pane(cx, &workspace); // empty pane
7046        split_pane(cx, &workspace);
7047        let last_item = add_an_item_to_active_pane(cx, &workspace, 3);
7048
7049        cx.executor().run_until_parked();
7050
7051        workspace.update(cx, |workspace, cx| {
7052            let num_panes = workspace.panes().len();
7053            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7054            let active_item = workspace
7055                .active_pane()
7056                .read(cx)
7057                .active_item()
7058                .expect("item is in focus");
7059
7060            assert_eq!(num_panes, 4);
7061            assert_eq!(num_items_in_current_pane, 1);
7062            assert_eq!(active_item.item_id(), last_item.item_id());
7063        });
7064
7065        workspace.update(cx, |workspace, cx| {
7066            workspace.join_all_panes(cx);
7067        });
7068
7069        workspace.update(cx, |workspace, cx| {
7070            let num_panes = workspace.panes().len();
7071            let num_items_in_current_pane = workspace.active_pane().read(cx).items().count();
7072            let active_item = workspace
7073                .active_pane()
7074                .read(cx)
7075                .active_item()
7076                .expect("item is in focus");
7077
7078            assert_eq!(num_panes, 1);
7079            assert_eq!(num_items_in_current_pane, 3);
7080            assert_eq!(active_item.item_id(), last_item.item_id());
7081        });
7082    }
7083    struct TestModal(FocusHandle);
7084
7085    impl TestModal {
7086        fn new(cx: &mut ViewContext<Self>) -> Self {
7087            Self(cx.focus_handle())
7088        }
7089    }
7090
7091    impl EventEmitter<DismissEvent> for TestModal {}
7092
7093    impl FocusableView for TestModal {
7094        fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7095            self.0.clone()
7096        }
7097    }
7098
7099    impl ModalView for TestModal {}
7100
7101    impl Render for TestModal {
7102        fn render(&mut self, _cx: &mut ViewContext<TestModal>) -> impl IntoElement {
7103            div().track_focus(&self.0)
7104        }
7105    }
7106
7107    #[gpui::test]
7108    async fn test_panels(cx: &mut gpui::TestAppContext) {
7109        init_test(cx);
7110        let fs = FakeFs::new(cx.executor());
7111
7112        let project = Project::test(fs, [], cx).await;
7113        let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
7114
7115        let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| {
7116            let panel_1 = cx.new_view(|cx| TestPanel::new(DockPosition::Left, cx));
7117            workspace.add_panel(panel_1.clone(), cx);
7118            workspace
7119                .left_dock()
7120                .update(cx, |left_dock, cx| left_dock.set_open(true, cx));
7121            let panel_2 = cx.new_view(|cx| TestPanel::new(DockPosition::Right, cx));
7122            workspace.add_panel(panel_2.clone(), cx);
7123            workspace
7124                .right_dock()
7125                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
7126
7127            let left_dock = workspace.left_dock();
7128            assert_eq!(
7129                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7130                panel_1.panel_id()
7131            );
7132            assert_eq!(
7133                left_dock.read(cx).active_panel_size(cx).unwrap(),
7134                panel_1.size(cx)
7135            );
7136
7137            left_dock.update(cx, |left_dock, cx| {
7138                left_dock.resize_active_panel(Some(px(1337.)), cx)
7139            });
7140            assert_eq!(
7141                workspace
7142                    .right_dock()
7143                    .read(cx)
7144                    .visible_panel()
7145                    .unwrap()
7146                    .panel_id(),
7147                panel_2.panel_id(),
7148            );
7149
7150            (panel_1, panel_2)
7151        });
7152
7153        // Move panel_1 to the right
7154        panel_1.update(cx, |panel_1, cx| {
7155            panel_1.set_position(DockPosition::Right, cx)
7156        });
7157
7158        workspace.update(cx, |workspace, cx| {
7159            // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
7160            // Since it was the only panel on the left, the left dock should now be closed.
7161            assert!(!workspace.left_dock().read(cx).is_open());
7162            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
7163            let right_dock = workspace.right_dock();
7164            assert_eq!(
7165                right_dock.read(cx).visible_panel().unwrap().panel_id(),
7166                panel_1.panel_id()
7167            );
7168            assert_eq!(
7169                right_dock.read(cx).active_panel_size(cx).unwrap(),
7170                px(1337.)
7171            );
7172
7173            // Now we move panel_2 to the left
7174            panel_2.set_position(DockPosition::Left, cx);
7175        });
7176
7177        workspace.update(cx, |workspace, cx| {
7178            // Since panel_2 was not visible on the right, we don't open the left dock.
7179            assert!(!workspace.left_dock().read(cx).is_open());
7180            // And the right dock is unaffected in its displaying of panel_1
7181            assert!(workspace.right_dock().read(cx).is_open());
7182            assert_eq!(
7183                workspace
7184                    .right_dock()
7185                    .read(cx)
7186                    .visible_panel()
7187                    .unwrap()
7188                    .panel_id(),
7189                panel_1.panel_id(),
7190            );
7191        });
7192
7193        // Move panel_1 back to the left
7194        panel_1.update(cx, |panel_1, cx| {
7195            panel_1.set_position(DockPosition::Left, cx)
7196        });
7197
7198        workspace.update(cx, |workspace, cx| {
7199            // Since panel_1 was visible on the right, we open the left dock and make panel_1 active.
7200            let left_dock = workspace.left_dock();
7201            assert!(left_dock.read(cx).is_open());
7202            assert_eq!(
7203                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7204                panel_1.panel_id()
7205            );
7206            assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), px(1337.));
7207            // And the right dock should be closed as it no longer has any panels.
7208            assert!(!workspace.right_dock().read(cx).is_open());
7209
7210            // Now we move panel_1 to the bottom
7211            panel_1.set_position(DockPosition::Bottom, cx);
7212        });
7213
7214        workspace.update(cx, |workspace, cx| {
7215            // Since panel_1 was visible on the left, we close the left dock.
7216            assert!(!workspace.left_dock().read(cx).is_open());
7217            // The bottom dock is sized based on the panel's default size,
7218            // since the panel orientation changed from vertical to horizontal.
7219            let bottom_dock = workspace.bottom_dock();
7220            assert_eq!(
7221                bottom_dock.read(cx).active_panel_size(cx).unwrap(),
7222                panel_1.size(cx),
7223            );
7224            // Close bottom dock and move panel_1 back to the left.
7225            bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));
7226            panel_1.set_position(DockPosition::Left, cx);
7227        });
7228
7229        // Emit activated event on panel 1
7230        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate));
7231
7232        // Now the left dock is open and panel_1 is active and focused.
7233        workspace.update(cx, |workspace, cx| {
7234            let left_dock = workspace.left_dock();
7235            assert!(left_dock.read(cx).is_open());
7236            assert_eq!(
7237                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7238                panel_1.panel_id(),
7239            );
7240            assert!(panel_1.focus_handle(cx).is_focused(cx));
7241        });
7242
7243        // Emit closed event on panel 2, which is not active
7244        panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7245
7246        // Wo don't close the left dock, because panel_2 wasn't the active panel
7247        workspace.update(cx, |workspace, cx| {
7248            let left_dock = workspace.left_dock();
7249            assert!(left_dock.read(cx).is_open());
7250            assert_eq!(
7251                left_dock.read(cx).visible_panel().unwrap().panel_id(),
7252                panel_1.panel_id(),
7253            );
7254        });
7255
7256        // Emitting a ZoomIn event shows the panel as zoomed.
7257        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn));
7258        workspace.update(cx, |workspace, _| {
7259            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7260            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
7261        });
7262
7263        // Move panel to another dock while it is zoomed
7264        panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
7265        workspace.update(cx, |workspace, _| {
7266            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7267
7268            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7269        });
7270
7271        // This is a helper for getting a:
7272        // - valid focus on an element,
7273        // - that isn't a part of the panes and panels system of the Workspace,
7274        // - and doesn't trigger the 'on_focus_lost' API.
7275        let focus_other_view = {
7276            let workspace = workspace.clone();
7277            move |cx: &mut VisualTestContext| {
7278                workspace.update(cx, |workspace, cx| {
7279                    if let Some(_) = workspace.active_modal::<TestModal>(cx) {
7280                        workspace.toggle_modal(cx, TestModal::new);
7281                        workspace.toggle_modal(cx, TestModal::new);
7282                    } else {
7283                        workspace.toggle_modal(cx, TestModal::new);
7284                    }
7285                })
7286            }
7287        };
7288
7289        // If focus is transferred to another view that's not a panel or another pane, we still show
7290        // the panel as zoomed.
7291        focus_other_view(cx);
7292        workspace.update(cx, |workspace, _| {
7293            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7294            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7295        });
7296
7297        // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
7298        workspace.update(cx, |_, cx| cx.focus_self());
7299        workspace.update(cx, |workspace, _| {
7300            assert_eq!(workspace.zoomed, None);
7301            assert_eq!(workspace.zoomed_position, None);
7302        });
7303
7304        // If focus is transferred again to another view that's not a panel or a pane, we won't
7305        // show the panel as zoomed because it wasn't zoomed before.
7306        focus_other_view(cx);
7307        workspace.update(cx, |workspace, _| {
7308            assert_eq!(workspace.zoomed, None);
7309            assert_eq!(workspace.zoomed_position, None);
7310        });
7311
7312        // When the panel is activated, it is zoomed again.
7313        cx.dispatch_action(ToggleRightDock);
7314        workspace.update(cx, |workspace, _| {
7315            assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade()));
7316            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
7317        });
7318
7319        // Emitting a ZoomOut event unzooms the panel.
7320        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut));
7321        workspace.update(cx, |workspace, _| {
7322            assert_eq!(workspace.zoomed, None);
7323            assert_eq!(workspace.zoomed_position, None);
7324        });
7325
7326        // Emit closed event on panel 1, which is active
7327        panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close));
7328
7329        // Now the left dock is closed, because panel_1 was the active panel
7330        workspace.update(cx, |workspace, cx| {
7331            let right_dock = workspace.right_dock();
7332            assert!(!right_dock.read(cx).is_open());
7333        });
7334    }
7335
7336    mod register_project_item_tests {
7337        use ui::Context as _;
7338
7339        use super::*;
7340
7341        // View
7342        struct TestPngItemView {
7343            focus_handle: FocusHandle,
7344        }
7345        // Model
7346        struct TestPngItem {}
7347
7348        impl project::Item for TestPngItem {
7349            fn try_open(
7350                _project: &Model<Project>,
7351                path: &ProjectPath,
7352                cx: &mut AppContext,
7353            ) -> Option<Task<gpui::Result<Model<Self>>>> {
7354                if path.path.extension().unwrap() == "png" {
7355                    Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestPngItem {}) }))
7356                } else {
7357                    None
7358                }
7359            }
7360
7361            fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7362                None
7363            }
7364
7365            fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7366                None
7367            }
7368        }
7369
7370        impl Item for TestPngItemView {
7371            type Event = ();
7372        }
7373        impl EventEmitter<()> for TestPngItemView {}
7374        impl FocusableView for TestPngItemView {
7375            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7376                self.focus_handle.clone()
7377            }
7378        }
7379
7380        impl Render for TestPngItemView {
7381            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7382                Empty
7383            }
7384        }
7385
7386        impl ProjectItem for TestPngItemView {
7387            type Item = TestPngItem;
7388
7389            fn for_project_item(
7390                _project: Model<Project>,
7391                _item: Model<Self::Item>,
7392                cx: &mut ViewContext<Self>,
7393            ) -> Self
7394            where
7395                Self: Sized,
7396            {
7397                Self {
7398                    focus_handle: cx.focus_handle(),
7399                }
7400            }
7401        }
7402
7403        // View
7404        struct TestIpynbItemView {
7405            focus_handle: FocusHandle,
7406        }
7407        // Model
7408        struct TestIpynbItem {}
7409
7410        impl project::Item for TestIpynbItem {
7411            fn try_open(
7412                _project: &Model<Project>,
7413                path: &ProjectPath,
7414                cx: &mut AppContext,
7415            ) -> Option<Task<gpui::Result<Model<Self>>>> {
7416                if path.path.extension().unwrap() == "ipynb" {
7417                    Some(cx.spawn(|mut cx| async move { cx.new_model(|_| TestIpynbItem {}) }))
7418                } else {
7419                    None
7420                }
7421            }
7422
7423            fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
7424                None
7425            }
7426
7427            fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
7428                None
7429            }
7430        }
7431
7432        impl Item for TestIpynbItemView {
7433            type Event = ();
7434        }
7435        impl EventEmitter<()> for TestIpynbItemView {}
7436        impl FocusableView for TestIpynbItemView {
7437            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7438                self.focus_handle.clone()
7439            }
7440        }
7441
7442        impl Render for TestIpynbItemView {
7443            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7444                Empty
7445            }
7446        }
7447
7448        impl ProjectItem for TestIpynbItemView {
7449            type Item = TestIpynbItem;
7450
7451            fn for_project_item(
7452                _project: Model<Project>,
7453                _item: Model<Self::Item>,
7454                cx: &mut ViewContext<Self>,
7455            ) -> Self
7456            where
7457                Self: Sized,
7458            {
7459                Self {
7460                    focus_handle: cx.focus_handle(),
7461                }
7462            }
7463        }
7464
7465        struct TestAlternatePngItemView {
7466            focus_handle: FocusHandle,
7467        }
7468
7469        impl Item for TestAlternatePngItemView {
7470            type Event = ();
7471        }
7472
7473        impl EventEmitter<()> for TestAlternatePngItemView {}
7474        impl FocusableView for TestAlternatePngItemView {
7475            fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
7476                self.focus_handle.clone()
7477            }
7478        }
7479
7480        impl Render for TestAlternatePngItemView {
7481            fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
7482                Empty
7483            }
7484        }
7485
7486        impl ProjectItem for TestAlternatePngItemView {
7487            type Item = TestPngItem;
7488
7489            fn for_project_item(
7490                _project: Model<Project>,
7491                _item: Model<Self::Item>,
7492                cx: &mut ViewContext<Self>,
7493            ) -> Self
7494            where
7495                Self: Sized,
7496            {
7497                Self {
7498                    focus_handle: cx.focus_handle(),
7499                }
7500            }
7501        }
7502
7503        #[gpui::test]
7504        async fn test_register_project_item(cx: &mut TestAppContext) {
7505            init_test(cx);
7506
7507            cx.update(|cx| {
7508                register_project_item::<TestPngItemView>(cx);
7509                register_project_item::<TestIpynbItemView>(cx);
7510            });
7511
7512            let fs = FakeFs::new(cx.executor());
7513            fs.insert_tree(
7514                "/root1",
7515                json!({
7516                    "one.png": "BINARYDATAHERE",
7517                    "two.ipynb": "{ totally a notebook }",
7518                    "three.txt": "editing text, sure why not?"
7519                }),
7520            )
7521            .await;
7522
7523            let project = Project::test(fs, ["root1".as_ref()], cx).await;
7524            let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7525
7526            let worktree_id = project.update(cx, |project, cx| {
7527                project.worktrees(cx).next().unwrap().read(cx).id()
7528            });
7529
7530            let handle = workspace
7531                .update(cx, |workspace, cx| {
7532                    let project_path = (worktree_id, "one.png");
7533                    workspace.open_path(project_path, None, true, cx)
7534                })
7535                .await
7536                .unwrap();
7537
7538            // Now we can check if the handle we got back errored or not
7539            assert_eq!(
7540                handle.to_any().entity_type(),
7541                TypeId::of::<TestPngItemView>()
7542            );
7543
7544            let handle = workspace
7545                .update(cx, |workspace, cx| {
7546                    let project_path = (worktree_id, "two.ipynb");
7547                    workspace.open_path(project_path, None, true, cx)
7548                })
7549                .await
7550                .unwrap();
7551
7552            assert_eq!(
7553                handle.to_any().entity_type(),
7554                TypeId::of::<TestIpynbItemView>()
7555            );
7556
7557            let handle = workspace
7558                .update(cx, |workspace, cx| {
7559                    let project_path = (worktree_id, "three.txt");
7560                    workspace.open_path(project_path, None, true, cx)
7561                })
7562                .await;
7563            assert!(handle.is_err());
7564        }
7565
7566        #[gpui::test]
7567        async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) {
7568            init_test(cx);
7569
7570            cx.update(|cx| {
7571                register_project_item::<TestPngItemView>(cx);
7572                register_project_item::<TestAlternatePngItemView>(cx);
7573            });
7574
7575            let fs = FakeFs::new(cx.executor());
7576            fs.insert_tree(
7577                "/root1",
7578                json!({
7579                    "one.png": "BINARYDATAHERE",
7580                    "two.ipynb": "{ totally a notebook }",
7581                    "three.txt": "editing text, sure why not?"
7582                }),
7583            )
7584            .await;
7585
7586            let project = Project::test(fs, ["root1".as_ref()], cx).await;
7587            let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
7588
7589            let worktree_id = project.update(cx, |project, cx| {
7590                project.worktrees(cx).next().unwrap().read(cx).id()
7591            });
7592
7593            let handle = workspace
7594                .update(cx, |workspace, cx| {
7595                    let project_path = (worktree_id, "one.png");
7596                    workspace.open_path(project_path, None, true, cx)
7597                })
7598                .await
7599                .unwrap();
7600
7601            // This _must_ be the second item registered
7602            assert_eq!(
7603                handle.to_any().entity_type(),
7604                TypeId::of::<TestAlternatePngItemView>()
7605            );
7606
7607            let handle = workspace
7608                .update(cx, |workspace, cx| {
7609                    let project_path = (worktree_id, "three.txt");
7610                    workspace.open_path(project_path, None, true, cx)
7611                })
7612                .await;
7613            assert!(handle.is_err());
7614        }
7615    }
7616
7617    pub fn init_test(cx: &mut TestAppContext) {
7618        cx.update(|cx| {
7619            let settings_store = SettingsStore::test(cx);
7620            cx.set_global(settings_store);
7621            theme::init(theme::LoadThemes::JustBase, cx);
7622            language::init(cx);
7623            crate::init_settings(cx);
7624            Project::init_settings(cx);
7625        });
7626    }
7627}