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