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