extension_store_test.rs

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