remote_editing_collaboration_tests.rs

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