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