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