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