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::RemoteClient;
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;
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 path!("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 capabilities: lsp::ServerCapabilities {
426 completion_provider: Some(lsp::CompletionOptions::default()),
427 rename_provider: Some(lsp::OneOf::Left(true)),
428 ..lsp::ServerCapabilities::default()
429 },
430 ..FakeLspAdapter::default()
431 },
432 )
433 });
434
435 let mut fake_lsp = server_cx.update(|cx| {
436 headless.read(cx).languages.register_fake_language_server(
437 LanguageServerName("rust-analyzer".into()),
438 lsp::ServerCapabilities {
439 completion_provider: Some(lsp::CompletionOptions::default()),
440 rename_provider: Some(lsp::OneOf::Left(true)),
441 ..lsp::ServerCapabilities::default()
442 },
443 None,
444 )
445 });
446
447 cx.run_until_parked();
448
449 let worktree_id = project
450 .update(cx, |project, cx| {
451 project.find_or_create_worktree(path!("/code/project1"), true, cx)
452 })
453 .await
454 .unwrap()
455 .0
456 .read_with(cx, |worktree, _| worktree.id());
457
458 // Wait for the settings to synchronize
459 cx.run_until_parked();
460
461 let (buffer, _handle) = project
462 .update(cx, |project, cx| {
463 project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
464 })
465 .await
466 .unwrap();
467 cx.run_until_parked();
468
469 let fake_lsp = fake_lsp.next().await.unwrap();
470
471 cx.read(|cx| {
472 let file = buffer.read(cx).file();
473 assert_eq!(
474 language_settings(Some("Rust".into()), file, cx).language_servers,
475 ["rust-analyzer".to_string()]
476 )
477 });
478
479 let buffer_id = cx.read(|cx| {
480 let buffer = buffer.read(cx);
481 assert_eq!(buffer.language().unwrap().name(), "Rust".into());
482 buffer.remote_id()
483 });
484
485 server_cx.read(|cx| {
486 let buffer = headless
487 .read(cx)
488 .buffer_store
489 .read(cx)
490 .get(buffer_id)
491 .unwrap();
492
493 assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
494 });
495
496 server_cx.read(|cx| {
497 let lsp_store = headless.read(cx).lsp_store.read(cx);
498 assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
499 });
500
501 fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
502 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
503 label: "boop".to_string(),
504 ..Default::default()
505 }])))
506 });
507
508 let result = project
509 .update(cx, |project, cx| {
510 project.completions(
511 &buffer,
512 0,
513 CompletionContext {
514 trigger_kind: CompletionTriggerKind::INVOKED,
515 trigger_character: None,
516 },
517 cx,
518 )
519 })
520 .await
521 .unwrap();
522
523 assert_eq!(
524 result
525 .into_iter()
526 .flat_map(|response| response.completions)
527 .map(|c| c.label.text)
528 .collect::<Vec<_>>(),
529 vec!["boop".to_string()]
530 );
531
532 fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
533 Ok(Some(lsp::WorkspaceEdit {
534 changes: Some(
535 [(
536 lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
537 vec![lsp::TextEdit::new(
538 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
539 "two".to_string(),
540 )],
541 )]
542 .into_iter()
543 .collect(),
544 ),
545 ..Default::default()
546 }))
547 });
548
549 project
550 .update(cx, |project, cx| {
551 project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
552 })
553 .await
554 .unwrap();
555
556 cx.run_until_parked();
557 buffer.update(cx, |buffer, _| {
558 assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
559 })
560}
561
562#[gpui::test]
563async fn test_remote_cancel_language_server_work(
564 cx: &mut TestAppContext,
565 server_cx: &mut TestAppContext,
566) {
567 let fs = FakeFs::new(server_cx.executor());
568 fs.insert_tree(
569 path!("/code"),
570 json!({
571 "project1": {
572 ".git": {},
573 "README.md": "# project 1",
574 "src": {
575 "lib.rs": "fn one() -> usize { 1 }"
576 }
577 },
578 }),
579 )
580 .await;
581
582 let (project, headless) = init_test(&fs, cx, server_cx).await;
583
584 fs.insert_tree(
585 path!("/code/project1/.zed"),
586 json!({
587 "settings.json": r#"
588 {
589 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
590 "lsp": {
591 "rust-analyzer": {
592 "binary": {
593 "path": "~/.cargo/bin/rust-analyzer"
594 }
595 }
596 }
597 }"#
598 }),
599 )
600 .await;
601
602 cx.update_entity(&project, |project, _| {
603 project.languages().register_test_language(LanguageConfig {
604 name: "Rust".into(),
605 matcher: LanguageMatcher {
606 path_suffixes: vec!["rs".into()],
607 ..Default::default()
608 },
609 ..Default::default()
610 });
611 project.languages().register_fake_lsp_adapter(
612 "Rust",
613 FakeLspAdapter {
614 name: "rust-analyzer",
615 ..Default::default()
616 },
617 )
618 });
619
620 let mut fake_lsp = server_cx.update(|cx| {
621 headless.read(cx).languages.register_fake_language_server(
622 LanguageServerName("rust-analyzer".into()),
623 Default::default(),
624 None,
625 )
626 });
627
628 cx.run_until_parked();
629
630 let worktree_id = project
631 .update(cx, |project, cx| {
632 project.find_or_create_worktree(path!("/code/project1"), true, cx)
633 })
634 .await
635 .unwrap()
636 .0
637 .read_with(cx, |worktree, _| worktree.id());
638
639 cx.run_until_parked();
640
641 let (buffer, _handle) = project
642 .update(cx, |project, cx| {
643 project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
644 })
645 .await
646 .unwrap();
647
648 cx.run_until_parked();
649
650 let mut fake_lsp = fake_lsp.next().await.unwrap();
651
652 // Cancelling all language server work for a given buffer
653 {
654 // Two operations, one cancellable and one not.
655 fake_lsp
656 .start_progress_with(
657 "another-token",
658 lsp::WorkDoneProgressBegin {
659 cancellable: Some(false),
660 ..Default::default()
661 },
662 )
663 .await;
664
665 let progress_token = "the-progress-token";
666 fake_lsp
667 .start_progress_with(
668 progress_token,
669 lsp::WorkDoneProgressBegin {
670 cancellable: Some(true),
671 ..Default::default()
672 },
673 )
674 .await;
675
676 cx.executor().run_until_parked();
677
678 project.update(cx, |project, cx| {
679 project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
680 });
681
682 cx.executor().run_until_parked();
683
684 // Verify the cancellation was received on the server side
685 let cancel_notification = fake_lsp
686 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
687 .await;
688 assert_eq!(
689 cancel_notification.token,
690 lsp::NumberOrString::String(progress_token.into())
691 );
692 }
693
694 // Cancelling work by server_id and token
695 {
696 let server_id = fake_lsp.server.server_id();
697 let progress_token = "the-progress-token";
698
699 fake_lsp
700 .start_progress_with(
701 progress_token,
702 lsp::WorkDoneProgressBegin {
703 cancellable: Some(true),
704 ..Default::default()
705 },
706 )
707 .await;
708
709 cx.executor().run_until_parked();
710
711 project.update(cx, |project, cx| {
712 project.cancel_language_server_work(server_id, Some(progress_token.into()), cx)
713 });
714
715 cx.executor().run_until_parked();
716
717 // Verify the cancellation was received on the server side
718 let cancel_notification = fake_lsp
719 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
720 .await;
721 assert_eq!(
722 cancel_notification.token,
723 lsp::NumberOrString::String(progress_token.into())
724 );
725 }
726}
727
728#[gpui::test]
729async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
730 let fs = FakeFs::new(server_cx.executor());
731 fs.insert_tree(
732 path!("/code"),
733 json!({
734 "project1": {
735 ".git": {},
736 "README.md": "# project 1",
737 "src": {
738 "lib.rs": "fn one() -> usize { 1 }"
739 }
740 },
741 }),
742 )
743 .await;
744
745 let (project, _headless) = init_test(&fs, cx, server_cx).await;
746 let (worktree, _) = project
747 .update(cx, |project, cx| {
748 project.find_or_create_worktree(path!("/code/project1"), true, cx)
749 })
750 .await
751 .unwrap();
752
753 let worktree_id = cx.update(|cx| worktree.read(cx).id());
754
755 let buffer = project
756 .update(cx, |project, cx| {
757 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
758 })
759 .await
760 .unwrap();
761
762 fs.save(
763 &PathBuf::from(path!("/code/project1/src/lib.rs")),
764 &("bangles".to_string().into()),
765 LineEnding::Unix,
766 )
767 .await
768 .unwrap();
769
770 cx.run_until_parked();
771
772 buffer.update(cx, |buffer, cx| {
773 assert_eq!(buffer.text(), "bangles");
774 buffer.edit([(0..0, "a")], None, cx);
775 });
776
777 fs.save(
778 &PathBuf::from(path!("/code/project1/src/lib.rs")),
779 &("bloop".to_string().into()),
780 LineEnding::Unix,
781 )
782 .await
783 .unwrap();
784
785 cx.run_until_parked();
786 cx.update(|cx| {
787 assert!(buffer.read(cx).has_conflict());
788 });
789
790 project
791 .update(cx, |project, cx| {
792 project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
793 })
794 .await
795 .unwrap();
796 cx.run_until_parked();
797
798 cx.update(|cx| {
799 assert!(!buffer.read(cx).has_conflict());
800 });
801}
802
803#[gpui::test]
804async fn test_remote_resolve_path_in_buffer(
805 cx: &mut TestAppContext,
806 server_cx: &mut TestAppContext,
807) {
808 let fs = FakeFs::new(server_cx.executor());
809 // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
810 fs.insert_tree(
811 path!("/code"),
812 json!({
813 "project1": {
814 ".git": {},
815 "README.md": "# project 1",
816 "src": {
817 "lib.rs": "fn one() -> usize { 1 }"
818 }
819 },
820 "project2": {
821 ".git": {},
822 "README.md": "# project 2",
823 "src": {
824 "lib.rs": "fn two() -> usize { 2 }"
825 }
826 }
827 }),
828 )
829 .await;
830
831 let (project, _headless) = init_test(&fs, cx, server_cx).await;
832
833 let _ = project
834 .update(cx, |project, cx| {
835 project.find_or_create_worktree(path!("/code/project1"), true, cx)
836 })
837 .await
838 .unwrap();
839
840 let (worktree2, _) = project
841 .update(cx, |project, cx| {
842 project.find_or_create_worktree(path!("/code/project2"), true, cx)
843 })
844 .await
845 .unwrap();
846
847 let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
848
849 let buffer2 = project
850 .update(cx, |project, cx| {
851 project.open_buffer((worktree2_id, Path::new("src/lib.rs")), cx)
852 })
853 .await
854 .unwrap();
855
856 let path = project
857 .update(cx, |project, cx| {
858 project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
859 })
860 .await
861 .unwrap();
862 assert!(path.is_file());
863 assert_eq!(
864 path.abs_path().unwrap().to_string_lossy(),
865 path!("/code/project2/README.md")
866 );
867
868 let path = project
869 .update(cx, |project, cx| {
870 project.resolve_path_in_buffer("../README.md", &buffer2, cx)
871 })
872 .await
873 .unwrap();
874 assert!(path.is_file());
875 assert_eq!(
876 path.project_path().unwrap().clone(),
877 ProjectPath::from((worktree2_id, "README.md"))
878 );
879
880 let path = project
881 .update(cx, |project, cx| {
882 project.resolve_path_in_buffer("../src", &buffer2, cx)
883 })
884 .await
885 .unwrap();
886 assert_eq!(
887 path.project_path().unwrap().clone(),
888 ProjectPath::from((worktree2_id, "src"))
889 );
890 assert!(path.is_dir());
891}
892
893#[gpui::test]
894async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
895 let fs = FakeFs::new(server_cx.executor());
896 fs.insert_tree(
897 path!("/code"),
898 json!({
899 "project1": {
900 ".git": {},
901 "README.md": "# project 1",
902 "src": {
903 "lib.rs": "fn one() -> usize { 1 }"
904 }
905 },
906 }),
907 )
908 .await;
909
910 let (project, _headless) = init_test(&fs, cx, server_cx).await;
911
912 let path = project
913 .update(cx, |project, cx| {
914 project.resolve_abs_path(path!("/code/project1/README.md"), cx)
915 })
916 .await
917 .unwrap();
918
919 assert!(path.is_file());
920 assert_eq!(
921 path.abs_path().unwrap().to_string_lossy(),
922 path!("/code/project1/README.md")
923 );
924
925 let path = project
926 .update(cx, |project, cx| {
927 project.resolve_abs_path(path!("/code/project1/src"), cx)
928 })
929 .await
930 .unwrap();
931
932 assert!(path.is_dir());
933 assert_eq!(
934 path.abs_path().unwrap().to_string_lossy(),
935 path!("/code/project1/src")
936 );
937
938 let path = project
939 .update(cx, |project, cx| {
940 project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
941 })
942 .await;
943 assert!(path.is_none());
944}
945
946#[gpui::test(iterations = 10)]
947async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
948 let fs = FakeFs::new(server_cx.executor());
949 fs.insert_tree(
950 "/code",
951 json!({
952 "project1": {
953 ".git": {},
954 "README.md": "# project 1",
955 "src": {
956 "lib.rs": "fn one() -> usize { 1 }"
957 }
958 },
959 }),
960 )
961 .await;
962
963 let (project, _headless) = init_test(&fs, cx, server_cx).await;
964 let (worktree, _) = project
965 .update(cx, |project, cx| {
966 project.find_or_create_worktree("/code/project1", true, cx)
967 })
968 .await
969 .unwrap();
970 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
971
972 // Open a buffer on the client but cancel after a random amount of time.
973 let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
974 cx.executor().simulate_random_delay().await;
975 drop(buffer);
976
977 // Try opening the same buffer again as the client, and ensure we can
978 // still do it despite the cancellation above.
979 let buffer = project
980 .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
981 .await
982 .unwrap();
983
984 buffer.read_with(cx, |buf, _| {
985 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
986 });
987}
988
989#[gpui::test]
990async fn test_adding_then_removing_then_adding_worktrees(
991 cx: &mut TestAppContext,
992 server_cx: &mut TestAppContext,
993) {
994 let fs = FakeFs::new(server_cx.executor());
995 fs.insert_tree(
996 path!("/code"),
997 json!({
998 "project1": {
999 ".git": {},
1000 "README.md": "# project 1",
1001 "src": {
1002 "lib.rs": "fn one() -> usize { 1 }"
1003 }
1004 },
1005 "project2": {
1006 "README.md": "# project 2",
1007 },
1008 }),
1009 )
1010 .await;
1011
1012 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1013 let (_worktree, _) = project
1014 .update(cx, |project, cx| {
1015 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1016 })
1017 .await
1018 .unwrap();
1019
1020 let (worktree_2, _) = project
1021 .update(cx, |project, cx| {
1022 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1023 })
1024 .await
1025 .unwrap();
1026 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1027
1028 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1029
1030 let (worktree_2, _) = project
1031 .update(cx, |project, cx| {
1032 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1033 })
1034 .await
1035 .unwrap();
1036
1037 cx.run_until_parked();
1038 worktree_2.update(cx, |worktree, _cx| {
1039 assert!(worktree.is_visible());
1040 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1041 assert_eq!(entries.len(), 2);
1042 assert_eq!(
1043 entries[1].path.to_string_lossy().to_string(),
1044 "README.md".to_string()
1045 )
1046 })
1047}
1048
1049#[gpui::test]
1050async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1051 let fs = FakeFs::new(server_cx.executor());
1052 fs.insert_tree(
1053 path!("/code"),
1054 json!({
1055 "project1": {
1056 ".git": {},
1057 "README.md": "# project 1",
1058 "src": {
1059 "lib.rs": "fn one() -> usize { 1 }"
1060 }
1061 },
1062 }),
1063 )
1064 .await;
1065
1066 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1067 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1068 cx.executor().run_until_parked();
1069
1070 let buffer = buffer.await.unwrap();
1071
1072 cx.update(|cx| {
1073 assert_eq!(
1074 buffer.read(cx).text(),
1075 initial_server_settings_content()
1076 .to_string()
1077 .replace("\r\n", "\n")
1078 )
1079 })
1080}
1081
1082#[gpui::test(iterations = 20)]
1083async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1084 let fs = FakeFs::new(server_cx.executor());
1085 fs.insert_tree(
1086 path!("/code"),
1087 json!({
1088 "project1": {
1089 ".git": {},
1090 "README.md": "# project 1",
1091 "src": {
1092 "lib.rs": "fn one() -> usize { 1 }"
1093 }
1094 },
1095 }),
1096 )
1097 .await;
1098
1099 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1100
1101 let (worktree, _) = project
1102 .update(cx, |project, cx| {
1103 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1104 })
1105 .await
1106 .unwrap();
1107
1108 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1109 let buffer = project
1110 .update(cx, |project, cx| {
1111 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1112 })
1113 .await
1114 .unwrap();
1115
1116 buffer.update(cx, |buffer, cx| {
1117 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1118 let ix = buffer.text().find('1').unwrap();
1119 buffer.edit([(ix..ix + 1, "100")], None, cx);
1120 });
1121
1122 let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
1123 client
1124 .update(cx, |client, cx| client.simulate_disconnect(cx))
1125 .detach();
1126
1127 project
1128 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1129 .await
1130 .unwrap();
1131
1132 assert_eq!(
1133 fs.load(path!("/code/project1/src/lib.rs").as_ref())
1134 .await
1135 .unwrap(),
1136 "fn one() -> usize { 100 }"
1137 );
1138}
1139
1140#[gpui::test]
1141async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1142 let fs = FakeFs::new(server_cx.executor());
1143 fs.insert_tree(
1144 "/code",
1145 json!({
1146 "project1": {
1147 ".git": {},
1148 "README.md": "# project 1",
1149 },
1150 }),
1151 )
1152 .await;
1153
1154 let (project, _) = init_test(&fs, cx, server_cx).await;
1155
1156 let (worktree, _) = project
1157 .update(cx, |project, cx| {
1158 project.find_or_create_worktree("/code/project1", true, cx)
1159 })
1160 .await
1161 .unwrap();
1162
1163 cx.run_until_parked();
1164
1165 fs.rename(
1166 &PathBuf::from("/code/project1"),
1167 &PathBuf::from("/code/project2"),
1168 Default::default(),
1169 )
1170 .await
1171 .unwrap();
1172
1173 cx.run_until_parked();
1174 worktree.update(cx, |worktree, _| {
1175 assert_eq!(worktree.root_name(), "project2")
1176 })
1177}
1178
1179#[gpui::test]
1180async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1181 let fs = FakeFs::new(server_cx.executor());
1182 fs.insert_tree(
1183 "/code",
1184 json!({
1185 "project1": {
1186 ".git": {},
1187 "README.md": "# project 1",
1188 },
1189 }),
1190 )
1191 .await;
1192
1193 let (project, _) = init_test(&fs, cx, server_cx).await;
1194 let (worktree, _) = project
1195 .update(cx, |project, cx| {
1196 project.find_or_create_worktree("/code/project1", true, cx)
1197 })
1198 .await
1199 .unwrap();
1200
1201 cx.run_until_parked();
1202
1203 let entry = worktree
1204 .update(cx, |worktree, cx| {
1205 let entry = worktree.entry_for_path("README.md").unwrap();
1206 worktree.rename_entry(entry.id, Path::new("README.rst"), cx)
1207 })
1208 .await
1209 .unwrap()
1210 .into_included()
1211 .unwrap();
1212
1213 cx.run_until_parked();
1214
1215 worktree.update(cx, |worktree, _| {
1216 assert_eq!(worktree.entry_for_path("README.rst").unwrap().id, entry.id)
1217 });
1218}
1219
1220#[gpui::test]
1221async fn test_copy_file_into_remote_project(
1222 cx: &mut TestAppContext,
1223 server_cx: &mut TestAppContext,
1224) {
1225 let remote_fs = FakeFs::new(server_cx.executor());
1226 remote_fs
1227 .insert_tree(
1228 path!("/code"),
1229 json!({
1230 "project1": {
1231 ".git": {},
1232 "README.md": "# project 1",
1233 "src": {
1234 "main.rs": ""
1235 }
1236 },
1237 }),
1238 )
1239 .await;
1240
1241 let (project, _) = init_test(&remote_fs, cx, server_cx).await;
1242 let (worktree, _) = project
1243 .update(cx, |project, cx| {
1244 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1245 })
1246 .await
1247 .unwrap();
1248
1249 cx.run_until_parked();
1250
1251 let local_fs = project
1252 .read_with(cx, |project, _| project.fs().clone())
1253 .as_fake();
1254 local_fs
1255 .insert_tree(
1256 path!("/local-code"),
1257 json!({
1258 "dir1": {
1259 "file1": "file 1 content",
1260 "dir2": {
1261 "file2": "file 2 content",
1262 "dir3": {
1263 "file3": ""
1264 },
1265 "dir4": {}
1266 },
1267 "dir5": {}
1268 },
1269 "file4": "file 4 content"
1270 }),
1271 )
1272 .await;
1273
1274 worktree
1275 .update(cx, |worktree, cx| {
1276 worktree.copy_external_entries(
1277 Path::new("src").into(),
1278 vec![
1279 Path::new(path!("/local-code/dir1/file1")).into(),
1280 Path::new(path!("/local-code/dir1/dir2")).into(),
1281 ],
1282 local_fs.clone(),
1283 cx,
1284 )
1285 })
1286 .await
1287 .unwrap();
1288
1289 assert_eq!(
1290 remote_fs.paths(true),
1291 vec![
1292 PathBuf::from(path!("/")),
1293 PathBuf::from(path!("/code")),
1294 PathBuf::from(path!("/code/project1")),
1295 PathBuf::from(path!("/code/project1/.git")),
1296 PathBuf::from(path!("/code/project1/README.md")),
1297 PathBuf::from(path!("/code/project1/src")),
1298 PathBuf::from(path!("/code/project1/src/dir2")),
1299 PathBuf::from(path!("/code/project1/src/file1")),
1300 PathBuf::from(path!("/code/project1/src/main.rs")),
1301 PathBuf::from(path!("/code/project1/src/dir2/dir3")),
1302 PathBuf::from(path!("/code/project1/src/dir2/dir4")),
1303 PathBuf::from(path!("/code/project1/src/dir2/file2")),
1304 PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
1305 ]
1306 );
1307 assert_eq!(
1308 remote_fs
1309 .load(path!("/code/project1/src/file1").as_ref())
1310 .await
1311 .unwrap(),
1312 "file 1 content"
1313 );
1314 assert_eq!(
1315 remote_fs
1316 .load(path!("/code/project1/src/dir2/file2").as_ref())
1317 .await
1318 .unwrap(),
1319 "file 2 content"
1320 );
1321 assert_eq!(
1322 remote_fs
1323 .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
1324 .await
1325 .unwrap(),
1326 ""
1327 );
1328}
1329
1330// TODO: this test fails on Windows.
1331#[cfg(not(windows))]
1332#[gpui::test]
1333async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1334 let text_2 = "
1335 fn one() -> usize {
1336 1
1337 }
1338 "
1339 .unindent();
1340 let text_1 = "
1341 fn one() -> usize {
1342 0
1343 }
1344 "
1345 .unindent();
1346
1347 let fs = FakeFs::new(server_cx.executor());
1348 fs.insert_tree(
1349 "/code",
1350 json!({
1351 "project1": {
1352 ".git": {},
1353 "src": {
1354 "lib.rs": text_2
1355 },
1356 "README.md": "# project 1",
1357 },
1358 }),
1359 )
1360 .await;
1361 fs.set_index_for_repo(
1362 Path::new("/code/project1/.git"),
1363 &[("src/lib.rs".into(), text_1.clone())],
1364 );
1365 fs.set_head_for_repo(
1366 Path::new("/code/project1/.git"),
1367 &[("src/lib.rs".into(), text_1.clone())],
1368 "deadbeef",
1369 );
1370
1371 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1372 let (worktree, _) = project
1373 .update(cx, |project, cx| {
1374 project.find_or_create_worktree("/code/project1", true, cx)
1375 })
1376 .await
1377 .unwrap();
1378 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1379 cx.executor().run_until_parked();
1380
1381 let buffer = project
1382 .update(cx, |project, cx| {
1383 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1384 })
1385 .await
1386 .unwrap();
1387 let diff = project
1388 .update(cx, |project, cx| {
1389 project.open_uncommitted_diff(buffer.clone(), cx)
1390 })
1391 .await
1392 .unwrap();
1393
1394 diff.read_with(cx, |diff, cx| {
1395 assert_eq!(diff.base_text_string().unwrap(), text_1);
1396 assert_eq!(
1397 diff.secondary_diff()
1398 .unwrap()
1399 .read(cx)
1400 .base_text_string()
1401 .unwrap(),
1402 text_1
1403 );
1404 });
1405
1406 // stage the current buffer's contents
1407 fs.set_index_for_repo(
1408 Path::new("/code/project1/.git"),
1409 &[("src/lib.rs".into(), text_2.clone())],
1410 );
1411
1412 cx.executor().run_until_parked();
1413 diff.read_with(cx, |diff, cx| {
1414 assert_eq!(diff.base_text_string().unwrap(), text_1);
1415 assert_eq!(
1416 diff.secondary_diff()
1417 .unwrap()
1418 .read(cx)
1419 .base_text_string()
1420 .unwrap(),
1421 text_2
1422 );
1423 });
1424
1425 // commit the current buffer's contents
1426 fs.set_head_for_repo(
1427 Path::new("/code/project1/.git"),
1428 &[("src/lib.rs".into(), text_2.clone())],
1429 "deadbeef",
1430 );
1431
1432 cx.executor().run_until_parked();
1433 diff.read_with(cx, |diff, cx| {
1434 assert_eq!(diff.base_text_string().unwrap(), text_2);
1435 assert_eq!(
1436 diff.secondary_diff()
1437 .unwrap()
1438 .read(cx)
1439 .base_text_string()
1440 .unwrap(),
1441 text_2
1442 );
1443 });
1444}
1445
1446// TODO: this test fails on Windows.
1447#[cfg(not(windows))]
1448#[gpui::test]
1449async fn test_remote_git_diffs_when_recv_update_repository_delay(
1450 cx: &mut TestAppContext,
1451 server_cx: &mut TestAppContext,
1452) {
1453 use editor::Editor;
1454 use gpui::VisualContext;
1455 let text_2 = "
1456 fn one() -> usize {
1457 1
1458 }
1459 "
1460 .unindent();
1461 let text_1 = "
1462 fn one() -> usize {
1463 0
1464 }
1465 "
1466 .unindent();
1467
1468 let fs = FakeFs::new(server_cx.executor());
1469 fs.insert_tree(
1470 "/code",
1471 json!({
1472 "project1": {
1473 "src": {
1474 "lib.rs": text_2
1475 },
1476 "README.md": "# project 1",
1477 },
1478 }),
1479 )
1480 .await;
1481
1482 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1483 let (worktree, _) = project
1484 .update(cx, |project, cx| {
1485 project.find_or_create_worktree("/code/project1", true, cx)
1486 })
1487 .await
1488 .unwrap();
1489 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1490 let buffer = project
1491 .update(cx, |project, cx| {
1492 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1493 })
1494 .await
1495 .unwrap();
1496 let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
1497 cx.update(|cx| {
1498 workspace::init_settings(cx);
1499 editor::init_settings(cx);
1500 });
1501 let cx = cx.add_empty_window();
1502 let editor = cx.new_window_entity(|window, cx| {
1503 Editor::for_buffer(buffer, Some(project.clone()), window, cx)
1504 });
1505
1506 // Remote server will send proto::UpdateRepository after the instance of Editor create.
1507 fs.insert_tree(
1508 "/code",
1509 json!({
1510 "project1": {
1511 ".git": {},
1512 },
1513 }),
1514 )
1515 .await;
1516
1517 fs.set_index_for_repo(
1518 Path::new("/code/project1/.git"),
1519 &[("src/lib.rs".into(), text_1.clone())],
1520 );
1521 fs.set_head_for_repo(
1522 Path::new("/code/project1/.git"),
1523 &[("src/lib.rs".into(), text_1.clone())],
1524 "sha",
1525 );
1526
1527 cx.executor().run_until_parked();
1528 let diff = editor
1529 .read_with(cx, |editor, cx| {
1530 editor
1531 .buffer()
1532 .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
1533 })
1534 .unwrap();
1535
1536 diff.read_with(cx, |diff, cx| {
1537 assert_eq!(diff.base_text_string().unwrap(), text_1);
1538 assert_eq!(
1539 diff.secondary_diff()
1540 .unwrap()
1541 .read(cx)
1542 .base_text_string()
1543 .unwrap(),
1544 text_1
1545 );
1546 });
1547
1548 // stage the current buffer's contents
1549 fs.set_index_for_repo(
1550 Path::new("/code/project1/.git"),
1551 &[("src/lib.rs".into(), text_2.clone())],
1552 );
1553
1554 cx.executor().run_until_parked();
1555 diff.read_with(cx, |diff, cx| {
1556 assert_eq!(diff.base_text_string().unwrap(), text_1);
1557 assert_eq!(
1558 diff.secondary_diff()
1559 .unwrap()
1560 .read(cx)
1561 .base_text_string()
1562 .unwrap(),
1563 text_2
1564 );
1565 });
1566
1567 // commit the current buffer's contents
1568 fs.set_head_for_repo(
1569 Path::new("/code/project1/.git"),
1570 &[("src/lib.rs".into(), text_2.clone())],
1571 "sha",
1572 );
1573
1574 cx.executor().run_until_parked();
1575 diff.read_with(cx, |diff, cx| {
1576 assert_eq!(diff.base_text_string().unwrap(), text_2);
1577 assert_eq!(
1578 diff.secondary_diff()
1579 .unwrap()
1580 .read(cx)
1581 .base_text_string()
1582 .unwrap(),
1583 text_2
1584 );
1585 });
1586}
1587
1588#[gpui::test]
1589async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1590 let fs = FakeFs::new(server_cx.executor());
1591 fs.insert_tree(
1592 path!("/code"),
1593 json!({
1594 "project1": {
1595 ".git": {},
1596 "README.md": "# project 1",
1597 },
1598 }),
1599 )
1600 .await;
1601
1602 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1603 let branches = ["main", "dev", "feature-1"];
1604 let branches_set = branches
1605 .iter()
1606 .map(ToString::to_string)
1607 .collect::<HashSet<_>>();
1608 fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1609
1610 let (_worktree, _) = project
1611 .update(cx, |project, cx| {
1612 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1613 })
1614 .await
1615 .unwrap();
1616 // Give the worktree a bit of time to index the file system
1617 cx.run_until_parked();
1618
1619 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1620
1621 let remote_branches = repository
1622 .update(cx, |repository, _| repository.branches())
1623 .await
1624 .unwrap()
1625 .unwrap();
1626
1627 let new_branch = branches[2];
1628
1629 let remote_branches = remote_branches
1630 .into_iter()
1631 .map(|branch| branch.name().to_string())
1632 .collect::<HashSet<_>>();
1633
1634 assert_eq!(&remote_branches, &branches_set);
1635
1636 cx.update(|cx| {
1637 repository.update(cx, |repository, _cx| {
1638 repository.change_branch(new_branch.to_string())
1639 })
1640 })
1641 .await
1642 .unwrap()
1643 .unwrap();
1644
1645 cx.run_until_parked();
1646
1647 let server_branch = server_cx.update(|cx| {
1648 headless_project.update(cx, |headless_project, cx| {
1649 headless_project.git_store.update(cx, |git_store, cx| {
1650 git_store
1651 .repositories()
1652 .values()
1653 .next()
1654 .unwrap()
1655 .read(cx)
1656 .branch
1657 .as_ref()
1658 .unwrap()
1659 .clone()
1660 })
1661 })
1662 });
1663
1664 assert_eq!(server_branch.name(), branches[2]);
1665
1666 // Also try creating a new branch
1667 cx.update(|cx| {
1668 repository.update(cx, |repo, _cx| {
1669 repo.create_branch("totally-new-branch".to_string())
1670 })
1671 })
1672 .await
1673 .unwrap()
1674 .unwrap();
1675
1676 cx.update(|cx| {
1677 repository.update(cx, |repo, _cx| {
1678 repo.change_branch("totally-new-branch".to_string())
1679 })
1680 })
1681 .await
1682 .unwrap()
1683 .unwrap();
1684
1685 cx.run_until_parked();
1686
1687 let server_branch = server_cx.update(|cx| {
1688 headless_project.update(cx, |headless_project, cx| {
1689 headless_project.git_store.update(cx, |git_store, cx| {
1690 git_store
1691 .repositories()
1692 .values()
1693 .next()
1694 .unwrap()
1695 .read(cx)
1696 .branch
1697 .as_ref()
1698 .unwrap()
1699 .clone()
1700 })
1701 })
1702 });
1703
1704 assert_eq!(server_branch.name(), "totally-new-branch");
1705}
1706
1707#[gpui::test]
1708async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1709 let fs = FakeFs::new(server_cx.executor());
1710 fs.insert_tree(
1711 path!("/project"),
1712 json!({
1713 "a.txt": "A",
1714 "b.txt": "B",
1715 }),
1716 )
1717 .await;
1718
1719 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
1720 project
1721 .update(cx, |project, cx| {
1722 project.find_or_create_worktree(path!("/project"), true, cx)
1723 })
1724 .await
1725 .unwrap();
1726
1727 let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
1728 let model = Arc::new(FakeLanguageModel::default());
1729 let request = Arc::new(LanguageModelRequest::default());
1730
1731 let input = ReadFileToolInput {
1732 path: "project/b.txt".into(),
1733 start_line: None,
1734 end_line: None,
1735 };
1736 let exists_result = cx.update(|cx| {
1737 ReadFileTool::run(
1738 Arc::new(ReadFileTool),
1739 serde_json::to_value(input).unwrap(),
1740 request.clone(),
1741 project.clone(),
1742 action_log.clone(),
1743 model.clone(),
1744 None,
1745 cx,
1746 )
1747 });
1748 let output = exists_result.output.await.unwrap().content;
1749 assert_eq!(output, ToolResultContent::Text("B".to_string()));
1750
1751 let input = ReadFileToolInput {
1752 path: "project/c.txt".into(),
1753 start_line: None,
1754 end_line: None,
1755 };
1756 let does_not_exist_result = cx.update(|cx| {
1757 ReadFileTool::run(
1758 Arc::new(ReadFileTool),
1759 serde_json::to_value(input).unwrap(),
1760 request.clone(),
1761 project.clone(),
1762 action_log.clone(),
1763 model.clone(),
1764 None,
1765 cx,
1766 )
1767 });
1768 does_not_exist_result.output.await.unwrap_err();
1769}
1770
1771pub async fn init_test(
1772 server_fs: &Arc<FakeFs>,
1773 cx: &mut TestAppContext,
1774 server_cx: &mut TestAppContext,
1775) -> (Entity<Project>, Entity<HeadlessProject>) {
1776 let server_fs = server_fs.clone();
1777 cx.update(|cx| {
1778 release_channel::init(SemanticVersion::default(), cx);
1779 });
1780 server_cx.update(|cx| {
1781 release_channel::init(SemanticVersion::default(), cx);
1782 });
1783 init_logger();
1784
1785 let (opts, ssh_server_client) = RemoteClient::fake_server(cx, server_cx);
1786 let http_client = Arc::new(BlockedHttpClient);
1787 let node_runtime = NodeRuntime::unavailable();
1788 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1789 let proxy = Arc::new(ExtensionHostProxy::new());
1790 server_cx.update(HeadlessProject::init);
1791 let headless = server_cx.new(|cx| {
1792 client::init_settings(cx);
1793
1794 HeadlessProject::new(
1795 crate::HeadlessAppState {
1796 session: ssh_server_client,
1797 fs: server_fs.clone(),
1798 http_client,
1799 node_runtime,
1800 languages,
1801 extension_host_proxy: proxy,
1802 },
1803 cx,
1804 )
1805 });
1806
1807 let ssh = RemoteClient::fake_client(opts, cx).await;
1808 let project = build_project(ssh, cx);
1809 project
1810 .update(cx, {
1811 let headless = headless.clone();
1812 |_, cx| cx.on_release(|_, _| drop(headless))
1813 })
1814 .detach();
1815 (project, headless)
1816}
1817
1818fn init_logger() {
1819 zlog::init_test();
1820}
1821
1822fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1823 cx.update(|cx| {
1824 if !cx.has_global::<SettingsStore>() {
1825 let settings_store = SettingsStore::test(cx);
1826 cx.set_global(settings_store);
1827 }
1828 });
1829
1830 let client = cx.update(|cx| {
1831 Client::new(
1832 Arc::new(FakeSystemClock::new()),
1833 FakeHttpClient::with_404_response(),
1834 cx,
1835 )
1836 });
1837
1838 let node = NodeRuntime::unavailable();
1839 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1840 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1841 let fs = FakeFs::new(cx.executor());
1842
1843 cx.update(|cx| {
1844 Project::init(&client, cx);
1845 language::init(cx);
1846 });
1847
1848 cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, cx))
1849}