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