extension.rs

  1pub mod extension_builder;
  2mod extension_manifest;
  3mod slash_command;
  4
  5use std::path::{Path, PathBuf};
  6use std::sync::Arc;
  7
  8use anyhow::{anyhow, bail, Context as _, Result};
  9use async_trait::async_trait;
 10use gpui::Task;
 11use semantic_version::SemanticVersion;
 12
 13pub use crate::extension_manifest::*;
 14pub use crate::slash_command::*;
 15
 16#[async_trait]
 17pub trait WorktreeDelegate: Send + Sync + 'static {
 18    fn id(&self) -> u64;
 19    fn root_path(&self) -> String;
 20    async fn read_text_file(&self, path: PathBuf) -> Result<String>;
 21    async fn which(&self, binary_name: String) -> Option<String>;
 22    async fn shell_env(&self) -> Vec<(String, String)>;
 23}
 24
 25pub trait KeyValueStoreDelegate: Send + Sync + 'static {
 26    fn insert(&self, key: String, docs: String) -> Task<Result<()>>;
 27}
 28
 29#[async_trait]
 30pub trait Extension: Send + Sync + 'static {
 31    /// Returns the [`ExtensionManifest`] for this extension.
 32    fn manifest(&self) -> Arc<ExtensionManifest>;
 33
 34    /// Returns the path to this extension's working directory.
 35    fn work_dir(&self) -> Arc<Path>;
 36
 37    async fn complete_slash_command_argument(
 38        &self,
 39        command: SlashCommand,
 40        arguments: Vec<String>,
 41    ) -> Result<Vec<SlashCommandArgumentCompletion>>;
 42
 43    async fn run_slash_command(
 44        &self,
 45        command: SlashCommand,
 46        arguments: Vec<String>,
 47        resource: Option<Arc<dyn WorktreeDelegate>>,
 48    ) -> Result<SlashCommandOutput>;
 49
 50    async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>>;
 51
 52    async fn index_docs(
 53        &self,
 54        provider: Arc<str>,
 55        package_name: Arc<str>,
 56        kv_store: Arc<dyn KeyValueStoreDelegate>,
 57    ) -> Result<()>;
 58}
 59
 60pub fn parse_wasm_extension_version(
 61    extension_id: &str,
 62    wasm_bytes: &[u8],
 63) -> Result<SemanticVersion> {
 64    let mut version = None;
 65
 66    for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) {
 67        if let wasmparser::Payload::CustomSection(s) =
 68            part.context("error parsing wasm extension")?
 69        {
 70            if s.name() == "zed:api-version" {
 71                version = parse_wasm_extension_version_custom_section(s.data());
 72                if version.is_none() {
 73                    bail!(
 74                        "extension {} has invalid zed:api-version section: {:?}",
 75                        extension_id,
 76                        s.data()
 77                    );
 78                }
 79            }
 80        }
 81    }
 82
 83    // The reason we wait until we're done parsing all of the Wasm bytes to return the version
 84    // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid.
 85    //
 86    // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem
 87    // earlier as an `Err` rather than as a panic.
 88    version.ok_or_else(|| anyhow!("extension {} has no zed:api-version section", extension_id))
 89}
 90
 91fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVersion> {
 92    if data.len() == 6 {
 93        Some(SemanticVersion::new(
 94            u16::from_be_bytes([data[0], data[1]]) as _,
 95            u16::from_be_bytes([data[2], data[3]]) as _,
 96            u16::from_be_bytes([data[4], data[5]]) as _,
 97        ))
 98    } else {
 99        None
100    }
101}