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