remote_editing_collaboration_tests.rs

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