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