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