remote_editing_collaboration_tests.rs

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