extension_store_test.rs

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