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