extension_store_test.rs

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