1use crate::tests::TestServer;
2use call::ActiveCall;
3use collections::{HashMap, HashSet};
4
5use debugger_ui::debugger_panel::DebugPanel;
6use extension::ExtensionHostProxy;
7use fs::{FakeFs, Fs as _, RemoveOptions};
8use futures::StreamExt as _;
9use gpui::{
10 AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
11 VisualContext,
12};
13use http_client::BlockedHttpClient;
14use language::{
15 FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
16 language_settings::{
17 AllLanguageSettings, Formatter, PrettierSettings, SelectedFormatter, language_settings,
18 },
19 tree_sitter_typescript,
20};
21use node_runtime::NodeRuntime;
22use project::{
23 ProjectPath,
24 lsp_store::{FormatTrigger, LspFormatTarget},
25};
26use remote::SshRemoteClient;
27use remote_server::{HeadlessAppState, HeadlessProject};
28use rpc::proto;
29use serde_json::json;
30use settings::SettingsStore;
31use std::{path::Path, sync::Arc};
32use util::path;
33
34#[gpui::test(iterations = 10)]
35async fn test_sharing_an_ssh_remote_project(
36 cx_a: &mut TestAppContext,
37 cx_b: &mut TestAppContext,
38 server_cx: &mut TestAppContext,
39) {
40 let executor = cx_a.executor();
41 cx_a.update(|cx| {
42 release_channel::init(SemanticVersion::default(), cx);
43 });
44 server_cx.update(|cx| {
45 release_channel::init(SemanticVersion::default(), cx);
46 });
47 let mut server = TestServer::start(executor.clone()).await;
48 let client_a = server.create_client(cx_a, "user_a").await;
49 let client_b = server.create_client(cx_b, "user_b").await;
50 server
51 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
52 .await;
53
54 // Set up project on remote FS
55 let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
56 let remote_fs = FakeFs::new(server_cx.executor());
57 remote_fs
58 .insert_tree(
59 path!("/code"),
60 json!({
61 "project1": {
62 ".zed": {
63 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
64 },
65 "README.md": "# project 1",
66 "src": {
67 "lib.rs": "fn one() -> usize { 1 }"
68 }
69 },
70 "project2": {
71 "README.md": "# project 2",
72 },
73 }),
74 )
75 .await;
76
77 // User A connects to the remote project via SSH.
78 server_cx.update(HeadlessProject::init);
79 let remote_http_client = Arc::new(BlockedHttpClient);
80 let node = NodeRuntime::unavailable();
81 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
82 let _headless_project = server_cx.new(|cx| {
83 client::init_settings(cx);
84 HeadlessProject::new(
85 HeadlessAppState {
86 session: server_ssh,
87 fs: remote_fs.clone(),
88 http_client: remote_http_client,
89 node_runtime: node,
90 languages,
91 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
92 },
93 cx,
94 )
95 });
96
97 let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
98 let (project_a, worktree_id) = client_a
99 .build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
100 .await;
101
102 // While the SSH worktree is being scanned, user A shares the remote project.
103 let active_call_a = cx_a.read(ActiveCall::global);
104 let project_id = active_call_a
105 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
106 .await
107 .unwrap();
108
109 // User B joins the project.
110 let project_b = client_b.join_remote_project(project_id, cx_b).await;
111 let worktree_b = project_b
112 .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
113 .unwrap();
114
115 let worktree_a = project_a
116 .update(cx_a, |project, cx| project.worktree_for_id(worktree_id, cx))
117 .unwrap();
118
119 executor.run_until_parked();
120
121 worktree_a.update(cx_a, |worktree, _cx| {
122 assert_eq!(
123 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
124 vec![
125 Path::new(".zed"),
126 Path::new(".zed/settings.json"),
127 Path::new("README.md"),
128 Path::new("src"),
129 Path::new("src/lib.rs"),
130 ]
131 );
132 });
133
134 worktree_b.update(cx_b, |worktree, _cx| {
135 assert_eq!(
136 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
137 vec![
138 Path::new(".zed"),
139 Path::new(".zed/settings.json"),
140 Path::new("README.md"),
141 Path::new("src"),
142 Path::new("src/lib.rs"),
143 ]
144 );
145 });
146
147 // User B can open buffers in the remote project.
148 let buffer_b = project_b
149 .update(cx_b, |project, cx| {
150 project.open_buffer((worktree_id, "src/lib.rs"), cx)
151 })
152 .await
153 .unwrap();
154 buffer_b.update(cx_b, |buffer, cx| {
155 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
156 let ix = buffer.text().find('1').unwrap();
157 buffer.edit([(ix..ix + 1, "100")], None, cx);
158 });
159
160 executor.run_until_parked();
161
162 cx_b.read(|cx| {
163 let file = buffer_b.read(cx).file();
164 assert_eq!(
165 language_settings(Some("Rust".into()), file, cx).language_servers,
166 ["override-rust-analyzer".to_string()]
167 )
168 });
169
170 project_b
171 .update(cx_b, |project, cx| {
172 project.save_buffer_as(
173 buffer_b.clone(),
174 ProjectPath {
175 worktree_id: worktree_id.to_owned(),
176 path: Arc::from(Path::new("src/renamed.rs")),
177 },
178 cx,
179 )
180 })
181 .await
182 .unwrap();
183 assert_eq!(
184 remote_fs
185 .load(path!("/code/project1/src/renamed.rs").as_ref())
186 .await
187 .unwrap(),
188 "fn one() -> usize { 100 }"
189 );
190 cx_b.run_until_parked();
191 cx_b.update(|cx| {
192 assert_eq!(
193 buffer_b
194 .read(cx)
195 .file()
196 .unwrap()
197 .path()
198 .to_string_lossy()
199 .to_string(),
200 path!("src/renamed.rs").to_string()
201 );
202 });
203}
204
205#[gpui::test]
206async fn test_ssh_collaboration_git_branches(
207 executor: BackgroundExecutor,
208 cx_a: &mut TestAppContext,
209 cx_b: &mut TestAppContext,
210 server_cx: &mut TestAppContext,
211) {
212 cx_a.set_name("a");
213 cx_b.set_name("b");
214 server_cx.set_name("server");
215
216 cx_a.update(|cx| {
217 release_channel::init(SemanticVersion::default(), cx);
218 });
219 server_cx.update(|cx| {
220 release_channel::init(SemanticVersion::default(), cx);
221 });
222
223 let mut server = TestServer::start(executor.clone()).await;
224 let client_a = server.create_client(cx_a, "user_a").await;
225 let client_b = server.create_client(cx_b, "user_b").await;
226 server
227 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
228 .await;
229
230 // Set up project on remote FS
231 let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
232 let remote_fs = FakeFs::new(server_cx.executor());
233 remote_fs
234 .insert_tree("/project", serde_json::json!({ ".git":{} }))
235 .await;
236
237 let branches = ["main", "dev", "feature-1"];
238 let branches_set = branches
239 .iter()
240 .map(ToString::to_string)
241 .collect::<HashSet<_>>();
242 remote_fs.insert_branches(Path::new("/project/.git"), &branches);
243
244 // User A connects to the remote project via SSH.
245 server_cx.update(HeadlessProject::init);
246 let remote_http_client = Arc::new(BlockedHttpClient);
247 let node = NodeRuntime::unavailable();
248 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
249 let headless_project = server_cx.new(|cx| {
250 client::init_settings(cx);
251 HeadlessProject::new(
252 HeadlessAppState {
253 session: server_ssh,
254 fs: remote_fs.clone(),
255 http_client: remote_http_client,
256 node_runtime: node,
257 languages,
258 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
259 },
260 cx,
261 )
262 });
263
264 let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
265 let (project_a, _) = client_a
266 .build_ssh_project("/project", client_ssh, cx_a)
267 .await;
268
269 // While the SSH worktree is being scanned, user A shares the remote project.
270 let active_call_a = cx_a.read(ActiveCall::global);
271 let project_id = active_call_a
272 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
273 .await
274 .unwrap();
275
276 // User B joins the project.
277 let project_b = client_b.join_remote_project(project_id, cx_b).await;
278
279 // Give client A sometime to see that B has joined, and that the headless server
280 // has some git repositories
281 executor.run_until_parked();
282
283 let repo_b = cx_b.update(|cx| project_b.read(cx).active_repository(cx).unwrap());
284
285 let branches_b = cx_b
286 .update(|cx| repo_b.update(cx, |repo_b, _cx| repo_b.branches()))
287 .await
288 .unwrap()
289 .unwrap();
290
291 let new_branch = branches[2];
292
293 let branches_b = branches_b
294 .into_iter()
295 .map(|branch| branch.name().to_string())
296 .collect::<HashSet<_>>();
297
298 assert_eq!(&branches_b, &branches_set);
299
300 cx_b.update(|cx| {
301 repo_b.update(cx, |repo_b, _cx| {
302 repo_b.change_branch(new_branch.to_string())
303 })
304 })
305 .await
306 .unwrap()
307 .unwrap();
308
309 executor.run_until_parked();
310
311 let server_branch = server_cx.update(|cx| {
312 headless_project.update(cx, |headless_project, cx| {
313 headless_project.git_store.update(cx, |git_store, cx| {
314 git_store
315 .repositories()
316 .values()
317 .next()
318 .unwrap()
319 .read(cx)
320 .branch
321 .as_ref()
322 .unwrap()
323 .clone()
324 })
325 })
326 });
327
328 assert_eq!(server_branch.name(), branches[2]);
329
330 // Also try creating a new branch
331 cx_b.update(|cx| {
332 repo_b.update(cx, |repo_b, _cx| {
333 repo_b.create_branch("totally-new-branch".to_string())
334 })
335 })
336 .await
337 .unwrap()
338 .unwrap();
339
340 cx_b.update(|cx| {
341 repo_b.update(cx, |repo_b, _cx| {
342 repo_b.change_branch("totally-new-branch".to_string())
343 })
344 })
345 .await
346 .unwrap()
347 .unwrap();
348
349 executor.run_until_parked();
350
351 let server_branch = server_cx.update(|cx| {
352 headless_project.update(cx, |headless_project, cx| {
353 headless_project.git_store.update(cx, |git_store, cx| {
354 git_store
355 .repositories()
356 .values()
357 .next()
358 .unwrap()
359 .read(cx)
360 .branch
361 .as_ref()
362 .unwrap()
363 .clone()
364 })
365 })
366 });
367
368 assert_eq!(server_branch.name(), "totally-new-branch");
369
370 // Remove the git repository and check that all participants get the update.
371 remote_fs
372 .remove_dir("/project/.git".as_ref(), RemoveOptions::default())
373 .await
374 .unwrap();
375 executor.run_until_parked();
376
377 project_a.update(cx_a, |project, cx| {
378 pretty_assertions::assert_eq!(
379 project.git_store().read(cx).repo_snapshots(cx),
380 HashMap::default()
381 );
382 });
383 project_b.update(cx_b, |project, cx| {
384 pretty_assertions::assert_eq!(
385 project.git_store().read(cx).repo_snapshots(cx),
386 HashMap::default()
387 );
388 });
389}
390
391#[gpui::test]
392async fn test_ssh_collaboration_formatting_with_prettier(
393 executor: BackgroundExecutor,
394 cx_a: &mut TestAppContext,
395 cx_b: &mut TestAppContext,
396 server_cx: &mut TestAppContext,
397) {
398 cx_a.set_name("a");
399 cx_b.set_name("b");
400 server_cx.set_name("server");
401
402 cx_a.update(|cx| {
403 release_channel::init(SemanticVersion::default(), cx);
404 });
405 server_cx.update(|cx| {
406 release_channel::init(SemanticVersion::default(), cx);
407 });
408
409 let mut server = TestServer::start(executor.clone()).await;
410 let client_a = server.create_client(cx_a, "user_a").await;
411 let client_b = server.create_client(cx_b, "user_b").await;
412 server
413 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
414 .await;
415
416 let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
417 let remote_fs = FakeFs::new(server_cx.executor());
418 let buffer_text = "let one = \"two\"";
419 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
420 remote_fs
421 .insert_tree(
422 path!("/project"),
423 serde_json::json!({ "a.ts": buffer_text }),
424 )
425 .await;
426
427 let test_plugin = "test_plugin";
428 let ts_lang = Arc::new(Language::new(
429 LanguageConfig {
430 name: "TypeScript".into(),
431 matcher: LanguageMatcher {
432 path_suffixes: vec!["ts".to_string()],
433 ..LanguageMatcher::default()
434 },
435 ..LanguageConfig::default()
436 },
437 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
438 ));
439 client_a.language_registry().add(ts_lang.clone());
440 client_b.language_registry().add(ts_lang.clone());
441
442 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
443 let mut fake_language_servers = languages.register_fake_lsp(
444 "TypeScript",
445 FakeLspAdapter {
446 prettier_plugins: vec![test_plugin],
447 ..Default::default()
448 },
449 );
450
451 // User A connects to the remote project via SSH.
452 server_cx.update(HeadlessProject::init);
453 let remote_http_client = Arc::new(BlockedHttpClient);
454 let _headless_project = server_cx.new(|cx| {
455 client::init_settings(cx);
456 HeadlessProject::new(
457 HeadlessAppState {
458 session: server_ssh,
459 fs: remote_fs.clone(),
460 http_client: remote_http_client,
461 node_runtime: NodeRuntime::unavailable(),
462 languages,
463 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
464 },
465 cx,
466 )
467 });
468
469 let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
470 let (project_a, worktree_id) = client_a
471 .build_ssh_project(path!("/project"), client_ssh, cx_a)
472 .await;
473
474 // While the SSH worktree is being scanned, user A shares the remote project.
475 let active_call_a = cx_a.read(ActiveCall::global);
476 let project_id = active_call_a
477 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
478 .await
479 .unwrap();
480
481 // User B joins the project.
482 let project_b = client_b.join_remote_project(project_id, cx_b).await;
483 executor.run_until_parked();
484
485 // Opens the buffer and formats it
486 let (buffer_b, _handle) = project_b
487 .update(cx_b, |p, cx| {
488 p.open_buffer_with_lsp((worktree_id, "a.ts"), cx)
489 })
490 .await
491 .expect("user B opens buffer for formatting");
492
493 cx_a.update(|cx| {
494 SettingsStore::update_global(cx, |store, cx| {
495 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
496 file.defaults.formatter = Some(SelectedFormatter::Auto);
497 file.defaults.prettier = Some(PrettierSettings {
498 allowed: true,
499 ..PrettierSettings::default()
500 });
501 });
502 });
503 });
504 cx_b.update(|cx| {
505 SettingsStore::update_global(cx, |store, cx| {
506 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
507 file.defaults.formatter =
508 Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
509 name: None,
510 }]));
511 file.defaults.prettier = Some(PrettierSettings {
512 allowed: true,
513 ..PrettierSettings::default()
514 });
515 });
516 });
517 });
518 let fake_language_server = fake_language_servers.next().await.unwrap();
519 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(|_, _| async move {
520 panic!(
521 "Unexpected: prettier should be preferred since it's enabled and language supports it"
522 )
523 });
524
525 project_b
526 .update(cx_b, |project, cx| {
527 project.format(
528 HashSet::from_iter([buffer_b.clone()]),
529 LspFormatTarget::Buffers,
530 true,
531 FormatTrigger::Save,
532 cx,
533 )
534 })
535 .await
536 .unwrap();
537
538 executor.run_until_parked();
539 assert_eq!(
540 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
541 buffer_text.to_string() + "\n" + prettier_format_suffix,
542 "Prettier formatting was not applied to client buffer after client's request"
543 );
544
545 // User A opens and formats the same buffer too
546 let buffer_a = project_a
547 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
548 .await
549 .expect("user A opens buffer for formatting");
550
551 cx_a.update(|cx| {
552 SettingsStore::update_global(cx, |store, cx| {
553 store.update_user_settings::<AllLanguageSettings>(cx, |file| {
554 file.defaults.formatter = Some(SelectedFormatter::Auto);
555 file.defaults.prettier = Some(PrettierSettings {
556 allowed: true,
557 ..PrettierSettings::default()
558 });
559 });
560 });
561 });
562 project_a
563 .update(cx_a, |project, cx| {
564 project.format(
565 HashSet::from_iter([buffer_a.clone()]),
566 LspFormatTarget::Buffers,
567 true,
568 FormatTrigger::Manual,
569 cx,
570 )
571 })
572 .await
573 .unwrap();
574
575 executor.run_until_parked();
576 assert_eq!(
577 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
578 buffer_text.to_string() + "\n" + prettier_format_suffix + "\n" + prettier_format_suffix,
579 "Prettier formatting was not applied to client buffer after host's request"
580 );
581}
582
583#[gpui::test]
584async fn test_remote_server_debugger(
585 cx_a: &mut TestAppContext,
586 server_cx: &mut TestAppContext,
587 executor: BackgroundExecutor,
588) {
589 cx_a.update(|cx| {
590 release_channel::init(SemanticVersion::default(), cx);
591 command_palette_hooks::init(cx);
592 zlog::init_test();
593 dap_adapters::init(cx);
594 });
595 server_cx.update(|cx| {
596 release_channel::init(SemanticVersion::default(), cx);
597 dap_adapters::init(cx);
598 });
599 let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
600 let remote_fs = FakeFs::new(server_cx.executor());
601 remote_fs
602 .insert_tree(
603 path!("/code"),
604 json!({
605 "lib.rs": "fn one() -> usize { 1 }"
606 }),
607 )
608 .await;
609
610 // User A connects to the remote project via SSH.
611 server_cx.update(HeadlessProject::init);
612 let remote_http_client = Arc::new(BlockedHttpClient);
613 let node = NodeRuntime::unavailable();
614 let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
615 let _headless_project = server_cx.new(|cx| {
616 client::init_settings(cx);
617 HeadlessProject::new(
618 HeadlessAppState {
619 session: server_ssh,
620 fs: remote_fs.clone(),
621 http_client: remote_http_client,
622 node_runtime: node,
623 languages,
624 extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
625 },
626 cx,
627 )
628 });
629
630 let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
631 let mut server = TestServer::start(server_cx.executor()).await;
632 let client_a = server.create_client(cx_a, "user_a").await;
633 cx_a.update(|cx| {
634 debugger_ui::init(cx);
635 command_palette_hooks::init(cx);
636 });
637 let (project_a, _) = client_a
638 .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a)
639 .await;
640
641 let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a);
642
643 let debugger_panel = workspace
644 .update_in(cx_a, |_workspace, window, cx| {
645 cx.spawn_in(window, DebugPanel::load)
646 })
647 .await
648 .unwrap();
649
650 workspace.update_in(cx_a, |workspace, window, cx| {
651 workspace.add_panel(debugger_panel, window, cx);
652 });
653
654 cx_a.run_until_parked();
655 let debug_panel = workspace
656 .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx))
657 .unwrap();
658
659 let workspace_window = cx_a
660 .window_handle()
661 .downcast::<workspace::Workspace>()
662 .unwrap();
663
664 let session = debugger_ui::tests::start_debug_session(&workspace_window, cx_a, |_| {}).unwrap();
665 cx_a.run_until_parked();
666 debug_panel.update(cx_a, |debug_panel, cx| {
667 assert_eq!(
668 debug_panel.active_session().unwrap().read(cx).session(cx),
669 session
670 )
671 });
672
673 session.update(cx_a, |session, _| {
674 assert_eq!(session.binary().unwrap().command.as_deref(), Some("ssh"));
675 });
676
677 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
678 workspace.project().update(cx, |project, cx| {
679 project.dap_store().update(cx, |dap_store, cx| {
680 dap_store.shutdown_session(session.read(cx).session_id(), cx)
681 })
682 })
683 });
684
685 client_ssh.update(cx_a, |a, _| {
686 a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor)
687 });
688
689 shutdown_session.await.unwrap();
690}