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