1use assistant_slash_command::SlashCommandRegistry;
2use async_compression::futures::bufread::GzipEncoder;
3use collections::BTreeMap;
4use extension_host::ExtensionSettings;
5use extension_host::SchemaVersion;
6use extension_host::{
7 Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
8 ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
9 RELOAD_DEBOUNCE_DURATION,
10};
11use fs::{FakeFs, Fs, RealFs};
12use futures::{io::BufReader, AsyncReadExt, StreamExt};
13use gpui::{Context, SemanticVersion, TestAppContext};
14use http_client::{FakeHttpClient, Response};
15use indexed_docs::IndexedDocsRegistry;
16use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
17use node_runtime::NodeRuntime;
18use parking_lot::Mutex;
19use project::{Project, DEFAULT_COMPLETION_CONTEXT};
20use release_channel::AppVersion;
21use reqwest_client::ReqwestClient;
22use serde_json::json;
23use settings::{Settings as _, SettingsStore};
24use snippet_provider::SnippetRegistry;
25use std::{
26 ffi::OsString,
27 path::{Path, PathBuf},
28 sync::Arc,
29};
30use theme::ThemeRegistry;
31use util::test::temp_tree;
32
33#[cfg(test)]
34#[ctor::ctor]
35fn init_logger() {
36 if std::env::var("RUST_LOG").is_ok() {
37 env_logger::init();
38 }
39}
40
41#[gpui::test]
42async fn test_extension_store(cx: &mut TestAppContext) {
43 init_test(cx);
44
45 let fs = FakeFs::new(cx.executor());
46 let http_client = FakeHttpClient::with_200_response();
47
48 fs.insert_tree(
49 "/the-extension-dir",
50 json!({
51 "installed": {
52 "zed-monokai": {
53 "extension.json": r#"{
54 "id": "zed-monokai",
55 "name": "Zed Monokai",
56 "version": "2.0.0",
57 "themes": {
58 "Monokai Dark": "themes/monokai.json",
59 "Monokai Light": "themes/monokai.json",
60 "Monokai Pro Dark": "themes/monokai-pro.json",
61 "Monokai Pro Light": "themes/monokai-pro.json"
62 }
63 }"#,
64 "themes": {
65 "monokai.json": r#"{
66 "name": "Monokai",
67 "author": "Someone",
68 "themes": [
69 {
70 "name": "Monokai Dark",
71 "appearance": "dark",
72 "style": {}
73 },
74 {
75 "name": "Monokai Light",
76 "appearance": "light",
77 "style": {}
78 }
79 ]
80 }"#,
81 "monokai-pro.json": r#"{
82 "name": "Monokai Pro",
83 "author": "Someone",
84 "themes": [
85 {
86 "name": "Monokai Pro Dark",
87 "appearance": "dark",
88 "style": {}
89 },
90 {
91 "name": "Monokai Pro Light",
92 "appearance": "light",
93 "style": {}
94 }
95 ]
96 }"#,
97 }
98 },
99 "zed-ruby": {
100 "extension.json": r#"{
101 "id": "zed-ruby",
102 "name": "Zed Ruby",
103 "version": "1.0.0",
104 "grammars": {
105 "ruby": "grammars/ruby.wasm",
106 "embedded_template": "grammars/embedded_template.wasm"
107 },
108 "languages": {
109 "ruby": "languages/ruby",
110 "erb": "languages/erb"
111 }
112 }"#,
113 "grammars": {
114 "ruby.wasm": "",
115 "embedded_template.wasm": "",
116 },
117 "languages": {
118 "ruby": {
119 "config.toml": r#"
120 name = "Ruby"
121 grammar = "ruby"
122 path_suffixes = ["rb"]
123 "#,
124 "highlights.scm": "",
125 },
126 "erb": {
127 "config.toml": r#"
128 name = "ERB"
129 grammar = "embedded_template"
130 path_suffixes = ["erb"]
131 "#,
132 "highlights.scm": "",
133 }
134 },
135 }
136 }
137 }),
138 )
139 .await;
140
141 let mut expected_index = ExtensionIndex {
142 extensions: [
143 (
144 "zed-ruby".into(),
145 ExtensionIndexEntry {
146 manifest: Arc::new(ExtensionManifest {
147 id: "zed-ruby".into(),
148 name: "Zed Ruby".into(),
149 version: "1.0.0".into(),
150 schema_version: SchemaVersion::ZERO,
151 description: None,
152 authors: Vec::new(),
153 repository: None,
154 themes: Default::default(),
155 lib: Default::default(),
156 languages: vec!["languages/erb".into(), "languages/ruby".into()],
157 grammars: [
158 ("embedded_template".into(), GrammarManifestEntry::default()),
159 ("ruby".into(), GrammarManifestEntry::default()),
160 ]
161 .into_iter()
162 .collect(),
163 language_servers: BTreeMap::default(),
164 slash_commands: BTreeMap::default(),
165 indexed_docs_providers: BTreeMap::default(),
166 snippets: None,
167 }),
168 dev: false,
169 },
170 ),
171 (
172 "zed-monokai".into(),
173 ExtensionIndexEntry {
174 manifest: Arc::new(ExtensionManifest {
175 id: "zed-monokai".into(),
176 name: "Zed Monokai".into(),
177 version: "2.0.0".into(),
178 schema_version: SchemaVersion::ZERO,
179 description: None,
180 authors: vec![],
181 repository: None,
182 themes: vec![
183 "themes/monokai-pro.json".into(),
184 "themes/monokai.json".into(),
185 ],
186 lib: Default::default(),
187 languages: Default::default(),
188 grammars: BTreeMap::default(),
189 language_servers: BTreeMap::default(),
190 slash_commands: BTreeMap::default(),
191 indexed_docs_providers: BTreeMap::default(),
192 snippets: None,
193 }),
194 dev: false,
195 },
196 ),
197 ]
198 .into_iter()
199 .collect(),
200 languages: [
201 (
202 "ERB".into(),
203 ExtensionIndexLanguageEntry {
204 extension: "zed-ruby".into(),
205 path: "languages/erb".into(),
206 grammar: Some("embedded_template".into()),
207 matcher: LanguageMatcher {
208 path_suffixes: vec!["erb".into()],
209 first_line_pattern: None,
210 },
211 },
212 ),
213 (
214 "Ruby".into(),
215 ExtensionIndexLanguageEntry {
216 extension: "zed-ruby".into(),
217 path: "languages/ruby".into(),
218 grammar: Some("ruby".into()),
219 matcher: LanguageMatcher {
220 path_suffixes: vec!["rb".into()],
221 first_line_pattern: None,
222 },
223 },
224 ),
225 ]
226 .into_iter()
227 .collect(),
228 themes: [
229 (
230 "Monokai Dark".into(),
231 ExtensionIndexThemeEntry {
232 extension: "zed-monokai".into(),
233 path: "themes/monokai.json".into(),
234 },
235 ),
236 (
237 "Monokai Light".into(),
238 ExtensionIndexThemeEntry {
239 extension: "zed-monokai".into(),
240 path: "themes/monokai.json".into(),
241 },
242 ),
243 (
244 "Monokai Pro Dark".into(),
245 ExtensionIndexThemeEntry {
246 extension: "zed-monokai".into(),
247 path: "themes/monokai-pro.json".into(),
248 },
249 ),
250 (
251 "Monokai Pro Light".into(),
252 ExtensionIndexThemeEntry {
253 extension: "zed-monokai".into(),
254 path: "themes/monokai-pro.json".into(),
255 },
256 ),
257 ]
258 .into_iter()
259 .collect(),
260 };
261
262 let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
263 let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
264 let slash_command_registry = SlashCommandRegistry::new();
265 let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
266 let snippet_registry = Arc::new(SnippetRegistry::new());
267 let node_runtime = NodeRuntime::unavailable();
268
269 let store = cx.new_model(|cx| {
270 let extension_registration_hooks = crate::ConcreteExtensionRegistrationHooks::new(
271 theme_registry.clone(),
272 slash_command_registry.clone(),
273 indexed_docs_registry.clone(),
274 snippet_registry.clone(),
275 language_registry.clone(),
276 cx,
277 );
278
279 ExtensionStore::new(
280 PathBuf::from("/the-extension-dir"),
281 None,
282 extension_registration_hooks,
283 fs.clone(),
284 http_client.clone(),
285 http_client.clone(),
286 None,
287 node_runtime.clone(),
288 cx,
289 )
290 });
291
292 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
293 store.read_with(cx, |store, _| {
294 let index = &store.extension_index;
295 assert_eq!(index.extensions, expected_index.extensions);
296 assert_eq!(index.languages, expected_index.languages);
297 assert_eq!(index.themes, expected_index.themes);
298
299 assert_eq!(
300 language_registry.language_names(),
301 ["ERB", "Plain Text", "Ruby"]
302 );
303 assert_eq!(
304 theme_registry.list_names(),
305 [
306 "Monokai Dark",
307 "Monokai Light",
308 "Monokai Pro Dark",
309 "Monokai Pro Light",
310 "One Dark",
311 ]
312 );
313 });
314
315 fs.insert_tree(
316 "/the-extension-dir/installed/zed-gruvbox",
317 json!({
318 "extension.json": r#"{
319 "id": "zed-gruvbox",
320 "name": "Zed Gruvbox",
321 "version": "1.0.0",
322 "themes": {
323 "Gruvbox": "themes/gruvbox.json"
324 }
325 }"#,
326 "themes": {
327 "gruvbox.json": r#"{
328 "name": "Gruvbox",
329 "author": "Someone Else",
330 "themes": [
331 {
332 "name": "Gruvbox",
333 "appearance": "dark",
334 "style": {}
335 }
336 ]
337 }"#,
338 }
339 }),
340 )
341 .await;
342
343 expected_index.extensions.insert(
344 "zed-gruvbox".into(),
345 ExtensionIndexEntry {
346 manifest: Arc::new(ExtensionManifest {
347 id: "zed-gruvbox".into(),
348 name: "Zed Gruvbox".into(),
349 version: "1.0.0".into(),
350 schema_version: SchemaVersion::ZERO,
351 description: None,
352 authors: vec![],
353 repository: None,
354 themes: vec!["themes/gruvbox.json".into()],
355 lib: Default::default(),
356 languages: Default::default(),
357 grammars: BTreeMap::default(),
358 language_servers: BTreeMap::default(),
359 slash_commands: BTreeMap::default(),
360 indexed_docs_providers: BTreeMap::default(),
361 snippets: None,
362 }),
363 dev: false,
364 },
365 );
366 expected_index.themes.insert(
367 "Gruvbox".into(),
368 ExtensionIndexThemeEntry {
369 extension: "zed-gruvbox".into(),
370 path: "themes/gruvbox.json".into(),
371 },
372 );
373
374 #[allow(clippy::let_underscore_future)]
375 let _ = store.update(cx, |store, cx| store.reload(None, cx));
376
377 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
378 store.read_with(cx, |store, _| {
379 let index = &store.extension_index;
380 assert_eq!(index.extensions, expected_index.extensions);
381 assert_eq!(index.languages, expected_index.languages);
382 assert_eq!(index.themes, expected_index.themes);
383
384 assert_eq!(
385 theme_registry.list_names(),
386 [
387 "Gruvbox",
388 "Monokai Dark",
389 "Monokai Light",
390 "Monokai Pro Dark",
391 "Monokai Pro Light",
392 "One Dark",
393 ]
394 );
395 });
396
397 let prev_fs_metadata_call_count = fs.metadata_call_count();
398 let prev_fs_read_dir_call_count = fs.read_dir_call_count();
399
400 // Create new extension store, as if Zed were restarting.
401 drop(store);
402 let store = cx.new_model(|cx| {
403 let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
404 theme_registry.clone(),
405 slash_command_registry,
406 indexed_docs_registry,
407 snippet_registry,
408 language_registry.clone(),
409 cx,
410 );
411
412 ExtensionStore::new(
413 PathBuf::from("/the-extension-dir"),
414 None,
415 extension_api,
416 fs.clone(),
417 http_client.clone(),
418 http_client.clone(),
419 None,
420 node_runtime.clone(),
421 cx,
422 )
423 });
424
425 cx.executor().run_until_parked();
426 store.read_with(cx, |store, _| {
427 assert_eq!(store.extension_index, expected_index);
428 assert_eq!(
429 language_registry.language_names(),
430 ["ERB", "Plain Text", "Ruby"]
431 );
432 assert_eq!(
433 language_registry.grammar_names(),
434 ["embedded_template".into(), "ruby".into()]
435 );
436 assert_eq!(
437 theme_registry.list_names(),
438 [
439 "Gruvbox",
440 "Monokai Dark",
441 "Monokai Light",
442 "Monokai Pro Dark",
443 "Monokai Pro Light",
444 "One Dark",
445 ]
446 );
447
448 // The on-disk manifest limits the number of FS calls that need to be made
449 // on startup.
450 assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
451 assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
452 });
453
454 store.update(cx, |store, cx| {
455 store.uninstall_extension("zed-ruby".into(), cx)
456 });
457
458 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
459 expected_index.extensions.remove("zed-ruby");
460 expected_index.languages.remove("Ruby");
461 expected_index.languages.remove("ERB");
462
463 store.read_with(cx, |store, _| {
464 assert_eq!(store.extension_index, expected_index);
465 assert_eq!(language_registry.language_names(), ["Plain Text"]);
466 assert_eq!(language_registry.grammar_names(), []);
467 });
468}
469
470#[gpui::test]
471async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
472 init_test(cx);
473 cx.executor().allow_parking();
474
475 let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
476 .parent()
477 .unwrap()
478 .parent()
479 .unwrap();
480 let cache_dir = root_dir.join("target");
481 let test_extension_id = "test-extension";
482 let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
483
484 let fs = Arc::new(RealFs::default());
485 let extensions_dir = temp_tree(json!({
486 "installed": {},
487 "work": {}
488 }));
489 let project_dir = temp_tree(json!({
490 "test.gleam": ""
491 }));
492
493 let extensions_dir = extensions_dir.path().canonicalize().unwrap();
494 let project_dir = project_dir.path().canonicalize().unwrap();
495
496 let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
497
498 let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
499 let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
500 let slash_command_registry = SlashCommandRegistry::new();
501 let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
502 let snippet_registry = Arc::new(SnippetRegistry::new());
503 let node_runtime = NodeRuntime::unavailable();
504
505 let mut status_updates = language_registry.language_server_binary_statuses();
506
507 struct FakeLanguageServerVersion {
508 version: String,
509 binary_contents: String,
510 http_request_count: usize,
511 }
512
513 let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
514 version: "v1.2.3".into(),
515 binary_contents: "the-binary-contents".into(),
516 http_request_count: 0,
517 }));
518
519 let extension_client = FakeHttpClient::create({
520 let language_server_version = language_server_version.clone();
521 move |request| {
522 let language_server_version = language_server_version.clone();
523 async move {
524 let version = language_server_version.lock().version.clone();
525 let binary_contents = language_server_version.lock().binary_contents.clone();
526
527 let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
528 let asset_download_uri =
529 format!("https://fake-download.example.com/gleam-{version}");
530
531 let uri = request.uri().to_string();
532 if uri == github_releases_uri {
533 language_server_version.lock().http_request_count += 1;
534 Ok(Response::new(
535 json!([
536 {
537 "tag_name": version,
538 "prerelease": false,
539 "tarball_url": "",
540 "zipball_url": "",
541 "assets": [
542 {
543 "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
544 "browser_download_url": asset_download_uri
545 },
546 {
547 "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
548 "browser_download_url": asset_download_uri
549 },
550 {
551 "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
552 "browser_download_url": asset_download_uri
553 }
554 ]
555 }
556 ])
557 .to_string()
558 .into(),
559 ))
560 } else if uri == asset_download_uri {
561 language_server_version.lock().http_request_count += 1;
562 let mut bytes = Vec::<u8>::new();
563 let mut archive = async_tar::Builder::new(&mut bytes);
564 let mut header = async_tar::Header::new_gnu();
565 header.set_size(binary_contents.len() as u64);
566 archive
567 .append_data(&mut header, "gleam", binary_contents.as_bytes())
568 .await
569 .unwrap();
570 archive.into_inner().await.unwrap();
571 let mut gzipped_bytes = Vec::new();
572 let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
573 encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
574 Ok(Response::new(gzipped_bytes.into()))
575 } else {
576 Ok(Response::builder().status(404).body("not found".into())?)
577 }
578 }
579 }
580 });
581 let user_agent = cx.update(|cx| {
582 format!(
583 "Zed/{} ({}; {})",
584 AppVersion::global(cx),
585 std::env::consts::OS,
586 std::env::consts::ARCH
587 )
588 });
589 let builder_client =
590 Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
591
592 let extension_store = cx.new_model(|cx| {
593 let extension_api = crate::ConcreteExtensionRegistrationHooks::new(
594 theme_registry.clone(),
595 slash_command_registry,
596 indexed_docs_registry,
597 snippet_registry,
598 language_registry.clone(),
599 cx,
600 );
601 ExtensionStore::new(
602 extensions_dir.clone(),
603 Some(cache_dir),
604 extension_api,
605 fs.clone(),
606 extension_client.clone(),
607 builder_client,
608 None,
609 node_runtime,
610 cx,
611 )
612 });
613
614 // Ensure that debounces fire.
615 let mut events = cx.events(&extension_store);
616 let executor = cx.executor();
617 let _task = cx.executor().spawn(async move {
618 while let Some(event) = events.next().await {
619 if let extension_host::Event::StartedReloading = event {
620 executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
621 }
622 }
623 });
624
625 extension_store.update(cx, |_, cx| {
626 cx.subscribe(&extension_store, |_, _, event, _| {
627 if matches!(event, Event::ExtensionFailedToLoad(_)) {
628 panic!("extension failed to load");
629 }
630 })
631 .detach();
632 });
633
634 extension_store
635 .update(cx, |store, cx| {
636 store.install_dev_extension(test_extension_dir.clone(), cx)
637 })
638 .await
639 .unwrap();
640
641 let mut fake_servers = language_registry.register_fake_language_server(
642 LanguageServerName("gleam".into()),
643 lsp::ServerCapabilities {
644 completion_provider: Some(Default::default()),
645 ..Default::default()
646 },
647 None,
648 );
649
650 let buffer = project
651 .update(cx, |project, cx| {
652 project.open_local_buffer(project_dir.join("test.gleam"), cx)
653 })
654 .await
655 .unwrap();
656
657 let fake_server = fake_servers.next().await.unwrap();
658 let expected_server_path =
659 extensions_dir.join(format!("work/{test_extension_id}/gleam-v1.2.3/gleam"));
660 let expected_binary_contents = language_server_version.lock().binary_contents.clone();
661
662 assert_eq!(fake_server.binary.path, expected_server_path);
663 assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
664 assert_eq!(
665 fs.load(&expected_server_path).await.unwrap(),
666 expected_binary_contents
667 );
668 assert_eq!(language_server_version.lock().http_request_count, 2);
669 assert_eq!(
670 [
671 status_updates.next().await.unwrap(),
672 status_updates.next().await.unwrap(),
673 status_updates.next().await.unwrap(),
674 ],
675 [
676 (
677 LanguageServerName("gleam".into()),
678 LanguageServerBinaryStatus::CheckingForUpdate
679 ),
680 (
681 LanguageServerName("gleam".into()),
682 LanguageServerBinaryStatus::Downloading
683 ),
684 (
685 LanguageServerName("gleam".into()),
686 LanguageServerBinaryStatus::None
687 )
688 ]
689 );
690
691 // The extension creates custom labels for completion items.
692 fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
693 Ok(Some(lsp::CompletionResponse::Array(vec![
694 lsp::CompletionItem {
695 label: "foo".into(),
696 kind: Some(lsp::CompletionItemKind::FUNCTION),
697 detail: Some("fn() -> Result(Nil, Error)".into()),
698 ..Default::default()
699 },
700 lsp::CompletionItem {
701 label: "bar.baz".into(),
702 kind: Some(lsp::CompletionItemKind::FUNCTION),
703 detail: Some("fn(List(a)) -> a".into()),
704 ..Default::default()
705 },
706 lsp::CompletionItem {
707 label: "Quux".into(),
708 kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
709 detail: Some("fn(String) -> T".into()),
710 ..Default::default()
711 },
712 lsp::CompletionItem {
713 label: "my_string".into(),
714 kind: Some(lsp::CompletionItemKind::CONSTANT),
715 detail: Some("String".into()),
716 ..Default::default()
717 },
718 ])))
719 });
720
721 let completion_labels = project
722 .update(cx, |project, cx| {
723 project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
724 })
725 .await
726 .unwrap()
727 .into_iter()
728 .map(|c| c.label.text)
729 .collect::<Vec<_>>();
730 assert_eq!(
731 completion_labels,
732 [
733 "foo: fn() -> Result(Nil, Error)".to_string(),
734 "bar.baz: fn(List(a)) -> a".to_string(),
735 "Quux: fn(String) -> T".to_string(),
736 "my_string: String".to_string(),
737 ]
738 );
739
740 // Simulate a new version of the language server being released
741 language_server_version.lock().version = "v2.0.0".into();
742 language_server_version.lock().binary_contents = "the-new-binary-contents".into();
743 language_server_version.lock().http_request_count = 0;
744
745 // Start a new instance of the language server.
746 project.update(cx, |project, cx| {
747 project.restart_language_servers_for_buffers([buffer.clone()], cx)
748 });
749
750 // The extension has cached the binary path, and does not attempt
751 // to reinstall it.
752 let fake_server = fake_servers.next().await.unwrap();
753 assert_eq!(fake_server.binary.path, expected_server_path);
754 assert_eq!(
755 fs.load(&expected_server_path).await.unwrap(),
756 expected_binary_contents
757 );
758 assert_eq!(language_server_version.lock().http_request_count, 0);
759
760 // Reload the extension, clearing its cache.
761 // Start a new instance of the language server.
762 extension_store
763 .update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
764 .await;
765
766 cx.executor().run_until_parked();
767 project.update(cx, |project, cx| {
768 project.restart_language_servers_for_buffers([buffer.clone()], cx)
769 });
770
771 // The extension re-fetches the latest version of the language server.
772 let fake_server = fake_servers.next().await.unwrap();
773 let new_expected_server_path =
774 extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
775 let expected_binary_contents = language_server_version.lock().binary_contents.clone();
776 assert_eq!(fake_server.binary.path, new_expected_server_path);
777 assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
778 assert_eq!(
779 fs.load(&new_expected_server_path).await.unwrap(),
780 expected_binary_contents
781 );
782
783 // The old language server directory has been cleaned up.
784 assert!(fs.metadata(&expected_server_path).await.unwrap().is_none());
785}
786
787fn init_test(cx: &mut TestAppContext) {
788 cx.update(|cx| {
789 let store = SettingsStore::test(cx);
790 cx.set_global(store);
791 release_channel::init(SemanticVersion::default(), cx);
792 theme::init(theme::LoadThemes::JustBase, cx);
793 Project::init_settings(cx);
794 ExtensionSettings::register(cx);
795 language::init(cx);
796 });
797}