Add new `extension` crate (#20089)

Marshall Bowers created

This PR adds a new `extension` crate, containing some contents extracted
from the `extension_host`.

Right now it contains just the `ExtensionManifest` and
`ExtensionBuilder`, although we may move more of the extension interface
into here.

The introduction of the `extension` crate allows us to depend on it in
the `extension_cli`, thereby eliminating the need for the `no-webrtc`
feature on a number of crates.

Release Notes:

- N/A

Change summary

Cargo.lock                                        | 29 ++++++++-
Cargo.toml                                        |  2 
crates/call/Cargo.toml                            |  1 
crates/extension/Cargo.toml                       | 31 ++++++++++
crates/extension/LICENSE-GPL                      |  1 
crates/extension/src/extension.rs                 | 50 +++++++++++++++++
crates/extension/src/extension_builder.rs         |  6 +-
crates/extension/src/extension_manifest.rs        |  0 
crates/extension_cli/Cargo.toml                   |  2 
crates/extension_cli/src/main.rs                  |  2 
crates/extension_host/Cargo.toml                  |  7 --
crates/extension_host/src/extension_host.rs       |  8 +-
crates/extension_host/src/extension_store_test.rs |  2 
crates/extension_host/src/wasm_host.rs            | 48 +---------------
crates/workspace/Cargo.toml                       |  1 
15 files changed, 122 insertions(+), 68 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4089,6 +4089,29 @@ dependencies = [
  "zune-inflate",
 ]
 
+[[package]]
+name = "extension"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-compression",
+ "async-tar",
+ "collections",
+ "fs",
+ "futures 0.3.30",
+ "http_client",
+ "language",
+ "log",
+ "lsp",
+ "semantic_version",
+ "serde",
+ "serde_json",
+ "toml 0.8.19",
+ "wasm-encoder 0.215.0",
+ "wasmparser 0.215.0",
+ "wit-component",
+]
+
 [[package]]
 name = "extension_cli"
 version = "0.1.0"
