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                        capabilities: Vec::new(),
154                        lib: Default::default(),
155                        languages: vec!["languages/erb".into(), "languages/ruby".into()],
156                        grammars: [
157                            ("embedded_template".into(), GrammarManifestEntry::default()),
158                            ("ruby".into(), GrammarManifestEntry::default()),
159                        ]
160                        .into_iter()
161                        .collect(),
162                        language_servers: BTreeMap::default(),
163                        slash_commands: BTreeMap::default(),
164                        indexed_docs_providers: BTreeMap::default(),
165                        snippets: None,
166                    }),
167                    dev: false,
168                },
169            ),
170            (
171                "zed-monokai".into(),
172                ExtensionIndexEntry {
173                    manifest: Arc::new(ExtensionManifest {
174                        id: "zed-monokai".into(),
175                        name: "Zed Monokai".into(),
176                        version: "2.0.0".into(),
177                        schema_version: SchemaVersion::ZERO,
178                        description: None,
179                        authors: vec![],
180                        repository: None,
181                        themes: vec![
182                            "themes/monokai-pro.json".into(),
183                            "themes/monokai.json".into(),
184                        ],
185                        capabilities: Vec::new(),
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 = FakeNodeRuntime::new();
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            None,
276            node_runtime.clone(),
277            language_registry.clone(),
278            theme_registry.clone(),
279            slash_command_registry.clone(),
280            indexed_docs_registry.clone(),
281            snippet_registry.clone(),
282            cx,
283        )
284    });
285
286    cx.executor().advance_clock(super::RELOAD_DEBOUNCE_DURATION);
287    store.read_with(cx, |store, _| {
288        let index = &store.extension_index;
289        assert_eq!(index.extensions, expected_index.extensions);
290        assert_eq!(index.languages, expected_index.languages);
291        assert_eq!(index.themes, expected_index.themes);
292
293        assert_eq!(
294            language_registry.language_names(),
295            ["ERB", "Plain Text", "Ruby"]
296        );
297        assert_eq!(
298            theme_registry.list_names(false),
299            [
300                "Monokai Dark",
301                "Monokai Light",
302                "Monokai Pro Dark",
303                "Monokai Pro Light",
304                "One Dark",
305            ]
306        );
307    });
308
309    fs.insert_tree(
310        "/the-extension-dir/installed/zed-gruvbox",
311        json!({
312            "extension.json": r#"{
313                "id": "zed-gruvbox",
314                "name": "Zed Gruvbox",
315                "version": "1.0.0",
316                "themes": {
317                    "Gruvbox": "themes/gruvbox.json"
318                }
319            }"#,
320            "themes": {
321                "gruvbox.json": r#"{
322                    "name": "Gruvbox",
323                    "author": "Someone Else",
324                    "themes": [
325                        {
326                            "name": "Gruvbox",
327                            "appearance": "dark",
328                            "style": {}
329                        }
330                    ]
331                }"#,
332            }
333        }),
334    )
335    .await;
336
337    expected_index.extensions.insert(
338        "zed-gruvbox".into(),
339        ExtensionIndexEntry {
340            manifest: Arc::new(ExtensionManifest {
341                id: "zed-gruvbox".into(),
342                name: "Zed Gruvbox".into(),
343                version: "1.0.0".into(),
344                schema_version: SchemaVersion::ZERO,
345                description: None,
346                authors: vec![],
347                repository: None,
348                themes: vec!["themes/gruvbox.json".into()],
349                capabilities: Vec::new(),
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(false),
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            None,
404            node_runtime.clone(),
405            language_registry.clone(),
406            theme_registry.clone(),
407            slash_command_registry,
408            indexed_docs_registry,
409            snippet_registry,
410            cx,
411        )
412    });
413
414    cx.executor().run_until_parked();
415    store.read_with(cx, |store, _| {
416        assert_eq!(store.extension_index, expected_index);
417        assert_eq!(
418            language_registry.language_names(),
419            ["ERB", "Plain Text", "Ruby"]
420        );
421        assert_eq!(
422            language_registry.grammar_names(),
423            ["embedded_template".into(), "ruby".into()]
424        );
425        assert_eq!(
426            theme_registry.list_names(false),
427            [
428                "Gruvbox",
429                "Monokai Dark",
430                "Monokai Light",
431                "Monokai Pro Dark",
432                "Monokai Pro Light",
433                "One Dark",
434            ]
435        );
436
437        // The on-disk manifest limits the number of FS calls that need to be made
438        // on startup.
439        assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
440        assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
441    });
442
443    store.update(cx, |store, cx| {
444        store.uninstall_extension("zed-ruby".into(), cx)
445    });
446
447    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
448    expected_index.extensions.remove("zed-ruby");
449    expected_index.languages.remove("Ruby");
450    expected_index.languages.remove("ERB");
451
452    store.read_with(cx, |store, _| {
453        assert_eq!(store.extension_index, expected_index);
454        assert_eq!(language_registry.language_names(), ["Plain Text"]);
455        assert_eq!(language_registry.grammar_names(), []);
456    });
457}
458
459#[gpui::test]
460async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
461    init_test(cx);
462    cx.executor().allow_parking();
463
464    let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
465        .parent()
466        .unwrap()
467        .parent()
468        .unwrap();
469    let cache_dir = root_dir.join("target");
470    let test_extension_id = "test-extension";
471    let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
472
473    let fs = Arc::new(RealFs::default());
474    let extensions_dir = temp_tree(json!({
475        "installed": {},
476        "work": {}
477    }));
478    let project_dir = temp_tree(json!({
479        "test.gleam": ""
480    }));
481
482    let extensions_dir = extensions_dir.path().canonicalize().unwrap();
483    let project_dir = project_dir.path().canonicalize().unwrap();
484
485    let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
486
487    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
488    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
489    let slash_command_registry = SlashCommandRegistry::new();
490    let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
491    let snippet_registry = Arc::new(SnippetRegistry::new());
492    let node_runtime = FakeNodeRuntime::new();
493
494    let mut status_updates = language_registry.language_server_binary_statuses();
495
496    struct FakeLanguageServerVersion {
497        version: String,
498        binary_contents: String,
499        http_request_count: usize,
500    }
501
502    let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
503        version: "v1.2.3".into(),
504        binary_contents: "the-binary-contents".into(),
505        http_request_count: 0,
506    }));
507
508    let http_client = FakeHttpClient::create({
509        let language_server_version = language_server_version.clone();
510        move |request| {
511            let language_server_version = language_server_version.clone();
512            async move {
513                let version = language_server_version.lock().version.clone();
514                let binary_contents = language_server_version.lock().binary_contents.clone();
515
516                let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
517                let asset_download_uri =
518                    format!("https://fake-download.example.com/gleam-{version}");
519
520                let uri = request.uri().to_string();
521                if uri == github_releases_uri {
522                    language_server_version.lock().http_request_count += 1;
523                    Ok(Response::new(
524                        json!([
525                            {
526                                "tag_name": version,
527                                "prerelease": false,
528                                "tarball_url": "",
529                                "zipball_url": "",
530                                "assets": [
531                                    {
532                                        "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
533                                        "browser_download_url": asset_download_uri
534                                    },
535                                    {
536                                        "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
537                                        "browser_download_url": asset_download_uri
538                                    },
539                                    {
540                                        "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
541                                        "browser_download_url": asset_download_uri
542                                    }
543                                ]
544                            }
545                        ])
546                        .to_string()
547                        .into(),
548                    ))
549                } else if uri == asset_download_uri {
550                    language_server_version.lock().http_request_count += 1;
551                    let mut bytes = Vec::<u8>::new();
552                    let mut archive = async_tar::Builder::new(&mut bytes);
553                    let mut header = async_tar::Header::new_gnu();
554                    header.set_size(binary_contents.len() as u64);
555                    archive
556                        .append_data(&mut header, "gleam", binary_contents.as_bytes())
557                        .await
558                        .unwrap();
559                    archive.into_inner().await.unwrap();
560                    let mut gzipped_bytes = Vec::new();
561                    let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
562                    encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
563                    Ok(Response::new(gzipped_bytes.into()))
564                } else {
565                    Ok(Response::builder().status(404).body("not found".into())?)
566                }
567            }
568        }
569    });
570
571    let extension_store = cx.new_model(|cx| {
572        ExtensionStore::new(
573            extensions_dir.clone(),
574            Some(cache_dir),
575            fs.clone(),
576            http_client.clone(),
577            None,
578            node_runtime,
579            language_registry.clone(),
580            theme_registry.clone(),
581            slash_command_registry,
582            indexed_docs_registry,
583            snippet_registry,
584            cx,
585        )
586    });
587
588    // Ensure that debounces fire.
589    let mut events = cx.events(&extension_store);
590    let executor = cx.executor();
591    let _task = cx.executor().spawn(async move {
592        while let Some(event) = events.next().await {
593            match event {
594                crate::Event::StartedReloading => {
595                    executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
596                }
597                _ => (),
598            }
599        }
600    });
601
602    extension_store.update(cx, |_, cx| {
603        cx.subscribe(&extension_store, |_, _, event, _| {
604            if matches!(event, Event::ExtensionFailedToLoad(_)) {
605                panic!("extension failed to load");
606            }
607        })
608        .detach();
609    });
610
611    extension_store
612        .update(cx, |store, cx| {
613            store.install_dev_extension(test_extension_dir.clone(), cx)
614        })
615        .await
616        .unwrap();
617
618    let mut fake_servers = language_registry.fake_language_servers("Gleam");
619
620    let buffer = project
621        .update(cx, |project, cx| {
622            project.open_local_buffer(project_dir.join("test.gleam"), cx)
623        })
624        .await
625        .unwrap();
626
627    let fake_server = fake_servers.next().await.unwrap();
628    let expected_server_path =
629        extensions_dir.join(format!("work/{test_extension_id}/gleam-v1.2.3/gleam"));
630    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
631
632    assert_eq!(fake_server.binary.path, expected_server_path);
633    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
634    assert_eq!(
635        fs.load(&expected_server_path).await.unwrap(),
636        expected_binary_contents
637    );
638    assert_eq!(language_server_version.lock().http_request_count, 2);
639    assert_eq!(
640        [
641            status_updates.next().await.unwrap(),
642            status_updates.next().await.unwrap(),
643            status_updates.next().await.unwrap(),
644        ],
645        [
646            (
647                LanguageServerName("gleam".into()),
648                LanguageServerBinaryStatus::CheckingForUpdate
649            ),
650            (
651                LanguageServerName("gleam".into()),
652                LanguageServerBinaryStatus::Downloading
653            ),
654            (
655                LanguageServerName("gleam".into()),
656                LanguageServerBinaryStatus::None
657            )
658        ]
659    );
660
661    // The extension creates custom labels for completion items.
662    fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
663        Ok(Some(lsp::CompletionResponse::Array(vec![
664            lsp::CompletionItem {
665                label: "foo".into(),
666                kind: Some(lsp::CompletionItemKind::FUNCTION),
667                detail: Some("fn() -> Result(Nil, Error)".into()),
668                ..Default::default()
669            },
670            lsp::CompletionItem {
671                label: "bar.baz".into(),
672                kind: Some(lsp::CompletionItemKind::FUNCTION),
673                detail: Some("fn(List(a)) -> a".into()),
674                ..Default::default()
675            },
676            lsp::CompletionItem {
677                label: "Quux".into(),
678                kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
679                detail: Some("fn(String) -> T".into()),
680                ..Default::default()
681            },
682            lsp::CompletionItem {
683                label: "my_string".into(),
684                kind: Some(lsp::CompletionItemKind::CONSTANT),
685                detail: Some("String".into()),
686                ..Default::default()
687            },
688        ])))
689    });
690
691    let completion_labels = project
692        .update(cx, |project, cx| {
693            project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
694        })
695        .await
696        .unwrap()
697        .into_iter()
698        .map(|c| c.label.text)
699        .collect::<Vec<_>>();
700    assert_eq!(
701        completion_labels,
702        [
703            "foo: fn() -> Result(Nil, Error)".to_string(),
704            "bar.baz: fn(List(a)) -> a".to_string(),
705            "Quux: fn(String) -> T".to_string(),
706            "my_string: String".to_string(),
707        ]
708    );
709
710    // Simulate a new version of the language server being released
711    language_server_version.lock().version = "v2.0.0".into();
712    language_server_version.lock().binary_contents = "the-new-binary-contents".into();
713    language_server_version.lock().http_request_count = 0;
714
715    // Start a new instance of the language server.
716    project.update(cx, |project, cx| {
717        project.restart_language_servers_for_buffers([buffer.clone()], cx)
718    });
719
720    // The extension has cached the binary path, and does not attempt
721    // to reinstall it.
722    let fake_server = fake_servers.next().await.unwrap();
723    assert_eq!(fake_server.binary.path, expected_server_path);
724    assert_eq!(
725        fs.load(&expected_server_path).await.unwrap(),
726        expected_binary_contents
727    );
728    assert_eq!(language_server_version.lock().http_request_count, 0);
729
730    // Reload the extension, clearing its cache.
731    // Start a new instance of the language server.
732    extension_store
733        .update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
734        .await;
735
736    cx.executor().run_until_parked();
737    project.update(cx, |project, cx| {
738        project.restart_language_servers_for_buffers([buffer.clone()], cx)
739    });
740
741    // The extension re-fetches the latest version of the language server.
742    let fake_server = fake_servers.next().await.unwrap();
743    let new_expected_server_path =
744        extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
745    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
746    assert_eq!(fake_server.binary.path, new_expected_server_path);
747    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
748    assert_eq!(
749        fs.load(&new_expected_server_path).await.unwrap(),
750        expected_binary_contents
751    );
752
753    // The old language server directory has been cleaned up.
754    assert!(fs.metadata(&expected_server_path).await.unwrap().is_none());
755}
756
757fn init_test(cx: &mut TestAppContext) {
758    cx.update(|cx| {
759        let store = SettingsStore::test(cx);
760        cx.set_global(store);
761        release_channel::init(SemanticVersion::default(), cx);
762        theme::init(theme::LoadThemes::JustBase, cx);
763        Project::init_settings(cx);
764        ExtensionSettings::register(cx);
765        language::init(cx);
766    });
767}