extension_store_test.rs

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