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::{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    let _ = store.update(cx, |store, cx| store.reload(None, cx));
367
368    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
369    store.read_with(cx, |store, _| {
370        let index = &store.extension_index;
371        assert_eq!(index.extensions, expected_index.extensions);
372        assert_eq!(index.languages, expected_index.languages);
373        assert_eq!(index.themes, expected_index.themes);
374
375        assert_eq!(
376            theme_registry.list_names(false),
377            [
378                "Gruvbox",
379                "Monokai Dark",
380                "Monokai Light",
381                "Monokai Pro Dark",
382                "Monokai Pro Light",
383                "One Dark",
384            ]
385        );
386    });
387
388    let prev_fs_metadata_call_count = fs.metadata_call_count();
389    let prev_fs_read_dir_call_count = fs.read_dir_call_count();
390
391    // Create new extension store, as if Zed were restarting.
392    drop(store);
393    let store = cx.new_model(|cx| {
394        ExtensionStore::new(
395            PathBuf::from("/the-extension-dir"),
396            None,
397            fs.clone(),
398            http_client.clone(),
399            None,
400            node_runtime.clone(),
401            language_registry.clone(),
402            theme_registry.clone(),
403            slash_command_registry,
404            indexed_docs_registry,
405            snippet_registry,
406            cx,
407        )
408    });
409
410    cx.executor().run_until_parked();
411    store.read_with(cx, |store, _| {
412        assert_eq!(store.extension_index, expected_index);
413        assert_eq!(
414            language_registry.language_names(),
415            ["ERB", "Plain Text", "Ruby"]
416        );
417        assert_eq!(
418            language_registry.grammar_names(),
419            ["embedded_template".into(), "ruby".into()]
420        );
421        assert_eq!(
422            theme_registry.list_names(false),
423            [
424                "Gruvbox",
425                "Monokai Dark",
426                "Monokai Light",
427                "Monokai Pro Dark",
428                "Monokai Pro Light",
429                "One Dark",
430            ]
431        );
432
433        // The on-disk manifest limits the number of FS calls that need to be made
434        // on startup.
435        assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
436        assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
437    });
438
439    store.update(cx, |store, cx| {
440        store.uninstall_extension("zed-ruby".into(), cx)
441    });
442
443    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
444    expected_index.extensions.remove("zed-ruby");
445    expected_index.languages.remove("Ruby");
446    expected_index.languages.remove("ERB");
447
448    store.read_with(cx, |store, _| {
449        assert_eq!(store.extension_index, expected_index);
450        assert_eq!(language_registry.language_names(), ["Plain Text"]);
451        assert_eq!(language_registry.grammar_names(), []);
452    });
453}
454
455#[gpui::test]
456async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
457    init_test(cx);
458    cx.executor().allow_parking();
459
460    let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
461        .parent()
462        .unwrap()
463        .parent()
464        .unwrap();
465    let cache_dir = root_dir.join("target");
466    let test_extension_id = "test-extension";
467    let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
468
469    let fs = Arc::new(RealFs::default());
470    let extensions_dir = temp_tree(json!({
471        "installed": {},
472        "work": {}
473    }));
474    let project_dir = temp_tree(json!({
475        "test.gleam": ""
476    }));
477
478    let extensions_dir = extensions_dir.path().canonicalize().unwrap();
479    let project_dir = project_dir.path().canonicalize().unwrap();
480
481    let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
482
483    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
484    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
485    let slash_command_registry = SlashCommandRegistry::new();
486    let indexed_docs_registry = Arc::new(IndexedDocsRegistry::new(cx.executor()));
487    let snippet_registry = Arc::new(SnippetRegistry::new());
488    let node_runtime = FakeNodeRuntime::new();
489
490    let mut status_updates = language_registry.language_server_binary_statuses();
491
492    struct FakeLanguageServerVersion {
493        version: String,
494        binary_contents: String,
495        http_request_count: usize,
496    }
497
498    let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
499        version: "v1.2.3".into(),
500        binary_contents: "the-binary-contents".into(),
501        http_request_count: 0,
502    }));
503
504    let http_client = FakeHttpClient::create({
505        let language_server_version = language_server_version.clone();
506        move |request| {
507            let language_server_version = language_server_version.clone();
508            async move {
509                let version = language_server_version.lock().version.clone();
510                let binary_contents = language_server_version.lock().binary_contents.clone();
511
512                let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
513                let asset_download_uri =
514                    format!("https://fake-download.example.com/gleam-{version}");
515
516                let uri = request.uri().to_string();
517                if uri == github_releases_uri {
518                    language_server_version.lock().http_request_count += 1;
519                    Ok(Response::new(
520                        json!([
521                            {
522                                "tag_name": version,
523                                "prerelease": false,
524                                "tarball_url": "",
525                                "zipball_url": "",
526                                "assets": [
527                                    {
528                                        "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
529                                        "browser_download_url": asset_download_uri
530                                    },
531                                    {
532                                        "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
533                                        "browser_download_url": asset_download_uri
534                                    },
535                                    {
536                                        "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
537                                        "browser_download_url": asset_download_uri
538                                    }
539                                ]
540                            }
541                        ])
542                        .to_string()
543                        .into(),
544                    ))
545                } else if uri == asset_download_uri {
546                    language_server_version.lock().http_request_count += 1;
547                    let mut bytes = Vec::<u8>::new();
548                    let mut archive = async_tar::Builder::new(&mut bytes);
549                    let mut header = async_tar::Header::new_gnu();
550                    header.set_size(binary_contents.len() as u64);
551                    archive
552                        .append_data(&mut header, "gleam", binary_contents.as_bytes())
553                        .await
554                        .unwrap();
555                    archive.into_inner().await.unwrap();
556                    let mut gzipped_bytes = Vec::new();
557                    let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
558                    encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
559                    Ok(Response::new(gzipped_bytes.into()))
560                } else {
561                    Ok(Response::builder().status(404).body("not found".into())?)
562                }
563            }
564        }
565    });
566
567    let extension_store = cx.new_model(|cx| {
568        ExtensionStore::new(
569            extensions_dir.clone(),
570            Some(cache_dir),
571            fs.clone(),
572            http_client.clone(),
573            None,
574            node_runtime,
575            language_registry.clone(),
576            theme_registry.clone(),
577            slash_command_registry,
578            indexed_docs_registry,
579            snippet_registry,
580            cx,
581        )
582    });
583
584    // Ensure that debounces fire.
585    let mut events = cx.events(&extension_store);
586    let executor = cx.executor();
587    let _task = cx.executor().spawn(async move {
588        while let Some(event) = events.next().await {
589            match event {
590                crate::Event::StartedReloading => {
591                    executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
592                }
593                _ => (),
594            }
595        }
596    });
597
598    extension_store.update(cx, |_, cx| {
599        cx.subscribe(&extension_store, |_, _, event, _| {
600            if matches!(event, Event::ExtensionFailedToLoad(_)) {
601                panic!("extension failed to load");
602            }
603        })
604        .detach();
605    });
606
607    extension_store
608        .update(cx, |store, cx| {
609            store.install_dev_extension(test_extension_dir.clone(), cx)
610        })
611        .await
612        .unwrap();
613
614    let mut fake_servers = language_registry.fake_language_servers("Gleam");
615
616    let buffer = project
617        .update(cx, |project, cx| {
618            project.open_local_buffer(project_dir.join("test.gleam"), cx)
619        })
620        .await
621        .unwrap();
622
623    let fake_server = fake_servers.next().await.unwrap();
624    let expected_server_path =
625        extensions_dir.join(format!("work/{test_extension_id}/gleam-v1.2.3/gleam"));
626    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
627
628    assert_eq!(fake_server.binary.path, expected_server_path);
629    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
630    assert_eq!(
631        fs.load(&expected_server_path).await.unwrap(),
632        expected_binary_contents
633    );
634    assert_eq!(language_server_version.lock().http_request_count, 2);
635    assert_eq!(
636        [
637            status_updates.next().await.unwrap(),
638            status_updates.next().await.unwrap(),
639            status_updates.next().await.unwrap(),
640        ],
641        [
642            (
643                LanguageServerName("gleam".into()),
644                LanguageServerBinaryStatus::CheckingForUpdate
645            ),
646            (
647                LanguageServerName("gleam".into()),
648                LanguageServerBinaryStatus::Downloading
649            ),
650            (
651                LanguageServerName("gleam".into()),
652                LanguageServerBinaryStatus::None
653            )
654        ]
655    );
656
657    // The extension creates custom labels for completion items.
658    fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
659        Ok(Some(lsp::CompletionResponse::Array(vec![
660            lsp::CompletionItem {
661                label: "foo".into(),
662                kind: Some(lsp::CompletionItemKind::FUNCTION),
663                detail: Some("fn() -> Result(Nil, Error)".into()),
664                ..Default::default()
665            },
666            lsp::CompletionItem {
667                label: "bar.baz".into(),
668                kind: Some(lsp::CompletionItemKind::FUNCTION),
669                detail: Some("fn(List(a)) -> a".into()),
670                ..Default::default()
671            },
672            lsp::CompletionItem {
673                label: "Quux".into(),
674                kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
675                detail: Some("fn(String) -> T".into()),
676                ..Default::default()
677            },
678            lsp::CompletionItem {
679                label: "my_string".into(),
680                kind: Some(lsp::CompletionItemKind::CONSTANT),
681                detail: Some("String".into()),
682                ..Default::default()
683            },
684        ])))
685    });
686
687    let completion_labels = project
688        .update(cx, |project, cx| {
689            project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
690        })
691        .await
692        .unwrap()
693        .into_iter()
694        .map(|c| c.label.text)
695        .collect::<Vec<_>>();
696    assert_eq!(
697        completion_labels,
698        [
699            "foo: fn() -> Result(Nil, Error)".to_string(),
700            "bar.baz: fn(List(a)) -> a".to_string(),
701            "Quux: fn(String) -> T".to_string(),
702            "my_string: String".to_string(),
703        ]
704    );
705
706    // Simulate a new version of the language server being released
707    language_server_version.lock().version = "v2.0.0".into();
708    language_server_version.lock().binary_contents = "the-new-binary-contents".into();
709    language_server_version.lock().http_request_count = 0;
710
711    // Start a new instance of the language server.
712    project.update(cx, |project, cx| {
713        project.restart_language_servers_for_buffers([buffer.clone()], cx)
714    });
715
716    // The extension has cached the binary path, and does not attempt
717    // to reinstall it.
718    let fake_server = fake_servers.next().await.unwrap();
719    assert_eq!(fake_server.binary.path, expected_server_path);
720    assert_eq!(
721        fs.load(&expected_server_path).await.unwrap(),
722        expected_binary_contents
723    );
724    assert_eq!(language_server_version.lock().http_request_count, 0);
725
726    // Reload the extension, clearing its cache.
727    // Start a new instance of the language server.
728    extension_store
729        .update(cx, |store, cx| store.reload(Some("gleam".into()), cx))
730        .await;
731
732    cx.executor().run_until_parked();
733    project.update(cx, |project, cx| {
734        project.restart_language_servers_for_buffers([buffer.clone()], cx)
735    });
736
737    // The extension re-fetches the latest version of the language server.
738    let fake_server = fake_servers.next().await.unwrap();
739    let new_expected_server_path =
740        extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
741    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
742    assert_eq!(fake_server.binary.path, new_expected_server_path);
743    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
744    assert_eq!(
745        fs.load(&new_expected_server_path).await.unwrap(),
746        expected_binary_contents
747    );
748
749    // The old language server directory has been cleaned up.
750    assert!(fs.metadata(&expected_server_path).await.unwrap().is_none());
751}
752
753fn init_test(cx: &mut TestAppContext) {
754    cx.update(|cx| {
755        let store = SettingsStore::test(cx);
756        cx.set_global(store);
757        release_channel::init(SemanticVersion::default(), cx);
758        theme::init(theme::LoadThemes::JustBase, cx);
759        Project::init_settings(cx);
760        ExtensionSettings::register(cx);
761        language::init(cx);
762    });
763}