feat: load language grammars

This commit is contained in:
Jan-Bulthuis 2026-07-19 15:20:12 +02:00
parent d5ecc05345
commit 308dcf2911

View File

@ -1,24 +1,143 @@
use std::{path::PathBuf, sync::OnceLock}; use std::{borrow::Cow, collections::HashMap, error::Error, fs, path::Path, sync::OnceLock};
use tracing::{info, warn}; use tracing::{info, warn};
use tree_house::{
InjectionLanguageMarker, Language, LanguageConfig, LanguageLoader, highlighter::Highlight,
read_query,
};
use tree_house_bindings::Grammar;
use typst::foundations::{Func, NativeFunc, func}; use typst::foundations::{Func, NativeFunc, func};
use crate::AppConfig; use crate::AppConfig;
static REGISTRY: OnceLock<LanguageRegistry> = OnceLock::new(); static REGISTRY: OnceLock<LanguageRegistry> = OnceLock::new();
struct LanguageRegistry {} /// Class names used in highlighted code.
const HIGHLIGHT_NAMES: &[&str] = &[
"attribute",
"comment",
"comment.block",
"comment.line",
"constant",
"constant.builtin",
"constructor",
"function",
"function.builtin",
"function.macro",
"function.method",
"keyword",
"keyword.control",
"keyword.operator",
"label",
"namespace",
"number",
"operator",
"property",
"punctuation",
"punctuation.bracket",
"punctuation.delimiter",
"special",
"string",
"string.special",
"tag",
"type",
"type.builtin",
"variable",
"variable.builtin",
"variable.parameter",
];
/// Get the longest matching highlight name for a given scope.
fn highlight_for_scope(scope: &str) -> Option<Highlight> {
let mut candidate = scope;
loop {
if let Some(i) = HIGHLIGHT_NAMES.iter().position(|&n| n == candidate) {
return Some(Highlight::new(i as u32));
}
match candidate.rfind('.') {
Some(dot) => candidate = &candidate[..dot],
None => return None,
}
}
}
struct LanguageRegistry {
by_name: HashMap<String, Language>,
configs: Vec<LanguageConfig>,
}
impl LanguageRegistry { impl LanguageRegistry {
fn new(syntax_root: &PathBuf) -> Self { fn new(syntax_root: &Path) -> Self {
Self {} let mut by_name = HashMap::new();
let mut configs = Vec::new();
for entry in fs::read_dir(syntax_root).into_iter().flatten().flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
let so_path = entry.path().join(format!("{name}.so"));
if !so_path.exists() {
continue;
}
match load_language(&name, &entry.path(), syntax_root) {
Ok(config) => {
by_name.insert(name, Language::new(configs.len() as u32));
configs.push(config);
}
Err(e) => warn!("Failed to load grammar {name}: {e}"),
}
}
info!("Loaded {} syntax highlighting grammars", configs.len());
Self { by_name, configs }
}
}
fn load_language(
name: &str,
lang_dir: &Path,
syntax_root: &Path,
) -> Result<LanguageConfig, Box<dyn Error>> {
let grammar = unsafe { Grammar::new(name, &lang_dir.join(format!("{name}.so")))? };
let read = |kind: &str| {
read_query(name, |lang| {
fs::read_to_string(syntax_root.join(lang).join(format!("{kind}.scm")))
.unwrap_or_default()
})
};
let config = LanguageConfig::new(
grammar,
&read("highlights"),
&read("injections"),
&read("locals"),
)?;
config.configure(highlight_for_scope);
Ok(config)
}
impl LanguageLoader for LanguageRegistry {
fn language_for_marker(&self, marker: InjectionLanguageMarker<'_>) -> Option<Language> {
match marker {
InjectionLanguageMarker::Name(name) => self.by_name.get(name).copied(),
InjectionLanguageMarker::Match(text) => {
let name: Cow<str> = text.into();
self.by_name.get(name.as_ref()).copied()
}
_ => None,
}
}
fn get_config(&self, lang: Language) -> Option<&LanguageConfig> {
self.configs.get(lang.idx())
} }
} }
pub fn init(config: &AppConfig) { pub fn init(config: &AppConfig) {
match &config.syntax_root { match &config.syntax_root {
Some(syntax_root) => { Some(syntax_root) => {
info!("Loading syntax files"); info!("Loading syntax files from {}", syntax_root.display());
REGISTRY.get_or_init(|| LanguageRegistry::new(syntax_root)); REGISTRY.get_or_init(|| LanguageRegistry::new(syntax_root));
} }
None => { None => {