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