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