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