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