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