1use crate::headless_project::HeadlessProject;
2use client::{Client, UserStore};
3use clock::FakeSystemClock;
4use fs::{FakeFs, Fs};
5use gpui::{Context, Model, TestAppContext};
6use http_client::{BlockedHttpClient, FakeHttpClient};
7use language::{
8 language_settings::{language_settings, AllLanguageSettings},
9 Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName,
10 LineEnding,
11};
12use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind};
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 (project, _headless, fs) = init_test(cx, server_cx).await;
30 let (worktree, _) = project
31 .update(cx, |project, cx| {
32 project.find_or_create_worktree("/code/project1", true, cx)
33 })
34 .await
35 .unwrap();
36
37 // The client sees the worktree's contents.
38 cx.executor().run_until_parked();
39 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
40 worktree.update(cx, |worktree, _cx| {
41 assert_eq!(
42 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
43 vec![
44 Path::new("README.md"),
45 Path::new("src"),
46 Path::new("src/lib.rs"),
47 ]
48 );
49 });
50
51 // The user opens a buffer in the remote worktree. The buffer's
52 // contents are loaded from the remote filesystem.
53 let buffer = project
54 .update(cx, |project, cx| {
55 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
56 })
57 .await
58 .unwrap();
59
60 buffer.update(cx, |buffer, cx| {
61 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
62 assert_eq!(
63 buffer.diff_base().unwrap().to_string(),
64 "fn one() -> usize { 0 }"
65 );
66 let ix = buffer.text().find('1').unwrap();
67 buffer.edit([(ix..ix + 1, "100")], None, cx);
68 });
69
70 // The user saves the buffer. The new contents are written to the
71 // remote filesystem.
72 project
73 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
74 .await
75 .unwrap();
76 assert_eq!(
77 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
78 "fn one() -> usize { 100 }"
79 );
80
81 // A new file is created in the remote filesystem. The user
82 // sees the new file.
83 fs.save(
84 "/code/project1/src/main.rs".as_ref(),
85 &"fn main() {}".into(),
86 Default::default(),
87 )
88 .await
89 .unwrap();
90 cx.executor().run_until_parked();
91 worktree.update(cx, |worktree, _cx| {
92 assert_eq!(
93 worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
94 vec![
95 Path::new("README.md"),
96 Path::new("src"),
97 Path::new("src/lib.rs"),
98 Path::new("src/main.rs"),
99 ]
100 );
101 });
102
103 // A file that is currently open in a buffer is renamed.
104 fs.rename(
105 "/code/project1/src/lib.rs".as_ref(),
106 "/code/project1/src/lib2.rs".as_ref(),
107 Default::default(),
108 )
109 .await
110 .unwrap();
111 cx.executor().run_until_parked();
112 buffer.update(cx, |buffer, _| {
113 assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
114 });
115
116 fs.set_index_for_repo(
117 Path::new("/code/project1/.git"),
118 &[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())],
119 );
120 cx.executor().run_until_parked();
121 buffer.update(cx, |buffer, _| {
122 assert_eq!(
123 buffer.diff_base().unwrap().to_string(),
124 "fn one() -> usize { 100 }"
125 );
126 });
127}
128
129#[gpui::test]
130async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
131 let (project, headless, _) = init_test(cx, server_cx).await;
132
133 project
134 .update(cx, |project, cx| {
135 project.find_or_create_worktree("/code/project1", true, cx)
136 })
137 .await
138 .unwrap();
139
140 cx.run_until_parked();
141
142 async fn do_search(project: &Model<Project>, mut cx: TestAppContext) -> Model<Buffer> {
143 let mut receiver = project.update(&mut cx, |project, cx| {
144 project.search(
145 SearchQuery::text(
146 "project",
147 false,
148 true,
149 false,
150 Default::default(),
151 Default::default(),
152 None,
153 )
154 .unwrap(),
155 cx,
156 )
157 });
158
159 let first_response = receiver.next().await.unwrap();
160 let SearchResult::Buffer { buffer, .. } = first_response else {
161 panic!("incorrect result");
162 };
163 buffer.update(&mut cx, |buffer, cx| {
164 assert_eq!(
165 buffer.file().unwrap().full_path(cx).to_string_lossy(),
166 "project1/README.md"
167 )
168 });
169
170 assert!(receiver.next().await.is_none());
171 buffer
172 }
173
174 let buffer = do_search(&project, cx.clone()).await;
175
176 // test that the headless server is tracking which buffers we have open correctly.
177 cx.run_until_parked();
178 headless.update(server_cx, |headless, cx| {
179 assert!(!headless.buffer_store.read(cx).shared_buffers().is_empty())
180 });
181 do_search(&project, cx.clone()).await;
182
183 cx.update(|_| {
184 drop(buffer);
185 });
186 cx.run_until_parked();
187 headless.update(server_cx, |headless, cx| {
188 assert!(headless.buffer_store.read(cx).shared_buffers().is_empty())
189 });
190
191 do_search(&project, cx.clone()).await;
192}
193
194#[gpui::test]
195async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
196 let (project, headless, fs) = init_test(cx, server_cx).await;
197
198 cx.update_global(|settings_store: &mut SettingsStore, cx| {
199 settings_store.set_user_settings(
200 r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
201 cx,
202 )
203 })
204 .unwrap();
205
206 cx.run_until_parked();
207
208 server_cx.read(|cx| {
209 assert_eq!(
210 AllLanguageSettings::get_global(cx)
211 .language(None, Some(&"Rust".into()), cx)
212 .language_servers,
213 ["..."] // local settings are ignored
214 )
215 });
216
217 server_cx
218 .update_global(|settings_store: &mut SettingsStore, cx| {
219 settings_store.set_server_settings(
220 r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
221 cx,
222 )
223 })
224 .unwrap();
225
226 cx.run_until_parked();
227
228 server_cx.read(|cx| {
229 assert_eq!(
230 AllLanguageSettings::get_global(cx)
231 .language(None, Some(&"Rust".into()), cx)
232 .language_servers,
233 ["from-server-settings".to_string()]
234 )
235 });
236
237 fs.insert_tree(
238 "/code/project1/.zed",
239 json!({
240 "settings.json": r#"
241 {
242 "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
243 "lsp": {
244 "override-rust-analyzer": {
245 "binary": {
246 "path": "~/.cargo/bin/rust-analyzer"
247 }
248 }
249 }
250 }"#
251 }),
252 )
253 .await;
254
255 let worktree_id = project
256 .update(cx, |project, cx| {
257 project.find_or_create_worktree("/code/project1", true, cx)
258 })
259 .await
260 .unwrap()
261 .0
262 .read_with(cx, |worktree, _| worktree.id());
263
264 let buffer = project
265 .update(cx, |project, cx| {
266 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
267 })
268 .await
269 .unwrap();
270 cx.run_until_parked();
271
272 server_cx.read(|cx| {
273 let worktree_id = headless
274 .read(cx)
275 .worktree_store
276 .read(cx)
277 .worktrees()
278 .next()
279 .unwrap()
280 .read(cx)
281 .id();
282 assert_eq!(
283 AllLanguageSettings::get(
284 Some(SettingsLocation {
285 worktree_id,
286 path: Path::new("src/lib.rs")
287 }),
288 cx
289 )
290 .language(None, Some(&"Rust".into()), cx)
291 .language_servers,
292 ["override-rust-analyzer".to_string()]
293 )
294 });
295
296 cx.read(|cx| {
297 let file = buffer.read(cx).file();
298 assert_eq!(
299 language_settings(Some("Rust".into()), file, cx).language_servers,
300 ["override-rust-analyzer".to_string()]
301 )
302 });
303}
304
305#[gpui::test]
306async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
307 let (project, headless, fs) = init_test(cx, server_cx).await;
308
309 fs.insert_tree(
310 "/code/project1/.zed",
311 json!({
312 "settings.json": r#"
313 {
314 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
315 "lsp": {
316 "rust-analyzer": {
317 "binary": {
318 "path": "~/.cargo/bin/rust-analyzer"
319 }
320 }
321 }
322 }"#
323 }),
324 )
325 .await;
326
327 cx.update_model(&project, |project, _| {
328 project.languages().register_test_language(LanguageConfig {
329 name: "Rust".into(),
330 matcher: LanguageMatcher {
331 path_suffixes: vec!["rs".into()],
332 ..Default::default()
333 },
334 ..Default::default()
335 });
336 project.languages().register_fake_lsp_adapter(
337 "Rust",
338 FakeLspAdapter {
339 name: "rust-analyzer",
340 ..Default::default()
341 },
342 )
343 });
344
345 let mut fake_lsp = server_cx.update(|cx| {
346 headless.read(cx).languages.register_fake_language_server(
347 LanguageServerName("rust-analyzer".into()),
348 Default::default(),
349 None,
350 )
351 });
352
353 cx.run_until_parked();
354
355 let worktree_id = project
356 .update(cx, |project, cx| {
357 project.find_or_create_worktree("/code/project1", true, cx)
358 })
359 .await
360 .unwrap()
361 .0
362 .read_with(cx, |worktree, _| worktree.id());
363
364 // Wait for the settings to synchronize
365 cx.run_until_parked();
366
367 let buffer = project
368 .update(cx, |project, cx| {
369 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
370 })
371 .await
372 .unwrap();
373 cx.run_until_parked();
374
375 let fake_lsp = fake_lsp.next().await.unwrap();
376
377 cx.read(|cx| {
378 let file = buffer.read(cx).file();
379 assert_eq!(
380 language_settings(Some("Rust".into()), file, cx).language_servers,
381 ["rust-analyzer".to_string()]
382 )
383 });
384
385 let buffer_id = cx.read(|cx| {
386 let buffer = buffer.read(cx);
387 assert_eq!(buffer.language().unwrap().name(), "Rust".into());
388 buffer.remote_id()
389 });
390
391 server_cx.read(|cx| {
392 let buffer = headless
393 .read(cx)
394 .buffer_store
395 .read(cx)
396 .get(buffer_id)
397 .unwrap();
398
399 assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
400 });
401
402 server_cx.read(|cx| {
403 let lsp_store = headless.read(cx).lsp_store.read(cx);
404 assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
405 });
406
407 fake_lsp.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
408 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
409 label: "boop".to_string(),
410 ..Default::default()
411 }])))
412 });
413
414 let result = project
415 .update(cx, |project, cx| {
416 project.completions(
417 &buffer,
418 0,
419 CompletionContext {
420 trigger_kind: CompletionTriggerKind::INVOKED,
421 trigger_character: None,
422 },
423 cx,
424 )
425 })
426 .await
427 .unwrap();
428
429 assert_eq!(
430 result.into_iter().map(|c| c.label.text).collect::<Vec<_>>(),
431 vec!["boop".to_string()]
432 );
433
434 fake_lsp.handle_request::<lsp::request::Rename, _, _>(|_, _| async move {
435 Ok(Some(lsp::WorkspaceEdit {
436 changes: Some(
437 [(
438 lsp::Url::from_file_path("/code/project1/src/lib.rs").unwrap(),
439 vec![lsp::TextEdit::new(
440 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
441 "two".to_string(),
442 )],
443 )]
444 .into_iter()
445 .collect(),
446 ),
447 ..Default::default()
448 }))
449 });
450
451 project
452 .update(cx, |project, cx| {
453 project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
454 })
455 .await
456 .unwrap();
457
458 cx.run_until_parked();
459 buffer.update(cx, |buffer, _| {
460 assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
461 })
462}
463
464#[gpui::test]
465async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
466 let (project, _headless, fs) = init_test(cx, server_cx).await;
467 let (worktree, _) = project
468 .update(cx, |project, cx| {
469 project.find_or_create_worktree("/code/project1", true, cx)
470 })
471 .await
472 .unwrap();
473
474 let worktree_id = cx.update(|cx| worktree.read(cx).id());
475
476 let buffer = project
477 .update(cx, |project, cx| {
478 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
479 })
480 .await
481 .unwrap();
482
483 fs.save(
484 &PathBuf::from("/code/project1/src/lib.rs"),
485 &("bangles".to_string().into()),
486 LineEnding::Unix,
487 )
488 .await
489 .unwrap();
490
491 cx.run_until_parked();
492
493 buffer.update(cx, |buffer, cx| {
494 assert_eq!(buffer.text(), "bangles");
495 buffer.edit([(0..0, "a")], None, cx);
496 });
497
498 fs.save(
499 &PathBuf::from("/code/project1/src/lib.rs"),
500 &("bloop".to_string().into()),
501 LineEnding::Unix,
502 )
503 .await
504 .unwrap();
505
506 cx.run_until_parked();
507 cx.update(|cx| {
508 assert!(buffer.read(cx).has_conflict());
509 });
510
511 project
512 .update(cx, |project, cx| {
513 project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
514 })
515 .await
516 .unwrap();
517 cx.run_until_parked();
518
519 cx.update(|cx| {
520 assert!(!buffer.read(cx).has_conflict());
521 });
522}
523
524#[gpui::test]
525async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
526 let (project, _headless, _fs) = init_test(cx, server_cx).await;
527 let (worktree, _) = project
528 .update(cx, |project, cx| {
529 project.find_or_create_worktree("/code/project1", true, cx)
530 })
531 .await
532 .unwrap();
533
534 let worktree_id = cx.update(|cx| worktree.read(cx).id());
535
536 let buffer = project
537 .update(cx, |project, cx| {
538 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
539 })
540 .await
541 .unwrap();
542
543 let path = project
544 .update(cx, |project, cx| {
545 project.resolve_existing_file_path("/code/project1/README.md", &buffer, cx)
546 })
547 .await
548 .unwrap();
549 assert_eq!(
550 path.abs_path().unwrap().to_string_lossy(),
551 "/code/project1/README.md"
552 );
553
554 let path = project
555 .update(cx, |project, cx| {
556 project.resolve_existing_file_path("../README.md", &buffer, cx)
557 })
558 .await
559 .unwrap();
560
561 assert_eq!(
562 path.project_path().unwrap().clone(),
563 ProjectPath::from((worktree_id, "README.md"))
564 );
565}
566
567#[gpui::test(iterations = 10)]
568async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
569 let (project, _headless, _fs) = init_test(cx, server_cx).await;
570 let (worktree, _) = project
571 .update(cx, |project, cx| {
572 project.find_or_create_worktree("/code/project1", true, cx)
573 })
574 .await
575 .unwrap();
576 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
577
578 // Open a buffer on the client but cancel after a random amount of time.
579 let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
580 cx.executor().simulate_random_delay().await;
581 drop(buffer);
582
583 // Try opening the same buffer again as the client, and ensure we can
584 // still do it despite the cancellation above.
585 let buffer = project
586 .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
587 .await
588 .unwrap();
589
590 buffer.read_with(cx, |buf, _| {
591 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
592 });
593}
594
595#[gpui::test]
596async fn test_adding_then_removing_then_adding_worktrees(
597 cx: &mut TestAppContext,
598 server_cx: &mut TestAppContext,
599) {
600 let (project, _headless, _fs) = init_test(cx, server_cx).await;
601 let (_worktree, _) = project
602 .update(cx, |project, cx| {
603 project.find_or_create_worktree("/code/project1", true, cx)
604 })
605 .await
606 .unwrap();
607
608 let (worktree_2, _) = project
609 .update(cx, |project, cx| {
610 project.find_or_create_worktree("/code/project2", true, cx)
611 })
612 .await
613 .unwrap();
614 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
615
616 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
617
618 let (worktree_2, _) = project
619 .update(cx, |project, cx| {
620 project.find_or_create_worktree("/code/project2", true, cx)
621 })
622 .await
623 .unwrap();
624
625 cx.run_until_parked();
626 worktree_2.update(cx, |worktree, _cx| {
627 assert!(worktree.is_visible());
628 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
629 assert_eq!(entries.len(), 2);
630 assert_eq!(
631 entries[1].path.to_string_lossy().to_string(),
632 "README.md".to_string()
633 )
634 })
635}
636
637#[gpui::test]
638async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
639 let (project, _headless, _fs) = init_test(cx, server_cx).await;
640 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
641 cx.executor().run_until_parked();
642 let buffer = buffer.await.unwrap();
643
644 cx.update(|cx| {
645 assert_eq!(
646 buffer.read(cx).text(),
647 initial_server_settings_content().to_string()
648 )
649 })
650}
651
652#[gpui::test(iterations = 20)]
653async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
654 let (project, _headless, fs) = init_test(cx, server_cx).await;
655
656 let (worktree, _) = project
657 .update(cx, |project, cx| {
658 project.find_or_create_worktree("/code/project1", true, cx)
659 })
660 .await
661 .unwrap();
662
663 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
664 let buffer = project
665 .update(cx, |project, cx| {
666 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
667 })
668 .await
669 .unwrap();
670
671 buffer.update(cx, |buffer, cx| {
672 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
673 let ix = buffer.text().find('1').unwrap();
674 buffer.edit([(ix..ix + 1, "100")], None, cx);
675 });
676
677 let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
678 client
679 .update(cx, |client, cx| client.simulate_disconnect(cx))
680 .detach();
681
682 project
683 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
684 .await
685 .unwrap();
686
687 assert_eq!(
688 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
689 "fn one() -> usize { 100 }"
690 );
691}
692
693fn init_logger() {
694 if std::env::var("RUST_LOG").is_ok() {
695 env_logger::try_init().ok();
696 }
697}
698
699async fn init_test(
700 cx: &mut TestAppContext,
701 server_cx: &mut TestAppContext,
702) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
703 init_logger();
704
705 let (forwarder, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
706 let fs = FakeFs::new(server_cx.executor());
707 fs.insert_tree(
708 "/code",
709 json!({
710 "project1": {
711 ".git": {},
712 "README.md": "# project 1",
713 "src": {
714 "lib.rs": "fn one() -> usize { 1 }"
715 }
716 },
717 "project2": {
718 "README.md": "# project 2",
719 },
720 }),
721 )
722 .await;
723 fs.set_index_for_repo(
724 Path::new("/code/project1/.git"),
725 &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
726 );
727
728 server_cx.update(HeadlessProject::init);
729 let http_client = Arc::new(BlockedHttpClient);
730 let node_runtime = NodeRuntime::unavailable();
731 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
732 let headless = server_cx.new_model(|cx| {
733 client::init_settings(cx);
734
735 HeadlessProject::new(
736 crate::HeadlessAppState {
737 session: ssh_server_client,
738 fs: fs.clone(),
739 http_client,
740 node_runtime,
741 languages,
742 },
743 cx,
744 )
745 });
746
747 let ssh = SshRemoteClient::fake_client(forwarder, cx).await;
748 let project = build_project(ssh, cx);
749 project
750 .update(cx, {
751 let headless = headless.clone();
752 |_, cx| cx.on_release(|_, _| drop(headless))
753 })
754 .detach();
755 (project, headless, fs)
756}
757
758fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
759 cx.update(|cx| {
760 let settings_store = SettingsStore::test(cx);
761 cx.set_global(settings_store);
762 });
763
764 let client = cx.update(|cx| {
765 Client::new(
766 Arc::new(FakeSystemClock::default()),
767 FakeHttpClient::with_404_response(),
768 cx,
769 )
770 });
771
772 let node = NodeRuntime::unavailable();
773 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
774 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
775 let fs = FakeFs::new(cx.executor());
776 cx.update(|cx| {
777 Project::init(&client, cx);
778 language::init(cx);
779 });
780
781 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
782}