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