feat: add function to get LanguageConfiguration from a LayerId

Co-authored-by: the-mikedavis <mcarsondavis@gmail.com>
pull/12759/head
Nikita Revenco 2025-01-31 21:44:17 +00:00 committed by Nik Revenco
parent 48dec3ee8f
commit 70f27b390d
6 changed files with 107 additions and 48 deletions

View File

@ -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<String>, // 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<Arc<HighlightConfiguration>> {
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<Query> {
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<Arc<LanguageConfiguration>>,
language_config_ids_by_extension: HashMap<String, usize>, // Vec<usize>
language_configs: HopSlotMap<LanguageId, Arc<LanguageConfiguration>>,
language_config_ids_by_extension: HashMap<String, LanguageId>, // Vec<LanguageId>
language_config_ids_glob_matcher: FileTypeGlobMatcher,
language_config_ids_by_shebang: HashMap<String, usize>,
language_config_ids_by_shebang: HashMap<String, LanguageId>,
language_server_configs: HashMap<String, LanguageServerConfiguration>,
@ -919,14 +922,17 @@ pub type LoaderError = globset::Error;
impl Loader {
pub fn new(config: Configuration) -> Result<Self, LoaderError> {
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<Arc<LanguageConfiguration>> {
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<String>,
name: impl PartialEq<String>,
) -> Option<Arc<LanguageConfiguration>> {
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<Arc<LanguageConfiguration>> {
// 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<Arc<LanguageConfiguration>> {
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<str> = (*file).into();
@ -1053,7 +1057,7 @@ impl Loader {
}
pub fn language_configs(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
self.language_configs.iter()
self.language_configs.values()
}
pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
@ -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<LanguageConfiguration> {
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<usize>,
@ -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),

View File

@ -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)?

View File

@ -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)

View File

@ -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`,

View File

@ -134,7 +134,7 @@ pub fn lang_features() -> Result<String, DynError> {
let mut langs = config
.language
.iter()
.map(|l| l.language_id.clone())
.map(|l| l.language_name.clone())
.collect::<Vec<_>>();
langs.sort_unstable();
@ -148,9 +148,9 @@ pub fn lang_features() -> Result<String, DynError> {
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(

View File

@ -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(())
}