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