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