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