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