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