extension_store_test.rs

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