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