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