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}