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