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