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