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