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