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