Add logic for managing language and theme extensions (#7467)
This PR adds the initial support for loading extensions in Zed. ### Extensions Directory Extensions are loaded from the extensions directory. The extensions directory has the following structure: ``` extensions/ installed/ extension-a/ grammars/ languages/ extension-b/ themes/ manifest.json ``` The `manifest.json` file is used internally by Zed to keep track of which extensions are installed. This file should be maintained automatically, and shouldn't require any direct interaction with it. Extensions can provide Tree-sitter grammars, languages, and themes. Release Notes: - N/A --------- Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
parent
6b598a07d9
commit
6edeea7c8a
77 changed files with 1503 additions and 387 deletions
295
crates/extension/src/extension_store_test.rs
Normal file
295
crates/extension/src/extension_store_test.rs
Normal file
|
@ -0,0 +1,295 @@
|
|||
use crate::{
|
||||
ExtensionStore, GrammarManifestEntry, LanguageManifestEntry, Manifest, ThemeManifestEntry,
|
||||
};
|
||||
use fs::FakeFs;
|
||||
use gpui::{Context, TestAppContext};
|
||||
use language::{LanguageMatcher, LanguageRegistry};
|
||||
use serde_json::json;
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use theme::ThemeRegistry;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
fs.insert_tree(
|
||||
"/the-extension-dir",
|
||||
json!({
|
||||
"installed": {
|
||||
"zed-monokai": {
|
||||
"themes": {
|
||||
"monokai.json": r#"{
|
||||
"name": "Monokai",
|
||||
"author": "Someone",
|
||||
"themes": [
|
||||
{
|
||||
"name": "Monokai Dark",
|
||||
"appearance": "dark",
|
||||
"style": {}
|
||||
},
|
||||
{
|
||||
"name": "Monokai Light",
|
||||
"appearance": "light",
|
||||
"style": {}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
"monokai-pro.json": r#"{
|
||||
"name": "Monokai Pro",
|
||||
"author": "Someone",
|
||||
"themes": [
|
||||
{
|
||||
"name": "Monokai Pro Dark",
|
||||
"appearance": "dark",
|
||||
"style": {}
|
||||
},
|
||||
{
|
||||
"name": "Monokai Pro Light",
|
||||
"appearance": "light",
|
||||
"style": {}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
}
|
||||
},
|
||||
"zed-ruby": {
|
||||
"grammars": {
|
||||
"ruby.wasm": "",
|
||||
"embedded_template.wasm": "",
|
||||
},
|
||||
"languages": {
|
||||
"ruby": {
|
||||
"config.toml": r#"
|
||||
name = "Ruby"
|
||||
grammar = "ruby"
|
||||
path_suffixes = ["rb"]
|
||||
"#,
|
||||
"highlights.scm": "",
|
||||
},
|
||||
"erb": {
|
||||
"config.toml": r#"
|
||||
name = "ERB"
|
||||
grammar = "embedded_template"
|
||||
path_suffixes = ["erb"]
|
||||
"#,
|
||||
"highlights.scm": "",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut expected_manifest = Manifest {
|
||||
grammars: [
|
||||
(
|
||||
"embedded_template".into(),
|
||||
GrammarManifestEntry {
|
||||
extension: "zed-ruby".into(),
|
||||
path: "grammars/embedded_template.wasm".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"ruby".into(),
|
||||
GrammarManifestEntry {
|
||||
extension: "zed-ruby".into(),
|
||||
path: "grammars/ruby.wasm".into(),
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
languages: [
|
||||
(
|
||||
"ERB".into(),
|
||||
LanguageManifestEntry {
|
||||
extension: "zed-ruby".into(),
|
||||
path: "languages/erb".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["erb".into()],
|
||||
first_line_pattern: None,
|
||||
},
|
||||
},
|
||||
),
|
||||
(
|
||||
"Ruby".into(),
|
||||
LanguageManifestEntry {
|
||||
extension: "zed-ruby".into(),
|
||||
path: "languages/ruby".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rb".into()],
|
||||
first_line_pattern: None,
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
themes: [
|
||||
(
|
||||
"Monokai Dark".into(),
|
||||
ThemeManifestEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai.json".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Monokai Light".into(),
|
||||
ThemeManifestEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai.json".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Monokai Pro Dark".into(),
|
||||
ThemeManifestEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai-pro.json".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Monokai Pro Light".into(),
|
||||
ThemeManifestEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai-pro.json".into(),
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::test());
|
||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||
|
||||
let store = cx.new_model(|cx| {
|
||||
ExtensionStore::new(
|
||||
PathBuf::from("/the-extension-dir"),
|
||||
fs.clone(),
|
||||
language_registry.clone(),
|
||||
theme_registry.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
store.read_with(cx, |store, _| {
|
||||
let manifest = store.manifest.read();
|
||||
assert_eq!(manifest.grammars, expected_manifest.grammars);
|
||||
assert_eq!(manifest.languages, expected_manifest.languages);
|
||||
assert_eq!(manifest.themes, expected_manifest.themes);
|
||||
|
||||
assert_eq!(
|
||||
language_registry.language_names(),
|
||||
["ERB", "Plain Text", "Ruby"]
|
||||
);
|
||||
assert_eq!(
|
||||
theme_registry.list_names(false),
|
||||
[
|
||||
"Monokai Dark",
|
||||
"Monokai Light",
|
||||
"Monokai Pro Dark",
|
||||
"Monokai Pro Light",
|
||||
"One Dark",
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
fs.insert_tree(
|
||||
"/the-extension-dir/installed/zed-gruvbox",
|
||||
json!({
|
||||
"themes": {
|
||||
"gruvbox.json": r#"{
|
||||
"name": "Gruvbox",
|
||||
"author": "Someone Else",
|
||||
"themes": [
|
||||
{
|
||||
"name": "Gruvbox",
|
||||
"appearance": "dark",
|
||||
"style": {}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
expected_manifest.themes.insert(
|
||||
"Gruvbox".into(),
|
||||
ThemeManifestEntry {
|
||||
extension: "zed-gruvbox".into(),
|
||||
path: "themes/gruvbox.json".into(),
|
||||
},
|
||||
);
|
||||
|
||||
store
|
||||
.update(cx, |store, cx| store.reload(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
store.read_with(cx, |store, _| {
|
||||
let manifest = store.manifest.read();
|
||||
assert_eq!(manifest.grammars, expected_manifest.grammars);
|
||||
assert_eq!(manifest.languages, expected_manifest.languages);
|
||||
assert_eq!(manifest.themes, expected_manifest.themes);
|
||||
|
||||
assert_eq!(
|
||||
theme_registry.list_names(false),
|
||||
[
|
||||
"Gruvbox",
|
||||
"Monokai Dark",
|
||||
"Monokai Light",
|
||||
"Monokai Pro Dark",
|
||||
"Monokai Pro Light",
|
||||
"One Dark",
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
let prev_fs_metadata_call_count = fs.metadata_call_count();
|
||||
let prev_fs_read_dir_call_count = fs.read_dir_call_count();
|
||||
|
||||
// Create new extension store, as if Zed were restarting.
|
||||
drop(store);
|
||||
let store = cx.new_model(|cx| {
|
||||
ExtensionStore::new(
|
||||
PathBuf::from("/the-extension-dir"),
|
||||
fs.clone(),
|
||||
language_registry.clone(),
|
||||
theme_registry.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
store.read_with(cx, |store, _| {
|
||||
let manifest = store.manifest.read();
|
||||
assert_eq!(manifest.grammars, expected_manifest.grammars);
|
||||
assert_eq!(manifest.languages, expected_manifest.languages);
|
||||
assert_eq!(manifest.themes, expected_manifest.themes);
|
||||
|
||||
assert_eq!(
|
||||
language_registry.language_names(),
|
||||
["ERB", "Plain Text", "Ruby"]
|
||||
);
|
||||
assert_eq!(
|
||||
theme_registry.list_names(false),
|
||||
[
|
||||
"Gruvbox",
|
||||
"Monokai Dark",
|
||||
"Monokai Light",
|
||||
"Monokai Pro Dark",
|
||||
"Monokai Pro Light",
|
||||
"One Dark",
|
||||
]
|
||||
);
|
||||
|
||||
// The on-disk manifest limits the number of FS calls that need to be made
|
||||
// on startup.
|
||||
assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
|
||||
assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue