1use crate::{
2 ExtensionStore, GrammarManifestEntry, LanguageManifestEntry, Manifest, ThemeManifestEntry,
3};
4use fs::FakeFs;
5use gpui::{Context, TestAppContext};
6use language::{LanguageMatcher, LanguageRegistry};
7use serde_json::json;
8use settings::SettingsStore;
9use std::{path::PathBuf, sync::Arc};
10use theme::ThemeRegistry;
11use util::http::FakeHttpClient;
12
13#[gpui::test]
14async fn test_extension_store(cx: &mut TestAppContext) {
15 cx.update(|cx| {
16 let store = SettingsStore::test(cx);
17 cx.set_global(store);
18 theme::init(theme::LoadThemes::JustBase, cx);
19 });
20
21 let fs = FakeFs::new(cx.executor());
22 let http_client = FakeHttpClient::with_200_response();
23
24 fs.insert_tree(
25 "/the-extension-dir",
26 json!({
27 "installed": {
28 "zed-monokai": {
29 "extension.json": r#"{
30 "id": "zed-monokai",
31 "name": "Zed Monokai",
32 "version": "2.0.0"
33 }"#,
34 "themes": {
35 "monokai.json": r#"{
36 "name": "Monokai",
37 "author": "Someone",
38 "themes": [
39 {
40 "name": "Monokai Dark",
41 "appearance": "dark",
42 "style": {}
43 },
44 {
45 "name": "Monokai Light",
46 "appearance": "light",
47 "style": {}
48 }
49 ]
50 }"#,
51 "monokai-pro.json": r#"{
52 "name": "Monokai Pro",
53 "author": "Someone",
54 "themes": [
55 {
56 "name": "Monokai Pro Dark",
57 "appearance": "dark",
58 "style": {}
59 },
60 {
61 "name": "Monokai Pro Light",
62 "appearance": "light",
63 "style": {}
64 }
65 ]
66 }"#,
67 }
68 },
69 "zed-ruby": {
70 "extension.json": r#"{
71 "id": "zed-ruby",
72 "name": "Zed Ruby",
73 "version": "1.0.0"
74 }"#,
75 "grammars": {
76 "ruby.wasm": "",
77 "embedded_template.wasm": "",
78 },
79 "languages": {
80 "ruby": {
81 "config.toml": r#"
82 name = "Ruby"
83 grammar = "ruby"
84 path_suffixes = ["rb"]
85 "#,
86 "highlights.scm": "",
87 },
88 "erb": {
89 "config.toml": r#"
90 name = "ERB"
91 grammar = "embedded_template"
92 path_suffixes = ["erb"]
93 "#,
94 "highlights.scm": "",
95 }
96 },
97 }
98 }
99 }),
100 )
101 .await;
102
103 let mut expected_manifest = Manifest {
104 extensions: [
105 ("zed-ruby".into(), "1.0.0".into()),
106 ("zed-monokai".into(), "2.0.0".into()),
107 ]
108 .into_iter()
109 .collect(),
110 grammars: [
111 (
112 "embedded_template".into(),
113 GrammarManifestEntry {
114 extension: "zed-ruby".into(),
115 path: "grammars/embedded_template.wasm".into(),
116 },
117 ),
118 (
119 "ruby".into(),
120 GrammarManifestEntry {
121 extension: "zed-ruby".into(),
122 path: "grammars/ruby.wasm".into(),
123 },
124 ),
125 ]
126 .into_iter()
127 .collect(),
128 languages: [
129 (
130 "ERB".into(),
131 LanguageManifestEntry {
132 extension: "zed-ruby".into(),
133 path: "languages/erb".into(),
134 grammar: Some("embedded_template".into()),
135 matcher: LanguageMatcher {
136 path_suffixes: vec!["erb".into()],
137 first_line_pattern: None,
138 },
139 },
140 ),
141 (
142 "Ruby".into(),
143 LanguageManifestEntry {
144 extension: "zed-ruby".into(),
145 path: "languages/ruby".into(),
146 grammar: Some("ruby".into()),
147 matcher: LanguageMatcher {
148 path_suffixes: vec!["rb".into()],
149 first_line_pattern: None,
150 },
151 },
152 ),
153 ]
154 .into_iter()
155 .collect(),
156 themes: [
157 (
158 "Monokai Dark".into(),
159 ThemeManifestEntry {
160 extension: "zed-monokai".into(),
161 path: "themes/monokai.json".into(),
162 },
163 ),
164 (
165 "Monokai Light".into(),
166 ThemeManifestEntry {
167 extension: "zed-monokai".into(),
168 path: "themes/monokai.json".into(),
169 },
170 ),
171 (
172 "Monokai Pro Dark".into(),
173 ThemeManifestEntry {
174 extension: "zed-monokai".into(),
175 path: "themes/monokai-pro.json".into(),
176 },
177 ),
178 (
179 "Monokai Pro Light".into(),
180 ThemeManifestEntry {
181 extension: "zed-monokai".into(),
182 path: "themes/monokai-pro.json".into(),
183 },
184 ),
185 ]
186 .into_iter()
187 .collect(),
188 };
189
190 let language_registry = Arc::new(LanguageRegistry::test());
191 let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
192
193 let store = cx.new_model(|cx| {
194 ExtensionStore::new(
195 PathBuf::from("/the-extension-dir"),
196 fs.clone(),
197 http_client.clone(),
198 language_registry.clone(),
199 theme_registry.clone(),
200 cx,
201 )
202 });
203
204 cx.executor().run_until_parked();
205 store.read_with(cx, |store, _| {
206 let manifest = store.manifest.read();
207 assert_eq!(manifest.grammars, expected_manifest.grammars);
208 assert_eq!(manifest.languages, expected_manifest.languages);
209 assert_eq!(manifest.themes, expected_manifest.themes);
210
211 assert_eq!(
212 language_registry.language_names(),
213 ["ERB", "Plain Text", "Ruby"]
214 );
215 assert_eq!(
216 theme_registry.list_names(false),
217 [
218 "Monokai Dark",
219 "Monokai Light",
220 "Monokai Pro Dark",
221 "Monokai Pro Light",
222 "One Dark",
223 ]
224 );
225 });
226
227 fs.insert_tree(
228 "/the-extension-dir/installed/zed-gruvbox",
229 json!({
230 "extension.json": r#"{
231 "id": "zed-gruvbox",
232 "name": "Zed Gruvbox",
233 "version": "1.0.0"
234 }"#,
235 "themes": {
236 "gruvbox.json": r#"{
237 "name": "Gruvbox",
238 "author": "Someone Else",
239 "themes": [
240 {
241 "name": "Gruvbox",
242 "appearance": "dark",
243 "style": {}
244 }
245 ]
246 }"#,
247 }
248 }),
249 )
250 .await;
251
252 expected_manifest.themes.insert(
253 "Gruvbox".into(),
254 ThemeManifestEntry {
255 extension: "zed-gruvbox".into(),
256 path: "themes/gruvbox.json".into(),
257 },
258 );
259
260 store.update(cx, |store, cx| store.reload(cx));
261
262 cx.executor().run_until_parked();
263 store.read_with(cx, |store, _| {
264 let manifest = store.manifest.read();
265 assert_eq!(manifest.grammars, expected_manifest.grammars);
266 assert_eq!(manifest.languages, expected_manifest.languages);
267 assert_eq!(manifest.themes, expected_manifest.themes);
268
269 assert_eq!(
270 theme_registry.list_names(false),
271 [
272 "Gruvbox",
273 "Monokai Dark",
274 "Monokai Light",
275 "Monokai Pro Dark",
276 "Monokai Pro Light",
277 "One Dark",
278 ]
279 );
280 });
281
282 let prev_fs_metadata_call_count = fs.metadata_call_count();
283 let prev_fs_read_dir_call_count = fs.read_dir_call_count();
284
285 // Create new extension store, as if Zed were restarting.
286 drop(store);
287 let store = cx.new_model(|cx| {
288 ExtensionStore::new(
289 PathBuf::from("/the-extension-dir"),
290 fs.clone(),
291 http_client.clone(),
292 language_registry.clone(),
293 theme_registry.clone(),
294 cx,
295 )
296 });
297
298 cx.executor().run_until_parked();
299 store.read_with(cx, |store, _| {
300 let manifest = store.manifest.read();
301 assert_eq!(manifest.grammars, expected_manifest.grammars);
302 assert_eq!(manifest.languages, expected_manifest.languages);
303 assert_eq!(manifest.themes, expected_manifest.themes);
304
305 assert_eq!(
306 language_registry.language_names(),
307 ["ERB", "Plain Text", "Ruby"]
308 );
309 assert_eq!(
310 language_registry.grammar_names(),
311 ["embedded_template".into(), "ruby".into()]
312 );
313 assert_eq!(
314 theme_registry.list_names(false),
315 [
316 "Gruvbox",
317 "Monokai Dark",
318 "Monokai Light",
319 "Monokai Pro Dark",
320 "Monokai Pro Light",
321 "One Dark",
322 ]
323 );
324
325 // The on-disk manifest limits the number of FS calls that need to be made
326 // on startup.
327 assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
328 assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
329 });
330
331 store.update(cx, |store, cx| {
332 store.uninstall_extension("zed-ruby".into(), cx)
333 });
334
335 cx.executor().run_until_parked();
336 expected_manifest.extensions.remove("zed-ruby");
337 expected_manifest.languages.remove("Ruby");
338 expected_manifest.languages.remove("ERB");
339 expected_manifest.grammars.remove("ruby");
340 expected_manifest.grammars.remove("embedded_template");
341
342 store.read_with(cx, |store, _| {
343 let manifest = store.manifest.read();
344 assert_eq!(manifest.grammars, expected_manifest.grammars);
345 assert_eq!(manifest.languages, expected_manifest.languages);
346 assert_eq!(manifest.themes, expected_manifest.themes);
347
348 assert_eq!(language_registry.language_names(), ["Plain Text"]);
349 assert_eq!(language_registry.grammar_names(), []);
350 });
351}