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