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