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