@@ -1,63 +1,155 @@
1use anyhow::{anyhow, Context, Result};
1use anyhow::{anyhow, Context, Result};
2+use libloading::{Library, Symbol};
3+use serde::{Deserialize, Serialize};
2use std::fs;
4use std::fs;
3use std::time::SystemTime;
5use std::time::SystemTime;
5 path::{Path, PathBuf},
8 path::{Path, PathBuf},
6 process::Command,
9 process::Command,
7 sync::mpsc::channel,
10 sync::mpsc::channel,
12+use tree_sitter::Language;
10−use helix_core::syntax::{GrammarConfiguration, GrammarSelection, GrammarSource, DYLIB_EXTENSION};
14+#[cfg(unix)]
15+const DYLIB_EXTENSION: &str = "so";
18+const DYLIB_EXTENSION: &str = "dll";
20+#[derive(Debug, Serialize, Deserialize)]
21+struct Configuration {
22+ #[serde(rename = "use-grammars")]
23+ pub grammar_selection: Option<GrammarSelection>,
24+ pub grammar: Vec<GrammarConfiguration>,
27+#[derive(Debug, Serialize, Deserialize)]
28+#[serde(rename_all = "lowercase", untagged)]
29+pub enum GrammarSelection {
30+ Only(HashSet<String>),
31+ Except(HashSet<String>),
34+#[derive(Debug, Serialize, Deserialize)]
35+#[serde(deny_unknown_fields)]
36+pub struct GrammarConfiguration {
37+ #[serde(rename = "name")]
38+ pub grammar_id: String,
39+ pub source: GrammarSource,
42+#[derive(Debug, Serialize, Deserialize)]
43+#[serde(rename_all = "lowercase", untagged)]
44+pub enum GrammarSource {
49+ #[serde(rename = "git")]
51+ #[serde(rename = "rev")]
53+ subpath: Option<String>,
12const BUILD_TARGET: &str = env!("BUILD_TARGET");
57const BUILD_TARGET: &str = env!("BUILD_TARGET");
13const REMOTE_NAME: &str = "origin";
58const REMOTE_NAME: &str = "origin";
60+pub fn get_language(name: &str) -> Result<Language> {
61+ let name = name.to_ascii_lowercase();
62+ let mut library_path = crate::runtime_dir().join("grammars").join(&name);
63+ library_path.set_extension(DYLIB_EXTENSION);
65+ let library = unsafe { Library::new(&library_path) }
66+ .with_context(|| format!("Error opening dynamic library {library_path:?}"))?;
67+ let language_fn_name = format!("tree_sitter_{}", name.replace('-', "_"));
68+ let language = unsafe {
69+ let language_fn: Symbol<unsafe extern "C" fn() -> Language> = library
70+ .get(language_fn_name.as_bytes())
71+ .with_context(|| format!("Failed to load symbol {language_fn_name}"))?;
74+ std::mem::forget(library);
15pub fn fetch_grammars() -> Result<()> {
78pub fn fetch_grammars() -> Result<()> {
16− run_parallel(get_grammar_configs(), fetch_grammar, "fetch")
79+ run_parallel(get_grammar_configs()?, fetch_grammar, "fetch")
19pub fn build_grammars() -> Result<()> {
82pub fn build_grammars() -> Result<()> {
20− run_parallel(get_grammar_configs(), build_grammar, "build")
83+ run_parallel(get_grammar_configs()?, build_grammar, "build")
86+// Returns the set of grammar configurations the user requests.
87+// Grammars are configured in the default and user `languages.toml` and are
88+// merged. The `grammar_selection` key of the config is then used to filter
89+// down all grammars into a subset of the user's choosing.
90+fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
91+ let config: Configuration = crate::user_lang_config()
92+ .context("Could not parse languages.toml")?
95+ let grammars = match config.grammar_selection {
96+ Some(GrammarSelection::Only(selections)) => config
99+ .filter(|grammar| selections.contains(&grammar.grammar_id))
101+ Some(GrammarSelection::Except(rejections)) => config
104+ .filter(|grammar| !rejections.contains(&grammar.grammar_id))
106+ None => config.grammar,
23fn run_parallel<F>(grammars: Vec<GrammarConfiguration>, job: F, action: &'static str) -> Result<()>
112fn run_parallel<F>(grammars: Vec<GrammarConfiguration>, job: F, action: &'static str) -> Result<()>
25 F: Fn(GrammarConfiguration) -> Result<()> + std::marker::Send + 'static + Copy,
114 F: Fn(GrammarConfiguration) -> Result<()> + std::marker::Send + 'static + Copy,
28 let pool = threadpool::Builder::new().build();
116 let pool = threadpool::Builder::new().build();
29 let (tx, rx) = channel();
117 let (tx, rx) = channel();
31 for grammar in grammars {
119 for grammar in grammars {
32 let tx = tx.clone();
120 let tx = tx.clone();
35 pool.execute(move || {
122 pool.execute(move || {
36− let grammar_id = grammar.grammar_id.clone();
37− job(grammar).unwrap_or_else(|err| {
38− eprintln!("Failed to {} grammar '{}'\n{}", action, grammar_id, err)
42− tx.send(1).unwrap();
123+ tx.send(job(grammar)).unwrap();
45 pool.join();
126 pool.join();
47− if rx.try_iter().sum::<usize>() == n_jobs {
128+ // TODO: print all failures instead of the first one found.
129+ if let Some(failure) = rx.try_iter().find_map(|result| result.err()) {
131+ "Failed to {} some grammar(s).\n{}",
50− Err(anyhow!("Failed to {} some grammar(s).", action))
136+ Ok(())
54fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
140fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
55− if let GrammarSource::Git { remote, revision } = grammar.source {
141+ if let GrammarSource::Git {
142+ remote, revision, ..
56− let grammar_dir = helix_core::runtime_dir()
145+ let grammar_dir = crate::runtime_dir()
57 .join("grammars/sources")
146 .join("grammars/sources")
58− .join(grammar.grammar_id.clone());
147+ .join(&grammar.grammar_id);
149+ fs::create_dir_all(&grammar_dir).context(format!(
60− fs::create_dir_all(grammar_dir.clone()).expect("Could not create grammar directory");
150+ "Could not create grammar directory {:?}",
62 // create the grammar dir contains a git directory
154 // create the grammar dir contains a git directory
63 if !grammar_dir.join(".git").is_dir() {
155 if !grammar_dir.join(".git").is_dir() {
@@ -65,12 +157,12 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
67 // ensure the remote matches the configured remote
159 // ensure the remote matches the configured remote
68− if get_remote_url(&grammar_dir).map_or(true, |s| s.trim_end() != remote) {
160+ if get_remote_url(&grammar_dir).map_or(true, |s| s != remote) {
69 set_remote(&grammar_dir, &remote)?;
161 set_remote(&grammar_dir, &remote)?;
72 // ensure the revision matches the configured revision
164 // ensure the revision matches the configured revision
73− if get_revision(&grammar_dir).map_or(true, |s| s.trim_end() != revision) {
165+ if get_revision(&grammar_dir).map_or(true, |s| s != revision) {
74 // Fetch the exact revision from the remote.
166 // Fetch the exact revision from the remote.
75 // Supported by server-side git since v2.5.0 (July 2015),
167 // Supported by server-side git since v2.5.0 (July 2015),
76 // enabled by default on major git hosts.
168 // enabled by default on major git hosts.
@@ -94,33 +186,38 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
95// Sets the remote for a repository to the given URL, creating the remote if
187// Sets the remote for a repository to the given URL, creating the remote if
96// it does not yet exist.
188// it does not yet exist.
97−fn set_remote(repository: &Path, remote_url: &str) -> Result<String> {
189+fn set_remote(repository_dir: &Path, remote_url: &str) -> Result<String> {
98− git(repository, ["remote", "set-url", REMOTE_NAME, remote_url])
192+ ["remote", "set-url", REMOTE_NAME, remote_url],
99− .or_else(|_| git(repository, ["remote", "add", REMOTE_NAME, remote_url]))
194+ .or_else(|_| git(repository_dir, ["remote", "add", REMOTE_NAME, remote_url]))
102−fn get_remote_url(repository: &Path) -> Option<String> {
197+fn get_remote_url(repository_dir: &Path) -> Option<String> {
103− git(repository, ["remote", "get-url", REMOTE_NAME]).ok()
198+ git(repository_dir, ["remote", "get-url", REMOTE_NAME]).ok()
106−fn get_revision(repository: &Path) -> Option<String> {
201+fn get_revision(repository_dir: &Path) -> Option<String> {
107− git(repository, ["rev-parse", "HEAD"]).ok()
202+ git(repository_dir, ["rev-parse", "HEAD"]).ok()
110// A wrapper around 'git' commands which returns stdout in success and a
205// A wrapper around 'git' commands which returns stdout in success and a
111// helpful error message showing the command, stdout, and stderr in error.
206// helpful error message showing the command, stdout, and stderr in error.
112−fn git<I, S>(repository: &Path, args: I) -> Result<String>
207+fn git<I, S>(repository_dir: &Path, args: I) -> Result<String>
114 I: IntoIterator<Item = S>,
209 I: IntoIterator<Item = S>,
115 S: AsRef<std::ffi::OsStr>,
210 S: AsRef<std::ffi::OsStr>,
117 let output = Command::new("git")
212 let output = Command::new("git")
118 .args(args)
213 .args(args)
119− .current_dir(repository)
214+ .current_dir(repository_dir)
120 .output()?;
215 .output()?;
122 if output.status.success() {
217 if output.status.success() {
123− Ok(String::from_utf8_lossy(&output.stdout).into_owned())
218+ Ok(String::from_utf8_lossy(&output.stdout)
125 // TODO: figure out how to display the git command using `args`
222 // TODO: figure out how to display the git command using `args`
126 Err(anyhow!(
223 Err(anyhow!(
@@ -132,52 +229,37 @@ where
134fn build_grammar(grammar: GrammarConfiguration) -> Result<()> {
231fn build_grammar(grammar: GrammarConfiguration) -> Result<()> {
232+ println!("{:#?}", grammar);
135− let grammar_dir = if let GrammarSource::Local { ref path } = grammar.source {
233+ let grammar_dir = if let GrammarSource::Local { path } = &grammar.source {
136− PathBuf::from(path)
234+ PathBuf::from(&path)
138− helix_core::runtime_dir()
236+ crate::runtime_dir()
139 .join("grammars/sources")
237 .join("grammars/sources")
140− .join(grammar.grammar_id.clone())
238+ .join(&grammar.grammar_id)
143− grammar_dir.read_dir().with_context(|| {
241+ let grammar_dir_entries = grammar_dir.read_dir().with_context(|| {
145− "The directory {:?} is empty, you probably need to use 'hx --fetch-grammars'?",
242+ format!("Failed to read directory {grammar_dir:?}. Did you use 'hx --fetch-grammars'?")
245+ if grammar_dir_entries.count() == 0 {
247+ "Directory {grammar_dir:?} is empty. Did you use 'hx --fetch-grammars'?"
150− let path = match grammar.path {
251+ let path = match &grammar.source {
252+ GrammarSource::Git {
253+ subpath: Some(subpath),
151− Some(ref subpath) => grammar_dir.join(subpath),
255+ } => grammar_dir.join(subpath),
152− None => grammar_dir,
256+ _ => grammar_dir,
154 .join("src");
258 .join("src");
156 build_tree_sitter_library(&path, grammar)
260 build_tree_sitter_library(&path, grammar)
159−// Returns the set of grammar configurations the user requests.
160−// Grammars are configured in the default and user `languages.toml` and are
161−// merged. The `grammar_selection` key of the config is then used to filter
162−// down all grammars into a subset of the user's choosing.
163−fn get_grammar_configs() -> Vec<GrammarConfiguration> {
164− let config = helix_core::config::user_syntax_loader().expect("Could not parse languages.toml");
166− match config.grammar_selection {
167− Some(GrammarSelection::Only(selections)) => config
170− .filter(|grammar| selections.contains(&grammar.grammar_id))
172− Some(GrammarSelection::Except(rejections)) => config
175− .filter(|grammar| !rejections.contains(&grammar.grammar_id))
177− None => config.grammar,
181fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) -> Result<()> {
263fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) -> Result<()> {
182 let header_path = src_path;
264 let header_path = src_path;
183 let parser_path = src_path.join("parser.c");
265 let parser_path = src_path.join("parser.c");
@@ -193,8 +275,8 @@ fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) ->
196− let parser_lib_path = helix_core::runtime_dir().join("../runtime/grammars");
278+ let parser_lib_path = crate::runtime_dir().join("grammars");
197− let mut library_path = parser_lib_path.join(grammar.grammar_id.clone());
279+ let mut library_path = parser_lib_path.join(&grammar.grammar_id);
198 library_path.set_extension(DYLIB_EXTENSION);
280 library_path.set_extension(DYLIB_EXTENSION);
200 let recompile = needs_recompile(&library_path, &parser_path, &scanner_path)
282 let recompile = needs_recompile(&library_path, &parser_path, &scanner_path)
@@ -210,7 +292,7 @@ fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) ->
210 let mut config = cc::Build::new();
292 let mut config = cc::Build::new();
212 .cpp(true)
294 .cpp(true)
213− .opt_level(2)
295+ .opt_level(3)
214 .cargo_metadata(false)
296 .cargo_metadata(false)
215 .host(BUILD_TARGET)
297 .host(BUILD_TARGET)
216 .target(BUILD_TARGET);
298 .target(BUILD_TARGET);
@@ -245,7 +327,7 @@ fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) ->
245 .arg(header_path)
327 .arg(header_path)
246 .arg("-o")
328 .arg("-o")
247 .arg(&library_path)
329 .arg(&library_path)
248− .arg("-O2");
330+ .arg("-O3");
249 if let Some(scanner_path) = scanner_path.as_ref() {
331 if let Some(scanner_path) = scanner_path.as_ref() {
250 if scanner_path.extension() == Some("c".as_ref()) {
332 if scanner_path.extension() == Some("c".as_ref()) {
251 command.arg("-xc").arg("-std=c99").arg(scanner_path);
333 command.arg("-xc").arg("-std=c99").arg(scanner_path);
@@ -294,3 +376,13 @@ fn needs_recompile(
294fn mtime(path: &Path) -> Result<SystemTime> {
376fn mtime(path: &Path) -> Result<SystemTime> {
295 Ok(fs::metadata(path)?.modified()?)
377 Ok(fs::metadata(path)?.modified()?)
380+/// Gives the contents of a file from a language's `runtime/queries/<lang>`
382+pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
383+ let path = crate::RUNTIME_DIR
387+ std::fs::read_to_string(&path)