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