remote_editing_collaboration_tests.rs

  1use crate::tests::TestServer;
  2use call::ActiveCall;
  3use collections::{HashMap, HashSet};
  4
  5use debugger_ui::debugger_panel::DebugPanel;
  6use extension::ExtensionHostProxy;
  7use fs::{FakeFs, Fs as _, RemoveOptions};
  8use futures::StreamExt as _;
  9use gpui::{
 10    AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
 11    VisualContext,
 12};
 13use http_client::BlockedHttpClient;
 14use language::{
 15    FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
 16    language_settings::{
 17        AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
 18        language_settings,
 19    },
 20    tree_sitter_typescript,
 21};
 22use node_runtime::NodeRuntime;
 23use project::{
 24    ProjectPath,
 25    lsp_store::{FormatTrigger, LspFormatTarget},
 26};
 27use remote::SshRemoteClient;
 28use remote_server::{HeadlessAppState, HeadlessProject};
 29use rpc::proto;
 30use serde_json::json;
 31use settings::SettingsStore;
 32use std::{path::Path, sync::Arc};
 33use util::{path, separator};
 34
 35#[gpui::test(iterations = 10)]
 36async fn test_sharing_an_ssh_remote_project(
 37    cx_a: &mut TestAppContext,
 38    cx_b: &mut TestAppContext,
 39    server_cx: &mut TestAppContext,
 40) {
 41    let executor = cx_a.executor();
 42    cx_a.update(|cx| {
 43        release_channel::init(SemanticVersion::default(), cx);
 44    });
 45    server_cx.update(|cx| {
 46        release_channel::init(SemanticVersion::default(), cx);
 47    });
 48    let mut server = TestServer::start(executor.clone()).await;
 49    let client_a = server.create_client(cx_a, "user_a").await;
 50    let client_b = server.create_client(cx_b, "user_b").await;
 51    server
 52        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 53        .await;
 54
 55    // Set up project on remote FS
 56    let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
 57    let remote_fs = FakeFs::new(server_cx.executor());
 58    remote_fs
 59        .insert_tree(
 60            path!("/code"),
 61            json!({
 62                "project1": {
 63                    ".zed": {
 64                        "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
 65                    },
 66                    "README.md": "# project 1",
 67                    "src": {
 68                        "lib.rs": "fn one() -> usize { 1 }"
 69                    }
 70                },
 71                "project2": {
 72                    "README.md": "# project 2",
 73                },
 74            }),
 75        )
 76        .await;
 77
 78    // User A connects to the remote project via SSH.
 79    server_cx.update(HeadlessProject::init);
 80    let remote_http_client = Arc::new(BlockedHttpClient);
 81    let node = NodeRuntime::unavailable();
 82    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 83    let _headless_project = server_cx.new(|cx| {
 84        client::init_settings(cx);
 85        HeadlessProject::new(
 86            HeadlessAppState {
 87                session: server_ssh,
 88                fs: remote_fs.clone(),
 89                http_client: remote_http_client,
 90                node_runtime: node,
 91                languages,
 92                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 93            },
 94            cx,
 95        )
 96    });
 97
 98    let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
 99    let (project_a, worktree_id) = client_a
100        .build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
101        .await;
102
103    // While the SSH worktree is being scanned, user A shares the remote project.
104    let active_call_a = cx_a.read(ActiveCall::global);
105    let project_id = active_call_a
106        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
107        .await
108        .unwrap();
109
110    // User B joins the project.
111    let project_b = client_b.join_remote_project(project_id, cx_b).await;
112    let worktree_b = project_b
113        .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
114        .unwrap();
115
116    let worktree_a = project_a
117        .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
118        .unwrap();
119
120    executor.run_until_parked();
121
122    worktree_a.update(cx_a, |worktree, _cx| {
123        assert_eq!(
124            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
125            vec![
126                Path::new(".zed"),
127                Path::new(".zed/settings.json"),
128                Path::new("README.md"),
129                Path::new("src"),
130                Path::new("src/lib.rs"),
131            ]
132        );
133    });
134
135    worktree_b.update(cx_b, |worktree, _cx| {
136        assert_eq!(
137            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
138            vec![
139                Path::new(".zed"),
140                Path::new(".zed/settings.json"),
141                Path::new("README.md"),
142                Path::new("src"),
143                Path::new("src/lib.rs"),
144            ]
145        );
146    });
147
148    // User B can open buffers in the remote project.
149    let buffer_b = project_b
150        .update(cx_b, |project, cx| {
151            project.open_buffer((worktree_id, "src/lib.rs"), cx)
152        })
153        .await
154        .unwrap();
155    buffer_b.update(cx_b, |buffer, cx| {
156        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
157        let ix = buffer.text().find('1').unwrap();
158        buffer.edit([(ix..ix + 1, "100")], None, cx);
159    });
160
161    executor.run_until_parked();
162
163    cx_b.read(|cx| {
164        let file = buffer_b.read(cx).file();
165        assert_eq!(
166            language_settings(Some("Rust".into()), file, cx).language_servers,
167            ["override-rust-analyzer".to_string()]
168        )
169    });
170
171    project_b
172        .update(cx_b, |project, cx| {
173            project.save_buffer_as(
174                buffer_b.clone(),
175                ProjectPath {
176                    worktree_id: worktree_id.to_owned(),
177                    path: Arc::from(Path::new("src/renamed.rs")),
178                },
179                cx,
180            )
181        })
182        .await
183        .unwrap();
184    assert_eq!(
185        remote_fs
186            .load(path!("/code/project1/src/renamed.rs").as_ref())
187            .await
188            .unwrap(),
189        "fn one() -> usize { 100 }"
190    );
191    cx_b.run_until_parked();
192    cx_b.update(|cx| {
193        assert_eq!(
194            buffer_b
195                .read(cx)
196                .file()
197                .unwrap()
198                .path()
199                .to_string_lossy()
200                .to_string(),
201            separator!("src/renamed.rs").to_string()
202        );
203    });
204}
205
206#[gpui::test]
207async fn test_ssh_collaboration_git_branches(
208    executor: BackgroundExecutor,
209    cx_a: &mut TestAppContext,
210    cx_b: &mut TestAppContext,
211    server_cx: &mut TestAppContext,
212) {
213    cx_a.set_name("a");
214    cx_b.set_name("b");
215    server_cx.set_name("server");
216
217    cx_a.update(|cx| {
218        release_channel::init(SemanticVersion::default(), cx);
219    });
220    server_cx.update(|cx| {
221        release_channel::init(SemanticVersion::default(), cx);
222    });
223
224    let mut server = TestServer::start(executor.clone()).await;
225    let client_a = server.create_client(cx_a, "user_a").await;
226    let client_b = server.create_client(cx_b, "user_b").await;
227    server
228        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
229        .await;
230
231    // Set up project on remote FS
232    let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
233    let remote_fs = FakeFs::new(server_cx.executor());
234    remote_fs
235        .insert_tree("/project", serde_json::json!({ ".git":{} }))
236        .await;
237
238    let branches = ["main", "dev", "feature-1"];
239    let branches_set = branches
240        .iter()
241        .map(ToString::to_string)
242        .collect::<HashSet<_>>();
243    remote_fs.insert_branches(Path::new("/project/.git"), &branches);
244
245    // User A connects to the remote project via SSH.
246    server_cx.update(HeadlessProject::init);
247    let remote_http_client = Arc::new(BlockedHttpClient);
248    let node = NodeRuntime::unavailable();
249    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
250    let headless_project = server_cx.new(|cx| {
251        client::init_settings(cx);
252        HeadlessProject::new(
253            HeadlessAppState {
254                session: server_ssh,
255                fs: remote_fs.clone(),
256                http_client: remote_http_client,
257                node_runtime: node,
258                languages,
259                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
260            },
261            cx,
262        )
263    });
264
265    let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
266    let (project_a, _) = client_a
267        .build_ssh_project("/project", client_ssh, cx_a)
268        .await;
269
270    // While the SSH worktree is being scanned, user A shares the remote project.
271    let active_call_a = cx_a.read(ActiveCall::global);
272    let project_id = active_call_a
273        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
274        .await
275        .unwrap();
276
277    // User B joins the project.
278    let project_b = client_b.join_remote_project(project_id, cx_b).await;
279
280    // Give client A sometime to see that B has joined, and that the headless server
281    // has some git repositories
282    executor.run_until_parked();
283
284    let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
285
286    let branches_b = cx_b
287        .update(|cx| repo_b.update(cx, |repo_b, _cx| repo_b.branches()))
288        .await
289        .unwrap()
290        .unwrap();
291
292    let new_branch = branches[2];
293
294    let branches_b = branches_b
295        .into_iter()
296        .map(|branch| branch.name().to_string())
297        .collect::<HashSet<_>>();
298
299    assert_eq!(&branches_b, &branches_set);
300
301    cx_b.update(|cx| {
302        repo_b.update(cx, |repo_b, _cx| {
303            repo_b.change_branch(new_branch.to_string())
304        })
305    })
306    .await
307    .unwrap()
308    .unwrap();
309
310    executor.run_until_parked();
311
312    let server_branch = server_cx.update(|cx| {
313        headless_project.update(cx, |headless_project, cx| {
314            headless_project.git_store.update(cx, |git_store, cx| {
315                git_store
316                    .repositories()
317                    .values()
318                    .next()
319                    .unwrap()
320                    .read(cx)
321                    .branch
322                    .as_ref()
323                    .unwrap()
324                    .clone()
325            })
326        })
327    });
328
329    assert_eq!(server_branch.name(), branches[2]);
330
331    // Also try creating a new branch
332    cx_b.update(|cx| {
333        repo_b.update(cx, |repo_b, _cx| {
334            repo_b.create_branch("totally-new-branch".to_string())
335        })
336    })
337    .await
338    .unwrap()
339    .unwrap();
340
341    cx_b.update(|cx| {
342        repo_b.update(cx, |repo_b, _cx| {
343            repo_b.change_branch("totally-new-branch".to_string())
344        })
345    })
346    .await
347    .unwrap()
348    .unwrap();
349
350    executor.run_until_parked();
351
352    let server_branch = server_cx.update(|cx| {
353        headless_project.update(cx, |headless_project, cx| {
354            headless_project.git_store.update(cx, |git_store, cx| {
355                git_store
356                    .repositories()
357                    .values()
358                    .next()
359                    .unwrap()
360                    .read(cx)
361                    .branch
362                    .as_ref()
363                    .unwrap()
364                    .clone()
365            })
366        })
367    });
368
369    assert_eq!(server_branch.name(), "totally-new-branch");
370
371    // Remove the git repository and check that all participants get the update.
372    remote_fs
373        .remove_dir("/project/.git".as_ref(), RemoveOptions::default())
374        .await
375        .unwrap();
376    executor.run_until_parked();
377
378    project_a.update(cx_a, |project, cx| {
379        pretty_assertions::assert_eq!(
380            project.git_store().read(cx).repo_snapshots(cx),
381            HashMap::default()
382        );
383    });
384    project_b.update(cx_b, |project, cx| {
385        pretty_assertions::assert_eq!(
386            project.git_store().read(cx).repo_snapshots(cx),
387            HashMap::default()
388        );
389    });
390}
391
392#[gpui::test]
393async fn test_ssh_collaboration_formatting_with_prettier(
394    executor: BackgroundExecutor,
395    cx_a: &mut TestAppContext,
396    cx_b: &mut TestAppContext,
397    server_cx: &mut TestAppContext,
398) {
399    cx_a.set_name("a");
400    cx_b.set_name("b");
401    server_cx.set_name("server");
402
403    cx_a.update(|cx| {
404        release_channel::init(SemanticVersion::default(), cx);
405    });
406    server_cx.update(|cx| {
407        release_channel::init(SemanticVersion::default(), cx);
408    });
409
410    let mut server = TestServer::start(executor.clone()).await;
411    let client_a = server.create_client(cx_a, "user_a").await;
412    let client_b = server.create_client(cx_b, "user_b").await;
413    server
414        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
415        .await;
416
417    let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
418    let remote_fs = FakeFs::new(server_cx.executor());
419    let buffer_text = "let one = \"two\"";
420    let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
421    remote_fs
422        .insert_tree(
423            path!("/project"),
424            serde_json::json!({ "a.ts": buffer_text }),
425        )
426        .await;
427
428    let test_plugin = "test_plugin";
429    let ts_lang = Arc::new(Language::new(
430        LanguageConfig {
431            name: "TypeScript".into(),
432            matcher: LanguageMatcher {
433                path_suffixes: vec!["ts".to_string()],
434                ..LanguageMatcher::default()
435            },
436            ..LanguageConfig::default()
437        },
438        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
439    ));
440    client_a.language_registry().add(ts_lang.clone());
441    client_b.language_registry().add(ts_lang.clone());
442
443    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
444    let mut fake_language_servers = languages.register_fake_lsp(
445        "TypeScript",
446        FakeLspAdapter {
447            prettier_plugins: vec![test_plugin],
448            ..Default::default()
449        },
450    );
451
452    // User A connects to the remote project via SSH.
453    server_cx.update(HeadlessProject::init);
454    let remote_http_client = Arc::new(BlockedHttpClient);
455    let _headless_project = server_cx.new(|cx| {
456        client::init_settings(cx);
457        HeadlessProject::new(
458            HeadlessAppState {
459                session: server_ssh,
460                fs: remote_fs.clone(),
461                http_client: remote_http_client,
462                node_runtime: NodeRuntime::unavailable(),
463                languages,
464                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
465            },
466            cx,
467        )
468    });
469
470    let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
471    let (project_a, worktree_id) = client_a
472        .build_ssh_project(path!("/project"), client_ssh, cx_a)
473        .await;
474
475    // While the SSH worktree is being scanned, user A shares the remote project.
476    let active_call_a = cx_a.read(ActiveCall::global);
477    let project_id = active_call_a
478        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
479        .await
480        .unwrap();
481
482    // User B joins the project.
483    let project_b = client_b.join_remote_project(project_id, cx_b).await;
484    executor.run_until_parked();
485
486    // Opens the buffer and formats it
487    let (buffer_b, _handle) = project_b
488        .update(cx_b, |p, cx| {
489            p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
490        })
491        .await
492        .expect("user B opens buffer for formatting");
493
494    cx_a.update(|cx| {
495        SettingsStore::update_global(cx, |store, cx| {
496            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
497                file.defaults.formatter = Some(SelectedFormatter::Auto);
498                file.defaults.prettier = Some(PrettierSettings {
499                    allowed: true,
500                    ..PrettierSettings::default()
501                });
502            });
503        });
504    });
505    cx_b.update(|cx| {
506        SettingsStore::update_global(cx, |store, cx| {
507            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
508                file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
509                    vec![Formatter::LanguageServer { name: None }].into(),
510                )));
511                file.defaults.prettier = Some(PrettierSettings {
512                    allowed: true,
513                    ..PrettierSettings::default()
514                });
515            });
516        });
517    });
518    let fake_language_server = fake_language_servers.next().await.unwrap();
519    fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
520        panic!(
521            "Unexpected: prettier should be preferred since it's enabled and language supports it"
522        )
523    });
524
525    project_b
526        .update(cx_b, |project, cx| {
527            project.format(
528                HashSet::from_iter([buffer_b.clone()]),
529                LspFormatTarget::Buffers,
530                true,
531                FormatTrigger::Save,
532                cx,
533            )
534        })
535        .await
536        .unwrap();
537
538    executor.run_until_parked();
539    assert_eq!(
540        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
541        buffer_text.to_string() + "\n" + prettier_format_suffix,
542        "Prettier formatting was not applied to client buffer after client's request"
543    );
544
545    // User A opens and formats the same buffer too
546    let buffer_a = project_a
547        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
548        .await
549        .expect("user A opens buffer for formatting");
550
551    cx_a.update(|cx| {
552        SettingsStore::update_global(cx, |store, cx| {
553            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
554                file.defaults.formatter = Some(SelectedFormatter::Auto);
555                file.defaults.prettier = Some(PrettierSettings {
556                    allowed: true,
557                    ..PrettierSettings::default()
558                });
559            });
560        });
561    });
562    project_a
563        .update(cx_a, |project, cx| {
564            project.format(
565                HashSet::from_iter([buffer_a.clone()]),
566                LspFormatTarget::Buffers,
567                true,
568                FormatTrigger::Manual,
569                cx,
570            )
571        })
572        .await
573        .unwrap();
574
575    executor.run_until_parked();
576    assert_eq!(
577        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
578        buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
579        "Prettier formatting was not applied to client buffer after host's request"
580    );
581}
582
583#[gpui::test]
584async fn test_remote_server_debugger(
585    cx_a: &mut TestAppContext,
586    server_cx: &mut TestAppContext,
587    executor: BackgroundExecutor,
588) {
589    cx_a.update(|cx| {
590        release_channel::init(SemanticVersion::default(), cx);
591        command_palette_hooks::init(cx);
592        zlog::init_test();
593        dap_adapters::init(cx);
594    });
595    server_cx.update(|cx| {
596        release_channel::init(SemanticVersion::default(), cx);
597        dap_adapters::init(cx);
598    });
599    let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
600    let remote_fs = FakeFs::new(server_cx.executor());
601    remote_fs
602        .insert_tree(
603            path!("/code"),
604            json!({
605                "lib.rs": "fn one() -> usize { 1 }"
606            }),
607        )
608        .await;
609
610    // User A connects to the remote project via SSH.
611    server_cx.update(HeadlessProject::init);
612    let remote_http_client = Arc::new(BlockedHttpClient);
613    let node = NodeRuntime::unavailable();
614    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
615    let _headless_project = server_cx.new(|cx| {
616        client::init_settings(cx);
617        HeadlessProject::new(
618            HeadlessAppState {
619                session: server_ssh,
620                fs: remote_fs.clone(),
621                http_client: remote_http_client,
622                node_runtime: node,
623                languages,
624                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
625            },
626            cx,
627        )
628    });
629
630    let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
631    let mut server = TestServer::start(server_cx.executor()).await;
632    let client_a = server.create_client(cx_a, "user_a").await;
633    cx_a.update(|cx| {
634        debugger_ui::init(cx);
635        command_palette_hooks::init(cx);
636    });
637    let (project_a, _) = client_a
638        .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
639        .await;
640
641    let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
642
643    let debugger_panel = workspace
644        .update_in(cx_a, |_workspace, window, cx| {
645            cx.spawn_in(window, DebugPanel::load)
646        })
647        .await
648        .unwrap();
649
650    workspace.update_in(cx_a, |workspace, window, cx| {
651        workspace.add_panel(debugger_panel, window, cx);
652    });
653
654    cx_a.run_until_parked();
655    let debug_panel = workspace
656        .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
657        .unwrap();
658
659    let workspace_window = cx_a
660        .window_handle()
661        .downcast::<workspace::Workspace>()
662        .unwrap();
663
664    let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
665    cx_a.run_until_parked();
666    debug_panel.update(cx_a, |debug_panel, cx| {
667        assert_eq!(
668            debug_panel.active_session().unwrap().read(cx).session(cx),
669            session
670        )
671    });
672
673    session.update(cx_a, |session, _| {
674        assert_eq!(session.binary().command, "ssh");
675    });
676
677    let shutdown_session = workspace.update(cx_a, |workspace, cx| {
678        workspace.project().update(cx, |project, cx| {
679            project.dap_store().update(cx, |dap_store, cx| {
680                dap_store.shutdown_session(session.read(cx).session_id(), cx)
681            })
682        })
683    });
684
685    client_ssh.update(cx_a, |a, _| {
686        a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
687    });
688
689    shutdown_session.await.unwrap();
690}