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