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