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