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.handle_request::<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.into_iter().map(|c| c.label.text).collect::<Vec<_>>(),
510 vec!["boop".to_string()]
511 );
512
513 fake_lsp.handle_request::<lsp::request::Rename, _, _>(|_, _| async move {
514 Ok(Some(lsp::WorkspaceEdit {
515 changes: Some(
516 [(
517 lsp::Url::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
518 vec![lsp::TextEdit::new(
519 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
520 "two".to_string(),
521 )],
522 )]
523 .into_iter()
524 .collect(),
525 ),
526 ..Default::default()
527 }))
528 });
529
530 project
531 .update(cx, |project, cx| {
532 project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
533 })
534 .await
535 .unwrap();
536
537 cx.run_until_parked();
538 buffer.update(cx, |buffer, _| {
539 assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
540 })
541}
542
543#[gpui::test]
544async fn test_remote_cancel_language_server_work(
545 cx: &mut TestAppContext,
546 server_cx: &mut TestAppContext,
547) {
548 let fs = FakeFs::new(server_cx.executor());
549 fs.insert_tree(
550 path!("/code"),
551 json!({
552 "project1": {
553 ".git": {},
554 "README.md": "# project 1",
555 "src": {
556 "lib.rs": "fn one() -> usize { 1 }"
557 }
558 },
559 }),
560 )
561 .await;
562
563 let (project, headless) = init_test(&fs, cx, server_cx).await;
564
565 fs.insert_tree(
566 path!("/code/project1/.zed"),
567 json!({
568 "settings.json": r#"
569 {
570 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
571 "lsp": {
572 "rust-analyzer": {
573 "binary": {
574 "path": "~/.cargo/bin/rust-analyzer"
575 }
576 }
577 }
578 }"#
579 }),
580 )
581 .await;
582
583 cx.update_entity(&project, |project, _| {
584 project.languages().register_test_language(LanguageConfig {
585 name: "Rust".into(),
586 matcher: LanguageMatcher {
587 path_suffixes: vec!["rs".into()],
588 ..Default::default()
589 },
590 ..Default::default()
591 });
592 project.languages().register_fake_lsp_adapter(
593 "Rust",
594 FakeLspAdapter {
595 name: "rust-analyzer",
596 ..Default::default()
597 },
598 )
599 });
600
601 let mut fake_lsp = server_cx.update(|cx| {
602 headless.read(cx).languages.register_fake_language_server(
603 LanguageServerName("rust-analyzer".into()),
604 Default::default(),
605 None,
606 )
607 });
608
609 cx.run_until_parked();
610
611 let worktree_id = project
612 .update(cx, |project, cx| {
613 project.find_or_create_worktree(path!("/code/project1"), true, cx)
614 })
615 .await
616 .unwrap()
617 .0
618 .read_with(cx, |worktree, _| worktree.id());
619
620 cx.run_until_parked();
621
622 let (buffer, _handle) = project
623 .update(cx, |project, cx| {
624 project.open_buffer_with_lsp((worktree_id, Path::new("src/lib.rs")), cx)
625 })
626 .await
627 .unwrap();
628
629 cx.run_until_parked();
630
631 let mut fake_lsp = fake_lsp.next().await.unwrap();
632
633 // Cancelling all language server work for a given buffer
634 {
635 // Two operations, one cancellable and one not.
636 fake_lsp
637 .start_progress_with(
638 "another-token",
639 lsp::WorkDoneProgressBegin {
640 cancellable: Some(false),
641 ..Default::default()
642 },
643 )
644 .await;
645
646 let progress_token = "the-progress-token";
647 fake_lsp
648 .start_progress_with(
649 progress_token,
650 lsp::WorkDoneProgressBegin {
651 cancellable: Some(true),
652 ..Default::default()
653 },
654 )
655 .await;
656
657 cx.executor().run_until_parked();
658
659 project.update(cx, |project, cx| {
660 project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
661 });
662
663 cx.executor().run_until_parked();
664
665 // Verify the cancellation was received on the server side
666 let cancel_notification = fake_lsp
667 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
668 .await;
669 assert_eq!(
670 cancel_notification.token,
671 lsp::NumberOrString::String(progress_token.into())
672 );
673 }
674
675 // Cancelling work by server_id and token
676 {
677 let server_id = fake_lsp.server.server_id();
678 let progress_token = "the-progress-token";
679
680 fake_lsp
681 .start_progress_with(
682 progress_token,
683 lsp::WorkDoneProgressBegin {
684 cancellable: Some(true),
685 ..Default::default()
686 },
687 )
688 .await;
689
690 cx.executor().run_until_parked();
691
692 project.update(cx, |project, cx| {
693 project.cancel_language_server_work(server_id, Some(progress_token.into()), cx)
694 });
695
696 cx.executor().run_until_parked();
697
698 // Verify the cancellation was received on the server side
699 let cancel_notification = fake_lsp
700 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
701 .await;
702 assert_eq!(
703 cancel_notification.token,
704 lsp::NumberOrString::String(progress_token.into())
705 );
706 }
707}
708
709#[gpui::test]
710async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
711 let fs = FakeFs::new(server_cx.executor());
712 fs.insert_tree(
713 path!("/code"),
714 json!({
715 "project1": {
716 ".git": {},
717 "README.md": "# project 1",
718 "src": {
719 "lib.rs": "fn one() -> usize { 1 }"
720 }
721 },
722 }),
723 )
724 .await;
725
726 let (project, _headless) = init_test(&fs, cx, server_cx).await;
727 let (worktree, _) = project
728 .update(cx, |project, cx| {
729 project.find_or_create_worktree(path!("/code/project1"), true, cx)
730 })
731 .await
732 .unwrap();
733
734 let worktree_id = cx.update(|cx| worktree.read(cx).id());
735
736 let buffer = project
737 .update(cx, |project, cx| {
738 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
739 })
740 .await
741 .unwrap();
742
743 fs.save(
744 &PathBuf::from(path!("/code/project1/src/lib.rs")),
745 &("bangles".to_string().into()),
746 LineEnding::Unix,
747 )
748 .await
749 .unwrap();
750
751 cx.run_until_parked();
752
753 buffer.update(cx, |buffer, cx| {
754 assert_eq!(buffer.text(), "bangles");
755 buffer.edit([(0..0, "a")], None, cx);
756 });
757
758 fs.save(
759 &PathBuf::from(path!("/code/project1/src/lib.rs")),
760 &("bloop".to_string().into()),
761 LineEnding::Unix,
762 )
763 .await
764 .unwrap();
765
766 cx.run_until_parked();
767 cx.update(|cx| {
768 assert!(buffer.read(cx).has_conflict());
769 });
770
771 project
772 .update(cx, |project, cx| {
773 project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
774 })
775 .await
776 .unwrap();
777 cx.run_until_parked();
778
779 cx.update(|cx| {
780 assert!(!buffer.read(cx).has_conflict());
781 });
782}
783
784#[gpui::test]
785async fn test_remote_resolve_path_in_buffer(
786 cx: &mut TestAppContext,
787 server_cx: &mut TestAppContext,
788) {
789 let fs = FakeFs::new(server_cx.executor());
790 // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
791 fs.insert_tree(
792 path!("/code"),
793 json!({
794 "project1": {
795 ".git": {},
796 "README.md": "# project 1",
797 "src": {
798 "lib.rs": "fn one() -> usize { 1 }"
799 }
800 },
801 "project2": {
802 ".git": {},
803 "README.md": "# project 2",
804 "src": {
805 "lib.rs": "fn two() -> usize { 2 }"
806 }
807 }
808 }),
809 )
810 .await;
811
812 let (project, _headless) = init_test(&fs, cx, server_cx).await;
813
814 let _ = project
815 .update(cx, |project, cx| {
816 project.find_or_create_worktree(path!("/code/project1"), true, cx)
817 })
818 .await
819 .unwrap();
820
821 let (worktree2, _) = project
822 .update(cx, |project, cx| {
823 project.find_or_create_worktree(path!("/code/project2"), true, cx)
824 })
825 .await
826 .unwrap();
827
828 let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
829
830 let buffer2 = project
831 .update(cx, |project, cx| {
832 project.open_buffer((worktree2_id, Path::new("src/lib.rs")), cx)
833 })
834 .await
835 .unwrap();
836
837 let path = project
838 .update(cx, |project, cx| {
839 project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
840 })
841 .await
842 .unwrap();
843 assert!(path.is_file());
844 assert_eq!(
845 path.abs_path().unwrap().to_string_lossy(),
846 path!("/code/project2/README.md")
847 );
848
849 let path = project
850 .update(cx, |project, cx| {
851 project.resolve_path_in_buffer("../README.md", &buffer2, cx)
852 })
853 .await
854 .unwrap();
855 assert!(path.is_file());
856 assert_eq!(
857 path.project_path().unwrap().clone(),
858 ProjectPath::from((worktree2_id, "README.md"))
859 );
860
861 let path = project
862 .update(cx, |project, cx| {
863 project.resolve_path_in_buffer("../src", &buffer2, cx)
864 })
865 .await
866 .unwrap();
867 assert_eq!(
868 path.project_path().unwrap().clone(),
869 ProjectPath::from((worktree2_id, "src"))
870 );
871 assert!(path.is_dir());
872}
873
874#[gpui::test]
875async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
876 let fs = FakeFs::new(server_cx.executor());
877 fs.insert_tree(
878 path!("/code"),
879 json!({
880 "project1": {
881 ".git": {},
882 "README.md": "# project 1",
883 "src": {
884 "lib.rs": "fn one() -> usize { 1 }"
885 }
886 },
887 }),
888 )
889 .await;
890
891 let (project, _headless) = init_test(&fs, cx, server_cx).await;
892
893 let path = project
894 .update(cx, |project, cx| {
895 project.resolve_abs_path(path!("/code/project1/README.md"), cx)
896 })
897 .await
898 .unwrap();
899
900 assert!(path.is_file());
901 assert_eq!(
902 path.abs_path().unwrap().to_string_lossy(),
903 path!("/code/project1/README.md")
904 );
905
906 let path = project
907 .update(cx, |project, cx| {
908 project.resolve_abs_path(path!("/code/project1/src"), cx)
909 })
910 .await
911 .unwrap();
912
913 assert!(path.is_dir());
914 assert_eq!(
915 path.abs_path().unwrap().to_string_lossy(),
916 path!("/code/project1/src")
917 );
918
919 let path = project
920 .update(cx, |project, cx| {
921 project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
922 })
923 .await;
924 assert!(path.is_none());
925}
926
927#[gpui::test(iterations = 10)]
928async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
929 let fs = FakeFs::new(server_cx.executor());
930 fs.insert_tree(
931 "/code",
932 json!({
933 "project1": {
934 ".git": {},
935 "README.md": "# project 1",
936 "src": {
937 "lib.rs": "fn one() -> usize { 1 }"
938 }
939 },
940 }),
941 )
942 .await;
943
944 let (project, _headless) = init_test(&fs, cx, server_cx).await;
945 let (worktree, _) = project
946 .update(cx, |project, cx| {
947 project.find_or_create_worktree("/code/project1", true, cx)
948 })
949 .await
950 .unwrap();
951 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
952
953 // Open a buffer on the client but cancel after a random amount of time.
954 let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
955 cx.executor().simulate_random_delay().await;
956 drop(buffer);
957
958 // Try opening the same buffer again as the client, and ensure we can
959 // still do it despite the cancellation above.
960 let buffer = project
961 .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
962 .await
963 .unwrap();
964
965 buffer.read_with(cx, |buf, _| {
966 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
967 });
968}
969
970#[gpui::test]
971async fn test_adding_then_removing_then_adding_worktrees(
972 cx: &mut TestAppContext,
973 server_cx: &mut TestAppContext,
974) {
975 let fs = FakeFs::new(server_cx.executor());
976 fs.insert_tree(
977 path!("/code"),
978 json!({
979 "project1": {
980 ".git": {},
981 "README.md": "# project 1",
982 "src": {
983 "lib.rs": "fn one() -> usize { 1 }"
984 }
985 },
986 "project2": {
987 "README.md": "# project 2",
988 },
989 }),
990 )
991 .await;
992
993 let (project, _headless) = init_test(&fs, cx, server_cx).await;
994 let (_worktree, _) = project
995 .update(cx, |project, cx| {
996 project.find_or_create_worktree(path!("/code/project1"), true, cx)
997 })
998 .await
999 .unwrap();
1000
1001 let (worktree_2, _) = project
1002 .update(cx, |project, cx| {
1003 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1004 })
1005 .await
1006 .unwrap();
1007 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1008
1009 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1010
1011 let (worktree_2, _) = project
1012 .update(cx, |project, cx| {
1013 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1014 })
1015 .await
1016 .unwrap();
1017
1018 cx.run_until_parked();
1019 worktree_2.update(cx, |worktree, _cx| {
1020 assert!(worktree.is_visible());
1021 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1022 assert_eq!(entries.len(), 2);
1023 assert_eq!(
1024 entries[1].path.to_string_lossy().to_string(),
1025 "README.md".to_string()
1026 )
1027 })
1028}
1029
1030#[gpui::test]
1031async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1032 let fs = FakeFs::new(server_cx.executor());
1033 fs.insert_tree(
1034 path!("/code"),
1035 json!({
1036 "project1": {
1037 ".git": {},
1038 "README.md": "# project 1",
1039 "src": {
1040 "lib.rs": "fn one() -> usize { 1 }"
1041 }
1042 },
1043 }),
1044 )
1045 .await;
1046
1047 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1048 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1049 cx.executor().run_until_parked();
1050
1051 let buffer = buffer.await.unwrap();
1052
1053 cx.update(|cx| {
1054 assert_eq!(
1055 buffer.read(cx).text(),
1056 initial_server_settings_content()
1057 .to_string()
1058 .replace("\r\n", "\n")
1059 )
1060 })
1061}
1062
1063#[gpui::test(iterations = 20)]
1064async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1065 let fs = FakeFs::new(server_cx.executor());
1066 fs.insert_tree(
1067 path!("/code"),
1068 json!({
1069 "project1": {
1070 ".git": {},
1071 "README.md": "# project 1",
1072 "src": {
1073 "lib.rs": "fn one() -> usize { 1 }"
1074 }
1075 },
1076 }),
1077 )
1078 .await;
1079
1080 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1081
1082 let (worktree, _) = project
1083 .update(cx, |project, cx| {
1084 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1085 })
1086 .await
1087 .unwrap();
1088
1089 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1090 let buffer = project
1091 .update(cx, |project, cx| {
1092 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1093 })
1094 .await
1095 .unwrap();
1096
1097 buffer.update(cx, |buffer, cx| {
1098 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1099 let ix = buffer.text().find('1').unwrap();
1100 buffer.edit([(ix..ix + 1, "100")], None, cx);
1101 });
1102
1103 let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
1104 client
1105 .update(cx, |client, cx| client.simulate_disconnect(cx))
1106 .detach();
1107
1108 project
1109 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1110 .await
1111 .unwrap();
1112
1113 assert_eq!(
1114 fs.load(path!("/code/project1/src/lib.rs").as_ref())
1115 .await
1116 .unwrap(),
1117 "fn one() -> usize { 100 }"
1118 );
1119}
1120
1121#[gpui::test]
1122async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1123 let fs = FakeFs::new(server_cx.executor());
1124 fs.insert_tree(
1125 "/code",
1126 json!({
1127 "project1": {
1128 ".git": {},
1129 "README.md": "# project 1",
1130 },
1131 }),
1132 )
1133 .await;
1134
1135 let (project, _) = init_test(&fs, cx, server_cx).await;
1136
1137 let (worktree, _) = project
1138 .update(cx, |project, cx| {
1139 project.find_or_create_worktree("/code/project1", true, cx)
1140 })
1141 .await
1142 .unwrap();
1143
1144 cx.run_until_parked();
1145
1146 fs.rename(
1147 &PathBuf::from("/code/project1"),
1148 &PathBuf::from("/code/project2"),
1149 Default::default(),
1150 )
1151 .await
1152 .unwrap();
1153
1154 cx.run_until_parked();
1155 worktree.update(cx, |worktree, _| {
1156 assert_eq!(worktree.root_name(), "project2")
1157 })
1158}
1159
1160#[gpui::test]
1161async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1162 let fs = FakeFs::new(server_cx.executor());
1163 fs.insert_tree(
1164 "/code",
1165 json!({
1166 "project1": {
1167 ".git": {},
1168 "README.md": "# project 1",
1169 },
1170 }),
1171 )
1172 .await;
1173
1174 let (project, _) = init_test(&fs, cx, server_cx).await;
1175 let (worktree, _) = project
1176 .update(cx, |project, cx| {
1177 project.find_or_create_worktree("/code/project1", true, cx)
1178 })
1179 .await
1180 .unwrap();
1181
1182 cx.run_until_parked();
1183
1184 let entry = worktree
1185 .update(cx, |worktree, cx| {
1186 let entry = worktree.entry_for_path("README.md").unwrap();
1187 worktree.rename_entry(entry.id, Path::new("README.rst"), cx)
1188 })
1189 .await
1190 .unwrap()
1191 .to_included()
1192 .unwrap();
1193
1194 cx.run_until_parked();
1195
1196 worktree.update(cx, |worktree, _| {
1197 assert_eq!(worktree.entry_for_path("README.rst").unwrap().id, entry.id)
1198 });
1199}
1200
1201#[gpui::test]
1202async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1203 let text_2 = "
1204 fn one() -> usize {
1205 1
1206 }
1207 "
1208 .unindent();
1209 let text_1 = "
1210 fn one() -> usize {
1211 0
1212 }
1213 "
1214 .unindent();
1215
1216 let fs = FakeFs::new(server_cx.executor());
1217 fs.insert_tree(
1218 "/code",
1219 json!({
1220 "project1": {
1221 ".git": {},
1222 "src": {
1223 "lib.rs": text_2
1224 },
1225 "README.md": "# project 1",
1226 },
1227 }),
1228 )
1229 .await;
1230 fs.set_index_for_repo(
1231 Path::new("/code/project1/.git"),
1232 &[("src/lib.rs".into(), text_1.clone())],
1233 );
1234 fs.set_head_for_repo(
1235 Path::new("/code/project1/.git"),
1236 &[("src/lib.rs".into(), text_1.clone())],
1237 );
1238
1239 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1240 let (worktree, _) = project
1241 .update(cx, |project, cx| {
1242 project.find_or_create_worktree("/code/project1", true, cx)
1243 })
1244 .await
1245 .unwrap();
1246 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1247 cx.executor().run_until_parked();
1248
1249 let buffer = project
1250 .update(cx, |project, cx| {
1251 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1252 })
1253 .await
1254 .unwrap();
1255 let diff = project
1256 .update(cx, |project, cx| {
1257 project.open_uncommitted_diff(buffer.clone(), cx)
1258 })
1259 .await
1260 .unwrap();
1261
1262 diff.read_with(cx, |diff, cx| {
1263 assert_eq!(diff.base_text_string().unwrap(), text_1);
1264 assert_eq!(
1265 diff.secondary_diff()
1266 .unwrap()
1267 .read(cx)
1268 .base_text_string()
1269 .unwrap(),
1270 text_1
1271 );
1272 });
1273
1274 // stage the current buffer's contents
1275 fs.set_index_for_repo(
1276 Path::new("/code/project1/.git"),
1277 &[("src/lib.rs".into(), text_2.clone())],
1278 );
1279
1280 cx.executor().run_until_parked();
1281 diff.read_with(cx, |diff, cx| {
1282 assert_eq!(diff.base_text_string().unwrap(), text_1);
1283 assert_eq!(
1284 diff.secondary_diff()
1285 .unwrap()
1286 .read(cx)
1287 .base_text_string()
1288 .unwrap(),
1289 text_2
1290 );
1291 });
1292
1293 // commit the current buffer's contents
1294 fs.set_head_for_repo(
1295 Path::new("/code/project1/.git"),
1296 &[("src/lib.rs".into(), text_2.clone())],
1297 );
1298
1299 cx.executor().run_until_parked();
1300 diff.read_with(cx, |diff, cx| {
1301 assert_eq!(diff.base_text_string().unwrap(), text_2);
1302 assert_eq!(
1303 diff.secondary_diff()
1304 .unwrap()
1305 .read(cx)
1306 .base_text_string()
1307 .unwrap(),
1308 text_2
1309 );
1310 });
1311}
1312
1313#[gpui::test]
1314async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1315 let fs = FakeFs::new(server_cx.executor());
1316 fs.insert_tree(
1317 "/code",
1318 json!({
1319 "project1": {
1320 ".git": {},
1321 "README.md": "# project 1",
1322 },
1323 }),
1324 )
1325 .await;
1326
1327 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1328 let branches = ["main", "dev", "feature-1"];
1329 let branches_set = branches
1330 .iter()
1331 .map(ToString::to_string)
1332 .collect::<HashSet<_>>();
1333 fs.insert_branches(Path::new("/code/project1/.git"), &branches);
1334
1335 let (worktree, _) = project
1336 .update(cx, |project, cx| {
1337 project.find_or_create_worktree("/code/project1", true, cx)
1338 })
1339 .await
1340 .unwrap();
1341
1342 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1343 let root_path = ProjectPath::root_path(worktree_id);
1344 // Give the worktree a bit of time to index the file system
1345 cx.run_until_parked();
1346
1347 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1348
1349 let remote_branches = repository
1350 .update(cx, |repository, _| repository.branches())
1351 .await
1352 .unwrap()
1353 .unwrap();
1354
1355 let new_branch = branches[2];
1356
1357 let remote_branches = remote_branches
1358 .into_iter()
1359 .map(|branch| branch.name.to_string())
1360 .collect::<HashSet<_>>();
1361
1362 assert_eq!(&remote_branches, &branches_set);
1363
1364 cx.update(|cx| repository.read(cx).change_branch(new_branch))
1365 .await
1366 .unwrap()
1367 .unwrap();
1368
1369 cx.run_until_parked();
1370
1371 let server_branch = server_cx.update(|cx| {
1372 headless_project.update(cx, |headless_project, cx| {
1373 headless_project
1374 .worktree_store
1375 .update(cx, |worktree_store, cx| {
1376 worktree_store
1377 .current_branch(root_path.clone(), cx)
1378 .unwrap()
1379 })
1380 })
1381 });
1382
1383 assert_eq!(server_branch.name, branches[2]);
1384
1385 // Also try creating a new branch
1386 cx.update(|cx| repository.read(cx).create_branch("totally-new-branch"))
1387 .await
1388 .unwrap()
1389 .unwrap();
1390
1391 cx.update(|cx| repository.read(cx).change_branch("totally-new-branch"))
1392 .await
1393 .unwrap()
1394 .unwrap();
1395
1396 cx.run_until_parked();
1397
1398 let server_branch = server_cx.update(|cx| {
1399 headless_project.update(cx, |headless_project, cx| {
1400 headless_project
1401 .worktree_store
1402 .update(cx, |worktree_store, cx| {
1403 worktree_store.current_branch(root_path, cx).unwrap()
1404 })
1405 })
1406 });
1407
1408 assert_eq!(server_branch.name, "totally-new-branch");
1409}
1410
1411pub async fn init_test(
1412 server_fs: &Arc<FakeFs>,
1413 cx: &mut TestAppContext,
1414 server_cx: &mut TestAppContext,
1415) -> (Entity<Project>, Entity<HeadlessProject>) {
1416 let server_fs = server_fs.clone();
1417 cx.update(|cx| {
1418 release_channel::init(SemanticVersion::default(), cx);
1419 });
1420 server_cx.update(|cx| {
1421 release_channel::init(SemanticVersion::default(), cx);
1422 });
1423 init_logger();
1424
1425 let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
1426 let http_client = Arc::new(BlockedHttpClient);
1427 let node_runtime = NodeRuntime::unavailable();
1428 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1429 let proxy = Arc::new(ExtensionHostProxy::new());
1430 server_cx.update(HeadlessProject::init);
1431 let headless = server_cx.new(|cx| {
1432 client::init_settings(cx);
1433
1434 HeadlessProject::new(
1435 crate::HeadlessAppState {
1436 session: ssh_server_client,
1437 fs: server_fs.clone(),
1438 http_client,
1439 node_runtime,
1440 languages,
1441 extension_host_proxy: proxy,
1442 },
1443 cx,
1444 )
1445 });
1446
1447 let ssh = SshRemoteClient::fake_client(opts, cx).await;
1448 let project = build_project(ssh, cx);
1449 project
1450 .update(cx, {
1451 let headless = headless.clone();
1452 |_, cx| cx.on_release(|_, _| drop(headless))
1453 })
1454 .detach();
1455 (project, headless)
1456}
1457
1458fn init_logger() {
1459 if std::env::var("RUST_LOG").is_ok() {
1460 env_logger::try_init().ok();
1461 }
1462}
1463
1464fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1465 cx.update(|cx| {
1466 if !cx.has_global::<SettingsStore>() {
1467 let settings_store = SettingsStore::test(cx);
1468 cx.set_global(settings_store);
1469 }
1470 });
1471
1472 let client = cx.update(|cx| {
1473 Client::new(
1474 Arc::new(FakeSystemClock::new()),
1475 FakeHttpClient::with_404_response(),
1476 cx,
1477 )
1478 });
1479
1480 let node = NodeRuntime::unavailable();
1481 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1482 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1483 let fs = FakeFs::new(cx.executor());
1484
1485 cx.update(|cx| {
1486 Project::init(&client, cx);
1487 language::init(cx);
1488 });
1489
1490 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1491}