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