1/// todo(windows)
2/// The tests in this file assume that server_cx is running on Windows too.
3/// We neead to find a way to test Windows-Non-Windows interactions.
4use crate::headless_project::HeadlessProject;
5use assistant_tool::{Tool as _, ToolResultContent};
6use assistant_tools::{ReadFileTool, ReadFileToolInput};
7use client::{Client, UserStore};
8use clock::FakeSystemClock;
9use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel};
10
11use extension::ExtensionHostProxy;
12use fs::{FakeFs, Fs};
13use gpui::{AppContext as _, Entity, SemanticVersion, TestAppContext};
14use http_client::{BlockedHttpClient, FakeHttpClient};
15use language::{
16 Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
17 language_settings::{AllLanguageSettings, language_settings},
18};
19use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName};
20use node_runtime::NodeRuntime;
21use project::{
22 Project, ProjectPath,
23 search::{SearchQuery, SearchResult},
24};
25use remote::SshRemoteClient;
26use serde_json::json;
27use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
28use smol::stream::StreamExt;
29use std::{
30 collections::HashSet,
31 path::{Path, PathBuf},
32 sync::Arc,
33};
34#[cfg(not(windows))]
35use unindent::Unindent as _;
36use util::{path, separator};
37
38#[gpui::test]
39async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
40 let fs = FakeFs::new(server_cx.executor());
41 fs.insert_tree(
42 path!("/code"),
43 json!({
44 "project1": {
45 ".git": {},
46 "README.md": "# project 1",
47 "src": {
48 "lib.rs": "fn one() -> usize { 1 }"
49 }
50 },
51 "project2": {
52 "README.md": "# project 2",
53 },
54 }),
55 )
56 .await;
57 fs.set_index_for_repo(
58 Path::new(path!("/code/project1/.git")),
59 &[("src/lib.rs".into(), "fn one() -> usize { 0 }".into())],
60 );
61
62 let (project, _headless) = init_test(&fs, cx, server_cx).await;
63 let (worktree, _) = project
64 .update(cx, |project, cx| {
65 project.find_or_create_worktree(path!("/code/project1"), true, cx)
66 })
67 .await
68 .unwrap();
69
70 // The client sees the worktree's contents.
71 cx.executor().run_until_parked();
72 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
73 worktree.update(cx, |worktree, _cx| {
74 assert_eq!(
75 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
76 vec![
77 Path::new("README.md"),
78 Path::new("src"),
79 Path::new("src/lib.rs"),
80 ]
81 );
82 });
83
84 // The user opens a buffer in the remote worktree. The buffer's
85 // contents are loaded from the remote filesystem.
86 let buffer = project
87 .update(cx, |project, cx| {
88 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
89 })
90 .await
91 .unwrap();
92 let diff = project
93 .update(cx, |project, cx| {
94 project.open_unstaged_diff(buffer.clone(), cx)
95 })
96 .await
97 .unwrap();
98
99 diff.update(cx, |diff, _| {
100 assert_eq!(diff.base_text_string().unwrap(), "fn one() -> usize { 0 }");
101 });
102
103 buffer.update(cx, |buffer, cx| {
104 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
105 let ix = buffer.text().find('1').unwrap();
106 buffer.edit([(ix..ix + 1, "100")], None, cx);
107 });
108
109 // The user saves the buffer. The new contents are written to the
110 // remote filesystem.
111 project
112 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
113 .await
114 .unwrap();
115 assert_eq!(
116 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
117 "fn one() -> usize { 100 }"
118 );
119
120 // A new file is created in the remote filesystem. The user
121 // sees the new file.
122 fs.save(
123 path!("/code/project1/src/main.rs").as_ref(),
124 &"fn main() {}".into(),
125 Default::default(),
126 )
127 .await
128 .unwrap();
129 cx.executor().run_until_parked();
130 worktree.update(cx, |worktree, _cx| {
131 assert_eq!(
132 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
133 vec![
134 Path::new("README.md"),
135 Path::new("src"),
136 Path::new("src/lib.rs"),
137 Path::new("src/main.rs"),
138 ]
139 );
140 });
141
142 // A file that is currently open in a buffer is renamed.
143 fs.rename(
144 path!("/code/project1/src/lib.rs").as_ref(),
145 path!("/code/project1/src/lib2.rs").as_ref(),
146 Default::default(),
147 )
148 .await
149 .unwrap();
150 cx.executor().run_until_parked();
151 buffer.update(cx, |buffer, _| {
152 assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
153 });
154
155 fs.set_index_for_repo(
156 Path::new(path!("/code/project1/.git")),
157 &[("src/lib2.rs".into(), "fn one() -> usize { 100 }".into())],
158 );
159 cx.executor().run_until_parked();
160 diff.update(cx, |diff, _| {
161 assert_eq!(
162 diff.base_text_string().unwrap(),
163 "fn one() -> usize { 100 }"
164 );
165 });
166}
167
168#[gpui::test]
169async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
170 let fs = FakeFs::new(server_cx.executor());
171 fs.insert_tree(
172 path!("/code"),
173 json!({
174 "project1": {
175 ".git": {},
176 "README.md": "# project 1",
177 "src": {
178 "lib.rs": "fn one() -> usize { 1 }"
179 }
180 },
181 }),
182 )
183 .await;
184
185 let (project, headless) = init_test(&fs, cx, server_cx).await;
186
187 project
188 .update(cx, |project, cx| {
189 project.find_or_create_worktree(path!("/code/project1"), true, cx)
190 })
191 .await
192 .unwrap();
193
194 cx.run_until_parked();
195
196 async fn do_search(project: &Entity<Project>, mut cx: TestAppContext) -> Entity<Buffer> {
197 let receiver = project.update(&mut cx, |project, cx| {
198 project.search(
199 SearchQuery::text(
200 "project",
201 false,
202 true,
203 false,
204 Default::default(),
205 Default::default(),
206 false,
207 None,
208 )
209 .unwrap(),
210 cx,
211 )
212 });
213
214 let first_response = receiver.recv().await.unwrap();
215 let SearchResult::Buffer { buffer, .. } = first_response else {
216 panic!("incorrect result");
217 };
218 buffer.update(&mut cx, |buffer, cx| {
219 assert_eq!(
220 buffer.file().unwrap().full_path(cx).to_string_lossy(),
221 separator!("project1/README.md")
222 )
223 });
224
225 assert!(receiver.recv().await.is_err());
226 buffer
227 }
228
229 let buffer = do_search(&project, cx.clone()).await;
230
231 // test that the headless server is tracking which buffers we have open correctly.
232 cx.run_until_parked();
233 headless.update(server_cx, |headless, cx| {
234 assert!(headless.buffer_store.read(cx).has_shared_buffers())
235 });
236 do_search(&project, cx.clone()).await;
237
238 cx.update(|_| {
239 drop(buffer);
240 });
241 cx.run_until_parked();
242 headless.update(server_cx, |headless, cx| {
243 assert!(!headless.buffer_store.read(cx).has_shared_buffers())
244 });
245
246 do_search(&project, cx.clone()).await;
247}
248
249#[gpui::test]
250async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
251 let fs = FakeFs::new(server_cx.executor());
252 fs.insert_tree(
253 "/code",
254 json!({
255 "project1": {
256 ".git": {},
257 "README.md": "# project 1",
258 "src": {
259 "lib.rs": "fn one() -> usize { 1 }"
260 }
261 },
262 }),
263 )
264 .await;
265
266 let (project, headless) = init_test(&fs, cx, server_cx).await;
267
268 cx.update_global(|settings_store: &mut SettingsStore, cx| {
269 settings_store.set_user_settings(
270 r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
271 cx,
272 )
273 })
274 .unwrap();
275
276 cx.run_until_parked();
277
278 server_cx.read(|cx| {
279 assert_eq!(
280 AllLanguageSettings::get_global(cx)
281 .language(None, Some(&"Rust".into()), cx)
282 .language_servers,
283 ["..."] // local settings are ignored
284 )
285 });
286
287 server_cx
288 .update_global(|settings_store: &mut SettingsStore, cx| {
289 settings_store.set_server_settings(
290 r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
291 cx,
292 )
293 })
294 .unwrap();
295
296 cx.run_until_parked();
297
298 server_cx.read(|cx| {
299 assert_eq!(
300 AllLanguageSettings::get_global(cx)
301 .language(None, Some(&"Rust".into()), cx)
302 .language_servers,
303 ["from-server-settings".to_string()]
304 )
305 });
306
307 fs.insert_tree(
308 "/code/project1/.zed",
309 json!({
310 "settings.json": r#"
311 {
312 "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
313 "lsp": {
314 "override-rust-analyzer": {
315 "binary": {
316 "path": "~/.cargo/bin/rust-analyzer"
317 }
318 }
319 }
320 }"#
321 }),
322 )
323 .await;
324
325 let worktree_id = project
326 .update(cx, |project, cx| {
327 project.find_or_create_worktree("/code/project1", true, cx)
328 })
329 .await
330 .unwrap()
331 .0
332 .read_with(cx, |worktree, _| worktree.id());
333
334 let buffer = project
335 .update(cx, |project, cx| {
336 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
337 })
338 .await
339 .unwrap();
340 cx.run_until_parked();
341
342 server_cx.read(|cx| {
343 let worktree_id = headless
344 .read(cx)
345 .worktree_store
346 .read(cx)
347 .worktrees()
348 .next()
349 .unwrap()
350 .read(cx)
351 .id();
352 assert_eq!(
353 AllLanguageSettings::get(
354 Some(SettingsLocation {
355 worktree_id,
356 path: Path::new("src/lib.rs")
357 }),
358 cx
359 )
360 .language(None, Some(&"Rust".into()), cx)
361 .language_servers,
362 ["override-rust-analyzer".to_string()]
363 )
364 });
365
366 cx.read(|cx| {
367 let file = buffer.read(cx).file();
368 assert_eq!(
369 language_settings(Some("Rust".into()), file, cx).language_servers,
370 ["override-rust-analyzer".to_string()]
371 )
372 });
373}
374
375#[gpui::test]
376async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
377 let fs = FakeFs::new(server_cx.executor());
378 fs.insert_tree(
379 path!("/code"),
380 json!({
381 "project1": {
382 ".git": {},
383 "README.md": "# project 1",
384 "src": {
385 "lib.rs": "fn one() -> usize { 1 }"
386 }
387 },
388 }),
389 )
390 .await;
391
392 let (project, headless) = init_test(&fs, cx, server_cx).await;
393
394 fs.insert_tree(
395 path!("/code/project1/.zed"),
396 json!({
397 "settings.json": r#"
398 {
399 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
400 "lsp": {
401 "rust-analyzer": {
402 "binary": {
403 "path": "~/.cargo/bin/rust-analyzer"
404 }
405 }
406 }
407 }"#
408 }),
409 )
410 .await;
411
412 cx.update_entity(&project, |project, _| {
413 project.languages().register_test_language(LanguageConfig {
414 name: "Rust".into(),
415 matcher: LanguageMatcher {
416 path_suffixes: vec!["rs".into()],
417 ..Default::default()
418 },
419 ..Default::default()
420 });
421 project.languages().register_fake_lsp_adapter(
422 "Rust",
423 FakeLspAdapter {
424 name: "rust-analyzer",
425 ..Default::default()
426 },
427 )
428 });
429
430 let mut fake_lsp = server_cx.update(|cx| {
431 headless.read(cx).languages.register_fake_language_server(
432 LanguageServerName("rust-analyzer".into()),
433 Default::default(),
434 None,
435 )
436 });
437
438 cx.run_until_parked();
439
440 let worktree_id = project
441 .update(cx, |project, cx| {
442 project.find_or_create_worktree(path!("/code/project1"), true, cx)
443 })
444 .await
445 .unwrap()
446 .0
447 .read_with(cx, |worktree, _| worktree.id());
448
449 // Wait for the settings to synchronize
450 cx.run_until_parked();
451
452 let (buffer, _handle) = project
453 .update(cx, |project, cx| {
454 project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
455 })
456 .await
457 .unwrap();
458 cx.run_until_parked();
459
460 let fake_lsp = fake_lsp.next().await.unwrap();
461
462 cx.read(|cx| {
463 let file = buffer.read(cx).file();
464 assert_eq!(
465 language_settings(Some("Rust".into()), file, cx).language_servers,
466 ["rust-analyzer".to_string()]
467 )
468 });
469
470 let buffer_id = cx.read(|cx| {
471 let buffer = buffer.read(cx);
472 assert_eq!(buffer.language().unwrap().name(), "Rust".into());
473 buffer.remote_id()
474 });
475
476 server_cx.read(|cx| {
477 let buffer = headless
478 .read(cx)
479 .buffer_store
480 .read(cx)
481 .get(buffer_id)
482 .unwrap();
483
484 assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
485 });
486
487 server_cx.read(|cx| {
488 let lsp_store = headless.read(cx).lsp_store.read(cx);
489 assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
490 });
491
492 fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
493 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
494 label: "boop".to_string(),
495 ..Default::default()
496 }])))
497 });
498
499 let result = project
500 .update(cx, |project, cx| {
501 project.completions(
502 &buffer,
503 0,
504 CompletionContext {
505 trigger_kind: CompletionTriggerKind::INVOKED,
506 trigger_character: None,
507 },
508 cx,
509 )
510 })
511 .await
512 .unwrap();
513
514 assert_eq!(
515 result
516 .into_iter()
517 .flat_map(|response| response.completions)
518 .map(|c| c.label.text)
519 .collect::<Vec<_>>(),
520 vec!["boop".to_string()]
521 );
522
523 fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
524 Ok(Some(lsp::WorkspaceEdit {
525 changes: Some(
526 [(
527 lsp::Url::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
528 vec![lsp::TextEdit::new(
529 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
530 "two".to_string(),
531 )],
532 )]
533 .into_iter()
534 .collect(),
535 ),
536 ..Default::default()
537 }))
538 });
539
540 project
541 .update(cx, |project, cx| {
542 project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
543 })
544 .await
545 .unwrap();
546
547 cx.run_until_parked();
548 buffer.update(cx, |buffer, _| {
549 assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
550 })
551}
552
553#[gpui::test]
554async fn test_remote_cancel_language_server_work(
555 cx: &mut TestAppContext,
556 server_cx: &mut TestAppContext,
557) {
558 let fs = FakeFs::new(server_cx.executor());
559 fs.insert_tree(
560 path!("/code"),
561 json!({
562 "project1": {
563 ".git": {},
564 "README.md": "# project 1",
565 "src": {
566 "lib.rs": "fn one() -> usize { 1 }"
567 }
568 },
569 }),
570 )
571 .await;
572
573 let (project, headless) = init_test(&fs, cx, server_cx).await;
574
575 fs.insert_tree(
576 path!("/code/project1/.zed"),
577 json!({
578 "settings.json": r#"
579 {
580 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
581 "lsp": {
582 "rust-analyzer": {
583 "binary": {
584 "path": "~/.cargo/bin/rust-analyzer"
585 }
586 }
587 }
588 }"#
589 }),
590 )
591 .await;
592
593 cx.update_entity(&project, |project, _| {
594 project.languages().register_test_language(LanguageConfig {
595 name: "Rust".into(),
596 matcher: LanguageMatcher {
597 path_suffixes: vec!["rs".into()],
598 ..Default::default()
599 },
600 ..Default::default()
601 });
602 project.languages().register_fake_lsp_adapter(
603 "Rust",
604 FakeLspAdapter {
605 name: "rust-analyzer",
606 ..Default::default()
607 },
608 )
609 });
610
611 let mut fake_lsp = server_cx.update(|cx| {
612 headless.read(cx).languages.register_fake_language_server(
613 LanguageServerName("rust-analyzer".into()),
614 Default::default(),
615 None,
616 )
617 });
618
619 cx.run_until_parked();
620
621 let worktree_id = project
622 .update(cx, |project, cx| {
623 project.find_or_create_worktree(path!("/code/project1"), true, cx)
624 })
625 .await
626 .unwrap()
627 .0
628 .read_with(cx, |worktree, _| worktree.id());
629
630 cx.run_until_parked();
631
632 let (buffer, _handle) = project
633 .update(cx, |project, cx| {
634 project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
635 })
636 .await
637 .unwrap();
638
639 cx.run_until_parked();
640
641 let mut fake_lsp = fake_lsp.next().await.unwrap();
642
643 // Cancelling all language server work for a given buffer
644 {
645 // Two operations, one cancellable and one not.
646 fake_lsp
647 .start_progress_with(
648 "another-token",
649 lsp::WorkDoneProgressBegin {
650 cancellable: Some(false),
651 ..Default::default()
652 },
653 )
654 .await;
655
656 let progress_token = "the-progress-token";
657 fake_lsp
658 .start_progress_with(
659 progress_token,
660 lsp::WorkDoneProgressBegin {
661 cancellable: Some(true),
662 ..Default::default()
663 },
664 )
665 .await;
666
667 cx.executor().run_until_parked();
668
669 project.update(cx, |project, cx| {
670 project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
671 });
672
673 cx.executor().run_until_parked();
674
675 // Verify the cancellation was received on the server side
676 let cancel_notification = fake_lsp
677 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
678 .await;
679 assert_eq!(
680 cancel_notification.token,
681 lsp::NumberOrString::String(progress_token.into())
682 );
683 }
684
685 // Cancelling work by server_id and token
686 {
687 let server_id = fake_lsp.server.server_id();
688 let progress_token = "the-progress-token";
689
690 fake_lsp
691 .start_progress_with(
692 progress_token,
693 lsp::WorkDoneProgressBegin {
694 cancellable: Some(true),
695 ..Default::default()
696 },
697 )
698 .await;
699
700 cx.executor().run_until_parked();
701
702 project.update(cx, |project, cx| {
703 project.cancel_language_server_work(server_id, Some(progress_token.into()), cx)
704 });
705
706 cx.executor().run_until_parked();
707
708 // Verify the cancellation was received on the server side
709 let cancel_notification = fake_lsp
710 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
711 .await;
712 assert_eq!(
713 cancel_notification.token,
714 lsp::NumberOrString::String(progress_token.into())
715 );
716 }
717}
718
719#[gpui::test]
720async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
721 let fs = FakeFs::new(server_cx.executor());
722 fs.insert_tree(
723 path!("/code"),
724 json!({
725 "project1": {
726 ".git": {},
727 "README.md": "# project 1",
728 "src": {
729 "lib.rs": "fn one() -> usize { 1 }"
730 }
731 },
732 }),
733 )
734 .await;
735
736 let (project, _headless) = init_test(&fs, cx, server_cx).await;
737 let (worktree, _) = project
738 .update(cx, |project, cx| {
739 project.find_or_create_worktree(path!("/code/project1"), true, cx)
740 })
741 .await
742 .unwrap();
743
744 let worktree_id = cx.update(|cx| worktree.read(cx).id());
745
746 let buffer = project
747 .update(cx, |project, cx| {
748 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
749 })
750 .await
751 .unwrap();
752
753 fs.save(
754 &PathBuf::from(path!("/code/project1/src/lib.rs")),
755 &("bangles".to_string().into()),
756 LineEnding::Unix,
757 )
758 .await
759 .unwrap();
760
761 cx.run_until_parked();
762
763 buffer.update(cx, |buffer, cx| {
764 assert_eq!(buffer.text(), "bangles");
765 buffer.edit([(0..0, "a")], None, cx);
766 });
767
768 fs.save(
769 &PathBuf::from(path!("/code/project1/src/lib.rs")),
770 &("bloop".to_string().into()),
771 LineEnding::Unix,
772 )
773 .await
774 .unwrap();
775
776 cx.run_until_parked();
777 cx.update(|cx| {
778 assert!(buffer.read(cx).has_conflict());
779 });
780
781 project
782 .update(cx, |project, cx| {
783 project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
784 })
785 .await
786 .unwrap();
787 cx.run_until_parked();
788
789 cx.update(|cx| {
790 assert!(!buffer.read(cx).has_conflict());
791 });
792}
793
794#[gpui::test]
795async fn test_remote_resolve_path_in_buffer(
796 cx: &mut TestAppContext,
797 server_cx: &mut TestAppContext,
798) {
799 let fs = FakeFs::new(server_cx.executor());
800 // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
801 fs.insert_tree(
802 path!("/code"),
803 json!({
804 "project1": {
805 ".git": {},
806 "README.md": "# project 1",
807 "src": {
808 "lib.rs": "fn one() -> usize { 1 }"
809 }
810 },
811 "project2": {
812 ".git": {},
813 "README.md": "# project 2",
814 "src": {
815 "lib.rs": "fn two() -> usize { 2 }"
816 }
817 }
818 }),
819 )
820 .await;
821
822 let (project, _headless) = init_test(&fs, cx, server_cx).await;
823
824 let _ = project
825 .update(cx, |project, cx| {
826 project.find_or_create_worktree(path!("/code/project1"), true, cx)
827 })
828 .await
829 .unwrap();
830
831 let (worktree2, _) = project
832 .update(cx, |project, cx| {
833 project.find_or_create_worktree(path!("/code/project2"), true, cx)
834 })
835 .await
836 .unwrap();
837
838 let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
839
840 let buffer2 = project
841 .update(cx, |project, cx| {
842 project.open_buffer((worktree2_id, Path::new("src/lib.rs")), cx)
843 })
844 .await
845 .unwrap();
846
847 let path = project
848 .update(cx, |project, cx| {
849 project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
850 })
851 .await
852 .unwrap();
853 assert!(path.is_file());
854 assert_eq!(
855 path.abs_path().unwrap().to_string_lossy(),
856 path!("/code/project2/README.md")
857 );
858
859 let path = project
860 .update(cx, |project, cx| {
861 project.resolve_path_in_buffer("../README.md", &buffer2, cx)
862 })
863 .await
864 .unwrap();
865 assert!(path.is_file());
866 assert_eq!(
867 path.project_path().unwrap().clone(),
868 ProjectPath::from((worktree2_id, "README.md"))
869 );
870
871 let path = project
872 .update(cx, |project, cx| {
873 project.resolve_path_in_buffer("../src", &buffer2, cx)
874 })
875 .await
876 .unwrap();
877 assert_eq!(
878 path.project_path().unwrap().clone(),
879 ProjectPath::from((worktree2_id, "src"))
880 );
881 assert!(path.is_dir());
882}
883
884#[gpui::test]
885async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
886 let fs = FakeFs::new(server_cx.executor());
887 fs.insert_tree(
888 path!("/code"),
889 json!({
890 "project1": {
891 ".git": {},
892 "README.md": "# project 1",
893 "src": {
894 "lib.rs": "fn one() -> usize { 1 }"
895 }
896 },
897 }),
898 )
899 .await;
900
901 let (project, _headless) = init_test(&fs, cx, server_cx).await;
902
903 let path = project
904 .update(cx, |project, cx| {
905 project.resolve_abs_path(path!("/code/project1/README.md"), cx)
906 })
907 .await
908 .unwrap();
909
910 assert!(path.is_file());
911 assert_eq!(
912 path.abs_path().unwrap().to_string_lossy(),
913 path!("/code/project1/README.md")
914 );
915
916 let path = project
917 .update(cx, |project, cx| {
918 project.resolve_abs_path(path!("/code/project1/src"), cx)
919 })
920 .await
921 .unwrap();
922
923 assert!(path.is_dir());
924 assert_eq!(
925 path.abs_path().unwrap().to_string_lossy(),
926 path!("/code/project1/src")
927 );
928
929 let path = project
930 .update(cx, |project, cx| {
931 project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
932 })
933 .await;
934 assert!(path.is_none());
935}
936
937#[gpui::test(iterations = 10)]
938async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
939 let fs = FakeFs::new(server_cx.executor());
940 fs.insert_tree(
941 "/code",
942 json!({
943 "project1": {
944 ".git": {},
945 "README.md": "# project 1",
946 "src": {
947 "lib.rs": "fn one() -> usize { 1 }"
948 }
949 },
950 }),
951 )
952 .await;
953
954 let (project, _headless) = init_test(&fs, cx, server_cx).await;
955 let (worktree, _) = project
956 .update(cx, |project, cx| {
957 project.find_or_create_worktree("/code/project1", true, cx)
958 })
959 .await
960 .unwrap();
961 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
962
963 // Open a buffer on the client but cancel after a random amount of time.
964 let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
965 cx.executor().simulate_random_delay().await;
966 drop(buffer);
967
968 // Try opening the same buffer again as the client, and ensure we can
969 // still do it despite the cancellation above.
970 let buffer = project
971 .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
972 .await
973 .unwrap();
974
975 buffer.read_with(cx, |buf, _| {
976 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
977 });
978}
979
980#[gpui::test]
981async fn test_adding_then_removing_then_adding_worktrees(
982 cx: &mut TestAppContext,
983 server_cx: &mut TestAppContext,
984) {
985 let fs = FakeFs::new(server_cx.executor());
986 fs.insert_tree(
987 path!("/code"),
988 json!({
989 "project1": {
990 ".git": {},
991 "README.md": "# project 1",
992 "src": {
993 "lib.rs": "fn one() -> usize { 1 }"
994 }
995 },
996 "project2": {
997 "README.md": "# project 2",
998 },
999 }),
1000 )
1001 .await;
1002
1003 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1004 let (_worktree, _) = project
1005 .update(cx, |project, cx| {
1006 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1007 })
1008 .await
1009 .unwrap();
1010
1011 let (worktree_2, _) = project
1012 .update(cx, |project, cx| {
1013 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1014 })
1015 .await
1016 .unwrap();
1017 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1018
1019 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1020
1021 let (worktree_2, _) = project
1022 .update(cx, |project, cx| {
1023 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1024 })
1025 .await
1026 .unwrap();
1027
1028 cx.run_until_parked();
1029 worktree_2.update(cx, |worktree, _cx| {
1030 assert!(worktree.is_visible());
1031 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1032 assert_eq!(entries.len(), 2);
1033 assert_eq!(
1034 entries[1].path.to_string_lossy().to_string(),
1035 "README.md".to_string()
1036 )
1037 })
1038}
1039
1040#[gpui::test]
1041async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1042 let fs = FakeFs::new(server_cx.executor());
1043 fs.insert_tree(
1044 path!("/code"),
1045 json!({
1046 "project1": {
1047 ".git": {},
1048 "README.md": "# project 1",
1049 "src": {
1050 "lib.rs": "fn one() -> usize { 1 }"
1051 }
1052 },
1053 }),
1054 )
1055 .await;
1056
1057 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1058 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1059 cx.executor().run_until_parked();
1060
1061 let buffer = buffer.await.unwrap();
1062
1063 cx.update(|cx| {
1064 assert_eq!(
1065 buffer.read(cx).text(),
1066 initial_server_settings_content()
1067 .to_string()
1068 .replace("\r\n", "\n")
1069 )
1070 })
1071}
1072
1073#[gpui::test(iterations = 20)]
1074async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1075 let fs = FakeFs::new(server_cx.executor());
1076 fs.insert_tree(
1077 path!("/code"),
1078 json!({
1079 "project1": {
1080 ".git": {},
1081 "README.md": "# project 1",
1082 "src": {
1083 "lib.rs": "fn one() -> usize { 1 }"
1084 }
1085 },
1086 }),
1087 )
1088 .await;
1089
1090 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1091
1092 let (worktree, _) = project
1093 .update(cx, |project, cx| {
1094 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1095 })
1096 .await
1097 .unwrap();
1098
1099 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1100 let buffer = project
1101 .update(cx, |project, cx| {
1102 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1103 })
1104 .await
1105 .unwrap();
1106
1107 buffer.update(cx, |buffer, cx| {
1108 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1109 let ix = buffer.text().find('1').unwrap();
1110 buffer.edit([(ix..ix + 1, "100")], None, cx);
1111 });
1112
1113 let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
1114 client
1115 .update(cx, |client, cx| client.simulate_disconnect(cx))
1116 .detach();
1117
1118 project
1119 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1120 .await
1121 .unwrap();
1122
1123 assert_eq!(
1124 fs.load(path!("/code/project1/src/lib.rs").as_ref())
1125 .await
1126 .unwrap(),
1127 "fn one() -> usize { 100 }"
1128 );
1129}
1130
1131#[gpui::test]
1132async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1133 let fs = FakeFs::new(server_cx.executor());
1134 fs.insert_tree(
1135 "/code",
1136 json!({
1137 "project1": {
1138 ".git": {},
1139 "README.md": "# project 1",
1140 },
1141 }),
1142 )
1143 .await;
1144
1145 let (project, _) = init_test(&fs, cx, server_cx).await;
1146
1147 let (worktree, _) = project
1148 .update(cx, |project, cx| {
1149 project.find_or_create_worktree("/code/project1", true, cx)
1150 })
1151 .await
1152 .unwrap();
1153
1154 cx.run_until_parked();
1155
1156 fs.rename(
1157 &PathBuf::from("/code/project1"),
1158 &PathBuf::from("/code/project2"),
1159 Default::default(),
1160 )
1161 .await
1162 .unwrap();
1163
1164 cx.run_until_parked();
1165 worktree.update(cx, |worktree, _| {
1166 assert_eq!(worktree.root_name(), "project2")
1167 })
1168}
1169
1170#[gpui::test]
1171async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1172 let fs = FakeFs::new(server_cx.executor());
1173 fs.insert_tree(
1174 "/code",
1175 json!({
1176 "project1": {
1177 ".git": {},
1178 "README.md": "# project 1",
1179 },
1180 }),
1181 )
1182 .await;
1183
1184 let (project, _) = init_test(&fs, cx, server_cx).await;
1185 let (worktree, _) = project
1186 .update(cx, |project, cx| {
1187 project.find_or_create_worktree("/code/project1", true, cx)
1188 })
1189 .await
1190 .unwrap();
1191
1192 cx.run_until_parked();
1193
1194 let entry = worktree
1195 .update(cx, |worktree, cx| {
1196 let entry = worktree.entry_for_path("README.md").unwrap();
1197 worktree.rename_entry(entry.id, Path::new("README.rst"), cx)
1198 })
1199 .await
1200 .unwrap()
1201 .to_included()
1202 .unwrap();
1203
1204 cx.run_until_parked();
1205
1206 worktree.update(cx, |worktree, _| {
1207 assert_eq!(worktree.entry_for_path("README.rst").unwrap().id, entry.id)
1208 });
1209}
1210
1211#[gpui::test]
1212async fn test_copy_file_into_remote_project(
1213 cx: &mut TestAppContext,
1214 server_cx: &mut TestAppContext,
1215) {
1216 let remote_fs = FakeFs::new(server_cx.executor());
1217 remote_fs
1218 .insert_tree(
1219 path!("/code"),
1220 json!({
1221 "project1": {
1222 ".git": {},
1223 "README.md": "# project 1",
1224 "src": {
1225 "main.rs": ""
1226 }
1227 },
1228 }),
1229 )
1230 .await;
1231
1232 let (project, _) = init_test(&remote_fs, cx, server_cx).await;
1233 let (worktree, _) = project
1234 .update(cx, |project, cx| {
1235 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1236 })
1237 .await
1238 .unwrap();
1239
1240 cx.run_until_parked();
1241
1242 let local_fs = project
1243 .read_with(cx, |project, _| project.fs().clone())
1244 .as_fake();
1245 local_fs
1246 .insert_tree(
1247 path!("/local-code"),
1248 json!({
1249 "dir1": {
1250 "file1": "file 1 content",
1251 "dir2": {
1252 "file2": "file 2 content",
1253 "dir3": {
1254 "file3": ""
1255 },
1256 "dir4": {}
1257 },
1258 "dir5": {}
1259 },
1260 "file4": "file 4 content"
1261 }),
1262 )
1263 .await;
1264
1265 worktree
1266 .update(cx, |worktree, cx| {
1267 worktree.copy_external_entries(
1268 Path::new("src").into(),
1269 vec![
1270 Path::new(path!("/local-code/dir1/file1")).into(),
1271 Path::new(path!("/local-code/dir1/dir2")).into(),
1272 ],
1273 local_fs.clone(),
1274 cx,
1275 )
1276 })
1277 .await
1278 .unwrap();
1279
1280 assert_eq!(
1281 remote_fs.paths(true),
1282 vec![
1283 PathBuf::from(path!("/")),
1284 PathBuf::from(path!("/code")),
1285 PathBuf::from(path!("/code/project1")),
1286 PathBuf::from(path!("/code/project1/.git")),
1287 PathBuf::from(path!("/code/project1/README.md")),
1288 PathBuf::from(path!("/code/project1/src")),
1289 PathBuf::from(path!("/code/project1/src/dir2")),
1290 PathBuf::from(path!("/code/project1/src/file1")),
1291 PathBuf::from(path!("/code/project1/src/main.rs")),
1292 PathBuf::from(path!("/code/project1/src/dir2/dir3")),
1293 PathBuf::from(path!("/code/project1/src/dir2/dir4")),
1294 PathBuf::from(path!("/code/project1/src/dir2/file2")),
1295 PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
1296 ]
1297 );
1298 assert_eq!(
1299 remote_fs
1300 .load(path!("/code/project1/src/file1").as_ref())
1301 .await
1302 .unwrap(),
1303 "file 1 content"
1304 );
1305 assert_eq!(
1306 remote_fs
1307 .load(path!("/code/project1/src/dir2/file2").as_ref())
1308 .await
1309 .unwrap(),
1310 "file 2 content"
1311 );
1312 assert_eq!(
1313 remote_fs
1314 .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
1315 .await
1316 .unwrap(),
1317 ""
1318 );
1319}
1320
1321// TODO: this test fails on Windows.
1322#[cfg(not(windows))]
1323#[gpui::test]
1324async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1325 let text_2 = "
1326 fn one() -> usize {
1327 1
1328 }
1329 "
1330 .unindent();
1331 let text_1 = "
1332 fn one() -> usize {
1333 0
1334 }
1335 "
1336 .unindent();
1337
1338 let fs = FakeFs::new(server_cx.executor());
1339 fs.insert_tree(
1340 "/code",
1341 json!({
1342 "project1": {
1343 ".git": {},
1344 "src": {
1345 "lib.rs": text_2
1346 },
1347 "README.md": "# project 1",
1348 },
1349 }),
1350 )
1351 .await;
1352 fs.set_index_for_repo(
1353 Path::new("/code/project1/.git"),
1354 &[("src/lib.rs".into(), text_1.clone())],
1355 );
1356 fs.set_head_for_repo(
1357 Path::new("/code/project1/.git"),
1358 &[("src/lib.rs".into(), text_1.clone())],
1359 );
1360
1361 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1362 let (worktree, _) = project
1363 .update(cx, |project, cx| {
1364 project.find_or_create_worktree("/code/project1", true, cx)
1365 })
1366 .await
1367 .unwrap();
1368 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1369 cx.executor().run_until_parked();
1370
1371 let buffer = project
1372 .update(cx, |project, cx| {
1373 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1374 })
1375 .await
1376 .unwrap();
1377 let diff = project
1378 .update(cx, |project, cx| {
1379 project.open_uncommitted_diff(buffer.clone(), cx)
1380 })
1381 .await
1382 .unwrap();
1383
1384 diff.read_with(cx, |diff, cx| {
1385 assert_eq!(diff.base_text_string().unwrap(), text_1);
1386 assert_eq!(
1387 diff.secondary_diff()
1388 .unwrap()
1389 .read(cx)
1390 .base_text_string()
1391 .unwrap(),
1392 text_1
1393 );
1394 });
1395
1396 // stage the current buffer's contents
1397 fs.set_index_for_repo(
1398 Path::new("/code/project1/.git"),
1399 &[("src/lib.rs".into(), text_2.clone())],
1400 );
1401
1402 cx.executor().run_until_parked();
1403 diff.read_with(cx, |diff, cx| {
1404 assert_eq!(diff.base_text_string().unwrap(), text_1);
1405 assert_eq!(
1406 diff.secondary_diff()
1407 .unwrap()
1408 .read(cx)
1409 .base_text_string()
1410 .unwrap(),
1411 text_2
1412 );
1413 });
1414
1415 // commit the current buffer's contents
1416 fs.set_head_for_repo(
1417 Path::new("/code/project1/.git"),
1418 &[("src/lib.rs".into(), text_2.clone())],
1419 );
1420
1421 cx.executor().run_until_parked();
1422 diff.read_with(cx, |diff, cx| {
1423 assert_eq!(diff.base_text_string().unwrap(), text_2);
1424 assert_eq!(
1425 diff.secondary_diff()
1426 .unwrap()
1427 .read(cx)
1428 .base_text_string()
1429 .unwrap(),
1430 text_2
1431 );
1432 });
1433}
1434
1435#[gpui::test]
1436async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1437 let fs = FakeFs::new(server_cx.executor());
1438 fs.insert_tree(
1439 path!("/code"),
1440 json!({
1441 "project1": {
1442 ".git": {},
1443 "README.md": "# project 1",
1444 },
1445 }),
1446 )
1447 .await;
1448
1449 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1450 let branches = ["main", "dev", "feature-1"];
1451 let branches_set = branches
1452 .iter()
1453 .map(ToString::to_string)
1454 .collect::<HashSet<_>>();
1455 fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1456
1457 let (_worktree, _) = project
1458 .update(cx, |project, cx| {
1459 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1460 })
1461 .await
1462 .unwrap();
1463 // Give the worktree a bit of time to index the file system
1464 cx.run_until_parked();
1465
1466 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1467
1468 let remote_branches = repository
1469 .update(cx, |repository, _| repository.branches())
1470 .await
1471 .unwrap()
1472 .unwrap();
1473
1474 let new_branch = branches[2];
1475
1476 let remote_branches = remote_branches
1477 .into_iter()
1478 .map(|branch| branch.name().to_string())
1479 .collect::<HashSet<_>>();
1480
1481 assert_eq!(&remote_branches, &branches_set);
1482
1483 cx.update(|cx| {
1484 repository.update(cx, |repository, _cx| {
1485 repository.change_branch(new_branch.to_string())
1486 })
1487 })
1488 .await
1489 .unwrap()
1490 .unwrap();
1491
1492 cx.run_until_parked();
1493
1494 let server_branch = server_cx.update(|cx| {
1495 headless_project.update(cx, |headless_project, cx| {
1496 headless_project.git_store.update(cx, |git_store, cx| {
1497 git_store
1498 .repositories()
1499 .values()
1500 .next()
1501 .unwrap()
1502 .read(cx)
1503 .branch
1504 .as_ref()
1505 .unwrap()
1506 .clone()
1507 })
1508 })
1509 });
1510
1511 assert_eq!(server_branch.name(), branches[2]);
1512
1513 // Also try creating a new branch
1514 cx.update(|cx| {
1515 repository.update(cx, |repo, _cx| {
1516 repo.create_branch("totally-new-branch".to_string())
1517 })
1518 })
1519 .await
1520 .unwrap()
1521 .unwrap();
1522
1523 cx.update(|cx| {
1524 repository.update(cx, |repo, _cx| {
1525 repo.change_branch("totally-new-branch".to_string())
1526 })
1527 })
1528 .await
1529 .unwrap()
1530 .unwrap();
1531
1532 cx.run_until_parked();
1533
1534 let server_branch = server_cx.update(|cx| {
1535 headless_project.update(cx, |headless_project, cx| {
1536 headless_project.git_store.update(cx, |git_store, cx| {
1537 git_store
1538 .repositories()
1539 .values()
1540 .next()
1541 .unwrap()
1542 .read(cx)
1543 .branch
1544 .as_ref()
1545 .unwrap()
1546 .clone()
1547 })
1548 })
1549 });
1550
1551 assert_eq!(server_branch.name(), "totally-new-branch");
1552}
1553
1554#[gpui::test]
1555async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1556 let fs = FakeFs::new(server_cx.executor());
1557 fs.insert_tree(
1558 path!("/project"),
1559 json!({
1560 "a.txt": "A",
1561 "b.txt": "B",
1562 }),
1563 )
1564 .await;
1565
1566 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
1567 project
1568 .update(cx, |project, cx| {
1569 project.find_or_create_worktree(path!("/project"), true, cx)
1570 })
1571 .await
1572 .unwrap();
1573
1574 let action_log = cx.new(|_| assistant_tool::ActionLog::new(project.clone()));
1575 let model = Arc::new(FakeLanguageModel::default());
1576 let request = Arc::new(LanguageModelRequest::default());
1577
1578 let input = ReadFileToolInput {
1579 path: "project/b.txt".into(),
1580 start_line: None,
1581 end_line: None,
1582 };
1583 let exists_result = cx.update(|cx| {
1584 ReadFileTool::run(
1585 Arc::new(ReadFileTool),
1586 serde_json::to_value(input).unwrap(),
1587 request.clone(),
1588 project.clone(),
1589 action_log.clone(),
1590 model.clone(),
1591 None,
1592 cx,
1593 )
1594 });
1595 let output = exists_result.output.await.unwrap().content;
1596 assert_eq!(output, ToolResultContent::Text("B".to_string()));
1597
1598 let input = ReadFileToolInput {
1599 path: "project/c.txt".into(),
1600 start_line: None,
1601 end_line: None,
1602 };
1603 let does_not_exist_result = cx.update(|cx| {
1604 ReadFileTool::run(
1605 Arc::new(ReadFileTool),
1606 serde_json::to_value(input).unwrap(),
1607 request.clone(),
1608 project.clone(),
1609 action_log.clone(),
1610 model.clone(),
1611 None,
1612 cx,
1613 )
1614 });
1615 does_not_exist_result.output.await.unwrap_err();
1616}
1617
1618pub async fn init_test(
1619 server_fs: &Arc<FakeFs>,
1620 cx: &mut TestAppContext,
1621 server_cx: &mut TestAppContext,
1622) -> (Entity<Project>, Entity<HeadlessProject>) {
1623 let server_fs = server_fs.clone();
1624 cx.update(|cx| {
1625 release_channel::init(SemanticVersion::default(), cx);
1626 });
1627 server_cx.update(|cx| {
1628 release_channel::init(SemanticVersion::default(), cx);
1629 });
1630 init_logger();
1631
1632 let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
1633 let http_client = Arc::new(BlockedHttpClient);
1634 let node_runtime = NodeRuntime::unavailable();
1635 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1636 let proxy = Arc::new(ExtensionHostProxy::new());
1637 server_cx.update(HeadlessProject::init);
1638 let headless = server_cx.new(|cx| {
1639 client::init_settings(cx);
1640
1641 HeadlessProject::new(
1642 crate::HeadlessAppState {
1643 session: ssh_server_client,
1644 fs: server_fs.clone(),
1645 http_client,
1646 node_runtime,
1647 languages,
1648 extension_host_proxy: proxy,
1649 },
1650 cx,
1651 )
1652 });
1653
1654 let ssh = SshRemoteClient::fake_client(opts, cx).await;
1655 let project = build_project(ssh, cx);
1656 project
1657 .update(cx, {
1658 let headless = headless.clone();
1659 |_, cx| cx.on_release(|_, _| drop(headless))
1660 })
1661 .detach();
1662 (project, headless)
1663}
1664
1665fn init_logger() {
1666 zlog::init_test();
1667}
1668
1669fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1670 cx.update(|cx| {
1671 if !cx.has_global::<SettingsStore>() {
1672 let settings_store = SettingsStore::test(cx);
1673 cx.set_global(settings_store);
1674 }
1675 });
1676
1677 let client = cx.update(|cx| {
1678 Client::new(
1679 Arc::new(FakeSystemClock::new()),
1680 FakeHttpClient::with_404_response(),
1681 cx,
1682 )
1683 });
1684
1685 let node = NodeRuntime::unavailable();
1686 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1687 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1688 let fs = FakeFs::new(cx.executor());
1689
1690 cx.update(|cx| {
1691 Project::init(&client, cx);
1692 language::init(cx);
1693 });
1694
1695 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1696}