extension_store_test.rs

  1use crate::{
  2    Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
  3    ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
  4    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 _, 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::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                        language_model_providers: BTreeMap::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                        language_model_providers: BTreeMap::default(),
201                    }),
202                    dev: false,
203                },
204            ),
205        ]
206        .into_iter()
207        .collect(),
208        languages: [
209            (
210                "ERB".into(),
211                ExtensionIndexLanguageEntry {
212                    extension: "zed-ruby".into(),
213                    path: "languages/erb".into(),
214                    grammar: Some("embedded_template".into()),
215                    hidden: false,
216                    matcher: LanguageMatcher {
217                        path_suffixes: vec!["erb".into()],
218                        first_line_pattern: None,
219                    },
220                },
221            ),
222            (
223                "Ruby".into(),
224                ExtensionIndexLanguageEntry {
225                    extension: "zed-ruby".into(),
226                    path: "languages/ruby".into(),
227                    grammar: Some("ruby".into()),
228                    hidden: false,
229                    matcher: LanguageMatcher {
230                        path_suffixes: vec!["rb".into()],
231                        first_line_pattern: None,
232                    },
233                },
234            ),
235        ]
236        .into_iter()
237        .collect(),
238        themes: [
239            (
240                "Monokai Dark".into(),
241                ExtensionIndexThemeEntry {
242                    extension: "zed-monokai".into(),
243                    path: "themes/monokai.json".into(),
244                },
245            ),
246            (
247                "Monokai Light".into(),
248                ExtensionIndexThemeEntry {
249                    extension: "zed-monokai".into(),
250                    path: "themes/monokai.json".into(),
251                },
252            ),
253            (
254                "Monokai Pro Dark".into(),
255                ExtensionIndexThemeEntry {
256                    extension: "zed-monokai".into(),
257                    path: "themes/monokai-pro.json".into(),
258                },
259            ),
260            (
261                "Monokai Pro Light".into(),
262                ExtensionIndexThemeEntry {
263                    extension: "zed-monokai".into(),
264                    path: "themes/monokai-pro.json".into(),
265                },
266            ),
267        ]
268        .into_iter()
269        .collect(),
270        icon_themes: BTreeMap::default(),
271    };
272
273    let proxy = Arc::new(ExtensionHostProxy::new());
274    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
275    theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
276    let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
277    language_extension::init(LspAccess::Noop, proxy.clone(), language_registry.clone());
278    let node_runtime = NodeRuntime::unavailable();
279
280    let store = cx.new(|cx| {
281        ExtensionStore::new(
282            PathBuf::from("/the-extension-dir"),
283            None,
284            proxy.clone(),
285            fs.clone(),
286            http_client.clone(),
287            http_client.clone(),
288            None,
289            node_runtime.clone(),
290            cx,
291        )
292    });
293
294    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
295    store.read_with(cx, |store, _| {
296        let index = &store.extension_index;
297        assert_eq!(index.extensions, expected_index.extensions);
298
299        for ((actual_key, actual_language), (expected_key, expected_language)) in
300            index.languages.iter().zip(expected_index.languages.iter())
301        {
302            assert_eq!(actual_key, expected_key);
303            assert_eq!(actual_language.grammar, expected_language.grammar);
304            assert_eq!(actual_language.matcher, expected_language.matcher);
305            assert_eq!(actual_language.hidden, expected_language.hidden);
306        }
307        assert_eq!(index.themes, expected_index.themes);
308
309        assert_eq!(
310            language_registry.language_names(),
311            [
312                LanguageName::new_static("ERB"),
313                LanguageName::new_static("Plain Text"),
314                LanguageName::new_static("Ruby"),
315            ]
316        );
317        assert_eq!(
318            theme_registry.list_names(),
319            [
320                "Monokai Dark",
321                "Monokai Light",
322                "Monokai Pro Dark",
323                "Monokai Pro Light",
324                "One Dark",
325            ]
326        );
327    });
328
329    fs.insert_tree(
330        "/the-extension-dir/installed/zed-gruvbox",
331        json!({
332            "extension.json": r#"{
333                "id": "zed-gruvbox",
334                "name": "Zed Gruvbox",
335                "version": "1.0.0",
336                "themes": {
337                    "Gruvbox": "themes/gruvbox.json"
338                }
339            }"#,
340            "themes": {
341                "gruvbox.json": r#"{
342                    "name": "Gruvbox",
343                    "author": "Someone Else",
344                    "themes": [
345                        {
346                            "name": "Gruvbox",
347                            "appearance": "dark",
348                            "style": {}
349                        }
350                    ]
351                }"#,
352            }
353        }),
354    )
355    .await;
356
357    expected_index.extensions.insert(
358        "zed-gruvbox".into(),
359        ExtensionIndexEntry {
360            manifest: Arc::new(ExtensionManifest {
361                id: "zed-gruvbox".into(),
362                name: "Zed Gruvbox".into(),
363                version: "1.0.0".into(),
364                schema_version: SchemaVersion::ZERO,
365                description: None,
366                authors: vec![],
367                repository: None,
368                themes: vec!["themes/gruvbox.json".into()],
369                icon_themes: Vec::new(),
370                lib: Default::default(),
371                languages: Default::default(),
372                grammars: BTreeMap::default(),
373                language_servers: BTreeMap::default(),
374                context_servers: BTreeMap::default(),
375                agent_servers: BTreeMap::default(),
376                slash_commands: BTreeMap::default(),
377                snippets: None,
378                capabilities: Vec::new(),
379                debug_adapters: Default::default(),
380                debug_locators: Default::default(),
381                language_model_providers: BTreeMap::default(),
382            }),
383            dev: false,
384        },
385    );
386    expected_index.themes.insert(
387        "Gruvbox".into(),
388        ExtensionIndexThemeEntry {
389            extension: "zed-gruvbox".into(),
390            path: "themes/gruvbox.json".into(),
391        },
392    );
393
394    #[allow(clippy::let_underscore_future)]
395    let _ = store.update(cx, |store, cx| store.reload(None, cx));
396
397    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
398    store.read_with(cx, |store, _| {
399        let index = &store.extension_index;
400
401        for ((actual_key, actual_language), (expected_key, expected_language)) in
402            index.languages.iter().zip(expected_index.languages.iter())
403        {
404            assert_eq!(actual_key, expected_key);
405            assert_eq!(actual_language.grammar, expected_language.grammar);
406            assert_eq!(actual_language.matcher, expected_language.matcher);
407            assert_eq!(actual_language.hidden, expected_language.hidden);
408        }
409
410        assert_eq!(index.extensions, expected_index.extensions);
411        assert_eq!(index.themes, expected_index.themes);
412
413        assert_eq!(
414            theme_registry.list_names(),
415            [
416                "Gruvbox",
417                "Monokai Dark",
418                "Monokai Light",
419                "Monokai Pro Dark",
420                "Monokai Pro Light",
421                "One Dark",
422            ]
423        );
424    });
425
426    let prev_fs_metadata_call_count = fs.metadata_call_count();
427    let prev_fs_read_dir_call_count = fs.read_dir_call_count();
428
429    // Create new extension store, as if Zed were restarting.
430    drop(store);
431    let store = cx.new(|cx| {
432        ExtensionStore::new(
433            PathBuf::from("/the-extension-dir"),
434            None,
435            proxy,
436            fs.clone(),
437            http_client.clone(),
438            http_client.clone(),
439            None,
440            node_runtime.clone(),
441            cx,
442        )
443    });
444
445    cx.executor().run_until_parked();
446    store.read_with(cx, |store, _| {
447        assert_eq!(store.extension_index.extensions, expected_index.extensions);
448        assert_eq!(store.extension_index.themes, expected_index.themes);
449        assert_eq!(
450            store.extension_index.icon_themes,
451            expected_index.icon_themes
452        );
453
454        for ((actual_key, actual_language), (expected_key, expected_language)) in store
455            .extension_index
456            .languages
457            .iter()
458            .zip(expected_index.languages.iter())
459        {
460            assert_eq!(actual_key, expected_key);
461            assert_eq!(actual_language.grammar, expected_language.grammar);
462            assert_eq!(actual_language.matcher, expected_language.matcher);
463            assert_eq!(actual_language.hidden, expected_language.hidden);
464        }
465
466        assert_eq!(
467            language_registry.language_names(),
468            [
469                LanguageName::new_static("ERB"),
470                LanguageName::new_static("Plain Text"),
471                LanguageName::new_static("Ruby"),
472            ]
473        );
474        assert_eq!(
475            language_registry.grammar_names(),
476            ["embedded_template".into(), "ruby".into()]
477        );
478        assert_eq!(
479            theme_registry.list_names(),
480            [
481                "Gruvbox",
482                "Monokai Dark",
483                "Monokai Light",
484                "Monokai Pro Dark",
485                "Monokai Pro Light",
486                "One Dark",
487            ]
488        );
489
490        // The on-disk manifest limits the number of FS calls that need to be made
491        // on startup.
492        assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
493        assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
494    });
495
496    store.update(cx, |store, cx| {
497        store
498            .uninstall_extension("zed-ruby".into(), cx)
499            .detach_and_log_err(cx);
500    });
501
502    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
503    expected_index.extensions.remove("zed-ruby");
504    expected_index.languages.remove("Ruby");
505    expected_index.languages.remove("ERB");
506
507    store.read_with(cx, |store, _| {
508        assert_eq!(store.extension_index.extensions, expected_index.extensions);
509        assert_eq!(store.extension_index.themes, expected_index.themes);
510        assert_eq!(
511            store.extension_index.icon_themes,
512            expected_index.icon_themes
513        );
514
515        for ((actual_key, actual_language), (expected_key, expected_language)) in store
516            .extension_index
517            .languages
518            .iter()
519            .zip(expected_index.languages.iter())
520        {
521            assert_eq!(actual_key, expected_key);
522            assert_eq!(actual_language.grammar, expected_language.grammar);
523            assert_eq!(actual_language.matcher, expected_language.matcher);
524            assert_eq!(actual_language.hidden, expected_language.hidden);
525        }
526
527        assert_eq!(
528            language_registry.language_names(),
529            [LanguageName::new_static("Plain Text")]
530        );
531        assert_eq!(language_registry.grammar_names(), []);
532    });
533}
534
535#[gpui::test]
536async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
537    init_test(cx);
538    cx.executor().allow_parking();
539
540    async fn await_or_timeout<T>(
541        what: &'static str,
542        seconds: u64,
543        future: impl std::future::Future<Output = T>,
544    ) -> T {
545        use futures::FutureExt as _;
546        use gpui::Timer;
547
548        let timeout = Timer::after(std::time::Duration::from_secs(seconds));
549
550        futures::select! {
551            output = future.fuse() => output,
552            _ = futures::FutureExt::fuse(timeout) => panic!(
553            "[test_extension_store_with_test_extension] timed out after {seconds}s while {what}"
554        )
555        }
556    }
557
558    let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
559        .parent()
560        .unwrap()
561        .parent()
562        .unwrap();
563    let cache_dir = root_dir.join("target");
564    let test_extension_id = "test-extension";
565    let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
566
567    let fs = Arc::new(RealFs::new(None, cx.executor()));
568    let extensions_tree = TempTree::new(json!({
569        "installed": {},
570        "work": {}
571    }));
572    let project_dir = TempTree::new(json!({
573        "test.gleam": ""
574    }));
575
576    let extensions_dir = extensions_tree.path().canonicalize().unwrap();
577    let project_dir = project_dir.path().canonicalize().unwrap();
578
579    let project = await_or_timeout(
580        "awaiting Project::test",
581        5,
582        Project::test(fs.clone(), [project_dir.as_path()], cx),
583    )
584    .await;
585
586    let proxy = Arc::new(ExtensionHostProxy::new());
587    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
588    theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
589    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
590    language_extension::init(
591        LspAccess::ViaLspStore(project.update(cx, |project, _| project.lsp_store())),
592        proxy.clone(),
593        language_registry.clone(),
594    );
595    let node_runtime = NodeRuntime::unavailable();
596
597    let mut status_updates = language_registry.language_server_binary_statuses();
598
599    struct FakeLanguageServerVersion {
600        version: String,
601        binary_contents: String,
602        http_request_count: usize,
603    }
604
605    let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
606        version: "v1.2.3".into(),
607        binary_contents: "the-binary-contents".into(),
608        http_request_count: 0,
609    }));
610
611    let extension_client = FakeHttpClient::create({
612        let language_server_version = language_server_version.clone();
613        move |request| {
614            let language_server_version = language_server_version.clone();
615            async move {
616                let version = language_server_version.lock().version.clone();
617                let binary_contents = language_server_version.lock().binary_contents.clone();
618
619                let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
620                let asset_download_uri =
621                    format!("https://fake-download.example.com/gleam-{version}");
622
623                let uri = request.uri().to_string();
624                if uri == github_releases_uri {
625                    language_server_version.lock().http_request_count += 1;
626                    Ok(Response::new(
627                        json!([
628                            {
629                                "tag_name": version,
630                                "prerelease": false,
631                                "tarball_url": "",
632                                "zipball_url": "",
633                                "assets": [
634                                    {
635                                        "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
636                                        "browser_download_url": asset_download_uri
637                                    },
638                                    {
639                                        "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
640                                        "browser_download_url": asset_download_uri
641                                    },
642                                    {
643                                        "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
644                                        "browser_download_url": asset_download_uri
645                                    },
646                                    {
647                                        "name": format!("gleam-{version}-x86_64-pc-windows-msvc.tar.gz"),
648                                        "browser_download_url": asset_download_uri
649                                    }
650                                ]
651                            }
652                        ])
653                        .to_string()
654                        .into(),
655                    ))
656                } else if uri == asset_download_uri {
657                    language_server_version.lock().http_request_count += 1;
658                    let mut bytes = Vec::<u8>::new();
659                    let mut archive = async_tar::Builder::new(&mut bytes);
660                    let mut header = async_tar::Header::new_gnu();
661                    header.set_size(binary_contents.len() as u64);
662                    archive
663                        .append_data(&mut header, "gleam", binary_contents.as_bytes())
664                        .await
665                        .unwrap();
666                    archive.into_inner().await.unwrap();
667                    let mut gzipped_bytes = Vec::new();
668                    let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
669                    encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
670                    Ok(Response::new(gzipped_bytes.into()))
671                } else {
672                    Ok(Response::builder().status(404).body("not found".into())?)
673                }
674            }
675        }
676    });
677    let user_agent = cx.update(|cx| {
678        format!(
679            "Zed/{} ({}; {})",
680            AppVersion::global(cx),
681            std::env::consts::OS,
682            std::env::consts::ARCH
683        )
684    });
685    let builder_client =
686        Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
687
688    let extension_store = cx.new(|cx| {
689        ExtensionStore::new(
690            extensions_dir.clone(),
691            Some(cache_dir),
692            proxy,
693            fs.clone(),
694            extension_client.clone(),
695            builder_client,
696            None,
697            node_runtime,
698            cx,
699        )
700    });
701
702    // Ensure that debounces fire.
703    let mut events = cx.events(&extension_store);
704    let executor = cx.executor();
705    let _task = cx.executor().spawn(async move {
706        while let Some(event) = events.next().await {
707            if let Event::StartedReloading = event {
708                executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
709            }
710        }
711    });
712
713    extension_store.update(cx, |_, cx| {
714        cx.subscribe(&extension_store, |_, _, event, _| {
715            if matches!(event, Event::ExtensionFailedToLoad(_)) {
716                panic!("extension failed to load");
717            }
718        })
719        .detach();
720    });
721
722    await_or_timeout(
723        "awaiting install_dev_extension",
724        60,
725        extension_store.update(cx, |store, cx| {
726            store.install_dev_extension(test_extension_dir.clone(), cx)
727        }),
728    )
729    .await
730    .unwrap();
731
732    let mut fake_servers = language_registry.register_fake_lsp_server(
733        LanguageServerName("gleam".into()),
734        lsp::ServerCapabilities {
735            completion_provider: Some(Default::default()),
736            ..Default::default()
737        },
738        None,
739    );
740    cx.executor().run_until_parked();
741
742    let (buffer, _handle) = await_or_timeout(
743        "awaiting open_local_buffer_with_lsp",
744        5,
745        project.update(cx, |project, cx| {
746            project.open_local_buffer_with_lsp(project_dir.join("test.gleam"), cx)
747        }),
748    )
749    .await
750    .unwrap();
751    cx.executor().run_until_parked();
752
753    let fake_server = await_or_timeout("awaiting first fake server spawn", 10, fake_servers.next())
754        .await
755        .unwrap();
756
757    let work_dir = extensions_dir.join(format!("work/{test_extension_id}"));
758    let expected_server_path = work_dir.join("gleam-v1.2.3/gleam");
759    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
760
761    // check that IO operations in extension work correctly
762    assert!(work_dir.join("dir-created-with-rel-path").exists());
763    assert!(work_dir.join("dir-created-with-abs-path").exists());
764    assert!(work_dir.join("file-created-with-abs-path").exists());
765    assert!(work_dir.join("file-created-with-rel-path").exists());
766
767    assert_eq!(fake_server.binary.path, expected_server_path);
768    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
769    assert_eq!(
770        await_or_timeout(
771            "awaiting fs.load(expected_server_path)",
772            5,
773            fs.load(&expected_server_path)
774        )
775        .await
776        .unwrap(),
777        expected_binary_contents
778    );
779    assert_eq!(language_server_version.lock().http_request_count, 2);
780    assert_eq!(
781        [
782            await_or_timeout("awaiting status_updates #1", 5, status_updates.next())
783                .await
784                .unwrap(),
785            await_or_timeout("awaiting status_updates #2", 5, status_updates.next())
786                .await
787                .unwrap(),
788            await_or_timeout("awaiting status_updates #3", 5, status_updates.next())
789                .await
790                .unwrap(),
791            await_or_timeout("awaiting status_updates #4", 5, status_updates.next())
792                .await
793                .unwrap(),
794        ],
795        [
796            (
797                LanguageServerName::new_static("gleam"),
798                BinaryStatus::Starting
799            ),
800            (
801                LanguageServerName::new_static("gleam"),
802                BinaryStatus::CheckingForUpdate
803            ),
804            (
805                LanguageServerName::new_static("gleam"),
806                BinaryStatus::Downloading
807            ),
808            (LanguageServerName::new_static("gleam"), BinaryStatus::None)
809        ]
810    );
811
812    // The extension creates custom labels for completion items.
813    fake_server.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
814        Ok(Some(lsp::CompletionResponse::Array(vec![
815            lsp::CompletionItem {
816                label: "foo".into(),
817                kind: Some(lsp::CompletionItemKind::FUNCTION),
818                detail: Some("fn() -> Result(Nil, Error)".into()),
819                ..Default::default()
820            },
821            lsp::CompletionItem {
822                label: "bar.baz".into(),
823                kind: Some(lsp::CompletionItemKind::FUNCTION),
824                detail: Some("fn(List(a)) -> a".into()),
825                ..Default::default()
826            },
827            lsp::CompletionItem {
828                label: "Quux".into(),
829                kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
830                detail: Some("fn(String) -> T".into()),
831                ..Default::default()
832            },
833            lsp::CompletionItem {
834                label: "my_string".into(),
835                kind: Some(lsp::CompletionItemKind::CONSTANT),
836                detail: Some("String".into()),
837                ..Default::default()
838            },
839        ])))
840    });
841
842    // `register_fake_lsp_server` can yield a server instance before the client has finished the LSP
843    // initialization handshake. Wait until we observe the client's `initialized` notification before
844    // issuing requests like completion.
845    await_or_timeout("awaiting LSP Initialized notification", 5, async {
846        fake_server
847            .clone()
848            .try_receive_notification::<lsp::notification::Initialized>()
849            .await;
850    })
851    .await;
852
853    let completion_labels = await_or_timeout(
854        "awaiting completions",
855        5,
856        project.update(cx, |project, cx| {
857            project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
858        }),
859    )
860    .await
861    .unwrap()
862    .into_iter()
863    .flat_map(|response| response.completions)
864    .map(|c| c.label.text)
865    .collect::<Vec<_>>();
866    assert_eq!(
867        completion_labels,
868        [
869            "foo: fn() -> Result(Nil, Error)".to_string(),
870            "bar.baz: fn(List(a)) -> a".to_string(),
871            "Quux: fn(String) -> T".to_string(),
872            "my_string: String".to_string(),
873        ]
874    );
875
876    // Simulate a new version of the language server being released
877    language_server_version.lock().version = "v2.0.0".into();
878    language_server_version.lock().binary_contents = "the-new-binary-contents".into();
879    language_server_version.lock().http_request_count = 0;
880
881    // Start a new instance of the language server.
882    project.update(cx, |project, cx| {
883        project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
884    });
885    cx.executor().run_until_parked();
886
887    // The extension has cached the binary path, and does not attempt
888    // to reinstall it.
889    let fake_server = await_or_timeout("awaiting second fake server spawn", 5, fake_servers.next())
890        .await
891        .unwrap();
892    assert_eq!(fake_server.binary.path, expected_server_path);
893    assert_eq!(
894        await_or_timeout(
895            "awaiting fs.load(expected_server_path) after restart",
896            5,
897            fs.load(&expected_server_path)
898        )
899        .await
900        .unwrap(),
901        expected_binary_contents
902    );
903    assert_eq!(language_server_version.lock().http_request_count, 0);
904
905    // Reload the extension, clearing its cache.
906    // Start a new instance of the language server.
907    await_or_timeout(
908        "awaiting extension_store.reload(test-extension)",
909        5,
910        extension_store.update(cx, |store, cx| {
911            store.reload(Some("test-extension".into()), cx)
912        }),
913    )
914    .await;
915    cx.executor().run_until_parked();
916    project.update(cx, |project, cx| {
917        project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx)
918    });
919
920    // The extension re-fetches the latest version of the language server.
921    let fake_server = await_or_timeout("awaiting third fake server spawn", 5, fake_servers.next())
922        .await
923        .unwrap();
924    let new_expected_server_path =
925        extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
926    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
927    assert_eq!(fake_server.binary.path, new_expected_server_path);
928    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
929    assert_eq!(
930        await_or_timeout(
931            "awaiting fs.load(new_expected_server_path)",
932            5,
933            fs.load(&new_expected_server_path)
934        )
935        .await
936        .unwrap(),
937        expected_binary_contents
938    );
939
940    // The old language server directory has been cleaned up.
941    assert!(
942        await_or_timeout(
943            "awaiting fs.metadata(expected_server_path)",
944            5,
945            fs.metadata(&expected_server_path)
946        )
947        .await
948        .unwrap()
949        .is_none()
950    );
951}
952
953fn init_test(cx: &mut TestAppContext) {
954    cx.update(|cx| {
955        let store = SettingsStore::test(cx);
956        cx.set_global(store);
957        release_channel::init(semver::Version::new(0, 0, 0), cx);
958        extension::init(cx);
959        theme::init(theme::LoadThemes::JustBase, cx);
960        gpui_tokio::init(cx);
961    });
962}