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