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