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