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.
This commit is contained in:
parent
08ee949dcb
commit
4fc991fdec
23 changed files with 419 additions and 374 deletions
19
Cargo.lock
generated
19
Cargo.lock
generated
|
@ -358,12 +358,11 @@ dependencies = [
|
||||||
name = "helix-core"
|
name = "helix-core"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"chrono",
|
"chrono",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"etcetera",
|
"etcetera",
|
||||||
"libloading",
|
"helix-loader",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"quickcheck",
|
"quickcheck",
|
||||||
|
@ -397,6 +396,21 @@ dependencies = [
|
||||||
"which",
|
"which",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "helix-loader"
|
||||||
|
version = "0.6.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"cc",
|
||||||
|
"etcetera",
|
||||||
|
"libloading",
|
||||||
|
"once_cell",
|
||||||
|
"serde",
|
||||||
|
"threadpool",
|
||||||
|
"toml",
|
||||||
|
"tree-sitter",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "helix-lsp"
|
name = "helix-lsp"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
@ -432,6 +446,7 @@ dependencies = [
|
||||||
"grep-searcher",
|
"grep-searcher",
|
||||||
"helix-core",
|
"helix-core",
|
||||||
"helix-dap",
|
"helix-dap",
|
||||||
|
"helix-loader",
|
||||||
"helix-lsp",
|
"helix-lsp",
|
||||||
"helix-tui",
|
"helix-tui",
|
||||||
"helix-view",
|
"helix-view",
|
||||||
|
|
|
@ -6,6 +6,7 @@ members = [
|
||||||
"helix-tui",
|
"helix-tui",
|
||||||
"helix-lsp",
|
"helix-lsp",
|
||||||
"helix-dap",
|
"helix-dap",
|
||||||
|
"helix-loader",
|
||||||
"xtask",
|
"xtask",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,6 @@ Grammar configuration takes these keys:
|
||||||
| Key | Description |
|
| Key | Description |
|
||||||
| --- | ----------- |
|
| --- | ----------- |
|
||||||
| `name` | The name of the tree-sitter grammar |
|
| `name` | The name of the tree-sitter grammar |
|
||||||
| `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 |
|
|
||||||
| `source` | The method of fetching the grammar - a table with a schema defined below |
|
| `source` | The method of fetching the grammar - a table with a schema defined below |
|
||||||
|
|
||||||
Where `source` is a table with either these keys when using a grammar from a
|
Where `source` is a table with either these keys when using a grammar from a
|
||||||
|
@ -61,6 +60,7 @@ git repository:
|
||||||
| --- | ----------- |
|
| --- | ----------- |
|
||||||
| `git` | A git remote URL from which the grammar should be cloned |
|
| `git` | A git remote URL from which the grammar should be cloned |
|
||||||
| `rev` | The revision (commit hash or tag) which should be fetched |
|
| `rev` | The revision (commit hash or tag) which should be fetched |
|
||||||
|
| `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 |
|
||||||
|
|
||||||
Or a `path` key with an absolute path to a locally available grammar directory.
|
Or a `path` key with an absolute path to a locally available grammar directory.
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
| ----------- | ----------- |
|
| ----------- | ----------- |
|
||||||
| helix-core | Core editing primitives, functional. |
|
| helix-core | Core editing primitives, functional. |
|
||||||
| helix-lsp | Language server client |
|
| helix-lsp | Language server client |
|
||||||
|
| helix-dap | Debug Adapter Protocol (DAP) client |
|
||||||
|
| helix-loader | Functions for building, fetching, and loading external resources |
|
||||||
| helix-view | UI abstractions for use in backends, imperative shell. |
|
| helix-view | UI abstractions for use in backends, imperative shell. |
|
||||||
| helix-term | Terminal UI |
|
| helix-term | Terminal UI |
|
||||||
| helix-tui | TUI primitives, forked from tui-rs, inspired by Cursive |
|
| helix-tui | TUI primitives, forked from tui-rs, inspired by Cursive |
|
||||||
|
|
|
@ -13,6 +13,8 @@ include = ["src/**/*", "README.md"]
|
||||||
[features]
|
[features]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
helix-loader = { version = "0.6", path = "../helix-loader" }
|
||||||
|
|
||||||
ropey = "1.3"
|
ropey = "1.3"
|
||||||
smallvec = "1.8"
|
smallvec = "1.8"
|
||||||
smartstring = "1.0.0"
|
smartstring = "1.0.0"
|
||||||
|
@ -33,13 +35,11 @@ toml = "0.5"
|
||||||
|
|
||||||
similar = "2.1"
|
similar = "2.1"
|
||||||
|
|
||||||
etcetera = "0.3"
|
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
|
|
||||||
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
|
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
|
||||||
|
|
||||||
libloading = "0.7"
|
etcetera = "0.3"
|
||||||
anyhow = "1"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
quickcheck = { version = "1", default-features = false }
|
quickcheck = { version = "1", default-features = false }
|
||||||
|
|
|
@ -1,33 +1,10 @@
|
||||||
use crate::merge_toml_values;
|
|
||||||
|
|
||||||
/// Default bultin-in languages.toml.
|
|
||||||
pub fn default_lang_config() -> toml::Value {
|
|
||||||
toml::from_slice(include_bytes!("../../languages.toml"))
|
|
||||||
.expect("Could not parse bultin-in languages.toml to valid toml")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User configured languages.toml file, merged with the default config.
|
|
||||||
pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
|
|
||||||
let def_lang_conf = default_lang_config();
|
|
||||||
let data = std::fs::read(crate::config_dir().join("languages.toml"));
|
|
||||||
let user_lang_conf = match data {
|
|
||||||
Ok(raw) => {
|
|
||||||
let value = toml::from_slice(&raw)?;
|
|
||||||
merge_toml_values(def_lang_conf, value)
|
|
||||||
}
|
|
||||||
Err(_) => def_lang_conf,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(user_lang_conf)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Syntax configuration loader based on built-in languages.toml.
|
/// Syntax configuration loader based on built-in languages.toml.
|
||||||
pub fn default_syntax_loader() -> crate::syntax::Configuration {
|
pub fn default_syntax_loader() -> crate::syntax::Configuration {
|
||||||
default_lang_config()
|
helix_loader::default_lang_config()
|
||||||
.try_into()
|
.try_into()
|
||||||
.expect("Could not serialize built-in languages.toml")
|
.expect("Could not serialize built-in languages.toml")
|
||||||
}
|
}
|
||||||
/// Syntax configuration loader based on user configured languages.toml.
|
/// Syntax configuration loader based on user configured languages.toml.
|
||||||
pub fn user_syntax_loader() -> Result<crate::syntax::Configuration, toml::de::Error> {
|
pub fn user_syntax_loader() -> Result<crate::syntax::Configuration, toml::de::Error> {
|
||||||
user_lang_config()?.try_into()
|
helix_loader::user_lang_config()?.try_into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -444,8 +444,6 @@ where
|
||||||
debugger: None,
|
debugger: None,
|
||||||
auto_pairs: None,
|
auto_pairs: None,
|
||||||
}],
|
}],
|
||||||
grammar: vec![],
|
|
||||||
grammar_selection: None,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// set runtime path so we can find the queries
|
// set runtime path so we can find the queries
|
||||||
|
|
|
@ -33,9 +33,6 @@ pub mod unicode {
|
||||||
pub use unicode_width as width;
|
pub use unicode_width as width;
|
||||||
}
|
}
|
||||||
|
|
||||||
static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
|
|
||||||
once_cell::sync::Lazy::new(runtime_dir);
|
|
||||||
|
|
||||||
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
|
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
|
||||||
line.chars().position(|ch| !ch.is_whitespace())
|
line.chars().position(|ch| !ch.is_whitespace())
|
||||||
}
|
}
|
||||||
|
@ -85,144 +82,6 @@ pub fn find_root(root: Option<&str>, root_markers: &[String]) -> Option<std::pat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runtime_dir() -> std::path::PathBuf {
|
|
||||||
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
|
|
||||||
return dir.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
const RT_DIR: &str = "runtime";
|
|
||||||
let conf_dir = config_dir().join(RT_DIR);
|
|
||||||
if conf_dir.exists() {
|
|
||||||
return conf_dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
|
|
||||||
// this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
|
|
||||||
return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback to location of the executable being run
|
|
||||||
std::env::current_exe()
|
|
||||||
.ok()
|
|
||||||
.and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config_dir() -> std::path::PathBuf {
|
|
||||||
// TODO: allow env var override
|
|
||||||
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
|
|
||||||
let mut path = strategy.config_dir();
|
|
||||||
path.push("helix");
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache_dir() -> std::path::PathBuf {
|
|
||||||
// TODO: allow env var override
|
|
||||||
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
|
|
||||||
let mut path = strategy.cache_dir();
|
|
||||||
path.push("helix");
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config_file() -> std::path::PathBuf {
|
|
||||||
config_dir().join("config.toml")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lang_config_file() -> std::path::PathBuf {
|
|
||||||
config_dir().join("languages.toml")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log_file() -> std::path::PathBuf {
|
|
||||||
cache_dir().join("helix.log")
|
|
||||||
}
|
|
||||||
|
|
||||||
// right overrides left
|
|
||||||
pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value {
|
|
||||||
use toml::Value;
|
|
||||||
|
|
||||||
fn get_name(v: &Value) -> Option<&str> {
|
|
||||||
v.get("name").and_then(Value::as_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
match (left, right) {
|
|
||||||
(Value::Array(mut left_items), Value::Array(right_items)) => {
|
|
||||||
left_items.reserve(right_items.len());
|
|
||||||
for rvalue in right_items {
|
|
||||||
let lvalue = get_name(&rvalue)
|
|
||||||
.and_then(|rname| left_items.iter().position(|v| get_name(v) == Some(rname)))
|
|
||||||
.map(|lpos| left_items.remove(lpos));
|
|
||||||
let mvalue = match lvalue {
|
|
||||||
Some(lvalue) => merge_toml_values(lvalue, rvalue),
|
|
||||||
None => rvalue,
|
|
||||||
};
|
|
||||||
left_items.push(mvalue);
|
|
||||||
}
|
|
||||||
Value::Array(left_items)
|
|
||||||
}
|
|
||||||
(Value::Table(mut left_map), Value::Table(right_map)) => {
|
|
||||||
for (rname, rvalue) in right_map {
|
|
||||||
match left_map.remove(&rname) {
|
|
||||||
Some(lvalue) => {
|
|
||||||
let merged_value = merge_toml_values(lvalue, rvalue);
|
|
||||||
left_map.insert(rname, merged_value);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
left_map.insert(rname, rvalue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Value::Table(left_map)
|
|
||||||
}
|
|
||||||
// Catch everything else we didn't handle, and use the right value
|
|
||||||
(_, value) => value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod merge_toml_tests {
|
|
||||||
use super::merge_toml_values;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn language_tomls() {
|
|
||||||
use toml::Value;
|
|
||||||
|
|
||||||
const USER: &str = "
|
|
||||||
[[language]]
|
|
||||||
name = \"nix\"
|
|
||||||
test = \"bbb\"
|
|
||||||
indent = { tab-width = 4, unit = \" \", test = \"aaa\" }
|
|
||||||
";
|
|
||||||
|
|
||||||
let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
|
|
||||||
.expect("Couldn't parse built-in languages config");
|
|
||||||
let user: Value = toml::from_str(USER).unwrap();
|
|
||||||
|
|
||||||
let merged = merge_toml_values(base, user);
|
|
||||||
let languages = merged.get("language").unwrap().as_array().unwrap();
|
|
||||||
let nix = languages
|
|
||||||
.iter()
|
|
||||||
.find(|v| v.get("name").unwrap().as_str().unwrap() == "nix")
|
|
||||||
.unwrap();
|
|
||||||
let nix_indent = nix.get("indent").unwrap();
|
|
||||||
|
|
||||||
// We changed tab-width and unit in indent so check them if they are the new values
|
|
||||||
assert_eq!(
|
|
||||||
nix_indent.get("tab-width").unwrap().as_integer().unwrap(),
|
|
||||||
4
|
|
||||||
);
|
|
||||||
assert_eq!(nix_indent.get("unit").unwrap().as_str().unwrap(), " ");
|
|
||||||
// We added a new keys, so check them
|
|
||||||
assert_eq!(nix.get("test").unwrap().as_str().unwrap(), "bbb");
|
|
||||||
assert_eq!(nix_indent.get("test").unwrap().as_str().unwrap(), "aaa");
|
|
||||||
// We didn't change comment-token so it should be same
|
|
||||||
assert_eq!(nix.get("comment-token").unwrap().as_str().unwrap(), "#");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use etcetera::home_dir;
|
|
||||||
|
|
||||||
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
|
|
||||||
|
|
||||||
pub use ropey::{Rope, RopeBuilder, RopeSlice};
|
pub use ropey::{Rope, RopeBuilder, RopeSlice};
|
||||||
|
|
||||||
// pub use tendril::StrTendril as Tendril;
|
// pub use tendril::StrTendril as Tendril;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
use etcetera::home_dir;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
/// Replaces users home directory from `path` with tilde `~` if the directory
|
/// Replaces users home directory from `path` with tilde `~` if the directory
|
||||||
/// is available, otherwise returns the path unchanged.
|
/// is available, otherwise returns the path unchanged.
|
||||||
pub fn fold_home_dir(path: &Path) -> PathBuf {
|
pub fn fold_home_dir(path: &Path) -> PathBuf {
|
||||||
if let Ok(home) = super::home_dir() {
|
if let Ok(home) = home_dir() {
|
||||||
if path.starts_with(&home) {
|
if path.starts_with(&home) {
|
||||||
// it's ok to unwrap, the path starts with home dir
|
// it's ok to unwrap, the path starts with home dir
|
||||||
return PathBuf::from("~").join(path.strip_prefix(&home).unwrap());
|
return PathBuf::from("~").join(path.strip_prefix(&home).unwrap());
|
||||||
|
@ -20,7 +21,7 @@ pub fn expand_tilde(path: &Path) -> PathBuf {
|
||||||
let mut components = path.components().peekable();
|
let mut components = path.components().peekable();
|
||||||
if let Some(Component::Normal(c)) = components.peek() {
|
if let Some(Component::Normal(c)) = components.peek() {
|
||||||
if c == &"~" {
|
if c == &"~" {
|
||||||
if let Ok(home) = super::home_dir() {
|
if let Ok(home) = home_dir() {
|
||||||
// it's ok to unwrap, the path starts with `~`
|
// it's ok to unwrap, the path starts with `~`
|
||||||
return home.join(path.strip_prefix("~").unwrap());
|
return home.join(path.strip_prefix("~").unwrap());
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,6 @@ use crate::{
|
||||||
Rope, RopeSlice, Tendril,
|
Rope, RopeSlice, Tendril,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use libloading::{Library, Symbol};
|
|
||||||
use tree_sitter::Language;
|
|
||||||
|
|
||||||
use arc_swap::{ArcSwap, Guard};
|
use arc_swap::{ArcSwap, Guard};
|
||||||
use slotmap::{DefaultKey as LayerId, HopSlotMap};
|
use slotmap::{DefaultKey as LayerId, HopSlotMap};
|
||||||
|
|
||||||
|
@ -27,33 +23,7 @@ use std::{
|
||||||
use once_cell::sync::{Lazy, OnceCell};
|
use once_cell::sync::{Lazy, OnceCell};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[cfg(unix)]
|
use helix_loader::grammar::{get_language, load_runtime_file};
|
||||||
pub const DYLIB_EXTENSION: &str = "so";
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub const DYLIB_EXTENSION: &str = "dll";
|
|
||||||
|
|
||||||
fn replace_dashes_with_underscores(name: &str) -> String {
|
|
||||||
name.replace('-', "_")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_language(runtime_path: &std::path::Path, name: &str) -> Result<Language> {
|
|
||||||
let name = name.to_ascii_lowercase();
|
|
||||||
let mut library_path = runtime_path.join("grammars").join(&name);
|
|
||||||
library_path.set_extension(DYLIB_EXTENSION);
|
|
||||||
|
|
||||||
let library = unsafe { Library::new(&library_path) }
|
|
||||||
.with_context(|| format!("Error opening dynamic library {:?}", &library_path))?;
|
|
||||||
let language_fn_name = format!("tree_sitter_{}", replace_dashes_with_underscores(&name));
|
|
||||||
let language = unsafe {
|
|
||||||
let language_fn: Symbol<unsafe extern "C" fn() -> Language> = library
|
|
||||||
.get(language_fn_name.as_bytes())
|
|
||||||
.with_context(|| format!("Failed to load symbol {}", language_fn_name))?;
|
|
||||||
language_fn()
|
|
||||||
};
|
|
||||||
std::mem::forget(library);
|
|
||||||
Ok(language)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
|
fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
|
||||||
where
|
where
|
||||||
|
@ -81,19 +51,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
#[serde(rename = "use-grammars")]
|
|
||||||
pub grammar_selection: Option<GrammarSelection>,
|
|
||||||
pub language: Vec<LanguageConfiguration>,
|
pub language: Vec<LanguageConfiguration>,
|
||||||
pub grammar: Vec<GrammarConfiguration>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase", untagged)]
|
|
||||||
pub enum GrammarSelection {
|
|
||||||
Only(HashSet<String>),
|
|
||||||
Except(HashSet<String>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// largely based on tree-sitter/cli/src/loader.rs
|
// largely based on tree-sitter/cli/src/loader.rs
|
||||||
|
@ -279,29 +238,6 @@ pub struct IndentQuery {
|
||||||
pub outdent: HashSet<String>,
|
pub outdent: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct GrammarConfiguration {
|
|
||||||
#[serde(rename = "name")]
|
|
||||||
pub grammar_id: String, // c-sharp, rust
|
|
||||||
pub source: GrammarSource,
|
|
||||||
pub path: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum GrammarSource {
|
|
||||||
Local {
|
|
||||||
path: String,
|
|
||||||
},
|
|
||||||
Git {
|
|
||||||
#[serde(rename = "git")]
|
|
||||||
remote: String,
|
|
||||||
#[serde(rename = "rev")]
|
|
||||||
revision: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TextObjectQuery {
|
pub struct TextObjectQuery {
|
||||||
pub query: Query,
|
pub query: Query,
|
||||||
|
@ -398,14 +334,6 @@ impl TextObjectQuery {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
|
|
||||||
let path = crate::RUNTIME_DIR
|
|
||||||
.join("queries")
|
|
||||||
.join(language)
|
|
||||||
.join(filename);
|
|
||||||
std::fs::read_to_string(&path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_query(language: &str, filename: &str) -> String {
|
fn read_query(language: &str, filename: &str) -> String {
|
||||||
static INHERITS_REGEX: Lazy<Regex> =
|
static INHERITS_REGEX: Lazy<Regex> =
|
||||||
Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
|
Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
|
||||||
|
@ -451,10 +379,7 @@ impl LanguageConfiguration {
|
||||||
if highlights_query.is_empty() {
|
if highlights_query.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let language = get_language(
|
let language = get_language(self.grammar.as_deref().unwrap_or(&self.language_id))
|
||||||
&crate::RUNTIME_DIR,
|
|
||||||
self.grammar.as_deref().unwrap_or(&self.language_id),
|
|
||||||
)
|
|
||||||
.map_err(|e| log::info!("{}", e))
|
.map_err(|e| log::info!("{}", e))
|
||||||
.ok()?;
|
.ok()?;
|
||||||
let config = HighlightConfiguration::new(
|
let config = HighlightConfiguration::new(
|
||||||
|
@ -2116,13 +2041,9 @@ mod test {
|
||||||
.map(String::from)
|
.map(String::from)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let loader = Loader::new(Configuration {
|
let loader = Loader::new(Configuration { language: vec![] });
|
||||||
language: vec![],
|
|
||||||
grammar: vec![],
|
|
||||||
grammar_selection: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
let language = get_language(&crate::RUNTIME_DIR, "Rust").unwrap();
|
let language = get_language("Rust").unwrap();
|
||||||
let config = HighlightConfiguration::new(
|
let config = HighlightConfiguration::new(
|
||||||
language,
|
language,
|
||||||
&std::fs::read_to_string("../runtime/grammars/sources/rust/queries/highlights.scm")
|
&std::fs::read_to_string("../runtime/grammars/sources/rust/queries/highlights.scm")
|
||||||
|
|
23
helix-loader/Cargo.toml
Normal file
23
helix-loader/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "helix-loader"
|
||||||
|
version = "0.6.0"
|
||||||
|
description = "A post-modern text editor."
|
||||||
|
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
categories = ["editor"]
|
||||||
|
repository = "https://github.com/helix-editor/helix"
|
||||||
|
homepage = "https://helix-editor.com"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
toml = "0.5"
|
||||||
|
etcetera = "0.3"
|
||||||
|
tree-sitter = "0.20"
|
||||||
|
libloading = "0.7"
|
||||||
|
once_cell = "1.9"
|
||||||
|
|
||||||
|
# cloning/compiling tree-sitter grammars
|
||||||
|
cc = { version = "1" }
|
||||||
|
threadpool = { version = "1.0" }
|
6
helix-loader/build.rs
Normal file
6
helix-loader/build.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
fn main() {
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-env=BUILD_TARGET={}",
|
||||||
|
std::env::var("TARGET").unwrap()
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,63 +1,155 @@
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use libloading::{Library, Symbol};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashSet,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Command,
|
process::Command,
|
||||||
sync::mpsc::channel,
|
sync::mpsc::channel,
|
||||||
};
|
};
|
||||||
|
use tree_sitter::Language;
|
||||||
|
|
||||||
use helix_core::syntax::{GrammarConfiguration, GrammarSelection, GrammarSource, DYLIB_EXTENSION};
|
#[cfg(unix)]
|
||||||
|
const DYLIB_EXTENSION: &str = "so";
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
const DYLIB_EXTENSION: &str = "dll";
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct Configuration {
|
||||||
|
#[serde(rename = "use-grammars")]
|
||||||
|
pub grammar_selection: Option<GrammarSelection>,
|
||||||
|
pub grammar: Vec<GrammarConfiguration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase", untagged)]
|
||||||
|
pub enum GrammarSelection {
|
||||||
|
Only(HashSet<String>),
|
||||||
|
Except(HashSet<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct GrammarConfiguration {
|
||||||
|
#[serde(rename = "name")]
|
||||||
|
pub grammar_id: String,
|
||||||
|
pub source: GrammarSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase", untagged)]
|
||||||
|
pub enum GrammarSource {
|
||||||
|
Local {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
Git {
|
||||||
|
#[serde(rename = "git")]
|
||||||
|
remote: String,
|
||||||
|
#[serde(rename = "rev")]
|
||||||
|
revision: String,
|
||||||
|
subpath: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const BUILD_TARGET: &str = env!("BUILD_TARGET");
|
const BUILD_TARGET: &str = env!("BUILD_TARGET");
|
||||||
const REMOTE_NAME: &str = "origin";
|
const REMOTE_NAME: &str = "origin";
|
||||||
|
|
||||||
|
pub fn get_language(name: &str) -> Result<Language> {
|
||||||
|
let name = name.to_ascii_lowercase();
|
||||||
|
let mut library_path = crate::runtime_dir().join("grammars").join(&name);
|
||||||
|
library_path.set_extension(DYLIB_EXTENSION);
|
||||||
|
|
||||||
|
let library = unsafe { Library::new(&library_path) }
|
||||||
|
.with_context(|| format!("Error opening dynamic library {library_path:?}"))?;
|
||||||
|
let language_fn_name = format!("tree_sitter_{}", name.replace('-', "_"));
|
||||||
|
let language = unsafe {
|
||||||
|
let language_fn: Symbol<unsafe extern "C" fn() -> Language> = library
|
||||||
|
.get(language_fn_name.as_bytes())
|
||||||
|
.with_context(|| format!("Failed to load symbol {language_fn_name}"))?;
|
||||||
|
language_fn()
|
||||||
|
};
|
||||||
|
std::mem::forget(library);
|
||||||
|
Ok(language)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fetch_grammars() -> Result<()> {
|
pub fn fetch_grammars() -> Result<()> {
|
||||||
run_parallel(get_grammar_configs(), fetch_grammar, "fetch")
|
run_parallel(get_grammar_configs()?, fetch_grammar, "fetch")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_grammars() -> Result<()> {
|
pub fn build_grammars() -> Result<()> {
|
||||||
run_parallel(get_grammar_configs(), build_grammar, "build")
|
run_parallel(get_grammar_configs()?, build_grammar, "build")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the set of grammar configurations the user requests.
|
||||||
|
// Grammars are configured in the default and user `languages.toml` and are
|
||||||
|
// merged. The `grammar_selection` key of the config is then used to filter
|
||||||
|
// down all grammars into a subset of the user's choosing.
|
||||||
|
fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
|
||||||
|
let config: Configuration = crate::user_lang_config()
|
||||||
|
.context("Could not parse languages.toml")?
|
||||||
|
.try_into()?;
|
||||||
|
|
||||||
|
let grammars = match config.grammar_selection {
|
||||||
|
Some(GrammarSelection::Only(selections)) => config
|
||||||
|
.grammar
|
||||||
|
.into_iter()
|
||||||
|
.filter(|grammar| selections.contains(&grammar.grammar_id))
|
||||||
|
.collect(),
|
||||||
|
Some(GrammarSelection::Except(rejections)) => config
|
||||||
|
.grammar
|
||||||
|
.into_iter()
|
||||||
|
.filter(|grammar| !rejections.contains(&grammar.grammar_id))
|
||||||
|
.collect(),
|
||||||
|
None => config.grammar,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(grammars)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_parallel<F>(grammars: Vec<GrammarConfiguration>, job: F, action: &'static str) -> Result<()>
|
fn run_parallel<F>(grammars: Vec<GrammarConfiguration>, job: F, action: &'static str) -> Result<()>
|
||||||
where
|
where
|
||||||
F: Fn(GrammarConfiguration) -> Result<()> + std::marker::Send + 'static + Copy,
|
F: Fn(GrammarConfiguration) -> Result<()> + std::marker::Send + 'static + Copy,
|
||||||
{
|
{
|
||||||
let mut n_jobs = 0;
|
|
||||||
let pool = threadpool::Builder::new().build();
|
let pool = threadpool::Builder::new().build();
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
|
|
||||||
for grammar in grammars {
|
for grammar in grammars {
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
n_jobs += 1;
|
|
||||||
|
|
||||||
pool.execute(move || {
|
pool.execute(move || {
|
||||||
let grammar_id = grammar.grammar_id.clone();
|
tx.send(job(grammar)).unwrap();
|
||||||
job(grammar).unwrap_or_else(|err| {
|
|
||||||
eprintln!("Failed to {} grammar '{}'\n{}", action, grammar_id, err)
|
|
||||||
});
|
|
||||||
|
|
||||||
// report progress
|
|
||||||
tx.send(1).unwrap();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
pool.join();
|
pool.join();
|
||||||
|
|
||||||
if rx.try_iter().sum::<usize>() == n_jobs {
|
// TODO: print all failures instead of the first one found.
|
||||||
Ok(())
|
if let Some(failure) = rx.try_iter().find_map(|result| result.err()) {
|
||||||
|
Err(anyhow!(
|
||||||
|
"Failed to {} some grammar(s).\n{}",
|
||||||
|
action,
|
||||||
|
failure
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("Failed to {} some grammar(s).", action))
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
|
fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
|
||||||
if let GrammarSource::Git { remote, revision } = grammar.source {
|
if let GrammarSource::Git {
|
||||||
let grammar_dir = helix_core::runtime_dir()
|
remote, revision, ..
|
||||||
|
} = grammar.source
|
||||||
|
{
|
||||||
|
let grammar_dir = crate::runtime_dir()
|
||||||
.join("grammars/sources")
|
.join("grammars/sources")
|
||||||
.join(grammar.grammar_id.clone());
|
.join(&grammar.grammar_id);
|
||||||
|
|
||||||
fs::create_dir_all(grammar_dir.clone()).expect("Could not create grammar directory");
|
fs::create_dir_all(&grammar_dir).context(format!(
|
||||||
|
"Could not create grammar directory {:?}",
|
||||||
|
grammar_dir
|
||||||
|
))?;
|
||||||
|
|
||||||
// create the grammar dir contains a git directory
|
// create the grammar dir contains a git directory
|
||||||
if !grammar_dir.join(".git").is_dir() {
|
if !grammar_dir.join(".git").is_dir() {
|
||||||
|
@ -65,12 +157,12 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure the remote matches the configured remote
|
// ensure the remote matches the configured remote
|
||||||
if get_remote_url(&grammar_dir).map_or(true, |s| s.trim_end() != remote) {
|
if get_remote_url(&grammar_dir).map_or(true, |s| s != remote) {
|
||||||
set_remote(&grammar_dir, &remote)?;
|
set_remote(&grammar_dir, &remote)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensure the revision matches the configured revision
|
// ensure the revision matches the configured revision
|
||||||
if get_revision(&grammar_dir).map_or(true, |s| s.trim_end() != revision) {
|
if get_revision(&grammar_dir).map_or(true, |s| s != revision) {
|
||||||
// Fetch the exact revision from the remote.
|
// Fetch the exact revision from the remote.
|
||||||
// Supported by server-side git since v2.5.0 (July 2015),
|
// Supported by server-side git since v2.5.0 (July 2015),
|
||||||
// enabled by default on major git hosts.
|
// enabled by default on major git hosts.
|
||||||
|
@ -94,33 +186,38 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<()> {
|
||||||
|
|
||||||
// Sets the remote for a repository to the given URL, creating the remote if
|
// Sets the remote for a repository to the given URL, creating the remote if
|
||||||
// it does not yet exist.
|
// it does not yet exist.
|
||||||
fn set_remote(repository: &Path, remote_url: &str) -> Result<String> {
|
fn set_remote(repository_dir: &Path, remote_url: &str) -> Result<String> {
|
||||||
git(repository, ["remote", "set-url", REMOTE_NAME, remote_url])
|
git(
|
||||||
.or_else(|_| git(repository, ["remote", "add", REMOTE_NAME, remote_url]))
|
repository_dir,
|
||||||
|
["remote", "set-url", REMOTE_NAME, remote_url],
|
||||||
|
)
|
||||||
|
.or_else(|_| git(repository_dir, ["remote", "add", REMOTE_NAME, remote_url]))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_remote_url(repository: &Path) -> Option<String> {
|
fn get_remote_url(repository_dir: &Path) -> Option<String> {
|
||||||
git(repository, ["remote", "get-url", REMOTE_NAME]).ok()
|
git(repository_dir, ["remote", "get-url", REMOTE_NAME]).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_revision(repository: &Path) -> Option<String> {
|
fn get_revision(repository_dir: &Path) -> Option<String> {
|
||||||
git(repository, ["rev-parse", "HEAD"]).ok()
|
git(repository_dir, ["rev-parse", "HEAD"]).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
// A wrapper around 'git' commands which returns stdout in success and a
|
// A wrapper around 'git' commands which returns stdout in success and a
|
||||||
// helpful error message showing the command, stdout, and stderr in error.
|
// helpful error message showing the command, stdout, and stderr in error.
|
||||||
fn git<I, S>(repository: &Path, args: I) -> Result<String>
|
fn git<I, S>(repository_dir: &Path, args: I) -> Result<String>
|
||||||
where
|
where
|
||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
S: AsRef<std::ffi::OsStr>,
|
S: AsRef<std::ffi::OsStr>,
|
||||||
{
|
{
|
||||||
let output = Command::new("git")
|
let output = Command::new("git")
|
||||||
.args(args)
|
.args(args)
|
||||||
.current_dir(repository)
|
.current_dir(repository_dir)
|
||||||
.output()?;
|
.output()?;
|
||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
Ok(String::from_utf8_lossy(&output.stdout)
|
||||||
|
.trim_end()
|
||||||
|
.to_owned())
|
||||||
} else {
|
} else {
|
||||||
// TODO: figure out how to display the git command using `args`
|
// TODO: figure out how to display the git command using `args`
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
|
@ -132,52 +229,37 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_grammar(grammar: GrammarConfiguration) -> Result<()> {
|
fn build_grammar(grammar: GrammarConfiguration) -> Result<()> {
|
||||||
let grammar_dir = if let GrammarSource::Local { ref path } = grammar.source {
|
println!("{:#?}", grammar);
|
||||||
PathBuf::from(path)
|
let grammar_dir = if let GrammarSource::Local { path } = &grammar.source {
|
||||||
|
PathBuf::from(&path)
|
||||||
} else {
|
} else {
|
||||||
helix_core::runtime_dir()
|
crate::runtime_dir()
|
||||||
.join("grammars/sources")
|
.join("grammars/sources")
|
||||||
.join(grammar.grammar_id.clone())
|
.join(&grammar.grammar_id)
|
||||||
};
|
};
|
||||||
|
|
||||||
grammar_dir.read_dir().with_context(|| {
|
let grammar_dir_entries = grammar_dir.read_dir().with_context(|| {
|
||||||
format!(
|
format!("Failed to read directory {grammar_dir:?}. Did you use 'hx --fetch-grammars'?")
|
||||||
"The directory {:?} is empty, you probably need to use 'hx --fetch-grammars'?",
|
|
||||||
grammar_dir
|
|
||||||
)
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let path = match grammar.path {
|
if grammar_dir_entries.count() == 0 {
|
||||||
Some(ref subpath) => grammar_dir.join(subpath),
|
return Err(anyhow!(
|
||||||
None => grammar_dir,
|
"Directory {grammar_dir:?} is empty. Did you use 'hx --fetch-grammars'?"
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = match &grammar.source {
|
||||||
|
GrammarSource::Git {
|
||||||
|
subpath: Some(subpath),
|
||||||
|
..
|
||||||
|
} => grammar_dir.join(subpath),
|
||||||
|
_ => grammar_dir,
|
||||||
}
|
}
|
||||||
.join("src");
|
.join("src");
|
||||||
|
|
||||||
build_tree_sitter_library(&path, grammar)
|
build_tree_sitter_library(&path, grammar)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the set of grammar configurations the user requests.
|
|
||||||
// Grammars are configured in the default and user `languages.toml` and are
|
|
||||||
// merged. The `grammar_selection` key of the config is then used to filter
|
|
||||||
// down all grammars into a subset of the user's choosing.
|
|
||||||
fn get_grammar_configs() -> Vec<GrammarConfiguration> {
|
|
||||||
let config = helix_core::config::user_syntax_loader().expect("Could not parse languages.toml");
|
|
||||||
|
|
||||||
match config.grammar_selection {
|
|
||||||
Some(GrammarSelection::Only(selections)) => config
|
|
||||||
.grammar
|
|
||||||
.into_iter()
|
|
||||||
.filter(|grammar| selections.contains(&grammar.grammar_id))
|
|
||||||
.collect(),
|
|
||||||
Some(GrammarSelection::Except(rejections)) => config
|
|
||||||
.grammar
|
|
||||||
.into_iter()
|
|
||||||
.filter(|grammar| !rejections.contains(&grammar.grammar_id))
|
|
||||||
.collect(),
|
|
||||||
None => config.grammar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) -> Result<()> {
|
fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) -> Result<()> {
|
||||||
let header_path = src_path;
|
let header_path = src_path;
|
||||||
let parser_path = src_path.join("parser.c");
|
let parser_path = src_path.join("parser.c");
|
||||||
|
@ -193,8 +275,8 @@ fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) ->
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let parser_lib_path = helix_core::runtime_dir().join("../runtime/grammars");
|
let parser_lib_path = crate::runtime_dir().join("grammars");
|
||||||
let mut library_path = parser_lib_path.join(grammar.grammar_id.clone());
|
let mut library_path = parser_lib_path.join(&grammar.grammar_id);
|
||||||
library_path.set_extension(DYLIB_EXTENSION);
|
library_path.set_extension(DYLIB_EXTENSION);
|
||||||
|
|
||||||
let recompile = needs_recompile(&library_path, &parser_path, &scanner_path)
|
let recompile = needs_recompile(&library_path, &parser_path, &scanner_path)
|
||||||
|
@ -210,7 +292,7 @@ fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) ->
|
||||||
let mut config = cc::Build::new();
|
let mut config = cc::Build::new();
|
||||||
config
|
config
|
||||||
.cpp(true)
|
.cpp(true)
|
||||||
.opt_level(2)
|
.opt_level(3)
|
||||||
.cargo_metadata(false)
|
.cargo_metadata(false)
|
||||||
.host(BUILD_TARGET)
|
.host(BUILD_TARGET)
|
||||||
.target(BUILD_TARGET);
|
.target(BUILD_TARGET);
|
||||||
|
@ -245,7 +327,7 @@ fn build_tree_sitter_library(src_path: &Path, grammar: GrammarConfiguration) ->
|
||||||
.arg(header_path)
|
.arg(header_path)
|
||||||
.arg("-o")
|
.arg("-o")
|
||||||
.arg(&library_path)
|
.arg(&library_path)
|
||||||
.arg("-O2");
|
.arg("-O3");
|
||||||
if let Some(scanner_path) = scanner_path.as_ref() {
|
if let Some(scanner_path) = scanner_path.as_ref() {
|
||||||
if scanner_path.extension() == Some("c".as_ref()) {
|
if scanner_path.extension() == Some("c".as_ref()) {
|
||||||
command.arg("-xc").arg("-std=c99").arg(scanner_path);
|
command.arg("-xc").arg("-std=c99").arg(scanner_path);
|
||||||
|
@ -294,3 +376,13 @@ fn needs_recompile(
|
||||||
fn mtime(path: &Path) -> Result<SystemTime> {
|
fn mtime(path: &Path) -> Result<SystemTime> {
|
||||||
Ok(fs::metadata(path)?.modified()?)
|
Ok(fs::metadata(path)?.modified()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gives the contents of a file from a language's `runtime/queries/<lang>`
|
||||||
|
/// directory
|
||||||
|
pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
|
||||||
|
let path = crate::RUNTIME_DIR
|
||||||
|
.join("queries")
|
||||||
|
.join(language)
|
||||||
|
.join(filename);
|
||||||
|
std::fs::read_to_string(&path)
|
||||||
|
}
|
161
helix-loader/src/lib.rs
Normal file
161
helix-loader/src/lib.rs
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
pub mod grammar;
|
||||||
|
|
||||||
|
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
|
||||||
|
|
||||||
|
pub static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
|
||||||
|
once_cell::sync::Lazy::new(runtime_dir);
|
||||||
|
|
||||||
|
pub fn runtime_dir() -> std::path::PathBuf {
|
||||||
|
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
|
||||||
|
return dir.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
const RT_DIR: &str = "runtime";
|
||||||
|
let conf_dir = config_dir().join(RT_DIR);
|
||||||
|
if conf_dir.exists() {
|
||||||
|
return conf_dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
|
||||||
|
// this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
|
||||||
|
return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to location of the executable being run
|
||||||
|
std::env::current_exe()
|
||||||
|
.ok()
|
||||||
|
.and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config_dir() -> std::path::PathBuf {
|
||||||
|
// TODO: allow env var override
|
||||||
|
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
|
||||||
|
let mut path = strategy.config_dir();
|
||||||
|
path.push("helix");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cache_dir() -> std::path::PathBuf {
|
||||||
|
// TODO: allow env var override
|
||||||
|
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
|
||||||
|
let mut path = strategy.cache_dir();
|
||||||
|
path.push("helix");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn config_file() -> std::path::PathBuf {
|
||||||
|
config_dir().join("config.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lang_config_file() -> std::path::PathBuf {
|
||||||
|
config_dir().join("languages.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_file() -> std::path::PathBuf {
|
||||||
|
cache_dir().join("helix.log")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default bultin-in languages.toml.
|
||||||
|
pub fn default_lang_config() -> toml::Value {
|
||||||
|
toml::from_slice(include_bytes!("../../languages.toml"))
|
||||||
|
.expect("Could not parse bultin-in languages.toml to valid toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// User configured languages.toml file, merged with the default config.
|
||||||
|
pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
|
||||||
|
let def_lang_conf = default_lang_config();
|
||||||
|
let data = std::fs::read(crate::config_dir().join("languages.toml"));
|
||||||
|
let user_lang_conf = match data {
|
||||||
|
Ok(raw) => {
|
||||||
|
let value = toml::from_slice(&raw)?;
|
||||||
|
merge_toml_values(def_lang_conf, value)
|
||||||
|
}
|
||||||
|
Err(_) => def_lang_conf,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(user_lang_conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// right overrides left
|
||||||
|
pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value {
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
fn get_name(v: &Value) -> Option<&str> {
|
||||||
|
v.get("name").and_then(Value::as_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
match (left, right) {
|
||||||
|
(Value::Array(mut left_items), Value::Array(right_items)) => {
|
||||||
|
left_items.reserve(right_items.len());
|
||||||
|
for rvalue in right_items {
|
||||||
|
let lvalue = get_name(&rvalue)
|
||||||
|
.and_then(|rname| left_items.iter().position(|v| get_name(v) == Some(rname)))
|
||||||
|
.map(|lpos| left_items.remove(lpos));
|
||||||
|
let mvalue = match lvalue {
|
||||||
|
Some(lvalue) => merge_toml_values(lvalue, rvalue),
|
||||||
|
None => rvalue,
|
||||||
|
};
|
||||||
|
left_items.push(mvalue);
|
||||||
|
}
|
||||||
|
Value::Array(left_items)
|
||||||
|
}
|
||||||
|
(Value::Table(mut left_map), Value::Table(right_map)) => {
|
||||||
|
for (rname, rvalue) in right_map {
|
||||||
|
match left_map.remove(&rname) {
|
||||||
|
Some(lvalue) => {
|
||||||
|
let merged_value = merge_toml_values(lvalue, rvalue);
|
||||||
|
left_map.insert(rname, merged_value);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
left_map.insert(rname, rvalue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Table(left_map)
|
||||||
|
}
|
||||||
|
// Catch everything else we didn't handle, and use the right value
|
||||||
|
(_, value) => value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod merge_toml_tests {
|
||||||
|
use super::merge_toml_values;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn language_tomls() {
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
const USER: &str = "
|
||||||
|
[[language]]
|
||||||
|
name = \"nix\"
|
||||||
|
test = \"bbb\"
|
||||||
|
indent = { tab-width = 4, unit = \" \", test = \"aaa\" }
|
||||||
|
";
|
||||||
|
|
||||||
|
let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
|
||||||
|
.expect("Couldn't parse built-in languages config");
|
||||||
|
let user: Value = toml::from_str(USER).unwrap();
|
||||||
|
|
||||||
|
let merged = merge_toml_values(base, user);
|
||||||
|
let languages = merged.get("language").unwrap().as_array().unwrap();
|
||||||
|
let nix = languages
|
||||||
|
.iter()
|
||||||
|
.find(|v| v.get("name").unwrap().as_str().unwrap() == "nix")
|
||||||
|
.unwrap();
|
||||||
|
let nix_indent = nix.get("indent").unwrap();
|
||||||
|
|
||||||
|
// We changed tab-width and unit in indent so check them if they are the new values
|
||||||
|
assert_eq!(
|
||||||
|
nix_indent.get("tab-width").unwrap().as_integer().unwrap(),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
assert_eq!(nix_indent.get("unit").unwrap().as_str().unwrap(), " ");
|
||||||
|
// We added a new keys, so check them
|
||||||
|
assert_eq!(nix.get("test").unwrap().as_str().unwrap(), "bbb");
|
||||||
|
assert_eq!(nix_indent.get("test").unwrap().as_str().unwrap(), "aaa");
|
||||||
|
// We didn't change comment-token so it should be same
|
||||||
|
assert_eq!(nix.get("comment-token").unwrap().as_str().unwrap(), "#");
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ helix-core = { version = "0.6", path = "../helix-core" }
|
||||||
helix-view = { version = "0.6", path = "../helix-view" }
|
helix-view = { version = "0.6", path = "../helix-view" }
|
||||||
helix-lsp = { version = "0.6", path = "../helix-lsp" }
|
helix-lsp = { version = "0.6", path = "../helix-lsp" }
|
||||||
helix-dap = { version = "0.6", path = "../helix-dap" }
|
helix-dap = { version = "0.6", path = "../helix-dap" }
|
||||||
|
helix-loader = { version = "0.6", path = "../helix-loader" }
|
||||||
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
once_cell = "1.10"
|
once_cell = "1.10"
|
||||||
|
|
|
@ -14,10 +14,5 @@ fn main() {
|
||||||
None => env!("CARGO_PKG_VERSION").into(),
|
None => env!("CARGO_PKG_VERSION").into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
|
||||||
"cargo:rustc-env=BUILD_TARGET={}",
|
|
||||||
std::env::var("TARGET").unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
|
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,10 +61,10 @@ impl Application {
|
||||||
let mut compositor = Compositor::new()?;
|
let mut compositor = Compositor::new()?;
|
||||||
let size = compositor.size();
|
let size = compositor.size();
|
||||||
|
|
||||||
let conf_dir = helix_core::config_dir();
|
let conf_dir = helix_loader::config_dir();
|
||||||
|
|
||||||
let theme_loader =
|
let theme_loader =
|
||||||
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
|
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_loader::runtime_dir()));
|
||||||
|
|
||||||
let true_color = config.editor.true_color || crate::true_color();
|
let true_color = config.editor.true_color || crate::true_color();
|
||||||
let theme = config
|
let theme = config
|
||||||
|
@ -109,7 +109,7 @@ impl Application {
|
||||||
compositor.push(editor_view);
|
compositor.push(editor_view);
|
||||||
|
|
||||||
if args.load_tutor {
|
if args.load_tutor {
|
||||||
let path = helix_core::runtime_dir().join("tutor.txt");
|
let path = helix_loader::runtime_dir().join("tutor.txt");
|
||||||
editor.open(path, Action::VerticalSplit)?;
|
editor.open(path, Action::VerticalSplit)?;
|
||||||
// Unset path to prevent accidentally saving to the original tutor file.
|
// Unset path to prevent accidentally saving to the original tutor file.
|
||||||
doc_mut!(editor).set_path(None)?;
|
doc_mut!(editor).set_path(None)?;
|
||||||
|
|
|
@ -828,7 +828,7 @@ fn tutor(
|
||||||
_args: &[Cow<str>],
|
_args: &[Cow<str>],
|
||||||
_event: PromptEvent,
|
_event: PromptEvent,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let path = helix_core::runtime_dir().join("tutor.txt");
|
let path = helix_loader::runtime_dir().join("tutor.txt");
|
||||||
cx.editor.open(path, Action::Replace)?;
|
cx.editor.open(path, Action::Replace)?;
|
||||||
// Unset path to prevent accidentally saving to the original tutor file.
|
// Unset path to prevent accidentally saving to the original tutor file.
|
||||||
doc_mut!(cx.editor).set_path(None)?;
|
doc_mut!(cx.editor).set_path(None)?;
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
use crossterm::style::{Color, Print, Stylize};
|
use crossterm::style::{Color, Print, Stylize};
|
||||||
use helix_core::{
|
use helix_core::config::{default_syntax_loader, user_syntax_loader};
|
||||||
config::{default_syntax_loader, user_syntax_loader},
|
use helix_loader::grammar::load_runtime_file;
|
||||||
syntax::load_runtime_file,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub enum TsFeature {
|
pub enum TsFeature {
|
||||||
|
@ -43,10 +41,10 @@ impl TsFeature {
|
||||||
|
|
||||||
/// Display general diagnostics.
|
/// Display general diagnostics.
|
||||||
pub fn general() {
|
pub fn general() {
|
||||||
let config_file = helix_core::config_file();
|
let config_file = helix_loader::config_file();
|
||||||
let lang_file = helix_core::lang_config_file();
|
let lang_file = helix_loader::lang_config_file();
|
||||||
let log_file = helix_core::log_file();
|
let log_file = helix_loader::log_file();
|
||||||
let rt_dir = helix_core::runtime_dir();
|
let rt_dir = helix_loader::runtime_dir();
|
||||||
|
|
||||||
if config_file.exists() {
|
if config_file.exists() {
|
||||||
println!("Config file: {}", config_file.display());
|
println!("Config file: {}", config_file.display());
|
||||||
|
|
|
@ -7,7 +7,6 @@ pub mod commands;
|
||||||
pub mod compositor;
|
pub mod compositor;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod grammars;
|
|
||||||
pub mod job;
|
pub mod job;
|
||||||
pub mod keymap;
|
pub mod keymap;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
|
@ -40,7 +40,7 @@ fn main() -> Result<()> {
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main_impl() -> Result<i32> {
|
async fn main_impl() -> Result<i32> {
|
||||||
let logpath = helix_core::log_file();
|
let logpath = helix_loader::log_file();
|
||||||
let parent = logpath.parent().unwrap();
|
let parent = logpath.parent().unwrap();
|
||||||
if !parent.exists() {
|
if !parent.exists() {
|
||||||
std::fs::create_dir_all(parent).ok();
|
std::fs::create_dir_all(parent).ok();
|
||||||
|
@ -105,21 +105,21 @@ FLAGS:
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.fetch_grammars {
|
if args.fetch_grammars {
|
||||||
helix_term::grammars::fetch_grammars()?;
|
helix_loader::grammar::fetch_grammars()?;
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.build_grammars {
|
if args.build_grammars {
|
||||||
helix_term::grammars::build_grammars()?;
|
helix_loader::grammar::build_grammars()?;
|
||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let conf_dir = helix_core::config_dir();
|
let conf_dir = helix_loader::config_dir();
|
||||||
if !conf_dir.exists() {
|
if !conf_dir.exists() {
|
||||||
std::fs::create_dir_all(&conf_dir).ok();
|
std::fs::create_dir_all(&conf_dir).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = match std::fs::read_to_string(helix_core::config_file()) {
|
let config = match std::fs::read_to_string(helix_loader::config_file()) {
|
||||||
Ok(config) => toml::from_str(&config)
|
Ok(config) => toml::from_str(&config)
|
||||||
.map(merge_keys)
|
.map(merge_keys)
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
|
|
|
@ -218,9 +218,9 @@ pub mod completers {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
|
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
|
||||||
let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes"));
|
let mut names = theme::Loader::read_names(&helix_loader::runtime_dir().join("themes"));
|
||||||
names.extend(theme::Loader::read_names(
|
names.extend(theme::Loader::read_names(
|
||||||
&helix_core::config_dir().join("themes"),
|
&helix_loader::config_dir().join("themes"),
|
||||||
));
|
));
|
||||||
names.push("default".into());
|
names.push("default".into());
|
||||||
names.push("base16_default".into());
|
names.push("base16_default".into());
|
||||||
|
|
|
@ -302,8 +302,7 @@ indent = { tab-width = 2, unit = " " }
|
||||||
|
|
||||||
[[grammar]]
|
[[grammar]]
|
||||||
name = "typescript"
|
name = "typescript"
|
||||||
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "3e897ea5925f037cfae2e551f8e6b12eec2a201a" }
|
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "3e897ea5925f037cfae2e551f8e6b12eec2a201a", subpath = "typescript" }
|
||||||
path = "typescript"
|
|
||||||
|
|
||||||
[[language]]
|
[[language]]
|
||||||
name = "tsx"
|
name = "tsx"
|
||||||
|
@ -317,8 +316,7 @@ indent = { tab-width = 2, unit = " " }
|
||||||
|
|
||||||
[[grammar]]
|
[[grammar]]
|
||||||
name = "tsx"
|
name = "tsx"
|
||||||
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "3e897ea5925f037cfae2e551f8e6b12eec2a201a" }
|
source = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "3e897ea5925f037cfae2e551f8e6b12eec2a201a", subpath = "tsx" }
|
||||||
path = "tsx"
|
|
||||||
|
|
||||||
[[language]]
|
[[language]]
|
||||||
name = "css"
|
name = "css"
|
||||||
|
@ -522,8 +520,7 @@ indent = { tab-width = 2, unit = " " }
|
||||||
|
|
||||||
[[grammar]]
|
[[grammar]]
|
||||||
name = "ocaml"
|
name = "ocaml"
|
||||||
source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "23d419ba45789c5a47d31448061557716b02750a" }
|
source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "23d419ba45789c5a47d31448061557716b02750a", subpath = "ocaml" }
|
||||||
path = "ocaml"
|
|
||||||
|
|
||||||
[[language]]
|
[[language]]
|
||||||
name = "ocaml-interface"
|
name = "ocaml-interface"
|
||||||
|
@ -536,8 +533,7 @@ indent = { tab-width = 2, unit = " "}
|
||||||
|
|
||||||
[[grammar]]
|
[[grammar]]
|
||||||
name = "ocaml-interface"
|
name = "ocaml-interface"
|
||||||
source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "23d419ba45789c5a47d31448061557716b02750a" }
|
source = { git = "https://github.com/tree-sitter/tree-sitter-ocaml", rev = "23d419ba45789c5a47d31448061557716b02750a", subpath = "interface" }
|
||||||
path = "interface"
|
|
||||||
|
|
||||||
[[language]]
|
[[language]]
|
||||||
name = "lua"
|
name = "lua"
|
||||||
|
|
Loading…
Add table
Reference in a new issue