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