remote_editing_collaboration_tests.rs

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