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