1#[cfg(not(target_arch = "wasm32"))]
5use anyhow::anyhow;
6#[cfg(not(target_arch = "wasm32"))]
7use gix::{
8 interrupt::IS_INTERRUPTED,
9 progress::Discard,
10 remote::{self, fetch, fetch::refmap, Direction},
11 worktree::state::checkout,
12 ObjectId,
13};
14use std::collections::HashSet;
15#[cfg(not(target_arch = "wasm32"))]
16use std::num::NonZero;
17#[cfg(not(target_arch = "wasm32"))]
18use std::path::PathBuf;
19
20use crate::error::TopiaryConfigResult;
21#[cfg(not(target_arch = "wasm32"))]
22use crate::error::{TopiaryConfigError, TopiaryConfigFetchingError};
23
24#[derive(Debug, serde::Deserialize, PartialEq, serde::Serialize, Clone)]
27pub struct Language {
28 pub name: String,
31
32 pub config: LanguageConfiguration,
35}
36
37#[derive(Debug, serde::Deserialize, PartialEq, serde::Serialize, Clone)]
38pub struct LanguageConfiguration {
39 pub extensions: HashSet<String>,
42
43 pub indent: Option<String>,
47
48 pub grammar: Grammar,
50}
51
52#[derive(Debug, serde::Deserialize, PartialEq, serde::Serialize, Clone)]
53pub struct Grammar {
54 #[cfg(not(target_arch = "wasm32"))]
55 pub source: GrammarSource,
56 pub symbol: Option<String>,
61}
62
63#[derive(Debug, serde::Deserialize, PartialEq, serde::Serialize, Clone)]
64#[cfg(not(target_arch = "wasm32"))]
65pub enum GrammarSource {
66 #[serde(rename = "git")]
67 Git(GitSource),
68 #[serde(rename = "path")]
69 Path(PathBuf),
70}
71
72#[derive(Debug, serde::Deserialize, PartialEq, serde::Serialize, Clone)]
73#[cfg(not(target_arch = "wasm32"))]
74pub struct GitSource {
75 pub git: String,
77 pub rev: String,
79 pub subdir: Option<String>,
81}
82
83impl Language {
84 pub fn new(name: String, config: LanguageConfiguration) -> Self {
85 Self { name, config }
86 }
87
88 #[cfg(not(target_arch = "wasm32"))]
89 pub fn find_query_file(&self) -> TopiaryConfigResult<PathBuf> {
90 let basename = PathBuf::from(self.name.as_str()).with_extension("scm");
91
92 #[rustfmt::skip]
93 let potentials: [Option<PathBuf>; 4] = [
94 std::env::var("TOPIARY_LANGUAGE_DIR").map(PathBuf::from).ok(),
95 option_env!("TOPIARY_LANGUAGE_DIR").map(PathBuf::from),
96 Some(PathBuf::from("./topiary-queries/queries")),
97 Some(PathBuf::from("../topiary-queries/queries")),
98 ];
99
100 potentials
101 .into_iter()
102 .flatten()
103 .map(|path| path.join(&basename))
104 .find(|path| path.exists())
105 .ok_or_else(|| TopiaryConfigError::QueryFileNotFound(basename))
106 }
107
108 #[cfg(not(target_arch = "wasm32"))]
109 pub fn library_path(&self) -> std::io::Result<PathBuf> {
111 match &self.config.grammar.source {
112 GrammarSource::Git(git_source) => {
113 let mut library_path = crate::project_dirs().cache_dir().to_path_buf();
114 library_path.push(self.name.clone());
115 std::fs::create_dir_all(&library_path)?;
116
117 library_path.push(git_source.rev.clone());
120 library_path.set_extension(std::env::consts::DLL_EXTENSION);
121
122 Ok(library_path)
123 }
124
125 GrammarSource::Path(path) => Ok(path.to_path_buf()),
126 }
127 }
128
129 #[cfg(not(target_arch = "wasm32"))]
130 pub fn grammar(
133 &self,
134 ) -> Result<topiary_tree_sitter_facade::Language, TopiaryConfigFetchingError> {
135 let library_path = self.library_path()?;
136
137 if !library_path.is_file() {
139 match &self.config.grammar.source {
140 GrammarSource::Git(git_source) => {
141 git_source.fetch_and_compile(&self.name, library_path.clone())?
142 }
143 GrammarSource::Path(_) => {
144 return Err(TopiaryConfigFetchingError::GrammarFileNotFound(
145 library_path,
146 ))
147 }
148 }
149 }
150
151 assert!(library_path.is_file());
152 log::debug!("Loading grammar from {}", library_path.to_string_lossy());
153
154 use libloading::{Library, Symbol};
155
156 let library = unsafe { Library::new(&library_path) }?;
157 let language_fn_name = if let Some(symbol_name) = self.config.grammar.symbol.clone() {
158 symbol_name
159 } else {
160 format!("tree_sitter_{}", self.name.replace('-', "_"))
161 };
162
163 let language = unsafe {
164 let language_fn: Symbol<unsafe extern "C" fn() -> *const ()> =
165 library.get(language_fn_name.as_bytes())?;
166 tree_sitter_language::LanguageFn::from_raw(*language_fn)
167 };
168 std::mem::forget(library);
169 Ok(topiary_tree_sitter_facade::Language::from(language))
170 }
171
172 #[cfg(target_arch = "wasm32")]
173 pub async fn grammar(&self) -> TopiaryConfigResult<topiary_tree_sitter_facade::Language> {
174 let language_name = self.name.as_str();
175
176 let grammar_path = if language_name == "tree_sitter_query" {
177 "/playground/scripts/tree-sitter-query.wasm".to_string()
178 } else {
179 format!("/playground/scripts/tree-sitter-{language_name}.wasm")
180 };
181
182 Ok(
183 topiary_web_tree_sitter_sys::Language::load_path(&grammar_path)
184 .await
185 .map_err(|e| {
186 let error: topiary_tree_sitter_facade::LanguageError = e.into();
187 error
188 })?
189 .into(),
190 )
191 }
192}
193
194type Result<T, E = TopiaryConfigFetchingError> = std::result::Result<T, E>;
195
196trait GitResult<T> {
197 fn wrap_err(self) -> Result<T>;
198}
199
200impl<T, E: Into<anyhow::Error>> GitResult<T> for Result<T, E> {
201 fn wrap_err(self) -> Result<T> {
202 self.map_err(|e| TopiaryConfigFetchingError::Git(e.into()))
203 }
204}
205
206#[cfg(not(target_arch = "wasm32"))]
207impl GitSource {
208 fn fetch_and_compile(
209 &self,
210 name: &str,
211 library_path: PathBuf,
212 ) -> Result<(), TopiaryConfigFetchingError> {
213 log::info!(
214 "{}: Language Grammar not found, attempting to fetch and compile it",
215 name
216 );
217 let tmp_dir = tempfile::tempdir()?;
222
223 self.fetch_and_compile_with_dir(name, library_path, false, tmp_dir.keep())
224 }
225
226 pub fn fetch_and_compile_with_dir(
229 &self,
230 name: &str,
231 library_path: PathBuf,
232 force: bool,
233 tmp_dir: PathBuf,
234 ) -> Result<(), TopiaryConfigFetchingError> {
235 if !force && library_path.is_file() {
236 log::info!("{}: Built grammar already exists; nothing to do", name);
237 return Ok(());
238 }
239 let tmp_dir = tmp_dir.join(name);
240 std::fs::create_dir_all(&tmp_dir)?;
241
242 let git_tempdir = tempfile::tempdir().wrap_err()?;
244 let repo = gix::init(git_tempdir.path()).wrap_err()?;
245
246 let remote = repo
247 .remote_at(self.git.as_str())
248 .wrap_err()?
249 .with_fetch_tags(fetch::Tags::None)
250 .with_refspecs(Some(self.rev.as_str()), Direction::Fetch)
251 .wrap_err()?;
252
253 let connection = remote.connect(Direction::Fetch).wrap_err()?;
258 let outcome = connection
259 .prepare_fetch(&mut Discard, remote::ref_map::Options::default())
260 .wrap_err()?
261 .with_shallow(fetch::Shallow::DepthAtRemote(NonZero::new(1).unwrap()))
264 .receive(&mut Discard, &IS_INTERRUPTED)
265 .wrap_err()?;
266
267 if outcome.ref_map.mappings.len() > 1 {
268 return Err(anyhow!("we only asked for 1 ref; why did we get more?")).wrap_err();
269 }
270 if outcome.ref_map.mappings.is_empty() {
271 return Err(anyhow!("Ref not found: {:?} {:?}", self.git, self.rev,)).wrap_err();
272 }
273
274 let object_id = source_object_id(&outcome.ref_map.mappings[0].remote)?;
275 let object = repo.find_object(object_id).wrap_err()?;
276 let tree_id = object.peel_to_tree().wrap_err()?.id();
277 let mut index = repo.index_from_tree(&tree_id).wrap_err()?;
278
279 log::info!("{}: Checking out {} {}", name, self.git, self.rev);
280 checkout(
281 &mut index,
282 &tmp_dir,
283 repo.objects.clone(),
284 &Discard,
285 &Discard,
286 &IS_INTERRUPTED,
287 checkout::Options {
288 overwrite_existing: true,
289 ..Default::default()
290 },
291 )
292 .wrap_err()?;
293 index.write(Default::default()).wrap_err()?;
294
295 let grammar_path = match self.subdir.clone() {
297 Some(subdir) => tmp_dir.join(subdir),
299 None => tmp_dir,
300 };
301
302 log::info!("{name}: Building grammar");
304 let mut loader =
305 tree_sitter_loader::Loader::new().map_err(TopiaryConfigFetchingError::Build)?;
306 loader.debug_build(false);
307 loader.force_rebuild(true);
308 loader
309 .compile_parser_at_path(&grammar_path, library_path, &[])
310 .map_err(TopiaryConfigFetchingError::Build)?;
311
312 log::info!("{name}: Grammar successfully compiled");
313 Ok(())
314 }
315}
316
317fn source_object_id(source: &refmap::Source) -> Result<ObjectId> {
318 match source {
319 refmap::Source::ObjectId(id) => Ok(*id),
320 refmap::Source::Ref(r) => {
321 let (_name, id, peeled) = r.unpack();
322
323 Ok(peeled
324 .or(id)
325 .ok_or_else(|| anyhow!("unborn reference"))
326 .wrap_err()?
327 .to_owned())
328 }
329 }
330}