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