1use crate::{
2 build_extension::{CompileExtensionOptions, ExtensionBuilder},
3 ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionIndexThemeEntry,
4 ExtensionManifest, ExtensionStore, GrammarManifestEntry, RELOAD_DEBOUNCE_DURATION,
5};
6use async_compression::futures::bufread::GzipEncoder;
7use collections::BTreeMap;
8use fs::{FakeFs, Fs};
9use futures::{io::BufReader, AsyncReadExt, StreamExt};
10use gpui::{Context, TestAppContext};
11use language::{
12 Language, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus,
13 LanguageServerName,
14};
15use node_runtime::FakeNodeRuntime;
16use project::Project;
17use serde_json::json;
18use settings::SettingsStore;
19use std::{
20 ffi::OsString,
21 path::{Path, PathBuf},
22 sync::Arc,
23};
24use theme::ThemeRegistry;
25use util::http::{self, FakeHttpClient, Response};
26
27#[gpui::test]
28async fn test_extension_store(cx: &mut TestAppContext) {
29 cx.update(|cx| {
30 let store = SettingsStore::test(cx);
31 cx.set_global(store);
32 theme::init(theme::LoadThemes::JustBase, cx);
33 });
34
35 let fs = FakeFs::new(cx.executor());
36 let http_client = FakeHttpClient::with_200_response();
37
38 fs.insert_tree(
39 "/the-extension-dir",
40 json!({
41 "installed": {
42 "zed-monokai": {
43 "extension.json": r#"{
44 "id": "zed-monokai",
45 "name": "Zed Monokai",
46 "version": "2.0.0",
47 "themes": {
48 "Monokai Dark": "themes/monokai.json",
49 "Monokai Light": "themes/monokai.json",
50 "Monokai Pro Dark": "themes/monokai-pro.json",
51 "Monokai Pro Light": "themes/monokai-pro.json"
52 }
53 }"#,
54 "themes": {
55 "monokai.json": r#"{
56 "name": "Monokai",
57 "author": "Someone",
58 "themes": [
59 {
60 "name": "Monokai Dark",
61 "appearance": "dark",
62 "style": {}
63 },
64 {
65 "name": "Monokai Light",
66 "appearance": "light",
67 "style": {}
68 }
69 ]
70 }"#,
71 "monokai-pro.json": r#"{
72 "name": "Monokai Pro",
73 "author": "Someone",
74 "themes": [
75 {
76 "name": "Monokai Pro Dark",
77 "appearance": "dark",
78 "style": {}
79 },
80 {
81 "name": "Monokai Pro Light",
82 "appearance": "light",
83 "style": {}
84 }
85 ]
86 }"#,
87 }
88 },
89 "zed-ruby": {
90 "extension.json": r#"{
91 "id": "zed-ruby",
92 "name": "Zed Ruby",
93 "version": "1.0.0",
94 "grammars": {
95 "ruby": "grammars/ruby.wasm",
96 "embedded_template": "grammars/embedded_template.wasm"
97 },
98 "languages": {
99 "ruby": "languages/ruby",
100 "erb": "languages/erb"
101 }
102 }"#,
103 "grammars": {
104 "ruby.wasm": "",
105 "embedded_template.wasm": "",
106 },
107 "languages": {
108 "ruby": {
109 "config.toml": r#"
110 name = "Ruby"
111 grammar = "ruby"
112 path_suffixes = ["rb"]
113 "#,
114 "highlights.scm": "",
115 },
116 "erb": {
117 "config.toml": r#"
118 name = "ERB"
119 grammar = "embedded_template"
120 path_suffixes = ["erb"]
121 "#,
122 "highlights.scm": "",
123 }
124 },
125 }
126 }
127 }),
128 )
129 .await;
130
131 let mut expected_index = ExtensionIndex {
132 extensions: [
133 (
134 "zed-ruby".into(),
135 ExtensionIndexEntry {
136 manifest: Arc::new(ExtensionManifest {
137 id: "zed-ruby".into(),
138 name: "Zed Ruby".into(),
139 version: "1.0.0".into(),
140 description: None,
141 authors: Vec::new(),
142 repository: None,
143 themes: Default::default(),
144 lib: Default::default(),
145 languages: vec!["languages/erb".into(), "languages/ruby".into()],
146 grammars: [
147 ("embedded_template".into(), GrammarManifestEntry::default()),
148 ("ruby".into(), GrammarManifestEntry::default()),
149 ]
150 .into_iter()
151 .collect(),
152 language_servers: BTreeMap::default(),
153 }),
154 dev: false,
155 },
156 ),
157 (
158 "zed-monokai".into(),
159 ExtensionIndexEntry {
160 manifest: Arc::new(ExtensionManifest {
161 id: "zed-monokai".into(),
162 name: "Zed Monokai".into(),
163 version: "2.0.0".into(),
164 description: None,
165 authors: vec![],
166 repository: None,
167 themes: vec![
168 "themes/monokai-pro.json".into(),
169 "themes/monokai.json".into(),
170 ],
171 lib: Default::default(),
172 languages: Default::default(),
173 grammars: BTreeMap::default(),
174 language_servers: BTreeMap::default(),
175 }),
176 dev: false,
177 },
178 ),
179 ]
180 .into_iter()
181 .collect(),
182 languages: [
183 (
184 "ERB".into(),
185 ExtensionIndexLanguageEntry {
186 extension: "zed-ruby".into(),
187 path: "languages/erb".into(),
188 grammar: Some("embedded_template".into()),
189 matcher: LanguageMatcher {
190 path_suffixes: vec!["erb".into()],
191 first_line_pattern: None,
192 },
193 },
194 ),
195 (
196 "Ruby".into(),
197 ExtensionIndexLanguageEntry {
198 extension: "zed-ruby".into(),
199 path: "languages/ruby".into(),
200 grammar: Some("ruby".into()),
201 matcher: LanguageMatcher {
202 path_suffixes: vec!["rb".into()],
203 first_line_pattern: None,
204 },
205 },
206 ),
207 ]
208 .into_iter()
209 .collect(),
210 themes: [
211 (
212 "Monokai Dark".into(),
213 ExtensionIndexThemeEntry {
214 extension: "zed-monokai".into(),
215 path: "themes/monokai.json".into(),
216 },
217 ),
218 (
219 "Monokai Light".into(),
220 ExtensionIndexThemeEntry {
221 extension: "zed-monokai".into(),
222 path: "themes/monokai.json".into(),
223 },
224 ),
225 (
226 "Monokai Pro Dark".into(),
227 ExtensionIndexThemeEntry {
228 extension: "zed-monokai".into(),
229 path: "themes/monokai-pro.json".into(),
230 },
231 ),
232 (
233 "Monokai Pro Light".into(),
234 ExtensionIndexThemeEntry {
235 extension: "zed-monokai".into(),
236 path: "themes/monokai-pro.json".into(),
237 },
238 ),
239 ]
240 .into_iter()
241 .collect(),
242 };
243
244 let language_registry = Arc::new(LanguageRegistry::test());
245 let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
246 let node_runtime = FakeNodeRuntime::new();
247
248 let store = cx.new_model(|cx| {
249 ExtensionStore::new(
250 PathBuf::from("/the-extension-dir"),
251 fs.clone(),
252 http_client.clone(),
253 node_runtime.clone(),
254 language_registry.clone(),
255 theme_registry.clone(),
256 cx,
257 )
258 });
259
260 cx.executor().advance_clock(super::RELOAD_DEBOUNCE_DURATION);
261 store.read_with(cx, |store, _| {
262 let index = &store.extension_index;
263 assert_eq!(index.extensions, expected_index.extensions);
264 assert_eq!(index.languages, expected_index.languages);
265 assert_eq!(index.themes, expected_index.themes);
266
267 assert_eq!(
268 language_registry.language_names(),
269 ["ERB", "Plain Text", "Ruby"]
270 );
271 assert_eq!(
272 theme_registry.list_names(false),
273 [
274 "Monokai Dark",
275 "Monokai Light",
276 "Monokai Pro Dark",
277 "Monokai Pro Light",
278 "One Dark",
279 ]
280 );
281 });
282
283 fs.insert_tree(
284 "/the-extension-dir/installed/zed-gruvbox",
285 json!({
286 "extension.json": r#"{
287 "id": "zed-gruvbox",
288 "name": "Zed Gruvbox",
289 "version": "1.0.0",
290 "themes": {
291 "Gruvbox": "themes/gruvbox.json"
292 }
293 }"#,
294 "themes": {
295 "gruvbox.json": r#"{
296 "name": "Gruvbox",
297 "author": "Someone Else",
298 "themes": [
299 {
300 "name": "Gruvbox",
301 "appearance": "dark",
302 "style": {}
303 }
304 ]
305 }"#,
306 }
307 }),
308 )
309 .await;
310
311 expected_index.extensions.insert(
312 "zed-gruvbox".into(),
313 ExtensionIndexEntry {
314 manifest: Arc::new(ExtensionManifest {
315 id: "zed-gruvbox".into(),
316 name: "Zed Gruvbox".into(),
317 version: "1.0.0".into(),
318 description: None,
319 authors: vec![],
320 repository: None,
321 themes: vec!["themes/gruvbox.json".into()],
322 lib: Default::default(),
323 languages: Default::default(),
324 grammars: BTreeMap::default(),
325 language_servers: BTreeMap::default(),
326 }),
327 dev: false,
328 },
329 );
330 expected_index.themes.insert(
331 "Gruvbox".into(),
332 ExtensionIndexThemeEntry {
333 extension: "zed-gruvbox".into(),
334 path: "themes/gruvbox.json".into(),
335 },
336 );
337
338 let _ = store.update(cx, |store, _| store.reload(None));
339
340 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
341 store.read_with(cx, |store, _| {
342 let index = &store.extension_index;
343 assert_eq!(index.extensions, expected_index.extensions);
344 assert_eq!(index.languages, expected_index.languages);
345 assert_eq!(index.themes, expected_index.themes);
346
347 assert_eq!(
348 theme_registry.list_names(false),
349 [
350 "Gruvbox",
351 "Monokai Dark",
352 "Monokai Light",
353 "Monokai Pro Dark",
354 "Monokai Pro Light",
355 "One Dark",
356 ]
357 );
358 });
359
360 let prev_fs_metadata_call_count = fs.metadata_call_count();
361 let prev_fs_read_dir_call_count = fs.read_dir_call_count();
362
363 // Create new extension store, as if Zed were restarting.
364 drop(store);
365 let store = cx.new_model(|cx| {
366 ExtensionStore::new(
367 PathBuf::from("/the-extension-dir"),
368 fs.clone(),
369 http_client.clone(),
370 node_runtime.clone(),
371 language_registry.clone(),
372 theme_registry.clone(),
373 cx,
374 )
375 });
376
377 cx.executor().run_until_parked();
378 store.read_with(cx, |store, _| {
379 assert_eq!(store.extension_index, expected_index);
380 assert_eq!(
381 language_registry.language_names(),
382 ["ERB", "Plain Text", "Ruby"]
383 );
384 assert_eq!(
385 language_registry.grammar_names(),
386 ["embedded_template".into(), "ruby".into()]
387 );
388 assert_eq!(
389 theme_registry.list_names(false),
390 [
391 "Gruvbox",
392 "Monokai Dark",
393 "Monokai Light",
394 "Monokai Pro Dark",
395 "Monokai Pro Light",
396 "One Dark",
397 ]
398 );
399
400 // The on-disk manifest limits the number of FS calls that need to be made
401 // on startup.
402 assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
403 assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
404 });
405
406 store.update(cx, |store, cx| {
407 store.uninstall_extension("zed-ruby".into(), cx)
408 });
409
410 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
411 expected_index.extensions.remove("zed-ruby");
412 expected_index.languages.remove("Ruby");
413 expected_index.languages.remove("ERB");
414
415 store.read_with(cx, |store, _| {
416 assert_eq!(store.extension_index, expected_index);
417 assert_eq!(language_registry.language_names(), ["Plain Text"]);
418 assert_eq!(language_registry.grammar_names(), []);
419 });
420}
421
422#[gpui::test]
423async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
424 init_test(cx);
425
426 let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
427 .parent()
428 .unwrap()
429 .parent()
430 .unwrap();
431 let cache_dir = root_dir.join("target");
432 let gleam_extension_dir = root_dir.join("extensions").join("gleam");
433
434 cx.executor().allow_parking();
435 ExtensionBuilder::new(cache_dir, http::client())
436 .compile_extension(
437 &gleam_extension_dir,
438 CompileExtensionOptions { release: false },
439 )
440 .await
441 .unwrap();
442 cx.executor().forbid_parking();
443
444 let fs = FakeFs::new(cx.executor());
445 fs.insert_tree("/the-extension-dir", json!({ "installed": {} }))
446 .await;
447 fs.insert_tree_from_real_fs("/the-extension-dir/installed/gleam", gleam_extension_dir)
448 .await;
449
450 fs.insert_tree(
451 "/the-project-dir",
452 json!({
453 ".tool-versions": "rust 1.73.0",
454 "test.gleam": ""
455 }),
456 )
457 .await;
458
459 let project = Project::test(fs.clone(), ["/the-project-dir".as_ref()], cx).await;
460
461 let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
462 let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
463 let node_runtime = FakeNodeRuntime::new();
464
465 let mut status_updates = language_registry.language_server_binary_statuses();
466
467 let http_client = FakeHttpClient::create({
468 move |request| async move {
469 match request.uri().to_string().as_str() {
470 "https://api.github.com/repos/gleam-lang/gleam/releases" => Ok(Response::new(
471 json!([
472 {
473 "tag_name": "v1.2.3",
474 "prerelease": false,
475 "tarball_url": "",
476 "zipball_url": "",
477 "assets": [
478 {
479 "name": "gleam-v1.2.3-aarch64-apple-darwin.tar.gz",
480 "browser_download_url": "http://example.com/the-download"
481 }
482 ]
483 }
484 ])
485 .to_string()
486 .into(),
487 )),
488
489 "http://example.com/the-download" => {
490 let mut bytes = Vec::<u8>::new();
491 let mut archive = async_tar::Builder::new(&mut bytes);
492 let mut header = async_tar::Header::new_gnu();
493 let content = "the-gleam-binary-contents".as_bytes();
494 header.set_size(content.len() as u64);
495 archive
496 .append_data(&mut header, "gleam", content)
497 .await
498 .unwrap();
499 archive.into_inner().await.unwrap();
500
501 let mut gzipped_bytes = Vec::new();
502 let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
503 encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
504
505 Ok(Response::new(gzipped_bytes.into()))
506 }
507
508 _ => Ok(Response::builder().status(404).body("not found".into())?),
509 }
510 }
511 });
512
513 let _store = cx.new_model(|cx| {
514 ExtensionStore::new(
515 PathBuf::from("/the-extension-dir"),
516 fs.clone(),
517 http_client.clone(),
518 node_runtime,
519 language_registry.clone(),
520 theme_registry.clone(),
521 cx,
522 )
523 });
524
525 cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
526
527 let mut fake_servers = language_registry.fake_language_servers("Gleam");
528
529 let buffer = project
530 .update(cx, |project, cx| {
531 project.open_local_buffer("/the-project-dir/test.gleam", cx)
532 })
533 .await
534 .unwrap();
535
536 project.update(cx, |project, cx| {
537 project.set_language_for_buffer(
538 &buffer,
539 Arc::new(Language::new(
540 LanguageConfig {
541 name: "Gleam".into(),
542 ..Default::default()
543 },
544 None,
545 )),
546 cx,
547 )
548 });
549
550 let fake_server = fake_servers.next().await.unwrap();
551
552 assert_eq!(
553 fs.load("/the-extension-dir/work/gleam/gleam-v1.2.3/gleam".as_ref())
554 .await
555 .unwrap(),
556 "the-gleam-binary-contents"
557 );
558
559 assert_eq!(
560 fake_server.binary.path,
561 PathBuf::from("/the-extension-dir/work/gleam/gleam-v1.2.3/gleam")
562 );
563 assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
564
565 assert_eq!(
566 [
567 status_updates.next().await.unwrap(),
568 status_updates.next().await.unwrap(),
569 status_updates.next().await.unwrap(),
570 ],
571 [
572 (
573 LanguageServerName("gleam".into()),
574 LanguageServerBinaryStatus::CheckingForUpdate
575 ),
576 (
577 LanguageServerName("gleam".into()),
578 LanguageServerBinaryStatus::Downloading
579 ),
580 (
581 LanguageServerName("gleam".into()),
582 LanguageServerBinaryStatus::Downloaded
583 )
584 ]
585 );
586}
587
588fn init_test(cx: &mut TestAppContext) {
589 cx.update(|cx| {
590 let store = SettingsStore::test(cx);
591 cx.set_global(store);
592 theme::init(theme::LoadThemes::JustBase, cx);
593 Project::init_settings(cx);
594 language::init(cx);
595 });
596}