@@ -4096,7 +4119,7 @@ dependencies = [
  "anyhow",
  "clap",
  "env_logger 0.11.5",
- "extension_host",
+ "extension",
  "fs",
  "language",
  "log",
@@ -4124,6 +4147,7 @@ dependencies = [
  "collections",
  "ctor",
  "env_logger 0.11.5",
+ "extension",
  "fs",
  "futures 0.3.30",
  "gpui",
@@ -4151,11 +4175,8 @@ dependencies = [
  "ui",
  "url",
  "util",
- "wasm-encoder 0.215.0",
- "wasmparser 0.215.0",
  "wasmtime",
  "wasmtime-wasi",
- "wit-component",
  "workspace",
 ]
 

Cargo.toml 🔗

@@ -27,6 +27,7 @@ members = [
     "crates/docs_preprocessor",
     "crates/editor",
     "crates/evals",
+    "crates/extension",
     "crates/extension_api",
     "crates/extension_cli",
     "crates/extension_host",
@@ -201,6 +202,7 @@ copilot = { path = "crates/copilot" }
 db = { path = "crates/db" }
 diagnostics = { path = "crates/diagnostics" }
 editor = { path = "crates/editor" }
+extension = { path = "crates/extension" }
 extension_host = { path = "crates/extension_host" }
 extensions_ui = { path = "crates/extensions_ui" }
 feature_flags = { path = "crates/feature_flags" }

crates/call/Cargo.toml 🔗

@@ -13,7 +13,6 @@ path = "src/call.rs"
 doctest = false
 
 [features]
-no-webrtc = ["live_kit_client/no-webrtc"]
 test-support = [
     "client/test-support",
     "collections/test-support",

crates/extension/Cargo.toml 🔗

@@ -0,0 +1,31 @@
+[package]
+name = "extension"
+version = "0.1.0"
+edition = "2021"
+publish = false
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/extension.rs"
+
+[dependencies]
+anyhow.workspace = true
+async-compression.workspace = true
+async-tar.workspace = true
+collections.workspace = true
+fs.workspace = true
+futures.workspace = true
+http_client.workspace = true
+language.workspace = true
+log.workspace = true
+lsp.workspace = true
+semantic_version.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+toml.workspace = true
+wasm-encoder.workspace = true
+wasmparser.workspace = true
+wit-component.workspace = true

crates/extension/src/extension.rs 🔗

@@ -0,0 +1,50 @@
+pub mod extension_builder;
+mod extension_manifest;
+
+use anyhow::{anyhow, bail, Context as _, Result};
+use semantic_version::SemanticVersion;
+
+pub use crate::extension_manifest::*;
+
+pub fn parse_wasm_extension_version(
+    extension_id: &str,
+    wasm_bytes: &[u8],
+) -> Result<SemanticVersion> {
+    let mut version = None;
+
+    for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
+        if let wasmparser::Payload::CustomSection(s) =
+            part.context("error parsing wasm extension")?
+        {
+            if s.name() == "zed:api-version" {
+                version = parse_wasm_extension_version_custom_section(s.data());
+                if version.is_none() {
+                    bail!(
+                        "extension {} has invalid zed:api-version section: {:?}",
+                        extension_id,
+                        s.data()
+                    );
+                }
+            }
+        }
+    }
+
+    // The reason we wait until we're done parsing all of the Wasm bytes to return the version
+    // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid.
+    //
+    // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem
+    // earlier as an `Err` rather than as a panic.
+    version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id))
+}
+
+fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVersion> {
+    if data.len() == 6 {
+        Some(SemanticVersion::new(
+            u16::from_be_bytes([data[0], data[1]]) as _,
+            u16::from_be_bytes([data[2], data[3]]) as _,
+            u16::from_be_bytes([data[4], data[5]]) as _,
+        ))
+    } else {
+        None
+    }
+}

crates/extension_host/src/extension_builder.rs → crates/extension/src/extension_builder.rs 🔗

@@ -1,6 +1,6 @@
-use crate::wasm_host::parse_wasm_extension_version;
-use crate::ExtensionManifest;
-use crate::{extension_manifest::ExtensionLibraryKind, GrammarManifestEntry};
+use crate::{
+    parse_wasm_extension_version, ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry,
+};
 use anyhow::{anyhow, bail, Context as _, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;

crates/extension_cli/Cargo.toml 🔗

@@ -16,7 +16,7 @@ path = "src/main.rs"
 anyhow.workspace = true
 clap = { workspace = true, features = ["derive"] }
 env_logger.workspace = true
-extension_host = { workspace = true, features = ["no-webrtc"] }
+extension.workspace = true
 fs.workspace = true
 language.workspace = true
 log.workspace = true

crates/extension_cli/src/main.rs 🔗

@@ -9,7 +9,7 @@ use std::{
 use ::fs::{copy_recursive, CopyOptions, Fs, RealFs};
 use anyhow::{anyhow, bail, Context, Result};
 use clap::Parser;
-use extension_host::{
+use extension::{
     extension_builder::{CompileExtensionOptions, ExtensionBuilder},
     ExtensionManifest,
 };

crates/extension_host/Cargo.toml 🔗

@@ -12,9 +12,6 @@ workspace = true
 path = "src/extension_host.rs"
 doctest = false
 
-[features]
-no-webrtc = ["workspace/no-webrtc"]
-
 [dependencies]
 anyhow.workspace = true
 assistant_slash_command.workspace = true
@@ -23,6 +20,7 @@ async-tar.workspace = true
 async-trait.workspace = true
 client.workspace = true
 collections.workspace = true
+extension.workspace = true
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
@@ -48,11 +46,8 @@ toml.workspace = true
 ui.workspace = true
 url.workspace = true
 util.workspace = true
-wasm-encoder.workspace = true
-wasmparser.workspace = true
 wasmtime-wasi.workspace = true
 wasmtime.workspace = true
-wit-component.workspace = true
 workspace.workspace = true
 
 [dev-dependencies]

crates/extension_host/src/extension_host.rs 🔗

@@ -1,7 +1,5 @@
-pub mod extension_builder;
 mod extension_indexed_docs_provider;
 mod extension_lsp_adapter;
-mod extension_manifest;
 mod extension_settings;
 mod extension_slash_command;
 mod wasm_host;
@@ -10,7 +8,6 @@ mod wasm_host;
 mod extension_store_test;
 
 use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider;
-use crate::extension_manifest::SchemaVersion;
 use crate::extension_slash_command::ExtensionSlashCommand;
 use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit};
 use anyhow::{anyhow, bail, Context as _, Result};
@@ -19,7 +16,8 @@ use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
 use collections::{btree_map, BTreeMap, HashSet};
-use extension_builder::{CompileExtensionOptions, ExtensionBuilder};
+use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
+use extension::SchemaVersion;
 use fs::{Fs, RemoveOptions};
 use futures::{
     channel::{
@@ -62,7 +60,7 @@ use wasm_host::{
     WasmExtension, WasmHost,
 };
 
-pub use extension_manifest::{
+pub use extension::{
     ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, OldExtensionManifest,
 };
 pub use extension_settings::ExtensionSettings;

crates/extension_host/src/extension_store_test.rs 🔗

@@ -1,4 +1,3 @@
-use crate::extension_manifest::SchemaVersion;
 use crate::extension_settings::ExtensionSettings;
 use crate::{
     Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
@@ -8,6 +7,7 @@ use crate::{
 use assistant_slash_command::SlashCommandRegistry;
 use async_compression::futures::bufread::GzipEncoder;
 use collections::BTreeMap;
+use extension::SchemaVersion;
 use fs::{FakeFs, Fs, RealFs};
 use futures::{io::BufReader, AsyncReadExt, StreamExt};
 use gpui::{Context, SemanticVersion, TestAppContext};

crates/extension_host/src/wasm_host.rs 🔗

@@ -1,7 +1,7 @@
 pub(crate) mod wit;
 
 use crate::ExtensionManifest;
-use anyhow::{anyhow, bail, Context as _, Result};
+use anyhow::{anyhow, Context as _, Result};
 use fs::{normalize_path, Fs};
 use futures::future::LocalBoxFuture;
 use futures::{
@@ -112,7 +112,8 @@ impl WasmHost {
     ) -> Task<Result<WasmExtension>> {
         let this = self.clone();
         executor.clone().spawn(async move {
-            let zed_api_version = parse_wasm_extension_version(&manifest.id, &wasm_bytes)?;
+            let zed_api_version =
+                extension::parse_wasm_extension_version(&manifest.id, &wasm_bytes)?;
 
             let component = Component::from_binary(&this.engine, &wasm_bytes)
                 .context("failed to compile wasm component")?;
@@ -197,49 +198,6 @@ impl WasmHost {
     }
 }
 
-pub fn parse_wasm_extension_version(
-    extension_id: &str,
-    wasm_bytes: &[u8],
-) -> Result<SemanticVersion> {
-    let mut version = None;
-
-    for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
-        if let wasmparser::Payload::CustomSection(s) =
-            part.context("error parsing wasm extension")?
-        {
-            if s.name() == "zed:api-version" {
-                version = parse_wasm_extension_version_custom_section(s.data());
-                if version.is_none() {
-                    bail!(
-                        "extension {} has invalid zed:api-version section: {:?}",
-                        extension_id,
-                        s.data()
-                    );
-                }
-            }
-        }
-    }
-
-    // The reason we wait until we're done parsing all of the Wasm bytes to return the version
-    // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid.
-    //
-    // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem
-    // earlier as an `Err` rather than as a panic.
-    version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id))
-}
-
-fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVersion> {
-    if data.len() == 6 {
-        Some(SemanticVersion::new(
-            u16::from_be_bytes([data[0], data[1]]) as _,
-            u16::from_be_bytes([data[2], data[3]]) as _,
-            u16::from_be_bytes([data[4], data[5]]) as _,
-        ))
-    } else {
-        None
-    }
-}
-
 impl WasmExtension {
     pub async fn call<T, Fn>(&self, f: Fn) -> T
     where

crates/workspace/Cargo.toml 🔗

@@ -13,7 +13,6 @@ path = "src/workspace.rs"
 doctest = false
 
 [features]
-no-webrtc = ["call/no-webrtc"]
 test-support = [
     "call/test-support",
     "client/test-support",