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