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