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