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, HashSet};
8use extension::ExtensionHostProxy;
9use fs::{FakeFs, Fs, RealFs};
10use futures::{AsyncReadExt, StreamExt, io::BufReader};
11use gpui::{AppContext as _, SemanticVersion, 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 debug_locators: Default::default(),
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 debug_adapters: Default::default(),
197 debug_locators: 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 debug_locators: Default::default(),
374 }),
375 dev: false,
376 },
377 );
378 expected_index.themes.insert(
379 "Gruvbox".into(),
380 ExtensionIndexThemeEntry {
381 extension: "zed-gruvbox".into(),
382 path: "themes/gruvbox.json".into(),
383 },
384 );
385
386 #[allow(clippy::let_underscore_future)]
387 let _ = store.update(cx, |store, cx| store.reload(None, cx));
388
389 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
390 store.read_with(cx, |store, _| {
391 let index = &store.extension_index;
392
393 for ((actual_key, actual_language), (expected_key, expected_language)) in
394 index.languages.iter().zip(expected_index.languages.iter())
395 {
396 assert_eq!(actual_key, expected_key);
397 assert_eq!(actual_language.grammar, expected_language.grammar);
398 assert_eq!(actual_language.matcher, expected_language.matcher);
399 assert_eq!(actual_language.hidden, expected_language.hidden);
400 }
401
402 assert_eq!(index.extensions, expected_index.extensions);
403 assert_eq!(index.themes, expected_index.themes);
404
405 assert_eq!(
406 theme_registry.list_names(),
407 [
408 "Gruvbox",
409 "Monokai Dark",
410 "Monokai Light",
411 "Monokai Pro Dark",
412 "Monokai Pro Light",
413 "One Dark",
414 ]
415 );
416 });
417
418 let prev_fs_metadata_call_count = fs.metadata_call_count();
419 let prev_fs_read_dir_call_count = fs.read_dir_call_count();
420
421 // Create new extension store, as if Zed were restarting.
422 drop(store);
423 let store = cx.new(|cx| {
424 ExtensionStore::new(
425 PathBuf::from("/the-extension-dir"),
426 None,
427 proxy,
428 fs.clone(),
429 http_client.clone(),
430 http_client.clone(),
431 None,
432 node_runtime.clone(),
433 cx,
434 )
435 });
436
437 cx.executor().run_until_parked();
438 store.read_with(cx, |store, _| {
439 assert_eq!(store.extension_index.extensions, expected_index.extensions);
440 assert_eq!(store.extension_index.themes, expected_index.themes);
441 assert_eq!(
442 store.extension_index.icon_themes,
443 expected_index.icon_themes
444 );
445
446 for ((actual_key, actual_language), (expected_key, expected_language)) in store
447 .extension_index
448 .languages
449 .iter()
450 .zip(expected_index.languages.iter())
451 {
452 assert_eq!(actual_key, expected_key);
453 assert_eq!(actual_language.grammar, expected_language.grammar);
454 assert_eq!(actual_language.matcher, expected_language.matcher);
455 assert_eq!(actual_language.hidden, expected_language.hidden);
456 }
457
458 assert_eq!(
459 language_registry.language_names(),
460 ["ERB", "Plain Text", "Ruby"]
461 );
462 assert_eq!(
463 language_registry.grammar_names(),
464 ["embedded_template".into(), "ruby".into()]
465 );
466 assert_eq!(
467 theme_registry.list_names(),
468 [
469 "Gruvbox",
470 "Monokai Dark",
471 "Monokai Light",
472 "Monokai Pro Dark",
473 "Monokai Pro Light",
474 "One Dark",
475 ]
476 );
477
478 // The on-disk manifest limits the number of FS calls that need to be made
479 // on startup.
480 assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
481 assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
482 });
483
484 store.update(cx, |store, cx| {
485 store
486 .uninstall_extension("zed-ruby".into(), cx)
487 .detach_and_log_err(cx);
488 });
489
490 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
491 expected_index.extensions.remove("zed-ruby");
492 expected_index.languages.remove("Ruby");
493 expected_index.languages.remove("ERB");
494
495 store.read_with(cx, |store, _| {
496 assert_eq!(store.extension_index.extensions, expected_index.extensions);
497 assert_eq!(store.extension_index.themes, expected_index.themes);
498 assert_eq!(
499 store.extension_index.icon_themes,
500 expected_index.icon_themes
501 );
502
503 for ((actual_key, actual_language), (expected_key, expected_language)) in store
504 .extension_index
505 .languages
506 .iter()
507 .zip(expected_index.languages.iter())
508 {
509 assert_eq!(actual_key, expected_key);
510 assert_eq!(actual_language.grammar, expected_language.grammar);
511 assert_eq!(actual_language.matcher, expected_language.matcher);
512 assert_eq!(actual_language.hidden, expected_language.hidden);
513 }
514
515 assert_eq!(language_registry.language_names(), ["Plain Text"]);
516 assert_eq!(language_registry.grammar_names(), []);
517 });
518}
519
520// todo(windows)
521// Disable this test on Windows for now. Because this test hangs at
522// `let fake_server = fake_servers.next().await.unwrap();`.
523// Reenable this test when we figure out why.
524#[gpui::test]
525#[cfg_attr(target_os = "windows", ignore)]
526async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
527 init_test(cx);
528 cx.executor().allow_parking();
529
530 let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
531 .parent()
532 .unwrap()
533 .parent()
534 .unwrap();
535 let cache_dir = root_dir.join("target");
536 let test_extension_id = "test-extension";
537 let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
538
539 let fs = Arc::new(RealFs::new(None, cx.executor()));
540 let extensions_dir = TempTree::new(json!({
541 "installed": {},
542 "work": {}
543 }));
544 let project_dir = TempTree::new(json!({
545 "test.gleam": ""
546 }));
547
548 let extensions_dir = extensions_dir.path().canonicalize().unwrap();
549 let project_dir = project_dir.path().canonicalize().unwrap();
550
551 let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
552
553 let proxy = Arc::new(ExtensionHostProxy::new());
554 let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
555 theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
556 let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
557 language_extension::init(proxy.clone(), language_registry.clone());
558 let node_runtime = NodeRuntime::unavailable();
559
560 let mut status_updates = language_registry.language_server_binary_statuses();
561
562 struct FakeLanguageServerVersion {
563 version: String,
564 binary_contents: String,
565 http_request_count: usize,
566 }
567
568 let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
569 version: "v1.2.3".into(),
570 binary_contents: "the-binary-contents".into(),
571 http_request_count: 0,
572 }));
573
574 let extension_client = FakeHttpClient::create({
575 let language_server_version = language_server_version.clone();
576 move |request| {
577 let language_server_version = language_server_version.clone();
578 async move {
579 let version = language_server_version.lock().version.clone();
580 let binary_contents = language_server_version.lock().binary_contents.clone();
581
582 let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
583 let asset_download_uri =
584 format!("https://fake-download.example.com/gleam-{version}");
585
586 let uri = request.uri().to_string();
587 if uri == github_releases_uri {
588 language_server_version.lock().http_request_count += 1;
589 Ok(Response::new(
590 json!([
591 {
592 "tag_name": version,
593 "prerelease": false,
594 "tarball_url": "",
595 "zipball_url": "",
596 "assets": [
597 {
598 "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
599 "browser_download_url": asset_download_uri
600 },
601 {
602 "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
603 "browser_download_url": asset_download_uri
604 },
605 {
606 "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
607 "browser_download_url": asset_download_uri
608 }
609 ]
610 }
611 ])
612 .to_string()
613 .into(),
614 ))
615 } else if uri == asset_download_uri {
616 language_server_version.lock().http_request_count += 1;
617 let mut bytes = Vec::<u8>::new();
618 let mut archive = async_tar::Builder::new(&mut bytes);
619 let mut header = async_tar::Header::new_gnu();
620 header.set_size(binary_contents.len() as u64);
621 archive
622 .append_data(&mut header, "gleam", binary_contents.as_bytes())
623 .await
624 .unwrap();
625 archive.into_inner().await.unwrap();
626 let mut gzipped_bytes = Vec::new();
627 let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
628 encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
629 Ok(Response::new(gzipped_bytes.into()))
630 } else {
631 Ok(Response::builder().status(404).body("not found".into())?)
632 }
633 }
634 }
635 });
636 let user_agent = cx.update(|cx| {
637 format!(
638 "Zed/{} ({}; {})",
639 AppVersion::global(cx),
640 std::env::consts::OS,
641 std::env::consts::ARCH
642 )
643 });
644 let builder_client =
645 Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
646
647 let extension_store = cx.new(|cx| {
648 ExtensionStore::new(
649 extensions_dir.clone(),
650 Some(cache_dir),
651 proxy,
652 fs.clone(),
653 extension_client.clone(),
654 builder_client,
655 None,
656 node_runtime,
657 cx,
658 )
659 });
660
661 // Ensure that debounces fire.
662 let mut events = cx.events(&extension_store);
663 let executor = cx.executor();
664 let _task = cx.executor().spawn(async move {
665 while let Some(event) = events.next().await {
666 if let Event::StartedReloading = event {
667 executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
668 }
669 }
670 });
671
672 extension_store.update(cx, |_, cx| {
673 cx.subscribe(&extension_store, |_, _, event, _| {
674 if matches!(event, Event::ExtensionFailedToLoad(_)) {
675 panic!("extension failed to load");
676 }
677 })
678 .detach();
679 });
680
681 extension_store
682 .update(cx, |store, cx| {
683 store.install_dev_extension(test_extension_dir.clone(), cx)
684 })
685 .await
686 .unwrap();
687
688 let mut fake_servers = language_registry.register_fake_language_server(
689 LanguageServerName("gleam".into()),
690 lsp::ServerCapabilities {
691 completion_provider: Some(Default::default()),
692 ..Default::default()
693 },
694 None,
695 );
696
697 let (buffer, _handle) = project
698 .update(cx, |project, cx| {
699 project.open_local_buffer_with_lsp(project_dir.join("test.gleam"), cx)
700 })
701 .await
702 .unwrap();
703
704 // todo(windows)
705 // This test hangs here on Windows.
706 let fake_server = fake_servers.next().await.unwrap();
707 let expected_server_path =
708 extensions_dir.join(format!("work/{test_extension_id}/gleam-v1.2.3/gleam"));
709 let expected_binary_contents = language_server_version.lock().binary_contents.clone();
710
711 assert_eq!(fake_server.binary.path, expected_server_path);
712 assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
713 assert_eq!(
714 fs.load(&expected_server_path).await.unwrap(),
715 expected_binary_contents
716 );
717 assert_eq!(language_server_version.lock().http_request_count, 2);
718 assert_eq!(
719 [
720 status_updates.next().await.unwrap(),
721 status_updates.next().await.unwrap(),
722 status_updates.next().await.unwrap(),
723 status_updates.next().await.unwrap(),
724 ],
725 [
726 (
727 LanguageServerName::new_static("gleam"),
728 BinaryStatus::Starting
729 ),
730 (
731 LanguageServerName::new_static("gleam"),
732 BinaryStatus::CheckingForUpdate
733 ),
734 (
735 LanguageServerName::new_static("gleam"),
736 BinaryStatus::Downloading
737 ),
738 (LanguageServerName::new_static("gleam"), BinaryStatus::None)
739 ]
740 );
741
742 // The extension creates custom labels for completion items.
743 fake_server.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
744 Ok(Some(lsp::CompletionResponse::Array(vec![
745 lsp::CompletionItem {
746 label: "foo".into(),
747 kind: Some(lsp::CompletionItemKind::FUNCTION),
748 detail: Some("fn() -> Result(Nil, Error)".into()),
749 ..Default::default()
750 },
751 lsp::CompletionItem {
752 label: "bar.baz".into(),
753 kind: Some(lsp::CompletionItemKind::FUNCTION),
754 detail: Some("fn(List(a)) -> a".into()),
755 ..Default::default()
756 },
757 lsp::CompletionItem {
758 label: "Quux".into(),
759 kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
760 detail: Some("fn(String) -> T".into()),
761 ..Default::default()
762 },
763 lsp::CompletionItem {
764 label: "my_string".into(),
765 kind: Some(lsp::CompletionItemKind::CONSTANT),
766 detail: Some("String".into()),
767 ..Default::default()
768 },
769 ])))
770 });
771
772 let completion_labels = project
773 .update(cx, |project, cx| {
774 project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
775 })
776 .await
777 .unwrap()
778 .into_iter()
779 .flat_map(|response| response.completions)
780 .map(|c| c.label.text)
781 .collect::<Vec<_>>();
782 assert_eq!(
783 completion_labels,
784 [
785 "foo: fn() -> Result(Nil, Error)".to_string(),
786 "bar.baz: fn(List(a)) -> a".to_string(),
787 "Quux: fn(String) -> T".to_string(),
788 "my_string: String".to_string(),
789 ]
790 );
791
792 // Simulate a new version of the language server being released
793 language_server_version.lock().version = "v2.0.0".into();
794 language_server_version.lock().binary_contents = "the-new-binary-contents".into();
795 language_server_version.lock().http_request_count = 0;
796
797 // Start a new instance of the language server.
798 project.update(cx, |project, cx| {
799 project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
800 });
801 cx.executor().run_until_parked();
802
803 // The extension has cached the binary path, and does not attempt
804 // to reinstall it.
805 let fake_server = fake_servers.next().await.unwrap();
806 assert_eq!(fake_server.binary.path, expected_server_path);
807 assert_eq!(
808 fs.load(&expected_server_path).await.unwrap(),
809 expected_binary_contents
810 );
811 assert_eq!(language_server_version.lock().http_request_count, 0);
812
813 // Reload the extension, clearing its cache.
814 // Start a new instance of the language server.
815 extension_store
816 .update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
817 .await;
818
819 cx.executor().run_until_parked();
820 project.update(cx, |project, cx| {
821 project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
822 });
823
824 // The extension re-fetches the latest version of the language server.
825 let fake_server = fake_servers.next().await.unwrap();
826 let new_expected_server_path =
827 extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
828 let expected_binary_contents = language_server_version.lock().binary_contents.clone();
829 assert_eq!(fake_server.binary.path, new_expected_server_path);
830 assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
831 assert_eq!(
832 fs.load(&new_expected_server_path).await.unwrap(),
833 expected_binary_contents
834 );
835
836 // The old language server directory has been cleaned up.
837 assert!(fs.metadata(&expected_server_path).await.unwrap().is_none());
838}
839
840fn init_test(cx: &mut TestAppContext) {
841 cx.update(|cx| {
842 let store = SettingsStore::test(cx);
843 cx.set_global(store);
844 release_channel::init(SemanticVersion::default(), cx);
845 extension::init(cx);
846 theme::init(theme::LoadThemes::JustBase, cx);
847 Project::init_settings(cx);
848 ExtensionSettings::register(cx);
849 language::init(cx);
850 });
851}