Add a command for building and installing a locally-developed Zed extension (#8781)
This PR adds an `zed: Install Local Extension` action, which lets you select a path to a folder containing a Zed extension, and install that . When you select a directory, the extension will be compiled (both the Tree-sitter grammars and the Rust code for the extension itself) and installed as a Zed extension, using a symlink. ### Details A few dependencies are needed to build an extension: * The Rust `wasm32-wasi` target. This is automatically installed if needed via `rustup`. * A wasi-preview1 adapter WASM module, for building WASM components with Rust. This is automatically downloaded if needed from a `wasmtime` GitHub release * For building Tree-sitter parsers, a distribution of `wasi-sdk`. This is automatically downloaded if needed from a `wasi-sdk` GitHub release. The downloaded artifacts are cached in a support directory called `Zed/extensions/build`. ### Tasks UX * [x] Show local extensions in the Extensions view * [x] Provide a button for recompiling a linked extension * [x] Make this action discoverable by adding a button for it on the Extensions view * [ ] Surface errors (don't just write them to the Zed log) Packaging * [ ] Create a separate executable that performs the extension compilation. We'll switch the packaging system in our [extensions](https://github.com/zed-industries/extensions) repo to use this binary, so that there is one canonical definition of how to build/package an extensions. ### Release Notes: - N/A --------- Co-authored-by: Marshall <marshall@zed.dev> Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
parent
e273198ada
commit
675ae24964
18 changed files with 1662 additions and 763 deletions
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionManifest,
|
||||
ExtensionStore, GrammarManifestEntry,
|
||||
build_extension::{CompileExtensionOptions, ExtensionBuilder},
|
||||
ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, ExtensionIndexThemeEntry,
|
||||
ExtensionManifest, ExtensionStore, GrammarManifestEntry, RELOAD_DEBOUNCE_DURATION,
|
||||
};
|
||||
use async_compression::futures::bufread::GzipEncoder;
|
||||
use collections::BTreeMap;
|
||||
|
@ -21,7 +22,7 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
use theme::ThemeRegistry;
|
||||
use util::http::{FakeHttpClient, Response};
|
||||
use util::http::{self, FakeHttpClient, Response};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_extension_store(cx: &mut TestAppContext) {
|
||||
|
@ -131,45 +132,49 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||
extensions: [
|
||||
(
|
||||
"zed-ruby".into(),
|
||||
ExtensionManifest {
|
||||
id: "zed-ruby".into(),
|
||||
name: "Zed Ruby".into(),
|
||||
version: "1.0.0".into(),
|
||||
description: None,
|
||||
authors: Vec::new(),
|
||||
repository: None,
|
||||
themes: Default::default(),
|
||||
lib: Default::default(),
|
||||
languages: vec!["languages/erb".into(), "languages/ruby".into()],
|
||||
grammars: [
|
||||
("embedded_template".into(), GrammarManifestEntry::default()),
|
||||
("ruby".into(), GrammarManifestEntry::default()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
language_servers: BTreeMap::default(),
|
||||
}
|
||||
.into(),
|
||||
ExtensionIndexEntry {
|
||||
manifest: Arc::new(ExtensionManifest {
|
||||
id: "zed-ruby".into(),
|
||||
name: "Zed Ruby".into(),
|
||||
version: "1.0.0".into(),
|
||||
description: None,
|
||||
authors: Vec::new(),
|
||||
repository: None,
|
||||
themes: Default::default(),
|
||||
lib: Default::default(),
|
||||
languages: vec!["languages/erb".into(), "languages/ruby".into()],
|
||||
grammars: [
|
||||
("embedded_template".into(), GrammarManifestEntry::default()),
|
||||
("ruby".into(), GrammarManifestEntry::default()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
language_servers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
),
|
||||
(
|
||||
"zed-monokai".into(),
|
||||
ExtensionManifest {
|
||||
id: "zed-monokai".into(),
|
||||
name: "Zed Monokai".into(),
|
||||
version: "2.0.0".into(),
|
||||
description: None,
|
||||
authors: vec![],
|
||||
repository: None,
|
||||
themes: vec![
|
||||
"themes/monokai-pro.json".into(),
|
||||
"themes/monokai.json".into(),
|
||||
],
|
||||
lib: Default::default(),
|
||||
languages: Default::default(),
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: BTreeMap::default(),
|
||||
}
|
||||
.into(),
|
||||
ExtensionIndexEntry {
|
||||
manifest: Arc::new(ExtensionManifest {
|
||||
id: "zed-monokai".into(),
|
||||
name: "Zed Monokai".into(),
|
||||
version: "2.0.0".into(),
|
||||
description: None,
|
||||
authors: vec![],
|
||||
repository: None,
|
||||
themes: vec![
|
||||
"themes/monokai-pro.json".into(),
|
||||
"themes/monokai.json".into(),
|
||||
],
|
||||
lib: Default::default(),
|
||||
languages: Default::default(),
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
|
@ -205,28 +210,28 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||
themes: [
|
||||
(
|
||||
"Monokai Dark".into(),
|
||||
ExtensionIndexEntry {
|
||||
ExtensionIndexThemeEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai.json".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Monokai Light".into(),
|
||||
ExtensionIndexEntry {
|
||||
ExtensionIndexThemeEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai.json".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Monokai Pro Dark".into(),
|
||||
ExtensionIndexEntry {
|
||||
ExtensionIndexThemeEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai-pro.json".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"Monokai Pro Light".into(),
|
||||
ExtensionIndexEntry {
|
||||
ExtensionIndexThemeEntry {
|
||||
extension: "zed-monokai".into(),
|
||||
path: "themes/monokai-pro.json".into(),
|
||||
},
|
||||
|
@ -252,7 +257,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||
)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
cx.executor().advance_clock(super::RELOAD_DEBOUNCE_DURATION);
|
||||
store.read_with(cx, |store, _| {
|
||||
let index = &store.extension_index;
|
||||
assert_eq!(index.extensions, expected_index.extensions);
|
||||
|
@ -305,32 +310,34 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||
|
||||
expected_index.extensions.insert(
|
||||
"zed-gruvbox".into(),
|
||||
ExtensionManifest {
|
||||
id: "zed-gruvbox".into(),
|
||||
name: "Zed Gruvbox".into(),
|
||||
version: "1.0.0".into(),
|
||||
description: None,
|
||||
authors: vec![],
|
||||
repository: None,
|
||||
themes: vec!["themes/gruvbox.json".into()],
|
||||
lib: Default::default(),
|
||||
languages: Default::default(),
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: BTreeMap::default(),
|
||||
}
|
||||
.into(),
|
||||
ExtensionIndexEntry {
|
||||
manifest: Arc::new(ExtensionManifest {
|
||||
id: "zed-gruvbox".into(),
|
||||
name: "Zed Gruvbox".into(),
|
||||
version: "1.0.0".into(),
|
||||
description: None,
|
||||
authors: vec![],
|
||||
repository: None,
|
||||
themes: vec!["themes/gruvbox.json".into()],
|
||||
lib: Default::default(),
|
||||
languages: Default::default(),
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: BTreeMap::default(),
|
||||
}),
|
||||
dev: false,
|
||||
},
|
||||
);
|
||||
expected_index.themes.insert(
|
||||
"Gruvbox".into(),
|
||||
ExtensionIndexEntry {
|
||||
ExtensionIndexThemeEntry {
|
||||
extension: "zed-gruvbox".into(),
|
||||
path: "themes/gruvbox.json".into(),
|
||||
},
|
||||
);
|
||||
|
||||
store.update(cx, |store, cx| store.reload(cx));
|
||||
let _ = store.update(cx, |store, _| store.reload(None));
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
|
||||
store.read_with(cx, |store, _| {
|
||||
let index = &store.extension_index;
|
||||
assert_eq!(index.extensions, expected_index.extensions);
|
||||
|
@ -400,7 +407,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||
store.uninstall_extension("zed-ruby".into(), cx)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
|
||||
expected_index.extensions.remove("zed-ruby");
|
||||
expected_index.languages.remove("Ruby");
|
||||
expected_index.languages.remove("ERB");
|
||||
|
@ -416,17 +423,23 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||
async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let gleam_extension_dir = PathBuf::from_iter([
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"..",
|
||||
"..",
|
||||
"extensions",
|
||||
"gleam",
|
||||
])
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap();
|
||||
let cache_dir = root_dir.join("target");
|
||||
let gleam_extension_dir = root_dir.join("extensions").join("gleam");
|
||||
|
||||
compile_extension("zed_gleam", &gleam_extension_dir);
|
||||
cx.executor().allow_parking();
|
||||
ExtensionBuilder::new(cache_dir, http::client())
|
||||
.compile_extension(
|
||||
&gleam_extension_dir,
|
||||
CompileExtensionOptions { release: false },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().forbid_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/the-extension-dir", json!({ "installed": {} }))
|
||||
|
@ -509,7 +522,7 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
|
|||
)
|
||||
});
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
|
||||
|
||||
let mut fake_servers = language_registry.fake_language_servers("Gleam");
|
||||
|
||||
|
@ -572,27 +585,6 @@ async fn test_extension_store_with_gleam_extension(cx: &mut TestAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
fn compile_extension(name: &str, extension_dir_path: &Path) {
|
||||
let output = std::process::Command::new("cargo")
|
||||
.args(["component", "build", "--target-dir"])
|
||||
.arg(extension_dir_path.join("target"))
|
||||
.current_dir(&extension_dir_path)
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"failed to build component {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let mut wasm_path = PathBuf::from(extension_dir_path);
|
||||
wasm_path.extend(["target", "wasm32-wasi", "debug", name]);
|
||||
wasm_path.set_extension("wasm");
|
||||
|
||||
std::fs::rename(wasm_path, extension_dir_path.join("extension.wasm")).unwrap();
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue