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