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