diff --git a/src/syntax.rs b/src/syntax.rs index c450ef4..eb6eadf 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -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 tree_house::{ + InjectionLanguageMarker, Language, LanguageConfig, LanguageLoader, highlighter::Highlight, + read_query, +}; +use tree_house_bindings::Grammar; use typst::foundations::{Func, NativeFunc, func}; use crate::AppConfig; static REGISTRY: OnceLock = 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 { + 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, + configs: Vec, +} impl LanguageRegistry { - fn new(syntax_root: &PathBuf) -> Self { - Self {} + fn new(syntax_root: &Path) -> 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> { + 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 { + match marker { + InjectionLanguageMarker::Name(name) => self.by_name.get(name).copied(), + InjectionLanguageMarker::Match(text) => { + let name: Cow = 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) { match &config.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)); } None => {