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::{all_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(Some(&"Rust".into()))
212 .language_servers,
213 ["from-local-settings".to_string()]
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(Some(&"Rust".into()))
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(Some(&"Rust".into()))
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 all_language_settings(file, cx)
300 .language(Some(&"Rust".into()))
301 .language_servers,
302 ["override-rust-analyzer".to_string()]
303 )
304 });
305}
306
307#[gpui::test]
308async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
309 let (project, headless, fs) = init_test(cx, server_cx).await;
310
311 fs.insert_tree(
312 "/code/project1/.zed",
313 json!({
314 "settings.json": r#"
315 {
316 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
317 "lsp": {
318 "rust-analyzer": {
319 "binary": {
320 "path": "~/.cargo/bin/rust-analyzer"
321 }
322 }
323 }
324 }"#
325 }),
326 )
327 .await;
328
329 cx.update_model(&project, |project, _| {
330 project.languages().register_test_language(LanguageConfig {
331 name: "Rust".into(),
332 matcher: LanguageMatcher {
333 path_suffixes: vec!["rs".into()],
334 ..Default::default()
335 },
336 ..Default::default()
337 });
338 project.languages().register_fake_lsp_adapter(
339 "Rust",
340 FakeLspAdapter {
341 name: "rust-analyzer",
342 ..Default::default()
343 },
344 )
345 });
346
347 let mut fake_lsp = server_cx.update(|cx| {
348 headless.read(cx).languages.register_fake_language_server(
349 LanguageServerName("rust-analyzer".into()),
350 Default::default(),
351 None,
352 )
353 });
354
355 cx.run_until_parked();
356
357 let worktree_id = project
358 .update(cx, |project, cx| {
359 project.find_or_create_worktree("/code/project1", true, cx)
360 })
361 .await
362 .unwrap()
363 .0
364 .read_with(cx, |worktree, _| worktree.id());
365
366 // Wait for the settings to synchronize
367 cx.run_until_parked();
368
369 let buffer = project
370 .update(cx, |project, cx| {
371 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
372 })
373 .await
374 .unwrap();
375 cx.run_until_parked();
376
377 let fake_lsp = fake_lsp.next().await.unwrap();
378
379 cx.read(|cx| {
380 let file = buffer.read(cx).file();
381 assert_eq!(
382 all_language_settings(file, cx)
383 .language(Some(&"Rust".into()))
384 .language_servers,
385 ["rust-analyzer".to_string()]
386 )
387 });
388
389 let buffer_id = cx.read(|cx| {
390 let buffer = buffer.read(cx);
391 assert_eq!(buffer.language().unwrap().name(), "Rust".into());
392 buffer.remote_id()
393 });
394
395 server_cx.read(|cx| {
396 let buffer = headless
397 .read(cx)
398 .buffer_store
399 .read(cx)
400 .get(buffer_id)
401 .unwrap();
402
403 assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
404 });
405
406 server_cx.read(|cx| {
407 let lsp_store = headless.read(cx).lsp_store.read(cx);
408 assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
409 });
410
411 fake_lsp.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
412 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
413 label: "boop".to_string(),
414 ..Default::default()
415 }])))
416 });
417
418 let result = project
419 .update(cx, |project, cx| {
420 project.completions(
421 &buffer,
422 0,
423 CompletionContext {
424 trigger_kind: CompletionTriggerKind::INVOKED,
425 trigger_character: None,
426 },
427 cx,
428 )
429 })
430 .await
431 .unwrap();
432
433 assert_eq!(
434 result.into_iter().map(|c| c.label.text).collect::<Vec<_>>(),
435 vec!["boop".to_string()]
436 );
437
438 fake_lsp.handle_request::<lsp::request::Rename, _, _>(|_, _| async move {
439 Ok(Some(lsp::WorkspaceEdit {
440 changes: Some(
441 [(
442 lsp::Url::from_file_path("/code/project1/src/lib.rs").unwrap(),
443 vec![lsp::TextEdit::new(
444 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
445 "two".to_string(),
446 )],
447 )]
448 .into_iter()
449 .collect(),
450 ),
451 ..Default::default()
452 }))
453 });
454
455 project
456 .update(cx, |project, cx| {
457 project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
458 })
459 .await
460 .unwrap();
461
462 cx.run_until_parked();
463 buffer.update(cx, |buffer, _| {
464 assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
465 })
466}
467
468#[gpui::test]
469async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
470 let (project, _headless, fs) = init_test(cx, server_cx).await;
471 let (worktree, _) = project
472 .update(cx, |project, cx| {
473 project.find_or_create_worktree("/code/project1", true, cx)
474 })
475 .await
476 .unwrap();
477
478 let worktree_id = cx.update(|cx| worktree.read(cx).id());
479
480 let buffer = project
481 .update(cx, |project, cx| {
482 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
483 })
484 .await
485 .unwrap();
486 buffer.update(cx, |buffer, cx| {
487 buffer.edit([(0..0, "a")], None, cx);
488 });
489
490 fs.save(
491 &PathBuf::from("/code/project1/src/lib.rs"),
492 &("bloop".to_string().into()),
493 LineEnding::Unix,
494 )
495 .await
496 .unwrap();
497
498 cx.run_until_parked();
499 cx.update(|cx| {
500 assert!(buffer.read(cx).has_conflict());
501 });
502
503 project
504 .update(cx, |project, cx| {
505 project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
506 })
507 .await
508 .unwrap();
509 cx.run_until_parked();
510
511 cx.update(|cx| {
512 assert!(!buffer.read(cx).has_conflict());
513 });
514}
515
516#[gpui::test]
517async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
518 let (project, _headless, _fs) = init_test(cx, server_cx).await;
519 let (worktree, _) = project
520 .update(cx, |project, cx| {
521 project.find_or_create_worktree("/code/project1", true, cx)
522 })
523 .await
524 .unwrap();
525
526 let worktree_id = cx.update(|cx| worktree.read(cx).id());
527
528 let buffer = project
529 .update(cx, |project, cx| {
530 project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
531 })
532 .await
533 .unwrap();
534
535 let path = project
536 .update(cx, |project, cx| {
537 project.resolve_existing_file_path("/code/project1/README.md", &buffer, cx)
538 })
539 .await
540 .unwrap();
541 assert_eq!(
542 path.abs_path().unwrap().to_string_lossy(),
543 "/code/project1/README.md"
544 );
545
546 let path = project
547 .update(cx, |project, cx| {
548 project.resolve_existing_file_path("../README.md", &buffer, cx)
549 })
550 .await
551 .unwrap();
552
553 assert_eq!(
554 path.project_path().unwrap().clone(),
555 ProjectPath::from((worktree_id, "README.md"))
556 );
557}
558
559#[gpui::test(iterations = 10)]
560async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
561 let (project, _headless, _fs) = init_test(cx, server_cx).await;
562 let (worktree, _) = project
563 .update(cx, |project, cx| {
564 project.find_or_create_worktree("/code/project1", true, cx)
565 })
566 .await
567 .unwrap();
568 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
569
570 // Open a buffer on the client but cancel after a random amount of time.
571 let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
572 cx.executor().simulate_random_delay().await;
573 drop(buffer);
574
575 // Try opening the same buffer again as the client, and ensure we can
576 // still do it despite the cancellation above.
577 let buffer = project
578 .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
579 .await
580 .unwrap();
581
582 buffer.read_with(cx, |buf, _| {
583 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
584 });
585}
586
587#[gpui::test]
588async fn test_adding_then_removing_then_adding_worktrees(
589 cx: &mut TestAppContext,
590 server_cx: &mut TestAppContext,
591) {
592 let (project, _headless, _fs) = init_test(cx, server_cx).await;
593 let (_worktree, _) = project
594 .update(cx, |project, cx| {
595 project.find_or_create_worktree("/code/project1", true, cx)
596 })
597 .await
598 .unwrap();
599
600 let (worktree_2, _) = project
601 .update(cx, |project, cx| {
602 project.find_or_create_worktree("/code/project2", true, cx)
603 })
604 .await
605 .unwrap();
606 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
607
608 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
609
610 let (worktree_2, _) = project
611 .update(cx, |project, cx| {
612 project.find_or_create_worktree("/code/project2", true, cx)
613 })
614 .await
615 .unwrap();
616
617 cx.run_until_parked();
618 worktree_2.update(cx, |worktree, _cx| {
619 assert!(worktree.is_visible());
620 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
621 assert_eq!(entries.len(), 2);
622 assert_eq!(
623 entries[1].path.to_string_lossy().to_string(),
624 "README.md".to_string()
625 )
626 })
627}
628
629#[gpui::test]
630async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
631 let (project, _headless, _fs) = init_test(cx, server_cx).await;
632 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
633 cx.executor().run_until_parked();
634 let buffer = buffer.await.unwrap();
635
636 cx.update(|cx| {
637 assert_eq!(
638 buffer.read(cx).text(),
639 initial_server_settings_content().to_string()
640 )
641 })
642}
643
644fn init_logger() {
645 if std::env::var("RUST_LOG").is_ok() {
646 env_logger::try_init().ok();
647 }
648}
649
650async fn init_test(
651 cx: &mut TestAppContext,
652 server_cx: &mut TestAppContext,
653) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
654 let (ssh_remote_client, ssh_server_client) = SshRemoteClient::fake(cx, server_cx);
655 init_logger();
656
657 let fs = FakeFs::new(server_cx.executor());
658 fs.insert_tree(
659 "/code",
660 json!({
661 "project1": {
662 ".git": {},
663 "README.md": "# project 1",
664 "src": {
665 "lib.rs": "fn one() -> usize { 1 }"
666 }
667 },
668 "project2": {
669 "README.md": "# project 2",
670 },
671 }),
672 )
673 .await;
674 fs.set_index_for_repo(
675 Path::new("/code/project1/.git"),
676 &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
677 );
678
679 server_cx.update(HeadlessProject::init);
680 let http_client = Arc::new(BlockedHttpClient);
681 let node_runtime = NodeRuntime::unavailable();
682 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
683 let headless = server_cx.new_model(|cx| {
684 client::init_settings(cx);
685
686 HeadlessProject::new(
687 crate::HeadlessAppState {
688 session: ssh_server_client,
689 fs: fs.clone(),
690 http_client,
691 node_runtime,
692 languages,
693 },
694 cx,
695 )
696 });
697 let project = build_project(ssh_remote_client, cx);
698
699 project
700 .update(cx, {
701 let headless = headless.clone();
702 |_, cx| cx.on_release(|_, _| drop(headless))
703 })
704 .detach();
705 (project, headless, fs)
706}
707
708fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
709 cx.update(|cx| {
710 let settings_store = SettingsStore::test(cx);
711 cx.set_global(settings_store);
712 });
713
714 let client = cx.update(|cx| {
715 Client::new(
716 Arc::new(FakeSystemClock::default()),
717 FakeHttpClient::with_404_response(),
718 cx,
719 )
720 });
721
722 let node = NodeRuntime::unavailable();
723 let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
724 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
725 let fs = FakeFs::new(cx.executor());
726 cx.update(|cx| {
727 Project::init(&client, cx);
728 language::init(cx);
729 });
730
731 cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
732}