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