extension_store_test.rs

  1use crate::extension_manifest::SchemaVersion;
  2use crate::extension_settings::ExtensionSettings;
  3use crate::{
  4    Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
  5    ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
  6    RELOAD_DEBOUNCE_DURATION,
  7};
  8use assistant_slash_command::SlashCommandRegistry;
  9use async_compression::futures::bufread::GzipEncoder;
 10use collections::BTreeMap;
 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 isahc_http_client::IsahcHttpClient;
 17use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName};
 18use node_runtime::FakeNodeRuntime;
 19use parking_lot::Mutex;
 20use project::{Project, DEFAULT_COMPLETION_CONTEXT};
 21use release_channel::AppVersion;
 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 = FakeNodeRuntime::new();
268
269    let store = cx.new_model(|cx| {
270        ExtensionStore::new(
271            PathBuf::from("/the-extension-dir"),
272            None,
273            fs.clone(),
274            http_client.clone(),
275            http_client.clone(),
276            None,
277            node_runtime.clone(),
278            language_registry.clone(),
279            theme_registry.clone(),
280            slash_command_registry.clone(),
281            indexed_docs_registry.clone(),
282            snippet_registry.clone(),
283            cx,
284        )
285    });
286
287    cx.executor().advance_clock(super::RELOAD_DEBOUNCE_DURATION);
288    store.read_with(cx, |store, _| {
289        let index = &store.extension_index;
290        assert_eq!(index.extensions, expected_index.extensions);
291        assert_eq!(index.languages, expected_index.languages);
292        assert_eq!(index.themes, expected_index.themes);
293
294        assert_eq!(
295            language_registry.language_names(),
296            ["ERB", "Plain Text", "Ruby"]
297        );
298        assert_eq!(
299            theme_registry.list_names(false),
300            [
301                "Monokai Dark",
302                "Monokai Light",
303                "Monokai Pro Dark",
304                "Monokai Pro Light",
305                "One Dark",
306            ]
307        );
308    });
309
310    fs.insert_tree(
311        "/the-extension-dir/installed/zed-gruvbox",
312        json!({
313            "extension.json": r#"{
314                "id": "zed-gruvbox",
315                "name": "Zed Gruvbox",
316                "version": "1.0.0",
317                "themes": {
318                    "Gruvbox": "themes/gruvbox.json"
319                }
320            }"#,
321            "themes": {
322                "gruvbox.json": r#"{
323                    "name": "Gruvbox",
324                    "author": "Someone Else",
325                    "themes": [
326                        {
327                            "name": "Gruvbox",
328                            "appearance": "dark",
329                            "style": {}
330                        }
331                    ]
332                }"#,
333            }
334        }),
335    )
336    .await;
337
338    expected_index.extensions.insert(
339        "zed-gruvbox".into(),
340        ExtensionIndexEntry {
341            manifest: Arc::new(ExtensionManifest {
342                id: "zed-gruvbox".into(),
343                name: "Zed Gruvbox".into(),
344                version: "1.0.0".into(),
345                schema_version: SchemaVersion::ZERO,
346                description: None,
347                authors: vec![],
348                repository: None,
349                themes: vec!["themes/gruvbox.json".into()],
350                lib: Default::default(),
351                languages: Default::default(),
352                grammars: BTreeMap::default(),
353                language_servers: BTreeMap::default(),
354                slash_commands: BTreeMap::default(),
355                indexed_docs_providers: BTreeMap::default(),
356                snippets: None,
357            }),
358            dev: false,
359        },
360    );
361    expected_index.themes.insert(
362        "Gruvbox".into(),
363        ExtensionIndexThemeEntry {
364            extension: "zed-gruvbox".into(),
365            path: "themes/gruvbox.json".into(),
366        },
367    );
368
369    #[allow(clippy::let_underscore_future)]
370    let _ = store.update(cx, |store, cx| store.reload(None, cx));
371
372    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
373    store.read_with(cx, |store, _| {
374        let index = &store.extension_index;
375        assert_eq!(index.extensions, expected_index.extensions);
376        assert_eq!(index.languages, expected_index.languages);
377        assert_eq!(index.themes, expected_index.themes);
378
379        assert_eq!(
380            theme_registry.list_names(false),
381            [
382                "Gruvbox",
383                "Monokai Dark",
384                "Monokai Light",
385                "Monokai Pro Dark",
386                "Monokai Pro Light",
387                "One Dark",
388            ]
389        );
390    });
391
392    let prev_fs_metadata_call_count = fs.metadata_call_count();
393    let prev_fs_read_dir_call_count = fs.read_dir_call_count();
394
395    // Create new extension store, as if Zed were restarting.
396    drop(store);
397    let store = cx.new_model(|cx| {
398        ExtensionStore::new(
399            PathBuf::from("/the-extension-dir"),
400            None,
401            fs.clone(),
402            http_client.clone(),
403            http_client.clone(),
404            None,
405            node_runtime.clone(),
406            language_registry.clone(),
407            theme_registry.clone(),
408            slash_command_registry,
409            indexed_docs_registry,
410            snippet_registry,
411            cx,
412        )
413    });
414
415    cx.executor().run_until_parked();
416    store.read_with(cx, |store, _| {
417        assert_eq!(store.extension_index, expected_index);
418        assert_eq!(
419            language_registry.language_names(),
420            ["ERB", "Plain Text", "Ruby"]
421        );
422        assert_eq!(
423            language_registry.grammar_names(),
424            ["embedded_template".into(), "ruby".into()]
425        );
426        assert_eq!(
427            theme_registry.list_names(false),
428            [
429                "Gruvbox",
430                "Monokai Dark",
431                "Monokai Light",
432                "Monokai Pro Dark",
433                "Monokai Pro Light",
434                "One Dark",
435            ]
436        );
437
438        // The on-disk manifest limits the number of FS calls that need to be made
439        // on startup.
440        assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
441        assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
442    });
443
444    store.update(cx, |store, cx| {
445        store.uninstall_extension("zed-ruby".into(), cx)
446    });
447
448    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
449    expected_index.extensions.remove("zed-ruby");
450    expected_index.languages.remove("Ruby");
451    expected_index.languages.remove("ERB");
452
453    store.read_with(cx, |store, _| {
454        assert_eq!(store.extension_index, expected_index);
455        assert_eq!(language_registry.language_names(), ["Plain Text"]);
456        assert_eq!(language_registry.grammar_names(), []);
457    });
458}
459
460// TODO remove
461#[ignore]
462#[gpui::test]
463async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
464    init_test(cx);
465    cx.executor().allow_parking();
466
467    let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
468        .parent()
469        .unwrap()
470        .parent()
471        .unwrap();
472    let cache_dir = root_dir.join("target");
473    let test_extension_id = "test-extension";
474    let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
475
476    let fs = Arc::new(RealFs::default());
477    let extensions_dir = temp_tree(json!({
478        "installed": {},
479        "work": {}
480    }));
481    let project_dir = temp_tree(json!({
482        "test.gleam": ""
483    }));
484
485    let extensions_dir = extensions_dir.path().canonicalize().unwrap();
486    let project_dir = project_dir.path().canonicalize().unwrap();
487
488    let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
489
490    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
491    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
492    let slash_command_registry = SlashCommandRegistry::new();
493    let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
494    let snippet_registry = Arc::new(SnippetRegistry::new());
495    let node_runtime = FakeNodeRuntime::new();
496
497    let mut status_updates = language_registry.language_server_binary_statuses();
498
499    struct FakeLanguageServerVersion {
500        version: String,
501        binary_contents: String,
502        http_request_count: usize,
503    }
504
505    let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
506        version: "v1.2.3".into(),
507        binary_contents: "the-binary-contents".into(),
508        http_request_count: 0,
509    }));
510
511    let extension_client = FakeHttpClient::create({
512        let language_server_version = language_server_version.clone();
513        move |request| {
514            let language_server_version = language_server_version.clone();
515            async move {
516                let version = language_server_version.lock().version.clone();
517                let binary_contents = language_server_version.lock().binary_contents.clone();
518
519                let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
520                let asset_download_uri =
521                    format!("https://fake-download.example.com/gleam-{version}");
522
523                let uri = request.uri().to_string();
524                if uri == github_releases_uri {
525                    language_server_version.lock().http_request_count += 1;
526                    Ok(Response::new(
527                        json!([
528                            {
529                                "tag_name": version,
530                                "prerelease": false,
531                                "tarball_url": "",
532                                "zipball_url": "",
533                                "assets": [
534                                    {
535                                        "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
536                                        "browser_download_url": asset_download_uri
537                                    },
538                                    {
539                                        "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
540                                        "browser_download_url": asset_download_uri
541                                    },
542                                    {
543                                        "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
544                                        "browser_download_url": asset_download_uri
545                                    }
546                                ]
547                            }
548                        ])
549                        .to_string()
550                        .into(),
551                    ))
552                } else if uri == asset_download_uri {
553                    language_server_version.lock().http_request_count += 1;
554                    let mut bytes = Vec::<u8>::new();
555                    let mut archive = async_tar::Builder::new(&mut bytes);
556                    let mut header = async_tar::Header::new_gnu();
557                    header.set_size(binary_contents.len() as u64);
558                    archive
559                        .append_data(&mut header, "gleam", binary_contents.as_bytes())
560                        .await
561                        .unwrap();
562                    archive.into_inner().await.unwrap();
563                    let mut gzipped_bytes = Vec::new();
564                    let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
565                    encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
566                    Ok(Response::new(gzipped_bytes.into()))
567                // } else if uri == WASI_ADAPTER_URL {
568                //     let binary_contents =
569                //         include_bytes!("wasi_snapshot_preview1.reactor.wasm").as_slice();
570                //     Ok(Response::new(binary_contents.into()))
571                } else {
572                    Ok(Response::builder().status(404).body("not found".into())?)
573                }
574            }
575        }
576    });
577    let user_agent = cx.update(|cx| {
578        format!(
579            "Zed/{} ({}; {})",
580            AppVersion::global(cx),
581            std::env::consts::OS,
582            std::env::consts::ARCH
583        )
584    });
585    let builder_client = IsahcHttpClient::new(None, Some(user_agent));
586
587    let extension_store = cx.new_model(|cx| {
588        ExtensionStore::new(
589            extensions_dir.clone(),
590            Some(cache_dir),
591            fs.clone(),
592            extension_client.clone(),
593            builder_client,
594            None,
595            node_runtime,
596            language_registry.clone(),
597            theme_registry.clone(),
598            slash_command_registry,
599            indexed_docs_registry,
600            snippet_registry,
601            cx,
602        )
603    });
604
605    // Ensure that debounces fire.
606    let mut events = cx.events(&extension_store);
607    let executor = cx.executor();
608    let _task = cx.executor().spawn(async move {
609        while let Some(event) = events.next().await {
610            if let crate::Event::StartedReloading = event {
611                executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
612            }
613        }
614    });
615
616    extension_store.update(cx, |_, cx| {
617        cx.subscribe(&extension_store, |_, _, event, _| {
618            if matches!(event, Event::ExtensionFailedToLoad(_)) {
619                panic!("extension failed to load");
620            }
621        })
622        .detach();
623    });
624
625    extension_store
626        .update(cx, |store, cx| {
627            store.install_dev_extension(test_extension_dir.clone(), cx)
628        })
629        .await
630        .unwrap();
631
632    let mut fake_servers = language_registry.register_fake_language_server(
633        LanguageServerName("gleam".into()),
634        lsp::ServerCapabilities {
635            completion_provider: Some(Default::default()),
636            ..Default::default()
637        },
638        None,
639    );
640
641    let buffer = project
642        .update(cx, |project, cx| {
643            project.open_local_buffer(project_dir.join("test.gleam"), cx)
644        })
645        .await
646        .unwrap();
647
648    let fake_server = fake_servers.next().await.unwrap();
649    let expected_server_path =
650        extensions_dir.join(format!("work/{test_extension_id}/gleam-v1.2.3/gleam"));
651    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
652
653    assert_eq!(fake_server.binary.path, expected_server_path);
654    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
655    assert_eq!(
656        fs.load(&expected_server_path).await.unwrap(),
657        expected_binary_contents
658    );
659    assert_eq!(language_server_version.lock().http_request_count, 2);
660    assert_eq!(
661        [
662            status_updates.next().await.unwrap(),
663            status_updates.next().await.unwrap(),
664            status_updates.next().await.unwrap(),
665        ],
666        [
667            (
668                LanguageServerName("gleam".into()),
669                LanguageServerBinaryStatus::CheckingForUpdate
670            ),
671            (
672                LanguageServerName("gleam".into()),
673                LanguageServerBinaryStatus::Downloading
674            ),
675            (
676                LanguageServerName("gleam".into()),
677                LanguageServerBinaryStatus::None
678            )
679        ]
680    );
681
682    // The extension creates custom labels for completion items.
683    fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
684        Ok(Some(lsp::CompletionResponse::Array(vec![
685            lsp::CompletionItem {
686                label: "foo".into(),
687                kind: Some(lsp::CompletionItemKind::FUNCTION),
688                detail: Some("fn() -> Result(Nil, Error)".into()),
689                ..Default::default()
690            },
691            lsp::CompletionItem {
692                label: "bar.baz".into(),
693                kind: Some(lsp::CompletionItemKind::FUNCTION),
694                detail: Some("fn(List(a)) -> a".into()),
695                ..Default::default()
696            },
697            lsp::CompletionItem {
698                label: "Quux".into(),
699                kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
700                detail: Some("fn(String) -> T".into()),
701                ..Default::default()
702            },
703            lsp::CompletionItem {
704                label: "my_string".into(),
705                kind: Some(lsp::CompletionItemKind::CONSTANT),
706                detail: Some("String".into()),
707                ..Default::default()
708            },
709        ])))
710    });
711
712    let completion_labels = project
713        .update(cx, |project, cx| {
714            project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
715        })
716        .await
717        .unwrap()
718        .into_iter()
719        .map(|c| c.label.text)
720        .collect::<Vec<_>>();
721    assert_eq!(
722        completion_labels,
723        [
724            "foo: fn() -> Result(Nil, Error)".to_string(),
725            "bar.baz: fn(List(a)) -> a".to_string(),
726            "Quux: fn(String) -> T".to_string(),
727            "my_string: String".to_string(),
728        ]
729    );
730
731    // Simulate a new version of the language server being released
732    language_server_version.lock().version = "v2.0.0".into();
733    language_server_version.lock().binary_contents = "the-new-binary-contents".into();
734    language_server_version.lock().http_request_count = 0;
735
736    // Start a new instance of the language server.
737    project.update(cx, |project, cx| {
738        project.restart_language_servers_for_buffers([buffer.clone()], cx)
739    });
740
741    // The extension has cached the binary path, and does not attempt
742    // to reinstall it.
743    let fake_server = fake_servers.next().await.unwrap();
744    assert_eq!(fake_server.binary.path, expected_server_path);
745    assert_eq!(
746        fs.load(&expected_server_path).await.unwrap(),
747        expected_binary_contents
748    );
749    assert_eq!(language_server_version.lock().http_request_count, 0);
750
751    // Reload the extension, clearing its cache.
752    // Start a new instance of the language server.
753    extension_store
754        .update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
755        .await;
756
757    cx.executor().run_until_parked();
758    project.update(cx, |project, cx| {
759        project.restart_language_servers_for_buffers([buffer.clone()], cx)
760    });
761
762    // The extension re-fetches the latest version of the language server.
763    let fake_server = fake_servers.next().await.unwrap();
764    let new_expected_server_path =
765        extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
766    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
767    assert_eq!(fake_server.binary.path, new_expected_server_path);
768    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
769    assert_eq!(
770        fs.load(&new_expected_server_path).await.unwrap(),
771        expected_binary_contents
772    );
773
774    // The old language server directory has been cleaned up.
775    assert!(fs.metadata(&expected_server_path).await.unwrap().is_none());
776}
777
778fn init_test(cx: &mut TestAppContext) {
779    cx.update(|cx| {
780        let store = SettingsStore::test(cx);
781        cx.set_global(store);
782        release_channel::init(SemanticVersion::default(), cx);
783        theme::init(theme::LoadThemes::JustBase, cx);
784        Project::init_settings(cx);
785        ExtensionSettings::register(cx);
786        language::init(cx);
787    });
788}