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 fs.insert_tree(
791 path!("/code"),
792 json!({
793 "project1": {
794 ".git": {},
795 "README.md": "# project 1",
796 "src": {
797 "lib.rs": "fn one() -> usize { 1 }"
798 }
799 },
800 }),
801 )
802 .await;
803
804 let (project, _headless) = init_test(&fs, cx, server_cx).await;
805 let (worktree, _) = project
806 .update(cx, |project, cx| {
807 project.find_or_create_worktree(path!("/code/project1"), true, cx)
808 })
809 .await
810 .unwrap();
811
812 let worktree_id = cx.update(|cx| worktree.read(cx).id());
813
814 let buffer = project
815 .update(cx, |project, cx| {
816 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
817 })
818 .await
819 .unwrap();
820
821 let path = project
822 .update(cx, |project, cx| {
823 project.resolve_path_in_buffer(path!("/code/project1/README.md"), &buffer, cx)
824 })
825 .await
826 .unwrap();
827 assert!(path.is_file());
828 assert_eq!(
829 path.abs_path().unwrap().to_string_lossy(),
830 path!("/code/project1/README.md")
831 );
832
833 let path = project
834 .update(cx, |project, cx| {
835 project.resolve_path_in_buffer("../README.md", &buffer, cx)
836 })
837 .await
838 .unwrap();
839 assert!(path.is_file());
840 assert_eq!(
841 path.project_path().unwrap().clone(),
842 ProjectPath::from((worktree_id, "README.md"))
843 );
844
845 let path = project
846 .update(cx, |project, cx| {
847 project.resolve_path_in_buffer("../src", &buffer, cx)
848 })
849 .await
850 .unwrap();
851 assert_eq!(
852 path.project_path().unwrap().clone(),
853 ProjectPath::from((worktree_id, "src"))
854 );
855 assert!(path.is_dir());
856}
857
858#[gpui::test]
859async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
860 let fs = FakeFs::new(server_cx.executor());
861 fs.insert_tree(
862 path!("/code"),
863 json!({
864 "project1": {
865 ".git": {},
866 "README.md": "# project 1",
867 "src": {
868 "lib.rs": "fn one() -> usize { 1 }"
869 }
870 },
871 }),
872 )
873 .await;
874
875 let (project, _headless) = init_test(&fs, cx, server_cx).await;
876
877 let path = project
878 .update(cx, |project, cx| {
879 project.resolve_abs_path(path!("/code/project1/README.md"), cx)
880 })
881 .await
882 .unwrap();
883
884 assert!(path.is_file());
885 assert_eq!(
886 path.abs_path().unwrap().to_string_lossy(),
887 path!("/code/project1/README.md")
888 );
889
890 let path = project
891 .update(cx, |project, cx| {
892 project.resolve_abs_path(path!("/code/project1/src"), cx)
893 })
894 .await
895 .unwrap();
896
897 assert!(path.is_dir());
898 assert_eq!(
899 path.abs_path().unwrap().to_string_lossy(),
900 path!("/code/project1/src")
901 );
902
903 let path = project
904 .update(cx, |project, cx| {
905 project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
906 })
907 .await;
908 assert!(path.is_none());
909}
910
911#[gpui::test(iterations = 10)]
912async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
913 let fs = FakeFs::new(server_cx.executor());
914 fs.insert_tree(
915 "/code",
916 json!({
917 "project1": {
918 ".git": {},
919 "README.md": "# project 1",
920 "src": {
921 "lib.rs": "fn one() -> usize { 1 }"
922 }
923 },
924 }),
925 )
926 .await;
927
928 let (project, _headless) = init_test(&fs, cx, server_cx).await;
929 let (worktree, _) = project
930 .update(cx, |project, cx| {
931 project.find_or_create_worktree("/code/project1", true, cx)
932 })
933 .await
934 .unwrap();
935 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
936
937 // Open a buffer on the client but cancel after a random amount of time.
938 let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
939 cx.executor().simulate_random_delay().await;
940 drop(buffer);
941
942 // Try opening the same buffer again as the client, and ensure we can
943 // still do it despite the cancellation above.
944 let buffer = project
945 .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
946 .await
947 .unwrap();
948
949 buffer.read_with(cx, |buf, _| {
950 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
951 });
952}
953
954#[gpui::test]
955async fn test_adding_then_removing_then_adding_worktrees(
956 cx: &mut TestAppContext,
957 server_cx: &mut TestAppContext,
958) {
959 let fs = FakeFs::new(server_cx.executor());
960 fs.insert_tree(
961 path!("/code"),
962 json!({
963 "project1": {
964 ".git": {},
965 "README.md": "# project 1",
966 "src": {
967 "lib.rs": "fn one() -> usize { 1 }"
968 }
969 },
970 "project2": {
971 "README.md": "# project 2",
972 },
973 }),
974 )
975 .await;
976
977 let (project, _headless) = init_test(&fs, cx, server_cx).await;
978 let (_worktree, _) = project
979 .update(cx, |project, cx| {
980 project.find_or_create_worktree(path!("/code/project1"), true, cx)
981 })
982 .await
983 .unwrap();
984
985 let (worktree_2, _) = project
986 .update(cx, |project, cx| {
987 project.find_or_create_worktree(path!("/code/project2"), true, cx)
988 })
989 .await
990 .unwrap();
991 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
992
993 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
994
995 let (worktree_2, _) = project
996 .update(cx, |project, cx| {
997 project.find_or_create_worktree(path!("/code/project2"), true, cx)
998 })
999 .await
1000 .unwrap();
1001
1002 cx.run_until_parked();
1003 worktree_2.update(cx, |worktree, _cx| {
1004 assert!(worktree.is_visible());
1005 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1006 assert_eq!(entries.len(), 2);
1007 assert_eq!(
1008 entries[1].path.to_string_lossy().to_string(),
1009 "README.md".to_string()
1010 )
1011 })
1012}
1013
1014#[gpui::test]
1015async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1016 let fs = FakeFs::new(server_cx.executor());
1017 fs.insert_tree(
1018 path!("/code"),
1019 json!({
1020 "project1": {
1021 ".git": {},
1022 "README.md": "# project 1",
1023 "src": {
1024 "lib.rs": "fn one() -> usize { 1 }"
1025 }
1026 },
1027 }),
1028 )
1029 .await;
1030
1031 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1032 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1033 cx.executor().run_until_parked();
1034
1035 let buffer = buffer.await.unwrap();
1036
1037 cx.update(|cx| {
1038 assert_eq!(
1039 buffer.read(cx).text(),
1040 initial_server_settings_content()
1041 .to_string()
1042 .replace("\r\n", "\n")
1043 )
1044 })
1045}
1046
1047#[gpui::test(iterations = 20)]
1048async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1049 let fs = FakeFs::new(server_cx.executor());
1050 fs.insert_tree(
1051 path!("/code"),
1052 json!({
1053 "project1": {
1054 ".git": {},
1055 "README.md": "# project 1",
1056 "src": {
1057 "lib.rs": "fn one() -> usize { 1 }"
1058 }
1059 },
1060 }),
1061 )
1062 .await;
1063
1064 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1065
1066 let (worktree, _) = project
1067 .update(cx, |project, cx| {
1068 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1069 })
1070 .await
1071 .unwrap();
1072
1073 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1074 let buffer = project
1075 .update(cx, |project, cx| {
1076 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1077 })
1078 .await
1079 .unwrap();
1080
1081 buffer.update(cx, |buffer, cx| {
1082 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1083 let ix = buffer.text().find('1').unwrap();
1084 buffer.edit([(ix..ix + 1, "100")], None, cx);
1085 });
1086
1087 let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
1088 client
1089 .update(cx, |client, cx| client.simulate_disconnect(cx))
1090 .detach();
1091
1092 project
1093 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1094 .await
1095 .unwrap();
1096
1097 assert_eq!(
1098 fs.load(path!("/code/project1/src/lib.rs").as_ref())
1099 .await
1100 .unwrap(),
1101 "fn one() -> usize { 100 }"
1102 );
1103}
1104
1105#[gpui::test]
1106async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1107 let fs = FakeFs::new(server_cx.executor());
1108 fs.insert_tree(
1109 "/code",
1110 json!({
1111 "project1": {
1112 ".git": {},
1113 "README.md": "# project 1",
1114 },
1115 }),
1116 )
1117 .await;
1118
1119 let (project, _) = init_test(&fs, cx, server_cx).await;
1120
1121 let (worktree, _) = project
1122 .update(cx, |project, cx| {
1123 project.find_or_create_worktree("/code/project1", true, cx)
1124 })
1125 .await
1126 .unwrap();
1127
1128 cx.run_until_parked();
1129
1130 fs.rename(
1131 &PathBuf::from("/code/project1"),
1132 &PathBuf::from("/code/project2"),
1133 Default::default(),
1134 )
1135 .await
1136 .unwrap();
1137
1138 cx.run_until_parked();
1139 worktree.update(cx, |worktree, _| {
1140 assert_eq!(worktree.root_name(), "project2")
1141 })
1142}
1143
1144#[gpui::test]
1145async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1146 let fs = FakeFs::new(server_cx.executor());
1147 fs.insert_tree(
1148 "/code",
1149 json!({
1150 "project1": {
1151 ".git": {},
1152 "README.md": "# project 1",
1153 },
1154 }),
1155 )
1156 .await;
1157
1158 let (project, _) = init_test(&fs, cx, server_cx).await;
1159 let (worktree, _) = project
1160 .update(cx, |project, cx| {
1161 project.find_or_create_worktree("/code/project1", true, cx)
1162 })
1163 .await
1164 .unwrap();
1165
1166 cx.run_until_parked();
1167
1168 let entry = worktree
1169 .update(cx, |worktree, cx| {
1170 let entry = worktree.entry_for_path("README.md").unwrap();
1171 worktree.rename_entry(entry.id, Path::new("README.rst"), cx)
1172 })
1173 .await
1174 .unwrap()
1175 .to_included()
1176 .unwrap();
1177
1178 cx.run_until_parked();
1179
1180 worktree.update(cx, |worktree, _| {
1181 assert_eq!(worktree.entry_for_path("README.rst").unwrap().id, entry.id)
1182 });
1183}
1184
1185#[gpui::test]
1186async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1187 let text_2 = "
1188 fn one() -> usize {
1189 1
1190 }
1191 "
1192 .unindent();
1193 let text_1 = "
1194 fn one() -> usize {
1195 0
1196 }
1197 "
1198 .unindent();
1199
1200 let fs = FakeFs::new(server_cx.executor());
1201 fs.insert_tree(
1202 "/code",
1203 json!({
1204 "project1": {
1205 ".git": {},
1206 "src": {
1207 "lib.rs": text_2
1208 },
1209 "README.md": "# project 1",
1210 },
1211 }),
1212 )
1213 .await;
1214 fs.set_index_for_repo(
1215 Path::new("/code/project1/.git"),
1216 &[("src/lib.rs".into(), text_1.clone())],
1217 );
1218 fs.set_head_for_repo(
1219 Path::new("/code/project1/.git"),
1220 &[("src/lib.rs".into(), text_1.clone())],
1221 );
1222
1223 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1224 let (worktree, _) = project
1225 .update(cx, |project, cx| {
1226 project.find_or_create_worktree("/code/project1", true, cx)
1227 })
1228 .await
1229 .unwrap();
1230 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1231 cx.executor().run_until_parked();
1232
1233 let buffer = project
1234 .update(cx, |project, cx| {
1235 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1236 })
1237 .await
1238 .unwrap();
1239 let diff = project
1240 .update(cx, |project, cx| {
1241 project.open_uncommitted_diff(buffer.clone(), cx)
1242 })
1243 .await
1244 .unwrap();
1245
1246 diff.read_with(cx, |diff, cx| {
1247 assert_eq!(diff.base_text_string().unwrap(), text_1);
1248 assert_eq!(
1249 diff.secondary_diff()
1250 .unwrap()
1251 .read(cx)
1252 .base_text_string()
1253 .unwrap(),
1254 text_1
1255 );
1256 });
1257
1258 // stage the current buffer's contents
1259 fs.set_index_for_repo(
1260 Path::new("/code/project1/.git"),
1261 &[("src/lib.rs".into(), text_2.clone())],
1262 );
1263
1264 cx.executor().run_until_parked();
1265 diff.read_with(cx, |diff, cx| {
1266 assert_eq!(diff.base_text_string().unwrap(), text_1);
1267 assert_eq!(
1268 diff.secondary_diff()
1269 .unwrap()
1270 .read(cx)
1271 .base_text_string()
1272 .unwrap(),
1273 text_2
1274 );
1275 });
1276
1277 // commit the current buffer's contents
1278 fs.set_head_for_repo(
1279 Path::new("/code/project1/.git"),
1280 &[("src/lib.rs".into(), text_2.clone())],
1281 );
1282
1283 cx.executor().run_until_parked();
1284 diff.read_with(cx, |diff, cx| {
1285 assert_eq!(diff.base_text_string().unwrap(), text_2);
1286 assert_eq!(
1287 diff.secondary_diff()
1288 .unwrap()
1289 .read(cx)
1290 .base_text_string()
1291 .unwrap(),
1292 text_2
1293 );
1294 });
1295}
1296
1297#[gpui::test]
1298async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1299 let fs = FakeFs::new(server_cx.executor());
1300 fs.insert_tree(
1301 "/code",
1302 json!({
1303 "project1": {
1304 ".git": {},
1305 "README.md": "# project 1",
1306 },
1307 }),
1308 )
1309 .await;
1310
1311 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1312 let branches = ["main", "dev", "feature-1"];
1313 let branches_set = branches
1314 .iter()
1315 .map(ToString::to_string)
1316 .collect::<HashSet<_>>();
1317 fs.insert_branches(Path::new("/code/project1/.git"), &branches);
1318
1319 let (worktree, _) = project
1320 .update(cx, |project, cx| {
1321 project.find_or_create_worktree("/code/project1", true, cx)
1322 })
1323 .await
1324 .unwrap();
1325
1326 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1327 let root_path = ProjectPath::root_path(worktree_id);
1328 // Give the worktree a bit of time to index the file system
1329 cx.run_until_parked();
1330
1331 let remote_branches = project
1332 .update(cx, |project, cx| project.branches(root_path.clone(), cx))
1333 .await
1334 .unwrap();
1335
1336 let new_branch = branches[2];
1337
1338 let remote_branches = remote_branches
1339 .into_iter()
1340 .map(|branch| branch.name.to_string())
1341 .collect::<HashSet<_>>();
1342
1343 assert_eq!(&remote_branches, &branches_set);
1344
1345 cx.update(|cx| {
1346 project.update(cx, |project, cx| {
1347 project.update_or_create_branch(root_path.clone(), new_branch.to_string(), cx)
1348 })
1349 })
1350 .await
1351 .unwrap();
1352
1353 cx.run_until_parked();
1354
1355 let server_branch = server_cx.update(|cx| {
1356 headless_project.update(cx, |headless_project, cx| {
1357 headless_project
1358 .worktree_store
1359 .update(cx, |worktree_store, cx| {
1360 worktree_store
1361 .current_branch(root_path.clone(), cx)
1362 .unwrap()
1363 })
1364 })
1365 });
1366
1367 assert_eq!(server_branch.name, branches[2]);
1368
1369 // Also try creating a new branch
1370 cx.update(|cx| {
1371 project.update(cx, |project, cx| {
1372 project.update_or_create_branch(root_path.clone(), "totally-new-branch".to_string(), cx)
1373 })
1374 })
1375 .await
1376 .unwrap();
1377
1378 cx.run_until_parked();
1379
1380 let server_branch = server_cx.update(|cx| {
1381 headless_project.update(cx, |headless_project, cx| {
1382 headless_project
1383 .worktree_store
1384 .update(cx, |worktree_store, cx| {
1385 worktree_store.current_branch(root_path, cx).unwrap()
1386 })
1387 })
1388 });
1389
1390 assert_eq!(server_branch.name, "totally-new-branch");
1391}
1392
1393pub async fn init_test(
1394 server_fs: &Arc<FakeFs>,
1395 cx: &mut TestAppContext,
1396 server_cx: &mut TestAppContext,
1397) -> (Entity<Project>, Entity<HeadlessProject>) {
1398 let server_fs = server_fs.clone();
1399 cx.update(|cx| {
1400 release_channel::init(SemanticVersion::default(), cx);
1401 });
1402 server_cx.update(|cx| {
1403 release_channel::init(SemanticVersion::default(), cx);
1404 });
1405 init_logger();
1406
1407 let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
1408 let http_client = Arc::new(BlockedHttpClient);
1409 let node_runtime = NodeRuntime::unavailable();
1410 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1411 let proxy = Arc::new(ExtensionHostProxy::new());
1412 server_cx.update(HeadlessProject::init);
1413 let headless = server_cx.new(|cx| {
1414 client::init_settings(cx);
1415
1416 HeadlessProject::new(
1417 crate::HeadlessAppState {
1418 session: ssh_server_client,
1419 fs: server_fs.clone(),
1420 http_client,
1421 node_runtime,
1422 languages,
1423 extension_host_proxy: proxy,
1424 },
1425 cx,
1426 )
1427 });
1428
1429 let ssh = SshRemoteClient::fake_client(opts, cx).await;
1430 let project = build_project(ssh, cx);
1431 project
1432 .update(cx, {
1433 let headless = headless.clone();
1434 |_, cx| cx.on_release(|_, _| drop(headless))
1435 })
1436 .detach();
1437 (project, headless)
1438}
1439
1440fn init_logger() {
1441 if std::env::var("RUST_LOG").is_ok() {
1442 env_logger::try_init().ok();
1443 }
1444}
1445
1446fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1447 cx.update(|cx| {
1448 if !cx.has_global::<SettingsStore>() {
1449 let settings_store = SettingsStore::test(cx);
1450 cx.set_global(settings_store);
1451 }
1452 });
1453
1454 let client = cx.update(|cx| {
1455 Client::new(
1456 Arc::new(FakeSystemClock::new()),
1457 FakeHttpClient::with_404_response(),
1458 cx,
1459 )
1460 });
1461
1462 let node = NodeRuntime::unavailable();
1463 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1464 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1465 let fs = FakeFs::new(cx.executor());
1466
1467 cx.update(|cx| {
1468 Project::init(&client, cx);
1469 language::init(cx);
1470 });
1471
1472 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1473}