use crate::{LanguageId, LanguageMatcher, LanguageName, LoadedLanguage, ManifestName}; use anyhow::Result; use collections::FxHashMap; use globset::GlobSet; use smallvec::SmallVec; use std::{cell::LazyCell, path::Path, sync::Arc}; use sum_tree::Bias; use text::{Point, Rope}; use unicase::UniCase; #[derive(Clone)] pub struct AvailableLanguage { pub(super) id: LanguageId, pub(super) name: LanguageName, pub(super) grammar: Option>, pub(super) matcher: Arc, pub(super) hidden: bool, pub(super) load: Arc Result + 'static + Send + Sync>, pub(super) loaded: bool, pub(super) manifest_name: Option, } impl AvailableLanguage { pub fn id(&self) -> LanguageId { self.id } pub fn name(&self) -> LanguageName { self.name.clone() } pub fn matcher(&self) -> &LanguageMatcher { &self.matcher } pub fn hidden(&self) -> bool { self.hidden } } #[derive(Default)] pub(super) struct AvailableLanguages(Vec); #[derive(Copy, Clone, Default)] enum LanguageMatchPrecedence { #[default] Undetermined, PathOrContent(usize), UserConfigured(usize), } impl AvailableLanguages { pub(super) fn register( &mut self, name: LanguageName, grammar: Option>, matcher: Arc, hidden: bool, manifest_name: Option, load: Arc Result + 'static + Send + Sync>, ) -> bool { if let Some(existing_language) = self .0 .iter_mut() .find(|existing_language| existing_language.name == name) { existing_language.grammar = grammar; existing_language.matcher = matcher; existing_language.load = load; existing_language.manifest_name = manifest_name; false } else { self.add(AvailableLanguage { id: LanguageId::new(), name, grammar, matcher, hidden, load, loaded: false, manifest_name, }); true } } pub(super) fn add(&mut self, language: AvailableLanguage) { self.0.push(language); } pub(super) fn unloaded_language_names(&self) -> Vec { self.0 .iter() .filter_map(|language| (!language.loaded).then_some(language.name.clone())) .collect() } #[cfg(any(test, feature = "test-support"))] pub(super) fn name_for_id(&self, id: LanguageId) -> Option { self.0 .iter() .find(|language| language.id == id) .map(|language| language.name.clone()) } pub(super) fn get_language(&self, id: LanguageId) -> Option<&AvailableLanguage> { self.0.iter().find(|language| language.id == id) } pub(super) fn find_name_by_extension(&self, extension: &str) -> Option { self.0 .iter() .find(|language| { language .matcher .path_suffixes .iter() .any(|suffix| suffix == extension) }) .map(|language| language.name.clone()) } pub(super) fn find_by_exact_name(&self, name: &str) -> Option { self.0 .iter() .find(|language| language.name.0.as_ref() == name) .cloned() } pub(super) fn find_by_modeline_name(&self, modeline_name: &str) -> Option { let modeline_name = modeline_name.to_lowercase(); self.0 .iter() .find(|language| { language .matcher .modeline_aliases .iter() .any(|alias| alias.to_lowercase() == modeline_name) }) .or_else(|| { self.0.iter().find(|language| { language .grammar .as_ref() .is_some_and(|grammar| grammar.to_lowercase() == modeline_name) }) }) .or_else(|| { self.0 .iter() .find(|language| language.name.0.to_lowercase() == modeline_name) }) .cloned() } pub(super) fn mark_all_unloaded(&mut self) { for language in &mut self.0 { language.loaded = false; } } pub(super) fn remove(&mut self, names: &[LanguageName]) { self.0.retain(|language| !names.contains(&language.name)); } pub(super) fn mark_loaded(&mut self, id: LanguageId) { if let Some(language) = self.0.iter_mut().find(|language| language.id == id) { language.loaded = true; } } pub(super) fn find_by_name(&self, name: &str) -> Option { let name = UniCase::new(name); self.find_best_match( |language_name, _, current_best_match| match current_best_match { LanguageMatchPrecedence::Undetermined if UniCase::new(&language_name.0) == name => { Some(LanguageMatchPrecedence::PathOrContent(name.len())) } LanguageMatchPrecedence::Undetermined | LanguageMatchPrecedence::UserConfigured(_) | LanguageMatchPrecedence::PathOrContent(_) => None, }, ) } pub(super) fn find_by_name_or_extension(&self, string: &str) -> Option { let string = UniCase::new(string); self.find_best_match(|name, matcher, current_best_match| { let name_matches = || { UniCase::new(&name.0) == string || matcher .path_suffixes .iter() .any(|suffix| UniCase::new(suffix) == string) }; match current_best_match { LanguageMatchPrecedence::Undetermined => { name_matches().then_some(LanguageMatchPrecedence::PathOrContent(string.len())) } LanguageMatchPrecedence::PathOrContent(len) => (string.len() > len && name_matches()) .then_some(LanguageMatchPrecedence::PathOrContent(string.len())), LanguageMatchPrecedence::UserConfigured(_) => None, } }) } pub(super) fn find_for_file( &self, path: &Path, content: Option<&Rope>, user_file_types: Option<&FxHashMap, (GlobSet, Vec)>>, ) -> Option { let filename = path.file_name().and_then(|filename| filename.to_str()); // `Path.extension()` returns None for files with a leading '.' // and no other extension which is not the desired behavior here, // as we want `.zshrc` to result in extension being `Some("zshrc")` let extension = filename.and_then(|filename| filename.split('.').next_back()); let path_suffixes = [extension, filename, path.to_str()] .iter() .filter_map(|suffix| suffix.map(|suffix| (suffix, globset::Candidate::new(suffix)))) .collect::>(); let content = LazyCell::new(|| { content.map(|content| { let end = content.clip_point(Point::new(0, 256), Bias::Left); let end = content.point_to_offset(end); content.chunks_in_range(0..end).collect::() }) }); self.find_best_match(move |language_name, matcher, current_best_match| { let path_matches_default_suffix = || { let len = matcher .path_suffixes .iter() .fold(0, |acc: usize, path_suffix: &String| { let ext = ".".to_string() + path_suffix; let matched_suffix_len = path_suffixes .iter() .find(|(suffix, _)| suffix.ends_with(&ext) || suffix == path_suffix) .map(|(suffix, _)| suffix.len()); matched_suffix_len.map_or(acc, |len| acc.max(len)) }); (len > 0).then_some(len) }; let path_matches_custom_suffix = || { user_file_types .and_then(|types| types.get(language_name.as_ref())) .and_then(|(custom_suffixes, _)| { path_suffixes .iter() .find(|(_, candidate)| custom_suffixes.is_match_candidate(candidate)) .map(|(suffix, _)| suffix.len()) }) }; let content_matches = || { matcher.first_line_pattern.as_ref().is_some_and(|pattern| { content .as_ref() .is_some_and(|content| pattern.is_match(content)) }) }; // Only return a match for the given file if we have a better match than // the current one. match current_best_match { LanguageMatchPrecedence::PathOrContent(current_len) => { if let Some(len) = path_matches_custom_suffix() { // >= because user config should win tie with system ext len (len >= current_len).then_some(LanguageMatchPrecedence::UserConfigured(len)) } else if let Some(len) = path_matches_default_suffix() { // >= because user config should win tie with system ext len (len >= current_len).then_some(LanguageMatchPrecedence::PathOrContent(len)) } else { None } } LanguageMatchPrecedence::Undetermined => { if let Some(len) = path_matches_custom_suffix() { Some(LanguageMatchPrecedence::UserConfigured(len)) } else if let Some(len) = path_matches_default_suffix() { Some(LanguageMatchPrecedence::PathOrContent(len)) } else if content_matches() { Some(LanguageMatchPrecedence::PathOrContent(1)) } else { None } } LanguageMatchPrecedence::UserConfigured(_) => None, } }) } fn find_best_match( &self, callback: impl Fn( &LanguageName, &LanguageMatcher, LanguageMatchPrecedence, ) -> Option, ) -> Option { self.0 .iter() .rev() .fold(None, |best_language_match, language| { let current_match_type = best_language_match .as_ref() .map_or(LanguageMatchPrecedence::default(), |(_, score)| *score); let language_score = callback(&language.name, &language.matcher, current_match_type); match (language_score, current_match_type) { // no current best, so our candidate is better ( Some( LanguageMatchPrecedence::PathOrContent(_) | LanguageMatchPrecedence::UserConfigured(_), ), LanguageMatchPrecedence::Undetermined, ) => language_score.map(|new_score| (language, new_score)), // our candidate is better only if the name is longer ( Some(LanguageMatchPrecedence::PathOrContent(new_len)), LanguageMatchPrecedence::PathOrContent(current_len), ) | ( Some(LanguageMatchPrecedence::UserConfigured(new_len)), LanguageMatchPrecedence::UserConfigured(current_len), ) | ( Some(LanguageMatchPrecedence::PathOrContent(new_len)), LanguageMatchPrecedence::UserConfigured(current_len), ) => { if new_len > current_len { language_score.map(|new_score| (language, new_score)) } else { best_language_match } } // our candidate is better if the name is longer or equal to ( Some(LanguageMatchPrecedence::UserConfigured(new_len)), LanguageMatchPrecedence::PathOrContent(current_len), ) => { if new_len >= current_len { language_score.map(|new_score| (language, new_score)) } else { best_language_match } } // no candidate, use current best (None, _) | (Some(LanguageMatchPrecedence::Undetermined), _) => { best_language_match } } }) .map(|(available_language, _)| available_language.id()) } }