workspace.rs

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