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