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