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