mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
Add a schema to extensions, to prevent installing extensions on too old of a Zed version (#9599)
Release Notes: - N/A --------- Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
parent
b1feeb9f29
commit
585e8671e3
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -3482,6 +3482,7 @@ dependencies = [
|
|||||||
"settings",
|
"settings",
|
||||||
"theme",
|
"theme",
|
||||||
"toml 0.8.10",
|
"toml 0.8.10",
|
||||||
|
"url",
|
||||||
"util",
|
"util",
|
||||||
"wasm-encoder",
|
"wasm-encoder",
|
||||||
"wasmparser",
|
"wasmparser",
|
||||||
@ -12630,7 +12631,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed_extension_api"
|
name = "zed_extension_api"
|
||||||
version = "0.1.0"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wit-bindgen",
|
"wit-bindgen",
|
||||||
]
|
]
|
||||||
|
@ -470,7 +470,11 @@ impl Telemetry {
|
|||||||
|
|
||||||
let request = http::Request::builder()
|
let request = http::Request::builder()
|
||||||
.method(Method::POST)
|
.method(Method::POST)
|
||||||
.uri(this.http_client.build_zed_api_url("/telemetry/events"))
|
.uri(
|
||||||
|
this.http_client
|
||||||
|
.build_zed_api_url("/telemetry/events", &[])?
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
.header("Content-Type", "text/plain")
|
.header("Content-Type", "text/plain")
|
||||||
.header("x-zed-checksum", checksum)
|
.header("x-zed-checksum", checksum)
|
||||||
.body(json_bytes.into());
|
.body(json_bytes.into());
|
||||||
|
@ -373,6 +373,7 @@ CREATE TABLE extension_versions (
|
|||||||
authors TEXT NOT NULL,
|
authors TEXT NOT NULL,
|
||||||
repository TEXT NOT NULL,
|
repository TEXT NOT NULL,
|
||||||
description TEXT NOT NULL,
|
description TEXT NOT NULL,
|
||||||
|
schema_version INTEGER NOT NULL DEFAULT 0,
|
||||||
download_count INTEGER NOT NULL DEFAULT 0,
|
download_count INTEGER NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY (extension_id, version)
|
PRIMARY KEY (extension_id, version)
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
ALTER TABLE extension_versions ADD COLUMN schema_version INTEGER NOT NULL DEFAULT 0;
|
@ -12,6 +12,7 @@ use axum::{
|
|||||||
Extension, Json, Router,
|
Extension, Json, Router,
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
|
use rpc::ExtensionApiManifest;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
use time::PrimitiveDateTime;
|
use time::PrimitiveDateTime;
|
||||||
@ -33,6 +34,8 @@ pub fn router() -> Router {
|
|||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GetExtensionsParams {
|
struct GetExtensionsParams {
|
||||||
filter: Option<String>,
|
filter: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
max_schema_version: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@ -51,20 +54,14 @@ struct GetExtensionsResponse {
|
|||||||
pub data: Vec<ExtensionMetadata>,
|
pub data: Vec<ExtensionMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct ExtensionManifest {
|
|
||||||
name: String,
|
|
||||||
version: String,
|
|
||||||
description: Option<String>,
|
|
||||||
authors: Vec<String>,
|
|
||||||
repository: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_extensions(
|
async fn get_extensions(
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
Query(params): Query<GetExtensionsParams>,
|
Query(params): Query<GetExtensionsParams>,
|
||||||
) -> Result<Json<GetExtensionsResponse>> {
|
) -> Result<Json<GetExtensionsResponse>> {
|
||||||
let extensions = app.db.get_extensions(params.filter.as_deref(), 500).await?;
|
let extensions = app
|
||||||
|
.db
|
||||||
|
.get_extensions(params.filter.as_deref(), params.max_schema_version, 500)
|
||||||
|
.await?;
|
||||||
Ok(Json(GetExtensionsResponse { data: extensions }))
|
Ok(Json(GetExtensionsResponse { data: extensions }))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,7 +264,7 @@ async fn fetch_extension_manifest(
|
|||||||
})?
|
})?
|
||||||
.to_vec();
|
.to_vec();
|
||||||
let manifest =
|
let manifest =
|
||||||
serde_json::from_slice::<ExtensionManifest>(&manifest_bytes).with_context(|| {
|
serde_json::from_slice::<ExtensionApiManifest>(&manifest_bytes).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"invalid manifest for extension {extension_id} version {version}: {}",
|
"invalid manifest for extension {extension_id} version {version}: {}",
|
||||||
String::from_utf8_lossy(&manifest_bytes)
|
String::from_utf8_lossy(&manifest_bytes)
|
||||||
@ -287,6 +284,7 @@ async fn fetch_extension_manifest(
|
|||||||
description: manifest.description.unwrap_or_default(),
|
description: manifest.description.unwrap_or_default(),
|
||||||
authors: manifest.authors,
|
authors: manifest.authors,
|
||||||
repository: manifest.repository,
|
repository: manifest.repository,
|
||||||
|
schema_version: manifest.schema_version.unwrap_or(0),
|
||||||
published_at,
|
published_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -731,6 +731,7 @@ pub struct NewExtensionVersion {
|
|||||||
pub description: String,
|
pub description: String,
|
||||||
pub authors: Vec<String>,
|
pub authors: Vec<String>,
|
||||||
pub repository: String,
|
pub repository: String,
|
||||||
|
pub schema_version: i32,
|
||||||
pub published_at: PrimitiveDateTime,
|
pub published_at: PrimitiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,27 +4,28 @@ impl Database {
|
|||||||
pub async fn get_extensions(
|
pub async fn get_extensions(
|
||||||
&self,
|
&self,
|
||||||
filter: Option<&str>,
|
filter: Option<&str>,
|
||||||
|
max_schema_version: i32,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<ExtensionMetadata>> {
|
) -> Result<Vec<ExtensionMetadata>> {
|
||||||
self.transaction(|tx| async move {
|
self.transaction(|tx| async move {
|
||||||
let mut condition = Condition::all();
|
let mut condition = Condition::all().add(
|
||||||
|
extension::Column::LatestVersion
|
||||||
|
.into_expr()
|
||||||
|
.eq(extension_version::Column::Version.into_expr()),
|
||||||
|
);
|
||||||
if let Some(filter) = filter {
|
if let Some(filter) = filter {
|
||||||
let fuzzy_name_filter = Self::fuzzy_like_string(filter);
|
let fuzzy_name_filter = Self::fuzzy_like_string(filter);
|
||||||
condition = condition.add(Expr::cust_with_expr("name ILIKE $1", fuzzy_name_filter));
|
condition = condition.add(Expr::cust_with_expr("name ILIKE $1", fuzzy_name_filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
let extensions = extension::Entity::find()
|
let extensions = extension::Entity::find()
|
||||||
|
.inner_join(extension_version::Entity)
|
||||||
|
.select_also(extension_version::Entity)
|
||||||
.filter(condition)
|
.filter(condition)
|
||||||
|
.filter(extension_version::Column::SchemaVersion.lte(max_schema_version))
|
||||||
.order_by_desc(extension::Column::TotalDownloadCount)
|
.order_by_desc(extension::Column::TotalDownloadCount)
|
||||||
.order_by_asc(extension::Column::Name)
|
.order_by_asc(extension::Column::Name)
|
||||||
.limit(Some(limit as u64))
|
.limit(Some(limit as u64))
|
||||||
.filter(
|
|
||||||
extension::Column::LatestVersion
|
|
||||||
.into_expr()
|
|
||||||
.eq(extension_version::Column::Version.into_expr()),
|
|
||||||
)
|
|
||||||
.inner_join(extension_version::Entity)
|
|
||||||
.select_also(extension_version::Entity)
|
|
||||||
.all(&*tx)
|
.all(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@ -170,6 +171,7 @@ impl Database {
|
|||||||
authors: ActiveValue::Set(version.authors.join(", ")),
|
authors: ActiveValue::Set(version.authors.join(", ")),
|
||||||
repository: ActiveValue::Set(version.repository.clone()),
|
repository: ActiveValue::Set(version.repository.clone()),
|
||||||
description: ActiveValue::Set(version.description.clone()),
|
description: ActiveValue::Set(version.description.clone()),
|
||||||
|
schema_version: ActiveValue::Set(version.schema_version),
|
||||||
download_count: ActiveValue::NotSet,
|
download_count: ActiveValue::NotSet,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
@ -13,6 +13,7 @@ pub struct Model {
|
|||||||
pub authors: String,
|
pub authors: String,
|
||||||
pub repository: String,
|
pub repository: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
pub schema_version: i32,
|
||||||
pub download_count: i64,
|
pub download_count: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
|||||||
let versions = db.get_known_extension_versions().await.unwrap();
|
let versions = db.get_known_extension_versions().await.unwrap();
|
||||||
assert!(versions.is_empty());
|
assert!(versions.is_empty());
|
||||||
|
|
||||||
let extensions = db.get_extensions(None, 5).await.unwrap();
|
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
|
||||||
assert!(extensions.is_empty());
|
assert!(extensions.is_empty());
|
||||||
|
|
||||||
let t0 = OffsetDateTime::from_unix_timestamp_nanos(0).unwrap();
|
let t0 = OffsetDateTime::from_unix_timestamp_nanos(0).unwrap();
|
||||||
@ -33,6 +33,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
|||||||
description: "an extension".into(),
|
description: "an extension".into(),
|
||||||
authors: vec!["max".into()],
|
authors: vec!["max".into()],
|
||||||
repository: "ext1/repo".into(),
|
repository: "ext1/repo".into(),
|
||||||
|
schema_version: 1,
|
||||||
published_at: t0,
|
published_at: t0,
|
||||||
},
|
},
|
||||||
NewExtensionVersion {
|
NewExtensionVersion {
|
||||||
@ -41,6 +42,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
|||||||
description: "a good extension".into(),
|
description: "a good extension".into(),
|
||||||
authors: vec!["max".into(), "marshall".into()],
|
authors: vec!["max".into(), "marshall".into()],
|
||||||
repository: "ext1/repo".into(),
|
repository: "ext1/repo".into(),
|
||||||
|
schema_version: 1,
|
||||||
published_at: t0,
|
published_at: t0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -53,6 +55,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
|||||||
description: "a great extension".into(),
|
description: "a great extension".into(),
|
||||||
authors: vec!["marshall".into()],
|
authors: vec!["marshall".into()],
|
||||||
repository: "ext2/repo".into(),
|
repository: "ext2/repo".into(),
|
||||||
|
schema_version: 0,
|
||||||
published_at: t0,
|
published_at: t0,
|
||||||
}],
|
}],
|
||||||
),
|
),
|
||||||
@ -75,7 +78,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// The latest version of each extension is returned.
|
// The latest version of each extension is returned.
|
||||||
let extensions = db.get_extensions(None, 5).await.unwrap();
|
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
extensions,
|
extensions,
|
||||||
&[
|
&[
|
||||||
@ -102,6 +105,22 @@ async fn test_extensions(db: &Arc<Database>) {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Extensions with too new of a schema version are excluded.
|
||||||
|
let extensions = db.get_extensions(None, 0, 5).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
extensions,
|
||||||
|
&[ExtensionMetadata {
|
||||||
|
id: "ext2".into(),
|
||||||
|
name: "Extension Two".into(),
|
||||||
|
version: "0.2.0".into(),
|
||||||
|
authors: vec!["marshall".into()],
|
||||||
|
description: "a great extension".into(),
|
||||||
|
repository: "ext2/repo".into(),
|
||||||
|
published_at: t0,
|
||||||
|
download_count: 0
|
||||||
|
},]
|
||||||
|
);
|
||||||
|
|
||||||
// Record extensions being downloaded.
|
// Record extensions being downloaded.
|
||||||
for _ in 0..7 {
|
for _ in 0..7 {
|
||||||
assert!(db.record_extension_download("ext2", "0.0.2").await.unwrap());
|
assert!(db.record_extension_download("ext2", "0.0.2").await.unwrap());
|
||||||
@ -122,7 +141,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
|||||||
.unwrap());
|
.unwrap());
|
||||||
|
|
||||||
// Extensions are returned in descending order of total downloads.
|
// Extensions are returned in descending order of total downloads.
|
||||||
let extensions = db.get_extensions(None, 5).await.unwrap();
|
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
extensions,
|
extensions,
|
||||||
&[
|
&[
|
||||||
@ -161,6 +180,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
|||||||
description: "a real good extension".into(),
|
description: "a real good extension".into(),
|
||||||
authors: vec!["max".into(), "marshall".into()],
|
authors: vec!["max".into(), "marshall".into()],
|
||||||
repository: "ext1/repo".into(),
|
repository: "ext1/repo".into(),
|
||||||
|
schema_version: 1,
|
||||||
published_at: t0,
|
published_at: t0,
|
||||||
}],
|
}],
|
||||||
),
|
),
|
||||||
@ -172,6 +192,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
|||||||
description: "an old extension".into(),
|
description: "an old extension".into(),
|
||||||
authors: vec!["marshall".into()],
|
authors: vec!["marshall".into()],
|
||||||
repository: "ext2/repo".into(),
|
repository: "ext2/repo".into(),
|
||||||
|
schema_version: 0,
|
||||||
published_at: t0,
|
published_at: t0,
|
||||||
}],
|
}],
|
||||||
),
|
),
|
||||||
@ -196,7 +217,7 @@ async fn test_extensions(db: &Arc<Database>) {
|
|||||||
.collect()
|
.collect()
|
||||||
);
|
);
|
||||||
|
|
||||||
let extensions = db.get_extensions(None, 5).await.unwrap();
|
let extensions = db.get_extensions(None, 1, 5).await.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
extensions,
|
extensions,
|
||||||
&[
|
&[
|
||||||
|
@ -37,6 +37,7 @@ serde_json.workspace = true
|
|||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
|
url.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
wasm-encoder.workspace = true
|
wasm-encoder.workspace = true
|
||||||
wasmtime.workspace = true
|
wasmtime.workspace = true
|
||||||
|
@ -29,6 +29,7 @@ pub struct ExtensionManifest {
|
|||||||
pub id: Arc<str>,
|
pub id: Arc<str>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version: Arc<str>,
|
pub version: Arc<str>,
|
||||||
|
pub schema_version: i32,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
@ -35,6 +35,7 @@ use std::{
|
|||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use theme::{ThemeRegistry, ThemeSettings};
|
use theme::{ThemeRegistry, ThemeSettings};
|
||||||
|
use url::Url;
|
||||||
use util::{
|
use util::{
|
||||||
http::{AsyncBody, HttpClient, HttpClientWithUrl},
|
http::{AsyncBody, HttpClient, HttpClientWithUrl},
|
||||||
paths::EXTENSIONS_DIR,
|
paths::EXTENSIONS_DIR,
|
||||||
@ -49,6 +50,8 @@ pub use extension_manifest::{
|
|||||||
const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
|
const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
|
||||||
const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
|
const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
|
const CURRENT_SCHEMA_VERSION: i64 = 1;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ExtensionsApiResponse {
|
pub struct ExtensionsApiResponse {
|
||||||
pub data: Vec<ExtensionApiResponse>,
|
pub data: Vec<ExtensionApiResponse>,
|
||||||
@ -377,15 +380,18 @@ impl ExtensionStore {
|
|||||||
search: Option<&str>,
|
search: Option<&str>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Vec<ExtensionApiResponse>>> {
|
) -> Task<Result<Vec<ExtensionApiResponse>>> {
|
||||||
let url = self.http_client.build_zed_api_url(&format!(
|
let version = CURRENT_SCHEMA_VERSION.to_string();
|
||||||
"/extensions{query}",
|
let mut query = vec![("max_schema_version", version.as_str())];
|
||||||
query = search
|
if let Some(search) = search {
|
||||||
.map(|search| format!("?filter={search}"))
|
query.push(("filter", search));
|
||||||
.unwrap_or_default()
|
}
|
||||||
));
|
|
||||||
|
let url = self.http_client.build_zed_api_url("/extensions", &query);
|
||||||
let http_client = self.http_client.clone();
|
let http_client = self.http_client.clone();
|
||||||
cx.spawn(move |_, _| async move {
|
cx.spawn(move |_, _| async move {
|
||||||
let mut response = http_client.get(&url, AsyncBody::empty(), true).await?;
|
let mut response = http_client
|
||||||
|
.get(&url?.as_ref(), AsyncBody::empty(), true)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let mut body = Vec::new();
|
let mut body = Vec::new();
|
||||||
response
|
response
|
||||||
@ -420,7 +426,7 @@ impl ExtensionStore {
|
|||||||
fn install_or_upgrade_extension_at_endpoint(
|
fn install_or_upgrade_extension_at_endpoint(
|
||||||
&mut self,
|
&mut self,
|
||||||
extension_id: Arc<str>,
|
extension_id: Arc<str>,
|
||||||
url: String,
|
url: Url,
|
||||||
operation: ExtensionOperation,
|
operation: ExtensionOperation,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
@ -447,7 +453,7 @@ impl ExtensionStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut response = http_client
|
let mut response = http_client
|
||||||
.get(&url, Default::default(), true)
|
.get(&url.as_ref(), Default::default(), true)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| anyhow!("error downloading extension: {}", err))?;
|
.map_err(|err| anyhow!("error downloading extension: {}", err))?;
|
||||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||||
@ -482,9 +488,13 @@ impl ExtensionStore {
|
|||||||
) {
|
) {
|
||||||
log::info!("installing extension {extension_id} latest version");
|
log::info!("installing extension {extension_id} latest version");
|
||||||
|
|
||||||
let url = self
|
let Some(url) = self
|
||||||
.http_client
|
.http_client
|
||||||
.build_zed_api_url(&format!("/extensions/{extension_id}/download"));
|
.build_zed_api_url(&format!("/extensions/{extension_id}/download"), &[])
|
||||||
|
.log_err()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
self.install_or_upgrade_extension_at_endpoint(
|
self.install_or_upgrade_extension_at_endpoint(
|
||||||
extension_id,
|
extension_id,
|
||||||
@ -511,9 +521,16 @@ impl ExtensionStore {
|
|||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
log::info!("installing extension {extension_id} {version}");
|
log::info!("installing extension {extension_id} {version}");
|
||||||
let url = self
|
let Some(url) = self
|
||||||
.http_client
|
.http_client
|
||||||
.build_zed_api_url(&format!("/extensions/{extension_id}/{version}/download"));
|
.build_zed_api_url(
|
||||||
|
&format!("/extensions/{extension_id}/{version}/download"),
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.log_err()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
self.install_or_upgrade_extension_at_endpoint(extension_id, url, operation, cx);
|
self.install_or_upgrade_extension_at_endpoint(extension_id, url, operation, cx);
|
||||||
}
|
}
|
||||||
@ -1104,6 +1121,7 @@ fn manifest_from_old_manifest(
|
|||||||
description: manifest_json.description,
|
description: manifest_json.description,
|
||||||
repository: manifest_json.repository,
|
repository: manifest_json.repository,
|
||||||
authors: manifest_json.authors,
|
authors: manifest_json.authors,
|
||||||
|
schema_version: 0,
|
||||||
lib: Default::default(),
|
lib: Default::default(),
|
||||||
themes: {
|
themes: {
|
||||||
let mut themes = manifest_json.themes.into_values().collect::<Vec<_>>();
|
let mut themes = manifest_json.themes.into_values().collect::<Vec<_>>();
|
||||||
|
@ -145,6 +145,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||||||
id: "zed-ruby".into(),
|
id: "zed-ruby".into(),
|
||||||
name: "Zed Ruby".into(),
|
name: "Zed Ruby".into(),
|
||||||
version: "1.0.0".into(),
|
version: "1.0.0".into(),
|
||||||
|
schema_version: 0,
|
||||||
description: None,
|
description: None,
|
||||||
authors: Vec::new(),
|
authors: Vec::new(),
|
||||||
repository: None,
|
repository: None,
|
||||||
@ -169,6 +170,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||||||
id: "zed-monokai".into(),
|
id: "zed-monokai".into(),
|
||||||
name: "Zed Monokai".into(),
|
name: "Zed Monokai".into(),
|
||||||
version: "2.0.0".into(),
|
version: "2.0.0".into(),
|
||||||
|
schema_version: 0,
|
||||||
description: None,
|
description: None,
|
||||||
authors: vec![],
|
authors: vec![],
|
||||||
repository: None,
|
repository: None,
|
||||||
@ -324,6 +326,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||||||
id: "zed-gruvbox".into(),
|
id: "zed-gruvbox".into(),
|
||||||
name: "Zed Gruvbox".into(),
|
name: "Zed Gruvbox".into(),
|
||||||
version: "1.0.0".into(),
|
version: "1.0.0".into(),
|
||||||
|
schema_version: 0,
|
||||||
description: None,
|
description: None,
|
||||||
authors: vec![],
|
authors: vec![],
|
||||||
repository: None,
|
repository: None,
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "zed_extension_api"
|
name = "zed_extension_api"
|
||||||
version = "0.1.0"
|
version = "0.0.1"
|
||||||
|
description = "APIs for creating Zed extensions in Rust"
|
||||||
|
repository = "https://github.com/zed-industries/zed"
|
||||||
|
documentation = "https://docs.rs/zed_extension_api"
|
||||||
|
keywords = ["zed", "extension"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
56
crates/extension_api/README.md
Normal file
56
crates/extension_api/README.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# The Zed Rust Extension API
|
||||||
|
|
||||||
|
This crate lets you write extensions for Zed in Rust.
|
||||||
|
|
||||||
|
## Extension Manifest
|
||||||
|
|
||||||
|
You'll need an `extension.toml` file at the root of your extension directory, with the following structure:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
id = "my-extension"
|
||||||
|
name = "My Extension"
|
||||||
|
description = "..."
|
||||||
|
version = "0.0.1"
|
||||||
|
schema_version = 1
|
||||||
|
authors = ["Your Name <you@example.com>"]
|
||||||
|
repository = "https://github.com/your/extension-repository"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cargo metadata
|
||||||
|
|
||||||
|
Zed extensions are packaged as WebAssembly files. In your Cargo.toml, you'll
|
||||||
|
need to set your `crate-type` accordingly:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
zed_extension_api = "0.0.1"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementing an Extension
|
||||||
|
|
||||||
|
To define your extension, create a type that implements the `Extension` trait, and register it.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use zed_extension_api as zed;
|
||||||
|
|
||||||
|
struct MyExtension {
|
||||||
|
// ... state
|
||||||
|
}
|
||||||
|
|
||||||
|
impl zed::Extension for MyExtension {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
zed::register_extension!(MyExtension);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing your extension
|
||||||
|
|
||||||
|
To run your extension in Zed as you're developing it:
|
||||||
|
|
||||||
|
- Open the extensions view using the `zed: extensions` action in the command palette.
|
||||||
|
- Click the `Add Dev Extension` button in the top right
|
||||||
|
- Choose the path to your extension directory.
|
@ -94,6 +94,7 @@ async fn main() -> Result<()> {
|
|||||||
version: manifest.version.to_string(),
|
version: manifest.version.to_string(),
|
||||||
description: manifest.description,
|
description: manifest.description,
|
||||||
authors: manifest.authors,
|
authors: manifest.authors,
|
||||||
|
schema_version: Some(manifest.schema_version),
|
||||||
repository: manifest
|
repository: manifest
|
||||||
.repository
|
.repository
|
||||||
.ok_or_else(|| anyhow!("missing repository in extension manifest"))?,
|
.ok_or_else(|| anyhow!("missing repository in extension manifest"))?,
|
||||||
|
@ -684,7 +684,7 @@ impl Drop for RoomDelegate {
|
|||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
CFRelease(self.native_delegate.0);
|
CFRelease(self.native_delegate.0);
|
||||||
let _ = Weak::from_raw(self.weak_room);
|
let _ = Weak::from_raw(self.weak_room as *mut Room);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,4 +7,5 @@ pub struct ExtensionApiManifest {
|
|||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub authors: Vec<String>,
|
pub authors: Vec<String>,
|
||||||
pub repository: String,
|
pub repository: String,
|
||||||
|
pub schema_version: Option<i32>,
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ impl HttpClientWithUrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a Zed API URL using the given path.
|
/// Builds a Zed API URL using the given path.
|
||||||
pub fn build_zed_api_url(&self, path: &str) -> String {
|
pub fn build_zed_api_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||||
let base_url = self.base_url();
|
let base_url = self.base_url();
|
||||||
let base_api_url = match base_url.as_ref() {
|
let base_api_url = match base_url.as_ref() {
|
||||||
"https://zed.dev" => "https://api.zed.dev",
|
"https://zed.dev" => "https://api.zed.dev",
|
||||||
@ -63,7 +63,10 @@ impl HttpClientWithUrl {
|
|||||||
other => other,
|
other => other,
|
||||||
};
|
};
|
||||||
|
|
||||||
format!("{}{}", base_api_url, path)
|
Ok(Url::parse_with_params(
|
||||||
|
&format!("{}{}", base_api_url, path),
|
||||||
|
query,
|
||||||
|
)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -783,7 +783,7 @@ async fn upload_previous_crashes(
|
|||||||
.unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this.
|
.unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this.
|
||||||
let mut uploaded = last_uploaded.clone();
|
let mut uploaded = last_uploaded.clone();
|
||||||
|
|
||||||
let crash_report_url = http.build_zed_api_url("/telemetry/crashes");
|
let crash_report_url = http.build_zed_api_url("/telemetry/crashes", &[])?;
|
||||||
|
|
||||||
for dir in [&*CRASHES_DIR, &*CRASHES_RETIRED_DIR] {
|
for dir in [&*CRASHES_DIR, &*CRASHES_RETIRED_DIR] {
|
||||||
let mut children = smol::fs::read_dir(&dir).await?;
|
let mut children = smol::fs::read_dir(&dir).await?;
|
||||||
@ -809,7 +809,7 @@ async fn upload_previous_crashes(
|
|||||||
.await
|
.await
|
||||||
.context("error reading crash file")?;
|
.context("error reading crash file")?;
|
||||||
|
|
||||||
let mut request = Request::post(&crash_report_url)
|
let mut request = Request::post(&crash_report_url.to_string())
|
||||||
.redirect_policy(isahc::config::RedirectPolicy::Follow)
|
.redirect_policy(isahc::config::RedirectPolicy::Follow)
|
||||||
.header("Content-Type", "text/plain");
|
.header("Content-Type", "text/plain");
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ id = "gleam"
|
|||||||
name = "Gleam"
|
name = "Gleam"
|
||||||
description = "Gleam support for Zed"
|
description = "Gleam support for Zed"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
schema_version = 1
|
||||||
authors = ["Marshall Bowers <elliott.codes@gmail.com>"]
|
authors = ["Marshall Bowers <elliott.codes@gmail.com>"]
|
||||||
repository = "https://github.com/zed-industries/zed"
|
repository = "https://github.com/zed-industries/zed"
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ id = "uiua"
|
|||||||
name = "Uiua"
|
name = "Uiua"
|
||||||
description = "Uiua support for Zed"
|
description = "Uiua support for Zed"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
schema_version = 1
|
||||||
authors = ["Max Brunsfeld <max@zed.dev>"]
|
authors = ["Max Brunsfeld <max@zed.dev>"]
|
||||||
repository = "https://github.com/zed-industries/zed"
|
repository = "https://github.com/zed-industries/zed"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user