remote_editing_collaboration_tests.rs

   1use crate::TestServer;
   2use call::ActiveCall;
   3use collections::{HashMap, HashSet};
   4
   5use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling};
   6use debugger_ui::debugger_panel::DebugPanel;
   7use editor::{Editor, EditorMode, MultiBuffer};
   8use extension::ExtensionHostProxy;
   9use fs::{FakeFs, Fs as _, RemoveOptions};
  10use futures::StreamExt as _;
  11use gpui::{
  12    AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as _, VisualContext as _,
  13};
  14use http_client::BlockedHttpClient;
  15use language::{
  16    FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
  17    language_settings::{Formatter, FormatterList, language_settings},
  18    rust_lang, tree_sitter_typescript,
  19};
  20use node_runtime::NodeRuntime;
  21use project::{
  22    ProjectPath,
  23    debugger::session::ThreadId,
  24    lsp_store::{FormatTrigger, LspFormatTarget},
  25    trusted_worktrees::{PathTrust, TrustedWorktrees},
  26};
  27use remote::RemoteClient;
  28use remote_server::{HeadlessAppState, HeadlessProject};
  29use rpc::proto;
  30use serde_json::json;
  31use settings::{
  32    InlayHintSettingsContent, LanguageServerFormatterSpecifier, PrettierSettingsContent,
  33    SettingsStore,
  34};
  35use std::{
  36    path::Path,
  37    sync::{
  38        Arc,
  39        atomic::{AtomicUsize, Ordering},
  40    },
  41    time::Duration,
  42};
  43use task::TcpArgumentsTemplate;
  44use util::{path, rel_path::rel_path};
  45
  46#[gpui::test(iterations = 10)]
  47async fn test_sharing_an_ssh_remote_project(
  48    cx_a: &mut TestAppContext,
  49    cx_b: &mut TestAppContext,
  50    server_cx: &mut TestAppContext,
  51) {
  52    let executor = cx_a.executor();
  53    cx_a.update(|cx| {
  54        release_channel::init(semver::Version::new(0, 0, 0), cx);
  55    });
  56    server_cx.update(|cx| {
  57        release_channel::init(semver::Version::new(0, 0, 0), cx);
  58    });
  59    let mut server = TestServer::start(executor.clone()).await;
  60    let client_a = server.create_client(cx_a, "user_a").await;
  61    let client_b = server.create_client(cx_b, "user_b").await;
  62    server
  63        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
  64        .await;
  65
  66    // Set up project on remote FS
  67    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
  68    let remote_fs = FakeFs::new(server_cx.executor());
  69    remote_fs
  70        .insert_tree(
  71            path!("/code"),
  72            json!({
  73                "project1": {
  74                    ".zed": {
  75                        "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
  76                    },
  77                    "README.md": "# project 1",
  78                    "src": {
  79                        "lib.rs": "fn one() -> usize { 1 }"
  80                    }
  81                },
  82                "project2": {
  83                    "README.md": "# project 2",
  84                },
  85            }),
  86        )
  87        .await;
  88
  89    // User A connects to the remote project via SSH.
  90    server_cx.update(HeadlessProject::init);
  91    let remote_http_client = Arc::new(BlockedHttpClient);
  92    let node = NodeRuntime::unavailable();
  93    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
  94    let _headless_project = server_cx.new(|cx| {
  95        HeadlessProject::new(
  96            HeadlessAppState {
  97                session: server_ssh,
  98                fs: remote_fs.clone(),
  99                http_client: remote_http_client,
 100                node_runtime: node,
 101                languages,
 102                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 103                startup_time: std::time::Instant::now(),
 104            },
 105            false,
 106            cx,
 107        )
 108    });
 109
 110    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 111    let (project_a, worktree_id) = client_a
 112        .build_ssh_project(path!("/code/project1"), client_ssh, false, cx_a)
 113        .await;
 114
 115    // While the SSH worktree is being scanned, user A shares the remote project.
 116    let active_call_a = cx_a.read(ActiveCall::global);
 117    let project_id = active_call_a
 118        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 119        .await
 120        .unwrap();
 121
 122    // User B joins the project.
 123    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 124    let worktree_b = project_b
 125        .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
 126        .unwrap();
 127
 128    let worktree_a = project_a
 129        .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
 130        .unwrap();
 131
 132    executor.run_until_parked();
 133
 134    worktree_a.update(cx_a, |worktree, _cx| {
 135        assert_eq!(
 136            worktree.paths().collect::<Vec<_>>(),
 137            vec![
 138                rel_path(".zed"),
 139                rel_path(".zed/settings.json"),
 140                rel_path("README.md"),
 141                rel_path("src"),
 142                rel_path("src/lib.rs"),
 143            ]
 144        );
 145    });
 146
 147    worktree_b.update(cx_b, |worktree, _cx| {
 148        assert_eq!(
 149            worktree.paths().collect::<Vec<_>>(),
 150            vec![
 151                rel_path(".zed"),
 152                rel_path(".zed/settings.json"),
 153                rel_path("README.md"),
 154                rel_path("src"),
 155                rel_path("src/lib.rs"),
 156            ]
 157        );
 158    });
 159
 160    // User B can open buffers in the remote project.
 161    let buffer_b = project_b
 162        .update(cx_b, |project, cx| {
 163            project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
 164        })
 165        .await
 166        .unwrap();
 167    buffer_b.update(cx_b, |buffer, cx| {
 168        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
 169        let ix = buffer.text().find('1').unwrap();
 170        buffer.edit([(ix..ix + 1, "100")], None, cx);
 171    });
 172
 173    executor.run_until_parked();
 174
 175    cx_b.read(|cx| {
 176        let file = buffer_b.read(cx).file();
 177        assert_eq!(
 178            language_settings(Some("Rust".into()), file, cx).language_servers,
 179            ["override-rust-analyzer".to_string()]
 180        )
 181    });
 182
 183    project_b
 184        .update(cx_b, |project, cx| {
 185            project.save_buffer_as(
 186                buffer_b.clone(),
 187                ProjectPath {
 188                    worktree_id: worktree_id.to_owned(),
 189                    path: rel_path("src/renamed.rs").into(),
 190                },
 191                cx,
 192            )
 193        })
 194        .await
 195        .unwrap();
 196    assert_eq!(
 197        remote_fs
 198            .load(path!("/code/project1/src/renamed.rs").as_ref())
 199            .await
 200            .unwrap(),
 201        "fn one() -> usize { 100 }"
 202    );
 203    cx_b.run_until_parked();
 204    cx_b.update(|cx| {
 205        assert_eq!(
 206            buffer_b.read(cx).file().unwrap().path().as_ref(),
 207            rel_path("src/renamed.rs")
 208        );
 209    });
 210}
 211
 212#[gpui::test]
 213async fn test_ssh_collaboration_git_branches(
 214    executor: BackgroundExecutor,
 215    cx_a: &mut TestAppContext,
 216    cx_b: &mut TestAppContext,
 217    server_cx: &mut TestAppContext,
 218) {
 219    cx_a.set_name("a");
 220    cx_b.set_name("b");
 221    server_cx.set_name("server");
 222
 223    cx_a.update(|cx| {
 224        release_channel::init(semver::Version::new(0, 0, 0), cx);
 225    });
 226    server_cx.update(|cx| {
 227        release_channel::init(semver::Version::new(0, 0, 0), cx);
 228    });
 229
 230    let mut server = TestServer::start(executor.clone()).await;
 231    let client_a = server.create_client(cx_a, "user_a").await;
 232    let client_b = server.create_client(cx_b, "user_b").await;
 233    server
 234        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 235        .await;
 236
 237    // Set up project on remote FS
 238    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 239    let remote_fs = FakeFs::new(server_cx.executor());
 240    remote_fs
 241        .insert_tree("/project", serde_json::json!({ ".git":{} }))
 242        .await;
 243
 244    let branches = ["main", "dev", "feature-1"];
 245    let branches_set = branches
 246        .iter()
 247        .map(ToString::to_string)
 248        .collect::<HashSet<_>>();
 249    remote_fs.insert_branches(Path::new("/project/.git"), &branches);
 250
 251    // User A connects to the remote project via SSH.
 252    server_cx.update(HeadlessProject::init);
 253    let remote_http_client = Arc::new(BlockedHttpClient);
 254    let node = NodeRuntime::unavailable();
 255    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 256    let headless_project = server_cx.new(|cx| {
 257        HeadlessProject::new(
 258            HeadlessAppState {
 259                session: server_ssh,
 260                fs: remote_fs.clone(),
 261                http_client: remote_http_client,
 262                node_runtime: node,
 263                languages,
 264                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 265                startup_time: std::time::Instant::now(),
 266            },
 267            false,
 268            cx,
 269        )
 270    });
 271
 272    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 273    let (project_a, _) = client_a
 274        .build_ssh_project("/project", client_ssh, false, cx_a)
 275        .await;
 276
 277    // While the SSH worktree is being scanned, user A shares the remote project.
 278    let active_call_a = cx_a.read(ActiveCall::global);
 279    let project_id = active_call_a
 280        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 281        .await
 282        .unwrap();
 283
 284    // User B joins the project.
 285    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 286
 287    // Give client A sometime to see that B has joined, and that the headless server
 288    // has some git repositories
 289    executor.run_until_parked();
 290
 291    let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
 292
 293    let branches_b = cx_b
 294        .update(|cx| repo_b.update(cx, |repo_b, _cx| repo_b.branches()))
 295        .await
 296        .unwrap()
 297        .unwrap();
 298
 299    let new_branch = branches[2];
 300
 301    let branches_b = branches_b
 302        .into_iter()
 303        .map(|branch| branch.name().to_string())
 304        .collect::<HashSet<_>>();
 305
 306    assert_eq!(&branches_b, &branches_set);
 307
 308    cx_b.update(|cx| {
 309        repo_b.update(cx, |repo_b, _cx| {
 310            repo_b.change_branch(new_branch.to_string())
 311        })
 312    })
 313    .await
 314    .unwrap()
 315    .unwrap();
 316
 317    executor.run_until_parked();
 318
 319    let server_branch = server_cx.update(|cx| {
 320        headless_project.update(cx, |headless_project, cx| {
 321            headless_project.git_store.update(cx, |git_store, cx| {
 322                git_store
 323                    .repositories()
 324                    .values()
 325                    .next()
 326                    .unwrap()
 327                    .read(cx)
 328                    .branch
 329                    .as_ref()
 330                    .unwrap()
 331                    .clone()
 332            })
 333        })
 334    });
 335
 336    assert_eq!(server_branch.name(), branches[2]);
 337
 338    // Also try creating a new branch
 339    cx_b.update(|cx| {
 340        repo_b.update(cx, |repo_b, _cx| {
 341            repo_b.create_branch("totally-new-branch".to_string(), None)
 342        })
 343    })
 344    .await
 345    .unwrap()
 346    .unwrap();
 347
 348    cx_b.update(|cx| {
 349        repo_b.update(cx, |repo_b, _cx| {
 350            repo_b.change_branch("totally-new-branch".to_string())
 351        })
 352    })
 353    .await
 354    .unwrap()
 355    .unwrap();
 356
 357    executor.run_until_parked();
 358
 359    let server_branch = server_cx.update(|cx| {
 360        headless_project.update(cx, |headless_project, cx| {
 361            headless_project.git_store.update(cx, |git_store, cx| {
 362                git_store
 363                    .repositories()
 364                    .values()
 365                    .next()
 366                    .unwrap()
 367                    .read(cx)
 368                    .branch
 369                    .as_ref()
 370                    .unwrap()
 371                    .clone()
 372            })
 373        })
 374    });
 375
 376    assert_eq!(server_branch.name(), "totally-new-branch");
 377
 378    // Remove the git repository and check that all participants get the update.
 379    remote_fs
 380        .remove_dir("/project/.git".as_ref(), RemoveOptions::default())
 381        .await
 382        .unwrap();
 383    executor.run_until_parked();
 384
 385    project_a.update(cx_a, |project, cx| {
 386        pretty_assertions::assert_eq!(
 387            project.git_store().read(cx).repo_snapshots(cx),
 388            HashMap::default()
 389        );
 390    });
 391    project_b.update(cx_b, |project, cx| {
 392        pretty_assertions::assert_eq!(
 393            project.git_store().read(cx).repo_snapshots(cx),
 394            HashMap::default()
 395        );
 396    });
 397}
 398
 399#[gpui::test]
 400async fn test_ssh_collaboration_formatting_with_prettier(
 401    executor: BackgroundExecutor,
 402    cx_a: &mut TestAppContext,
 403    cx_b: &mut TestAppContext,
 404    server_cx: &mut TestAppContext,
 405) {
 406    cx_a.set_name("a");
 407    cx_b.set_name("b");
 408    server_cx.set_name("server");
 409
 410    cx_a.update(|cx| {
 411        release_channel::init(semver::Version::new(0, 0, 0), cx);
 412    });
 413    server_cx.update(|cx| {
 414        release_channel::init(semver::Version::new(0, 0, 0), cx);
 415    });
 416
 417    let mut server = TestServer::start(executor.clone()).await;
 418    let client_a = server.create_client(cx_a, "user_a").await;
 419    let client_b = server.create_client(cx_b, "user_b").await;
 420    server
 421        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 422        .await;
 423
 424    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 425    let remote_fs = FakeFs::new(server_cx.executor());
 426    let buffer_text = "let one = \"two\"";
 427    let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
 428    remote_fs
 429        .insert_tree(
 430            path!("/project"),
 431            serde_json::json!({ "a.ts": buffer_text }),
 432        )
 433        .await;
 434
 435    let test_plugin = "test_plugin";
 436    let ts_lang = Arc::new(Language::new(
 437        LanguageConfig {
 438            name: "TypeScript".into(),
 439            matcher: LanguageMatcher {
 440                path_suffixes: vec!["ts".to_string()],
 441                ..LanguageMatcher::default()
 442            },
 443            ..LanguageConfig::default()
 444        },
 445        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
 446    ));
 447    client_a.language_registry().add(ts_lang.clone());
 448    client_b.language_registry().add(ts_lang.clone());
 449
 450    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 451    let mut fake_language_servers = languages.register_fake_lsp(
 452        "TypeScript",
 453        FakeLspAdapter {
 454            prettier_plugins: vec![test_plugin],
 455            ..Default::default()
 456        },
 457    );
 458
 459    // User A connects to the remote project via SSH.
 460    server_cx.update(HeadlessProject::init);
 461    let remote_http_client = Arc::new(BlockedHttpClient);
 462    let _headless_project = server_cx.new(|cx| {
 463        HeadlessProject::new(
 464            HeadlessAppState {
 465                session: server_ssh,
 466                fs: remote_fs.clone(),
 467                http_client: remote_http_client,
 468                node_runtime: NodeRuntime::unavailable(),
 469                languages,
 470                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 471                startup_time: std::time::Instant::now(),
 472            },
 473            false,
 474            cx,
 475        )
 476    });
 477
 478    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 479    let (project_a, worktree_id) = client_a
 480        .build_ssh_project(path!("/project"), client_ssh, false, cx_a)
 481        .await;
 482
 483    // While the SSH worktree is being scanned, user A shares the remote project.
 484    let active_call_a = cx_a.read(ActiveCall::global);
 485    let project_id = active_call_a
 486        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
 487        .await
 488        .unwrap();
 489
 490    // User B joins the project.
 491    let project_b = client_b.join_remote_project(project_id, cx_b).await;
 492    executor.run_until_parked();
 493
 494    // Opens the buffer and formats it
 495    let (buffer_b, _handle) = project_b
 496        .update(cx_b, |p, cx| {
 497            p.open_buffer_with_lsp((worktree_id, rel_path("a.ts")), cx)
 498        })
 499        .await
 500        .expect("user B opens buffer for formatting");
 501
 502    cx_a.update(|cx| {
 503        SettingsStore::update_global(cx, |store, cx| {
 504            store.update_user_settings(cx, |file| {
 505                file.project.all_languages.defaults.formatter = Some(FormatterList::default());
 506                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
 507                    allowed: Some(true),
 508                    ..Default::default()
 509                });
 510            });
 511        });
 512    });
 513    cx_b.update(|cx| {
 514        SettingsStore::update_global(cx, |store, cx| {
 515            store.update_user_settings(cx, |file| {
 516                file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
 517                    Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
 518                ));
 519                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
 520                    allowed: Some(true),
 521                    ..Default::default()
 522                });
 523            });
 524        });
 525    });
 526    let fake_language_server = fake_language_servers.next().await.unwrap();
 527    fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
 528        panic!(
 529            "Unexpected: prettier should be preferred since it's enabled and language supports it"
 530        )
 531    });
 532
 533    project_b
 534        .update(cx_b, |project, cx| {
 535            project.format(
 536                HashSet::from_iter([buffer_b.clone()]),
 537                LspFormatTarget::Buffers,
 538                true,
 539                FormatTrigger::Save,
 540                cx,
 541            )
 542        })
 543        .await
 544        .unwrap();
 545
 546    executor.run_until_parked();
 547    assert_eq!(
 548        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
 549        buffer_text.to_string() + "\n" + prettier_format_suffix,
 550        "Prettier formatting was not applied to client buffer after client's request"
 551    );
 552
 553    // User A opens and formats the same buffer too
 554    let buffer_a = project_a
 555        .update(cx_a, |p, cx| {
 556            p.open_buffer((worktree_id, rel_path("a.ts")), cx)
 557        })
 558        .await
 559        .expect("user A opens buffer for formatting");
 560
 561    cx_a.update(|cx| {
 562        SettingsStore::update_global(cx, |store, cx| {
 563            store.update_user_settings(cx, |file| {
 564                file.project.all_languages.defaults.formatter = Some(FormatterList::default());
 565                file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
 566                    allowed: Some(true),
 567                    ..Default::default()
 568                });
 569            });
 570        });
 571    });
 572    project_a
 573        .update(cx_a, |project, cx| {
 574            project.format(
 575                HashSet::from_iter([buffer_a.clone()]),
 576                LspFormatTarget::Buffers,
 577                true,
 578                FormatTrigger::Manual,
 579                cx,
 580            )
 581        })
 582        .await
 583        .unwrap();
 584
 585    executor.run_until_parked();
 586    assert_eq!(
 587        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
 588        buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
 589        "Prettier formatting was not applied to client buffer after host's request"
 590    );
 591}
 592
 593#[gpui::test]
 594async fn test_remote_server_debugger(
 595    cx_a: &mut TestAppContext,
 596    server_cx: &mut TestAppContext,
 597    executor: BackgroundExecutor,
 598) {
 599    cx_a.update(|cx| {
 600        release_channel::init(semver::Version::new(0, 0, 0), cx);
 601        command_palette_hooks::init(cx);
 602        zlog::init_test();
 603        dap_adapters::init(cx);
 604    });
 605    server_cx.update(|cx| {
 606        release_channel::init(semver::Version::new(0, 0, 0), cx);
 607        dap_adapters::init(cx);
 608    });
 609    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 610    let remote_fs = FakeFs::new(server_cx.executor());
 611    remote_fs
 612        .insert_tree(
 613            path!("/code"),
 614            json!({
 615                "lib.rs": "fn one() -> usize { 1 }"
 616            }),
 617        )
 618        .await;
 619
 620    // User A connects to the remote project via SSH.
 621    server_cx.update(HeadlessProject::init);
 622    let remote_http_client = Arc::new(BlockedHttpClient);
 623    let node = NodeRuntime::unavailable();
 624    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 625    let _headless_project = server_cx.new(|cx| {
 626        HeadlessProject::new(
 627            HeadlessAppState {
 628                session: server_ssh,
 629                fs: remote_fs.clone(),
 630                http_client: remote_http_client,
 631                node_runtime: node,
 632                languages,
 633                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 634                startup_time: std::time::Instant::now(),
 635            },
 636            false,
 637            cx,
 638        )
 639    });
 640
 641    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 642    let mut server = TestServer::start(server_cx.executor()).await;
 643    let client_a = server.create_client(cx_a, "user_a").await;
 644    cx_a.update(|cx| {
 645        debugger_ui::init(cx);
 646        command_palette_hooks::init(cx);
 647    });
 648    let (project_a, _) = client_a
 649        .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
 650        .await;
 651
 652    let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
 653
 654    let debugger_panel = workspace
 655        .update_in(cx_a, |_workspace, window, cx| {
 656            cx.spawn_in(window, DebugPanel::load)
 657        })
 658        .await
 659        .unwrap();
 660
 661    workspace.update_in(cx_a, |workspace, window, cx| {
 662        workspace.add_panel(debugger_panel, window, cx);
 663    });
 664
 665    cx_a.run_until_parked();
 666    let debug_panel = workspace
 667        .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
 668        .unwrap();
 669
 670    let workspace_window = cx_a
 671        .window_handle()
 672        .downcast::<workspace::MultiWorkspace>()
 673        .unwrap();
 674
 675    let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
 676    cx_a.run_until_parked();
 677    debug_panel.update(cx_a, |debug_panel, cx| {
 678        assert_eq!(
 679            debug_panel.active_session().unwrap().read(cx).session(cx),
 680            session.clone()
 681        )
 682    });
 683
 684    session.update(
 685        cx_a,
 686        |session: &mut project::debugger::session::Session, _| {
 687            assert_eq!(session.binary().unwrap().command.as_deref(), Some("mock"));
 688        },
 689    );
 690
 691    let shutdown_session = workspace.update(cx_a, |workspace, cx| {
 692        workspace.project().update(cx, |project, cx| {
 693            project.dap_store().update(cx, |dap_store, cx| {
 694                dap_store.shutdown_session(session.read(cx).session_id(), cx)
 695            })
 696        })
 697    });
 698
 699    client_ssh.update(cx_a, |a, _| {
 700        a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
 701    });
 702
 703    shutdown_session.await.unwrap();
 704}
 705
 706#[gpui::test]
 707async fn test_slow_adapter_startup_retries(
 708    cx_a: &mut TestAppContext,
 709    server_cx: &mut TestAppContext,
 710    executor: BackgroundExecutor,
 711) {
 712    cx_a.update(|cx| {
 713        release_channel::init(semver::Version::new(0, 0, 0), cx);
 714        command_palette_hooks::init(cx);
 715        zlog::init_test();
 716        dap_adapters::init(cx);
 717    });
 718    server_cx.update(|cx| {
 719        release_channel::init(semver::Version::new(0, 0, 0), cx);
 720        dap_adapters::init(cx);
 721    });
 722    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 723    let remote_fs = FakeFs::new(server_cx.executor());
 724    remote_fs
 725        .insert_tree(
 726            path!("/code"),
 727            json!({
 728                "lib.rs": "fn one() -> usize { 1 }"
 729            }),
 730        )
 731        .await;
 732
 733    // User A connects to the remote project via SSH.
 734    server_cx.update(HeadlessProject::init);
 735    let remote_http_client = Arc::new(BlockedHttpClient);
 736    let node = NodeRuntime::unavailable();
 737    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 738    let _headless_project = server_cx.new(|cx| {
 739        HeadlessProject::new(
 740            HeadlessAppState {
 741                session: server_ssh,
 742                fs: remote_fs.clone(),
 743                http_client: remote_http_client,
 744                node_runtime: node,
 745                languages,
 746                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 747                startup_time: std::time::Instant::now(),
 748            },
 749            false,
 750            cx,
 751        )
 752    });
 753
 754    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 755    let mut server = TestServer::start(server_cx.executor()).await;
 756    let client_a = server.create_client(cx_a, "user_a").await;
 757    cx_a.update(|cx| {
 758        debugger_ui::init(cx);
 759        command_palette_hooks::init(cx);
 760    });
 761    let (project_a, _) = client_a
 762        .build_ssh_project(path!("/code"), client_ssh.clone(), false, cx_a)
 763        .await;
 764
 765    let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
 766
 767    let debugger_panel = workspace
 768        .update_in(cx_a, |_workspace, window, cx| {
 769            cx.spawn_in(window, DebugPanel::load)
 770        })
 771        .await
 772        .unwrap();
 773
 774    workspace.update_in(cx_a, |workspace, window, cx| {
 775        workspace.add_panel(debugger_panel, window, cx);
 776    });
 777
 778    cx_a.run_until_parked();
 779    let debug_panel = workspace
 780        .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
 781        .unwrap();
 782
 783    let workspace_window = cx_a
 784        .window_handle()
 785        .downcast::<workspace::MultiWorkspace>()
 786        .unwrap();
 787
 788    let count = Arc::new(AtomicUsize::new(0));
 789    let session = debugger_ui::tests::start_debug_session_with(
 790        &workspace_window,
 791        cx_a,
 792        DebugTaskDefinition {
 793            adapter: "fake-adapter".into(),
 794            label: "test".into(),
 795            config: json!({
 796                "request": "launch"
 797            }),
 798            tcp_connection: Some(TcpArgumentsTemplate {
 799                port: None,
 800                host: None,
 801                timeout: None,
 802            }),
 803        },
 804        move |client| {
 805            let count = count.clone();
 806            client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| {
 807                if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 {
 808                    return RequestHandling::Exit;
 809                }
 810                RequestHandling::Respond(Ok(Capabilities::default()))
 811            });
 812        },
 813    )
 814    .unwrap();
 815    cx_a.run_until_parked();
 816
 817    let client = session.update(
 818        cx_a,
 819        |session: &mut project::debugger::session::Session, _| session.adapter_client().unwrap(),
 820    );
 821    client
 822        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
 823            reason: dap::StoppedEventReason::Pause,
 824            description: None,
 825            thread_id: Some(1),
 826            preserve_focus_hint: None,
 827            text: None,
 828            all_threads_stopped: None,
 829            hit_breakpoint_ids: None,
 830        }))
 831        .await;
 832
 833    cx_a.run_until_parked();
 834
 835    let active_session = debug_panel
 836        .update(cx_a, |this, _| this.active_session())
 837        .unwrap();
 838
 839    let running_state = active_session.update(cx_a, |active_session, _| {
 840        active_session.running_state().clone()
 841    });
 842
 843    assert_eq!(
 844        client.id(),
 845        running_state.read_with(cx_a, |running_state, _| running_state.session_id())
 846    );
 847    assert_eq!(
 848        ThreadId(1),
 849        running_state.read_with(cx_a, |running_state, _| running_state
 850            .selected_thread_id()
 851            .unwrap())
 852    );
 853
 854    let shutdown_session = workspace.update(cx_a, |workspace, cx| {
 855        workspace.project().update(cx, |project, cx| {
 856            project.dap_store().update(cx, |dap_store, cx| {
 857                dap_store.shutdown_session(session.read(cx).session_id(), cx)
 858            })
 859        })
 860    });
 861
 862    client_ssh.update(cx_a, |a, _| {
 863        a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
 864    });
 865
 866    shutdown_session.await.unwrap();
 867}
 868
 869#[gpui::test]
 870async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
 871    cx_a.update(|cx| {
 872        release_channel::init(semver::Version::new(0, 0, 0), cx);
 873        project::trusted_worktrees::init(HashMap::default(), cx);
 874    });
 875    server_cx.update(|cx| {
 876        release_channel::init(semver::Version::new(0, 0, 0), cx);
 877        project::trusted_worktrees::init(HashMap::default(), cx);
 878    });
 879
 880    let mut server = TestServer::start(cx_a.executor().clone()).await;
 881    let client_a = server.create_client(cx_a, "user_a").await;
 882
 883    let server_name = "override-rust-analyzer";
 884    let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
 885
 886    let (opts, server_ssh, _) = RemoteClient::fake_server(cx_a, server_cx);
 887    let remote_fs = FakeFs::new(server_cx.executor());
 888    remote_fs
 889        .insert_tree(
 890            path!("/projects"),
 891            json!({
 892                "project_a": {
 893                    ".zed": {
 894                        "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
 895                    },
 896                    "main.rs": "fn main() {}"
 897                },
 898                "project_b": { "lib.rs": "pub fn lib() {}" }
 899            }),
 900        )
 901        .await;
 902
 903    server_cx.update(HeadlessProject::init);
 904    let remote_http_client = Arc::new(BlockedHttpClient);
 905    let node = NodeRuntime::unavailable();
 906    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 907    languages.add(rust_lang());
 908
 909    let capabilities = lsp::ServerCapabilities {
 910        inlay_hint_provider: Some(lsp::OneOf::Left(true)),
 911        ..lsp::ServerCapabilities::default()
 912    };
 913    let mut fake_language_servers = languages.register_fake_lsp(
 914        "Rust",
 915        FakeLspAdapter {
 916            name: server_name,
 917            capabilities: capabilities.clone(),
 918            initializer: Some(Box::new({
 919                let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
 920                move |fake_server| {
 921                    let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
 922                    fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
 923                        move |_params, _| {
 924                            lsp_inlay_hint_request_count.fetch_add(1, Ordering::Release);
 925                            async move {
 926                                Ok(Some(vec![lsp::InlayHint {
 927                                    position: lsp::Position::new(0, 0),
 928                                    label: lsp::InlayHintLabel::String("hint".to_string()),
 929                                    kind: None,
 930                                    text_edits: None,
 931                                    tooltip: None,
 932                                    padding_left: None,
 933                                    padding_right: None,
 934                                    data: None,
 935                                }]))
 936                            }
 937                        },
 938                    );
 939                }
 940            })),
 941            ..FakeLspAdapter::default()
 942        },
 943    );
 944
 945    let _headless_project = server_cx.new(|cx| {
 946        HeadlessProject::new(
 947            HeadlessAppState {
 948                session: server_ssh,
 949                fs: remote_fs.clone(),
 950                http_client: remote_http_client,
 951                node_runtime: node,
 952                languages,
 953                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 954                startup_time: std::time::Instant::now(),
 955            },
 956            true,
 957            cx,
 958        )
 959    });
 960
 961    let client_ssh = RemoteClient::connect_mock(opts, cx_a).await;
 962    let (project_a, worktree_id_a) = client_a
 963        .build_ssh_project(path!("/projects/project_a"), client_ssh.clone(), true, cx_a)
 964        .await;
 965
 966    cx_a.update(|cx| {
 967        release_channel::init(semver::Version::new(0, 0, 0), cx);
 968
 969        SettingsStore::update_global(cx, |store, cx| {
 970            store.update_user_settings(cx, |settings| {
 971                let language_settings = &mut settings.project.all_languages.defaults;
 972                language_settings.inlay_hints = Some(InlayHintSettingsContent {
 973                    enabled: Some(true),
 974                    ..InlayHintSettingsContent::default()
 975                })
 976            });
 977        });
 978    });
 979
 980    project_a
 981        .update(cx_a, |project, cx| {
 982            project.languages().add(rust_lang());
 983            project.languages().register_fake_lsp_adapter(
 984                "Rust",
 985                FakeLspAdapter {
 986                    name: server_name,
 987                    capabilities,
 988                    ..FakeLspAdapter::default()
 989                },
 990            );
 991            project.find_or_create_worktree(path!("/projects/project_b"), true, cx)
 992        })
 993        .await
 994        .unwrap();
 995
 996    cx_a.run_until_parked();
 997
 998    let worktree_ids = project_a.read_with(cx_a, |project, cx| {
 999        project
1000            .worktrees(cx)
1001            .map(|wt| wt.read(cx).id())
1002            .collect::<Vec<_>>()
1003    });
1004    assert_eq!(worktree_ids.len(), 2);
1005
1006    let trusted_worktrees =
1007        cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
1008    let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
1009
1010    let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1011        store.can_trust(&worktree_store, worktree_ids[0], cx)
1012    });
1013    let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1014        store.can_trust(&worktree_store, worktree_ids[1], cx)
1015    });
1016    assert!(!can_trust_a, "project_a should be restricted initially");
1017    assert!(!can_trust_b, "project_b should be restricted initially");
1018
1019    let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
1020        store.has_restricted_worktrees(&worktree_store, cx)
1021    });
1022    assert!(has_restricted, "should have restricted worktrees");
1023
1024    let buffer_before_approval = project_a
1025        .update(cx_a, |project, cx| {
1026            project.open_buffer((worktree_id_a, rel_path("main.rs")), cx)
1027        })
1028        .await
1029        .unwrap();
1030
1031    let (editor, cx_a) = cx_a.add_window_view(|window, cx| {
1032        Editor::new(
1033            EditorMode::full(),
1034            cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
1035            Some(project_a.clone()),
1036            window,
1037            cx,
1038        )
1039    });
1040    cx_a.run_until_parked();
1041    let fake_language_server = fake_language_servers.next();
1042
1043    cx_a.read(|cx| {
1044        let file = buffer_before_approval.read(cx).file();
1045        assert_eq!(
1046            language_settings(Some("Rust".into()), file, cx).language_servers,
1047            ["...".to_string()],
1048            "remote .zed/settings.json must not sync before trust approval"
1049        )
1050    });
1051
1052    editor.update_in(cx_a, |editor, window, cx| {
1053        editor.handle_input("1", window, cx);
1054    });
1055    cx_a.run_until_parked();
1056    cx_a.executor().advance_clock(Duration::from_secs(1));
1057    assert_eq!(
1058        lsp_inlay_hint_request_count.load(Ordering::Acquire),
1059        0,
1060        "inlay hints must not be queried before trust approval"
1061    );
1062
1063    trusted_worktrees.update(cx_a, |store, cx| {
1064        store.trust(
1065            &worktree_store,
1066            HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
1067            cx,
1068        );
1069    });
1070    cx_a.run_until_parked();
1071
1072    cx_a.read(|cx| {
1073        let file = buffer_before_approval.read(cx).file();
1074        assert_eq!(
1075            language_settings(Some("Rust".into()), file, cx).language_servers,
1076            ["override-rust-analyzer".to_string()],
1077            "remote .zed/settings.json should sync after trust approval"
1078        )
1079    });
1080    let _fake_language_server = fake_language_server.await.unwrap();
1081    editor.update_in(cx_a, |editor, window, cx| {
1082        editor.handle_input("1", window, cx);
1083    });
1084    cx_a.run_until_parked();
1085    cx_a.executor().advance_clock(Duration::from_secs(1));
1086    assert!(
1087        lsp_inlay_hint_request_count.load(Ordering::Acquire) > 0,
1088        "inlay hints should be queried after trust approval"
1089    );
1090
1091    let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1092        store.can_trust(&worktree_store, worktree_ids[0], cx)
1093    });
1094    let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1095        store.can_trust(&worktree_store, worktree_ids[1], cx)
1096    });
1097    assert!(can_trust_a, "project_a should be trusted after trust()");
1098    assert!(!can_trust_b, "project_b should still be restricted");
1099
1100    trusted_worktrees.update(cx_a, |store, cx| {
1101        store.trust(
1102            &worktree_store,
1103            HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
1104            cx,
1105        );
1106    });
1107
1108    let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
1109        store.can_trust(&worktree_store, worktree_ids[0], cx)
1110    });
1111    let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
1112        store.can_trust(&worktree_store, worktree_ids[1], cx)
1113    });
1114    assert!(can_trust_a, "project_a should remain trusted");
1115    assert!(can_trust_b, "project_b should now be trusted");
1116
1117    let has_restricted_after = trusted_worktrees.read_with(cx_a, |store, cx| {
1118        store.has_restricted_worktrees(&worktree_store, cx)
1119    });
1120    assert!(
1121        !has_restricted_after,
1122        "should have no restricted worktrees after trusting both"
1123    );
1124}