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