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