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