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