From 62bacd7269c88d95638f698b9121d0bb192cb861 Mon Sep 17 00:00:00 2001 From: tinger Date: Thu, 26 Mar 2026 14:59:03 +0100 Subject: [PATCH] loader: Allow loading queries from the grammar repo When a `queries` key is provided for a grammar source the loader now uses this path to copy queries from the checkout when building a grammar. When no such key is given it attempts to restore previously backed up vendored queries by looking for a special `.backup` directory. --- helix-loader/src/grammar.rs | 117 +++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 3 deletions(-) diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index f1ee137f..1c8bffea 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -52,6 +52,7 @@ pub struct GrammarConfiguration { pub enum GrammarSource { Local { path: String, + queries: Option, }, Git { #[serde(rename = "git")] @@ -59,6 +60,7 @@ pub enum GrammarSource { #[serde(rename = "rev")] revision: String, subpath: Option, + queries: Option, }, } @@ -500,7 +502,7 @@ enum BuildStatus { } fn build_grammar(grammar: GrammarConfiguration, target: Option<&str>) -> Result { - let grammar_dir = if let GrammarSource::Local { path } = &grammar.source { + let grammar_dir = if let GrammarSource::Local { path, .. } = &grammar.source { PathBuf::from(&path) } else { crate::runtime_dirs() @@ -531,10 +533,119 @@ fn build_grammar(grammar: GrammarConfiguration, target: Option<&str>) -> Result< .. } => grammar_dir.join(subpath), _ => grammar_dir, + }; + + let queries = match &grammar.source { + GrammarSource::Local { queries, .. } => queries, + GrammarSource::Git { queries, .. } => queries, + }; + + if let Some(provided_queries_path) = queries { + install_provided_queries(&path, &grammar.grammar_id, provided_queries_path)?; + } else { + maybe_restore_vendored_queries(&grammar.grammar_id)?; } - .join("src"); - build_tree_sitter_library(&path, grammar, target) + build_tree_sitter_library(&path.join("src"), grammar, target) +} + +fn install_provided_queries( + grammar_dir: &Path, + grammar: &str, + provided_queries_path: &str, +) -> Result<()> { + let runtime = crate::runtime_dirs() + .first() + .expect("No runtime directories provided"); // guaranteed by post-condition + + let target_dir = runtime.join("queries").join(grammar); + let backup_dir = target_dir.join(".backup"); + + if !backup_dir.try_exists()? { + fs::create_dir_all(&backup_dir)?; + + // If no backup dir exists, then we need to save the vendored files, + // otherwise we already have backed them up and don't want to override + // them with previously provided queries. + for entry in target_dir.read_dir()? { + let query = entry?.file_name(); + if query == ".backup" { + continue; + } + fs::rename(target_dir.join(&query), backup_dir.join(&query))?; + } + } + + fs::create_dir_all(&target_dir)?; + + // Regardless of if we have a backup or not, when we provide queries we want + // to make sure we remove surplus files beforehand. At the off chance that + // they were removed upstream the old left over files could be incompatible + // with the new parser. + let mut vendored_was_empty = true; + for entry in target_dir.read_dir()? { + let entry = entry?; + if entry.file_name() == ".backup" { + continue; + } + fs::remove_file(entry.path())?; + vendored_was_empty = false; + } + + let source_dir = grammar_dir.join(provided_queries_path); + + let mut source_was_empty = true; + for entry in source_dir.read_dir()? { + let query = entry?.file_name(); + fs::copy(source_dir.join(&query), target_dir.join(&query))?; + source_was_empty = false; + } + + if vendored_was_empty && source_was_empty { + fs::remove_dir_all(&target_dir)?; + } + + Ok(()) +} + +fn maybe_restore_vendored_queries(grammar: &str) -> Result<()> { + let runtime = crate::runtime_dirs() + .first() + .expect("No runtime directories provided"); // guaranteed by post-condition + + let target_dir = runtime.join("queries").join(grammar); + let backup_dir = target_dir.join(".backup"); + + if !backup_dir.try_exists()? { + return Ok(()); + } + + // First we delete all previous files to make sure we don't use leave any + // surplus provided queries. + for entry in target_dir.read_dir()? { + let entry = entry?; + if entry.file_name() == ".backup" { + continue; + } + fs::remove_file(entry.path())?; + } + + // Copy the files and remove the backup directory to indicate we've got + // vendored queries again. + let mut vendored_is_empty = true; + for entry in backup_dir.read_dir()? { + let query = entry?.file_name(); + fs::copy(backup_dir.join(&query), target_dir.join(&query))?; + vendored_is_empty = false; + } + + fs::remove_dir_all(&backup_dir)?; + + if vendored_is_empty { + fs::remove_dir_all(&target_dir)?; + } + + Ok(()) } fn build_tree_sitter_library( -- 2.51.2