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