use anyhow::{anyhow, Context, Result}; use collections::{BTreeMap, HashMap}; use fs::Fs; use language::LanguageServerName; use semantic_version::SemanticVersion; use serde::{Deserialize, Serialize}; use std::{ ffi::OsStr, fmt, path::{Path, PathBuf}, sync::Arc, }; /// This is the old version of the extension manifest, from when it was `extension.json`. #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct OldExtensionManifest { pub name: String, pub version: Arc, #[serde(default)] pub description: Option, #[serde(default)] pub repository: Option, #[serde(default)] pub authors: Vec, #[serde(default)] pub themes: BTreeMap, PathBuf>, #[serde(default)] pub languages: BTreeMap, PathBuf>, #[serde(default)] pub grammars: BTreeMap, PathBuf>, } /// The schema version of the [`ExtensionManifest`]. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] pub struct SchemaVersion(pub i32); impl fmt::Display for SchemaVersion { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl SchemaVersion { pub const ZERO: Self = Self(0); pub fn is_v0(&self) -> bool { self == &Self::ZERO } } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct ExtensionManifest { pub id: Arc, pub name: String, pub version: Arc, pub schema_version: SchemaVersion, #[serde(default)] pub description: Option, #[serde(default)] pub repository: Option, #[serde(default)] pub authors: Vec, #[serde(default)] pub lib: LibManifestEntry, #[serde(default)] pub themes: Vec, #[serde(default)] pub languages: Vec, #[serde(default)] pub grammars: BTreeMap, GrammarManifestEntry>, #[serde(default)] pub language_servers: BTreeMap, #[serde(default)] pub slash_commands: BTreeMap, SlashCommandManifestEntry>, #[serde(default)] pub indexed_docs_providers: BTreeMap, IndexedDocsProviderEntry>, #[serde(default)] pub snippets: Option, } #[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct LibManifestEntry { pub kind: Option, pub version: Option, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub enum ExtensionLibraryKind { Rust, } #[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct GrammarManifestEntry { pub repository: String, #[serde(alias = "commit")] pub rev: String, #[serde(default)] pub path: Option, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct LanguageServerManifestEntry { /// Deprecated in favor of `languages`. #[serde(default)] language: Option>, /// The list of languages this language server should work with. #[serde(default)] languages: Vec>, #[serde(default)] pub language_ids: HashMap, #[serde(default)] pub code_action_kinds: Option>, } impl LanguageServerManifestEntry { /// Returns the list of languages for the language server. /// /// Prefer this over accessing the `language` or `languages` fields directly, /// as we currently support both. /// /// We can replace this with just field access for the `languages` field once /// we have removed `language`. pub fn languages(&self) -> impl IntoIterator> + '_ { let language = if self.languages.is_empty() { self.language.clone() } else { None }; self.languages.iter().cloned().chain(language) } } #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct SlashCommandManifestEntry { pub description: String, pub tooltip_text: String, pub requires_argument: bool, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct IndexedDocsProviderEntry {} impl ExtensionManifest { pub async fn load(fs: Arc, extension_dir: &Path) -> Result { let extension_name = extension_dir .file_name() .and_then(OsStr::to_str) .ok_or_else(|| anyhow!("invalid extension name"))?; let mut extension_manifest_path = extension_dir.join("extension.json"); if fs.is_file(&extension_manifest_path).await { let manifest_content = fs .load(&extension_manifest_path) .await .with_context(|| format!("failed to load {extension_name} extension.json"))?; let manifest_json = serde_json::from_str::(&manifest_content) .with_context(|| { format!("invalid extension.json for extension {extension_name}") })?; Ok(manifest_from_old_manifest(manifest_json, extension_name)) } else { extension_manifest_path.set_extension("toml"); let manifest_content = fs .load(&extension_manifest_path) .await .with_context(|| format!("failed to load {extension_name} extension.toml"))?; toml::from_str(&manifest_content) .with_context(|| format!("invalid extension.json for extension {extension_name}")) } } } fn manifest_from_old_manifest( manifest_json: OldExtensionManifest, extension_id: &str, ) -> ExtensionManifest { ExtensionManifest { id: extension_id.into(), name: manifest_json.name, version: manifest_json.version, description: manifest_json.description, repository: manifest_json.repository, authors: manifest_json.authors, schema_version: SchemaVersion::ZERO, lib: Default::default(), themes: { let mut themes = manifest_json.themes.into_values().collect::>(); themes.sort(); themes.dedup(); themes }, languages: { let mut languages = manifest_json.languages.into_values().collect::>(); languages.sort(); languages.dedup(); languages }, grammars: manifest_json .grammars .into_keys() .map(|grammar_name| (grammar_name, Default::default())) .collect(), language_servers: Default::default(), slash_commands: BTreeMap::default(), indexed_docs_providers: BTreeMap::default(), snippets: None, } }