remote_editing_collaboration_tests.rs

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