on master help sign in
migrate grammar fetching/building code into helix-loader crate

This is a rather large refactor that moves most of the code for loading, fetching, and building grammars into a new helix-loader module. This works well with the [[grammars]] syntax for languages.toml defined earlier: we only have to depend on the types for GrammarConfiguration in helix-loader and can leave all the [[language]] entries for helix-core.

commit4fc991fdeca5db36bd7be7197510e62a019e1677 authorMichael Davis <mcarsondavis@gmail.com> dateWed 16 Feb 2022, 07:57 (5 years ago) parent08ee949
23 files changed+419−374
Cargo.lock+17−2 Cargo.toml+1 book/src/guides/adding_languages.md+5−5 docs/architecture.md+9−7 helix-core/Cargo.toml+3−3 helix-core/src/config.rs+2−25 helix-core/src/indent.rs−2 helix-core/src/lib.rs−141 helix-core/src/path.rs+3−2 helix-core/src/syntax.rs+6−85 helix-loader/Cargo.toml+23 helix-loader/build.rs+6 helix-term/src/grammars.rs → helix-loader/src/grammar.rs+161−69 helix-loader/src/lib.rs+161 helix-term/Cargo.toml+1 helix-term/build.rs−5 helix-term/src/application.rs+3−3 helix-term/src/commands/typed.rs+1−1 helix-term/src/health.rs+6−8 helix-term/src/lib.rs−1 helix-term/src/main.rs+5−5 helix-term/src/ui/mod.rs+2−2 languages.toml+4−8
Cargo.lock+17−2
@@ -358,12 +358,11 @@ dependencies = [
358358name = "helix-core"
359359version = "0.6.0"
360360dependencies = [
361− "anyhow",
362361 "arc-swap",
363362 "chrono",
364363 "encoding_rs",
365364 "etcetera",
366− "libloading",
365+ "helix-loader",
367366 "log",
368367 "once_cell",
369368 "quickcheck",
@@ -397,6 +396,21 @@ dependencies = [
397396 "which",
398397]
399398
399+[[package]]
400+name = "helix-loader"
401+version = "0.6.0"
402+dependencies = [
403+ "anyhow",
404+ "cc",
405+ "etcetera",
406+ "libloading",
407+ "once_cell",
408+ "serde",
409+ "threadpool",
410+ "toml",
411+ "tree-sitter",
412+]
413+
400414[[package]]
401415name = "helix-lsp"
402416version = "0.6.0"
@@ -432,6 +446,7 @@ dependencies = [
432446 "grep-searcher",
433447 "helix-core",
434448 "helix-dap",
449+ "helix-loader",
435450 "helix-lsp",
436451 "helix-tui",
437452 "helix-view",
Cargo.toml+1
@@ -6,6 +6,7 @@ members = [
66 "helix-tui",
77 "helix-lsp",
88 "helix-dap",
9+ "helix-loader",
910 "xtask",
1011]
1112
book/src/guides/adding_languages.md+5−5
@@ -51,16 +51,16 @@ Grammar configuration takes these keys:
5151| Key | Description |
5252| --- | ----------- |
5353| `name` | The name of the tree-sitter grammar |
54−| `path` | A path within the grammar directory which should be built. Some grammar repositories host multiple grammars (for example `tree-sitter-typescript` and `tree-sitter-ocaml`) in subdirectories. This key is used to point `hx --build-grammars` to the correct path for compilation. When ommitted, the root of the grammar directory is used |
5554| `source` | The method of fetching the grammar - a table with a schema defined below |
5655
5756Where `source` is a table with either these keys when using a grammar from a
5857git repository:
5958
60−| Key | Description |
61−| --- | ----------- |
62−| `git` | A git remote URL from which the grammar should be cloned |
63−| `rev` | The revision (commit hash or tag) which should be fetched |
59+| Key | Description |
60+| --- | ----------- |
61+| `git` | A git remote URL from which the grammar should be cloned |
62+| `rev` | The revision (commit hash or tag) which should be fetched |
63+| `subpath` | A path within the grammar directory which should be built. Some grammar repositories host multiple grammars (for example `tree-sitter-typescript` and `tree-sitter-ocaml`) in subdirectories. This key is used to point `hx --build-grammars` to the correct path for compilation. When omitted, the root of repository is used |
6464
6565Or a `path` key with an absolute path to a locally available grammar directory.
6666
docs/architecture.md+9−7
@@ -1,11 +1,13 @@
11
2−| Crate | Description |
3−| ----------- | ----------- |
4−| helix-core | Core editing primitives, functional. |
5−| helix-lsp | Language server client |
6−| helix-view | UI abstractions for use in backends, imperative shell. |
7−| helix-term | Terminal UI |
8−| helix-tui | TUI primitives, forked from tui-rs, inspired by Cursive |
2+| Crate | Description |
3+| ----------- | ----------- |
4+| helix-core | Core editing primitives, functional. |
5+| helix-lsp | Language server client |
6+| helix-dap | Debug Adapter Protocol (DAP) client |
7+| helix-loader | Functions for building, fetching, and loading external resources |
8+| helix-view | UI abstractions for use in backends, imperative shell. |
9+| helix-term | Terminal UI |
10+| helix-tui | TUI primitives, forked from tui-rs, inspired by Cursive |
911
1012
1113This document contains a high-level overview of Helix internals.
helix-core/Cargo.toml+3−3
@@ -13,6 +13,8 @@ include = ["src/**/*", "README.md"]
1313[features]
1414
1515[dependencies]
16+helix-loader = { version = "0.6", path = "../helix-loader" }
17+
1618ropey = "1.3"
1719smallvec = "1.8"
1820smartstring = "1.0.0"
@@ -33,13 +35,11 @@ toml = "0.5"
3335
3436similar = "2.1"
3537
36−etcetera = "0.3"
3738encoding_rs = "0.8"
3839
3940chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
4041
41−libloading = "0.7"
42−anyhow = "1"
42+etcetera = "0.3"
4343
4444[dev-dependencies]
4545quickcheck = { version = "1", default-features = false }
helix-core/src/config.rs+2−25
@@ -1,33 +1,10 @@
1−use crate::merge_toml_values;
2−
3−/// Default bultin-in languages.toml.
4−pub fn default_lang_config() -> toml::Value {
5− toml::from_slice(include_bytes!("../../languages.toml"))
6− .expect("Could not parse bultin-in languages.toml to valid toml")
7−}
8−
9−/// User configured languages.toml file, merged with the default config.
10−pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
11− let def_lang_conf = default_lang_config();
12− let data = std::fs::read(crate::config_dir().join("languages.toml"));
13− let user_lang_conf = match data {
14− Ok(raw) => {
15− let value = toml::from_slice(&raw)?;
16− merge_toml_values(def_lang_conf, value)
17− }
18− Err(_) => def_lang_conf,
19− };
20−
21− Ok(user_lang_conf)
22−}
23−
241/// Syntax configuration loader based on built-in languages.toml.
252pub fn default_syntax_loader() -> crate::syntax::Configuration {
26− default_lang_config()
3+ helix_loader::default_lang_config()
274 .try_into()
285 .expect("Could not serialize built-in languages.toml")
296}
307/// Syntax configuration loader based on user configured languages.toml.
318pub fn user_syntax_loader() -> Result<crate::syntax::Configuration, toml::de::Error> {
32− user_lang_config()?.try_into()
9+ helix_loader::user_lang_config()?.try_into()
3310}
helix-core/src/indent.rs−2
@@ -444,8 +444,6 @@ where
444444 debugger: None,
445445 auto_pairs: None,
446446 }],
447− grammar: vec![],
448− grammar_selection: None,
449447 });
450448
451449 // set runtime path so we can find the queries
helix-core/src/lib.rs−141
@@ -33,9 +33,6 @@ pub mod unicode {
3333 pub use unicode_width as width;
3434}
3535
36−static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
37− once_cell::sync::Lazy::new(runtime_dir);
38−
3936pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
4037 line.chars().position(|ch| !ch.is_whitespace())
4138}
@@ -85,144 +82,6 @@ pub fn find_root(root: Option<&str>, root_markers: &[String]) -> Option<std::pat
8582 }
8683}
8784
88−pub fn runtime_dir() -> std::path::PathBuf {
89− if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
90− return dir.into();
91− }
92−
93− const RT_DIR: &str = "runtime";
94− let conf_dir = config_dir().join(RT_DIR);
95− if conf_dir.exists() {
96− return conf_dir;
97− }
98−
99− if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
100− // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
101− return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
102− }
103−
104− // fallback to location of the executable being run
105− std::env::current_exe()
106− .ok()
107− .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
108− .unwrap()
109−}
110−
111−pub fn config_dir() -> std::path::PathBuf {
112− // TODO: allow env var override
113− let strategy = choose_base_strategy().expect("Unable to find the config directory!");
114− let mut path = strategy.config_dir();
115− path.push("helix");
116− path
117−}
118−
119−pub fn cache_dir() -> std::path::PathBuf {
120− // TODO: allow env var override
121− let strategy = choose_base_strategy().expect("Unable to find the config directory!");
122− let mut path = strategy.cache_dir();
123− path.push("helix");
124− path
125−}
126−
127−pub fn config_file() -> std::path::PathBuf {
128− config_dir().join("config.toml")
129−}
130−
131−pub fn lang_config_file() -> std::path::PathBuf {
132− config_dir().join("languages.toml")
133−}
134−
135−pub fn log_file() -> std::path::PathBuf {
136− cache_dir().join("helix.log")
137−}
138−
139−// right overrides left
140−pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value {
141− use toml::Value;
142−
143− fn get_name(v: &Value) -> Option<&str> {
144− v.get("name").and_then(Value::as_str)
145− }
146−
147− match (left, right) {
148− (Value::Array(mut left_items), Value::Array(right_items)) => {
149− left_items.reserve(right_items.len());
150− for rvalue in right_items {
151− let lvalue = get_name(&rvalue)
152− .and_then(|rname| left_items.iter().position(|v| get_name(v) == Some(rname)))
153− .map(|lpos| left_items.remove(lpos));
154− let mvalue = match lvalue {
155− Some(lvalue) => merge_toml_values(lvalue, rvalue),
156− None => rvalue,
157− };
158− left_items.push(mvalue);
159− }
160− Value::Array(left_items)
161− }
162− (Value::Table(mut left_map), Value::Table(right_map)) => {
163− for (rname, rvalue) in right_map {
164− match left_map.remove(&rname) {
165− Some(lvalue) => {
166− let merged_value = merge_toml_values(lvalue, rvalue);
167− left_map.insert(rname, merged_value);
168− }
169− None => {
170− left_map.insert(rname, rvalue);
171− }
172− }
173− }
174− Value::Table(left_map)
175− }
176− // Catch everything else we didn't handle, and use the right value
177− (_, value) => value,
178− }
179−}
180−
181−#[cfg(test)]
182−mod merge_toml_tests {
183− use super::merge_toml_values;
184−
185− #[test]
186− fn language_tomls() {
187− use toml::Value;
188−
189− const USER: &str = "
190− [[language]]
191− name = \"nix\"
192− test = \"bbb\"
193− indent = { tab-width = 4, unit = \" \", test = \"aaa\" }
194− ";
195−
196− let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
197− .expect("Couldn't parse built-in languages config");
198− let user: Value = toml::from_str(USER).unwrap();
199−
200− let merged = merge_toml_values(base, user);
201− let languages = merged.get("language").unwrap().as_array().unwrap();
202− let nix = languages
203− .iter()
204− .find(|v| v.get("name").unwrap().as_str().unwrap() == "nix")
205− .unwrap();
206− let nix_indent = nix.get("indent").unwrap();
207−
208− // We changed tab-width and unit in indent so check them if they are the new values
209− assert_eq!(
210− nix_indent.get("tab-width").unwrap().as_integer().unwrap(),
211− 4
212− );
213− assert_eq!(nix_indent.get("unit").unwrap().as_str().unwrap(), " ");
214− // We added a new keys, so check them
215− assert_eq!(nix.get("test").unwrap().as_str().unwrap(), "bbb");
216− assert_eq!(nix_indent.get("test").unwrap().as_str().unwrap(), "aaa");
217− // We didn't change comment-token so it should be same
218− assert_eq!(nix.get("comment-token").unwrap().as_str().unwrap(), "#");
219− }
220−}
221−
222−pub use etcetera::home_dir;
223−
224−use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
225−
22685pub use ropey::{Rope, RopeBuilder, RopeSlice};
22786
22887// pub use tendril::StrTendril as Tendril;
helix-core/src/path.rs+3−2
@@ -1,9 +1,10 @@
1+use etcetera::home_dir;
12use std::path::{Component, Path, PathBuf};
23
34/// Replaces users home directory from `path` with tilde `~` if the directory
45/// is available, otherwise returns the path unchanged.
56pub fn fold_home_dir(path: &Path) -> PathBuf {
6− if let Ok(home) = super::home_dir() {
7+ if let Ok(home) = home_dir() {
78 if path.starts_with(&home) {
89 // it's ok to unwrap, the path starts with home dir
910 return PathBuf::from("~").join(path.strip_prefix(&home).unwrap());
@@ -20,7 +21,7 @@ pub fn expand_tilde(path: &Path) -> PathBuf {
2021 let mut components = path.components().peekable();
2122 if let Some(Component::Normal(c)) = components.peek() {
2223 if c == &"~" {
23− if let Ok(home) = super::home_dir() {
24+ if let Ok(home) = home_dir() {
2425 // it's ok to unwrap, the path starts with `~`
2526 return home.join(path.strip_prefix("~").unwrap());
2627 }
helix-core/src/syntax.rs+6−85
@@ -7,10 +7,6 @@ use crate::{
77 Rope, RopeSlice, Tendril,
88};
99
10−use anyhow::{Context, Result};
11−use libloading::{Library, Symbol};
12−use tree_sitter::Language;
13−
1410use arc_swap::{ArcSwap, Guard};
1511use slotmap::{DefaultKey as LayerId, HopSlotMap};
1612
@@ -27,33 +23,7 @@ use std::{
2723use once_cell::sync::{Lazy, OnceCell};
2824use serde::{Deserialize, Serialize};
2925
30−#[cfg(unix)]
31−pub const DYLIB_EXTENSION: &str = "so";
32−
33−#[cfg(windows)]
34−pub const DYLIB_EXTENSION: &str = "dll";
35−
36−fn replace_dashes_with_underscores(name: &str) -> String {
37− name.replace('-', "_")
38−}
39−
40−pub fn get_language(runtime_path: &std::path::Path, name: &str) -> Result<Language> {
41− let name = name.to_ascii_lowercase();
42− let mut library_path = runtime_path.join("grammars").join(&name);
43− library_path.set_extension(DYLIB_EXTENSION);
44−
45− let library = unsafe { Library::new(&library_path) }
46− .with_context(|| format!("Error opening dynamic library {:?}", &library_path))?;
47− let language_fn_name = format!("tree_sitter_{}", replace_dashes_with_underscores(&name));
48− let language = unsafe {
49− let language_fn: Symbol<unsafe extern "C" fn() -> Language> = library
50− .get(language_fn_name.as_bytes())
51− .with_context(|| format!("Failed to load symbol {}", language_fn_name))?;
52− language_fn()
53− };
54− std::mem::forget(library);
55− Ok(language)
56−}
26+use helix_loader::grammar::{get_language, load_runtime_file};
5727
5828fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
5929where
@@ -81,19 +51,8 @@ where
8151}
8252
8353#[derive(Debug, Serialize, Deserialize)]
84−#[serde(rename_all = "kebab-case", deny_unknown_fields)]
8554pub struct Configuration {
86− #[serde(rename = "use-grammars")]
87− pub grammar_selection: Option<GrammarSelection>,
8855 pub language: Vec<LanguageConfiguration>,
89− pub grammar: Vec<GrammarConfiguration>,
90−}
91−
92−#[derive(Debug, Serialize, Deserialize)]
93−#[serde(rename_all = "lowercase", untagged)]
94−pub enum GrammarSelection {
95− Only(HashSet<String>),
96− Except(HashSet<String>),
9756}
9857
9958// largely based on tree-sitter/cli/src/loader.rs
@@ -279,29 +238,6 @@ pub struct IndentQuery {
279238 pub outdent: HashSet<String>,
280239}
281240
282−#[derive(Debug, Serialize, Deserialize)]
283−pub struct GrammarConfiguration {
284− #[serde(rename = "name")]
285− pub grammar_id: String, // c-sharp, rust
286− pub source: GrammarSource,
287− pub path: Option<String>,
288−}
289−
290−#[derive(Debug, Serialize, Deserialize)]
291−#[serde(rename_all = "lowercase")]
292−#[serde(untagged)]
293−pub enum GrammarSource {
294− Local {
295− path: String,
296− },
297− Git {
298− #[serde(rename = "git")]
299− remote: String,
300− #[serde(rename = "rev")]
301− revision: String,
302− },
303−}
304−
305241#[derive(Debug)]
306242pub struct TextObjectQuery {
307243 pub query: Query,
@@ -398,14 +334,6 @@ impl TextObjectQuery {
398334 }
399335}
400336
401−pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
402− let path = crate::RUNTIME_DIR
403− .join("queries")
404− .join(language)
405− .join(filename);
406− std::fs::read_to_string(&path)
407−}
408−
409337fn read_query(language: &str, filename: &str) -> String {
410338 static INHERITS_REGEX: Lazy<Regex> =
411339 Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
@@ -451,12 +379,9 @@ impl LanguageConfiguration {
451379 if highlights_query.is_empty() {
452380 None
453381 } else {
454− let language = get_language(
455− &crate::RUNTIME_DIR,
456− self.grammar.as_deref().unwrap_or(&self.language_id),
457− )
458− .map_err(|e| log::info!("{}", e))
459− .ok()?;
382+ let language = get_language(self.grammar.as_deref().unwrap_or(&self.language_id))
383+ .map_err(|e| log::info!("{}", e))
384+ .ok()?;
460385 let config = HighlightConfiguration::new(
461386 language,
462387 &highlights_query,
@@ -2116,13 +2041,9 @@ mod test {
21162041 .map(String::from)
21172042 .collect();
21182043
2119− let loader = Loader::new(Configuration {
2120− language: vec![],
2121− grammar: vec![],
2122− grammar_selection: None,
2123− });
2044+ let loader = Loader::new(Configuration { language: vec![] });
21242045
2125− let language = get_language(&crate::RUNTIME_DIR, "Rust").unwrap();
2046+ let language = get_language("Rust").unwrap();
21262047 let config = HighlightConfiguration::new(
21272048 language,
21282049 &std::fs::read_to_string("../runtime/grammars/sources/rust/queries/highlights.scm")
helix-loader/Cargo.tomlnew file+23
@@ -0,0 +1,23 @@
1+[package]
2+name = "helix-loader"
3+version = "0.6.0"
4+description = "A post-modern text editor."
5+authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
6+edition = "2021"
7+license = "MPL-2.0"
8+categories = ["editor"]
9+repository = "https://github.com/helix-editor/helix"
10+homepage = "https://helix-editor.com"
11+
12+[dependencies]
13+anyhow = "1"
14+serde = { version = "1.0", features = ["derive"] }
15+toml = "0.5"
16+etcetera = "0.3"
17+tree-sitter = "0.20"
18+libloading = "0.7"
19+once_cell = "1.9"
20+
21+# cloning/compiling tree-sitter grammars
22+cc = { version = "1" }
23+threadpool = { version = "1.0" }
helix-loader/build.rsnew file+6
@@ -0,0 +1,6 @@
1+fn main() {
2+ println!(
3+ "cargo:rustc-env=BUILD_TARGET={}",
4+ std::env::var("TARGET").unwrap()
5+ );
6+}
helix-term/src/grammars.rs → helix-loader/src/grammar.rsrenamed+161−69
@@ -1,63 +1,155 @@
11use anyhow::{anyhow, Context, Result};
2+use libloading::{Library, Symbol};
3+use serde::{Deserialize, Serialize};
24use std::fs;
35use std::time::SystemTime;
46use std::{
7+ collections::HashSet,
58 path::{Path, PathBuf},
69 process::Command,
710 sync::mpsc::channel,
811};
12+use tree_sitter::Language;
913
10−use helix_core::syntax::{GrammarConfiguration, GrammarSelection, GrammarSource, DYLIB_EXTENSION};
14+#[cfg(unix)]
15+const DYLIB_EXTENSION: &str = "so";
16+
17+#[cfg(windows)]
18+const DYLIB_EXTENSION: &str = "dll";
19+
20+#[derive(Debug, Serialize, Deserialize)]
21+struct Configuration {
22+ #[serde(rename = "use-grammars")]
23+ pub grammar_selection: Option<GrammarSelection>,
24+ pub grammar: Vec<GrammarConfiguration>,
25+}
26+
27+#[derive(Debug, Serialize, Deserialize)]
28+#[serde(rename_all = "lowercase", untagged)]
29+pub enum GrammarSelection {
30+ Only(HashSet<String>),
31+ Except(HashSet<String>),
32+}
33+
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,
40+}
41+
42+#[derive(Debug, Serialize, Deserialize)]
43+#[serde(rename_all = "lowercase", untagged)]
44+pub enum GrammarSource {
45+ Local {
46+ path: String,
47+ },
48+ Git {
49+ #[serde(rename = "git")]
50+ remote: String,
51+ #[serde(rename = "rev")]
52+ revision: String,
53+ subpath: Option<String>,
54+ },
55+}
1156
1257const BUILD_TARGET: &str = env!("BUILD_TARGET");
1358const REMOTE_NAME: &str = "origin";
1459
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);
64+
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}"))?;
72+ language_fn()
73+ };
74+ std::mem::forget(library);
75+ Ok(language)
76+}
77+
1578pub fn fetch_grammars() -> Result<()> {
16− run_parallel(get_grammar_configs(), fetch_grammar, "fetch")
79+ run_parallel(get_grammar_configs()?, fetch_grammar, "fetch")
1780}
1881
1982pub fn build_grammars() -> Result<()> {
20− run_parallel(get_grammar_configs(), build_grammar, "build")
83+ run_parallel(get_grammar_configs()?, build_grammar, "build")
84+}
85+
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")?
93+ .try_into()?;
94+
95+ let grammars = match config.grammar_selection {
96+ Some(GrammarSelection::Only(selections)) => config
97+ .grammar
98+ .into_iter()
99+ .filter(|grammar| selections.contains(&grammar.grammar_id))
100+ .collect(),
101+ Some(GrammarSelection::Except(rejections)) => config
102+ .grammar
103+ .into_iter()
104+ .filter(|grammar| !rejections.contains(&grammar.grammar_id))
105+ .collect(),
106+ None => config.grammar,
107+ };
108+
109+ Ok(grammars)
21110}
22111
23112fn run_parallel<F>(grammars: Vec<GrammarConfiguration>, job: F, action: &'static str) -> Result<()>
24113where
25114 F: Fn(GrammarConfiguration) -> Result<()> + std::marker::Send + 'static + Copy,
26115{
27− let mut n_jobs = 0;
28116 let pool = threadpool::Builder::new().build();
29117 let (tx, rx) = channel();
30118
31119 for grammar in grammars {
32120 let tx = tx.clone();
33− n_jobs += 1;
34121
35122 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)
39− });
40−
41− // report progress
42− tx.send(1).unwrap();
123+ tx.send(job(grammar)).unwrap();
43124 });
44125 }
45126 pool.join();
46127
47− if rx.try_iter().sum::<usize>() == n_jobs {
48− Ok(())
128+ // TODO: print all failures instead of the first one found.
129+ if let Some(failure) = rx.try_iter().find_map(|result| result.err()) {
130+ Err(anyhow!(
131+ "Failed to {} some grammar(s).\n{}",
132+ action,
133+ failure
134+ ))
49135 } else {
50− Err(anyhow!("Failed to {} some grammar(s).", action))
136+ Ok(())
51137 }
52138}
53139
54140fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
55− if let GrammarSource::Git { remote, revision } = grammar.source {
56− let grammar_dir = helix_core::runtime_dir()
141+ if let GrammarSource::Git {
142+ remote, revision, ..
143+ } = grammar.source
144+ {
145+ let grammar_dir = crate::runtime_dir()
57146 .join("grammars/sources")
58− .join(grammar.grammar_id.clone());
147+ .join(&grammar.grammar_id);
59148
60− fs::create_dir_all(grammar_dir.clone()).expect("Could not create grammar directory");
149+ fs::create_dir_all(&grammar_dir).context(format!(
150+ "Could not create grammar directory {:?}",
151+ grammar_dir
152+ ))?;
61153
62154 // create the grammar dir contains a git directory
63155 if !grammar_dir.join(".git").is_dir() {
@@ -65,12 +157,12 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
65157 }
66158
67159 // 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) {
69161 set_remote(&grammar_dir, &remote)?;
70162 }
71163
72164 // 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) {
74166 // Fetch the exact revision from the remote.
75167 // Supported by server-side git since v2.5.0 (July 2015),
76168 // enabled by default on major git hosts.
@@ -94,33 +186,38 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
94186
95187// Sets the remote for a repository to the given URL, creating the remote if
96188// it does not yet exist.
97−fn set_remote(repository: &Path, remote_url: &str) -> Result<String> {
98− git(repository, ["remote", "set-url", REMOTE_NAME, remote_url])
99− .or_else(|_| git(repository, ["remote", "add", REMOTE_NAME, remote_url]))
189+fn set_remote(repository_dir: &Path, remote_url: &str) -> Result<String> {
190+ git(
191+ repository_dir,
192+ ["remote", "set-url", REMOTE_NAME, remote_url],
193+ )
194+ .or_else(|_| git(repository_dir, ["remote", "add", REMOTE_NAME, remote_url]))
100195}
101196
102−fn get_remote_url(repository: &Path) -> Option<String> {
103− git(repository, ["remote", "get-url", REMOTE_NAME]).ok()
197+fn get_remote_url(repository_dir: &Path) -> Option<String> {
198+ git(repository_dir, ["remote", "get-url", REMOTE_NAME]).ok()
104199}
105200
106−fn get_revision(repository: &Path) -> Option<String> {
107− git(repository, ["rev-parse", "HEAD"]).ok()
201+fn get_revision(repository_dir: &Path) -> Option<String> {
202+ git(repository_dir, ["rev-parse", "HEAD"]).ok()
108203}
109204
110205// A wrapper around 'git' commands which returns stdout in success and a
111206// 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>
113208where
114209 I: IntoIterator<Item = S>,
115210 S: AsRef<std::ffi::OsStr>,
116211{
117212 let output = Command::new("git")
118213 .args(args)
119− .current_dir(repository)
214+ .current_dir(repository_dir)
120215 .output()?;
121216
122217 if output.status.success() {
123− Ok(String::from_utf8_lossy(&output.stdout).into_owned())
218+ Ok(String::from_utf8_lossy(&output.stdout)
219+ .trim_end()
220+ .to_owned())
124221 } else {
125222 // TODO: figure out how to display the git command using `args`
126223 Err(anyhow!(
@@ -132,52 +229,37 @@ where
132229}
133230
134231fn build_grammar(grammar: GrammarConfiguration) -> Result<()> {
135− let grammar_dir = if let GrammarSource::Local { ref path } = grammar.source {
136− PathBuf::from(path)
232+ println!("{:#?}", grammar);
233+ let grammar_dir = if let GrammarSource::Local { path } = &grammar.source {
234+ PathBuf::from(&path)
137235 } else {
138− helix_core::runtime_dir()
236+ crate::runtime_dir()
139237 .join("grammars/sources")
140− .join(grammar.grammar_id.clone())
238+ .join(&grammar.grammar_id)
141239 };
142240
143− grammar_dir.read_dir().with_context(|| {
144− format!(
145− "The directory {:?} is empty, you probably need to use 'hx --fetch-grammars'?",
146− grammar_dir
147− )
241+ let grammar_dir_entries = grammar_dir.read_dir().with_context(|| {
242+ format!("Failed to read directory {grammar_dir:?}. Did you use 'hx --fetch-grammars'?")
148243 })?;
149244
150− let path = match grammar.path {
151− Some(ref subpath) => grammar_dir.join(subpath),
152− None => grammar_dir,
245+ if grammar_dir_entries.count() == 0 {
246+ return Err(anyhow!(
247+ "Directory {grammar_dir:?} is empty. Did you use 'hx --fetch-grammars'?"
248+ ));
249+ };
250+
251+ let path = match &grammar.source {
252+ GrammarSource::Git {
253+ subpath: Some(subpath),
254+ ..
255+ } => grammar_dir.join(subpath),
256+ _ => grammar_dir,
153257 }
154258 .join("src");
155259
156260 build_tree_sitter_library(&path, grammar)
157261}
158262
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");
165−
166− match config.grammar_selection {
167− Some(GrammarSelection::Only(selections)) => config
168− .grammar
169− .into_iter()
170− .filter(|grammar| selections.contains(&grammar.grammar_id))
171− .collect(),
172− Some(GrammarSelection::Except(rejections)) => config
173− .grammar
174− .into_iter()
175− .filter(|grammar| !rejections.contains(&grammar.grammar_id))
176− .collect(),
177− None => config.grammar,
178− }
179−}
180−
181263fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) -> Result<()> {
182264 let header_path = src_path;
183265 let parser_path = src_path.join("parser.c");
@@ -193,8 +275,8 @@ fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) ->
193275 None
194276 }
195277 };
196− let parser_lib_path = helix_core::runtime_dir().join("../runtime/grammars");
197− let mut library_path = parser_lib_path.join(grammar.grammar_id.clone());
278+ let parser_lib_path = crate::runtime_dir().join("grammars");
279+ let mut library_path = parser_lib_path.join(&grammar.grammar_id);
198280 library_path.set_extension(DYLIB_EXTENSION);
199281
200282 let recompile = needs_recompile(&library_path, &parser_path, &scanner_path)
@@ -210,7 +292,7 @@ fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) ->
210292 let mut config = cc::Build::new();
211293 config
212294 .cpp(true)
213− .opt_level(2)
295+ .opt_level(3)
214296 .cargo_metadata(false)
215297 .host(BUILD_TARGET)
216298 .target(BUILD_TARGET);
@@ -245,7 +327,7 @@ fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) ->
245327 .arg(header_path)
246328 .arg("-o")
247329 .arg(&library_path)
248− .arg("-O2");
330+ .arg("-O3");
249331 if let Some(scanner_path) = scanner_path.as_ref() {
250332 if scanner_path.extension() == Some("c".as_ref()) {
251333 command.arg("-xc").arg("-std=c99").arg(scanner_path);
@@ -294,3 +376,13 @@ fn needs_recompile(
294376fn mtime(path: &Path) -> Result<SystemTime> {
295377 Ok(fs::metadata(path)?.modified()?)
296378}
379+
380+/// Gives the contents of a file from a language's `runtime/queries/<lang>`
381+/// directory
382+pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
383+ let path = crate::RUNTIME_DIR
384+ .join("queries")
385+ .join(language)
386+ .join(filename);
387+ std::fs::read_to_string(&path)
388+}
helix-loader/src/lib.rsnew file+161
@@ -0,0 +1,161 @@
1+pub mod grammar;
2+
3+use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
4+
5+pub static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
6+ once_cell::sync::Lazy::new(runtime_dir);
7+
8+pub fn runtime_dir() -> std::path::PathBuf {
9+ if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
10+ return dir.into();
11+ }
12+
13+ const RT_DIR: &str = "runtime";
14+ let conf_dir = config_dir().join(RT_DIR);
15+ if conf_dir.exists() {
16+ return conf_dir;
17+ }
18+
19+ if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
20+ // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
21+ return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
22+ }
23+
24+ // fallback to location of the executable being run
25+ std::env::current_exe()
26+ .ok()
27+ .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
28+ .unwrap()
29+}
30+
31+pub fn config_dir() -> std::path::PathBuf {
32+ // TODO: allow env var override
33+ let strategy = choose_base_strategy().expect("Unable to find the config directory!");
34+ let mut path = strategy.config_dir();
35+ path.push("helix");
36+ path
37+}
38+
39+pub fn cache_dir() -> std::path::PathBuf {
40+ // TODO: allow env var override
41+ let strategy = choose_base_strategy().expect("Unable to find the config directory!");
42+ let mut path = strategy.cache_dir();
43+ path.push("helix");
44+ path
45+}
46+
47+pub fn config_file() -> std::path::PathBuf {
48+ config_dir().join("config.toml")
49+}
50+
51+pub fn lang_config_file() -> std::path::PathBuf {
52+ config_dir().join("languages.toml")
53+}
54+
55+pub fn log_file() -> std::path::PathBuf {
56+ cache_dir().join("helix.log")
57+}
58+
59+/// Default bultin-in languages.toml.
60+pub fn default_lang_config() -> toml::Value {
61+ toml::from_slice(include_bytes!("../../languages.toml"))
62+ .expect("Could not parse bultin-in languages.toml to valid toml")
63+}
64+
65+/// User configured languages.toml file, merged with the default config.
66+pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
67+ let def_lang_conf = default_lang_config();
68+ let data = std::fs::read(crate::config_dir().join("languages.toml"));
69+ let user_lang_conf = match data {
70+ Ok(raw) => {
71+ let value = toml::from_slice(&raw)?;
72+ merge_toml_values(def_lang_conf, value)
73+ }
74+ Err(_) => def_lang_conf,
75+ };
76+
77+ Ok(user_lang_conf)
78+}
79+
80+// right overrides left
81+pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value {
82+ use toml::Value;
83+
84+ fn get_name(v: &Value) -> Option<&str> {
85+ v.get("name").and_then(Value::as_str)
86+ }
87+
88+ match (left, right) {
89+ (Value::Array(mut left_items), Value::Array(right_items)) => {
90+ left_items.reserve(right_items.len());
91+ for rvalue in right_items {
92+ let lvalue = get_name(&rvalue)
93+ .and_then(|rname| left_items.iter().position(|v| get_name(v) == Some(rname)))
94+ .map(|lpos| left_items.remove(lpos));
95+ let mvalue = match lvalue {
96+ Some(lvalue) => merge_toml_values(lvalue, rvalue),
97+ None => rvalue,
98+ };
99+ left_items.push(mvalue);
100+ }
101+ Value::Array(left_items)
102+ }
103+ (Value::Table(mut left_map), Value::Table(right_map)) => {
104+ for (rname, rvalue) in right_map {
105+ match left_map.remove(&rname) {
106+ Some(lvalue) => {
107+ let merged_value = merge_toml_values(lvalue, rvalue);
108+ left_map.insert(rname, merged_value);
109+ }
110+ None => {
111+ left_map.insert(rname, rvalue);
112+ }
113+ }
114+ }
115+ Value::Table(left_map)
116+ }
117+ // Catch everything else we didn't handle, and use the right value
118+ (_, value) => value,
119+ }
120+}
121+
122+#[cfg(test)]
123+mod merge_toml_tests {
124+ use super::merge_toml_values;
125+
126+ #[test]
127+ fn language_tomls() {
128+ use toml::Value;
129+
130+ const USER: &str = "
131+ [[language]]
132+ name = \"nix\"
133+ test = \"bbb\"
134+ indent = { tab-width = 4, unit = \" \", test = \"aaa\" }
135+ ";
136+
137+ let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
138+ .expect("Couldn't parse built-in languages config");
139+ let user: Value = toml::from_str(USER).unwrap();
140+
141+ let merged = merge_toml_values(base, user);
142+ let languages = merged.get("language").unwrap().as_array().unwrap();
143+ let nix = languages
144+ .iter()
145+ .find(|v| v.get("name").unwrap().as_str().unwrap() == "nix")
146+ .unwrap();
147+ let nix_indent = nix.get("indent").unwrap();
148+
149+ // We changed tab-width and unit in indent so check them if they are the new values
150+ assert_eq!(
151+ nix_indent.get("tab-width").unwrap().as_integer().unwrap(),
152+ 4
153+ );
154+ assert_eq!(nix_indent.get("unit").unwrap().as_str().unwrap(), " ");
155+ // We added a new keys, so check them
156+ assert_eq!(nix.get("test").unwrap().as_str().unwrap(), "bbb");
157+ assert_eq!(nix_indent.get("test").unwrap().as_str().unwrap(), "aaa");
158+ // We didn't change comment-token so it should be same
159+ assert_eq!(nix.get("comment-token").unwrap().as_str().unwrap(), "#");
160+ }
161+}
helix-term/Cargo.toml+1
@@ -26,6 +26,7 @@ helix-core = { version = "0.6", path = "../helix-core" }
2626helix-view = { version = "0.6", path = "../helix-view" }
2727helix-lsp = { version = "0.6", path = "../helix-lsp" }
2828helix-dap = { version = "0.6", path = "../helix-dap" }
29+helix-loader = { version = "0.6", path = "../helix-loader" }
2930
3031anyhow = "1"
3132once_cell = "1.10"
helix-term/build.rs−5
@@ -14,10 +14,5 @@ fn main() {
1414 None => env!("CARGO_PKG_VERSION").into(),
1515 };
1616
17− println!(
18− "cargo:rustc-env=BUILD_TARGET={}",
19− std::env::var("TARGET").unwrap()
20− );
21−
2217 println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
2318}
helix-term/src/application.rs+3−3
@@ -61,10 +61,10 @@ impl Application {
6161 let mut compositor = Compositor::new()?;
6262 let size = compositor.size();
6363
64− let conf_dir = helix_core::config_dir();
64+ let conf_dir = helix_loader::config_dir();
6565
6666 let theme_loader =
67− std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
67+ std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_loader::runtime_dir()));
6868
6969 let true_color = config.editor.true_color || crate::true_color();
7070 let theme = config
@@ -109,7 +109,7 @@ impl Application {
109109 compositor.push(editor_view);
110110
111111 if args.load_tutor {
112− let path = helix_core::runtime_dir().join("tutor.txt");
112+ let path = helix_loader::runtime_dir().join("tutor.txt");
113113 editor.open(path, Action::VerticalSplit)?;
114114 // Unset path to prevent accidentally saving to the original tutor file.
115115 doc_mut!(editor).set_path(None)?;
helix-term/src/commands/typed.rs+1−1
@@ -828,7 +828,7 @@ fn tutor(
828828 _args: &[Cow<str>],
829829 _event: PromptEvent,
830830) -> anyhow::Result<()> {
831− let path = helix_core::runtime_dir().join("tutor.txt");
831+ let path = helix_loader::runtime_dir().join("tutor.txt");
832832 cx.editor.open(path, Action::Replace)?;
833833 // Unset path to prevent accidentally saving to the original tutor file.
834834 doc_mut!(cx.editor).set_path(None)?;
helix-term/src/health.rs+6−8
@@ -1,8 +1,6 @@
11use crossterm::style::{Color, Print, Stylize};
2−use helix_core::{
3− config::{default_syntax_loader, user_syntax_loader},
4− syntax::load_runtime_file,
5−};
2+use helix_core::config::{default_syntax_loader, user_syntax_loader};
3+use helix_loader::grammar::load_runtime_file;
64
75#[derive(Copy, Clone)]
86pub enum TsFeature {
@@ -43,10 +41,10 @@ impl TsFeature {
4341
4442/// Display general diagnostics.
4543pub fn general() {
46− let config_file = helix_core::config_file();
47− let lang_file = helix_core::lang_config_file();
48− let log_file = helix_core::log_file();
49− let rt_dir = helix_core::runtime_dir();
44+ let config_file = helix_loader::config_file();
45+ let lang_file = helix_loader::lang_config_file();
46+ let log_file = helix_loader::log_file();
47+ let rt_dir = helix_loader::runtime_dir();
5048
5149 if config_file.exists() {
5250 println!("Config file: {}", config_file.display());
helix-term/src/lib.rs−1
@@ -7,7 +7,6 @@ pub mod commands;
77pub mod compositor;
88pub mod config;
99pub mod health;
10−pub mod grammars;
1110pub mod job;
1211pub mod keymap;
1312pub mod ui;
helix-term/src/main.rs+5−5
@@ -40,7 +40,7 @@ fn main() -> Result<()> {
4040
4141#[tokio::main]
4242async fn main_impl() -> Result<i32> {
43− let logpath = helix_core::log_file();
43+ let logpath = helix_loader::log_file();
4444 let parent = logpath.parent().unwrap();
4545 if !parent.exists() {
4646 std::fs::create_dir_all(parent).ok();
@@ -105,21 +105,21 @@ FLAGS:
105105 }
106106
107107 if args.fetch_grammars {
108− helix_term::grammars::fetch_grammars()?;
108+ helix_loader::grammar::fetch_grammars()?;
109109 return Ok(0);
110110 }
111111
112112 if args.build_grammars {
113− helix_term::grammars::build_grammars()?;
113+ helix_loader::grammar::build_grammars()?;
114114 return Ok(0);
115115 }
116116
117− let conf_dir = helix_core::config_dir();
117+ let conf_dir = helix_loader::config_dir();
118118 if !conf_dir.exists() {
119119 std::fs::create_dir_all(&conf_dir).ok();
120120 }
121121
122− let config = match std::fs::read_to_string(helix_core::config_file()) {
122+ let config = match std::fs::read_to_string(helix_loader::config_file()) {
123123 Ok(config) => toml::from_str(&config)
124124 .map(merge_keys)
125125 .unwrap_or_else(|err| {
helix-term/src/ui/mod.rs+2−2
@@ -218,9 +218,9 @@ pub mod completers {
218218 }
219219
220220 pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
221− let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes"));
221+ let mut names = theme::Loader::read_names(&helix_loader::runtime_dir().join("themes"));
222222 names.extend(theme::Loader::read_names(
223− &helix_core::config_dir().join("themes"),
223+ &helix_loader::config_dir().join("themes"),
224224 ));
225225 names.push("default".into());
226226 names.push("base16_default".into());
languages.toml+4−8
@@ -302,8 +302,7 @@ indent = { tab-width = 2, unit = " " }
302302
303303[[grammar]]
304304name = "typescript"
305−source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "3e897ea5925f037cfae2e551f8e6b12eec2a201a" }
306−path = "typescript"
305+source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "3e897ea5925f037cfae2e551f8e6b12eec2a201a", subpath = "typescript" }
307306
308307[[language]]
309308name = "tsx"
@@ -317,8 +316,7 @@ indent = { tab-width = 2, unit = " " }
317316
318317[[grammar]]
319318name = "tsx"
320−source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "3e897ea5925f037cfae2e551f8e6b12eec2a201a" }
321−path = "tsx"
319+source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "3e897ea5925f037cfae2e551f8e6b12eec2a201a", subpath = "tsx" }
322320
323321[[language]]
324322name = "css"
@@ -522,8 +520,7 @@ indent = { tab-width = 2, unit = " " }
522520
523521[[grammar]]
524522name = "ocaml"
525−source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "23d419ba45789c5a47d31448061557716b02750a" }
526−path = "ocaml"
523+source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "23d419ba45789c5a47d31448061557716b02750a", subpath = "ocaml" }
527524
528525[[language]]
529526name = "ocaml-interface"
@@ -536,8 +533,7 @@ indent = { tab-width = 2, unit = " "}
536533
537534[[grammar]]
538535name = "ocaml-interface"
539−source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "23d419ba45789c5a47d31448061557716b02750a" }
540−path = "interface"
536+source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "23d419ba45789c5a47d31448061557716b02750a", subpath = "interface" }
541537
542538[[language]]
543539name = "lua"