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