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_path_in_buffer(
608 cx: &mut TestAppContext,
609 server_cx: &mut TestAppContext,
610) {
611 let fs = FakeFs::new(server_cx.executor());
612 fs.insert_tree(
613 "/code",
614 json!({
615 "project1": {
616 ".git": {},
617 "README.md": "# project 1",
618 "src": {
619 "lib.rs": "fn one() -> usize { 1 }"
620 }
621 },
622 }),
623 )
624 .await;
625
626 let (project, _headless) = init_test(&fs, cx, server_cx).await;
627 let (worktree, _) = project
628 .update(cx, |project, cx| {
629 project.find_or_create_worktree("/code/project1", true, cx)
630 })
631 .await
632 .unwrap();
633
634 let worktree_id = cx.update(|cx| worktree.read(cx).id());
635
636 let buffer = project
637 .update(cx, |project, cx| {
638 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
639 })
640 .await
641 .unwrap();
642
643 let path = project
644 .update(cx, |project, cx| {
645 project.resolve_path_in_buffer("/code/project1/README.md", &buffer, cx)
646 })
647 .await
648 .unwrap();
649 assert!(path.is_file());
650 assert_eq!(
651 path.abs_path().unwrap().to_string_lossy(),
652 "/code/project1/README.md"
653 );
654
655 let path = project
656 .update(cx, |project, cx| {
657 project.resolve_path_in_buffer("../README.md", &buffer, cx)
658 })
659 .await
660 .unwrap();
661 assert!(path.is_file());
662 assert_eq!(
663 path.project_path().unwrap().clone(),
664 ProjectPath::from((worktree_id, "README.md"))
665 );
666
667 let path = project
668 .update(cx, |project, cx| {
669 project.resolve_path_in_buffer("../src", &buffer, cx)
670 })
671 .await
672 .unwrap();
673 assert_eq!(
674 path.project_path().unwrap().clone(),
675 ProjectPath::from((worktree_id, "src"))
676 );
677 assert!(path.is_dir());
678}
679
680#[gpui::test]
681async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
682 let fs = FakeFs::new(server_cx.executor());
683 fs.insert_tree(
684 "/code",
685 json!({
686 "project1": {
687 ".git": {},
688 "README.md": "# project 1",
689 "src": {
690 "lib.rs": "fn one() -> usize { 1 }"
691 }
692 },
693 }),
694 )
695 .await;
696
697 let (project, _headless) = init_test(&fs, cx, server_cx).await;
698
699 let path = project
700 .update(cx, |project, cx| {
701 project.resolve_abs_path("/code/project1/README.md", cx)
702 })
703 .await
704 .unwrap();
705
706 assert!(path.is_file());
707 assert_eq!(
708 path.abs_path().unwrap().to_string_lossy(),
709 "/code/project1/README.md"
710 );
711
712 let path = project
713 .update(cx, |project, cx| {
714 project.resolve_abs_path("/code/project1/src", cx)
715 })
716 .await
717 .unwrap();
718
719 assert!(path.is_dir());
720 assert_eq!(
721 path.abs_path().unwrap().to_string_lossy(),
722 "/code/project1/src"
723 );
724
725 let path = project
726 .update(cx, |project, cx| {
727 project.resolve_abs_path("/code/project1/DOESNOTEXIST", cx)
728 })
729 .await;
730 assert!(path.is_none());
731}
732
733#[gpui::test(iterations = 10)]
734async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
735 let fs = FakeFs::new(server_cx.executor());
736 fs.insert_tree(
737 "/code",
738 json!({
739 "project1": {
740 ".git": {},
741 "README.md": "# project 1",
742 "src": {
743 "lib.rs": "fn one() -> usize { 1 }"
744 }
745 },
746 }),
747 )
748 .await;
749
750 let (project, _headless) = init_test(&fs, cx, server_cx).await;
751 let (worktree, _) = project
752 .update(cx, |project, cx| {
753 project.find_or_create_worktree("/code/project1", true, cx)
754 })
755 .await
756 .unwrap();
757 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
758
759 // Open a buffer on the client but cancel after a random amount of time.
760 let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
761 cx.executor().simulate_random_delay().await;
762 drop(buffer);
763
764 // Try opening the same buffer again as the client, and ensure we can
765 // still do it despite the cancellation above.
766 let buffer = project
767 .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
768 .await
769 .unwrap();
770
771 buffer.read_with(cx, |buf, _| {
772 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
773 });
774}
775
776#[gpui::test]
777async fn test_adding_then_removing_then_adding_worktrees(
778 cx: &mut TestAppContext,
779 server_cx: &mut TestAppContext,
780) {
781 let fs = FakeFs::new(server_cx.executor());
782 fs.insert_tree(
783 "/code",
784 json!({
785 "project1": {
786 ".git": {},
787 "README.md": "# project 1",
788 "src": {
789 "lib.rs": "fn one() -> usize { 1 }"
790 }
791 },
792 "project2": {
793 "README.md": "# project 2",
794 },
795 }),
796 )
797 .await;
798
799 let (project, _headless) = init_test(&fs, cx, server_cx).await;
800 let (_worktree, _) = project
801 .update(cx, |project, cx| {
802 project.find_or_create_worktree("/code/project1", true, cx)
803 })
804 .await
805 .unwrap();
806
807 let (worktree_2, _) = project
808 .update(cx, |project, cx| {
809 project.find_or_create_worktree("/code/project2", true, cx)
810 })
811 .await
812 .unwrap();
813 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
814
815 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
816
817 let (worktree_2, _) = project
818 .update(cx, |project, cx| {
819 project.find_or_create_worktree("/code/project2", true, cx)
820 })
821 .await
822 .unwrap();
823
824 cx.run_until_parked();
825 worktree_2.update(cx, |worktree, _cx| {
826 assert!(worktree.is_visible());
827 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
828 assert_eq!(entries.len(), 2);
829 assert_eq!(
830 entries[1].path.to_string_lossy().to_string(),
831 "README.md".to_string()
832 )
833 })
834}
835
836#[gpui::test]
837async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
838 let fs = FakeFs::new(server_cx.executor());
839 fs.insert_tree(
840 "/code",
841 json!({
842 "project1": {
843 ".git": {},
844 "README.md": "# project 1",
845 "src": {
846 "lib.rs": "fn one() -> usize { 1 }"
847 }
848 },
849 }),
850 )
851 .await;
852
853 let (project, _headless) = init_test(&fs, cx, server_cx).await;
854 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
855 cx.executor().run_until_parked();
856
857 let buffer = buffer.await.unwrap();
858
859 cx.update(|cx| {
860 assert_eq!(
861 buffer.read(cx).text(),
862 initial_server_settings_content().to_string()
863 )
864 })
865}
866
867#[gpui::test(iterations = 20)]
868async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
869 let fs = FakeFs::new(server_cx.executor());
870 fs.insert_tree(
871 "/code",
872 json!({
873 "project1": {
874 ".git": {},
875 "README.md": "# project 1",
876 "src": {
877 "lib.rs": "fn one() -> usize { 1 }"
878 }
879 },
880 }),
881 )
882 .await;
883
884 let (project, _headless) = init_test(&fs, cx, server_cx).await;
885
886 let (worktree, _) = project
887 .update(cx, |project, cx| {
888 project.find_or_create_worktree("/code/project1", true, cx)
889 })
890 .await
891 .unwrap();
892
893 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
894 let buffer = project
895 .update(cx, |project, cx| {
896 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
897 })
898 .await
899 .unwrap();
900
901 buffer.update(cx, |buffer, cx| {
902 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
903 let ix = buffer.text().find('1').unwrap();
904 buffer.edit([(ix..ix + 1, "100")], None, cx);
905 });
906
907 let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
908 client
909 .update(cx, |client, cx| client.simulate_disconnect(cx))
910 .detach();
911
912 project
913 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
914 .await
915 .unwrap();
916
917 assert_eq!(
918 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
919 "fn one() -> usize { 100 }"
920 );
921}
922
923#[gpui::test]
924async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
925 let fs = FakeFs::new(server_cx.executor());
926 fs.insert_tree(
927 "/code",
928 json!({
929 "project1": {
930 ".git": {},
931 "README.md": "# project 1",
932 },
933 }),
934 )
935 .await;
936
937 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
938 let branches = ["main", "dev", "feature-1"];
939 fs.insert_branches(Path::new("/code/project1/.git"), &branches);
940
941 let (worktree, _) = project
942 .update(cx, |project, cx| {
943 project.find_or_create_worktree("/code/project1", true, cx)
944 })
945 .await
946 .unwrap();
947
948 let worktree_id = cx.update(|cx| worktree.read(cx).id());
949 let root_path = ProjectPath::root_path(worktree_id);
950 // Give the worktree a bit of time to index the file system
951 cx.run_until_parked();
952
953 let remote_branches = project
954 .update(cx, |project, cx| project.branches(root_path.clone(), cx))
955 .await
956 .unwrap();
957
958 let new_branch = branches[2];
959
960 let remote_branches = remote_branches
961 .into_iter()
962 .map(|branch| branch.name)
963 .collect::<Vec<_>>();
964
965 assert_eq!(&remote_branches, &branches);
966
967 cx.update(|cx| {
968 project.update(cx, |project, cx| {
969 project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
970 })
971 })
972 .await
973 .unwrap();
974
975 cx.run_until_parked();
976
977 let server_branch = server_cx.update(|cx| {
978 headless_project.update(cx, |headless_project, cx| {
979 headless_project
980 .worktree_store
981 .update(cx, |worktree_store, cx| {
982 worktree_store
983 .current_branch(root_path.clone(), cx)
984 .unwrap()
985 })
986 })
987 });
988
989 assert_eq!(server_branch.as_ref(), branches[2]);
990
991 // Also try creating a new branch
992 cx.update(|cx| {
993 project.update(cx, |project, cx| {
994 project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
995 })
996 })
997 .await
998 .unwrap();
999
1000 cx.run_until_parked();
1001
1002 let server_branch = server_cx.update(|cx| {
1003 headless_project.update(cx, |headless_project, cx| {
1004 headless_project
1005 .worktree_store
1006 .update(cx, |worktree_store, cx| {
1007 worktree_store.current_branch(root_path, cx).unwrap()
1008 })
1009 })
1010 });
1011
1012 assert_eq!(server_branch.as_ref(), "totally-new-branch");
1013}
1014
1015pub async fn init_test(
1016 server_fs: &Arc<FakeFs>,
1017 cx: &mut TestAppContext,
1018 server_cx: &mut TestAppContext,
1019) -> (Model<Project>, Model<HeadlessProject>) {
1020 let server_fs = server_fs.clone();
1021 init_logger();
1022
1023 let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
1024 let http_client = Arc::new(BlockedHttpClient);
1025 let node_runtime = NodeRuntime::unavailable();
1026 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1027 server_cx.update(HeadlessProject::init);
1028 let headless = server_cx.new_model(|cx| {
1029 client::init_settings(cx);
1030
1031 HeadlessProject::new(
1032 crate::HeadlessAppState {
1033 session: ssh_server_client,
1034 fs: server_fs.clone(),
1035 http_client,
1036 node_runtime,
1037 languages,
1038 },
1039 cx,
1040 )
1041 });
1042
1043 let ssh = SshRemoteClient::fake_client(opts, cx).await;
1044 let project = build_project(ssh, cx);
1045 project
1046 .update(cx, {
1047 let headless = headless.clone();
1048 |_, cx| cx.on_release(|_, _| drop(headless))
1049 })
1050 .detach();
1051 (project, headless)
1052}
1053
1054fn init_logger() {
1055 if std::env::var("RUST_LOG").is_ok() {
1056 env_logger::try_init().ok();
1057 }
1058}
1059
1060fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
1061 cx.update(|cx| {
1062 if !cx.has_global::<SettingsStore>() {
1063 let settings_store = SettingsStore::test(cx);
1064 cx.set_global(settings_store);
1065 }
1066 });
1067
1068 let client = cx.update(|cx| {
1069 Client::new(
1070 Arc::new(FakeSystemClock::default()),
1071 FakeHttpClient::with_404_response(),
1072 cx,
1073 )
1074 });
1075
1076 let node = NodeRuntime::unavailable();
1077 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
1078 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1079 let fs = FakeFs::new(cx.executor());
1080
1081 cx.update(|cx| {
1082 Project::init(&client, cx);
1083 language::init(cx);
1084 });
1085
1086 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1087}