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