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}