extension_store_test.rs

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