main.rs

  1use std::{
  2    collections::HashMap,
  3    fs,
  4    path::{Path, PathBuf},
  5    sync::Arc,
  6};
  7
  8use ::fs::{Fs, RealFs};
  9use anyhow::{anyhow, Context, Result};
 10use clap::Parser;
 11use extension::{
 12    extension_builder::{CompileExtensionOptions, ExtensionBuilder},
 13    ExtensionStore,
 14};
 15use language::LanguageConfig;
 16use theme::ThemeRegistry;
 17use tree_sitter::{Language, Query, WasmStore};
 18
 19#[derive(Parser, Debug)]
 20#[command(name = "zed-extension")]
 21struct Args {
 22    /// The path to the extension directory
 23    extension_path: PathBuf,
 24    /// Whether to compile with optimizations
 25    #[arg(long)]
 26    release: bool,
 27    /// The path to a directory where build dependencies are downloaded
 28    #[arg(long)]
 29    scratch_dir: PathBuf,
 30}
 31
 32#[tokio::main]
 33async fn main() -> Result<()> {
 34    env_logger::init();
 35
 36    let args = Args::parse();
 37    let fs = Arc::new(RealFs);
 38    let engine = wasmtime::Engine::default();
 39    let mut wasm_store = WasmStore::new(engine)?;
 40
 41    let extension_path = args
 42        .extension_path
 43        .canonicalize()
 44        .context("can't canonicalize extension_path")?;
 45    let scratch_dir = args
 46        .scratch_dir
 47        .canonicalize()
 48        .context("can't canonicalize scratch_dir")?;
 49
 50    let manifest = ExtensionStore::load_extension_manifest(fs.clone(), &extension_path).await?;
 51    let builder = ExtensionBuilder::new(scratch_dir);
 52    builder
 53        .compile_extension(
 54            &extension_path,
 55            &manifest,
 56            CompileExtensionOptions {
 57                release: args.release,
 58            },
 59        )
 60        .await?;
 61
 62    let grammars = test_grammars(&extension_path, &mut wasm_store)?;
 63    test_languages(&extension_path, &grammars)?;
 64    test_themes(&extension_path, fs.clone()).await?;
 65
 66    Ok(())
 67}
 68
 69fn test_grammars(
 70    extension_path: &Path,
 71    wasm_store: &mut WasmStore,
 72) -> Result<HashMap<String, Language>> {
 73    let mut grammars = HashMap::default();
 74    let grammars_dir = extension_path.join("grammars");
 75    if !grammars_dir.exists() {
 76        return Ok(grammars);
 77    }
 78
 79    let entries = fs::read_dir(&grammars_dir)?;
 80    for entry in entries {
 81        let entry = entry?;
 82        let grammar_path = entry.path();
 83        let grammar_name = grammar_path.file_stem().unwrap().to_str().unwrap();
 84        if grammar_path.extension() == Some("wasm".as_ref()) {
 85            let wasm = fs::read(&grammar_path)?;
 86            let language = wasm_store.load_language(grammar_name, &wasm)?;
 87            log::info!("loaded grammar {grammar_name}");
 88            grammars.insert(grammar_name.into(), language);
 89        }
 90    }
 91
 92    Ok(grammars)
 93}
 94
 95fn test_languages(extension_path: &Path, grammars: &HashMap<String, Language>) -> Result<()> {
 96    let languages_dir = extension_path.join("languages");
 97    if !languages_dir.exists() {
 98        return Ok(());
 99    }
100
101    let entries = fs::read_dir(&languages_dir)?;
102    for entry in entries {
103        let entry = entry?;
104        let language_dir = entry.path();
105        let config_path = language_dir.join("config.toml");
106        let config_content = fs::read_to_string(&config_path)?;
107        let config: LanguageConfig = toml::from_str(&config_content)?;
108        let grammar = if let Some(name) = &config.grammar {
109            Some(
110                grammars
111                    .get(name.as_ref())
112                    .ok_or_else(|| anyhow!("language"))?,
113            )
114        } else {
115            None
116        };
117
118        let query_entries = fs::read_dir(&language_dir)?;
119        for entry in query_entries {
120            let entry = entry?;
121            let query_path = entry.path();
122            if query_path.extension() == Some("scm".as_ref()) {
123                let grammar = grammar.ok_or_else(|| {
124                    anyhow!(
125                        "language {} provides query {} but no grammar",
126                        config.name,
127                        query_path.display()
128                    )
129                })?;
130
131                let query_source = fs::read_to_string(&query_path)?;
132                let _query = Query::new(grammar, &query_source)?;
133            }
134        }
135
136        log::info!("loaded language {}", config.name);
137    }
138
139    Ok(())
140}
141
142async fn test_themes(extension_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
143    let themes_dir = extension_path.join("themes");
144    if !themes_dir.exists() {
145        return Ok(());
146    }
147
148    let entries = fs::read_dir(&themes_dir)?;
149    for entry in entries {
150        let entry = entry?;
151        let theme_path = entry.path();
152        if theme_path.extension() == Some("json".as_ref()) {
153            let theme_family = ThemeRegistry::read_user_theme(&entry.path(), fs.clone()).await?;
154            log::info!("loaded theme family {}", theme_family.name);
155        }
156    }
157
158    Ok(())
159}