Files
zed/crates/extension_cli/src/main.rs
T
yenru0 fe62a99bf5 gwen: Purge remaining Zed branding from user-facing surfaces
Sweep every user-visible product string to Gwen: app menus (Quit,
Hide, Gwen Repository), feedback links (repo/issues now point at
github.com/kk376/zed, email to yenru@yrkaierx.com), move-to-Applications
and update dialogs, provider and settings copy, CLI help, installer and
desktop/flatpak/snap/plist metadata.

Rename user-facing settings values with back-compat aliases: base_keymap
"Gwen" (accepts "Zed"), fonts ".GwenMono"/".GwenSans" (accept
".ZedMono"/".ZedSans"), icon theme "Gwen (Default)". Fix F10
app-menu keymap arguments to "Gwen" and align the Windows installer
AppMutex with release_channel::app_identifier(). Internal identifiers,
wire protocol names, upstream service URLs and legal attribution are
intentionally kept.
2026-09-29 09:24:34 +09:00

978 lines
33 KiB
Rust

use std::collections::BTreeSet;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr as _;
use std::sync::Arc;
use ::fs::{CopyOptions, Fs, RealFs, RemoveOptions, copy_recursive};
use anyhow::{Context as _, Result, anyhow, bail};
use clap::Parser;
use cloud_api_types::ExtensionProvides;
use extension::build_debug_adapter_schema_path;
use extension::extension_builder::CompilationConcurrency;
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
use extension::{ExtensionManifest, ExtensionSnippets};
use http_client::Url;
use language::LanguageConfig;
use language::QueryFile;
use reqwest_client::ReqwestClient;
use settings_content::SemanticTokenRules;
use snippet_provider::file_to_snippets;
use snippet_provider::format::VsSnippetsFile;
use task::TaskTemplates;
use tokio::process::Command;
use tree_sitter::{Language, Query, WasmStore};
struct TestingContext {
known_resources: BTreeSet<PathBuf>,
}
impl TestingContext {
fn new(manifest: &ExtensionManifest, extension_path: &Path) -> Self {
let known_resources = manifest
.snippets
.as_ref()
.map(ExtensionSnippets::paths)
.into_iter()
.flatten()
.map(|relative_path| extension_path.join(relative_path))
.collect();
Self { known_resources }
}
fn is_known_resource(&self, path: &Path) -> bool {
self.known_resources.contains(path)
}
}
#[derive(Parser, Debug)]
#[command(name = "zed-extension")]
struct Args {
/// The path to the extension directory
#[arg(long)]
source_dir: PathBuf,
/// The output directory to place the packaged extension.
#[arg(long)]
output_dir: PathBuf,
/// The path to a directory where build dependencies are downloaded
#[arg(long)]
scratch_dir: PathBuf,
}
#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();
let args = Args::parse();
let fs = RealFs::new(None, gpui_platform::background_executor());
let engine = wasmtime::Engine::default();
let mut wasm_store = WasmStore::new(&engine)?;
let extension_path = args
.source_dir
.canonicalize()
.context("failed to canonicalize source_dir")?;
fs.create_dir(&args.scratch_dir)
.await
.context("failed to create scratch dir")?;
let scratch_dir = args
.scratch_dir
.canonicalize()
.context("failed to canonicalize scratch_dir")?;
let output_dir = if args.output_dir.is_relative() {
env::current_dir()?.join(&args.output_dir)
} else {
args.output_dir
};
log::info!("loading extension manifest");
let mut manifest = ExtensionManifest::load(fs.clone(), &extension_path).await?;
log::info!("compiling extension");
let user_agent = format!(
"Gwen Extension CLI/{} ({}; {})",
env!("CARGO_PKG_VERSION"),
std::env::consts::OS,
std::env::consts::ARCH
);
let http_client = Arc::new(ReqwestClient::user_agent(&user_agent)?);
let builder = ExtensionBuilder::new(http_client, scratch_dir);
builder
.compile_extension(
&extension_path,
&mut manifest,
CompileExtensionOptions {
release: true,
max_concurrency: CompilationConcurrency::Unbounded,
},
fs.clone(),
)
.await
.context("failed to compile extension")?;
validate_extension_manifest(&manifest)?;
let extension_provides = manifest.provides();
validate_extension_features(&extension_provides)?;
let cx = TestingContext::new(&manifest, &extension_path);
let grammars = test_grammars(&manifest, &extension_path, &mut wasm_store)?;
test_languages(&manifest, &extension_path, &grammars, &cx)?;
test_themes(&manifest, &extension_path, fs.clone()).await?;
test_snippets(&manifest, &extension_path, fs.clone()).await?;
test_debug_adapter_schemas(&manifest, &extension_path, fs.clone()).await?;
let archive_dir = output_dir.join("archive");
fs.remove_dir(
&archive_dir,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await
.ok();
copy_extension_resources(&manifest, &extension_path, &archive_dir, fs.clone())
.await
.context("failed to copy extension resources")?;
let tar_output = Command::new("tar")
.current_dir(&output_dir)
.args(["-czvf", "archive.tar.gz", "-C", "archive", "."])
.output()
.await
.context("failed to run tar")?;
if !tar_output.status.success() {
bail!(
"failed to create archive.tar.gz: {}",
String::from_utf8_lossy(&tar_output.stderr)
);
}
let manifest_json = serde_json::to_string(&cloud_api_types::ExtensionApiManifest {
name: manifest.name,
version: manifest.version,
description: manifest.description,
authors: manifest.authors,
schema_version: Some(manifest.schema_version.0),
repository: manifest
.repository
.context("missing repository in extension manifest")?,
wasm_api_version: manifest.lib.version.map(|version| version.to_string()),
provides: extension_provides,
})?;
fs.remove_dir(
&archive_dir,
RemoveOptions {
recursive: true,
ignore_if_not_exists: false,
},
)
.await?;
fs.write(&output_dir.join("manifest.json"), manifest_json.as_bytes())
.await?;
Ok(())
}
async fn copy_extension_resources(
manifest: &ExtensionManifest,
extension_path: &Path,
output_dir: &Path,
fs: Arc<dyn Fs>,
) -> Result<()> {
fs.create_dir(output_dir)
.await
.context("failed to create output dir")?;
let manifest_toml = toml::to_string(&manifest).context("failed to serialize manifest")?;
fs.write(&output_dir.join("extension.toml"), manifest_toml.as_bytes())
.await
.context("failed to write extension.toml")?;
if manifest.lib.kind.is_some() {
fs.copy_file(
&extension_path.join("extension.wasm"),
&output_dir.join("extension.wasm"),
CopyOptions {
overwrite: true,
ignore_if_exists: false,
},
)
.await
.context("failed to copy extension.wasm")?;
}
if !manifest.grammars.is_empty() {
let source_grammars_dir = extension_path.join("grammars");
let output_grammars_dir = output_dir.join("grammars");
fs.create_dir(&output_grammars_dir).await?;
futures::future::try_join_all(manifest.grammars.keys().map(|grammar_name| {
let fs = fs.clone();
let source_grammars_dir = source_grammars_dir.as_path();
let output_grammars_dir = output_grammars_dir.as_path();
async move {
let mut grammar_filename = PathBuf::from(grammar_name.as_ref());
grammar_filename.set_extension("wasm");
fs.copy_file(
&source_grammars_dir.join(&grammar_filename),
&output_grammars_dir.join(&grammar_filename),
CopyOptions {
overwrite: true,
ignore_if_exists: false,
},
)
.await
.with_context(|| format!("failed to copy grammar '{}'", grammar_filename.display()))
}
}))
.await?;
}
if !manifest.themes.is_empty() {
let output_themes_dir = output_dir.join("themes");
fs.create_dir(&output_themes_dir).await?;
futures::future::try_join_all(manifest.themes.iter().map(|theme_path| {
let fs = fs.clone();
let output_themes_dir = output_themes_dir.as_path();
async move {
let theme_path = theme_path.as_std_path();
fs.copy_file(
&extension_path.join(theme_path),
&output_themes_dir.join(theme_path.file_name().context("invalid theme path")?),
CopyOptions {
overwrite: true,
ignore_if_exists: false,
},
)
.await
.with_context(|| format!("failed to copy theme '{}'", theme_path.display()))
}
}))
.await?;
}
if !manifest.icon_themes.is_empty() {
let output_icon_themes_dir = output_dir.join("icon_themes");
fs.create_dir(&output_icon_themes_dir).await?;
futures::future::try_join_all(manifest.icon_themes.iter().map(|icon_theme_path| {
let fs = fs.clone();
let output_icon_themes_dir = output_icon_themes_dir.as_path();
async move {
let icon_theme_path = icon_theme_path.as_std_path();
fs.copy_file(
&extension_path.join(icon_theme_path),
&output_icon_themes_dir.join(
icon_theme_path
.file_name()
.context("invalid icon theme path")?,
),
CopyOptions {
overwrite: true,
ignore_if_exists: false,
},
)
.await
.with_context(|| {
format!("failed to copy icon theme '{}'", icon_theme_path.display())
})
}
}))
.await?;
let output_icons_dir = output_dir.join("icons");
fs.create_dir(&output_icons_dir).await?;
copy_recursive(
fs.as_ref(),
&extension_path.join("icons"),
&output_icons_dir,
CopyOptions {
overwrite: true,
ignore_if_exists: false,
},
)
.await
.context("failed to copy icons")?;
}
if !manifest.languages.is_empty() {
let output_languages_dir = output_dir.join("languages");
fs.create_dir(&output_languages_dir).await?;
futures::future::try_join_all(manifest.languages.iter().map(|language_path| {
let fs = fs.clone();
let output_languages_dir = output_languages_dir.clone();
async move {
let language_path = language_path.as_std_path();
copy_recursive(
fs.as_ref(),
&extension_path.join(language_path),
&output_languages_dir
.join(language_path.file_name().context("invalid language path")?),
CopyOptions {
overwrite: true,
ignore_if_exists: false,
},
)
.await
.with_context(|| {
format!("failed to copy language dir '{}'", language_path.display())
})
}
}))
.await?;
}
if !manifest.debug_adapters.is_empty() {
futures::future::try_join_all(manifest.debug_adapters.iter().map(
|(debug_adapter, entry)| {
let fs = fs.clone();
let debug_adapter = debug_adapter.clone();
async move {
let schema_path =
extension::build_debug_adapter_schema_path(&debug_adapter, &entry)?;
let parent = schema_path.parent().with_context(|| {
format!("invalid empty schema path for {debug_adapter}")
})?;
let schema_path = schema_path.as_std_path();
fs.create_dir(&output_dir.join(parent)).await?;
copy_recursive(
fs.as_ref(),
&extension_path.join(schema_path),
&output_dir.join(schema_path),
CopyOptions {
overwrite: true,
ignore_if_exists: false,
},
)
.await
.with_context(|| {
format!(
"failed to copy debug adapter schema '{}'",
schema_path.display(),
)
})
}
},
))
.await?;
}
if let Some(snippets) = manifest.snippets.as_ref() {
futures::future::try_join_all(snippets.paths().map(|snippets_path| {
let fs = fs.clone();
async move {
let parent = snippets_path.parent();
if let Some(parent) = parent.filter(|p| p.components().next().is_some()) {
fs.create_dir(&output_dir.join(parent)).await?;
}
copy_recursive(
fs.as_ref(),
&extension_path.join(&snippets_path),
&output_dir.join(&snippets_path),
CopyOptions {
overwrite: true,
ignore_if_exists: false,
},
)
.await
.with_context(|| {
format!("failed to copy snippets from '{}'", snippets_path.display())
})
}
}))
.await?;
}
Ok(())
}
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
enum ExtensionFeatureError {
#[error("extension does not provide any features")]
NoFeatures,
#[error("extension must not provide other features along with themes")]
ThemesMixedWithOtherFeatures,
#[error("extension must not provide other features along with icon themes")]
IconThemesMixedWithOtherFeatures,
#[error(
"Slash commands have been deprecated and \
the slash command API will be removed in a future release. {}",
if *.sole_feature {
"Slash command extensions will no longer be accepted at this time."
} else {
"Please remove any slash-command related code from your extension."
}
)]
SlashCommandsDeprecated { sole_feature: bool },
}
fn validate_extension_features(
provides: &BTreeSet<ExtensionProvides>,
) -> Result<(), ExtensionFeatureError> {
if provides.is_empty() {
return Err(ExtensionFeatureError::NoFeatures);
}
let provides_single_feature = provides.len() == 1;
if provides.contains(&ExtensionProvides::Themes) && !provides_single_feature {
return Err(ExtensionFeatureError::ThemesMixedWithOtherFeatures);
}
if provides.contains(&ExtensionProvides::IconThemes) && !provides_single_feature {
return Err(ExtensionFeatureError::IconThemesMixedWithOtherFeatures);
}
if provides.contains(&ExtensionProvides::SlashCommands) {
return Err(ExtensionFeatureError::SlashCommandsDeprecated {
sole_feature: provides_single_feature,
});
}
Ok(())
}
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
enum ExtensionManifestValidationError {
#[error("extension manifest must specify a name")]
MissingName,
#[error("extension manifest must specify a description")]
MissingDescription,
#[error("extension manifest description must be more expressive than the name")]
DescriptionNotLongerThanName,
#[error("extension manifest must specify at least one author")]
MissingAuthors,
#[error("extension manifest must specify a repository")]
MissingRepository,
#[error("extension manifest repository is not a valid URL: {0}")]
InvalidRepository(String),
#[error(
"extension manifest must not provide language model providers, \
as these are currently unsupported"
)]
LanguageModelProvidersUnsupported,
}
fn validate_extension_manifest(
manifest: &ExtensionManifest,
) -> Result<(), ExtensionManifestValidationError> {
if manifest.name.trim().is_empty() {
return Err(ExtensionManifestValidationError::MissingName);
}
let description_is_empty = manifest
.description
.as_ref()
.is_none_or(|description| description.trim().is_empty());
if description_is_empty {
return Err(ExtensionManifestValidationError::MissingDescription);
}
let description_is_longer_than_name =
manifest.description.as_ref().is_some_and(|description| {
description.trim().chars().count() > manifest.name.trim().chars().count()
});
if !description_is_longer_than_name {
return Err(ExtensionManifestValidationError::DescriptionNotLongerThanName);
}
if manifest
.authors
.iter()
.all(|author| author.trim().is_empty())
{
return Err(ExtensionManifestValidationError::MissingAuthors);
}
let repository = manifest
.repository
.as_ref()
.map(|repository| repository.trim())
.filter(|repository| !repository.is_empty())
.ok_or(ExtensionManifestValidationError::MissingRepository)?;
let repository_url = repository
.parse::<Url>()
.map_err(|_| ExtensionManifestValidationError::InvalidRepository(repository.to_string()))?;
if repository_url.host_str().is_none() {
return Err(ExtensionManifestValidationError::InvalidRepository(
repository.to_string(),
));
};
if !manifest.language_model_providers.is_empty() {
return Err(ExtensionManifestValidationError::LanguageModelProvidersUnsupported);
}
Ok(())
}
fn test_grammars(
manifest: &ExtensionManifest,
extension_path: &Path,
wasm_store: &mut WasmStore,
) -> Result<HashMap<String, Language>> {
let mut grammars = HashMap::default();
let grammars_dir = extension_path.join("grammars");
for grammar_name in manifest.grammars.keys() {
let mut grammar_path = grammars_dir.join(grammar_name.as_ref());
grammar_path.set_extension("wasm");
let wasm = fs::read(&grammar_path)?;
let language = wasm_store.load_language(grammar_name, &wasm)?;
log::info!("loaded grammar {grammar_name}");
grammars.insert(grammar_name.to_string(), language);
}
Ok(grammars)
}
fn test_languages(
manifest: &ExtensionManifest,
extension_path: &Path,
grammars: &HashMap<String, Language>,
context: &TestingContext,
) -> Result<()> {
for relative_language_dir in &manifest.languages {
let language_dir = extension_path.join(relative_language_dir);
let config_path = language_dir.join(LanguageConfig::FILE_NAME);
let config = LanguageConfig::load(&config_path)?;
let grammar = if let Some(name) = &config.grammar {
Some(
grammars
.get(name.as_ref())
.with_context(|| format!("grammar not found: '{name}'"))?,
)
} else {
None
};
let query_entries = fs::read_dir(&language_dir)?;
for entry in query_entries {
let entry = entry?;
let file_path = entry.path();
let Some(file_name) = file_path.file_name().and_then(|name| name.to_str()) else {
continue;
};
match file_name {
LanguageConfig::FILE_NAME => {
// Loaded above
}
SemanticTokenRules::FILE_NAME => {
let _token_rules = SemanticTokenRules::load(&file_path)?;
}
TaskTemplates::FILE_NAME => {
let task_file_content = std::fs::read(&file_path).with_context(|| {
anyhow!(
"Failed to read tasks file at {path}",
path = file_path.display()
)
})?;
let _task_templates =
serde_json_lenient::from_slice::<TaskTemplates>(&task_file_content)
.with_context(|| {
anyhow!(
"Failed to parse tasks file at {path}",
path = file_path.display()
)
})?;
}
_ => {
if let Ok(_query) = file_name.parse::<QueryFile>() {
let grammar = grammar.with_context(|| {
format! {
"language {} provides query {} but no grammar",
config.name,
file_path.display()
}
})?;
let query_source = fs::read_to_string(&file_path)?;
let _query = Query::new(grammar, &query_source)?;
} else if file_name.ends_with(".scm") {
bail!("query {file_name} is not supported by Gwen and should be removed")
} else if !context.is_known_resource(&file_path) {
bail!(
"'{file_name}' is not a supported file in a language directory and should be removed"
)
}
}
}
}
log::info!("loaded language {}", config.name);
}
Ok(())
}
async fn test_themes(
manifest: &ExtensionManifest,
extension_path: &Path,
fs: Arc<dyn Fs>,
) -> Result<()> {
for relative_theme_path in &manifest.themes {
let theme_path = extension_path.join(relative_theme_path);
let theme_family =
theme_settings::deserialize_user_theme(&fs.load_bytes(&theme_path).await?)?;
log::info!("loaded theme family {}", theme_family.name);
for theme in &theme_family.themes {
if theme
.style
.colors
.deprecated_scrollbar_thumb_background
.is_some()
{
bail!(
r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#,
theme_name = theme.name
)
}
}
}
Ok(())
}
async fn test_snippets(
manifest: &ExtensionManifest,
extension_path: &Path,
fs: Arc<dyn Fs>,
) -> Result<()> {
for relative_snippet_path in manifest
.snippets
.as_ref()
.map(ExtensionSnippets::paths)
.into_iter()
.flatten()
{
let snippet_path = extension_path.join(relative_snippet_path);
let snippets_content = fs.load_bytes(&snippet_path).await?;
let snippets_file = serde_json_lenient::from_slice::<VsSnippetsFile>(&snippets_content)
.with_context(|| anyhow!("Failed to parse snippet file at {snippet_path:?}"))?;
let snippet_errors = file_to_snippets(snippets_file, &snippet_path)
.flat_map(Result::err)
.collect::<Vec<_>>();
let error_count = snippet_errors.len();
anyhow::ensure!(
error_count == 0,
"Could not parse {error_count} snippet{suffix} in file {snippet_path:?}:\n\n{snippet_errors}",
suffix = if error_count == 1 { "" } else { "s" },
snippet_errors = snippet_errors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
);
}
Ok(())
}
async fn test_debug_adapter_schemas(
manifest: &ExtensionManifest,
extension_path: &Path,
fs: Arc<dyn Fs>,
) -> Result<()> {
futures::future::try_join_all(manifest.debug_adapters.iter().map(
|(debug_adapter_name, meta)| {
let fs = fs.clone();
async move {
let debug_adapter_schema_path =
extension_path.join(build_debug_adapter_schema_path(debug_adapter_name, meta)?);
let debug_adapter_schema =
fs.load(&debug_adapter_schema_path).await.with_context(|| {
anyhow::anyhow!(
"failed to read debug adapter schema for \
`{debug_adapter_name}` from `{debug_adapter_schema_path:?}`"
)
})?;
_ = serde_json::Value::from_str(&debug_adapter_schema).with_context(|| {
anyhow::anyhow!(
"Debug adapter schema for `{debug_adapter_name}`\
(path: `{debug_adapter_schema_path:?}`) is not a valid JSON"
)
})?;
Ok(())
}
},
))
.await
.map(|_| ())
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use cloud_api_types::ExtensionProvides;
use extension::{LanguageModelProviderManifestEntry, SchemaVersion};
use super::*;
fn valid_manifest() -> ExtensionManifest {
ExtensionManifest {
id: "test".into(),
name: "Test Extension".to_string(),
version: "1.0.0".into(),
schema_version: SchemaVersion::ZERO,
description: Some("A test extension".to_string()),
repository: Some("https://github.com/zed-industries/zed".to_string()),
authors: vec!["Zed".to_string()],
lib: Default::default(),
themes: Vec::new(),
icon_themes: Vec::new(),
languages: Vec::new(),
grammars: BTreeMap::default(),
language_servers: BTreeMap::default(),
context_servers: BTreeMap::default(),
slash_commands: BTreeMap::default(),
snippets: None,
capabilities: Vec::new(),
debug_adapters: BTreeMap::default(),
debug_locators: BTreeMap::default(),
language_model_providers: BTreeMap::default(),
}
}
#[test]
fn test_validate_manifest_valid() {
assert!(validate_extension_manifest(&valid_manifest()).is_ok());
}
#[test]
fn test_validate_manifest_missing_name() {
let manifest = ExtensionManifest {
name: " ".to_string(),
..valid_manifest()
};
assert_eq!(
validate_extension_manifest(&manifest),
Err(ExtensionManifestValidationError::MissingName),
);
}
#[test]
fn test_validate_manifest_missing_description() {
let manifest = ExtensionManifest {
description: None,
..valid_manifest()
};
assert_eq!(
validate_extension_manifest(&manifest),
Err(ExtensionManifestValidationError::MissingDescription),
);
}
#[test]
fn test_validate_manifest_empty_description() {
let manifest = ExtensionManifest {
description: Some(" ".to_string()),
..valid_manifest()
};
assert_eq!(
validate_extension_manifest(&manifest),
Err(ExtensionManifestValidationError::MissingDescription),
);
}
#[test]
fn test_validate_manifest_description_equal_to_name() {
let manifest = ExtensionManifest {
description: Some("Test Extension".to_string()),
..valid_manifest()
};
assert_eq!(
validate_extension_manifest(&manifest),
Err(ExtensionManifestValidationError::DescriptionNotLongerThanName),
);
}
#[test]
fn test_validate_manifest_description_shorter_than_name() {
let manifest = ExtensionManifest {
description: Some("Test".to_string()),
..valid_manifest()
};
assert_eq!(
validate_extension_manifest(&manifest),
Err(ExtensionManifestValidationError::DescriptionNotLongerThanName),
);
}
#[test]
fn test_validate_manifest_missing_authors() {
let manifest = ExtensionManifest {
authors: Vec::new(),
..valid_manifest()
};
assert_eq!(
validate_extension_manifest(&manifest),
Err(ExtensionManifestValidationError::MissingAuthors),
);
}
#[test]
fn test_validate_manifest_blank_authors() {
let manifest = ExtensionManifest {
authors: vec![" ".to_string()],
..valid_manifest()
};
assert_eq!(
validate_extension_manifest(&manifest),
Err(ExtensionManifestValidationError::MissingAuthors),
);
}
#[test]
fn test_validate_manifest_missing_repository() {
let manifest = ExtensionManifest {
repository: None,
..valid_manifest()
};
assert_eq!(
validate_extension_manifest(&manifest),
Err(ExtensionManifestValidationError::MissingRepository),
);
}
#[test]
fn test_validate_manifest_invalid_repository() {
let manifest = ExtensionManifest {
repository: Some("not-a-valid-url".to_string()),
..valid_manifest()
};
assert_eq!(
validate_extension_manifest(&manifest),
Err(ExtensionManifestValidationError::InvalidRepository(
"not-a-valid-url".to_string()
)),
);
}
#[test]
fn test_validate_manifest_repository_without_host() {
let manifest = ExtensionManifest {
repository: Some("file:///some/local/path".to_string()),
..valid_manifest()
};
assert_eq!(
validate_extension_manifest(&manifest),
Err(ExtensionManifestValidationError::InvalidRepository(
"file:///some/local/path".to_string()
)),
);
}
#[test]
fn test_validate_manifest_language_model_providers_unsupported() {
let mut language_model_providers = BTreeMap::new();
language_model_providers.insert(
"provider".into(),
LanguageModelProviderManifestEntry {
name: "Provider".to_string(),
icon: None,
},
);
let manifest = ExtensionManifest {
language_model_providers,
..valid_manifest()
};
assert_eq!(
validate_extension_manifest(&manifest),
Err(ExtensionManifestValidationError::LanguageModelProvidersUnsupported),
);
}
#[test]
fn test_validate_empty_features() {
let provides = BTreeSet::new();
assert_eq!(
validate_extension_features(&provides),
Err(ExtensionFeatureError::NoFeatures),
);
}
#[test]
fn test_validate_single_language_feature() {
let provides = BTreeSet::from([ExtensionProvides::Languages]);
assert_eq!(validate_extension_features(&provides), Ok(()));
}
#[test]
fn test_validate_single_themes_feature() {
let provides = BTreeSet::from([ExtensionProvides::Themes]);
assert_eq!(validate_extension_features(&provides), Ok(()));
}
#[test]
fn test_validate_themes_with_other_features() {
let provides = BTreeSet::from([ExtensionProvides::Themes, ExtensionProvides::Languages]);
assert_eq!(
validate_extension_features(&provides),
Err(ExtensionFeatureError::ThemesMixedWithOtherFeatures),
);
}
#[test]
fn test_validate_single_icon_themes_feature() {
let provides = BTreeSet::from([ExtensionProvides::IconThemes]);
assert_eq!(validate_extension_features(&provides), Ok(()));
}
#[test]
fn test_validate_icon_themes_with_other_features() {
let provides = BTreeSet::from([ExtensionProvides::IconThemes, ExtensionProvides::Grammars]);
assert_eq!(
validate_extension_features(&provides),
Err(ExtensionFeatureError::IconThemesMixedWithOtherFeatures),
);
}
#[test]
fn test_validate_slash_commands_only() {
let provides = BTreeSet::from([ExtensionProvides::SlashCommands]);
assert_eq!(
validate_extension_features(&provides),
Err(ExtensionFeatureError::SlashCommandsDeprecated { sole_feature: true }),
);
}
#[test]
fn test_validate_slash_commands_with_other_features() {
let provides = BTreeSet::from([
ExtensionProvides::SlashCommands,
ExtensionProvides::Languages,
]);
assert_eq!(
validate_extension_features(&provides),
Err(ExtensionFeatureError::SlashCommandsDeprecated {
sole_feature: false
}),
);
}
#[test]
fn test_validate_multiple_non_theme_features() {
let provides = BTreeSet::from([
ExtensionProvides::Languages,
ExtensionProvides::Grammars,
ExtensionProvides::LanguageServers,
]);
assert_eq!(validate_extension_features(&provides), Ok(()));
}
}