remote_editing_collaboration_tests.rs

  1use crate::tests::TestServer;
  2use call::ActiveCall;
  3use collections::HashSet;
  4use extension::ExtensionHostProxy;
  5use fs::{FakeFs, Fs as _};
  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, worktree_id) = 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    let root_path = ProjectPath::root_path(worktree_id);
281
282    let branches_b = cx_b
283        .update(|cx| repo_b.read(cx).branches())
284        .await
285        .unwrap()
286        .unwrap();
287
288    let new_branch = branches[2];
289
290    let branches_b = branches_b
291        .into_iter()
292        .map(|branch| branch.name.to_string())
293        .collect::<HashSet<_>>();
294
295    assert_eq!(&branches_b, &branches_set);
296
297    cx_b.update(|cx| repo_b.read(cx).change_branch(new_branch))
298        .await
299        .unwrap()
300        .unwrap();
301
302    executor.run_until_parked();
303
304    let server_branch = server_cx.update(|cx| {
305        headless_project.update(cx, |headless_project, cx| {
306            headless_project
307                .worktree_store
308                .update(cx, |worktree_store, cx| {
309                    worktree_store
310                        .current_branch(root_path.clone(), cx)
311                        .unwrap()
312                })
313        })
314    });
315
316    assert_eq!(server_branch.name, branches[2]);
317
318    // Also try creating a new branch
319    cx_b.update(|cx| repo_b.read(cx).create_branch("totally-new-branch"))
320        .await
321        .unwrap()
322        .unwrap();
323
324    cx_b.update(|cx| repo_b.read(cx).change_branch("totally-new-branch"))
325        .await
326        .unwrap()
327        .unwrap();
328
329    executor.run_until_parked();
330
331    let server_branch = server_cx.update(|cx| {
332        headless_project.update(cx, |headless_project, cx| {
333            headless_project
334                .worktree_store
335                .update(cx, |worktree_store, cx| {
336                    worktree_store.current_branch(root_path, cx).unwrap()
337                })
338        })
339    });
340
341    assert_eq!(server_branch.name, "totally-new-branch");
342}
343
344#[gpui::test]
345async fn test_ssh_collaboration_formatting_with_prettier(
346    executor: BackgroundExecutor,
347    cx_a: &mut TestAppContext,
348    cx_b: &mut TestAppContext,
349    server_cx: &mut TestAppContext,
350) {
351    cx_a.set_name("a");
352    cx_b.set_name("b");
353    server_cx.set_name("server");
354
355    cx_a.update(|cx| {
356        release_channel::init(SemanticVersion::default(), cx);
357    });
358    server_cx.update(|cx| {
359        release_channel::init(SemanticVersion::default(), cx);
360    });
361
362    let mut server = TestServer::start(executor.clone()).await;
363    let client_a = server.create_client(cx_a, "user_a").await;
364    let client_b = server.create_client(cx_b, "user_b").await;
365    server
366        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
367        .await;
368
369    let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
370    let remote_fs = FakeFs::new(server_cx.executor());
371    let buffer_text = "let one = \"two\"";
372    let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
373    remote_fs
374        .insert_tree("/project", serde_json::json!({ "a.ts": buffer_text }))
375        .await;
376
377    let test_plugin = "test_plugin";
378    let ts_lang = Arc::new(Language::new(
379        LanguageConfig {
380            name: "TypeScript".into(),
381            matcher: LanguageMatcher {
382                path_suffixes: vec!["ts".to_string()],
383                ..LanguageMatcher::default()
384            },
385            ..LanguageConfig::default()
386        },
387        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
388    ));
389    client_a.language_registry().add(ts_lang.clone());
390    client_b.language_registry().add(ts_lang.clone());
391
392    let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
393    let mut fake_language_servers = languages.register_fake_lsp(
394        "TypeScript",
395        FakeLspAdapter {
396            prettier_plugins: vec![test_plugin],
397            ..Default::default()
398        },
399    );
400
401    // User A connects to the remote project via SSH.
402    server_cx.update(HeadlessProject::init);
403    let remote_http_client = Arc::new(BlockedHttpClient);
404    let _headless_project = server_cx.new(|cx| {
405        client::init_settings(cx);
406        HeadlessProject::new(
407            HeadlessAppState {
408                session: server_ssh,
409                fs: remote_fs.clone(),
410                http_client: remote_http_client,
411                node_runtime: NodeRuntime::unavailable(),
412                languages,
413                extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
414            },
415            cx,
416        )
417    });
418
419    let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
420    let (project_a, worktree_id) = client_a
421        .build_ssh_project("/project", client_ssh, cx_a)
422        .await;
423
424    // While the SSH worktree is being scanned, user A shares the remote project.
425    let active_call_a = cx_a.read(ActiveCall::global);
426    let project_id = active_call_a
427        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
428        .await
429        .unwrap();
430
431    // User B joins the project.
432    let project_b = client_b.join_remote_project(project_id, cx_b).await;
433    executor.run_until_parked();
434
435    // Opens the buffer and formats it
436    let (buffer_b, _handle) = project_b
437        .update(cx_b, |p, cx| {
438            p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
439        })
440        .await
441        .expect("user B opens buffer for formatting");
442
443    cx_a.update(|cx| {
444        SettingsStore::update_global(cx, |store, cx| {
445            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
446                file.defaults.formatter = Some(SelectedFormatter::Auto);
447                file.defaults.prettier = Some(PrettierSettings {
448                    allowed: true,
449                    ..PrettierSettings::default()
450                });
451            });
452        });
453    });
454    cx_b.update(|cx| {
455        SettingsStore::update_global(cx, |store, cx| {
456            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
457                file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
458                    vec![Formatter::LanguageServer { name: None }].into(),
459                )));
460                file.defaults.prettier = Some(PrettierSettings {
461                    allowed: true,
462                    ..PrettierSettings::default()
463                });
464            });
465        });
466    });
467    let fake_language_server = fake_language_servers.next().await.unwrap();
468    fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
469        panic!(
470            "Unexpected: prettier should be preferred since it's enabled and language supports it"
471        )
472    });
473
474    project_b
475        .update(cx_b, |project, cx| {
476            project.format(
477                HashSet::from_iter([buffer_b.clone()]),
478                LspFormatTarget::Buffers,
479                true,
480                FormatTrigger::Save,
481                cx,
482            )
483        })
484        .await
485        .unwrap();
486
487    executor.run_until_parked();
488    assert_eq!(
489        buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
490        buffer_text.to_string() + "\n" + prettier_format_suffix,
491        "Prettier formatting was not applied to client buffer after client's request"
492    );
493
494    // User A opens and formats the same buffer too
495    let buffer_a = project_a
496        .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
497        .await
498        .expect("user A opens buffer for formatting");
499
500    cx_a.update(|cx| {
501        SettingsStore::update_global(cx, |store, cx| {
502            store.update_user_settings::<AllLanguageSettings>(cx, |file| {
503                file.defaults.formatter = Some(SelectedFormatter::Auto);
504                file.defaults.prettier = Some(PrettierSettings {
505                    allowed: true,
506                    ..PrettierSettings::default()
507                });
508            });
509        });
510    });
511    project_a
512        .update(cx_a, |project, cx| {
513            project.format(
514                HashSet::from_iter([buffer_a.clone()]),
515                LspFormatTarget::Buffers,
516                true,
517                FormatTrigger::Manual,
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 + "\n" + prettier_format_suffix,
528        "Prettier formatting was not applied to client buffer after host's request"
529    );
530}