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