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<Arc<str>>, pub(super) matcher: Arc<LanguageMatcher>, pub(super) hidden: bool, pub(super) load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>, pub(super) loaded: bool, pub(super) manifest_name: Option<ManifestName>,} 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<AvailableLanguage>); #[derive(Copy, Clone, Default)]enum LanguageMatchPrecedence { #[default] Undetermined, PathOrContent(usize), UserConfigured(usize),} impl AvailableLanguages { pub(super) fn register( &mut self, name: LanguageName, grammar: Option<Arc<str>>, matcher: Arc<LanguageMatcher>, hidden: bool, manifest_name: Option<ManifestName>, load: Arc<dyn Fn() -> Result<LoadedLanguage> + '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<LanguageName> { 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<LanguageName> { 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<LanguageName> { 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<AvailableLanguage> { self.0 .iter() .find(|language| language.name.0.as_ref() == name) .cloned() } pub(super) fn find_by_modeline_name(&self, modeline_name: &str) -> Option<AvailableLanguage> { 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<LanguageId> { 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<LanguageId> { 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<Arc<str>, (GlobSet, Vec<String>)>>, ) -> Option<LanguageId> { 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::<SmallVec<[_; 3]>>(); 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::<String>() }) }); 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<LanguageMatchPrecedence>, ) -> Option<LanguageId> { 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()) }}
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<Arc<str>>,
pub(super) matcher: Arc<LanguageMatcher>,
pub(super) hidden: bool,
pub(super) load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
pub(super) loaded: bool,
pub(super) manifest_name: Option<ManifestName>,
}
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<AvailableLanguage>);
#[derive(Copy, Clone, Default)]
enum LanguageMatchPrecedence {
#[default]
Undetermined,
PathOrContent(usize),
UserConfigured(usize),
}
impl AvailableLanguages {
pub(super) fn register(
&mut self,
name: LanguageName,
grammar: Option<Arc<str>>,
matcher: Arc<LanguageMatcher>,
hidden: bool,
manifest_name: Option<ManifestName>,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + '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<LanguageName> {
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<LanguageName> {
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<LanguageName> {
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<AvailableLanguage> {
self.0
.iter()
.find(|language| language.name.0.as_ref() == name)
.cloned()
}
pub(super) fn find_by_modeline_name(&self, modeline_name: &str) -> Option<AvailableLanguage> {
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<LanguageId> {
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<LanguageId> {
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<Arc<str>, (GlobSet, Vec<String>)>>,
) -> Option<LanguageId> {
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::<SmallVec<[_; 3]>>();
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::<String>()
})
});
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<LanguageMatchPrecedence>,
) -> Option<LanguageId> {
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())
}
}