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, PrettierSettings, SelectedFormatter, language_settings,
 18    },
 19    tree_sitter_typescript,
 20};
 21use node_runtime::NodeRuntime;
 22use project::{
 23    ProjectPath,
 24    lsp_store::{FormatTrigger, LspFormatTarget},
 25};
 26use remote::SshRemoteClient;
 27use remote_server::{HeadlessAppState, HeadlessProject};
 28use rpc::proto;
 29use serde_json::json;
 30use settings::SettingsStore;
 31use std::{path::Path, sync::Arc};
 32use util::path;
 33
 34#[gpui::test(iterations = 10)]
 35async fn test_sharing_an_ssh_remote_project(
 36    cx_a: &mut TestAppContext,
 37    cx_b: &mut TestAppContext,
 38    server_cx: &mut TestAppContext,
 39) {
 40    let executor = cx_a.executor();
 41    cx_a.update(|cx| {
 42        release_channel::init(SemanticVersion::default(), cx);
 43    });
 44    server_cx.update(|cx| {
 45        release_channel::init(SemanticVersion::default(), cx);
 46    });
 47    let mut server = TestServer::start(executor.clone()).await;
 48    let client_a = server.create_client(cx_a, "user_a").await;
 49    let client_b = server.create_client(cx_b, "user_b").await;
 50    server
 51        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
 52        .await;
 53
 54    // Set up project on remote FS
 55    let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
 56    let remote_fs = FakeFs::new(server_cx.executor());
 57    remote_fs
 58        .insert_tree(
 59            path!("/code"),
 60            json!({
 61                "project1": {
 62                    ".zed": {
 63                        "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
 64                    },
 65                    "README.md": "# project 1",
 66                    "src": {
 67                        "lib.rs": "fn one() -> usize { 1 }"
 68                    }
 69                },
 70                "project2": {
 71                    "README.md": "# project 2",
 72                },
 73            }),
 74        )
 75        .await;
 76
 77    // User A connects to the remote project via SSH.
 78    server_cx.update(HeadlessProject::init);
 79    let remote_http_client = Arc::new(BlockedHttpClient);
 80    let node = NodeRuntime::unavailable();
 81    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
 82    let _headless_project = server_cx.new(|cx| {
 83        client::init_settings(cx);
 84        HeadlessProject::new(
 85            HeadlessAppState {
 86                session: server_ssh,
 87                fs: remote_fs.clone(),
 88                http_client: remote_http_client,
 89                node_runtime: node,
 90                languages,
 91                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
 92            },
 93            cx,
 94        )
 95    });
 96
 97    let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
 98    let (project_a, worktree_id) = client_a
 99        .build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
100        .await;
101
102    // While the SSH worktree is being scanned, user A shares the remote project.
103    let active_call_a = cx_a.read(ActiveCall::global);
104    let project_id = active_call_a
105        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
106        .await
107        .unwrap();
108
109    // User B joins the project.
110    let project_b = client_b.join_remote_project(project_id, cx_b).await;
111    let worktree_b = project_b
112        .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
113        .unwrap();
114
115    let worktree_a = project_a
116        .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
117        .unwrap();
118
119    executor.run_until_parked();
120
121    worktree_a.update(cx_a, |worktree, _cx| {
122        assert_eq!(
123            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
124            vec![
125                Path::new(".zed"),
126                Path::new(".zed/settings.json"),
127                Path::new("README.md"),
128                Path::new("src"),
129                Path::new("src/lib.rs"),
130            ]
131        );
132    });
133
134    worktree_b.update(cx_b, |worktree, _cx| {
135        assert_eq!(
136            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
137            vec![
138                Path::new(".zed"),
139                Path::new(".zed/settings.json"),
140                Path::new("README.md"),
141                Path::new("src"),
142                Path::new("src/lib.rs"),
143            ]
144        );
145    });
146
147    // User B can open buffers in the remote project.
148    let buffer_b = project_b
149        .update(cx_b, |project, cx| {
150            project.open_buffer((worktree_id, "src/lib.rs"), cx)
151        })
152        .await
153        .unwrap();
154    buffer_b.update(cx_b, |buffer, cx| {
155        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
156        let ix = buffer.text().find('1').unwrap();
157        buffer.edit([(ix..ix + 1, "100")], None, cx);
158    });
159
160    executor.run_until_parked();
161
162    cx_b.read(|cx| {
163        let file = buffer_b.read(cx).file();
164        assert_eq!(
165            language_settings(Some("Rust".into()), file, cx).language_servers,
166            ["override-rust-analyzer".to_string()]
167        )
168    });
169
170    project_b
171        .update(cx_b, |project, cx| {
172            project.save_buffer_as(
173                buffer_b.clone(),
174                ProjectPath {
175                    worktree_id: worktree_id.to_owned(),
176                    path: Arc::from(Path::new("src/renamed.rs")),
177                },
178                cx,
179            )
180        })
181        .await
182        .unwrap();
183    assert_eq!(
184        remote_fs
185            .load(path!("/code/project1/src/renamed.rs").as_ref())
186            .await
187            .unwrap(),
188        "fn one() -> usize { 100 }"
189    );
190    cx_b.run_until_parked();
191    cx_b.update(|cx| {
192        assert_eq!(
193            buffer_b
194                .read(cx)
195                .file()
196                .unwrap()
197                .path()
198                .to_string_lossy()
199                .to_string(),
200            path!("src/renamed.rs").to_string()
201        );
202    });
203}
204
205#[gpui::test]
206async fn test_ssh_collaboration_git_branches(
207    executor: BackgroundExecutor,
208    cx_a: &mut TestAppContext,
209    cx_b: &mut TestAppContext,
210    server_cx: &mut TestAppContext,
211) {
212    cx_a.set_name("a");
213    cx_b.set_name("b");
214    server_cx.set_name("server");
215
216    cx_a.update(|cx| {
217        release_channel::init(SemanticVersion::default(), cx);
218    });
219    server_cx.update(|cx| {
220        release_channel::init(SemanticVersion::default(), cx);
221    });
222
223    let mut server = TestServer::start(executor.clone()).await;
224    let client_a = server.create_client(cx_a, "user_a").await;
225    let client_b = server.create_client(cx_b, "user_b").await;
226    server
227        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
228        .await;
229
230    // Set up project on remote FS
231    let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
232    let remote_fs = FakeFs::new(server_cx.executor());
233    remote_fs
234        .insert_tree("/project", serde_json::json!({ ".git":{} }))
235        .await;
236
237    let branches = ["main", "dev", "feature-1"];
238    let branches_set = branches
239        .iter()
240        .map(ToString::to_string)
241        .collect::<HashSet<_>>();
242    remote_fs.insert_branches(Path::new("/project/.git"), &branches);
243
244    // User A connects to the remote project via SSH.
245    server_cx.update(HeadlessProject::init);
246    let remote_http_client = Arc::new(BlockedHttpClient);
247    let node = NodeRuntime::unavailable();
248    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
249    let headless_project = server_cx.new(|cx| {
250        client::init_settings(cx);
251        HeadlessProject::new(
252            HeadlessAppState {
253                session: server_ssh,
254                fs: remote_fs.clone(),
255                http_client: remote_http_client,
256                node_runtime: node,
257                languages,
258                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
259            },
260            cx,
261        )
262    });
263
264    let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
265    let (project_a, _) = client_a
266        .build_ssh_project("/project", client_ssh, cx_a)
267        .await;
268
269    // While the SSH worktree is being scanned, user A shares the remote project.
270    let active_call_a = cx_a.read(ActiveCall::global);
271    let project_id = active_call_a
272        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
273        .await
274        .unwrap();
275
276    // User B joins the project.
277    let project_b = client_b.join_remote_project(project_id, cx_b).await;
278
279    // Give client A sometime to see that B has joined, and that the headless server
280    // has some git repositories
281    executor.run_until_parked();
282
283    let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
284
285    let branches_b = cx_b
286        .update(|cx| repo_b.update(cx, |repo_b, _cx| repo_b.branches()))
287        .await
288        .unwrap()
289        .unwrap();
290
291    let new_branch = branches[2];
292
293    let branches_b = branches_b
294        .into_iter()
295        .map(|branch| branch.name().to_string())
296        .collect::<HashSet<_>>();
297
298    assert_eq!(&branches_b, &branches_set);
299
300    cx_b.update(|cx| {
301        repo_b.update(cx, |repo_b, _cx| {
302            repo_b.change_branch(new_branch.to_string())
303        })
304    })
305    .await
306    .unwrap()
307    .unwrap();
308
309    executor.run_until_parked();
310
311    let server_branch = server_cx.update(|cx| {
312        headless_project.update(cx, |headless_project, cx| {
313            headless_project.git_store.update(cx, |git_store, cx| {
314                git_store
315                    .repositories()
316                    .values()
317                    .next()
318                    .unwrap()
319                    .read(cx)
320                    .branch
321                    .as_ref()
322                    .unwrap()
323                    .clone()
324            })
325        })
326    });
327
328    assert_eq!(server_branch.name(), branches[2]);
329
330    // Also try creating a new branch
331    cx_b.update(|cx| {
332        repo_b.update(cx, |repo_b, _cx| {
333            repo_b.create_branch("totally-new-branch".to_string())
334        })
335    })
336    .await
337    .unwrap()
338    .unwrap();
339
340    cx_b.update(|cx| {
341        repo_b.update(cx, |repo_b, _cx| {
342            repo_b.change_branch("totally-new-branch".to_string())
343        })
344    })
345    .await
346    .unwrap()
347    .unwrap();
348
349    executor.run_until_parked();
350
351    let server_branch = server_cx.update(|cx| {
352        headless_project.update(cx, |headless_project, cx| {
353            headless_project.git_store.update(cx, |git_store, cx| {
354                git_store
355                    .repositories()
356                    .values()
357                    .next()
358                    .unwrap()
359                    .read(cx)
360                    .branch
361                    .as_ref()
362                    .unwrap()
363                    .clone()
364            })
365        })
366    });
367
368    assert_eq!(server_branch.name(), "totally-new-branch");
369
370    // Remove the git repository and check that all participants get the update.
371    remote_fs
372        .remove_dir("/project/.git".as_ref(), RemoveOptions::default())
373        .await
374        .unwrap();
375    executor.run_until_parked();
376
377    project_a.update(cx_a, |project, cx| {
378        pretty_assertions::assert_eq!(
379            project.git_store().read(cx).repo_snapshots(cx),
380            HashMap::default()
381        );
382    });
383    project_b.update(cx_b, |project, cx| {
384        pretty_assertions::assert_eq!(
385            project.git_store().read(cx).repo_snapshots(cx),
386            HashMap::default()
387        );
388    });
389}
390
391#[gpui::test]
392async fn test_ssh_collaboration_formatting_with_prettier(
393    executor: BackgroundExecutor,
394    cx_a: &mut TestAppContext,
395    cx_b: &mut TestAppContext,
396    server_cx: &mut TestAppContext,
397) {
398    cx_a.set_name("a");
399    cx_b.set_name("b");
400    server_cx.set_name("server");
401
402    cx_a.update(|cx| {
403        release_channel::init(SemanticVersion::default(), cx);
404    });
405    server_cx.update(|cx| {
406        release_channel::init(SemanticVersion::default(), cx);
407    });
408
409    let mut server = TestServer::start(executor.clone()).await;
410    let client_a = server.create_client(cx_a, "user_a").await;
411    let client_b = server.create_client(cx_b, "user_b").await;
412    server
413        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
414        .await;
415
416    let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
417    let remote_fs = FakeFs::new(server_cx.executor());
418    let buffer_text = "let one = \"two\"";
419    let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
420    remote_fs
421        .insert_tree(
422            path!("/project"),
423            serde_json::json!({ "a.ts": buffer_text }),
424        )
425        .await;
426
427    let test_plugin = "test_plugin";
428    let ts_lang = Arc::new(Language::new(
429        LanguageConfig {
430            name: "TypeScript".into(),
431            matcher: LanguageMatcher {
432                path_suffixes: vec!["ts".to_string()],
433                ..LanguageMatcher::default()
434            },
435            ..LanguageConfig::default()
436        },
437        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
438    ));
439    client_a.language_registry().add(ts_lang.clone());
440    client_b.language_registry().add(ts_lang.clone());
441
442    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
443    let mut fake_language_servers = languages.register_fake_lsp(
444        "TypeScript",
445        FakeLspAdapter {
446            prettier_plugins: vec![test_plugin],
447            ..Default::default()
448        },
449    );
450
451    // User A connects to the remote project via SSH.
452    server_cx.update(HeadlessProject::init);
453    let remote_http_client = Arc::new(BlockedHttpClient);
454    let _headless_project = server_cx.new(|cx| {
455        client::init_settings(cx);
456        HeadlessProject::new(
457            HeadlessAppState {
458                session: server_ssh,
459                fs: remote_fs.clone(),
460                http_client: remote_http_client,
461                node_runtime: NodeRuntime::unavailable(),
462                languages,
463                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
464            },
465            cx,
466        )
467    });
468
469    let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
470    let (project_a, worktree_id) = client_a
471        .build_ssh_project(path!("/project"), client_ssh, cx_a)
472        .await;
473
474    // While the SSH worktree is being scanned, user A shares the remote project.
475    let active_call_a = cx_a.read(ActiveCall::global);
476    let project_id = active_call_a
477        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
478        .await
479        .unwrap();
480
481    // User B joins the project.
482    let project_b = client_b.join_remote_project(project_id, cx_b).await;
483    executor.run_until_parked();
484
485    // Opens the buffer and formats it
486    let (buffer_b, _handle) = project_b
487        .update(cx_b, |p, cx| {
488            p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
489        })
490        .await
491        .expect("user B opens buffer for formatting");
492
493    cx_a.update(|cx| {
494        SettingsStore::update_global(cx, |store, cx| {
495            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
496                file.defaults.formatter = Some(SelectedFormatter::Auto);
497                file.defaults.prettier = Some(PrettierSettings {
498                    allowed: true,
499                    ..PrettierSettings::default()
500                });
501            });
502        });
503    });
504    cx_b.update(|cx| {
505        SettingsStore::update_global(cx, |store, cx| {
506            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
507                file.defaults.formatter =
508                    Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
509                        name: None,
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().unwrap().command.as_deref(), Some("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}