extension_store_test.rs

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