1use crate::headless_project::HeadlessProject;
2use client::{Client, UserStore};
3use clock::FakeSystemClock;
4use fs::{FakeFs, Fs};
5use gpui::{Context, Model, TestAppContext};
6use http_client::{BlockedHttpClient, FakeHttpClient};
7use language::{
8 language_settings::{language_settings, AllLanguageSettings},
9 Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName,
10 LineEnding,
11};
12use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind};
13use node_runtime::NodeRuntime;
14use project::{
15 search::{SearchQuery, SearchResult},
16 Project, ProjectPath,
17};
18use remote::SshRemoteClient;
19use serde_json::json;
20use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
21use smol::stream::StreamExt;
22use std::{
23 path::{Path, PathBuf},
24 sync::Arc,
25};
26
27#[gpui::test]
28async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
29 let fs = FakeFs::new(server_cx.executor());
30 fs.insert_tree(
31 "/code",
32 json!({
33 "project1": {
34 ".git": {},
35 "README.md": "# project 1",
36 "src": {
37 "lib.rs": "fn one() -> usize { 1 }"
38 }
39 },
40 "project2": {
41 "README.md": "# project 2",
42 },
43 }),
44 )
45 .await;
46 fs.set_index_for_repo(
47 Path::new("/code/project1/.git"),
48 &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
49 );
50
51 let (project, _headless) = init_test(&fs, cx, server_cx).await;
52 let (worktree, _) = project
53 .update(cx, |project, cx| {
54 project.find_or_create_worktree("/code/project1", true, cx)
55 })
56 .await
57 .unwrap();
58
59 // The client sees the worktree's contents.
60 cx.executor().run_until_parked();
61 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
62 worktree.update(cx, |worktree, _cx| {
63 assert_eq!(
64 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
65 vec![
66 Path::new("README.md"),
67 Path::new("src"),
68 Path::new("src/lib.rs"),
69 ]
70 );
71 });
72
73 // The user opens a buffer in the remote worktree. The buffer's
74 // contents are loaded from the remote filesystem.
75 let buffer = project
76 .update(cx, |project, cx| {
77 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
78 })
79 .await
80 .unwrap();
81
82 buffer.update(cx, |buffer, cx| {
83 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
84 assert_eq!(
85 buffer.diff_base().unwrap().to_string(),
86 "fn one() -> usize { 0 }"
87 );
88 let ix = buffer.text().find('1').unwrap();
89 buffer.edit([(ix..ix + 1, "100")], None, cx);
90 });
91
92 // The user saves the buffer. The new contents are written to the
93 // remote filesystem.
94 project
95 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
96 .await
97 .unwrap();
98 assert_eq!(
99 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
100 "fn one() -> usize { 100 }"
101 );
102
103 // A new file is created in the remote filesystem. The user
104 // sees the new file.
105 fs.save(
106 "/code/project1/src/main.rs".as_ref(),
107 &"fn main() {}".into(),
108 Default::default(),
109 )
110 .await
111 .unwrap();
112 cx.executor().run_until_parked();
113 worktree.update(cx, |worktree, _cx| {
114 assert_eq!(
115 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
116 vec![
117 Path::new("README.md"),
118 Path::new("src"),
119 Path::new("src/lib.rs"),
120 Path::new("src/main.rs"),
121 ]
122 );
123 });
124
125 // A file that is currently open in a buffer is renamed.
126 fs.rename(
127 "/code/project1/src/lib.rs".as_ref(),
128 "/code/project1/src/lib2.rs".as_ref(),
129 Default::default(),
130 )
131 .await
132 .unwrap();
133 cx.executor().run_until_parked();
134 buffer.update(cx, |buffer, _| {
135 assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
136 });
137
138 fs.set_index_for_repo(
139 Path::new("/code/project1/.git"),
140 &[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())],
141 );
142 cx.executor().run_until_parked();
143 buffer.update(cx, |buffer, _| {
144 assert_eq!(
145 buffer.diff_base().unwrap().to_string(),
146 "fn one() -> usize { 100 }"
147 );
148 });
149}
150
151#[gpui::test]
152async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
153 let fs = FakeFs::new(server_cx.executor());
154 fs.insert_tree(
155 "/code",
156 json!({
157 "project1": {
158 ".git": {},
159 "README.md": "# project 1",
160 "src": {
161 "lib.rs": "fn one() -> usize { 1 }"
162 }
163 },
164 }),
165 )
166 .await;
167
168 let (project, headless) = init_test(&fs, cx, server_cx).await;
169
170 project
171 .update(cx, |project, cx| {
172 project.find_or_create_worktree("/code/project1", true, cx)
173 })
174 .await
175 .unwrap();
176
177 cx.run_until_parked();
178
179 async fn do_search(project: &Model<Project>, mut cx: TestAppContext) -> Model<Buffer> {
180 let mut receiver = project.update(&mut cx, |project, cx| {
181 project.search(
182 SearchQuery::text(
183 "project",
184 false,
185 true,
186 false,
187 Default::default(),
188 Default::default(),
189 None,
190 )
191 .unwrap(),
192 cx,
193 )
194 });
195
196 let first_response = receiver.next().await.unwrap();
197 let SearchResult::Buffer { buffer, .. } = first_response else {
198 panic!("incorrect result");
199 };
200 buffer.update(&mut cx, |buffer, cx| {
201 assert_eq!(
202 buffer.file().unwrap().full_path(cx).to_string_lossy(),
203 "project1/README.md"
204 )
205 });
206
207 assert!(receiver.next().await.is_none());
208 buffer
209 }
210
211 let buffer = do_search(&project, cx.clone()).await;
212
213 // test that the headless server is tracking which buffers we have open correctly.
214 cx.run_until_parked();
215 headless.update(server_cx, |headless, cx| {
216 assert!(!headless.buffer_store.read(cx).shared_buffers().is_empty())
217 });
218 do_search(&project, cx.clone()).await;
219
220 cx.update(|_| {
221 drop(buffer);
222 });
223 cx.run_until_parked();
224 headless.update(server_cx, |headless, cx| {
225 assert!(headless.buffer_store.read(cx).shared_buffers().is_empty())
226 });
227
228 do_search(&project, cx.clone()).await;
229}
230
231#[gpui::test]
232async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
233 let fs = FakeFs::new(server_cx.executor());
234 fs.insert_tree(
235 "/code",
236 json!({
237 "project1": {
238 ".git": {},
239 "README.md": "# project 1",
240 "src": {
241 "lib.rs": "fn one() -> usize { 1 }"
242 }
243 },
244 }),
245 )
246 .await;
247
248 let (project, headless) = init_test(&fs, cx, server_cx).await;
249
250 cx.update_global(|settings_store: &mut SettingsStore, cx| {
251 settings_store.set_user_settings(
252 r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
253 cx,
254 )
255 })
256 .unwrap();
257
258 cx.run_until_parked();
259
260 server_cx.read(|cx| {
261 assert_eq!(
262 AllLanguageSettings::get_global(cx)
263 .language(None, Some(&"Rust".into()), cx)
264 .language_servers,
265 ["..."] // local settings are ignored
266 )
267 });
268
269 server_cx
270 .update_global(|settings_store: &mut SettingsStore, cx| {
271 settings_store.set_server_settings(
272 r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
273 cx,
274 )
275 })
276 .unwrap();
277
278 cx.run_until_parked();
279
280 server_cx.read(|cx| {
281 assert_eq!(
282 AllLanguageSettings::get_global(cx)
283 .language(None, Some(&"Rust".into()), cx)
284 .language_servers,
285 ["from-server-settings".to_string()]
286 )
287 });
288
289 fs.insert_tree(
290 "/code/project1/.zed",
291 json!({
292 "settings.json": r#"
293 {
294 "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
295 "lsp": {
296 "override-rust-analyzer": {
297 "binary": {
298 "path": "~/.cargo/bin/rust-analyzer"
299 }
300 }
301 }
302 }"#
303 }),
304 )
305 .await;
306
307 let worktree_id = project
308 .update(cx, |project, cx| {
309 project.find_or_create_worktree("/code/project1", true, cx)
310 })
311 .await
312 .unwrap()
313 .0
314 .read_with(cx, |worktree, _| worktree.id());
315
316 let buffer = project
317 .update(cx, |project, cx| {
318 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
319 })
320 .await
321 .unwrap();
322 cx.run_until_parked();
323
324 server_cx.read(|cx| {
325 let worktree_id = headless
326 .read(cx)
327 .worktree_store
328 .read(cx)
329 .worktrees()
330 .next()
331 .unwrap()
332 .read(cx)
333 .id();
334 assert_eq!(
335 AllLanguageSettings::get(
336 Some(SettingsLocation {
337 worktree_id,
338 path: Path::new("src/lib.rs")
339 }),
340 cx
341 )
342 .language(None, Some(&"Rust".into()), cx)
343 .language_servers,
344 ["override-rust-analyzer".to_string()]
345 )
346 });
347
348 cx.read(|cx| {
349 let file = buffer.read(cx).file();
350 assert_eq!(
351 language_settings(Some("Rust".into()), file, cx).language_servers,
352 ["override-rust-analyzer".to_string()]
353 )
354 });
355}
356
357#[gpui::test]
358async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
359 let fs = FakeFs::new(server_cx.executor());
360 fs.insert_tree(
361 "/code",
362 json!({
363 "project1": {
364 ".git": {},
365 "README.md": "# project 1",
366 "src": {
367 "lib.rs": "fn one() -> usize { 1 }"
368 }
369 },
370 }),
371 )
372 .await;
373
374 let (project, headless) = init_test(&fs, cx, server_cx).await;
375
376 fs.insert_tree(
377 "/code/project1/.zed",
378 json!({
379 "settings.json": r#"
380 {
381 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
382 "lsp": {
383 "rust-analyzer": {
384 "binary": {
385 "path": "~/.cargo/bin/rust-analyzer"
386 }
387 }
388 }
389 }"#
390 }),
391 )
392 .await;
393
394 cx.update_model(&project, |project, _| {
395 project.languages().register_test_language(LanguageConfig {
396 name: "Rust".into(),
397 matcher: LanguageMatcher {
398 path_suffixes: vec!["rs".into()],
399 ..Default::default()
400 },
401 ..Default::default()
402 });
403 project.languages().register_fake_lsp_adapter(
404 "Rust",
405 FakeLspAdapter {
406 name: "rust-analyzer",
407 ..Default::default()
408 },
409 )
410 });
411
412 let mut fake_lsp = server_cx.update(|cx| {
413 headless.read(cx).languages.register_fake_language_server(
414 LanguageServerName("rust-analyzer".into()),
415 Default::default(),
416 None,
417 )
418 });
419
420 cx.run_until_parked();
421
422 let worktree_id = project
423 .update(cx, |project, cx| {
424 project.find_or_create_worktree("/code/project1", true, cx)
425 })
426 .await
427 .unwrap()
428 .0
429 .read_with(cx, |worktree, _| worktree.id());
430
431 // Wait for the settings to synchronize
432 cx.run_until_parked();
433
434 let buffer = project
435 .update(cx, |project, cx| {
436 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
437 })
438 .await
439 .unwrap();
440 cx.run_until_parked();
441
442 let fake_lsp = fake_lsp.next().await.unwrap();
443
444 cx.read(|cx| {
445 let file = buffer.read(cx).file();
446 assert_eq!(
447 language_settings(Some("Rust".into()), file, cx).language_servers,
448 ["rust-analyzer".to_string()]
449 )
450 });
451
452 let buffer_id = cx.read(|cx| {
453 let buffer = buffer.read(cx);
454 assert_eq!(buffer.language().unwrap().name(), "Rust".into());
455 buffer.remote_id()
456 });
457
458 server_cx.read(|cx| {
459 let buffer = headless
460 .read(cx)
461 .buffer_store
462 .read(cx)
463 .get(buffer_id)
464 .unwrap();
465
466 assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
467 });
468
469 server_cx.read(|cx| {
470 let lsp_store = headless.read(cx).lsp_store.read(cx);
471 assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
472 });
473
474 fake_lsp.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
475 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
476 label: "boop".to_string(),
477 ..Default::default()
478 }])))
479 });
480
481 let result = project
482 .update(cx, |project, cx| {
483 project.completions(
484 &buffer,
485 0,
486 CompletionContext {
487 trigger_kind: CompletionTriggerKind::INVOKED,
488 trigger_character: None,
489 },
490 cx,
491 )
492 })
493 .await
494 .unwrap();
495
496 assert_eq!(
497 result.into_iter().map(|c| c.label.text).collect::<Vec<_>>(),
498 vec!["boop".to_string()]
499 );
500
501 fake_lsp.handle_request::<lsp::request::Rename, _, _>(|_, _| async move {
502 Ok(Some(lsp::WorkspaceEdit {
503 changes: Some(
504 [(
505 lsp::Url::from_file_path("/code/project1/src/lib.rs").unwrap(),
506 vec![lsp::TextEdit::new(
507 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
508 "two".to_string(),
509 )],
510 )]
511 .into_iter()
512 .collect(),
513 ),
514 ..Default::default()
515 }))
516 });
517
518 project
519 .update(cx, |project, cx| {
520 project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
521 })
522 .await
523 .unwrap();
524
525 cx.run_until_parked();
526 buffer.update(cx, |buffer, _| {
527 assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
528 })
529}
530
531#[gpui::test]
532async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
533 let fs = FakeFs::new(server_cx.executor());
534 fs.insert_tree(
535 "/code",
536 json!({
537 "project1": {
538 ".git": {},
539 "README.md": "# project 1",
540 "src": {
541 "lib.rs": "fn one() -> usize { 1 }"
542 }
543 },
544 }),
545 )
546 .await;
547
548 let (project, _headless) = init_test(&fs, cx, server_cx).await;
549 let (worktree, _) = project
550 .update(cx, |project, cx| {
551 project.find_or_create_worktree("/code/project1", true, cx)
552 })
553 .await
554 .unwrap();
555
556 let worktree_id = cx.update(|cx| worktree.read(cx).id());
557
558 let buffer = project
559 .update(cx, |project, cx| {
560 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
561 })
562 .await
563 .unwrap();
564
565 fs.save(
566 &PathBuf::from("/code/project1/src/lib.rs"),
567 &("bangles".to_string().into()),
568 LineEnding::Unix,
569 )
570 .await
571 .unwrap();
572
573 cx.run_until_parked();
574
575 buffer.update(cx, |buffer, cx| {
576 assert_eq!(buffer.text(), "bangles");
577 buffer.edit([(0..0, "a")], None, cx);
578 });
579
580 fs.save(
581 &PathBuf::from("/code/project1/src/lib.rs"),
582 &("bloop".to_string().into()),
583 LineEnding::Unix,
584 )
585 .await
586 .unwrap();
587
588 cx.run_until_parked();
589 cx.update(|cx| {
590 assert!(buffer.read(cx).has_conflict());
591 });
592
593 project
594 .update(cx, |project, cx| {
595 project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
596 })
597 .await
598 .unwrap();
599 cx.run_until_parked();
600
601 cx.update(|cx| {
602 assert!(!buffer.read(cx).has_conflict());
603 });
604}
605
606#[gpui::test]
607async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
608 let fs = FakeFs::new(server_cx.executor());
609 fs.insert_tree(
610 "/code",
611 json!({
612 "project1": {
613 ".git": {},
614 "README.md": "# project 1",
615 "src": {
616 "lib.rs": "fn one() -> usize { 1 }"
617 }
618 },
619 }),
620 )
621 .await;
622
623 let (project, _headless) = init_test(&fs, cx, server_cx).await;
624 let (worktree, _) = project
625 .update(cx, |project, cx| {
626 project.find_or_create_worktree("/code/project1", true, cx)
627 })
628 .await
629 .unwrap();
630
631 let worktree_id = cx.update(|cx| worktree.read(cx).id());
632
633 let buffer = project
634 .update(cx, |project, cx| {
635 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
636 })
637 .await
638 .unwrap();
639
640 let path = project
641 .update(cx, |project, cx| {
642 project.resolve_existing_file_path("/code/project1/README.md", &buffer, cx)
643 })
644 .await
645 .unwrap();
646 assert_eq!(
647 path.abs_path().unwrap().to_string_lossy(),
648 "/code/project1/README.md"
649 );
650
651 let path = project
652 .update(cx, |project, cx| {
653 project.resolve_existing_file_path("../README.md", &buffer, cx)
654 })
655 .await
656 .unwrap();
657
658 assert_eq!(
659 path.project_path().unwrap().clone(),
660 ProjectPath::from((worktree_id, "README.md"))
661 );
662}
663
664#[gpui::test(iterations = 10)]
665async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
666 let fs = FakeFs::new(server_cx.executor());
667 fs.insert_tree(
668 "/code",
669 json!({
670 "project1": {
671 ".git": {},
672 "README.md": "# project 1",
673 "src": {
674 "lib.rs": "fn one() -> usize { 1 }"
675 }
676 },
677 }),
678 )
679 .await;
680
681 let (project, _headless) = init_test(&fs, cx, server_cx).await;
682 let (worktree, _) = project
683 .update(cx, |project, cx| {
684 project.find_or_create_worktree("/code/project1", true, cx)
685 })
686 .await
687 .unwrap();
688 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
689
690 // Open a buffer on the client but cancel after a random amount of time.
691 let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
692 cx.executor().simulate_random_delay().await;
693 drop(buffer);
694
695 // Try opening the same buffer again as the client, and ensure we can
696 // still do it despite the cancellation above.
697 let buffer = project
698 .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
699 .await
700 .unwrap();
701
702 buffer.read_with(cx, |buf, _| {
703 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
704 });
705}
706
707#[gpui::test]
708async fn test_adding_then_removing_then_adding_worktrees(
709 cx: &mut TestAppContext,
710 server_cx: &mut TestAppContext,
711) {
712 let fs = FakeFs::new(server_cx.executor());
713 fs.insert_tree(
714 "/code",
715 json!({
716 "project1": {
717 ".git": {},
718 "README.md": "# project 1",
719 "src": {
720 "lib.rs": "fn one() -> usize { 1 }"
721 }
722 },
723 "project2": {
724 "README.md": "# project 2",
725 },
726 }),
727 )
728 .await;
729
730 let (project, _headless) = init_test(&fs, cx, server_cx).await;
731 let (_worktree, _) = project
732 .update(cx, |project, cx| {
733 project.find_or_create_worktree("/code/project1", true, cx)
734 })
735 .await
736 .unwrap();
737
738 let (worktree_2, _) = project
739 .update(cx, |project, cx| {
740 project.find_or_create_worktree("/code/project2", true, cx)
741 })
742 .await
743 .unwrap();
744 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
745
746 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
747
748 let (worktree_2, _) = project
749 .update(cx, |project, cx| {
750 project.find_or_create_worktree("/code/project2", true, cx)
751 })
752 .await
753 .unwrap();
754
755 cx.run_until_parked();
756 worktree_2.update(cx, |worktree, _cx| {
757 assert!(worktree.is_visible());
758 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
759 assert_eq!(entries.len(), 2);
760 assert_eq!(
761 entries[1].path.to_string_lossy().to_string(),
762 "README.md".to_string()
763 )
764 })
765}
766
767#[gpui::test]
768async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
769 let fs = FakeFs::new(server_cx.executor());
770 fs.insert_tree(
771 "/code",
772 json!({
773 "project1": {
774 ".git": {},
775 "README.md": "# project 1",
776 "src": {
777 "lib.rs": "fn one() -> usize { 1 }"
778 }
779 },
780 }),
781 )
782 .await;
783
784 let (project, _headless) = init_test(&fs, cx, server_cx).await;
785 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
786 cx.executor().run_until_parked();
787
788 let buffer = buffer.await.unwrap();
789
790 cx.update(|cx| {
791 assert_eq!(
792 buffer.read(cx).text(),
793 initial_server_settings_content().to_string()
794 )
795 })
796}
797
798#[gpui::test(iterations = 20)]
799async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
800 let fs = FakeFs::new(server_cx.executor());
801 fs.insert_tree(
802 "/code",
803 json!({
804 "project1": {
805 ".git": {},
806 "README.md": "# project 1",
807 "src": {
808 "lib.rs": "fn one() -> usize { 1 }"
809 }
810 },
811 }),
812 )
813 .await;
814
815 let (project, _headless) = init_test(&fs, cx, server_cx).await;
816
817 let (worktree, _) = project
818 .update(cx, |project, cx| {
819 project.find_or_create_worktree("/code/project1", true, cx)
820 })
821 .await
822 .unwrap();
823
824 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
825 let buffer = project
826 .update(cx, |project, cx| {
827 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
828 })
829 .await
830 .unwrap();
831
832 buffer.update(cx, |buffer, cx| {
833 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
834 let ix = buffer.text().find('1').unwrap();
835 buffer.edit([(ix..ix + 1, "100")], None, cx);
836 });
837
838 let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
839 client
840 .update(cx, |client, cx| client.simulate_disconnect(cx))
841 .detach();
842
843 project
844 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
845 .await
846 .unwrap();
847
848 assert_eq!(
849 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
850 "fn one() -> usize { 100 }"
851 );
852}
853
854#[gpui::test]
855async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
856 let fs = FakeFs::new(server_cx.executor());
857 fs.insert_tree(
858 "/code",
859 json!({
860 "project1": {
861 ".git": {},
862 "README.md": "# project 1",
863 },
864 }),
865 )
866 .await;
867
868 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
869 let branches = ["main", "dev", "feature-1"];
870 fs.insert_branches(Path::new("/code/project1/.git"), &branches);
871
872 let (worktree, _) = project
873 .update(cx, |project, cx| {
874 project.find_or_create_worktree("/code/project1", true, cx)
875 })
876 .await
877 .unwrap();
878
879 let worktree_id = cx.update(|cx| worktree.read(cx).id());
880 let root_path = ProjectPath::root_path(worktree_id);
881 // Give the worktree a bit of time to index the file system
882 cx.run_until_parked();
883
884 let remote_branches = project
885 .update(cx, |project, cx| project.branches(root_path.clone(), cx))
886 .await
887 .unwrap();
888
889 let new_branch = branches[2];
890
891 let remote_branches = remote_branches
892 .into_iter()
893 .map(|branch| branch.name)
894 .collect::<Vec<_>>();
895
896 assert_eq!(&remote_branches, &branches);
897
898 cx.update(|cx| {
899 project.update(cx, |project, cx| {
900 project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
901 })
902 })
903 .await
904 .unwrap();
905
906 cx.run_until_parked();
907
908 let server_branch = server_cx.update(|cx| {
909 headless_project.update(cx, |headless_project, cx| {
910 headless_project
911 .worktree_store
912 .update(cx, |worktree_store, cx| {
913 worktree_store
914 .current_branch(root_path.clone(), cx)
915 .unwrap()
916 })
917 })
918 });
919
920 assert_eq!(server_branch.as_ref(), branches[2]);
921
922 // Also try creating a new branch
923 cx.update(|cx| {
924 project.update(cx, |project, cx| {
925 project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
926 })
927 })
928 .await
929 .unwrap();
930
931 cx.run_until_parked();
932
933 let server_branch = server_cx.update(|cx| {
934 headless_project.update(cx, |headless_project, cx| {
935 headless_project
936 .worktree_store
937 .update(cx, |worktree_store, cx| {
938 worktree_store.current_branch(root_path, cx).unwrap()
939 })
940 })
941 });
942
943 assert_eq!(server_branch.as_ref(), "totally-new-branch");
944}
945
946pub async fn init_test(
947 server_fs: &Arc<FakeFs>,
948 cx: &mut TestAppContext,
949 server_cx: &mut TestAppContext,
950) -> (Model<Project>, Model<HeadlessProject>) {
951 let server_fs = server_fs.clone();
952 init_logger();
953
954 let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
955 let http_client = Arc::new(BlockedHttpClient);
956 let node_runtime = NodeRuntime::unavailable();
957 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
958 server_cx.update(HeadlessProject::init);
959 let headless = server_cx.new_model(|cx| {
960 client::init_settings(cx);
961
962 HeadlessProject::new(
963 crate::HeadlessAppState {
964 session: ssh_server_client,
965 fs: server_fs.clone(),
966 http_client,
967 node_runtime,
968 languages,
969 },
970 cx,
971 )
972 });
973
974 let ssh = SshRemoteClient::fake_client(opts, cx).await;
975 let project = build_project(ssh, cx);
976 project
977 .update(cx, {
978 let headless = headless.clone();
979 |_, cx| cx.on_release(|_, _| drop(headless))
980 })
981 .detach();
982 (project, headless)
983}
984
985fn init_logger() {
986 if std::env::var("RUST_LOG").is_ok() {
987 env_logger::try_init().ok();
988 }
989}
990
991fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
992 cx.update(|cx| {
993 if !cx.has_global::<SettingsStore>() {
994 let settings_store = SettingsStore::test(cx);
995 cx.set_global(settings_store);
996 }
997 });
998
999 let client = cx.update(|cx| {
1000 Client::new(
1001 Arc::new(FakeSystemClock::default()),
1002 FakeHttpClient::with_404_response(),
1003 cx,
1004 )
1005 });
1006
1007 let node = NodeRuntime::unavailable();
1008 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
1009 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1010 let fs = FakeFs::new(cx.executor());
1011
1012 cx.update(|cx| {
1013 Project::init(&client, cx);
1014 language::init(cx);
1015 });
1016
1017 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1018}