1use crate::{
2 Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
3 ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
4 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, FutureExt, StreamExt, io::BufReader};
11use gpui::{AppContext as _, BackgroundExecutor, 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::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 agent_servers: BTreeMap::default(),
163 slash_commands: BTreeMap::default(),
164 snippets: None,
165 capabilities: Vec::new(),
166 debug_adapters: Default::default(),
167 debug_locators: Default::default(),
168 language_model_providers: BTreeMap::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 language_model_providers: BTreeMap::default(),
201 }),
202 dev: false,
203 },
204 ),
205 ]
206 .into_iter()
207 .collect(),
208 languages: [
209 (
210 "ERB".into(),
211 ExtensionIndexLanguageEntry {
212 extension: "zed-ruby".into(),
213 path: "languages/erb".into(),
214 grammar: Some("embedded_template".into()),
215 hidden: false,
216 matcher: LanguageMatcher {
217 path_suffixes: vec!["erb".into()],
218 first_line_pattern: None,
219 ..LanguageMatcher::default()
220 },
221 },
222 ),
223 (
224 "Ruby".into(),
225 ExtensionIndexLanguageEntry {
226 extension: "zed-ruby".into(),
227 path: "languages/ruby".into(),
228 grammar: Some("ruby".into()),
229 hidden: false,
230 matcher: LanguageMatcher {
231 path_suffixes: vec!["rb".into()],
232 first_line_pattern: None,
233 ..LanguageMatcher::default()
234 },
235 },
236 ),
237 ]
238 .into_iter()
239 .collect(),
240 themes: [
241 (
242 "Monokai Dark".into(),
243 ExtensionIndexThemeEntry {
244 extension: "zed-monokai".into(),
245 path: "themes/monokai.json".into(),
246 },
247 ),
248 (
249 "Monokai Light".into(),
250 ExtensionIndexThemeEntry {
251 extension: "zed-monokai".into(),
252 path: "themes/monokai.json".into(),
253 },
254 ),
255 (
256 "Monokai Pro Dark".into(),
257 ExtensionIndexThemeEntry {
258 extension: "zed-monokai".into(),
259 path: "themes/monokai-pro.json".into(),
260 },
261 ),
262 (
263 "Monokai Pro Light".into(),
264 ExtensionIndexThemeEntry {
265 extension: "zed-monokai".into(),
266 path: "themes/monokai-pro.json".into(),
267 },
268 ),
269 ]
270 .into_iter()
271 .collect(),
272 icon_themes: BTreeMap::default(),
273 };
274
275 let proxy = Arc::new(ExtensionHostProxy::new());
276 let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
277 theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
278 let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
279 language_extension::init(LspAccess::Noop, proxy.clone(), language_registry.clone());
280 let node_runtime = NodeRuntime::unavailable();
281
282 let store = cx.new(|cx| {
283 ExtensionStore::new(
284 PathBuf::from("/the-extension-dir"),
285 None,
286 proxy.clone(),
287 fs.clone(),
288 http_client.clone(),
289 http_client.clone(),
290 None,
291 node_runtime.clone(),
292 cx,
293 )
294 });
295
296 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
297 store.read_with(cx, |store, _| {
298 let index = &store.extension_index;
299 assert_eq!(index.extensions, expected_index.extensions);
300
301 for ((actual_key, actual_language), (expected_key, expected_language)) in
302 index.languages.iter().zip(expected_index.languages.iter())
303 {
304 assert_eq!(actual_key, expected_key);
305 assert_eq!(actual_language.grammar, expected_language.grammar);
306 assert_eq!(actual_language.matcher, expected_language.matcher);
307 assert_eq!(actual_language.hidden, expected_language.hidden);
308 }
309 assert_eq!(index.themes, expected_index.themes);
310
311 assert_eq!(
312 language_registry.language_names(),
313 [
314 LanguageName::new_static("ERB"),
315 LanguageName::new_static("Plain Text"),
316 LanguageName::new_static("Ruby"),
317 ]
318 );
319 assert_eq!(
320 theme_registry.list_names(),
321 [
322 "Monokai Dark",
323 "Monokai Light",
324 "Monokai Pro Dark",
325 "Monokai Pro Light",
326 "One Dark",
327 ]
328 );
329 });
330
331 fs.insert_tree(
332 "/the-extension-dir/installed/zed-gruvbox",
333 json!({
334 "extension.json": r#"{
335 "id": "zed-gruvbox",
336 "name": "Zed Gruvbox",
337 "version": "1.0.0",
338 "themes": {
339 "Gruvbox": "themes/gruvbox.json"
340 }
341 }"#,
342 "themes": {
343 "gruvbox.json": r#"{
344 "name": "Gruvbox",
345 "author": "Someone Else",
346 "themes": [
347 {
348 "name": "Gruvbox",
349 "appearance": "dark",
350 "style": {}
351 }
352 ]
353 }"#,
354 }
355 }),
356 )
357 .await;
358
359 expected_index.extensions.insert(
360 "zed-gruvbox".into(),
361 ExtensionIndexEntry {
362 manifest: Arc::new(ExtensionManifest {
363 id: "zed-gruvbox".into(),
364 name: "Zed Gruvbox".into(),
365 version: "1.0.0".into(),
366 schema_version: SchemaVersion::ZERO,
367 description: None,
368 authors: vec![],
369 repository: None,
370 themes: vec!["themes/gruvbox.json".into()],
371 icon_themes: Vec::new(),
372 lib: Default::default(),
373 languages: Default::default(),
374 grammars: BTreeMap::default(),
375 language_servers: BTreeMap::default(),
376 context_servers: BTreeMap::default(),
377 agent_servers: BTreeMap::default(),
378 slash_commands: BTreeMap::default(),
379 snippets: None,
380 capabilities: Vec::new(),
381 debug_adapters: Default::default(),
382 debug_locators: Default::default(),
383 language_model_providers: BTreeMap::default(),
384 }),
385 dev: false,
386 },
387 );
388 expected_index.themes.insert(
389 "Gruvbox".into(),
390 ExtensionIndexThemeEntry {
391 extension: "zed-gruvbox".into(),
392 path: "themes/gruvbox.json".into(),
393 },
394 );
395
396 #[allow(clippy::let_underscore_future)]
397 let _ = store.update(cx, |store, cx| store.reload(None, cx));
398
399 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
400 store.read_with(cx, |store, _| {
401 let index = &store.extension_index;
402
403 for ((actual_key, actual_language), (expected_key, expected_language)) in
404 index.languages.iter().zip(expected_index.languages.iter())
405 {
406 assert_eq!(actual_key, expected_key);
407 assert_eq!(actual_language.grammar, expected_language.grammar);
408 assert_eq!(actual_language.matcher, expected_language.matcher);
409 assert_eq!(actual_language.hidden, expected_language.hidden);
410 }
411
412 assert_eq!(index.extensions, expected_index.extensions);
413 assert_eq!(index.themes, expected_index.themes);
414
415 assert_eq!(
416 theme_registry.list_names(),
417 [
418 "Gruvbox",
419 "Monokai Dark",
420 "Monokai Light",
421 "Monokai Pro Dark",
422 "Monokai Pro Light",
423 "One Dark",
424 ]
425 );
426 });
427
428 let prev_fs_metadata_call_count = fs.metadata_call_count();
429 let prev_fs_read_dir_call_count = fs.read_dir_call_count();
430
431 // Create new extension store, as if Zed were restarting.
432 drop(store);
433 let store = cx.new(|cx| {
434 ExtensionStore::new(
435 PathBuf::from("/the-extension-dir"),
436 None,
437 proxy,
438 fs.clone(),
439 http_client.clone(),
440 http_client.clone(),
441 None,
442 node_runtime.clone(),
443 cx,
444 )
445 });
446
447 cx.executor().run_until_parked();
448 store.read_with(cx, |store, _| {
449 assert_eq!(store.extension_index.extensions, expected_index.extensions);
450 assert_eq!(store.extension_index.themes, expected_index.themes);
451 assert_eq!(
452 store.extension_index.icon_themes,
453 expected_index.icon_themes
454 );
455
456 for ((actual_key, actual_language), (expected_key, expected_language)) in store
457 .extension_index
458 .languages
459 .iter()
460 .zip(expected_index.languages.iter())
461 {
462 assert_eq!(actual_key, expected_key);
463 assert_eq!(actual_language.grammar, expected_language.grammar);
464 assert_eq!(actual_language.matcher, expected_language.matcher);
465 assert_eq!(actual_language.hidden, expected_language.hidden);
466 }
467
468 assert_eq!(
469 language_registry.language_names(),
470 [
471 LanguageName::new_static("ERB"),
472 LanguageName::new_static("Plain Text"),
473 LanguageName::new_static("Ruby"),
474 ]
475 );
476 assert_eq!(
477 language_registry.grammar_names(),
478 ["embedded_template".into(), "ruby".into()]
479 );
480 assert_eq!(
481 theme_registry.list_names(),
482 [
483 "Gruvbox",
484 "Monokai Dark",
485 "Monokai Light",
486 "Monokai Pro Dark",
487 "Monokai Pro Light",
488 "One Dark",
489 ]
490 );
491
492 // The on-disk manifest limits the number of FS calls that need to be made
493 // on startup.
494 assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
495 assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
496 });
497
498 store.update(cx, |store, cx| {
499 store
500 .uninstall_extension("zed-ruby".into(), cx)
501 .detach_and_log_err(cx);
502 });
503
504 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
505 expected_index.extensions.remove("zed-ruby");
506 expected_index.languages.remove("Ruby");
507 expected_index.languages.remove("ERB");
508
509 store.read_with(cx, |store, _| {
510 assert_eq!(store.extension_index.extensions, expected_index.extensions);
511 assert_eq!(store.extension_index.themes, expected_index.themes);
512 assert_eq!(
513 store.extension_index.icon_themes,
514 expected_index.icon_themes
515 );
516
517 for ((actual_key, actual_language), (expected_key, expected_language)) in store
518 .extension_index
519 .languages
520 .iter()
521 .zip(expected_index.languages.iter())
522 {
523 assert_eq!(actual_key, expected_key);
524 assert_eq!(actual_language.grammar, expected_language.grammar);
525 assert_eq!(actual_language.matcher, expected_language.matcher);
526 assert_eq!(actual_language.hidden, expected_language.hidden);
527 }
528
529 assert_eq!(
530 language_registry.language_names(),
531 [LanguageName::new_static("Plain Text")]
532 );
533 assert_eq!(language_registry.grammar_names(), []);
534 });
535}
536
537#[gpui::test]
538async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
539 init_test(cx);
540 cx.executor().allow_parking();
541
542 let executor = cx.executor();
543 async fn await_or_timeout<T>(
544 executor: &BackgroundExecutor,
545 what: &'static str,
546 seconds: u64,
547 future: impl std::future::Future<Output = T>,
548 ) -> T {
549 let timeout = executor.timer(std::time::Duration::from_secs(seconds));
550
551 futures::select! {
552 output = future.fuse() => output,
553 _ = futures::FutureExt::fuse(timeout) => panic!(
554 "[test_extension_store_with_test_extension] timed out after {seconds}s while {what}"
555 )
556 }
557 }
558
559 let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
560 .parent()
561 .unwrap()
562 .parent()
563 .unwrap();
564 let cache_dir = root_dir.join("target");
565 let test_extension_id = "test-extension";
566 let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
567
568 let fs = Arc::new(RealFs::new(None, cx.executor()));
569 let extensions_tree = TempTree::new(json!({
570 "installed": {},
571 "work": {}
572 }));
573 let project_dir = TempTree::new(json!({
574 "test.gleam": ""
575 }));
576
577 let extensions_dir = extensions_tree.path().canonicalize().unwrap();
578 let project_dir = project_dir.path().canonicalize().unwrap();
579
580 let project = await_or_timeout(
581 &executor,
582 "awaiting Project::test",
583 5,
584 Project::test(fs.clone(), [project_dir.as_path()], cx),
585 )
586 .await;
587
588 let proxy = Arc::new(ExtensionHostProxy::new());
589 let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
590 theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
591 let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
592 language_extension::init(
593 LspAccess::ViaLspStore(project.update(cx, |project, _| project.lsp_store())),
594 proxy.clone(),
595 language_registry.clone(),
596 );
597 let node_runtime = NodeRuntime::unavailable();
598
599 let mut status_updates = language_registry.language_server_binary_statuses();
600
601 struct FakeLanguageServerVersion {
602 version: String,
603 binary_contents: String,
604 http_request_count: usize,
605 }
606
607 let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
608 version: "v1.2.3".into(),
609 binary_contents: "the-binary-contents".into(),
610 http_request_count: 0,
611 }));
612
613 let extension_client = FakeHttpClient::create({
614 let language_server_version = language_server_version.clone();
615 move |request| {
616 let language_server_version = language_server_version.clone();
617 async move {
618 let version = language_server_version.lock().version.clone();
619 let binary_contents = language_server_version.lock().binary_contents.clone();
620
621 let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
622 let asset_download_uri =
623 format!("https://fake-download.example.com/gleam-{version}");
624
625 let uri = request.uri().to_string();
626 if uri == github_releases_uri {
627 language_server_version.lock().http_request_count += 1;
628 Ok(Response::new(
629 json!([
630 {
631 "tag_name": version,
632 "prerelease": false,
633 "tarball_url": "",
634 "zipball_url": "",
635 "assets": [
636 {
637 "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
638 "browser_download_url": asset_download_uri
639 },
640 {
641 "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
642 "browser_download_url": asset_download_uri
643 },
644 {
645 "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
646 "browser_download_url": asset_download_uri
647 },
648 {
649 "name": format!("gleam-{version}-x86_64-pc-windows-msvc.tar.gz"),
650 "browser_download_url": asset_download_uri
651 }
652 ]
653 }
654 ])
655 .to_string()
656 .into(),
657 ))
658 } else if uri == asset_download_uri {
659 language_server_version.lock().http_request_count += 1;
660 let mut bytes = Vec::<u8>::new();
661 let mut archive = async_tar::Builder::new(&mut bytes);
662 let mut header = async_tar::Header::new_gnu();
663 header.set_size(binary_contents.len() as u64);
664 archive
665 .append_data(&mut header, "gleam", binary_contents.as_bytes())
666 .await
667 .unwrap();
668 archive.into_inner().await.unwrap();
669 let mut gzipped_bytes = Vec::new();
670 let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
671 encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
672 Ok(Response::new(gzipped_bytes.into()))
673 } else {
674 Ok(Response::builder().status(404).body("not found".into())?)
675 }
676 }
677 }
678 });
679 let user_agent = cx.update(|cx| {
680 format!(
681 "Zed/{} ({}; {})",
682 AppVersion::global(cx),
683 std::env::consts::OS,
684 std::env::consts::ARCH
685 )
686 });
687 let builder_client =
688 Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
689
690 let extension_store = cx.new(|cx| {
691 ExtensionStore::new(
692 extensions_dir.clone(),
693 Some(cache_dir),
694 proxy,
695 fs.clone(),
696 extension_client.clone(),
697 builder_client,
698 None,
699 node_runtime,
700 cx,
701 )
702 });
703
704 // Ensure that debounces fire.
705 let mut events = cx.events(&extension_store);
706 let executor = cx.executor();
707 let _task = cx.executor().spawn(async move {
708 while let Some(event) = events.next().await {
709 if let Event::StartedReloading = event {
710 executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
711 }
712 }
713 });
714
715 extension_store.update(cx, |_, cx| {
716 cx.subscribe(&extension_store, |_, _, event, _| {
717 if matches!(event, Event::ExtensionFailedToLoad(_)) {
718 panic!("extension failed to load");
719 }
720 })
721 .detach();
722 });
723
724 let executor = cx.executor();
725 await_or_timeout(
726 &executor,
727 "awaiting install_dev_extension",
728 60,
729 extension_store.update(cx, |store, cx| {
730 store.install_dev_extension(test_extension_dir.clone(), cx)
731 }),
732 )
733 .await
734 .unwrap();
735
736 let mut fake_servers = language_registry.register_fake_lsp_server(
737 LanguageServerName("gleam".into()),
738 lsp::ServerCapabilities {
739 completion_provider: Some(Default::default()),
740 ..Default::default()
741 },
742 None,
743 );
744 cx.executor().run_until_parked();
745
746 let (buffer, _handle) = await_or_timeout(
747 &executor,
748 "awaiting open_local_buffer_with_lsp",
749 5,
750 project.update(cx, |project, cx| {
751 project.open_local_buffer_with_lsp(project_dir.join("test.gleam"), cx)
752 }),
753 )
754 .await
755 .unwrap();
756 cx.executor().run_until_parked();
757
758 let fake_server = await_or_timeout(
759 &executor,
760 "awaiting first fake server spawn",
761 10,
762 fake_servers.next(),
763 )
764 .await
765 .unwrap();
766
767 let work_dir = extensions_dir.join(format!("work/{test_extension_id}"));
768 let expected_server_path = work_dir.join("gleam-v1.2.3/gleam");
769 let expected_binary_contents = language_server_version.lock().binary_contents.clone();
770
771 // check that IO operations in extension work correctly
772 assert!(work_dir.join("dir-created-with-rel-path").exists());
773 assert!(work_dir.join("dir-created-with-abs-path").exists());
774 assert!(work_dir.join("file-created-with-abs-path").exists());
775 assert!(work_dir.join("file-created-with-rel-path").exists());
776
777 assert_eq!(fake_server.binary.path, expected_server_path);
778 assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
779 assert_eq!(
780 await_or_timeout(
781 &executor,
782 "awaiting fs.load(expected_server_path)",
783 5,
784 fs.load(&expected_server_path)
785 )
786 .await
787 .unwrap(),
788 expected_binary_contents
789 );
790 assert_eq!(language_server_version.lock().http_request_count, 2);
791 assert_eq!(
792 [
793 await_or_timeout(
794 &executor,
795 "awaiting status_updates #1",
796 5,
797 status_updates.next()
798 )
799 .await
800 .unwrap(),
801 await_or_timeout(
802 &executor,
803 "awaiting status_updates #2",
804 5,
805 status_updates.next()
806 )
807 .await
808 .unwrap(),
809 await_or_timeout(
810 &executor,
811 "awaiting status_updates #3",
812 5,
813 status_updates.next()
814 )
815 .await
816 .unwrap(),
817 await_or_timeout(
818 &executor,
819 "awaiting status_updates #4",
820 5,
821 status_updates.next()
822 )
823 .await
824 .unwrap(),
825 ],
826 [
827 (
828 LanguageServerName::new_static("gleam"),
829 BinaryStatus::Starting
830 ),
831 (
832 LanguageServerName::new_static("gleam"),
833 BinaryStatus::CheckingForUpdate
834 ),
835 (
836 LanguageServerName::new_static("gleam"),
837 BinaryStatus::Downloading
838 ),
839 (LanguageServerName::new_static("gleam"), BinaryStatus::None)
840 ]
841 );
842
843 // The extension creates custom labels for completion items.
844 fake_server.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
845 Ok(Some(lsp::CompletionResponse::Array(vec![
846 lsp::CompletionItem {
847 label: "foo".into(),
848 kind: Some(lsp::CompletionItemKind::FUNCTION),
849 detail: Some("fn() -> Result(Nil, Error)".into()),
850 ..Default::default()
851 },
852 lsp::CompletionItem {
853 label: "bar.baz".into(),
854 kind: Some(lsp::CompletionItemKind::FUNCTION),
855 detail: Some("fn(List(a)) -> a".into()),
856 ..Default::default()
857 },
858 lsp::CompletionItem {
859 label: "Quux".into(),
860 kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
861 detail: Some("fn(String) -> T".into()),
862 ..Default::default()
863 },
864 lsp::CompletionItem {
865 label: "my_string".into(),
866 kind: Some(lsp::CompletionItemKind::CONSTANT),
867 detail: Some("String".into()),
868 ..Default::default()
869 },
870 ])))
871 });
872
873 // `register_fake_lsp_server` can yield a server instance before the client has finished the LSP
874 // initialization handshake. Wait until we observe the client's `initialized` notification before
875 // issuing requests like completion.
876 await_or_timeout(
877 &executor,
878 "awaiting LSP Initialized notification",
879 5,
880 async {
881 fake_server
882 .clone()
883 .try_receive_notification::<lsp::notification::Initialized>()
884 .await;
885 },
886 )
887 .await;
888
889 let completion_labels = await_or_timeout(
890 &executor,
891 "awaiting completions",
892 5,
893 project.update(cx, |project, cx| {
894 project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
895 }),
896 )
897 .await
898 .unwrap()
899 .into_iter()
900 .flat_map(|response| response.completions)
901 .map(|c| c.label.text)
902 .collect::<Vec<_>>();
903 assert_eq!(
904 completion_labels,
905 [
906 "foo: fn() -> Result(Nil, Error)".to_string(),
907 "bar.baz: fn(List(a)) -> a".to_string(),
908 "Quux: fn(String) -> T".to_string(),
909 "my_string: String".to_string(),
910 ]
911 );
912
913 // Simulate a new version of the language server being released
914 language_server_version.lock().version = "v2.0.0".into();
915 language_server_version.lock().binary_contents = "the-new-binary-contents".into();
916 language_server_version.lock().http_request_count = 0;
917
918 // Start a new instance of the language server.
919 project.update(cx, |project, cx| {
920 project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
921 });
922 cx.executor().run_until_parked();
923
924 // The extension has cached the binary path, and does not attempt
925 // to reinstall it.
926 let fake_server = await_or_timeout(
927 &executor,
928 "awaiting second fake server spawn",
929 5,
930 fake_servers.next(),
931 )
932 .await
933 .unwrap();
934 assert_eq!(fake_server.binary.path, expected_server_path);
935 assert_eq!(
936 await_or_timeout(
937 &executor,
938 "awaiting fs.load(expected_server_path) after restart",
939 5,
940 fs.load(&expected_server_path)
941 )
942 .await
943 .unwrap(),
944 expected_binary_contents
945 );
946 assert_eq!(language_server_version.lock().http_request_count, 0);
947
948 // Reload the extension, clearing its cache.
949 // Start a new instance of the language server.
950 await_or_timeout(
951 &executor,
952 "awaiting extension_store.reload(test-extension)",
953 5,
954 extension_store.update(cx, |store, cx| {
955 store.reload(Some("test-extension".into()), cx)
956 }),
957 )
958 .await;
959 cx.executor().run_until_parked();
960 project.update(cx, |project, cx| {
961 project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
962 });
963
964 // The extension re-fetches the latest version of the language server.
965 let fake_server = await_or_timeout(
966 &executor,
967 "awaiting third fake server spawn",
968 5,
969 fake_servers.next(),
970 )
971 .await
972 .unwrap();
973 let new_expected_server_path =
974 extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
975 let expected_binary_contents = language_server_version.lock().binary_contents.clone();
976 assert_eq!(fake_server.binary.path, new_expected_server_path);
977 assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
978 assert_eq!(
979 await_or_timeout(
980 &executor,
981 "awaiting fs.load(new_expected_server_path)",
982 5,
983 fs.load(&new_expected_server_path)
984 )
985 .await
986 .unwrap(),
987 expected_binary_contents
988 );
989
990 // The old language server directory has been cleaned up.
991 assert!(
992 await_or_timeout(
993 &executor,
994 "awaiting fs.metadata(expected_server_path)",
995 5,
996 fs.metadata(&expected_server_path)
997 )
998 .await
999 .unwrap()
1000 .is_none()
1001 );
1002}
1003
1004fn init_test(cx: &mut TestAppContext) {
1005 cx.update(|cx| {
1006 let store = SettingsStore::test(cx);
1007 cx.set_global(store);
1008 release_channel::init(semver::Version::new(0, 0, 0), cx);
1009 extension::init(cx);
1010 theme_settings::init(theme::LoadThemes::JustBase, cx);
1011 gpui_tokio::init(cx);
1012 });
1013}