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#[gpui::test]
1208async fn test_copy_file_into_remote_project(
1209 cx: &mut TestAppContext,
1210 server_cx: &mut TestAppContext,
1211) {
1212 let remote_fs = FakeFs::new(server_cx.executor());
1213 remote_fs
1214 .insert_tree(
1215 path!("/code"),
1216 json!({
1217 "project1": {
1218 ".git": {},
1219 "README.md": "# project 1",
1220 "src": {
1221 "main.rs": ""
1222 }
1223 },
1224 }),
1225 )
1226 .await;
1227
1228 let (project, _) = init_test(&remote_fs, cx, server_cx).await;
1229 let (worktree, _) = project
1230 .update(cx, |project, cx| {
1231 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1232 })
1233 .await
1234 .unwrap();
1235
1236 cx.run_until_parked();
1237
1238 let local_fs = project
1239 .read_with(cx, |project, _| project.fs().clone())
1240 .as_fake();
1241 local_fs
1242 .insert_tree(
1243 path!("/local-code"),
1244 json!({
1245 "dir1": {
1246 "file1": "file 1 content",
1247 "dir2": {
1248 "file2": "file 2 content",
1249 "dir3": {
1250 "file3": ""
1251 },
1252 "dir4": {}
1253 },
1254 "dir5": {}
1255 },
1256 "file4": "file 4 content"
1257 }),
1258 )
1259 .await;
1260
1261 worktree
1262 .update(cx, |worktree, cx| {
1263 worktree.copy_external_entries(
1264 Path::new("src").into(),
1265 vec![
1266 Path::new(path!("/local-code/dir1/file1")).into(),
1267 Path::new(path!("/local-code/dir1/dir2")).into(),
1268 ],
1269 local_fs.clone(),
1270 cx,
1271 )
1272 })
1273 .await
1274 .unwrap();
1275
1276 assert_eq!(
1277 remote_fs.paths(true),
1278 vec![
1279 PathBuf::from(path!("/")),
1280 PathBuf::from(path!("/code")),
1281 PathBuf::from(path!("/code/project1")),
1282 PathBuf::from(path!("/code/project1/.git")),
1283 PathBuf::from(path!("/code/project1/README.md")),
1284 PathBuf::from(path!("/code/project1/src")),
1285 PathBuf::from(path!("/code/project1/src/dir2")),
1286 PathBuf::from(path!("/code/project1/src/file1")),
1287 PathBuf::from(path!("/code/project1/src/main.rs")),
1288 PathBuf::from(path!("/code/project1/src/dir2/dir3")),
1289 PathBuf::from(path!("/code/project1/src/dir2/dir4")),
1290 PathBuf::from(path!("/code/project1/src/dir2/file2")),
1291 PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
1292 ]
1293 );
1294 assert_eq!(
1295 remote_fs
1296 .load(path!("/code/project1/src/file1").as_ref())
1297 .await
1298 .unwrap(),
1299 "file 1 content"
1300 );
1301 assert_eq!(
1302 remote_fs
1303 .load(path!("/code/project1/src/dir2/file2").as_ref())
1304 .await
1305 .unwrap(),
1306 "file 2 content"
1307 );
1308 assert_eq!(
1309 remote_fs
1310 .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
1311 .await
1312 .unwrap(),
1313 ""
1314 );
1315}
1316
1317// TODO: this test fails on Windows.
1318#[cfg(not(windows))]
1319#[gpui::test]
1320async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1321 let text_2 = "
1322 fn one() -> usize {
1323 1
1324 }
1325 "
1326 .unindent();
1327 let text_1 = "
1328 fn one() -> usize {
1329 0
1330 }
1331 "
1332 .unindent();
1333
1334 let fs = FakeFs::new(server_cx.executor());
1335 fs.insert_tree(
1336 "/code",
1337 json!({
1338 "project1": {
1339 ".git": {},
1340 "src": {
1341 "lib.rs": text_2
1342 },
1343 "README.md": "# project 1",
1344 },
1345 }),
1346 )
1347 .await;
1348 fs.set_index_for_repo(
1349 Path::new("/code/project1/.git"),
1350 &[("src/lib.rs".into(), text_1.clone())],
1351 );
1352 fs.set_head_for_repo(
1353 Path::new("/code/project1/.git"),
1354 &[("src/lib.rs".into(), text_1.clone())],
1355 );
1356
1357 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1358 let (worktree, _) = project
1359 .update(cx, |project, cx| {
1360 project.find_or_create_worktree("/code/project1", true, cx)
1361 })
1362 .await
1363 .unwrap();
1364 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1365 cx.executor().run_until_parked();
1366
1367 let buffer = project
1368 .update(cx, |project, cx| {
1369 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
1370 })
1371 .await
1372 .unwrap();
1373 let diff = project
1374 .update(cx, |project, cx| {
1375 project.open_uncommitted_diff(buffer.clone(), cx)
1376 })
1377 .await
1378 .unwrap();
1379
1380 diff.read_with(cx, |diff, cx| {
1381 assert_eq!(diff.base_text_string().unwrap(), text_1);
1382 assert_eq!(
1383 diff.secondary_diff()
1384 .unwrap()
1385 .read(cx)
1386 .base_text_string()
1387 .unwrap(),
1388 text_1
1389 );
1390 });
1391
1392 // stage the current buffer's contents
1393 fs.set_index_for_repo(
1394 Path::new("/code/project1/.git"),
1395 &[("src/lib.rs".into(), text_2.clone())],
1396 );
1397
1398 cx.executor().run_until_parked();
1399 diff.read_with(cx, |diff, cx| {
1400 assert_eq!(diff.base_text_string().unwrap(), text_1);
1401 assert_eq!(
1402 diff.secondary_diff()
1403 .unwrap()
1404 .read(cx)
1405 .base_text_string()
1406 .unwrap(),
1407 text_2
1408 );
1409 });
1410
1411 // commit the current buffer's contents
1412 fs.set_head_for_repo(
1413 Path::new("/code/project1/.git"),
1414 &[("src/lib.rs".into(), text_2.clone())],
1415 );
1416
1417 cx.executor().run_until_parked();
1418 diff.read_with(cx, |diff, cx| {
1419 assert_eq!(diff.base_text_string().unwrap(), text_2);
1420 assert_eq!(
1421 diff.secondary_diff()
1422 .unwrap()
1423 .read(cx)
1424 .base_text_string()
1425 .unwrap(),
1426 text_2
1427 );
1428 });
1429}
1430
1431#[gpui::test]
1432async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1433 let fs = FakeFs::new(server_cx.executor());
1434 fs.insert_tree(
1435 path!("/code"),
1436 json!({
1437 "project1": {
1438 ".git": {},
1439 "README.md": "# project 1",
1440 },
1441 }),
1442 )
1443 .await;
1444
1445 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1446 let branches = ["main", "dev", "feature-1"];
1447 let branches_set = branches
1448 .iter()
1449 .map(ToString::to_string)
1450 .collect::<HashSet<_>>();
1451 fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1452
1453 let (_worktree, _) = project
1454 .update(cx, |project, cx| {
1455 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1456 })
1457 .await
1458 .unwrap();
1459 // Give the worktree a bit of time to index the file system
1460 cx.run_until_parked();
1461
1462 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1463
1464 let remote_branches = repository
1465 .update(cx, |repository, _| repository.branches())
1466 .await
1467 .unwrap()
1468 .unwrap();
1469
1470 let new_branch = branches[2];
1471
1472 let remote_branches = remote_branches
1473 .into_iter()
1474 .map(|branch| branch.name.to_string())
1475 .collect::<HashSet<_>>();
1476
1477 assert_eq!(&remote_branches, &branches_set);
1478
1479 cx.update(|cx| {
1480 repository.update(cx, |repository, _cx| {
1481 repository.change_branch(new_branch.to_string())
1482 })
1483 })
1484 .await
1485 .unwrap()
1486 .unwrap();
1487
1488 cx.run_until_parked();
1489
1490 let server_branch = server_cx.update(|cx| {
1491 headless_project.update(cx, |headless_project, cx| {
1492 headless_project.git_store.update(cx, |git_store, cx| {
1493 git_store
1494 .repositories()
1495 .values()
1496 .next()
1497 .unwrap()
1498 .read(cx)
1499 .branch
1500 .as_ref()
1501 .unwrap()
1502 .clone()
1503 })
1504 })
1505 });
1506
1507 assert_eq!(server_branch.name, branches[2]);
1508
1509 // Also try creating a new branch
1510 cx.update(|cx| {
1511 repository.update(cx, |repo, _cx| {
1512 repo.create_branch("totally-new-branch".to_string())
1513 })
1514 })
1515 .await
1516 .unwrap()
1517 .unwrap();
1518
1519 cx.update(|cx| {
1520 repository.update(cx, |repo, _cx| {
1521 repo.change_branch("totally-new-branch".to_string())
1522 })
1523 })
1524 .await
1525 .unwrap()
1526 .unwrap();
1527
1528 cx.run_until_parked();
1529
1530 let server_branch = server_cx.update(|cx| {
1531 headless_project.update(cx, |headless_project, cx| {
1532 headless_project.git_store.update(cx, |git_store, cx| {
1533 git_store
1534 .repositories()
1535 .values()
1536 .next()
1537 .unwrap()
1538 .read(cx)
1539 .branch
1540 .as_ref()
1541 .unwrap()
1542 .clone()
1543 })
1544 })
1545 });
1546
1547 assert_eq!(server_branch.name, "totally-new-branch");
1548}
1549
1550pub async fn init_test(
1551 server_fs: &Arc<FakeFs>,
1552 cx: &mut TestAppContext,
1553 server_cx: &mut TestAppContext,
1554) -> (Entity<Project>, Entity<HeadlessProject>) {
1555 let server_fs = server_fs.clone();
1556 cx.update(|cx| {
1557 release_channel::init(SemanticVersion::default(), cx);
1558 });
1559 server_cx.update(|cx| {
1560 release_channel::init(SemanticVersion::default(), cx);
1561 });
1562 init_logger();
1563
1564 let (opts, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
1565 let http_client = Arc::new(BlockedHttpClient);
1566 let node_runtime = NodeRuntime::unavailable();
1567 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
1568 let debug_adapters = DapRegistry::default().into();
1569 let proxy = Arc::new(ExtensionHostProxy::new());
1570 server_cx.update(HeadlessProject::init);
1571 let headless = server_cx.new(|cx| {
1572 client::init_settings(cx);
1573
1574 HeadlessProject::new(
1575 crate::HeadlessAppState {
1576 session: ssh_server_client,
1577 fs: server_fs.clone(),
1578 http_client,
1579 node_runtime,
1580 languages,
1581 debug_adapters,
1582 extension_host_proxy: proxy,
1583 },
1584 cx,
1585 )
1586 });
1587
1588 let ssh = SshRemoteClient::fake_client(opts, cx).await;
1589 let project = build_project(ssh, cx);
1590 project
1591 .update(cx, {
1592 let headless = headless.clone();
1593 |_, cx| cx.on_release(|_, _| drop(headless))
1594 })
1595 .detach();
1596 (project, headless)
1597}
1598
1599fn init_logger() {
1600 if std::env::var("RUST_LOG").is_ok() {
1601 env_logger::try_init().ok();
1602 }
1603}
1604
1605fn build_project(ssh: Entity<SshRemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
1606 cx.update(|cx| {
1607 if !cx.has_global::<SettingsStore>() {
1608 let settings_store = SettingsStore::test(cx);
1609 cx.set_global(settings_store);
1610 }
1611 });
1612
1613 let client = cx.update(|cx| {
1614 Client::new(
1615 Arc::new(FakeSystemClock::new()),
1616 FakeHttpClient::with_404_response(),
1617 cx,
1618 )
1619 });
1620
1621 let node = NodeRuntime::unavailable();
1622 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
1623 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
1624 let fs = FakeFs::new(cx.executor());
1625
1626 cx.update(|cx| {
1627 Project::init(&client, cx);
1628 language::init(cx);
1629 });
1630
1631 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
1632}