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