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 client::{Client, UserStore};
6use clock::FakeSystemClock;
7use extension::ExtensionHostProxy;
8use fs::{FakeFs, Fs};
9use gpui::{AppContext as _, Entity, SemanticVersion, TestAppContext};
10use http_client::{BlockedHttpClient, FakeHttpClient};
11use language::{
12 language_settings::{language_settings, AllLanguageSettings},
13 Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
14};
15use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName};
16use node_runtime::NodeRuntime;
17use project::{
18 search::{SearchQuery, SearchResult},
19 Project, ProjectPath,
20};
21use remote::SshRemoteClient;
22use serde_json::json;
23use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
24use smol::stream::StreamExt;
25use std::{
26 collections::HashSet,
27 path::{Path, PathBuf},
28 sync::Arc,
29};
30use unindent::Unindent as _;
31use util::{path, separator};
32
33#[gpui::test]
34async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
35 let fs = FakeFs::new(server_cx.executor());
36 fs.insert_tree(
37 path!("/code"),
38 json!({
39 "project1": {
40 ".git": {},
41 "README.md": "# project 1",
42 "src": {
43 "lib.rs": "fn one() -> usize { 1 }"
44 }
45 },
46 "project2": {
47 "README.md": "# project 2",
48 },
49 }),
50 )
51 .await;
52 fs.set_index_for_repo(
53 Path::new(path!("/code/project1/.git")),
54 &[("src/lib.rs".into(), "fn one() -> usize { 0 }".into())],
55 );
56
57 let (project, _headless) = init_test(&fs, cx, server_cx).await;
58 let (worktree, _) = project
59 .update(cx, |project, cx| {
60 project.find_or_create_worktree(path!("/code/project1"), true, cx)
61 })
62 .await
63 .unwrap();
64
65 // The client sees the worktree's contents.
66 cx.executor().run_until_parked();
67 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
68 worktree.update(cx, |worktree, _cx| {
69 assert_eq!(
70 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
71 vec![
72 Path::new("README.md"),
73 Path::new("src"),
74 Path::new("src/lib.rs"),
75 ]
76 );
77 });
78
79 // The user opens a buffer in the remote worktree. The buffer's
80 // contents are loaded from the remote filesystem.
81 let buffer = project
82 .update(cx, |project, cx| {
83 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
84 })
85 .await
86 .unwrap();
87 let diff = project
88 .update(cx, |project, cx| {
89 project.open_unstaged_diff(buffer.clone(), cx)
90 })
91 .await
92 .unwrap();
93
94 diff.update(cx, |diff, _| {
95 assert_eq!(diff.base_text_string().unwrap(), "fn one() -> usize { 0 }");
96 });
97
98 buffer.update(cx, |buffer, cx| {
99 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
100 let ix = buffer.text().find('1').unwrap();
101 buffer.edit([(ix..ix + 1, "100")], None, cx);
102 });
103
104 // The user saves the buffer. The new contents are written to the
105 // remote filesystem.
106 project
107 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
108 .await
109 .unwrap();
110 assert_eq!(
111 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
112 "fn one() -> usize { 100 }"
113 );
114
115 // A new file is created in the remote filesystem. The user
116 // sees the new file.
117 fs.save(
118 path!("/code/project1/src/main.rs").as_ref(),
119 &"fn main() {}".into(),
120 Default::default(),
121 )
122 .await
123 .unwrap();
124 cx.executor().run_until_parked();
125 worktree.update(cx, |worktree, _cx| {
126 assert_eq!(
127 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
128 vec![
129 Path::new("README.md"),
130 Path::new("src"),
131 Path::new("src/lib.rs"),
132 Path::new("src/main.rs"),
133 ]
134 );
135 });
136
137 // A file that is currently open in a buffer is renamed.
138 fs.rename(
139 path!("/code/project1/src/lib.rs").as_ref(),
140 path!("/code/project1/src/lib2.rs").as_ref(),
141 Default::default(),
142 )
143 .await
144 .unwrap();
145 cx.executor().run_until_parked();
146 buffer.update(cx, |buffer, _| {
147 assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
148 });
149
150 fs.set_index_for_repo(
151 Path::new(path!("/code/project1/.git")),
152 &[("src/lib2.rs".into(), "fn one() -> usize { 100 }".into())],
153 );
154 cx.executor().run_until_parked();
155 diff.update(cx, |diff, _| {
156 assert_eq!(
157 diff.base_text_string().unwrap(),
158 "fn one() -> usize { 100 }"
159 );
160 });
161}
162
163#[gpui::test]
164async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
165 let fs = FakeFs::new(server_cx.executor());
166 fs.insert_tree(
167 path!("/code"),
168 json!({
169 "project1": {
170 ".git": {},
171 "README.md": "# project 1",
172 "src": {
173 "lib.rs": "fn one() -> usize { 1 }"
174 }
175 },
176 }),
177 )
178 .await;
179
180 let (project, headless) = init_test(&fs, cx, server_cx).await;
181
182 project
183 .update(cx, |project, cx| {
184 project.find_or_create_worktree(path!("/code/project1"), true, cx)
185 })
186 .await
187 .unwrap();
188
189 cx.run_until_parked();
190
191 async fn do_search(project: &Entity<Project>, mut cx: TestAppContext) -> Entity<Buffer> {
192 let receiver = project.update(&mut cx, |project, cx| {
193 project.search(
194 SearchQuery::text(
195 "project",
196 false,
197 true,
198 false,
199 Default::default(),
200 Default::default(),
201 None,
202 )
203 .unwrap(),
204 cx,
205 )
206 });
207
208 let first_response = receiver.recv().await.unwrap();
209 let SearchResult::Buffer { buffer, .. } = first_response else {
210 panic!("incorrect result");
211 };
212 buffer.update(&mut cx, |buffer, cx| {
213 assert_eq!(
214 buffer.file().unwrap().full_path(cx).to_string_lossy(),
215 separator!("project1/README.md")
216 )
217 });
218
219 assert!(receiver.recv().await.is_err());
220 buffer
221 }
222
223 let buffer = do_search(&project, cx.clone()).await;
224
225 // test that the headless server is tracking which buffers we have open correctly.
226 cx.run_until_parked();
227 headless.update(server_cx, |headless, cx| {
228 assert!(headless.buffer_store.read(cx).has_shared_buffers())
229 });
230 do_search(&project, cx.clone()).await;
231
232 cx.update(|_| {
233 drop(buffer);
234 });
235 cx.run_until_parked();
236 headless.update(server_cx, |headless, cx| {
237 assert!(!headless.buffer_store.read(cx).has_shared_buffers())
238 });
239
240 do_search(&project, cx.clone()).await;
241}
242
243#[gpui::test]
244async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
245 let fs = FakeFs::new(server_cx.executor());
246 fs.insert_tree(
247 "/code",
248 json!({
249 "project1": {
250 ".git": {},
251 "README.md": "# project 1",
252 "src": {
253 "lib.rs": "fn one() -> usize { 1 }"
254 }
255 },
256 }),
257 )
258 .await;
259
260 let (project, headless) = init_test(&fs, cx, server_cx).await;
261
262 cx.update_global(|settings_store: &mut SettingsStore, cx| {
263 settings_store.set_user_settings(
264 r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
265 cx,
266 )
267 })
268 .unwrap();
269
270 cx.run_until_parked();
271
272 server_cx.read(|cx| {
273 assert_eq!(
274 AllLanguageSettings::get_global(cx)
275 .language(None, Some(&"Rust".into()), cx)
276 .language_servers,
277 ["..."] // local settings are ignored
278 )
279 });
280
281 server_cx
282 .update_global(|settings_store: &mut SettingsStore, cx| {
283 settings_store.set_server_settings(
284 r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
285 cx,
286 )
287 })
288 .unwrap();
289
290 cx.run_until_parked();
291
292 server_cx.read(|cx| {
293 assert_eq!(
294 AllLanguageSettings::get_global(cx)
295 .language(None, Some(&"Rust".into()), cx)
296 .language_servers,
297 ["from-server-settings".to_string()]
298 )
299 });
300
301 fs.insert_tree(
302 "/code/project1/.zed",
303 json!({
304 "settings.json": r#"
305 {
306 "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
307 "lsp": {
308 "override-rust-analyzer": {
309 "binary": {
310 "path": "~/.cargo/bin/rust-analyzer"
311 }
312 }
313 }
314 }"#
315 }),
316 )
317 .await;
318
319 let worktree_id = project
320 .update(cx, |project, cx| {
321 project.find_or_create_worktree("/code/project1", true, cx)
322 })
323 .await
324 .unwrap()
325 .0
326 .read_with(cx, |worktree, _| worktree.id());
327
328 let buffer = project
329 .update(cx, |project, cx| {
330 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
331 })
332 .await
333 .unwrap();
334 cx.run_until_parked();
335
336 server_cx.read(|cx| {
337 let worktree_id = headless
338 .read(cx)
339 .worktree_store
340 .read(cx)
341 .worktrees()
342 .next()
343 .unwrap()
344 .read(cx)
345 .id();
346 assert_eq!(
347 AllLanguageSettings::get(
348 Some(SettingsLocation {
349 worktree_id,
350 path: Path::new("src/lib.rs")
351 }),
352 cx
353 )
354 .language(None, Some(&"Rust".into()), cx)
355 .language_servers,
356 ["override-rust-analyzer".to_string()]
357 )
358 });
359
360 cx.read(|cx| {
361 let file = buffer.read(cx).file();
362 assert_eq!(
363 language_settings(Some("Rust".into()), file, cx).language_servers,
364 ["override-rust-analyzer".to_string()]
365 )
366 });
367}
368
369#[gpui::test]
370async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
371 let fs = FakeFs::new(server_cx.executor());
372 fs.insert_tree(
373 path!("/code"),
374 json!({
375 "project1": {
376 ".git": {},
377 "README.md": "# project 1",
378 "src": {
379 "lib.rs": "fn one() -> usize { 1 }"
380 }
381 },
382 }),
383 )
384 .await;
385
386 let (project, headless) = init_test(&fs, cx, server_cx).await;
387
388 fs.insert_tree(
389 path!("/code/project1/.zed"),
390 json!({
391 "settings.json": r#"
392 {
393 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
394 "lsp": {
395 "rust-analyzer": {
396 "binary": {
397 "path": "~/.cargo/bin/rust-analyzer"
398 }
399 }
400 }
401 }"#
402 }),
403 )
404 .await;
405
406 cx.update_entity(&project, |project, _| {
407 project.languages().register_test_language(LanguageConfig {
408 name: "Rust".into(),
409 matcher: LanguageMatcher {
410 path_suffixes: vec!["rs".into()],
411 ..Default::default()
412 },
413 ..Default::default()
414 });
415 project.languages().register_fake_lsp_adapter(
416 "Rust",
417 FakeLspAdapter {
418 name: "rust-analyzer",
419 ..Default::default()
420 },
421 )
422 });
423
424 let mut fake_lsp = server_cx.update(|cx| {
425 headless.read(cx).languages.register_fake_language_server(
426 LanguageServerName("rust-analyzer".into()),
427 Default::default(),
428 None,
429 )
430 });
431
432 cx.run_until_parked();
433
434 let worktree_id = project
435 .update(cx, |project, cx| {
436 project.find_or_create_worktree(path!("/code/project1"), true, cx)
437 })
438 .await
439 .unwrap()
440 .0
441 .read_with(cx, |worktree, _| worktree.id());
442
443 // Wait for the settings to synchronize
444 cx.run_until_parked();
445
446 let (buffer, _handle) = project
447 .update(cx, |project, cx| {
448 project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
449 })
450 .await
451 .unwrap();
452 cx.run_until_parked();
453
454 let fake_lsp = fake_lsp.next().await.unwrap();
455
456 cx.read(|cx| {
457 let file = buffer.read(cx).file();
458 assert_eq!(
459 language_settings(Some("Rust".into()), file, cx).language_servers,
460 ["rust-analyzer".to_string()]
461 )
462 });
463
464 let buffer_id = cx.read(|cx| {
465 let buffer = buffer.read(cx);
466 assert_eq!(buffer.language().unwrap().name(), "Rust".into());
467 buffer.remote_id()
468 });
469
470 server_cx.read(|cx| {
471 let buffer = headless
472 .read(cx)
473 .buffer_store
474 .read(cx)
475 .get(buffer_id)
476 .unwrap();
477
478 assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
479 });
480
481 server_cx.read(|cx| {
482 let lsp_store = headless.read(cx).lsp_store.read(cx);
483 assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
484 });
485
486 fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
487 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
488 label: "boop".to_string(),
489 ..Default::default()
490 }])))
491 });
492
493 let result = project
494 .update(cx, |project, cx| {
495 project.completions(
496 &buffer,
497 0,
498 CompletionContext {
499 trigger_kind: CompletionTriggerKind::INVOKED,
500 trigger_character: None,
501 },
502 cx,
503 )
504 })
505 .await
506 .unwrap();
507
508 assert_eq!(
509 result
510 .unwrap()
511 .into_iter()
512 .map(|c| c.label.text)
513 .collect::<Vec<_>>(),
514 vec!["boop".to_string()]
515 );
516
517 fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
518 Ok(Some(lsp::WorkspaceEdit {
519 changes: Some(
520 [(
521 lsp::Url::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
522 vec![lsp::TextEdit::new(
523 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
524 "two".to_string(),
525 )],
526 )]
527 .into_iter()
528 .collect(),
529 ),
530 ..Default::default()
531 }))
532 });
533
534 project
535 .update(cx, |project, cx| {
536 project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
537 })
538 .await
539 .unwrap();
540
541 cx.run_until_parked();
542 buffer.update(cx, |buffer, _| {
543 assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
544 })
545}
546
547#[gpui::test]
548async fn test_remote_cancel_language_server_work(
549 cx: &mut TestAppContext,
550 server_cx: &mut TestAppContext,
551) {
552 let fs = FakeFs::new(server_cx.executor());
553 fs.insert_tree(
554 path!("/code"),
555 json!({
556 "project1": {
557 ".git": {},
558 "README.md": "# project 1",
559 "src": {
560 "lib.rs": "fn one() -> usize { 1 }"
561 }
562 },
563 }),
564 )
565 .await;
566
567 let (project, headless) = init_test(&fs, cx, server_cx).await;
568
569 fs.insert_tree(
570 path!("/code/project1/.zed"),
571 json!({
572 "settings.json": r#"
573 {
574 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
575 "lsp": {
576 "rust-analyzer": {
577 "binary": {
578 "path": "~/.cargo/bin/rust-analyzer"
579 }
580 }
581 }
582 }"#
583 }),
584 )
585 .await;
586
587 cx.update_entity(&project, |project, _| {
588 project.languages().register_test_language(LanguageConfig {
589 name: "Rust".into(),
590 matcher: LanguageMatcher {
591 path_suffixes: vec!["rs".into()],
592 ..Default::default()
593 },
594 ..Default::default()
595 });
596 project.languages().register_fake_lsp_adapter(
597 "Rust",
598 FakeLspAdapter {
599 name: "rust-analyzer",
600 ..Default::default()
601 },
602 )
603 });
604
605 let mut fake_lsp = server_cx.update(|cx| {
606 headless.read(cx).languages.register_fake_language_server(
607 LanguageServerName("rust-analyzer".into()),
608 Default::default(),
609 None,
610 )
611 });
612
613 cx.run_until_parked();
614
615 let worktree_id = project
616 .update(cx, |project, cx| {
617 project.find_or_create_worktree(path!("/code/project1"), true, cx)
618 })
619 .await
620 .unwrap()
621 .0
622 .read_with(cx, |worktree, _| worktree.id());
623
624 cx.run_until_parked();
625
626 let (buffer, _handle) = project
627 .update(cx, |project, cx| {
628 project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
629 })
630 .await
631 .unwrap();
632
633 cx.run_until_parked();
634
635 let mut fake_lsp = fake_lsp.next().await.unwrap();
636
637 // Cancelling all language server work for a given buffer
638 {
639 // Two operations, one cancellable and one not.
640 fake_lsp
641 .start_progress_with(
642 "another-token",
643 lsp::WorkDoneProgressBegin {
644 cancellable: Some(false),
645 ..Default::default()
646 },
647 )
648 .await;
649
650 let progress_token = "the-progress-token";
651 fake_lsp
652 .start_progress_with(
653 progress_token,
654 lsp::WorkDoneProgressBegin {
655 cancellable: Some(true),
656 ..Default::default()
657 },
658 )
659 .await;
660
661 cx.executor().run_until_parked();
662
663 project.update(cx, |project, cx| {
664 project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
665 });
666
667 cx.executor().run_until_parked();
668
669 // Verify the cancellation was received on the server side
670 let cancel_notification = fake_lsp
671 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
672 .await;
673 assert_eq!(
674 cancel_notification.token,
675 lsp::NumberOrString::String(progress_token.into())
676 );
677 }
678
679 // Cancelling work by server_id and token
680 {
681 let server_id = fake_lsp.server.server_id();
682 let progress_token = "the-progress-token";
683
684 fake_lsp
685 .start_progress_with(
686 progress_token,
687 lsp::WorkDoneProgressBegin {
688 cancellable: Some(true),
689 ..Default::default()
690 },
691 )
692 .await;
693
694 cx.executor().run_until_parked();
695
696 project.update(cx, |project, cx| {
697 project.cancel_language_server_work(server_id, Some(progress_token.into()), cx)
698 });
699
700 cx.executor().run_until_parked();
701
702 // Verify the cancellation was received on the server side
703 let cancel_notification = fake_lsp
704 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
705 .await;
706 assert_eq!(
707 cancel_notification.token,
708 lsp::NumberOrString::String(progress_token.into())
709 );
710 }
711}
712
713#[gpui::test]
714async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
715 let fs = FakeFs::new(server_cx.executor());
716 fs.insert_tree(
717 path!("/code"),
718 json!({
719 "project1": {
720 ".git": {},
721 "README.md": "# project 1",
722 "src": {
723 "lib.rs": "fn one() -> usize { 1 }"
724 }
725 },
726 }),
727 )
728 .await;
729
730 let (project, _headless) = init_test(&fs, cx, server_cx).await;
731 let (worktree, _) = project
732 .update(cx, |project, cx| {
733 project.find_or_create_worktree(path!("/code/project1"), true, cx)
734 })
735 .await
736 .unwrap();
737
738 let worktree_id = cx.update(|cx| worktree.read(cx).id());
739
740 let buffer = project
741 .update(cx, |project, cx| {
742 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
743 })
744 .await
745 .unwrap();
746
747 fs.save(
748 &PathBuf::from(path!("/code/project1/src/lib.rs")),
749 &("bangles".to_string().into()),
750 LineEnding::Unix,
751 )
752 .await
753 .unwrap();
754
755 cx.run_until_parked();
756
757 buffer.update(cx, |buffer, cx| {
758 assert_eq!(buffer.text(), "bangles");
759 buffer.edit([(0..0, "a")], None, cx);
760 });
761
762 fs.save(
763 &PathBuf::from(path!("/code/project1/src/lib.rs")),
764 &("bloop".to_string().into()),
765 LineEnding::Unix,
766 )
767 .await
768 .unwrap();
769
770 cx.run_until_parked();
771 cx.update(|cx| {
772 assert!(buffer.read(cx).has_conflict());
773 });
774
775 project
776 .update(cx, |project, cx| {
777 project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
778 })
779 .await
780 .unwrap();
781 cx.run_until_parked();
782
783 cx.update(|cx| {
784 assert!(!buffer.read(cx).has_conflict());
785 });
786}
787
788#[gpui::test]
789async fn test_remote_resolve_path_in_buffer(
790 cx: &mut TestAppContext,
791 server_cx: &mut TestAppContext,
792) {
793 let fs = FakeFs::new(server_cx.executor());
794 // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
795 fs.insert_tree(
796 path!("/code"),
797 json!({
798 "project1": {
799 ".git": {},
800 "README.md": "# project 1",
801 "src": {
802 "lib.rs": "fn one() -> usize { 1 }"
803 }
804 },
805 "project2": {
806 ".git": {},
807 "README.md": "# project 2",
808 "src": {
809 "lib.rs": "fn two() -> usize { 2 }"
810 }
811 }
812 }),
813 )
814 .await;
815
816 let (project, _headless) = init_test(&fs, cx, server_cx).await;
817
818 let _ = project
819 .update(cx, |project, cx| {
820 project.find_or_create_worktree(path!("/code/project1"), true, cx)
821 })
822 .await
823 .unwrap();
824
825 let (worktree2, _) = project
826 .update(cx, |project, cx| {
827 project.find_or_create_worktree(path!("/code/project2"), true, cx)
828 })
829 .await
830 .unwrap();
831
832 let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
833
834 let buffer2 = project
835 .update(cx, |project, cx| {
836 project.open_buffer((worktree2_id, Path::new("src/lib.rs")), cx)
837 })
838 .await
839 .unwrap();
840
841 let path = project
842 .update(cx, |project, cx| {
843 project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
844 })
845 .await
846 .unwrap();
847 assert!(path.is_file());
848 assert_eq!(
849 path.abs_path().unwrap().to_string_lossy(),
850 path!("/code/project2/README.md")
851 );
852
853 let path = project
854 .update(cx, |project, cx| {
855 project.resolve_path_in_buffer("../README.md", &buffer2, cx)
856 })
857 .await
858 .unwrap();
859 assert!(path.is_file());
860 assert_eq!(
861 path.project_path().unwrap().clone(),
862 ProjectPath::from((worktree2_id, "README.md"))
863 );
864
865 let path = project
866 .update(cx, |project, cx| {
867 project.resolve_path_in_buffer("../src", &buffer2, cx)
868 })
869 .await
870 .unwrap();
871 assert_eq!(
872 path.project_path().unwrap().clone(),
873 ProjectPath::from((worktree2_id, "src"))
874 );
875 assert!(path.is_dir());
876}
877
878#[gpui::test]
879async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
880 let fs = FakeFs::new(server_cx.executor());
881 fs.insert_tree(
882 path!("/code"),
883 json!({
884 "project1": {
885 ".git": {},
886 "README.md": "# project 1",
887 "src": {
888 "lib.rs": "fn one() -> usize { 1 }"
889 }
890 },
891 }),
892 )
893 .await;
894
895 let (project, _headless) = init_test(&fs, cx, server_cx).await;
896
897 let path = project
898 .update(cx, |project, cx| {
899 project.resolve_abs_path(path!("/code/project1/README.md"), cx)
900 })
901 .await
902 .unwrap();
903
904 assert!(path.is_file());
905 assert_eq!(
906 path.abs_path().unwrap().to_string_lossy(),
907 path!("/code/project1/README.md")
908 );
909
910 let path = project
911 .update(cx, |project, cx| {
912 project.resolve_abs_path(path!("/code/project1/src"), cx)
913 })
914 .await
915 .unwrap();
916
917 assert!(path.is_dir());
918 assert_eq!(
919 path.abs_path().unwrap().to_string_lossy(),
920 path!("/code/project1/src")
921 );
922
923 let path = project
924 .update(cx, |project, cx| {
925 project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
926 })
927 .await;
928 assert!(path.is_none());
929}
930
931#[gpui::test(iterations = 10)]
932async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
933 let fs = FakeFs::new(server_cx.executor());
934 fs.insert_tree(
935 "/code",
936 json!({
937 "project1": {
938 ".git": {},
939 "README.md": "# project 1",
940 "src": {
941 "lib.rs": "fn one() -> usize { 1 }"
942 }
943 },
944 }),
945 )
946 .await;
947
948 let (project, _headless) = init_test(&fs, cx, server_cx).await;
949 let (worktree, _) = project
950 .update(cx, |project, cx| {
951 project.find_or_create_worktree("/code/project1", true, cx)
952 })
953 .await
954 .unwrap();
955 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
956
957 // Open a buffer on the client but cancel after a random amount of time.
958 let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
959 cx.executor().simulate_random_delay().await;
960 drop(buffer);
961
962 // Try opening the same buffer again as the client, and ensure we can
963 // still do it despite the cancellation above.
964 let buffer = project
965 .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
966 .await
967 .unwrap();
968
969 buffer.read_with(cx, |buf, _| {
970 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
971 });
972}
973
974#[gpui::test]
975async fn test_adding_then_removing_then_adding_worktrees(
976 cx: &mut TestAppContext,
977 server_cx: &mut TestAppContext,
978) {
979 let fs = FakeFs::new(server_cx.executor());
980 fs.insert_tree(
981 path!("/code"),
982 json!({
983 "project1": {
984 ".git": {},
985 "README.md": "# project 1",
986 "src": {
987 "lib.rs": "fn one() -> usize { 1 }"
988 }
989 },
990 "project2": {
991 "README.md": "# project 2",
992 },
993 }),
994 )
995 .await;
996
997 let (project, _headless) = init_test(&fs, cx, server_cx).await;
998 let (_worktree, _) = project
999 .update(cx, |project, cx| {
1000 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1001 })
1002 .await
1003 .unwrap();
1004
1005 let (worktree_2, _) = project
1006 .update(cx, |project, cx| {
1007 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1008 })
1009 .await
1010 .unwrap();
1011 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1012
1013 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1014
1015 let (worktree_2, _) = project
1016 .update(cx, |project, cx| {
1017 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1018 })
1019 .await
1020 .unwrap();
1021
1022 cx.run_until_parked();
1023 worktree_2.update(cx, |worktree, _cx| {
1024 assert!(worktree.is_visible());
1025 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1026 assert_eq!(entries.len(), 2);
1027 assert_eq!(
1028 entries[1].path.to_string_lossy().to_string(),
1029 "README.md".to_string()
1030 )
1031 })
1032}
1033
1034#[gpui::test]
1035async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1036 let fs = FakeFs::new(server_cx.executor());
1037 fs.insert_tree(
1038 path!("/code"),
1039 json!({
1040 "project1": {
1041 ".git": {},
1042 "README.md": "# project 1",
1043 "src": {
1044 "lib.rs": "fn one() -> usize { 1 }"
1045 }
1046 },
1047 }),
1048 )
1049 .await;
1050
1051 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1052 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1053 cx.executor().run_until_parked();
1054
1055 let buffer = buffer.await.unwrap();
1056
1057 cx.update(|cx| {
1058 assert_eq!(
1059 buffer.read(cx).text(),
1060 initial_server_settings_content()
1061 .to_string()
1062 .replace("\r\n", "\n")
1063 )
1064 })
1065}
1066
1067#[gpui::test(iterations = 20)]
1068async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1069 let fs = FakeFs::new(server_cx.executor());
1070 fs.insert_tree(
1071 path!("/code"),
1072 json!({
1073 "project1": {
1074 ".git": {},
1075 "README.md": "# project 1",
1076 "src": {
1077 "lib.rs": "fn one() -> usize { 1 }"
1078 }
1079 },
1080 }),
1081 )
1082 .await;
1083
1084 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1085
1086 let (worktree, _) = project
1087 .update(cx, |project, cx| {
1088 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1089 })
1090 .await
1091 .unwrap();
1092
1093 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1094 let buffer = project
1095 .update(cx, |project, cx| {
1096 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1097 })
1098 .await
1099 .unwrap();
1100
1101 buffer.update(cx, |buffer, cx| {
1102 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1103 let ix = buffer.text().find('1').unwrap();
1104 buffer.edit([(ix..ix + 1, "100")], None, cx);
1105 });
1106
1107 let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
1108 client
1109 .update(cx, |client, cx| client.simulate_disconnect(cx))
1110 .detach();
1111
1112 project
1113 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1114 .await
1115 .unwrap();
1116
1117 assert_eq!(
1118 fs.load(path!("/code/project1/src/lib.rs").as_ref())
1119 .await
1120 .unwrap(),
1121 "fn one() -> usize { 100 }"
1122 );
1123}
1124
1125#[gpui::test]
1126async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1127 let fs = FakeFs::new(server_cx.executor());
1128 fs.insert_tree(
1129 "/code",
1130 json!({
1131 "project1": {
1132 ".git": {},
1133 "README.md": "# project 1",
1134 },
1135 }),
1136 )
1137 .await;
1138
1139 let (project, _) = init_test(&fs, cx, server_cx).await;
1140
1141 let (worktree, _) = project
1142 .update(cx, |project, cx| {
1143 project.find_or_create_worktree("/code/project1", true, cx)
1144 })
1145 .await
1146 .unwrap();
1147
1148 cx.run_until_parked();
1149
1150 fs.rename(
1151 &PathBuf::from("/code/project1"),
1152 &PathBuf::from("/code/project2"),
1153 Default::default(),
1154 )
1155 .await
1156 .unwrap();
1157
1158 cx.run_until_parked();
1159 worktree.update(cx, |worktree, _| {
1160 assert_eq!(worktree.root_name(), "project2")
1161 })
1162}
1163
1164#[gpui::test]
1165async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1166 let fs = FakeFs::new(server_cx.executor());
1167 fs.insert_tree(
1168 "/code",
1169 json!({
1170 "project1": {
1171 ".git": {},
1172 "README.md": "# project 1",
1173 },
1174 }),
1175 )
1176 .await;
1177
1178 let (project, _) = init_test(&fs, cx, server_cx).await;
1179 let (worktree, _) = project
1180 .update(cx, |project, cx| {
1181 project.find_or_create_worktree("/code/project1", true, cx)
1182 })
1183 .await
1184 .unwrap();
1185
1186 cx.run_until_parked();
1187
1188 let entry = worktree
1189 .update(cx, |worktree, cx| {
1190 let entry = worktree.entry_for_path("README.md").unwrap();
1191 worktree.rename_entry(entry.id, Path::new("README.rst"), cx)
1192 })
1193 .await
1194 .unwrap()
1195 .to_included()
1196 .unwrap();
1197
1198 cx.run_until_parked();
1199
1200 worktree.update(cx, |worktree, _| {
1201 assert_eq!(worktree.entry_for_path("README.rst").unwrap().id, entry.id)
1202 });
1203}
1204
1205#[gpui::test]
1206async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1207 let text_2 = "
1208 fn one() -> usize {
1209 1
1210 }
1211 "
1212 .unindent();
1213 let text_1 = "
1214 fn one() -> usize {
1215 0
1216 }
1217 "
1218 .unindent();
1219
1220 let fs = FakeFs::new(server_cx.executor());
1221 fs.insert_tree(
1222 "/code",
1223 json!({
1224 "project1": {
1225 ".git": {},
1226 "src": {
1227 "lib.rs": text_2
1228 },
1229 "README.md": "# project 1",
1230 },
1231 }),
1232 )
1233 .await;
1234 fs.set_index_for_repo(
1235 Path::new("/code/project1/.git"),
1236 &[("src/lib.rs".into(), text_1.clone())],
1237 );
1238 fs.set_head_for_repo(
1239 Path::new("/code/project1/.git"),
1240 &[("src/lib.rs".into(), text_1.clone())],
1241 );
1242
1243 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1244 let (worktree, _) = project
1245 .update(cx, |project, cx| {
1246 project.find_or_create_worktree("/code/project1", true, cx)
1247 })
1248 .await
1249 .unwrap();
1250 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1251 cx.executor().run_until_parked();
1252
1253 let buffer = project
1254 .update(cx, |project, cx| {
1255 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1256 })
1257 .await
1258 .unwrap();
1259 let diff = project
1260 .update(cx, |project, cx| {
1261 project.open_uncommitted_diff(buffer.clone(), cx)
1262 })
1263 .await
1264 .unwrap();
1265
1266 diff.read_with(cx, |diff, cx| {
1267 assert_eq!(diff.base_text_string().unwrap(), text_1);
1268 assert_eq!(
1269 diff.secondary_diff()
1270 .unwrap()
1271 .read(cx)
1272 .base_text_string()
1273 .unwrap(),
1274 text_1
1275 );
1276 });
1277
1278 // stage the current buffer's contents
1279 fs.set_index_for_repo(
1280 Path::new("/code/project1/.git"),
1281 &[("src/lib.rs".into(), text_2.clone())],
1282 );
1283
1284 cx.executor().run_until_parked();
1285 diff.read_with(cx, |diff, cx| {
1286 assert_eq!(diff.base_text_string().unwrap(), text_1);
1287 assert_eq!(
1288 diff.secondary_diff()
1289 .unwrap()
1290 .read(cx)
1291 .base_text_string()
1292 .unwrap(),
1293 text_2
1294 );
1295 });
1296
1297 // commit the current buffer's contents
1298 fs.set_head_for_repo(
1299 Path::new("/code/project1/.git"),
1300 &[("src/lib.rs".into(), text_2.clone())],
1301 );
1302
1303 cx.executor().run_until_parked();
1304 diff.read_with(cx, |diff, cx| {
1305 assert_eq!(diff.base_text_string().unwrap(), text_2);
1306 assert_eq!(
1307 diff.secondary_diff()
1308 .unwrap()
1309 .read(cx)
1310 .base_text_string()
1311 .unwrap(),
1312 text_2
1313 );
1314 });
1315}
1316
1317#[gpui::test]
1318async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1319 let fs = FakeFs::new(server_cx.executor());
1320 fs.insert_tree(
1321 path!("/code"),
1322 json!({
1323 "project1": {
1324 ".git": {},
1325 "README.md": "# project 1",
1326 },
1327 }),
1328 )
1329 .await;
1330
1331 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1332 let branches = ["main", "dev", "feature-1"];
1333 let branches_set = branches
1334 .iter()
1335 .map(ToString::to_string)
1336 .collect::<HashSet<_>>();
1337 fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1338
1339 let (_worktree, _) = project
1340 .update(cx, |project, cx| {
1341 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1342 })
1343 .await
1344 .unwrap();
1345 // Give the worktree a bit of time to index the file system
1346 cx.run_until_parked();
1347
1348 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1349
1350 let remote_branches = repository
1351 .update(cx, |repository, _| repository.branches())
1352 .await
1353 .unwrap()
1354 .unwrap();
1355
1356 let new_branch = branches[2];
1357
1358 let remote_branches = remote_branches
1359 .into_iter()
1360 .map(|branch| branch.name.to_string())
1361 .collect::<HashSet<_>>();
1362
1363 assert_eq!(&remote_branches, &branches_set);
1364
1365 cx.update(|cx| repository.read(cx).change_branch(new_branch.to_string()))
1366 .await
1367 .unwrap()
1368 .unwrap();
1369
1370 cx.run_until_parked();
1371
1372 let server_branch = server_cx.update(|cx| {
1373 headless_project.update(cx, |headless_project, cx| {
1374 headless_project.git_store.update(cx, |git_store, cx| {
1375 git_store
1376 .repositories()
1377 .values()
1378 .next()
1379 .unwrap()
1380 .read(cx)
1381 .current_branch()
1382 .unwrap()
1383 .clone()
1384 })
1385 })
1386 });
1387
1388 assert_eq!(server_branch.name, branches[2]);
1389
1390 // Also try creating a new branch
1391 cx.update(|cx| {
1392 repository
1393 .read(cx)
1394 .create_branch("totally-new-branch".to_string())
1395 })
1396 .await
1397 .unwrap()
1398 .unwrap();
1399
1400 cx.update(|cx| {
1401 repository
1402 .read(cx)
1403 .change_branch("totally-new-branch".to_string())
1404 })
1405 .await
1406 .unwrap()
1407 .unwrap();
1408
1409 cx.run_until_parked();
1410
1411 let server_branch = server_cx.update(|cx| {
1412 headless_project.update(cx, |headless_project, cx| {
1413 headless_project.git_store.update(cx, |git_store, cx| {
1414 git_store
1415 .repositories()
1416 .values()
1417 .next()
1418 .unwrap()
1419 .read(cx)
1420 .current_branch()
1421 .unwrap()
1422 .clone()
1423 })
1424 })
1425 });
1426
1427 assert_eq!(server_branch.name, "totally-new-branch");
1428}
1429
1430pub async fn init_test(
1431 server_fs: &Arc<FakeFs>,
1432 cx: &mut TestAppContext,
1433 server_cx: &mut TestAppContext,
1434) -> (Entity<Project>, Entity<HeadlessProject>) {
1435 let server_fs = server_fs.clone();
1436 cx.update(|cx| {
1437 release_channel::init(SemanticVersion::default(), cx);
1438 });
1439 server_cx.update(|cx| {
1440 release_channel::init(SemanticVersion::default(), cx);
1441 });
1442 init_logger();
1443
1444 let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
1445 let http_client = Arc::new(BlockedHttpClient);
1446 let node_runtime = NodeRuntime::unavailable();
1447 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1448 let proxy = Arc::new(ExtensionHostProxy::new());
1449 server_cx.update(HeadlessProject::init);
1450 let headless = server_cx.new(|cx| {
1451 client::init_settings(cx);
1452
1453 HeadlessProject::new(
1454 crate::HeadlessAppState {
1455 session: ssh_server_client,
1456 fs: server_fs.clone(),
1457 http_client,
1458 node_runtime,
1459 languages,
1460 extension_host_proxy: proxy,
1461 },
1462 cx,
1463 )
1464 });
1465
1466 let ssh = SshRemoteClient::fake_client(opts, cx).await;
1467 let project = build_project(ssh, cx);
1468 project
1469 .update(cx, {
1470 let headless = headless.clone();
1471 |_, cx| cx.on_release(|_, _| drop(headless))
1472 })
1473 .detach();
1474 (project, headless)
1475}
1476
1477fn init_logger() {
1478 if std::env::var("RUST_LOG").is_ok() {
1479 env_logger::try_init().ok();
1480 }
1481}
1482
1483fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1484 cx.update(|cx| {
1485 if !cx.has_global::<SettingsStore>() {
1486 let settings_store = SettingsStore::test(cx);
1487 cx.set_global(settings_store);
1488 }
1489 });
1490
1491 let client = cx.update(|cx| {
1492 Client::new(
1493 Arc::new(FakeSystemClock::new()),
1494 FakeHttpClient::with_404_response(),
1495 cx,
1496 )
1497 });
1498
1499 let node = NodeRuntime::unavailable();
1500 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1501 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1502 let fs = FakeFs::new(cx.executor());
1503
1504 cx.update(|cx| {
1505 Project::init(&client, cx);
1506 language::init(cx);
1507 });
1508
1509 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1510}