extension_store_test.rs

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