1/// todo(windows)
2/// The tests in this file assume that server_cx is running on Windows too.
3/// We neead to find a way to test Windows-Non-Windows interactions.
4use crate::headless_project::HeadlessProject;
5use assistant_tool::{Tool as _, ToolResultContent};
6use assistant_tools::{ReadFileTool, ReadFileToolInput};
7use client::{Client, UserStore};
8use clock::FakeSystemClock;
9use collections::{HashMap, HashSet};
10use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel};
11
12use extension::ExtensionHostProxy;
13use fs::{FakeFs, Fs};
14use gpui::{AppContext as _, Entity, SemanticVersion, TestAppContext};
15use http_client::{BlockedHttpClient, FakeHttpClient};
16use language::{
17 Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
18 language_settings::{AllLanguageSettings, language_settings},
19};
20use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName};
21use node_runtime::NodeRuntime;
22use project::{
23 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};
35#[cfg(not(windows))]
36use unindent::Unindent as _;
37use util::{path, rel_path::rel_path};
38
39#[gpui::test]
40async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
41 let fs = FakeFs::new(server_cx.executor());
42 fs.insert_tree(
43 path!("/code"),
44 json!({
45 "project1": {
46 ".git": {},
47 "README.md": "# project 1",
48 "src": {
49 "lib.rs": "fn one() -> usize { 1 }"
50 }
51 },
52 "project2": {
53 "README.md": "# project 2",
54 },
55 }),
56 )
57 .await;
58 fs.set_index_for_repo(
59 Path::new(path!("/code/project1/.git")),
60 &[("src/lib.rs", "fn one() -> usize { 0 }".into())],
61 );
62
63 let (project, _headless) = init_test(&fs, cx, server_cx).await;
64 let (worktree, _) = project
65 .update(cx, |project, cx| {
66 project.find_or_create_worktree(path!("/code/project1"), true, cx)
67 })
68 .await
69 .unwrap();
70
71 // The client sees the worktree's contents.
72 cx.executor().run_until_parked();
73 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
74 worktree.update(cx, |worktree, _cx| {
75 assert_eq!(
76 worktree.paths().collect::<Vec<_>>(),
77 vec![
78 rel_path("README.md"),
79 rel_path("src"),
80 rel_path("src/lib.rs"),
81 ]
82 );
83 });
84
85 // The user opens a buffer in the remote worktree. The buffer's
86 // contents are loaded from the remote filesystem.
87 let buffer = project
88 .update(cx, |project, cx| {
89 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
90 })
91 .await
92 .unwrap();
93 let diff = project
94 .update(cx, |project, cx| {
95 project.open_unstaged_diff(buffer.clone(), cx)
96 })
97 .await
98 .unwrap();
99
100 diff.update(cx, |diff, _| {
101 assert_eq!(diff.base_text_string().unwrap(), "fn one() -> usize { 0 }");
102 });
103
104 buffer.update(cx, |buffer, cx| {
105 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
106 let ix = buffer.text().find('1').unwrap();
107 buffer.edit([(ix..ix + 1, "100")], None, cx);
108 });
109
110 // The user saves the buffer. The new contents are written to the
111 // remote filesystem.
112 project
113 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
114 .await
115 .unwrap();
116 assert_eq!(
117 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
118 "fn one() -> usize { 100 }"
119 );
120
121 // A new file is created in the remote filesystem. The user
122 // sees the new file.
123 fs.save(
124 path!("/code/project1/src/main.rs").as_ref(),
125 &"fn main() {}".into(),
126 Default::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(server_id, Some(progress_token.into()), cx)
716 });
717
718 cx.executor().run_until_parked();
719
720 // Verify the cancellation was received on the server side
721 let cancel_notification = fake_lsp
722 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
723 .await;
724 assert_eq!(
725 cancel_notification.token,
726 lsp::NumberOrString::String(progress_token.into())
727 );
728 }
729}
730
731#[gpui::test]
732async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
733 let fs = FakeFs::new(server_cx.executor());
734 fs.insert_tree(
735 path!("/code"),
736 json!({
737 "project1": {
738 ".git": {},
739 "README.md": "# project 1",
740 "src": {
741 "lib.rs": "fn one() -> usize { 1 }"
742 }
743 },
744 }),
745 )
746 .await;
747
748 let (project, _headless) = init_test(&fs, cx, server_cx).await;
749 let (worktree, _) = project
750 .update(cx, |project, cx| {
751 project.find_or_create_worktree(path!("/code/project1"), true, cx)
752 })
753 .await
754 .unwrap();
755
756 let worktree_id = cx.update(|cx| worktree.read(cx).id());
757
758 let buffer = project
759 .update(cx, |project, cx| {
760 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
761 })
762 .await
763 .unwrap();
764
765 fs.save(
766 &PathBuf::from(path!("/code/project1/src/lib.rs")),
767 &("bangles".to_string().into()),
768 LineEnding::Unix,
769 )
770 .await
771 .unwrap();
772
773 cx.run_until_parked();
774
775 buffer.update(cx, |buffer, cx| {
776 assert_eq!(buffer.text(), "bangles");
777 buffer.edit([(0..0, "a")], None, cx);
778 });
779
780 fs.save(
781 &PathBuf::from(path!("/code/project1/src/lib.rs")),
782 &("bloop".to_string().into()),
783 LineEnding::Unix,
784 )
785 .await
786 .unwrap();
787
788 cx.run_until_parked();
789 cx.update(|cx| {
790 assert!(buffer.read(cx).has_conflict());
791 });
792
793 project
794 .update(cx, |project, cx| {
795 project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
796 })
797 .await
798 .unwrap();
799 cx.run_until_parked();
800
801 cx.update(|cx| {
802 assert!(!buffer.read(cx).has_conflict());
803 });
804}
805
806#[gpui::test]
807async fn test_remote_resolve_path_in_buffer(
808 cx: &mut TestAppContext,
809 server_cx: &mut TestAppContext,
810) {
811 let fs = FakeFs::new(server_cx.executor());
812 // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
813 fs.insert_tree(
814 path!("/code"),
815 json!({
816 "project1": {
817 ".git": {},
818 "README.md": "# project 1",
819 "src": {
820 "lib.rs": "fn one() -> usize { 1 }"
821 }
822 },
823 "project2": {
824 ".git": {},
825 "README.md": "# project 2",
826 "src": {
827 "lib.rs": "fn two() -> usize { 2 }"
828 }
829 }
830 }),
831 )
832 .await;
833
834 let (project, _headless) = init_test(&fs, cx, server_cx).await;
835
836 let _ = project
837 .update(cx, |project, cx| {
838 project.find_or_create_worktree(path!("/code/project1"), true, cx)
839 })
840 .await
841 .unwrap();
842
843 let (worktree2, _) = project
844 .update(cx, |project, cx| {
845 project.find_or_create_worktree(path!("/code/project2"), true, cx)
846 })
847 .await
848 .unwrap();
849
850 let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
851
852 cx.run_until_parked();
853
854 let buffer2 = project
855 .update(cx, |project, cx| {
856 project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx)
857 })
858 .await
859 .unwrap();
860
861 let path = project
862 .update(cx, |project, cx| {
863 project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
864 })
865 .await
866 .unwrap();
867 assert!(path.is_file());
868 assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
869
870 let path = project
871 .update(cx, |project, cx| {
872 project.resolve_path_in_buffer("../README.md", &buffer2, cx)
873 })
874 .await
875 .unwrap();
876 assert!(path.is_file());
877 assert_eq!(
878 path.project_path().unwrap().clone(),
879 (worktree2_id, rel_path("README.md")).into()
880 );
881
882 let path = project
883 .update(cx, |project, cx| {
884 project.resolve_path_in_buffer("../src", &buffer2, cx)
885 })
886 .await
887 .unwrap();
888 assert_eq!(
889 path.project_path().unwrap().clone(),
890 (worktree2_id, rel_path("src")).into()
891 );
892 assert!(path.is_dir());
893}
894
895#[gpui::test]
896async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
897 let fs = FakeFs::new(server_cx.executor());
898 fs.insert_tree(
899 path!("/code"),
900 json!({
901 "project1": {
902 ".git": {},
903 "README.md": "# project 1",
904 "src": {
905 "lib.rs": "fn one() -> usize { 1 }"
906 }
907 },
908 }),
909 )
910 .await;
911
912 let (project, _headless) = init_test(&fs, cx, server_cx).await;
913
914 let path = project
915 .update(cx, |project, cx| {
916 project.resolve_abs_path(path!("/code/project1/README.md"), cx)
917 })
918 .await
919 .unwrap();
920
921 assert!(path.is_file());
922 assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
923
924 let path = project
925 .update(cx, |project, cx| {
926 project.resolve_abs_path(path!("/code/project1/src"), cx)
927 })
928 .await
929 .unwrap();
930
931 assert!(path.is_dir());
932 assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
933
934 let path = project
935 .update(cx, |project, cx| {
936 project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
937 })
938 .await;
939 assert!(path.is_none());
940}
941
942#[gpui::test(iterations = 10)]
943async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
944 let fs = FakeFs::new(server_cx.executor());
945 fs.insert_tree(
946 "/code",
947 json!({
948 "project1": {
949 ".git": {},
950 "README.md": "# project 1",
951 "src": {
952 "lib.rs": "fn one() -> usize { 1 }"
953 }
954 },
955 }),
956 )
957 .await;
958
959 let (project, _headless) = init_test(&fs, cx, server_cx).await;
960 let (worktree, _) = project
961 .update(cx, |project, cx| {
962 project.find_or_create_worktree("/code/project1", true, cx)
963 })
964 .await
965 .unwrap();
966 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
967
968 // Open a buffer on the client but cancel after a random amount of time.
969 let buffer = project.update(cx, |p, cx| {
970 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
971 });
972 cx.executor().simulate_random_delay().await;
973 drop(buffer);
974
975 // Try opening the same buffer again as the client, and ensure we can
976 // still do it despite the cancellation above.
977 let buffer = project
978 .update(cx, |p, cx| {
979 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
980 })
981 .await
982 .unwrap();
983
984 buffer.read_with(cx, |buf, _| {
985 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
986 });
987}
988
989#[gpui::test]
990async fn test_adding_then_removing_then_adding_worktrees(
991 cx: &mut TestAppContext,
992 server_cx: &mut TestAppContext,
993) {
994 let fs = FakeFs::new(server_cx.executor());
995 fs.insert_tree(
996 path!("/code"),
997 json!({
998 "project1": {
999 ".git": {},
1000 "README.md": "# project 1",
1001 "src": {
1002 "lib.rs": "fn one() -> usize { 1 }"
1003 }
1004 },
1005 "project2": {
1006 "README.md": "# project 2",
1007 },
1008 }),
1009 )
1010 .await;
1011
1012 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1013 let (_worktree, _) = project
1014 .update(cx, |project, cx| {
1015 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1016 })
1017 .await
1018 .unwrap();
1019
1020 let (worktree_2, _) = project
1021 .update(cx, |project, cx| {
1022 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1023 })
1024 .await
1025 .unwrap();
1026 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1027
1028 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1029
1030 let (worktree_2, _) = project
1031 .update(cx, |project, cx| {
1032 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1033 })
1034 .await
1035 .unwrap();
1036
1037 cx.run_until_parked();
1038 worktree_2.update(cx, |worktree, _cx| {
1039 assert!(worktree.is_visible());
1040 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1041 assert_eq!(entries.len(), 2);
1042 assert_eq!(entries[1].path.as_unix_str(), "README.md")
1043 })
1044}
1045
1046#[gpui::test]
1047async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1048 let fs = FakeFs::new(server_cx.executor());
1049 fs.insert_tree(
1050 path!("/code"),
1051 json!({
1052 "project1": {
1053 ".git": {},
1054 "README.md": "# project 1",
1055 "src": {
1056 "lib.rs": "fn one() -> usize { 1 }"
1057 }
1058 },
1059 }),
1060 )
1061 .await;
1062
1063 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1064 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1065 cx.executor().run_until_parked();
1066
1067 let buffer = buffer.await.unwrap();
1068
1069 cx.update(|cx| {
1070 assert_eq!(
1071 buffer.read(cx).text(),
1072 initial_server_settings_content()
1073 .to_string()
1074 .replace("\r\n", "\n")
1075 )
1076 })
1077}
1078
1079#[gpui::test(iterations = 20)]
1080async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1081 let fs = FakeFs::new(server_cx.executor());
1082 fs.insert_tree(
1083 path!("/code"),
1084 json!({
1085 "project1": {
1086 ".git": {},
1087 "README.md": "# project 1",
1088 "src": {
1089 "lib.rs": "fn one() -> usize { 1 }"
1090 }
1091 },
1092 }),
1093 )
1094 .await;
1095
1096 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1097
1098 let (worktree, _) = project
1099 .update(cx, |project, cx| {
1100 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1101 })
1102 .await
1103 .unwrap();
1104
1105 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1106 let buffer = project
1107 .update(cx, |project, cx| {
1108 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1109 })
1110 .await
1111 .unwrap();
1112
1113 buffer.update(cx, |buffer, cx| {
1114 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1115 let ix = buffer.text().find('1').unwrap();
1116 buffer.edit([(ix..ix + 1, "100")], None, cx);
1117 });
1118
1119 let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
1120 client
1121 .update(cx, |client, cx| client.simulate_disconnect(cx))
1122 .detach();
1123
1124 project
1125 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1126 .await
1127 .unwrap();
1128
1129 assert_eq!(
1130 fs.load(path!("/code/project1/src/lib.rs").as_ref())
1131 .await
1132 .unwrap(),
1133 "fn one() -> usize { 100 }"
1134 );
1135}
1136
1137#[gpui::test]
1138async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1139 let fs = FakeFs::new(server_cx.executor());
1140 fs.insert_tree(
1141 "/code",
1142 json!({
1143 "project1": {
1144 ".git": {},
1145 "README.md": "# project 1",
1146 },
1147 }),
1148 )
1149 .await;
1150
1151 let (project, _) = init_test(&fs, cx, server_cx).await;
1152
1153 let (worktree, _) = project
1154 .update(cx, |project, cx| {
1155 project.find_or_create_worktree("/code/project1", true, cx)
1156 })
1157 .await
1158 .unwrap();
1159
1160 cx.run_until_parked();
1161
1162 fs.rename(
1163 &PathBuf::from("/code/project1"),
1164 &PathBuf::from("/code/project2"),
1165 Default::default(),
1166 )
1167 .await
1168 .unwrap();
1169
1170 cx.run_until_parked();
1171 worktree.update(cx, |worktree, _| {
1172 assert_eq!(worktree.root_name(), "project2")
1173 })
1174}
1175
1176#[gpui::test]
1177async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1178 let fs = FakeFs::new(server_cx.executor());
1179 fs.insert_tree(
1180 "/code",
1181 json!({
1182 "project1": {
1183 ".git": {},
1184 "README.md": "# project 1",
1185 },
1186 }),
1187 )
1188 .await;
1189
1190 let (project, _) = init_test(&fs, cx, server_cx).await;
1191 let (worktree, _) = project
1192 .update(cx, |project, cx| {
1193 project.find_or_create_worktree("/code/project1", true, cx)
1194 })
1195 .await
1196 .unwrap();
1197
1198 cx.run_until_parked();
1199
1200 let entry = project
1201 .update(cx, |project, cx| {
1202 let worktree = worktree.read(cx);
1203 let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
1204 project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
1205 })
1206 .await
1207 .unwrap()
1208 .into_included()
1209 .unwrap();
1210
1211 cx.run_until_parked();
1212
1213 worktree.update(cx, |worktree, _| {
1214 assert_eq!(
1215 worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
1216 entry.id
1217 )
1218 });
1219}
1220
1221#[gpui::test]
1222async fn test_copy_file_into_remote_project(
1223 cx: &mut TestAppContext,
1224 server_cx: &mut TestAppContext,
1225) {
1226 let remote_fs = FakeFs::new(server_cx.executor());
1227 remote_fs
1228 .insert_tree(
1229 path!("/code"),
1230 json!({
1231 "project1": {
1232 ".git": {},
1233 "README.md": "# project 1",
1234 "src": {
1235 "main.rs": ""
1236 }
1237 },
1238 }),
1239 )
1240 .await;
1241
1242 let (project, _) = init_test(&remote_fs, cx, server_cx).await;
1243 let (worktree, _) = project
1244 .update(cx, |project, cx| {
1245 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1246 })
1247 .await
1248 .unwrap();
1249
1250 cx.run_until_parked();
1251
1252 let local_fs = project
1253 .read_with(cx, |project, _| project.fs().clone())
1254 .as_fake();
1255 local_fs
1256 .insert_tree(
1257 path!("/local-code"),
1258 json!({
1259 "dir1": {
1260 "file1": "file 1 content",
1261 "dir2": {
1262 "file2": "file 2 content",
1263 "dir3": {
1264 "file3": ""
1265 },
1266 "dir4": {}
1267 },
1268 "dir5": {}
1269 },
1270 "file4": "file 4 content"
1271 }),
1272 )
1273 .await;
1274
1275 worktree
1276 .update(cx, |worktree, cx| {
1277 worktree.copy_external_entries(
1278 rel_path("src").into(),
1279 vec![
1280 Path::new(path!("/local-code/dir1/file1")).into(),
1281 Path::new(path!("/local-code/dir1/dir2")).into(),
1282 ],
1283 local_fs.clone(),
1284 cx,
1285 )
1286 })
1287 .await
1288 .unwrap();
1289
1290 assert_eq!(
1291 remote_fs.paths(true),
1292 vec![
1293 PathBuf::from(path!("/")),
1294 PathBuf::from(path!("/code")),
1295 PathBuf::from(path!("/code/project1")),
1296 PathBuf::from(path!("/code/project1/.git")),
1297 PathBuf::from(path!("/code/project1/README.md")),
1298 PathBuf::from(path!("/code/project1/src")),
1299 PathBuf::from(path!("/code/project1/src/dir2")),
1300 PathBuf::from(path!("/code/project1/src/file1")),
1301 PathBuf::from(path!("/code/project1/src/main.rs")),
1302 PathBuf::from(path!("/code/project1/src/dir2/dir3")),
1303 PathBuf::from(path!("/code/project1/src/dir2/dir4")),
1304 PathBuf::from(path!("/code/project1/src/dir2/file2")),
1305 PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
1306 ]
1307 );
1308 assert_eq!(
1309 remote_fs
1310 .load(path!("/code/project1/src/file1").as_ref())
1311 .await
1312 .unwrap(),
1313 "file 1 content"
1314 );
1315 assert_eq!(
1316 remote_fs
1317 .load(path!("/code/project1/src/dir2/file2").as_ref())
1318 .await
1319 .unwrap(),
1320 "file 2 content"
1321 );
1322 assert_eq!(
1323 remote_fs
1324 .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
1325 .await
1326 .unwrap(),
1327 ""
1328 );
1329}
1330
1331// TODO: this test fails on Windows.
1332#[cfg(not(windows))]
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// TODO: this test fails on Windows.
1448#[cfg(not(windows))]
1449#[gpui::test]
1450async fn test_remote_git_diffs_when_recv_update_repository_delay(
1451 cx: &mut TestAppContext,
1452 server_cx: &mut TestAppContext,
1453) {
1454 use editor::Editor;
1455 use gpui::VisualContext;
1456 let text_2 = "
1457 fn one() -> usize {
1458 1
1459 }
1460 "
1461 .unindent();
1462 let text_1 = "
1463 fn one() -> usize {
1464 0
1465 }
1466 "
1467 .unindent();
1468
1469 let fs = FakeFs::new(server_cx.executor());
1470 fs.insert_tree(
1471 "/code",
1472 json!({
1473 "project1": {
1474 "src": {
1475 "lib.rs": text_2
1476 },
1477 "README.md": "# project 1",
1478 },
1479 }),
1480 )
1481 .await;
1482
1483 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1484 let (worktree, _) = project
1485 .update(cx, |project, cx| {
1486 project.find_or_create_worktree("/code/project1", true, cx)
1487 })
1488 .await
1489 .unwrap();
1490 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1491 let buffer = project
1492 .update(cx, |project, cx| {
1493 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1494 })
1495 .await
1496 .unwrap();
1497 let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
1498 cx.update(|cx| {
1499 workspace::init_settings(cx);
1500 editor::init_settings(cx);
1501 });
1502 let cx = cx.add_empty_window();
1503 let editor = cx.new_window_entity(|window, cx| {
1504 Editor::for_buffer(buffer, Some(project.clone()), window, cx)
1505 });
1506
1507 // Remote server will send proto::UpdateRepository after the instance of Editor create.
1508 fs.insert_tree(
1509 "/code",
1510 json!({
1511 "project1": {
1512 ".git": {},
1513 },
1514 }),
1515 )
1516 .await;
1517
1518 fs.set_index_for_repo(
1519 Path::new("/code/project1/.git"),
1520 &[("src/lib.rs", text_1.clone())],
1521 );
1522 fs.set_head_for_repo(
1523 Path::new("/code/project1/.git"),
1524 &[("src/lib.rs", text_1.clone())],
1525 "sha",
1526 );
1527
1528 cx.executor().run_until_parked();
1529 let diff = editor
1530 .read_with(cx, |editor, cx| {
1531 editor
1532 .buffer()
1533 .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
1534 })
1535 .unwrap();
1536
1537 diff.read_with(cx, |diff, cx| {
1538 assert_eq!(diff.base_text_string().unwrap(), text_1);
1539 assert_eq!(
1540 diff.secondary_diff()
1541 .unwrap()
1542 .read(cx)
1543 .base_text_string()
1544 .unwrap(),
1545 text_1
1546 );
1547 });
1548
1549 // stage the current buffer's contents
1550 fs.set_index_for_repo(
1551 Path::new("/code/project1/.git"),
1552 &[("src/lib.rs", text_2.clone())],
1553 );
1554
1555 cx.executor().run_until_parked();
1556 diff.read_with(cx, |diff, cx| {
1557 assert_eq!(diff.base_text_string().unwrap(), text_1);
1558 assert_eq!(
1559 diff.secondary_diff()
1560 .unwrap()
1561 .read(cx)
1562 .base_text_string()
1563 .unwrap(),
1564 text_2
1565 );
1566 });
1567
1568 // commit the current buffer's contents
1569 fs.set_head_for_repo(
1570 Path::new("/code/project1/.git"),
1571 &[("src/lib.rs", text_2.clone())],
1572 "sha",
1573 );
1574
1575 cx.executor().run_until_parked();
1576 diff.read_with(cx, |diff, cx| {
1577 assert_eq!(diff.base_text_string().unwrap(), text_2);
1578 assert_eq!(
1579 diff.secondary_diff()
1580 .unwrap()
1581 .read(cx)
1582 .base_text_string()
1583 .unwrap(),
1584 text_2
1585 );
1586 });
1587}
1588
1589#[gpui::test]
1590async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1591 let fs = FakeFs::new(server_cx.executor());
1592 fs.insert_tree(
1593 path!("/code"),
1594 json!({
1595 "project1": {
1596 ".git": {},
1597 "README.md": "# project 1",
1598 },
1599 }),
1600 )
1601 .await;
1602
1603 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1604 let branches = ["main", "dev", "feature-1"];
1605 let branches_set = branches
1606 .iter()
1607 .map(ToString::to_string)
1608 .collect::<HashSet<_>>();
1609 fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1610
1611 let (_worktree, _) = project
1612 .update(cx, |project, cx| {
1613 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1614 })
1615 .await
1616 .unwrap();
1617 // Give the worktree a bit of time to index the file system
1618 cx.run_until_parked();
1619
1620 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1621
1622 let remote_branches = repository
1623 .update(cx, |repository, _| repository.branches())
1624 .await
1625 .unwrap()
1626 .unwrap();
1627
1628 let new_branch = branches[2];
1629
1630 let remote_branches = remote_branches
1631 .into_iter()
1632 .map(|branch| branch.name().to_string())
1633 .collect::<HashSet<_>>();
1634
1635 assert_eq!(&remote_branches, &branches_set);
1636
1637 cx.update(|cx| {
1638 repository.update(cx, |repository, _cx| {
1639 repository.change_branch(new_branch.to_string())
1640 })
1641 })
1642 .await
1643 .unwrap()
1644 .unwrap();
1645
1646 cx.run_until_parked();
1647
1648 let server_branch = server_cx.update(|cx| {
1649 headless_project.update(cx, |headless_project, cx| {
1650 headless_project.git_store.update(cx, |git_store, cx| {
1651 git_store
1652 .repositories()
1653 .values()
1654 .next()
1655 .unwrap()
1656 .read(cx)
1657 .branch
1658 .as_ref()
1659 .unwrap()
1660 .clone()
1661 })
1662 })
1663 });
1664
1665 assert_eq!(server_branch.name(), branches[2]);
1666
1667 // Also try creating a new branch
1668 cx.update(|cx| {
1669 repository.update(cx, |repo, _cx| {
1670 repo.create_branch("totally-new-branch".to_string())
1671 })
1672 })
1673 .await
1674 .unwrap()
1675 .unwrap();
1676
1677 cx.update(|cx| {
1678 repository.update(cx, |repo, _cx| {
1679 repo.change_branch("totally-new-branch".to_string())
1680 })
1681 })
1682 .await
1683 .unwrap()
1684 .unwrap();
1685
1686 cx.run_until_parked();
1687
1688 let server_branch = server_cx.update(|cx| {
1689 headless_project.update(cx, |headless_project, cx| {
1690 headless_project.git_store.update(cx, |git_store, cx| {
1691 git_store
1692 .repositories()
1693 .values()
1694 .next()
1695 .unwrap()
1696 .read(cx)
1697 .branch
1698 .as_ref()
1699 .unwrap()
1700 .clone()
1701 })
1702 })
1703 });
1704
1705 assert_eq!(server_branch.name(), "totally-new-branch");
1706}
1707
1708#[gpui::test]
1709async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1710 let fs = FakeFs::new(server_cx.executor());
1711 fs.insert_tree(
1712 path!("/project"),
1713 json!({
1714 "a.txt": "A",
1715 "b.txt": "B",
1716 }),
1717 )
1718 .await;
1719
1720 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
1721 project
1722 .update(cx, |project, cx| {
1723 project.find_or_create_worktree(path!("/project"), true, cx)
1724 })
1725 .await
1726 .unwrap();
1727
1728 let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
1729 let model = Arc::new(FakeLanguageModel::default());
1730 let request = Arc::new(LanguageModelRequest::default());
1731
1732 let input = ReadFileToolInput {
1733 path: "project/b.txt".into(),
1734 start_line: None,
1735 end_line: None,
1736 };
1737 let exists_result = cx.update(|cx| {
1738 ReadFileTool::run(
1739 Arc::new(ReadFileTool),
1740 serde_json::to_value(input).unwrap(),
1741 request.clone(),
1742 project.clone(),
1743 action_log.clone(),
1744 model.clone(),
1745 None,
1746 cx,
1747 )
1748 });
1749 let output = exists_result.output.await.unwrap().content;
1750 assert_eq!(output, ToolResultContent::Text("B".to_string()));
1751
1752 let input = ReadFileToolInput {
1753 path: "project/c.txt".into(),
1754 start_line: None,
1755 end_line: None,
1756 };
1757 let does_not_exist_result = cx.update(|cx| {
1758 ReadFileTool::run(
1759 Arc::new(ReadFileTool),
1760 serde_json::to_value(input).unwrap(),
1761 request.clone(),
1762 project.clone(),
1763 action_log.clone(),
1764 model.clone(),
1765 None,
1766 cx,
1767 )
1768 });
1769 does_not_exist_result.output.await.unwrap_err();
1770}
1771
1772#[gpui::test]
1773async fn test_remote_external_agent_server(
1774 cx: &mut TestAppContext,
1775 server_cx: &mut TestAppContext,
1776) {
1777 let fs = FakeFs::new(server_cx.executor());
1778 fs.insert_tree(path!("/project"), json!({})).await;
1779
1780 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
1781 project
1782 .update(cx, |project, cx| {
1783 project.find_or_create_worktree(path!("/project"), true, cx)
1784 })
1785 .await
1786 .unwrap();
1787 let names = project.update(cx, |project, cx| {
1788 project
1789 .agent_server_store()
1790 .read(cx)
1791 .external_agents()
1792 .map(|name| name.to_string())
1793 .collect::<Vec<_>>()
1794 });
1795 pretty_assertions::assert_eq!(names, ["gemini", "claude"]);
1796 server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
1797 settings_store
1798 .set_server_settings(
1799 &json!({
1800 "agent_servers": {
1801 "foo": {
1802 "command": "foo-cli",
1803 "args": ["--flag"],
1804 "env": {
1805 "VAR": "val"
1806 }
1807 }
1808 }
1809 })
1810 .to_string(),
1811 cx,
1812 )
1813 .unwrap();
1814 });
1815 server_cx.run_until_parked();
1816 cx.run_until_parked();
1817 let names = project.update(cx, |project, cx| {
1818 project
1819 .agent_server_store()
1820 .read(cx)
1821 .external_agents()
1822 .map(|name| name.to_string())
1823 .collect::<Vec<_>>()
1824 });
1825 pretty_assertions::assert_eq!(names, ["gemini", "foo", "claude"]);
1826 let (command, root, login) = project
1827 .update(cx, |project, cx| {
1828 project.agent_server_store().update(cx, |store, cx| {
1829 store
1830 .get_external_agent(&"foo".into())
1831 .unwrap()
1832 .get_command(
1833 None,
1834 HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
1835 None,
1836 None,
1837 &mut cx.to_async(),
1838 )
1839 })
1840 })
1841 .await
1842 .unwrap();
1843 assert_eq!(
1844 command,
1845 AgentServerCommand {
1846 path: "ssh".into(),
1847 args: vec!["foo-cli".into(), "--flag".into()],
1848 env: Some(HashMap::from_iter([
1849 ("VAR".into(), "val".into()),
1850 ("OTHER_VAR".into(), "other-val".into())
1851 ]))
1852 }
1853 );
1854 assert_eq!(&PathBuf::from(root), paths::home_dir());
1855 assert!(login.is_none());
1856}
1857
1858pub async fn init_test(
1859 server_fs: &Arc<FakeFs>,
1860 cx: &mut TestAppContext,
1861 server_cx: &mut TestAppContext,
1862) -> (Entity<Project>, Entity<HeadlessProject>) {
1863 let server_fs = server_fs.clone();
1864 cx.update(|cx| {
1865 release_channel::init(SemanticVersion::default(), cx);
1866 });
1867 server_cx.update(|cx| {
1868 release_channel::init(SemanticVersion::default(), cx);
1869 });
1870 init_logger();
1871
1872 let (opts, ssh_server_client) = RemoteClient::fake_server(cx, server_cx);
1873 let http_client = Arc::new(BlockedHttpClient);
1874 let node_runtime = NodeRuntime::unavailable();
1875 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1876 let proxy = Arc::new(ExtensionHostProxy::new());
1877 server_cx.update(HeadlessProject::init);
1878 let headless = server_cx.new(|cx| {
1879 client::init_settings(cx);
1880
1881 HeadlessProject::new(
1882 crate::HeadlessAppState {
1883 session: ssh_server_client,
1884 fs: server_fs.clone(),
1885 http_client,
1886 node_runtime,
1887 languages,
1888 extension_host_proxy: proxy,
1889 },
1890 cx,
1891 )
1892 });
1893
1894 let ssh = RemoteClient::fake_client(opts, cx).await;
1895 let project = build_project(ssh, cx);
1896 project
1897 .update(cx, {
1898 let headless = headless.clone();
1899 |_, cx| cx.on_release(|_, _| drop(headless))
1900 })
1901 .detach();
1902 (project, headless)
1903}
1904
1905fn init_logger() {
1906 zlog::init_test();
1907}
1908
1909fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1910 cx.update(|cx| {
1911 if !cx.has_global::<SettingsStore>() {
1912 let settings_store = SettingsStore::test(cx);
1913 cx.set_global(settings_store);
1914 }
1915 });
1916
1917 let client = cx.update(|cx| {
1918 Client::new(
1919 Arc::new(FakeSystemClock::new()),
1920 FakeHttpClient::with_404_response(),
1921 cx,
1922 )
1923 });
1924
1925 let node = NodeRuntime::unavailable();
1926 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1927 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1928 let fs = FakeFs::new(cx.executor());
1929
1930 cx.update(|cx| {
1931 Project::init(&client, cx);
1932 language::init(cx);
1933 });
1934
1935 cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, cx))
1936}