diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 7c50a5797..49b988640 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -15,7 +15,7 @@ use bitflags::bitflags; use globset::GlobSet; use hashbrown::raw::RawTable; use helix_stdx::rope::{self, RopeSliceExt}; -use slotmap::{DefaultKey as LayerId, HopSlotMap}; +use slotmap::{DefaultKey as LayerId, DefaultKey as LanguageId, HopSlotMap}; use std::{ borrow::Cow, @@ -92,8 +92,10 @@ pub struct Configuration { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LanguageConfiguration { + #[serde(skip)] + language_id: LanguageId, #[serde(rename = "name")] - pub language_id: String, // c-sharp, rust, tsx + pub language_name: String, // c-sharp, rust, tsx #[serde(rename = "language-id")] // see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem pub language_server_language_id: Option, // csharp, rust, typescriptreact, for the language-server @@ -743,32 +745,33 @@ pub fn read_query(language: &str, filename: &str) -> String { impl LanguageConfiguration { fn initialize_highlight(&self, scopes: &[String]) -> Option> { - let highlights_query = read_query(&self.language_id, "highlights.scm"); + let highlights_query = read_query(&self.language_name, "highlights.scm"); // always highlight syntax errors // highlights_query += "\n(ERROR) @error"; - let injections_query = read_query(&self.language_id, "injections.scm"); - let locals_query = read_query(&self.language_id, "locals.scm"); + let injections_query = read_query(&self.language_name, "injections.scm"); + let locals_query = read_query(&self.language_name, "locals.scm"); if highlights_query.is_empty() { None } else { - let language = get_language(self.grammar.as_deref().unwrap_or(&self.language_id)) + let language = get_language(self.grammar.as_deref().unwrap_or(&self.language_name)) .map_err(|err| { log::error!( "Failed to load tree-sitter parser for language {:?}: {:#}", - self.language_id, + self.language_name, err ) }) .ok()?; let config = HighlightConfiguration::new( + self.language_id, language, &highlights_query, &injections_query, &locals_query, ) - .map_err(|err| log::error!("Could not parse queries for language {:?}. Are your grammars out of sync? Try running 'hx --grammar fetch' and 'hx --grammar build'. This query could not be parsed: {:?}", self.language_id, err)) + .map_err(|err| log::error!("Could not parse queries for language {:?}. Are your grammars out of sync? Try running 'hx --grammar fetch' and 'hx --grammar build'. This query could not be parsed: {:?}", self.language_name, err)) .ok()?; config.configure(scopes); @@ -812,7 +815,7 @@ impl LanguageConfiguration { } fn load_query(&self, kind: &str) -> Option { - let query_text = read_query(&self.language_id, kind); + let query_text = read_query(&self.language_name, kind); if query_text.is_empty() { return None; } @@ -822,7 +825,7 @@ impl LanguageConfiguration { log::error!( "Failed to parse {} queries for {}: {}", kind, - self.language_id, + self.language_name, e ) }) @@ -862,11 +865,11 @@ pub struct SoftWrap { #[derive(Debug)] struct FileTypeGlob { glob: globset::Glob, - language_id: usize, + language_id: LanguageId, } impl FileTypeGlob { - fn new(glob: globset::Glob, language_id: usize) -> Self { + fn new(glob: globset::Glob, language_id: LanguageId) -> Self { Self { glob, language_id } } } @@ -890,7 +893,7 @@ impl FileTypeGlobMatcher { }) } - fn language_id_for_path(&self, path: &Path) -> Option<&usize> { + fn language_id_for_path(&self, path: &Path) -> Option<&LanguageId> { self.matcher .matches(path) .iter() @@ -905,10 +908,10 @@ impl FileTypeGlobMatcher { #[derive(Debug)] pub struct Loader { // highlight_names ? - language_configs: Vec>, - language_config_ids_by_extension: HashMap, // Vec + language_configs: HopSlotMap>, + language_config_ids_by_extension: HashMap, // Vec language_config_ids_glob_matcher: FileTypeGlobMatcher, - language_config_ids_by_shebang: HashMap, + language_config_ids_by_shebang: HashMap, language_server_configs: HashMap, @@ -919,14 +922,17 @@ pub type LoaderError = globset::Error; impl Loader { pub fn new(config: Configuration) -> Result { - let mut language_configs = Vec::new(); + let mut language_configs = HopSlotMap::new(); let mut language_config_ids_by_extension = HashMap::new(); let mut language_config_ids_by_shebang = HashMap::new(); let mut file_type_globs = Vec::new(); - for config in config.language { - // get the next id - let language_id = language_configs.len(); + for mut config in config.language { + let language_id = language_configs.insert_with_key(|id| { + config.language_id = id; + Arc::new(config) + }); + let config = &language_configs[language_id]; for file_type in &config.file_types { // entry().or_insert(Vec::new).push(language_id); @@ -942,8 +948,6 @@ impl Loader { for shebang in &config.shebangs { language_config_ids_by_shebang.insert(shebang.clone(), language_id); } - - language_configs.push(Arc::new(config)); } Ok(Self { @@ -989,18 +993,18 @@ impl Loader { pub fn language_config_for_scope(&self, scope: &str) -> Option> { self.language_configs - .iter() + .values() .find(|config| config.scope == scope) .cloned() } - pub fn language_config_for_language_id( + pub fn language_config_for_language_name( &self, - id: impl PartialEq, + name: impl PartialEq, ) -> Option> { self.language_configs - .iter() - .find(|config| id.eq(&config.language_id)) + .values() + .find(|config| name.eq(&config.language_name)) .cloned() } @@ -1008,7 +1012,7 @@ impl Loader { /// function will perform a regex match on the given string to find the closest language match. pub fn language_config_for_name(&self, slice: RopeSlice) -> Option> { // PERF: If the name matches up with the id, then this saves the need to do expensive regex. - let shortcircuit = self.language_config_for_language_id(slice); + let shortcircuit = self.language_config_for_language_name(slice); if shortcircuit.is_some() { return shortcircuit; } @@ -1017,12 +1021,12 @@ impl Loader { let mut best_match_length = 0; let mut best_match_position = None; - for (i, configuration) in self.language_configs.iter().enumerate() { + for (id, configuration) in self.language_configs.iter() { if let Some(injection_regex) = &configuration.injection_regex { if let Some(mat) = injection_regex.find(slice.regex_input()) { let length = mat.end() - mat.start(); if length > best_match_length { - best_match_position = Some(i); + best_match_position = Some(id); best_match_length = length; } } @@ -1037,7 +1041,7 @@ impl Loader { capture: &InjectionLanguageMarker, ) -> Option> { match capture { - InjectionLanguageMarker::LanguageId(id) => self.language_config_for_language_id(*id), + InjectionLanguageMarker::LanguageId(id) => self.language_config_for_language_name(*id), InjectionLanguageMarker::Name(name) => self.language_config_for_name(*name), InjectionLanguageMarker::Filename(file) => { let path_str: Cow = (*file).into(); @@ -1053,7 +1057,7 @@ impl Loader { } pub fn language_configs(&self) -> impl Iterator> { - self.language_configs.iter() + self.language_configs.values() } pub fn language_server_configs(&self) -> &HashMap { @@ -1066,7 +1070,7 @@ impl Loader { // Reconfigure existing grammars for config in self .language_configs - .iter() + .values() .filter(|cfg| cfg.is_highlight_initialized()) { config.reconfigure(&self.scopes()); @@ -1425,6 +1429,13 @@ impl Syntax { }) } + pub fn layer_config(&self, layer_id: LayerId) -> Arc { + let loader = &self.loader.load(); + let language_id = self.layers[layer_id].config.language_id; + + Arc::clone(&loader.language_configs[language_id]) + } + pub fn tree(&self) -> &Tree { self.layers[self.root].tree() } @@ -1795,6 +1806,7 @@ pub enum HighlightEvent { #[derive(Debug)] pub struct HighlightConfiguration { pub language: Grammar, + pub language_id: LanguageId, pub query: Query, injections_query: Query, combined_injections_patterns: Vec, @@ -1891,6 +1903,7 @@ impl HighlightConfiguration { /// /// Returns a `HighlightConfiguration` that can then be used with the `highlight` method. pub fn new( + language_id: LanguageId, language: Grammar, highlights_query: &str, injection_query: &str, @@ -1968,6 +1981,7 @@ impl HighlightConfiguration { let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]); Ok(Self { language, + language_id, query, injections_query, combined_injections_patterns, @@ -2795,7 +2809,8 @@ mod test { let textobject = TextObjectQuery { query }; let mut cursor = QueryCursor::new(); - let config = HighlightConfiguration::new(language, "", "", "").unwrap(); + let config = + HighlightConfiguration::new(LanguageId::default(), language, "", "", "").unwrap(); let syntax = Syntax::new( source.slice(..), Arc::new(config), @@ -2860,6 +2875,7 @@ mod test { let language = get_language("rust").unwrap(); let config = HighlightConfiguration::new( + LanguageId::default(), language, &std::fs::read_to_string("../runtime/grammars/sources/rust/queries/highlights.scm") .unwrap(), @@ -2971,7 +2987,8 @@ mod test { .unwrap(); let language = get_language(language_name).unwrap(); - let config = HighlightConfiguration::new(language, "", "", "").unwrap(); + let config = + HighlightConfiguration::new(LanguageId::default(), language, "", "", "").unwrap(); let syntax = Syntax::new( source.slice(..), Arc::new(config), diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 05e2f9c7e..47ed6ef27 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -183,7 +183,7 @@ pub fn languages_all() -> std::io::Result<()> { syn_loader_conf .language - .sort_unstable_by_key(|l| l.language_id.clone()); + .sort_unstable_by_key(|l| l.language_name.clone()); let check_binary = |cmd: Option<&str>| match cmd { Some(cmd) => match helix_stdx::env::which(cmd) { @@ -194,7 +194,7 @@ pub fn languages_all() -> std::io::Result<()> { }; for lang in &syn_loader_conf.language { - write!(stdout, "{}", fit(&lang.language_id))?; + write!(stdout, "{}", fit(&lang.language_name))?; let mut cmds = lang.language_servers.iter().filter_map(|ls| { syn_loader_conf @@ -214,7 +214,7 @@ pub fn languages_all() -> std::io::Result<()> { write!(stdout, "{}", check_binary(formatter))?; for ts_feat in TsFeature::all() { - match load_runtime_file(&lang.language_id, ts_feat.runtime_filename()).is_ok() { + match load_runtime_file(&lang.language_name, ts_feat.runtime_filename()).is_ok() { true => write!(stdout, "{}", color(fit("✓"), Color::Green))?, false => write!(stdout, "{}", color(fit("✘"), Color::Red))?, } @@ -257,7 +257,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> { let lang = match syn_loader_conf .language .iter() - .find(|l| l.language_id == lang_str) + .find(|l| l.language_name == lang_str) { Some(l) => l, None => { @@ -266,8 +266,11 @@ pub fn language(lang_str: String) -> std::io::Result<()> { let suggestions: Vec<&str> = syn_loader_conf .language .iter() - .filter(|l| l.language_id.starts_with(lang_str.chars().next().unwrap())) - .map(|l| l.language_id.as_str()) + .filter(|l| { + l.language_name + .starts_with(lang_str.chars().next().unwrap()) + }) + .map(|l| l.language_name.as_str()) .collect(); if !suggestions.is_empty() { let suggestions = suggestions.join(", "); @@ -301,7 +304,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> { .map(|formatter| formatter.command.to_string()), )?; - probe_parser(lang.grammar.as_ref().unwrap_or(&lang.language_id))?; + probe_parser(lang.grammar.as_ref().unwrap_or(&lang.language_name))?; for ts_feat in TsFeature::all() { probe_treesitter_feature(&lang_str, *ts_feat)? diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index a76adbe21..b648e2bca 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -496,7 +496,7 @@ pub mod completers { let loader = editor.syn_loader.load(); let language_ids = loader .language_configs() - .map(|config| &config.language_id) + .map(|config| &config.language_name) .chain(std::iter::once(&text)); fuzzy_match(input, language_ids, false) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 06a708f05..c00207f17 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1232,7 +1232,7 @@ impl Document { ) -> anyhow::Result<()> { let language_config = (*config_loader) .load() - .language_config_for_language_id(language_id) + .language_config_for_language_name(language_id) .ok_or_else(|| anyhow!("invalid language id: {}", language_id))?; self.set_language(Some(language_config), Some(config_loader)); Ok(()) @@ -1714,7 +1714,7 @@ impl Document { pub fn language_name(&self) -> Option<&str> { self.language .as_ref() - .map(|language| language.language_id.as_str()) + .map(|language| language.language_name.as_str()) } /// Language ID for the document. Either the `language-id`, diff --git a/xtask/src/docgen.rs b/xtask/src/docgen.rs index 2b58e7329..eb6337a5a 100644 --- a/xtask/src/docgen.rs +++ b/xtask/src/docgen.rs @@ -134,7 +134,7 @@ pub fn lang_features() -> Result { let mut langs = config .language .iter() - .map(|l| l.language_id.clone()) + .map(|l| l.language_name.clone()) .collect::>(); langs.sort_unstable(); @@ -148,9 +148,9 @@ pub fn lang_features() -> Result { let lc = config .language .iter() - .find(|l| l.language_id == lang) + .find(|l| l.language_name == lang) .unwrap(); // lang comes from config - row.push(lc.language_id.clone()); + row.push(lc.language_name.clone()); for (_feat, support_list) in &ts_features_to_langs { row.push( diff --git a/xtask/src/querycheck.rs b/xtask/src/querycheck.rs new file mode 100644 index 000000000..0915288fc --- /dev/null +++ b/xtask/src/querycheck.rs @@ -0,0 +1,39 @@ +use crate::DynError; + +pub fn query_check() -> Result<(), DynError> { + use crate::helpers::lang_config; + use helix_core::{syntax::read_query, tree_sitter::Query}; + use helix_loader::grammar::get_language; + + let query_files = [ + "highlights.scm", + "locals.scm", + "injections.scm", + "textobjects.scm", + "indents.scm", + ]; + + for language in lang_config().language { + let language_name = &language.language_name; + let grammar_name = language.grammar.as_ref().unwrap_or(language_name); + for query_file in query_files { + let language = get_language(grammar_name); + let query_text = read_query(language_name, query_file); + if let Ok(lang) = language { + if !query_text.is_empty() { + if let Err(reason) = Query::new(&lang, &query_text) { + return Err(format!( + "Failed to parse {} queries for {}: {}", + query_file, language_name, reason + ) + .into()); + } + } + } + } + } + + println!("Query check succeeded"); + + Ok(()) +}