remote_editing_collaboration_tests.rs

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