extension_store_test.rs

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