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