extension_store_test.rs

  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}