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