extension_cli: Include the list of what an extension provides in the generated manifest (#24295)

Marshall Bowers created

This PR updates the Zed extension CLI with support for populating the
`provides` field in the generated extension manifest.

This field will contain the set of features that the extension provides.

For example:

```
"provides": ["themes", "icon-themes"]
```

Release Notes:

- N/A

Change summary

crates/collab/src/db/queries/extensions.rs    |  2 
crates/collab/src/db/tests/extension_tests.rs | 12 +++
crates/extension_cli/src/main.rs              | 61 +++++++++++++++++---
crates/rpc/src/extension.rs                   | 18 ++++++
4 files changed, 81 insertions(+), 12 deletions(-)

Detailed changes

crates/collab/src/db/queries/extensions.rs 🔗

@@ -1,3 +1,4 @@
+use std::collections::BTreeSet;
 use std::str::FromStr;
 
 use chrono::Utc;
@@ -370,6 +371,7 @@ fn metadata_from_extension_and_version(
             repository: version.repository,
             schema_version: Some(version.schema_version),
             wasm_api_version: version.wasm_api_version,
+            provides: BTreeSet::default(),
         },
 
         published_at: convert_time_to_chrono(version.published_at),

crates/collab/src/db/tests/extension_tests.rs 🔗

@@ -1,10 +1,12 @@
+use std::collections::BTreeSet;
+use std::sync::Arc;
+
 use super::Database;
 use crate::db::ExtensionVersionConstraints;
 use crate::{
     db::{queries::extensions::convert_time_to_chrono, ExtensionMetadata, NewExtensionVersion},
     test_both_dbs,
 };
-use std::sync::Arc;
 
 test_both_dbs!(
     test_extensions,
@@ -97,6 +99,7 @@ async fn test_extensions(db: &Arc<Database>) {
                     repository: "ext1/repo".into(),
                     schema_version: Some(1),
                     wasm_api_version: None,
+                    provides: BTreeSet::default(),
                 },
                 published_at: t0_chrono,
                 download_count: 0,
@@ -111,6 +114,7 @@ async fn test_extensions(db: &Arc<Database>) {
                     repository: "ext2/repo".into(),
                     schema_version: Some(0),
                     wasm_api_version: None,
+                    provides: BTreeSet::default(),
                 },
                 published_at: t0_chrono,
                 download_count: 0
@@ -132,6 +136,7 @@ async fn test_extensions(db: &Arc<Database>) {
                 repository: "ext2/repo".into(),
                 schema_version: Some(0),
                 wasm_api_version: None,
+                provides: BTreeSet::default(),
             },
             published_at: t0_chrono,
             download_count: 0
@@ -172,6 +177,7 @@ async fn test_extensions(db: &Arc<Database>) {
                     repository: "ext2/repo".into(),
                     schema_version: Some(0),
                     wasm_api_version: None,
+                    provides: BTreeSet::default(),
                 },
                 published_at: t0_chrono,
                 download_count: 7
@@ -186,6 +192,7 @@ async fn test_extensions(db: &Arc<Database>) {
                     repository: "ext1/repo".into(),
                     schema_version: Some(1),
                     wasm_api_version: None,
+                    provides: BTreeSet::default(),
                 },
                 published_at: t0_chrono,
                 download_count: 5,
@@ -258,6 +265,7 @@ async fn test_extensions(db: &Arc<Database>) {
                     repository: "ext2/repo".into(),
                     schema_version: Some(0),
                     wasm_api_version: None,
+                    provides: BTreeSet::default(),
                 },
                 published_at: t0_chrono,
                 download_count: 7
@@ -272,6 +280,7 @@ async fn test_extensions(db: &Arc<Database>) {
                     repository: "ext1/repo".into(),
                     schema_version: Some(1),
                     wasm_api_version: None,
+                    provides: BTreeSet::default(),
                 },
                 published_at: t0_chrono,
                 download_count: 5,
@@ -378,6 +387,7 @@ async fn test_extensions_by_id(db: &Arc<Database>) {
                 repository: "ext1/repo".into(),
                 schema_version: Some(1),
                 wasm_api_version: Some("0.0.4".into()),
+                provides: BTreeSet::default(),
             },
             published_at: t0_chrono,
             download_count: 0,

crates/extension_cli/src/main.rs 🔗

@@ -1,20 +1,18 @@
-use std::{
-    collections::HashMap,
-    env, fs,
-    path::{Path, PathBuf},
-    process::Command,
-    sync::Arc,
-};
+use std::collections::{BTreeSet, HashMap};
+use std::env;
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::process::Command;
+use std::sync::Arc;
 
 use ::fs::{copy_recursive, CopyOptions, Fs, RealFs};
 use anyhow::{anyhow, bail, Context, Result};
 use clap::Parser;
-use extension::{
-    extension_builder::{CompileExtensionOptions, ExtensionBuilder},
-    ExtensionManifest,
-};
+use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
+use extension::ExtensionManifest;
 use language::LanguageConfig;
 use reqwest_client::ReqwestClient;
+use rpc::ExtensionProvides;
 use tree_sitter::{Language, Query, WasmStore};
 
 #[derive(Parser, Debug)]
@@ -99,6 +97,8 @@ async fn main() -> Result<()> {
         );
     }
 
+    let extension_provides = extension_provides(&manifest);
+
     let manifest_json = serde_json::to_string(&rpc::ExtensionApiManifest {
         name: manifest.name,
         version: manifest.version,
@@ -109,6 +109,7 @@ async fn main() -> Result<()> {
             .repository
             .ok_or_else(|| anyhow!("missing repository in extension manifest"))?,
         wasm_api_version: manifest.lib.version.map(|version| version.to_string()),
+        provides: extension_provides,
     })?;
     fs::remove_dir_all(&archive_dir)?;
     fs::write(output_dir.join("manifest.json"), manifest_json.as_bytes())?;
@@ -116,6 +117,44 @@ async fn main() -> Result<()> {
     Ok(())
 }
 
+/// Returns the set of features provided by the extension.
+fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet<ExtensionProvides> {
+    let mut provides = BTreeSet::default();
+    if !manifest.themes.is_empty() {
+        provides.insert(ExtensionProvides::Themes);
+    }
+
+    if !manifest.icon_themes.is_empty() {
+        provides.insert(ExtensionProvides::IconThemes);
+    }
+
+    if !manifest.languages.is_empty() {
+        provides.insert(ExtensionProvides::Languages);
+    }
+
+    if !manifest.grammars.is_empty() {
+        provides.insert(ExtensionProvides::Grammars);
+    }
+
+    if !manifest.language_servers.is_empty() {
+        provides.insert(ExtensionProvides::LanguageServers);
+    }
+
+    if !manifest.context_servers.is_empty() {
+        provides.insert(ExtensionProvides::ContextServers);
+    }
+
+    if !manifest.indexed_docs_providers.is_empty() {
+        provides.insert(ExtensionProvides::IndexedDocsProviders);
+    }
+
+    if manifest.snippets.is_some() {
+        provides.insert(ExtensionProvides::Snippets);
+    }
+
+    provides
+}
+
 async fn copy_extension_resources(
     manifest: &ExtensionManifest,
     extension_path: &Path,

crates/rpc/src/extension.rs 🔗

@@ -1,3 +1,5 @@
+use std::collections::BTreeSet;
+
 use chrono::{DateTime, Utc};
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
@@ -11,6 +13,22 @@ pub struct ExtensionApiManifest {
     pub repository: String,
     pub schema_version: Option<i32>,
     pub wasm_api_version: Option<String>,
+    #[serde(default)]
+    pub provides: BTreeSet<ExtensionProvides>,
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum ExtensionProvides {
+    Themes,
+    IconThemes,
+    Languages,
+    Grammars,
+    LanguageServers,
+    ContextServers,
+    SlashCommands,
+    IndexedDocsProviders,
+    Snippets,
 }
 
 #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]