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 agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream, ToolInput};
6use client::{Client, UserStore};
7use clock::FakeSystemClock;
8use collections::{HashMap, HashSet};
9use language_model::LanguageModelToolResultContent;
10use languages::rust_lang;
11
12use extension::ExtensionHostProxy;
13use fs::{FakeFs, Fs};
14use git::repository::Worktree as GitWorktree;
15use gpui::{AppContext as _, Entity, SharedString, TestAppContext};
16use http_client::{BlockedHttpClient, FakeHttpClient};
17use language::{
18 Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
19 language_settings::{AllLanguageSettings, LanguageSettings},
20};
21use lsp::{
22 CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT,
23 LanguageServerName,
24};
25use node_runtime::NodeRuntime;
26use project::{
27 ProgressToken, Project,
28 agent_server_store::AgentServerCommand,
29 search::{SearchQuery, SearchResult},
30};
31use remote::RemoteClient;
32use serde_json::json;
33use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
34use smol::stream::StreamExt;
35use std::{
36 path::{Path, PathBuf},
37 sync::Arc,
38};
39use unindent::Unindent as _;
40use util::{path, paths::PathMatcher, rel_path::rel_path};
41
42#[gpui::test]
43async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
44 let fs = FakeFs::new(server_cx.executor());
45 fs.insert_tree(
46 path!("/code"),
47 json!({
48 "project1": {
49 ".git": {},
50 "README.md": "# project 1",
51 "src": {
52 "lib.rs": "fn one() -> usize { 1 }"
53 }
54 },
55 "project2": {
56 "README.md": "# project 2",
57 },
58 }),
59 )
60 .await;
61 fs.set_index_for_repo(
62 Path::new(path!("/code/project1/.git")),
63 &[("src/lib.rs", "fn one() -> usize { 0 }".into())],
64 );
65
66 let (project, _headless) = init_test(&fs, cx, server_cx).await;
67 let (worktree, _) = project
68 .update(cx, |project, cx| {
69 project.find_or_create_worktree(path!("/code/project1"), true, cx)
70 })
71 .await
72 .unwrap();
73
74 // The client sees the worktree's contents.
75 cx.executor().run_until_parked();
76 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
77 worktree.update(cx, |worktree, _cx| {
78 assert_eq!(
79 worktree.paths().collect::<Vec<_>>(),
80 vec![
81 rel_path("README.md"),
82 rel_path("src"),
83 rel_path("src/lib.rs"),
84 ]
85 );
86 });
87
88 // The user opens a buffer in the remote worktree. The buffer's
89 // contents are loaded from the remote filesystem.
90 let buffer = project
91 .update(cx, |project, cx| {
92 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
93 })
94 .await
95 .unwrap();
96 let diff = project
97 .update(cx, |project, cx| {
98 project.open_unstaged_diff(buffer.clone(), cx)
99 })
100 .await
101 .unwrap();
102
103 diff.update(cx, |diff, cx| {
104 assert_eq!(
105 diff.base_text_string(cx).unwrap(),
106 "fn one() -> usize { 0 }"
107 );
108 });
109
110 buffer.update(cx, |buffer, cx| {
111 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
112 let ix = buffer.text().find('1').unwrap();
113 buffer.edit([(ix..ix + 1, "100")], None, cx);
114 });
115
116 // The user saves the buffer. The new contents are written to the
117 // remote filesystem.
118 project
119 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
120 .await
121 .unwrap();
122 assert_eq!(
123 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
124 "fn one() -> usize { 100 }"
125 );
126
127 // A new file is created in the remote filesystem. The user
128 // sees the new file.
129 fs.save(
130 path!("/code/project1/src/main.rs").as_ref(),
131 &"fn main() {}".into(),
132 Default::default(),
133 )
134 .await
135 .unwrap();
136 cx.executor().run_until_parked();
137 worktree.update(cx, |worktree, _cx| {
138 assert_eq!(
139 worktree.paths().collect::<Vec<_>>(),
140 vec![
141 rel_path("README.md"),
142 rel_path("src"),
143 rel_path("src/lib.rs"),
144 rel_path("src/main.rs"),
145 ]
146 );
147 });
148
149 // A file that is currently open in a buffer is renamed.
150 fs.rename(
151 path!("/code/project1/src/lib.rs").as_ref(),
152 path!("/code/project1/src/lib2.rs").as_ref(),
153 Default::default(),
154 )
155 .await
156 .unwrap();
157 cx.executor().run_until_parked();
158 buffer.update(cx, |buffer, _| {
159 assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs"));
160 });
161
162 fs.set_index_for_repo(
163 Path::new(path!("/code/project1/.git")),
164 &[("src/lib2.rs", "fn one() -> usize { 100 }".into())],
165 );
166 cx.executor().run_until_parked();
167 diff.update(cx, |diff, cx| {
168 assert_eq!(
169 diff.base_text_string(cx).unwrap(),
170 "fn one() -> usize { 100 }"
171 );
172 });
173}
174
175async fn do_search_and_assert(
176 project: &Entity<Project>,
177 query: &str,
178 files_to_include: PathMatcher,
179 match_full_paths: bool,
180 expected_paths: &[&str],
181 mut cx: TestAppContext,
182) -> Vec<Entity<Buffer>> {
183 let query = query.to_string();
184 let receiver = project.update(&mut cx, |project, cx| {
185 project.search(
186 SearchQuery::text(
187 query,
188 false,
189 true,
190 false,
191 files_to_include,
192 Default::default(),
193 match_full_paths,
194 None,
195 )
196 .unwrap(),
197 cx,
198 )
199 });
200
201 let mut buffers = Vec::new();
202 for expected_path in expected_paths {
203 let response = receiver.rx.recv().await.unwrap();
204 let SearchResult::Buffer { buffer, .. } = response else {
205 panic!("incorrect result");
206 };
207 buffer.update(&mut cx, |buffer, cx| {
208 assert_eq!(
209 buffer.file().unwrap().full_path(cx).to_string_lossy(),
210 *expected_path
211 )
212 });
213 buffers.push(buffer);
214 }
215
216 assert!(receiver.rx.recv().await.is_err());
217 buffers
218}
219
220#[gpui::test]
221async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
222 let fs = FakeFs::new(server_cx.executor());
223 fs.insert_tree(
224 path!("/code"),
225 json!({
226 "project1": {
227 ".git": {},
228 "README.md": "# project 1",
229 "src": {
230 "lib.rs": "fn one() -> usize { 1 }"
231 }
232 },
233 }),
234 )
235 .await;
236
237 let (project, headless) = init_test(&fs, cx, server_cx).await;
238
239 project
240 .update(cx, |project, cx| {
241 project.find_or_create_worktree(path!("/code/project1"), true, cx)
242 })
243 .await
244 .unwrap();
245
246 cx.run_until_parked();
247
248 let buffers = do_search_and_assert(
249 &project,
250 "project",
251 Default::default(),
252 false,
253 &[path!("project1/README.md")],
254 cx.clone(),
255 )
256 .await;
257 let buffer = buffers.into_iter().next().unwrap();
258
259 // test that the headless server is tracking which buffers we have open correctly.
260 cx.run_until_parked();
261 headless.update(server_cx, |headless, cx| {
262 assert!(headless.buffer_store.read(cx).has_shared_buffers())
263 });
264 do_search_and_assert(
265 &project,
266 "project",
267 Default::default(),
268 false,
269 &[path!("project1/README.md")],
270 cx.clone(),
271 )
272 .await;
273 server_cx.run_until_parked();
274 cx.update(|_| {
275 drop(buffer);
276 });
277 cx.run_until_parked();
278 server_cx.run_until_parked();
279 headless.update(server_cx, |headless, cx| {
280 assert!(!headless.buffer_store.read(cx).has_shared_buffers())
281 });
282
283 do_search_and_assert(
284 &project,
285 "project",
286 Default::default(),
287 false,
288 &[path!("project1/README.md")],
289 cx.clone(),
290 )
291 .await;
292}
293
294#[gpui::test]
295async fn test_remote_project_search_single_cpu(
296 cx: &mut TestAppContext,
297 server_cx: &mut TestAppContext,
298) {
299 let fs = FakeFs::new(server_cx.executor());
300 fs.insert_tree(
301 path!("/code"),
302 json!({
303 "project1": {
304 ".git": {},
305 "README.md": "# project 1",
306 "src": {
307 "lib.rs": "fn one() -> usize { 1 }"
308 }
309 },
310 }),
311 )
312 .await;
313
314 // Simulate a single-CPU environment (e.g. a devcontainer with 1 visible CPU).
315 // This causes the worker pool in project search to spawn num_cpus - 1 = 0 workers,
316 // which silently drops all search channels and produces zero results.
317 server_cx.executor().set_num_cpus(1);
318
319 let (project, _) = init_test(&fs, cx, server_cx).await;
320
321 project
322 .update(cx, |project, cx| {
323 project.find_or_create_worktree(path!("/code/project1"), true, cx)
324 })
325 .await
326 .unwrap();
327
328 cx.run_until_parked();
329
330 do_search_and_assert(
331 &project,
332 "project",
333 Default::default(),
334 false,
335 &[path!("project1/README.md")],
336 cx.clone(),
337 )
338 .await;
339}
340
341#[gpui::test]
342async fn test_remote_project_search_inclusion(
343 cx: &mut TestAppContext,
344 server_cx: &mut TestAppContext,
345) {
346 let fs = FakeFs::new(server_cx.executor());
347 fs.insert_tree(
348 path!("/code"),
349 json!({
350 "project1": {
351 "README.md": "# project 1",
352 },
353 "project2": {
354 "README.md": "# project 2",
355 },
356 }),
357 )
358 .await;
359
360 let (project, _) = init_test(&fs, cx, server_cx).await;
361
362 project
363 .update(cx, |project, cx| {
364 project.find_or_create_worktree(path!("/code/project1"), true, cx)
365 })
366 .await
367 .unwrap();
368
369 project
370 .update(cx, |project, cx| {
371 project.find_or_create_worktree(path!("/code/project2"), true, cx)
372 })
373 .await
374 .unwrap();
375
376 cx.run_until_parked();
377
378 // Case 1: Test search with path matcher limiting to only one worktree
379 let path_matcher = PathMatcher::new(
380 &["project1/*.md".to_owned()],
381 util::paths::PathStyle::local(),
382 )
383 .unwrap();
384 do_search_and_assert(
385 &project,
386 "project",
387 path_matcher,
388 true, // should be true in case of multiple worktrees
389 &[path!("project1/README.md")],
390 cx.clone(),
391 )
392 .await;
393
394 // Case 2: Test search without path matcher, matching both worktrees
395 do_search_and_assert(
396 &project,
397 "project",
398 Default::default(),
399 true, // should be true in case of multiple worktrees
400 &[path!("project1/README.md"), path!("project2/README.md")],
401 cx.clone(),
402 )
403 .await;
404}
405
406#[gpui::test]
407async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
408 let fs = FakeFs::new(server_cx.executor());
409 fs.insert_tree(
410 "/code",
411 json!({
412 "project1": {
413 ".git": {},
414 "README.md": "# project 1",
415 "src": {
416 "lib.rs": "fn one() -> usize { 1 }"
417 }
418 },
419 }),
420 )
421 .await;
422
423 let (project, headless) = init_test(&fs, cx, server_cx).await;
424
425 cx.update_global(|settings_store: &mut SettingsStore, cx| {
426 settings_store.set_user_settings(
427 r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
428 cx,
429 )
430 })
431 .unwrap();
432
433 cx.run_until_parked();
434
435 server_cx.read(|cx| {
436 assert_eq!(
437 AllLanguageSettings::get_global(cx)
438 .language(None, Some(&"Rust".into()), cx)
439 .language_servers,
440 ["from-local-settings"],
441 "User language settings should be synchronized with the server settings"
442 )
443 });
444
445 server_cx
446 .update_global(|settings_store: &mut SettingsStore, cx| {
447 settings_store.set_server_settings(
448 r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
449 cx,
450 )
451 })
452 .unwrap();
453
454 cx.run_until_parked();
455
456 server_cx.read(|cx| {
457 assert_eq!(
458 AllLanguageSettings::get_global(cx)
459 .language(None, Some(&"Rust".into()), cx)
460 .language_servers,
461 ["from-server-settings".to_string()],
462 "Server language settings should take precedence over the user settings"
463 )
464 });
465
466 fs.insert_tree(
467 "/code/project1/.zed",
468 json!({
469 "settings.json": r#"
470 {
471 "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
472 "lsp": {
473 "override-rust-analyzer": {
474 "binary": {
475 "path": "~/.cargo/bin/rust-analyzer"
476 }
477 }
478 }
479 }"#
480 }),
481 )
482 .await;
483
484 let worktree_id = project
485 .update(cx, |project, cx| {
486 project.languages().add(rust_lang());
487 project.find_or_create_worktree("/code/project1", true, cx)
488 })
489 .await
490 .unwrap()
491 .0
492 .read_with(cx, |worktree, _| worktree.id());
493
494 let buffer = project
495 .update(cx, |project, cx| {
496 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
497 })
498 .await
499 .unwrap();
500 cx.run_until_parked();
501
502 server_cx.read(|cx| {
503 let worktree_id = headless
504 .read(cx)
505 .worktree_store
506 .read(cx)
507 .worktrees()
508 .next()
509 .unwrap()
510 .read(cx)
511 .id();
512 assert_eq!(
513 AllLanguageSettings::get(
514 Some(SettingsLocation {
515 worktree_id,
516 path: rel_path("src/lib.rs")
517 }),
518 cx
519 )
520 .language(None, Some(&"Rust".into()), cx)
521 .language_servers,
522 ["override-rust-analyzer".to_string()]
523 )
524 });
525
526 cx.read(|cx| {
527 assert_eq!(
528 LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
529 ["override-rust-analyzer".to_string()]
530 )
531 });
532}
533
534#[gpui::test]
535async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
536 let fs = FakeFs::new(server_cx.executor());
537 fs.insert_tree(
538 path!("/code"),
539 json!({
540 "project1": {
541 ".git": {},
542 "README.md": "# project 1",
543 "src": {
544 "lib.rs": "fn one() -> usize { 1 }"
545 }
546 },
547 }),
548 )
549 .await;
550
551 let (project, headless) = init_test(&fs, cx, server_cx).await;
552
553 fs.insert_tree(
554 path!("/code/project1/.zed"),
555 json!({
556 "settings.json": r#"
557 {
558 "languages": {"Rust":{"language_servers":["rust-analyzer", "fake-analyzer"]}},
559 "lsp": {
560 "rust-analyzer": {
561 "binary": {
562 "path": "~/.cargo/bin/rust-analyzer"
563 }
564 },
565 "fake-analyzer": {
566 "binary": {
567 "path": "~/.cargo/bin/rust-analyzer"
568 }
569 }
570 }
571 }"#
572 }),
573 )
574 .await;
575
576 cx.update_entity(&project, |project, _| {
577 project.languages().register_test_language(LanguageConfig {
578 name: "Rust".into(),
579 matcher: LanguageMatcher {
580 path_suffixes: vec!["rs".into()],
581 ..Default::default()
582 },
583 ..Default::default()
584 });
585 project.languages().register_fake_lsp_adapter(
586 "Rust",
587 FakeLspAdapter {
588 name: "rust-analyzer",
589 capabilities: lsp::ServerCapabilities {
590 completion_provider: Some(lsp::CompletionOptions::default()),
591 rename_provider: Some(lsp::OneOf::Left(true)),
592 ..lsp::ServerCapabilities::default()
593 },
594 ..FakeLspAdapter::default()
595 },
596 );
597 project.languages().register_fake_lsp_adapter(
598 "Rust",
599 FakeLspAdapter {
600 name: "fake-analyzer",
601 capabilities: lsp::ServerCapabilities {
602 completion_provider: Some(lsp::CompletionOptions::default()),
603 rename_provider: Some(lsp::OneOf::Left(true)),
604 ..lsp::ServerCapabilities::default()
605 },
606 ..FakeLspAdapter::default()
607 },
608 )
609 });
610
611 let mut fake_lsp = server_cx.update(|cx| {
612 headless.read(cx).languages.register_fake_lsp_server(
613 LanguageServerName("rust-analyzer".into()),
614 lsp::ServerCapabilities {
615 completion_provider: Some(lsp::CompletionOptions::default()),
616 rename_provider: Some(lsp::OneOf::Left(true)),
617 ..lsp::ServerCapabilities::default()
618 },
619 None,
620 )
621 });
622
623 let mut fake_second_lsp = server_cx.update(|cx| {
624 headless.read(cx).languages.register_fake_lsp_adapter(
625 "Rust",
626 FakeLspAdapter {
627 name: "fake-analyzer",
628 capabilities: lsp::ServerCapabilities {
629 completion_provider: Some(lsp::CompletionOptions::default()),
630 rename_provider: Some(lsp::OneOf::Left(true)),
631 ..lsp::ServerCapabilities::default()
632 },
633 ..FakeLspAdapter::default()
634 },
635 );
636 headless.read(cx).languages.register_fake_lsp_server(
637 LanguageServerName("fake-analyzer".into()),
638 lsp::ServerCapabilities {
639 completion_provider: Some(lsp::CompletionOptions::default()),
640 rename_provider: Some(lsp::OneOf::Left(true)),
641 ..lsp::ServerCapabilities::default()
642 },
643 None,
644 )
645 });
646
647 cx.run_until_parked();
648
649 let worktree_id = project
650 .update(cx, |project, cx| {
651 project.languages().add(rust_lang());
652 project.find_or_create_worktree(path!("/code/project1"), true, cx)
653 })
654 .await
655 .unwrap()
656 .0
657 .read_with(cx, |worktree, _| worktree.id());
658
659 // Wait for the settings to synchronize
660 cx.run_until_parked();
661
662 let (buffer, _handle) = project
663 .update(cx, |project, cx| {
664 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
665 })
666 .await
667 .unwrap();
668 cx.run_until_parked();
669
670 let fake_lsp = fake_lsp.next().await.unwrap();
671 let fake_second_lsp = fake_second_lsp.next().await.unwrap();
672
673 cx.read(|cx| {
674 assert_eq!(
675 LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
676 ["rust-analyzer".to_string(), "fake-analyzer".to_string()]
677 )
678 });
679
680 let buffer_id = cx.read(|cx| {
681 let buffer = buffer.read(cx);
682 assert_eq!(buffer.language().unwrap().name(), "Rust");
683 buffer.remote_id()
684 });
685
686 server_cx.read(|cx| {
687 let buffer = headless
688 .read(cx)
689 .buffer_store
690 .read(cx)
691 .get(buffer_id)
692 .unwrap();
693
694 assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust");
695 });
696
697 server_cx.read(|cx| {
698 let lsp_store = headless.read(cx).lsp_store.read(cx);
699 assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 2);
700 });
701
702 fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
703 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
704 label: "boop".to_string(),
705 ..Default::default()
706 }])))
707 });
708
709 fake_second_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
710 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
711 label: "beep".to_string(),
712 ..Default::default()
713 }])))
714 });
715
716 let result = project
717 .update(cx, |project, cx| {
718 project.completions(
719 &buffer,
720 0,
721 CompletionContext {
722 trigger_kind: CompletionTriggerKind::INVOKED,
723 trigger_character: None,
724 },
725 cx,
726 )
727 })
728 .await
729 .unwrap();
730
731 assert_eq!(
732 result
733 .into_iter()
734 .flat_map(|response| response.completions)
735 .map(|c| c.label.text)
736 .collect::<Vec<_>>(),
737 vec!["boop".to_string(), "beep".to_string()]
738 );
739
740 fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
741 Ok(Some(lsp::WorkspaceEdit {
742 changes: Some(
743 [(
744 lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
745 vec![lsp::TextEdit::new(
746 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
747 "two".to_string(),
748 )],
749 )]
750 .into_iter()
751 .collect(),
752 ),
753 ..Default::default()
754 }))
755 });
756
757 project
758 .update(cx, |project, cx| {
759 project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
760 })
761 .await
762 .unwrap();
763
764 cx.run_until_parked();
765 buffer.update(cx, |buffer, _| {
766 assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
767 })
768}
769
770#[gpui::test]
771async fn test_remote_cancel_language_server_work(
772 cx: &mut TestAppContext,
773 server_cx: &mut TestAppContext,
774) {
775 let fs = FakeFs::new(server_cx.executor());
776 fs.insert_tree(
777 path!("/code"),
778 json!({
779 "project1": {
780 ".git": {},
781 "README.md": "# project 1",
782 "src": {
783 "lib.rs": "fn one() -> usize { 1 }"
784 }
785 },
786 }),
787 )
788 .await;
789
790 let (project, headless) = init_test(&fs, cx, server_cx).await;
791
792 fs.insert_tree(
793 path!("/code/project1/.zed"),
794 json!({
795 "settings.json": r#"
796 {
797 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
798 "lsp": {
799 "rust-analyzer": {
800 "binary": {
801 "path": "~/.cargo/bin/rust-analyzer"
802 }
803 }
804 }
805 }"#
806 }),
807 )
808 .await;
809
810 cx.update_entity(&project, |project, _| {
811 project.languages().register_test_language(LanguageConfig {
812 name: "Rust".into(),
813 matcher: LanguageMatcher {
814 path_suffixes: vec!["rs".into()],
815 ..Default::default()
816 },
817 ..Default::default()
818 });
819 project.languages().register_fake_lsp_adapter(
820 "Rust",
821 FakeLspAdapter {
822 name: "rust-analyzer",
823 ..Default::default()
824 },
825 )
826 });
827
828 let mut fake_lsp = server_cx.update(|cx| {
829 headless.read(cx).languages.register_fake_lsp_server(
830 LanguageServerName("rust-analyzer".into()),
831 Default::default(),
832 None,
833 )
834 });
835
836 cx.run_until_parked();
837
838 let worktree_id = project
839 .update(cx, |project, cx| {
840 project.find_or_create_worktree(path!("/code/project1"), true, cx)
841 })
842 .await
843 .unwrap()
844 .0
845 .read_with(cx, |worktree, _| worktree.id());
846
847 cx.run_until_parked();
848
849 let (buffer, _handle) = project
850 .update(cx, |project, cx| {
851 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
852 })
853 .await
854 .unwrap();
855
856 cx.run_until_parked();
857
858 let mut fake_lsp = fake_lsp.next().await.unwrap();
859
860 // Cancelling all language server work for a given buffer
861 {
862 // Two operations, one cancellable and one not.
863 fake_lsp
864 .start_progress_with(
865 "another-token",
866 lsp::WorkDoneProgressBegin {
867 cancellable: Some(false),
868 ..Default::default()
869 },
870 DEFAULT_LSP_REQUEST_TIMEOUT,
871 )
872 .await;
873
874 let progress_token = "the-progress-token";
875 fake_lsp
876 .start_progress_with(
877 progress_token,
878 lsp::WorkDoneProgressBegin {
879 cancellable: Some(true),
880 ..Default::default()
881 },
882 DEFAULT_LSP_REQUEST_TIMEOUT,
883 )
884 .await;
885
886 cx.executor().run_until_parked();
887
888 project.update(cx, |project, cx| {
889 project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
890 });
891
892 cx.executor().run_until_parked();
893
894 // Verify the cancellation was received on the server side
895 let cancel_notification = fake_lsp
896 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
897 .await;
898 assert_eq!(
899 cancel_notification.token,
900 lsp::NumberOrString::String(progress_token.into())
901 );
902 }
903
904 // Cancelling work by server_id and token
905 {
906 let server_id = fake_lsp.server.server_id();
907 let progress_token = "the-progress-token";
908
909 fake_lsp
910 .start_progress_with(
911 progress_token,
912 lsp::WorkDoneProgressBegin {
913 cancellable: Some(true),
914 ..Default::default()
915 },
916 DEFAULT_LSP_REQUEST_TIMEOUT,
917 )
918 .await;
919
920 cx.executor().run_until_parked();
921
922 project.update(cx, |project, cx| {
923 project.cancel_language_server_work(
924 server_id,
925 Some(ProgressToken::String(SharedString::from(progress_token))),
926 cx,
927 )
928 });
929
930 cx.executor().run_until_parked();
931
932 // Verify the cancellation was received on the server side
933 let cancel_notification = fake_lsp
934 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
935 .await;
936 assert_eq!(
937 cancel_notification.token,
938 lsp::NumberOrString::String(progress_token.to_owned())
939 );
940 }
941}
942
943#[gpui::test]
944async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
945 let fs = FakeFs::new(server_cx.executor());
946 fs.insert_tree(
947 path!("/code"),
948 json!({
949 "project1": {
950 ".git": {},
951 "README.md": "# project 1",
952 "src": {
953 "lib.rs": "fn one() -> usize { 1 }"
954 }
955 },
956 }),
957 )
958 .await;
959
960 let (project, _headless) = init_test(&fs, cx, server_cx).await;
961 let (worktree, _) = project
962 .update(cx, |project, cx| {
963 project.find_or_create_worktree(path!("/code/project1"), true, cx)
964 })
965 .await
966 .unwrap();
967
968 let worktree_id = cx.update(|cx| worktree.read(cx).id());
969
970 let buffer = project
971 .update(cx, |project, cx| {
972 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
973 })
974 .await
975 .unwrap();
976
977 fs.save(
978 &PathBuf::from(path!("/code/project1/src/lib.rs")),
979 &("bangles".to_string().into()),
980 LineEnding::Unix,
981 )
982 .await
983 .unwrap();
984
985 cx.run_until_parked();
986
987 buffer.update(cx, |buffer, cx| {
988 assert_eq!(buffer.text(), "bangles");
989 buffer.edit([(0..0, "a")], None, cx);
990 });
991
992 fs.save(
993 &PathBuf::from(path!("/code/project1/src/lib.rs")),
994 &("bloop".to_string().into()),
995 LineEnding::Unix,
996 )
997 .await
998 .unwrap();
999
1000 cx.run_until_parked();
1001 cx.update(|cx| {
1002 assert!(buffer.read(cx).has_conflict());
1003 });
1004
1005 project
1006 .update(cx, |project, cx| {
1007 project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
1008 })
1009 .await
1010 .unwrap();
1011 cx.run_until_parked();
1012
1013 cx.update(|cx| {
1014 assert!(!buffer.read(cx).has_conflict());
1015 });
1016}
1017
1018#[gpui::test]
1019async fn test_remote_resolve_path_in_buffer(
1020 cx: &mut TestAppContext,
1021 server_cx: &mut TestAppContext,
1022) {
1023 let fs = FakeFs::new(server_cx.executor());
1024 // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
1025 fs.insert_tree(
1026 path!("/code"),
1027 json!({
1028 "project1": {
1029 ".git": {},
1030 "README.md": "# project 1",
1031 "src": {
1032 "lib.rs": "fn one() -> usize { 1 }"
1033 }
1034 },
1035 "project2": {
1036 ".git": {},
1037 "README.md": "# project 2",
1038 "src": {
1039 "lib.rs": "fn two() -> usize { 2 }"
1040 }
1041 }
1042 }),
1043 )
1044 .await;
1045
1046 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1047
1048 let _ = project
1049 .update(cx, |project, cx| {
1050 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1051 })
1052 .await
1053 .unwrap();
1054
1055 let (worktree2, _) = project
1056 .update(cx, |project, cx| {
1057 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1058 })
1059 .await
1060 .unwrap();
1061
1062 let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
1063
1064 cx.run_until_parked();
1065
1066 let buffer2 = project
1067 .update(cx, |project, cx| {
1068 project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx)
1069 })
1070 .await
1071 .unwrap();
1072
1073 let path = project
1074 .update(cx, |project, cx| {
1075 project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
1076 })
1077 .await
1078 .unwrap();
1079 assert!(path.is_file());
1080 assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
1081
1082 let path = project
1083 .update(cx, |project, cx| {
1084 project.resolve_path_in_buffer("../README.md", &buffer2, cx)
1085 })
1086 .await
1087 .unwrap();
1088 assert!(path.is_file());
1089 assert_eq!(
1090 path.project_path().unwrap().clone(),
1091 (worktree2_id, rel_path("README.md")).into()
1092 );
1093
1094 let path = project
1095 .update(cx, |project, cx| {
1096 project.resolve_path_in_buffer("../src", &buffer2, cx)
1097 })
1098 .await
1099 .unwrap();
1100 assert_eq!(
1101 path.project_path().unwrap().clone(),
1102 (worktree2_id, rel_path("src")).into()
1103 );
1104 assert!(path.is_dir());
1105}
1106
1107#[gpui::test]
1108async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1109 let fs = FakeFs::new(server_cx.executor());
1110 fs.insert_tree(
1111 path!("/code"),
1112 json!({
1113 "project1": {
1114 ".git": {},
1115 "README.md": "# project 1",
1116 "src": {
1117 "lib.rs": "fn one() -> usize { 1 }"
1118 }
1119 },
1120 }),
1121 )
1122 .await;
1123
1124 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1125
1126 let path = project
1127 .update(cx, |project, cx| {
1128 project.resolve_abs_path(path!("/code/project1/README.md"), cx)
1129 })
1130 .await
1131 .unwrap();
1132
1133 assert!(path.is_file());
1134 assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
1135
1136 let path = project
1137 .update(cx, |project, cx| {
1138 project.resolve_abs_path(path!("/code/project1/src"), cx)
1139 })
1140 .await
1141 .unwrap();
1142
1143 assert!(path.is_dir());
1144 assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
1145
1146 let path = project
1147 .update(cx, |project, cx| {
1148 project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
1149 })
1150 .await;
1151 assert!(path.is_none());
1152}
1153
1154#[gpui::test(iterations = 10)]
1155async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1156 let fs = FakeFs::new(server_cx.executor());
1157 fs.insert_tree(
1158 "/code",
1159 json!({
1160 "project1": {
1161 ".git": {},
1162 "README.md": "# project 1",
1163 "src": {
1164 "lib.rs": "fn one() -> usize { 1 }"
1165 }
1166 },
1167 }),
1168 )
1169 .await;
1170
1171 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1172 let (worktree, _) = project
1173 .update(cx, |project, cx| {
1174 project.find_or_create_worktree("/code/project1", true, cx)
1175 })
1176 .await
1177 .unwrap();
1178 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
1179
1180 // Open a buffer on the client but cancel after a random amount of time.
1181 let buffer = project.update(cx, |p, cx| {
1182 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1183 });
1184 cx.executor().simulate_random_delay().await;
1185 drop(buffer);
1186
1187 // Try opening the same buffer again as the client, and ensure we can
1188 // still do it despite the cancellation above.
1189 let buffer = project
1190 .update(cx, |p, cx| {
1191 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1192 })
1193 .await
1194 .unwrap();
1195
1196 buffer.read_with(cx, |buf, _| {
1197 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
1198 });
1199}
1200
1201#[gpui::test]
1202async fn test_adding_then_removing_then_adding_worktrees(
1203 cx: &mut TestAppContext,
1204 server_cx: &mut TestAppContext,
1205) {
1206 let fs = FakeFs::new(server_cx.executor());
1207 fs.insert_tree(
1208 path!("/code"),
1209 json!({
1210 "project1": {
1211 ".git": {},
1212 "README.md": "# project 1",
1213 "src": {
1214 "lib.rs": "fn one() -> usize { 1 }"
1215 }
1216 },
1217 "project2": {
1218 "README.md": "# project 2",
1219 },
1220 }),
1221 )
1222 .await;
1223
1224 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1225 let (_worktree, _) = project
1226 .update(cx, |project, cx| {
1227 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1228 })
1229 .await
1230 .unwrap();
1231
1232 let (worktree_2, _) = project
1233 .update(cx, |project, cx| {
1234 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1235 })
1236 .await
1237 .unwrap();
1238 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1239
1240 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1241
1242 let (worktree_2, _) = project
1243 .update(cx, |project, cx| {
1244 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1245 })
1246 .await
1247 .unwrap();
1248
1249 cx.run_until_parked();
1250 worktree_2.update(cx, |worktree, _cx| {
1251 assert!(worktree.is_visible());
1252 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1253 assert_eq!(entries.len(), 2);
1254 assert_eq!(entries[1].path.as_unix_str(), "README.md")
1255 })
1256}
1257
1258#[gpui::test]
1259async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1260 let fs = FakeFs::new(server_cx.executor());
1261 fs.insert_tree(
1262 path!("/code"),
1263 json!({
1264 "project1": {
1265 ".git": {},
1266 "README.md": "# project 1",
1267 "src": {
1268 "lib.rs": "fn one() -> usize { 1 }"
1269 }
1270 },
1271 }),
1272 )
1273 .await;
1274
1275 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1276 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1277 cx.executor().run_until_parked();
1278
1279 let buffer = buffer.await.unwrap();
1280
1281 cx.update(|cx| {
1282 assert_eq!(
1283 buffer.read(cx).text(),
1284 initial_server_settings_content()
1285 .to_string()
1286 .replace("\r\n", "\n")
1287 )
1288 })
1289}
1290
1291#[gpui::test(iterations = 20)]
1292async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1293 let fs = FakeFs::new(server_cx.executor());
1294 fs.insert_tree(
1295 path!("/code"),
1296 json!({
1297 "project1": {
1298 ".git": {},
1299 "README.md": "# project 1",
1300 "src": {
1301 "lib.rs": "fn one() -> usize { 1 }"
1302 }
1303 },
1304 }),
1305 )
1306 .await;
1307
1308 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1309
1310 let (worktree, _) = project
1311 .update(cx, |project, cx| {
1312 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1313 })
1314 .await
1315 .unwrap();
1316
1317 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1318 let buffer = project
1319 .update(cx, |project, cx| {
1320 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1321 })
1322 .await
1323 .unwrap();
1324
1325 buffer.update(cx, |buffer, cx| {
1326 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1327 let ix = buffer.text().find('1').unwrap();
1328 buffer.edit([(ix..ix + 1, "100")], None, cx);
1329 });
1330
1331 let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
1332 client
1333 .update(cx, |client, cx| client.simulate_disconnect(cx))
1334 .detach();
1335
1336 project
1337 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1338 .await
1339 .unwrap();
1340
1341 assert_eq!(
1342 fs.load(path!("/code/project1/src/lib.rs").as_ref())
1343 .await
1344 .unwrap(),
1345 "fn one() -> usize { 100 }"
1346 );
1347}
1348
1349#[gpui::test]
1350async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1351 let fs = FakeFs::new(server_cx.executor());
1352 fs.insert_tree(
1353 "/code",
1354 json!({
1355 "project1": {
1356 ".git": {},
1357 "README.md": "# project 1",
1358 },
1359 }),
1360 )
1361 .await;
1362
1363 let (project, _) = init_test(&fs, cx, server_cx).await;
1364
1365 let (worktree, _) = project
1366 .update(cx, |project, cx| {
1367 project.find_or_create_worktree("/code/project1", true, cx)
1368 })
1369 .await
1370 .unwrap();
1371
1372 cx.run_until_parked();
1373
1374 fs.rename(
1375 &PathBuf::from("/code/project1"),
1376 &PathBuf::from("/code/project2"),
1377 Default::default(),
1378 )
1379 .await
1380 .unwrap();
1381
1382 cx.run_until_parked();
1383 worktree.update(cx, |worktree, _| {
1384 assert_eq!(worktree.root_name(), "project2")
1385 })
1386}
1387
1388#[gpui::test]
1389async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1390 let fs = FakeFs::new(server_cx.executor());
1391 fs.insert_tree(
1392 "/code",
1393 json!({
1394 "project1": {
1395 ".git": {},
1396 "README.md": "# project 1",
1397 },
1398 }),
1399 )
1400 .await;
1401
1402 let (project, _) = init_test(&fs, cx, server_cx).await;
1403 let (worktree, _) = project
1404 .update(cx, |project, cx| {
1405 project.find_or_create_worktree("/code/project1", true, cx)
1406 })
1407 .await
1408 .unwrap();
1409
1410 cx.run_until_parked();
1411
1412 let entry = project
1413 .update(cx, |project, cx| {
1414 let worktree = worktree.read(cx);
1415 let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
1416 project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
1417 })
1418 .await
1419 .unwrap()
1420 .into_included()
1421 .unwrap();
1422
1423 cx.run_until_parked();
1424
1425 worktree.update(cx, |worktree, _| {
1426 assert_eq!(
1427 worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
1428 entry.id
1429 )
1430 });
1431}
1432
1433#[gpui::test]
1434async fn test_copy_file_into_remote_project(
1435 cx: &mut TestAppContext,
1436 server_cx: &mut TestAppContext,
1437) {
1438 let remote_fs = FakeFs::new(server_cx.executor());
1439 remote_fs
1440 .insert_tree(
1441 path!("/code"),
1442 json!({
1443 "project1": {
1444 ".git": {},
1445 "README.md": "# project 1",
1446 "src": {
1447 "main.rs": ""
1448 }
1449 },
1450 }),
1451 )
1452 .await;
1453
1454 let (project, _) = init_test(&remote_fs, cx, server_cx).await;
1455 let (worktree, _) = project
1456 .update(cx, |project, cx| {
1457 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1458 })
1459 .await
1460 .unwrap();
1461
1462 cx.run_until_parked();
1463
1464 let local_fs = project
1465 .read_with(cx, |project, _| project.fs().clone())
1466 .as_fake();
1467 local_fs
1468 .insert_tree(
1469 path!("/local-code"),
1470 json!({
1471 "dir1": {
1472 "file1": "file 1 content",
1473 "dir2": {
1474 "file2": "file 2 content",
1475 "dir3": {
1476 "file3": ""
1477 },
1478 "dir4": {}
1479 },
1480 "dir5": {}
1481 },
1482 "file4": "file 4 content"
1483 }),
1484 )
1485 .await;
1486
1487 worktree
1488 .update(cx, |worktree, cx| {
1489 worktree.copy_external_entries(
1490 rel_path("src").into(),
1491 vec![
1492 Path::new(path!("/local-code/dir1/file1")).into(),
1493 Path::new(path!("/local-code/dir1/dir2")).into(),
1494 ],
1495 local_fs.clone(),
1496 cx,
1497 )
1498 })
1499 .await
1500 .unwrap();
1501
1502 assert_eq!(
1503 remote_fs.paths(true),
1504 vec![
1505 PathBuf::from(path!("/")),
1506 PathBuf::from(path!("/code")),
1507 PathBuf::from(path!("/code/project1")),
1508 PathBuf::from(path!("/code/project1/.git")),
1509 PathBuf::from(path!("/code/project1/README.md")),
1510 PathBuf::from(path!("/code/project1/src")),
1511 PathBuf::from(path!("/code/project1/src/dir2")),
1512 PathBuf::from(path!("/code/project1/src/file1")),
1513 PathBuf::from(path!("/code/project1/src/main.rs")),
1514 PathBuf::from(path!("/code/project1/src/dir2/dir3")),
1515 PathBuf::from(path!("/code/project1/src/dir2/dir4")),
1516 PathBuf::from(path!("/code/project1/src/dir2/file2")),
1517 PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
1518 ]
1519 );
1520 assert_eq!(
1521 remote_fs
1522 .load(path!("/code/project1/src/file1").as_ref())
1523 .await
1524 .unwrap(),
1525 "file 1 content"
1526 );
1527 assert_eq!(
1528 remote_fs
1529 .load(path!("/code/project1/src/dir2/file2").as_ref())
1530 .await
1531 .unwrap(),
1532 "file 2 content"
1533 );
1534 assert_eq!(
1535 remote_fs
1536 .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
1537 .await
1538 .unwrap(),
1539 ""
1540 );
1541}
1542
1543#[gpui::test]
1544async fn test_remote_root_repo_common_dir(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1545 let fs = FakeFs::new(server_cx.executor());
1546 fs.insert_tree(
1547 "/code",
1548 json!({
1549 "main_repo": {
1550 ".git": {},
1551 "file.txt": "content",
1552 },
1553 "no_git": {
1554 "file.txt": "content",
1555 },
1556 }),
1557 )
1558 .await;
1559
1560 // Create a linked worktree that points back to main_repo's .git.
1561 fs.add_linked_worktree_for_repo(
1562 Path::new("/code/main_repo/.git"),
1563 false,
1564 GitWorktree {
1565 path: PathBuf::from("/code/linked_worktree"),
1566 ref_name: Some("refs/heads/feature-branch".into()),
1567 sha: "abc123".into(),
1568 is_main: false,
1569 is_bare: false,
1570 },
1571 )
1572 .await;
1573
1574 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1575
1576 // Main repo: root_repo_common_dir should be the .git directory itself.
1577 let (worktree_main, _) = project
1578 .update(cx, |project, cx| {
1579 project.find_or_create_worktree("/code/main_repo", true, cx)
1580 })
1581 .await
1582 .unwrap();
1583 cx.executor().run_until_parked();
1584
1585 let common_dir = worktree_main.read_with(cx, |worktree, _| {
1586 worktree.snapshot().root_repo_common_dir().cloned()
1587 });
1588 assert_eq!(
1589 common_dir.as_deref(),
1590 Some(Path::new("/code/main_repo/.git")),
1591 );
1592
1593 // Linked worktree: root_repo_common_dir should point to the main repo's .git.
1594 let (worktree_linked, _) = project
1595 .update(cx, |project, cx| {
1596 project.find_or_create_worktree("/code/linked_worktree", true, cx)
1597 })
1598 .await
1599 .unwrap();
1600 cx.executor().run_until_parked();
1601
1602 let common_dir = worktree_linked.read_with(cx, |worktree, _| {
1603 worktree.snapshot().root_repo_common_dir().cloned()
1604 });
1605 assert_eq!(
1606 common_dir.as_deref(),
1607 Some(Path::new("/code/main_repo/.git")),
1608 );
1609
1610 // No git repo: root_repo_common_dir should be None.
1611 let (worktree_no_git, _) = project
1612 .update(cx, |project, cx| {
1613 project.find_or_create_worktree("/code/no_git", true, cx)
1614 })
1615 .await
1616 .unwrap();
1617 cx.executor().run_until_parked();
1618
1619 let common_dir = worktree_no_git.read_with(cx, |worktree, _| {
1620 worktree.snapshot().root_repo_common_dir().cloned()
1621 });
1622 assert_eq!(common_dir, None);
1623}
1624
1625#[gpui::test]
1626async fn test_remote_archive_git_operations_are_supported(
1627 cx: &mut TestAppContext,
1628 server_cx: &mut TestAppContext,
1629) {
1630 let fs = FakeFs::new(server_cx.executor());
1631 fs.insert_tree(
1632 "/project",
1633 json!({
1634 ".git": {},
1635 "file.txt": "content",
1636 }),
1637 )
1638 .await;
1639 fs.set_branch_name(Path::new("/project/.git"), Some("main"));
1640 fs.set_head_for_repo(
1641 Path::new("/project/.git"),
1642 &[("file.txt", "content".into())],
1643 "head-sha",
1644 );
1645
1646 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1647 project
1648 .update(cx, |project, cx| {
1649 project.find_or_create_worktree(Path::new("/project"), true, cx)
1650 })
1651 .await
1652 .expect("should open remote worktree");
1653 cx.run_until_parked();
1654
1655 let repository = project.read_with(cx, |project, cx| {
1656 project
1657 .active_repository(cx)
1658 .expect("remote project should have an active repository")
1659 });
1660
1661 let head_sha = cx
1662 .update(|cx| repository.update(cx, |repository, _| repository.head_sha()))
1663 .await
1664 .expect("head_sha request should complete")
1665 .expect("head_sha should succeed")
1666 .expect("HEAD should exist");
1667
1668 cx.run_until_parked();
1669
1670 cx.update(|cx| {
1671 repository.update(cx, |repository, _| {
1672 repository.update_ref("refs/zed-tests/archive-checkpoint".to_string(), head_sha)
1673 })
1674 })
1675 .await
1676 .expect("update_ref request should complete")
1677 .expect("update_ref should succeed for remote repository");
1678
1679 cx.run_until_parked();
1680
1681 cx.update(|cx| {
1682 repository.update(cx, |repository, _| {
1683 repository.delete_ref("refs/zed-tests/archive-checkpoint".to_string())
1684 })
1685 })
1686 .await
1687 .expect("delete_ref request should complete")
1688 .expect("delete_ref should succeed for remote repository");
1689
1690 cx.run_until_parked();
1691
1692 cx.update(|cx| repository.update(cx, |repository, _| repository.repair_worktrees()))
1693 .await
1694 .expect("repair_worktrees request should complete")
1695 .expect("repair_worktrees should succeed for remote repository");
1696
1697 cx.run_until_parked();
1698
1699 let (staged_commit_sha, unstaged_commit_sha) = cx
1700 .update(|cx| repository.update(cx, |repository, _| repository.create_archive_checkpoint()))
1701 .await
1702 .expect("create_archive_checkpoint request should complete")
1703 .expect("create_archive_checkpoint should succeed for remote repository");
1704
1705 cx.run_until_parked();
1706
1707 cx.update(|cx| {
1708 repository.update(cx, |repository, _| {
1709 repository.restore_archive_checkpoint(staged_commit_sha, unstaged_commit_sha)
1710 })
1711 })
1712 .await
1713 .expect("restore_archive_checkpoint request should complete")
1714 .expect("restore_archive_checkpoint should succeed for remote repository");
1715}
1716
1717#[gpui::test]
1718async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1719 let text_2 = "
1720 fn one() -> usize {
1721 1
1722 }
1723 "
1724 .unindent();
1725 let text_1 = "
1726 fn one() -> usize {
1727 0
1728 }
1729 "
1730 .unindent();
1731
1732 let fs = FakeFs::new(server_cx.executor());
1733 fs.insert_tree(
1734 "/code",
1735 json!({
1736 "project1": {
1737 ".git": {},
1738 "src": {
1739 "lib.rs": text_2
1740 },
1741 "README.md": "# project 1",
1742 },
1743 }),
1744 )
1745 .await;
1746 fs.set_index_for_repo(
1747 Path::new("/code/project1/.git"),
1748 &[("src/lib.rs", text_1.clone())],
1749 );
1750 fs.set_head_for_repo(
1751 Path::new("/code/project1/.git"),
1752 &[("src/lib.rs", text_1.clone())],
1753 "deadbeef",
1754 );
1755
1756 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1757 let (worktree, _) = project
1758 .update(cx, |project, cx| {
1759 project.find_or_create_worktree("/code/project1", true, cx)
1760 })
1761 .await
1762 .unwrap();
1763 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1764 cx.executor().run_until_parked();
1765
1766 let buffer = project
1767 .update(cx, |project, cx| {
1768 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1769 })
1770 .await
1771 .unwrap();
1772 let diff = project
1773 .update(cx, |project, cx| {
1774 project.open_uncommitted_diff(buffer.clone(), cx)
1775 })
1776 .await
1777 .unwrap();
1778
1779 diff.read_with(cx, |diff, cx| {
1780 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1781 assert_eq!(
1782 diff.secondary_diff()
1783 .unwrap()
1784 .read(cx)
1785 .base_text_string(cx)
1786 .unwrap(),
1787 text_1
1788 );
1789 });
1790
1791 // stage the current buffer's contents
1792 fs.set_index_for_repo(
1793 Path::new("/code/project1/.git"),
1794 &[("src/lib.rs", text_2.clone())],
1795 );
1796
1797 cx.executor().run_until_parked();
1798 diff.read_with(cx, |diff, cx| {
1799 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1800 assert_eq!(
1801 diff.secondary_diff()
1802 .unwrap()
1803 .read(cx)
1804 .base_text_string(cx)
1805 .unwrap(),
1806 text_2
1807 );
1808 });
1809
1810 // commit the current buffer's contents
1811 fs.set_head_for_repo(
1812 Path::new("/code/project1/.git"),
1813 &[("src/lib.rs", text_2.clone())],
1814 "deadbeef",
1815 );
1816
1817 cx.executor().run_until_parked();
1818 diff.read_with(cx, |diff, cx| {
1819 assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
1820 assert_eq!(
1821 diff.secondary_diff()
1822 .unwrap()
1823 .read(cx)
1824 .base_text_string(cx)
1825 .unwrap(),
1826 text_2
1827 );
1828 });
1829}
1830
1831#[gpui::test]
1832async fn test_remote_git_diffs_when_recv_update_repository_delay(
1833 cx: &mut TestAppContext,
1834 server_cx: &mut TestAppContext,
1835) {
1836 cx.update(|cx| {
1837 let settings_store = SettingsStore::test(cx);
1838 cx.set_global(settings_store);
1839 theme_settings::init(theme::LoadThemes::JustBase, cx);
1840 release_channel::init(semver::Version::new(0, 0, 0), cx);
1841 editor::init(cx);
1842 });
1843
1844 use editor::Editor;
1845 use gpui::VisualContext;
1846 let text_2 = "
1847 fn one() -> usize {
1848 1
1849 }
1850 "
1851 .unindent();
1852 let text_1 = "
1853 fn one() -> usize {
1854 0
1855 }
1856 "
1857 .unindent();
1858
1859 let fs = FakeFs::new(server_cx.executor());
1860 fs.insert_tree(
1861 path!("/code"),
1862 json!({
1863 "project1": {
1864 "src": {
1865 "lib.rs": text_2
1866 },
1867 "README.md": "# project 1",
1868 },
1869 }),
1870 )
1871 .await;
1872
1873 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1874 let (worktree, _) = project
1875 .update(cx, |project, cx| {
1876 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1877 })
1878 .await
1879 .unwrap();
1880 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1881 let buffer = project
1882 .update(cx, |project, cx| {
1883 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1884 })
1885 .await
1886 .unwrap();
1887 let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
1888
1889 let cx = cx.add_empty_window();
1890 let editor = cx.new_window_entity(|window, cx| {
1891 Editor::for_buffer(buffer, Some(project.clone()), window, cx)
1892 });
1893
1894 // Remote server will send proto::UpdateRepository after the instance of Editor create.
1895 fs.insert_tree(
1896 path!("/code"),
1897 json!({
1898 "project1": {
1899 ".git": {},
1900 },
1901 }),
1902 )
1903 .await;
1904
1905 fs.set_index_for_repo(
1906 Path::new(path!("/code/project1/.git")),
1907 &[("src/lib.rs", text_1.clone())],
1908 );
1909 fs.set_head_for_repo(
1910 Path::new(path!("/code/project1/.git")),
1911 &[("src/lib.rs", text_1.clone())],
1912 "sha",
1913 );
1914
1915 cx.executor().run_until_parked();
1916 let diff = editor
1917 .read_with(cx, |editor, cx| {
1918 editor
1919 .buffer()
1920 .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
1921 })
1922 .unwrap();
1923
1924 diff.read_with(cx, |diff, cx| {
1925 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1926 assert_eq!(
1927 diff.secondary_diff()
1928 .unwrap()
1929 .read(cx)
1930 .base_text_string(cx)
1931 .unwrap(),
1932 text_1
1933 );
1934 });
1935
1936 // stage the current buffer's contents
1937 fs.set_index_for_repo(
1938 Path::new(path!("/code/project1/.git")),
1939 &[("src/lib.rs", text_2.clone())],
1940 );
1941
1942 cx.executor().run_until_parked();
1943 diff.read_with(cx, |diff, cx| {
1944 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1945 assert_eq!(
1946 diff.secondary_diff()
1947 .unwrap()
1948 .read(cx)
1949 .base_text_string(cx)
1950 .unwrap(),
1951 text_2
1952 );
1953 });
1954
1955 // commit the current buffer's contents
1956 fs.set_head_for_repo(
1957 Path::new(path!("/code/project1/.git")),
1958 &[("src/lib.rs", text_2.clone())],
1959 "sha",
1960 );
1961
1962 cx.executor().run_until_parked();
1963 diff.read_with(cx, |diff, cx| {
1964 assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
1965 assert_eq!(
1966 diff.secondary_diff()
1967 .unwrap()
1968 .read(cx)
1969 .base_text_string(cx)
1970 .unwrap(),
1971 text_2
1972 );
1973 });
1974}
1975
1976#[gpui::test]
1977async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1978 let fs = FakeFs::new(server_cx.executor());
1979 fs.insert_tree(
1980 path!("/code"),
1981 json!({
1982 "project1": {
1983 ".git": {},
1984 "README.md": "# project 1",
1985 },
1986 }),
1987 )
1988 .await;
1989
1990 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1991 let branches = ["main", "dev", "feature-1"];
1992 let branches_set = branches
1993 .iter()
1994 .map(ToString::to_string)
1995 .collect::<HashSet<_>>();
1996 fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1997
1998 let (_worktree, _) = project
1999 .update(cx, |project, cx| {
2000 project.find_or_create_worktree(path!("/code/project1"), true, cx)
2001 })
2002 .await
2003 .unwrap();
2004 // Give the worktree a bit of time to index the file system
2005 cx.run_until_parked();
2006
2007 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
2008
2009 let remote_branches = repository
2010 .update(cx, |repository, _| repository.branches())
2011 .await
2012 .unwrap()
2013 .unwrap();
2014
2015 let new_branch = branches[2];
2016
2017 let remote_branches = remote_branches
2018 .into_iter()
2019 .map(|branch| branch.name().to_string())
2020 .collect::<HashSet<_>>();
2021
2022 assert_eq!(&remote_branches, &branches_set);
2023
2024 cx.update(|cx| {
2025 repository.update(cx, |repository, _cx| {
2026 repository.change_branch(new_branch.to_string())
2027 })
2028 })
2029 .await
2030 .unwrap()
2031 .unwrap();
2032
2033 cx.run_until_parked();
2034
2035 let server_branch = server_cx.update(|cx| {
2036 headless_project.update(cx, |headless_project, cx| {
2037 headless_project.git_store.update(cx, |git_store, cx| {
2038 git_store
2039 .repositories()
2040 .values()
2041 .next()
2042 .unwrap()
2043 .read(cx)
2044 .branch
2045 .as_ref()
2046 .unwrap()
2047 .clone()
2048 })
2049 })
2050 });
2051
2052 assert_eq!(server_branch.name(), branches[2]);
2053
2054 // Also try creating a new branch
2055 cx.update(|cx| {
2056 repository.update(cx, |repo, _cx| {
2057 repo.create_branch("totally-new-branch".to_string(), None)
2058 })
2059 })
2060 .await
2061 .unwrap()
2062 .unwrap();
2063
2064 cx.update(|cx| {
2065 repository.update(cx, |repo, _cx| {
2066 repo.change_branch("totally-new-branch".to_string())
2067 })
2068 })
2069 .await
2070 .unwrap()
2071 .unwrap();
2072
2073 cx.run_until_parked();
2074
2075 let server_branch = server_cx.update(|cx| {
2076 headless_project.update(cx, |headless_project, cx| {
2077 headless_project.git_store.update(cx, |git_store, cx| {
2078 git_store
2079 .repositories()
2080 .values()
2081 .next()
2082 .unwrap()
2083 .read(cx)
2084 .branch
2085 .as_ref()
2086 .unwrap()
2087 .clone()
2088 })
2089 })
2090 });
2091
2092 assert_eq!(server_branch.name(), "totally-new-branch");
2093}
2094
2095#[gpui::test]
2096async fn test_remote_git_checkpoints(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2097 let fs = FakeFs::new(server_cx.executor());
2098 fs.insert_tree(
2099 path!("/code"),
2100 json!({
2101 "project1": {
2102 ".git": {},
2103 "file.txt": "original content",
2104 },
2105 }),
2106 )
2107 .await;
2108
2109 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2110
2111 let (_worktree, _) = project
2112 .update(cx, |project, cx| {
2113 project.find_or_create_worktree(path!("/code/project1"), true, cx)
2114 })
2115 .await
2116 .unwrap();
2117 cx.run_until_parked();
2118
2119 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
2120
2121 // 1. Create a checkpoint of the original state
2122 let checkpoint_1 = repository
2123 .update(cx, |repository, _| repository.checkpoint())
2124 .await
2125 .unwrap()
2126 .unwrap();
2127
2128 // 2. Modify a file on the server-side fs
2129 fs.write(
2130 Path::new(path!("/code/project1/file.txt")),
2131 b"modified content",
2132 )
2133 .await
2134 .unwrap();
2135
2136 // 3. Create a second checkpoint with the modified state
2137 let checkpoint_2 = repository
2138 .update(cx, |repository, _| repository.checkpoint())
2139 .await
2140 .unwrap()
2141 .unwrap();
2142
2143 // 4. compare_checkpoints: same checkpoint with itself => equal
2144 let equal = repository
2145 .update(cx, |repository, _| {
2146 repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
2147 })
2148 .await
2149 .unwrap()
2150 .unwrap();
2151 assert!(equal, "a checkpoint compared with itself should be equal");
2152
2153 // 5. compare_checkpoints: different states => not equal
2154 let equal = repository
2155 .update(cx, |repository, _| {
2156 repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
2157 })
2158 .await
2159 .unwrap()
2160 .unwrap();
2161 assert!(
2162 !equal,
2163 "checkpoints of different states should not be equal"
2164 );
2165
2166 // 6. diff_checkpoints: same checkpoint => empty diff
2167 let diff = repository
2168 .update(cx, |repository, _| {
2169 repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
2170 })
2171 .await
2172 .unwrap()
2173 .unwrap();
2174 assert!(
2175 diff.is_empty(),
2176 "diff of identical checkpoints should be empty"
2177 );
2178
2179 // 7. diff_checkpoints: different checkpoints => non-empty diff mentioning the changed file
2180 let diff = repository
2181 .update(cx, |repository, _| {
2182 repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
2183 })
2184 .await
2185 .unwrap()
2186 .unwrap();
2187 assert!(
2188 !diff.is_empty(),
2189 "diff of different checkpoints should be non-empty"
2190 );
2191 assert!(
2192 diff.contains("file.txt"),
2193 "diff should mention the changed file"
2194 );
2195 assert!(
2196 diff.contains("original content"),
2197 "diff should contain removed content"
2198 );
2199 assert!(
2200 diff.contains("modified content"),
2201 "diff should contain added content"
2202 );
2203
2204 // 8. restore_checkpoint: restore to original state
2205 repository
2206 .update(cx, |repository, _| {
2207 repository.restore_checkpoint(checkpoint_1.clone())
2208 })
2209 .await
2210 .unwrap()
2211 .unwrap();
2212 cx.run_until_parked();
2213
2214 // 9. Create a checkpoint after restore
2215 let checkpoint_3 = repository
2216 .update(cx, |repository, _| repository.checkpoint())
2217 .await
2218 .unwrap()
2219 .unwrap();
2220
2221 // 10. compare_checkpoints: restored state matches original
2222 let equal = repository
2223 .update(cx, |repository, _| {
2224 repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
2225 })
2226 .await
2227 .unwrap()
2228 .unwrap();
2229 assert!(equal, "restored state should match original checkpoint");
2230
2231 // 11. diff_checkpoints: restored state vs original => empty diff
2232 let diff = repository
2233 .update(cx, |repository, _| {
2234 repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
2235 })
2236 .await
2237 .unwrap()
2238 .unwrap();
2239 assert!(diff.is_empty(), "diff after restore should be empty");
2240}
2241
2242#[gpui::test]
2243async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2244 let fs = FakeFs::new(server_cx.executor());
2245 fs.insert_tree(
2246 path!("/project"),
2247 json!({
2248 "a.txt": "A",
2249 "b.txt": "B",
2250 }),
2251 )
2252 .await;
2253
2254 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
2255 project
2256 .update(cx, |project, cx| {
2257 project.find_or_create_worktree(path!("/project"), true, cx)
2258 })
2259 .await
2260 .unwrap();
2261
2262 let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
2263
2264 let input = ReadFileToolInput {
2265 path: "project/b.txt".into(),
2266 start_line: None,
2267 end_line: None,
2268 };
2269 let read_tool = Arc::new(ReadFileTool::new(project, action_log, true));
2270 let (event_stream, _) = ToolCallEventStream::test();
2271
2272 let exists_result = cx.update(|cx| {
2273 read_tool
2274 .clone()
2275 .run(ToolInput::resolved(input), event_stream.clone(), cx)
2276 });
2277 let output = exists_result.await.unwrap();
2278 assert_eq!(output, LanguageModelToolResultContent::Text("B".into()));
2279
2280 let input = ReadFileToolInput {
2281 path: "project/c.txt".into(),
2282 start_line: None,
2283 end_line: None,
2284 };
2285 let does_not_exist_result =
2286 cx.update(|cx| read_tool.run(ToolInput::resolved(input), event_stream, cx));
2287 does_not_exist_result.await.unwrap_err();
2288}
2289
2290#[gpui::test]
2291async fn test_remote_external_agent_server(
2292 cx: &mut TestAppContext,
2293 server_cx: &mut TestAppContext,
2294) {
2295 let fs = FakeFs::new(server_cx.executor());
2296 fs.insert_tree(path!("/project"), json!({})).await;
2297
2298 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
2299 project
2300 .update(cx, |project, cx| {
2301 project.find_or_create_worktree(path!("/project"), true, cx)
2302 })
2303 .await
2304 .unwrap();
2305 let names = project.update(cx, |project, cx| {
2306 project
2307 .agent_server_store()
2308 .read(cx)
2309 .external_agents()
2310 .map(|name| name.to_string())
2311 .collect::<Vec<_>>()
2312 });
2313 pretty_assertions::assert_eq!(names, Vec::<String>::new());
2314 server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
2315 settings_store
2316 .set_server_settings(
2317 &json!({
2318 "agent_servers": {
2319 "foo": {
2320 "type": "custom",
2321 "command": "foo-cli",
2322 "args": ["--flag"],
2323 "env": {
2324 "VAR": "val"
2325 }
2326 }
2327 }
2328 })
2329 .to_string(),
2330 cx,
2331 )
2332 .unwrap();
2333 });
2334 server_cx.run_until_parked();
2335 cx.run_until_parked();
2336 let names = project.update(cx, |project, cx| {
2337 project
2338 .agent_server_store()
2339 .read(cx)
2340 .external_agents()
2341 .map(|name| name.to_string())
2342 .collect::<Vec<_>>()
2343 });
2344 pretty_assertions::assert_eq!(names, ["foo"]);
2345 let command = project
2346 .update(cx, |project, cx| {
2347 project.agent_server_store().update(cx, |store, cx| {
2348 store
2349 .get_external_agent(&"foo".into())
2350 .unwrap()
2351 .get_command(
2352 vec![],
2353 HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
2354 &mut cx.to_async(),
2355 )
2356 })
2357 })
2358 .await
2359 .unwrap();
2360 assert_eq!(
2361 command,
2362 AgentServerCommand {
2363 path: "foo-cli".into(),
2364 args: vec!["--flag".into()],
2365 env: Some(HashMap::from_iter([
2366 ("NO_BROWSER".into(), "1".into()),
2367 ("VAR".into(), "val".into()),
2368 ("OTHER_VAR".into(), "other-val".into())
2369 ]))
2370 }
2371 );
2372}
2373
2374pub async fn init_test(
2375 server_fs: &Arc<FakeFs>,
2376 cx: &mut TestAppContext,
2377 server_cx: &mut TestAppContext,
2378) -> (Entity<Project>, Entity<HeadlessProject>) {
2379 let server_fs = server_fs.clone();
2380 cx.update(|cx| {
2381 release_channel::init(semver::Version::new(0, 0, 0), cx);
2382 });
2383 server_cx.update(|cx| {
2384 release_channel::init(semver::Version::new(0, 0, 0), cx);
2385 });
2386 init_logger();
2387
2388 let (opts, ssh_server_client, _) = RemoteClient::fake_server(cx, server_cx);
2389 let http_client = Arc::new(BlockedHttpClient);
2390 let node_runtime = NodeRuntime::unavailable();
2391 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
2392 let proxy = Arc::new(ExtensionHostProxy::new());
2393 server_cx.update(HeadlessProject::init);
2394 let headless = server_cx.new(|cx| {
2395 HeadlessProject::new(
2396 crate::HeadlessAppState {
2397 session: ssh_server_client,
2398 fs: server_fs.clone(),
2399 http_client,
2400 node_runtime,
2401 languages,
2402 extension_host_proxy: proxy,
2403 startup_time: std::time::Instant::now(),
2404 },
2405 false,
2406 cx,
2407 )
2408 });
2409
2410 let ssh = RemoteClient::connect_mock(opts, cx).await;
2411 let project = build_project(ssh, cx);
2412 project
2413 .update(cx, {
2414 let headless = headless.clone();
2415 |_, cx| cx.on_release(|_, _| drop(headless))
2416 })
2417 .detach();
2418 (project, headless)
2419}
2420
2421fn init_logger() {
2422 zlog::init_test();
2423}
2424
2425fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
2426 cx.update(|cx| {
2427 if !cx.has_global::<SettingsStore>() {
2428 let settings_store = SettingsStore::test(cx);
2429 cx.set_global(settings_store);
2430 }
2431 });
2432
2433 let client = cx.update(|cx| {
2434 Client::new(
2435 Arc::new(FakeSystemClock::new()),
2436 FakeHttpClient::with_404_response(),
2437 cx,
2438 )
2439 });
2440
2441 let node = NodeRuntime::unavailable();
2442 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
2443 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
2444 let fs = FakeFs::new(cx.executor());
2445
2446 cx.update(|cx| {
2447 Project::init(&client, cx);
2448 });
2449
2450 cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, false, cx))
2451}