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