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