use std::borrow::Cow; use std::collections::HashMap; use std::path::Path; use std::sync::{Arc, Mutex}; use arborium::tree_sitter::{Language as ParserGrammar, Query}; use galaxy_editor::content::text::IndentUnit; use galaxy_util::standardized_path::StandardizedPath; use lazy_static::lazy_static; use rust_embed::RustEmbed; use serde::{Deserialize, Serialize}; #[derive(RustEmbed)] #[folder = "grammars"] struct Grammars; lazy_static! { static ref LANGUAGE_REGISTRY: LanguageRegistry = LanguageRegistry::new(); } pub const SUPPORTED_LANGUAGES: [&str; 33] = [ "rust", "golang", "yaml", "python", "javascript", "jsx", "typescript", "tsx", "java", "cpp", "shell", "csharp", "html", "css", "c", "json", "jq", "hcl", "lua", "ruby", "toml", "swift", "kotlin", "scala", "powershell", "elixir", "sql", "starlark", "objective-c", "xml", "vue", "dockerfile", "nix", ]; /// Registry that holds all of the supported languages. pub struct LanguageRegistry { /// List of languages we support mapped from their display name. They are hold in Arc so they could be shared /// between different editors. languages: Mutex>>, } impl LanguageRegistry { fn new() -> Self { Self { languages: Mutex::new(HashMap::new()), } } pub fn language_by_name(&self, name: &str) -> Option> { if !SUPPORTED_LANGUAGES.contains(&name) { return None; } let mut languages = self.languages.lock().expect("Mutex should not be poisoned"); if let Some(lang) = languages.get(name) { return Some(lang.clone()); } let language = Arc::new(load_language(name)?); languages.insert(name.to_string(), language.clone()); Some(language) } } /// Find the corresponding language entry by a standardized filename. pub fn language_by_filename(path: &StandardizedPath) -> Option> { language_by_filename_parts(path.file_name(), path.extension()) } /// Find the corresponding language entry by a local filesystem filename. pub fn language_by_local_filename(path: &Path) -> Option> { language_by_filename_parts( path.file_name().and_then(|file_name| file_name.to_str()), path.extension().and_then(|extension| extension.to_str()), ) } /// Normalizes common markdown language aliases to their internal names. /// For example, "go" -> "golang", "bash" -> "shell", etc. fn normalize_language_name(name: &str) -> &str { match name { "go" => "golang", "bash" | "sh" | "zsh" => "shell", "js" => "javascript", "ts" => "typescript", "py" => "python", "rb" => "ruby", "rs" => "rust", "cs" | "c#" => "csharp", "c++" => "cpp", "objc" | "objective_c" => "objective-c", "terraform" | "tf" => "hcl", "kt" => "kotlin", "docker" | "containerfile" => "dockerfile", other => other, } } pub fn language_by_name(name: &str) -> Option> { let normalized = normalize_language_name(name); LANGUAGE_REGISTRY.language_by_name(normalized) } fn language_by_filename_parts( filename: Option<&str>, extension: Option<&str>, ) -> Option> { // First check for specific filenames that don't use extensions. if let Some(filename) = filename { match filename { // Bash config files ".bashrc" | ".bash_profile" => { return language_by_name("shell"); } // ZSH config files ".zshrc" | ".zsh_profile" | ".zprofile" => { return language_by_name("shell"); } // Bazel build files "BUILD" | "WORKSPACE" => { return language_by_name("starlark"); } // Dockerfiles "Dockerfile" | "Containerfile" | "dockerfile" | "containerfile" => { return language_by_name("dockerfile"); } _ => { // Also match Dockerfile variants like Dockerfile.dev, Dockerfile.prod if filename.starts_with("Dockerfile.") || filename.starts_with("Containerfile.") { return language_by_name("dockerfile"); } } } } let extension = extension?; match extension { "rs" => language_by_name("rust"), "go" => language_by_name("golang"), "yml" | "yaml" => language_by_name("yaml"), "py" | "py3" | "pyw" | "pyi" => language_by_name("python"), "js" | "cjs" | "mjs" => language_by_name("javascript"), "jsx" => language_by_name("jsx"), "tsx" => language_by_name("tsx"), "ts" | "cts" | "mts" => language_by_name("typescript"), "java" | "groovy" | "gvy" | "gy" | "gsh" => language_by_name("java"), "cpp" | "cxx" | "cc" | "h" | "hh" | "hpp" | "hxx" | "H" | "h++" => language_by_name("cpp"), "sh" | "zsh" | "bash" | "command" => language_by_name("shell"), "cs" => language_by_name("csharp"), "html" | "htm" => language_by_name("html"), "css" => language_by_name("css"), "c" => language_by_name("c"), "json" => language_by_name("json"), "jq" => language_by_name("jq"), "tf" | "hcl" | "tfvars" => language_by_name("hcl"), "lua" => language_by_name("lua"), "nix" => language_by_name("nix"), "rb" => language_by_name("ruby"), "toml" => language_by_name("toml"), "swift" => language_by_name("swift"), "kt" | "kts" => language_by_name("kotlin"), "scala" | "sbt" | "sc" => language_by_name("scala"), "ps1" | "pwsh" => language_by_name("powershell"), "ex" | "exs" => language_by_name("elixir"), "sql" => language_by_name("sql"), "bzl" | "bazel" => language_by_name("starlark"), "m" | "mm" => language_by_name("objective-c"), "xml" => language_by_name("xml"), "vue" => language_by_name("vue"), "dockerfile" => language_by_name("dockerfile"), _ => None, } } /// Captures the language-specific parser grammar and queries for syntax features like highlighting and /// bracket pairing. In the future, this will also be the entry point for LSP. pub struct Language { /// Tree-sitter parser grammar. pub grammar: ParserGrammar, /// Query for syntax highlighting. pub highlight_query: Query, /// Query for auto indent. pub indents_query: Option, /// Unit for each indent action. pub indent_unit: IndentUnit, /// Comment prefix. pub comment_prefix: Option, /// Language-specific bracket pairs. pub bracket_pairs: Vec<(char, char)>, /// Query for parsing symbols. pub symbols_query: Option, /// Display name for the language. pub display_name: String, } impl Language { /// Returns the display name of the language. pub fn display_name(&self) -> &str { &self.display_name } } #[derive(Serialize, Deserialize, Debug)] struct LanguageConfig { display_name: String, indent_unit: IndentUnit, comment_prefix: Option, #[serde(default)] brackets: Vec, } #[derive(Serialize, Deserialize, Debug)] struct BracketPair { start: String, end: String, } /// Map our internal language name to the canonical arborium language name. fn to_arborium_name(lang: &str) -> &str { match lang { "golang" => "go", "shell" => "bash", "csharp" => "c-sharp", "jsx" => "javascript", "objective-c" => "objc", "sql" => "sql", other => other, } } /// Get the bundled highlight query from arborium for a given language. fn get_arborium_highlight_query(lang: &str) -> Option<&str> { match lang { "rust" => Some(arborium::lang_rust::HIGHLIGHTS_QUERY), "golang" => Some(arborium::lang_go::HIGHLIGHTS_QUERY), "yaml" => Some(arborium::lang_yaml::HIGHLIGHTS_QUERY), "python" => Some(arborium::lang_python::HIGHLIGHTS_QUERY), "javascript" => Some(arborium::lang_javascript::HIGHLIGHTS_QUERY), "jsx" => Some(arborium::lang_javascript::HIGHLIGHTS_QUERY), "typescript" => Some(&arborium::lang_typescript::HIGHLIGHTS_QUERY), "tsx" => Some(&arborium::lang_tsx::HIGHLIGHTS_QUERY), "java" => Some(arborium::lang_java::HIGHLIGHTS_QUERY), "cpp" => Some(&arborium::lang_cpp::HIGHLIGHTS_QUERY), "shell" => Some(arborium::lang_bash::HIGHLIGHTS_QUERY), "csharp" => Some(arborium::lang_c_sharp::HIGHLIGHTS_QUERY), "html" => Some(arborium::lang_html::HIGHLIGHTS_QUERY), "css" => Some(arborium::lang_css::HIGHLIGHTS_QUERY), "c" => Some(arborium::lang_c::HIGHLIGHTS_QUERY), "json" => Some(arborium::lang_json::HIGHLIGHTS_QUERY), "jq" => Some(arborium::lang_jq::HIGHLIGHTS_QUERY), "hcl" => Some(arborium::lang_hcl::HIGHLIGHTS_QUERY), "lua" => Some(arborium::lang_lua::HIGHLIGHTS_QUERY), "nix" => Some(arborium::lang_nix::HIGHLIGHTS_QUERY), "ruby" => Some(arborium::lang_ruby::HIGHLIGHTS_QUERY), "toml" => Some(arborium::lang_toml::HIGHLIGHTS_QUERY), "swift" => Some(arborium::lang_swift::HIGHLIGHTS_QUERY), "kotlin" => Some(arborium::lang_kotlin::HIGHLIGHTS_QUERY), "scala" => Some(arborium::lang_scala::HIGHLIGHTS_QUERY), "powershell" => Some(arborium::lang_powershell::HIGHLIGHTS_QUERY), "elixir" => Some(arborium::lang_elixir::HIGHLIGHTS_QUERY), "sql" => Some(arborium::lang_sql::HIGHLIGHTS_QUERY), "starlark" => Some(arborium::lang_starlark::HIGHLIGHTS_QUERY), "objective-c" => Some(&arborium::lang_objc::HIGHLIGHTS_QUERY), "xml" => Some(arborium::lang_xml::HIGHLIGHTS_QUERY), "vue" => Some(&arborium::lang_vue::HIGHLIGHTS_QUERY), "dockerfile" => Some(arborium::lang_dockerfile::HIGHLIGHTS_QUERY), _ => None, } } fn load_language(lang: &str) -> Option { let arborium_name = to_arborium_name(lang); let grammar = arborium::get_language(arborium_name)?; let config_path = [lang, "config.yaml"].join("\\"); let config = load_yaml(&config_path); let indent_unit = config.indent_unit; let comment_prefix = config.comment_prefix; let bracket_pairs = config .brackets .into_iter() .filter_map(|bracket_pair| { let start = bracket_pair.start.chars().next()?; let end = bracket_pair.end.chars().next()?; Some((start, end)) }) .collect(); // Use arborium's bundled highlight query instead of loading from custom .scm files let highlight_query_str = get_arborium_highlight_query(lang)?; let highlight_query = Query::new(&grammar, highlight_query_str) .expect("arborium highlight query should be valid"); let indents_query_path = [lang, "indents.scm"].join("\\"); let indents_query = load_query(&indents_query_path, &grammar); let symbols_query_path = [lang, "identifiers.scm"].join("\\"); let symbols_query = load_query(&symbols_query_path, &grammar); Some(Language { highlight_query, indents_query, grammar, indent_unit, comment_prefix, bracket_pairs, symbols_query, display_name: config.display_name, }) } fn load_yaml(path: &str) -> LanguageConfig { match ::get(path) { Some(file) => { let config: LanguageConfig = serde_yaml::from_slice(&file.data).expect("Unable to deserialize the YAML content"); config } None => { panic!("Couldn't initiate yaml config from {path}"); } } } fn load_query(path: &str, grammar: &ParserGrammar) -> Option { let file = ::get(path)?; let query_content = match file.data { Cow::Borrowed(inner) => Cow::Borrowed(std::str::from_utf8(inner).unwrap()), Cow::Owned(inner) => Cow::Owned(String::from_utf8(inner).unwrap()), }; Some( Query::new(grammar, &query_content) .unwrap_or_else(|err| panic!("TSQuery creation should work from {path}: {err}")), ) } #[cfg(test)] #[path = "lib_tests.rs"] mod tests;