remote_editing_collaboration_tests.rs

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