Skip to repository content368 lines · 13.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:57:07.383Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
available_languages.rs
1use crate::{LanguageId, LanguageMatcher, LanguageName, LoadedLanguage, ManifestName};
2use anyhow::Result;
3use collections::FxHashMap;
4use globset::GlobSet;
5use smallvec::SmallVec;
6use std::{cell::LazyCell, path::Path, sync::Arc};
7use sum_tree::Bias;
8use text::{Point, Rope};
9use unicase::UniCase;
10
11#[derive(Clone)]
12pub struct AvailableLanguage {
13 pub(super) id: LanguageId,
14 pub(super) name: LanguageName,
15 pub(super) grammar: Option<Arc<str>>,
16 pub(super) matcher: Arc<LanguageMatcher>,
17 pub(super) hidden: bool,
18 pub(super) load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
19 pub(super) loaded: bool,
20 pub(super) manifest_name: Option<ManifestName>,
21}
22
23impl AvailableLanguage {
24 pub fn id(&self) -> LanguageId {
25 self.id
26 }
27
28 pub fn name(&self) -> LanguageName {
29 self.name.clone()
30 }
31
32 pub fn matcher(&self) -> &LanguageMatcher {
33 &self.matcher
34 }
35
36 pub fn hidden(&self) -> bool {
37 self.hidden
38 }
39}
40
41#[derive(Default)]
42pub(super) struct AvailableLanguages(Vec<AvailableLanguage>);
43
44#[derive(Copy, Clone, Default)]
45enum LanguageMatchPrecedence {
46 #[default]
47 Undetermined,
48 PathOrContent(usize),
49 UserConfigured(usize),
50}
51
52impl AvailableLanguages {
53 pub(super) fn register(
54 &mut self,
55 name: LanguageName,
56 grammar: Option<Arc<str>>,
57 matcher: Arc<LanguageMatcher>,
58 hidden: bool,
59 manifest_name: Option<ManifestName>,
60 load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
61 ) -> bool {
62 if let Some(existing_language) = self
63 .0
64 .iter_mut()
65 .find(|existing_language| existing_language.name == name)
66 {
67 existing_language.grammar = grammar;
68 existing_language.matcher = matcher;
69 existing_language.load = load;
70 existing_language.manifest_name = manifest_name;
71 false
72 } else {
73 self.add(AvailableLanguage {
74 id: LanguageId::new(),
75 name,
76 grammar,
77 matcher,
78 hidden,
79 load,
80 loaded: false,
81 manifest_name,
82 });
83 true
84 }
85 }
86
87 pub(super) fn add(&mut self, language: AvailableLanguage) {
88 self.0.push(language);
89 }
90
91 pub(super) fn unloaded_language_names(&self) -> Vec<LanguageName> {
92 self.0
93 .iter()
94 .filter_map(|language| (!language.loaded).then_some(language.name.clone()))
95 .collect()
96 }
97
98 #[cfg(any(test, feature = "test-support"))]
99 pub(super) fn name_for_id(&self, id: LanguageId) -> Option<LanguageName> {
100 self.0
101 .iter()
102 .find(|language| language.id == id)
103 .map(|language| language.name.clone())
104 }
105
106 pub(super) fn get_language(&self, id: LanguageId) -> Option<&AvailableLanguage> {
107 self.0.iter().find(|language| language.id == id)
108 }
109
110 pub(super) fn find_name_by_extension(&self, extension: &str) -> Option<LanguageName> {
111 self.0
112 .iter()
113 .find(|language| {
114 language
115 .matcher
116 .path_suffixes
117 .iter()
118 .any(|suffix| suffix == extension)
119 })
120 .map(|language| language.name.clone())
121 }
122
123 pub(super) fn find_by_exact_name(&self, name: &str) -> Option<AvailableLanguage> {
124 self.0
125 .iter()
126 .find(|language| language.name.0.as_ref() == name)
127 .cloned()
128 }
129
130 pub(super) fn find_by_modeline_name(&self, modeline_name: &str) -> Option<AvailableLanguage> {
131 let modeline_name = modeline_name.to_lowercase();
132 self.0
133 .iter()
134 .find(|language| {
135 language
136 .matcher
137 .modeline_aliases
138 .iter()
139 .any(|alias| alias.to_lowercase() == modeline_name)
140 })
141 .or_else(|| {
142 self.0.iter().find(|language| {
143 language
144 .grammar
145 .as_ref()
146 .is_some_and(|grammar| grammar.to_lowercase() == modeline_name)
147 })
148 })
149 .or_else(|| {
150 self.0
151 .iter()
152 .find(|language| language.name.0.to_lowercase() == modeline_name)
153 })
154 .cloned()
155 }
156
157 pub(super) fn mark_all_unloaded(&mut self) {
158 for language in &mut self.0 {
159 language.loaded = false;
160 }
161 }
162
163 pub(super) fn remove(&mut self, names: &[LanguageName]) {
164 self.0.retain(|language| !names.contains(&language.name));
165 }
166
167 pub(super) fn mark_loaded(&mut self, id: LanguageId) {
168 if let Some(language) = self.0.iter_mut().find(|language| language.id == id) {
169 language.loaded = true;
170 }
171 }
172
173 pub(super) fn find_by_name(&self, name: &str) -> Option<LanguageId> {
174 let name = UniCase::new(name);
175 self.find_best_match(
176 |language_name, _, current_best_match| match current_best_match {
177 LanguageMatchPrecedence::Undetermined if UniCase::new(&language_name.0) == name => {
178 Some(LanguageMatchPrecedence::PathOrContent(name.len()))
179 }
180 LanguageMatchPrecedence::Undetermined
181 | LanguageMatchPrecedence::UserConfigured(_)
182 | LanguageMatchPrecedence::PathOrContent(_) => None,
183 },
184 )
185 }
186
187 pub(super) fn find_by_name_or_extension(&self, string: &str) -> Option<LanguageId> {
188 let string = UniCase::new(string);
189 self.find_best_match(|name, matcher, current_best_match| {
190 let name_matches = || {
191 UniCase::new(&name.0) == string
192 || matcher
193 .path_suffixes
194 .iter()
195 .any(|suffix| UniCase::new(suffix) == string)
196 };
197
198 match current_best_match {
199 LanguageMatchPrecedence::Undetermined => {
200 name_matches().then_some(LanguageMatchPrecedence::PathOrContent(string.len()))
201 }
202 LanguageMatchPrecedence::PathOrContent(len) => (string.len() > len
203 && name_matches())
204 .then_some(LanguageMatchPrecedence::PathOrContent(string.len())),
205 LanguageMatchPrecedence::UserConfigured(_) => None,
206 }
207 })
208 }
209
210 pub(super) fn find_for_file(
211 &self,
212 path: &Path,
213 content: Option<&Rope>,
214 user_file_types: Option<&FxHashMap<Arc<str>, (GlobSet, Vec<String>)>>,
215 ) -> Option<LanguageId> {
216 let filename = path.file_name().and_then(|filename| filename.to_str());
217 // `Path.extension()` returns None for files with a leading '.'
218 // and no other extension which is not the desired behavior here,
219 // as we want `.zshrc` to result in extension being `Some("zshrc")`
220 let extension = filename.and_then(|filename| filename.split('.').next_back());
221 let path_suffixes = [extension, filename, path.to_str()]
222 .iter()
223 .filter_map(|suffix| suffix.map(|suffix| (suffix, globset::Candidate::new(suffix))))
224 .collect::<SmallVec<[_; 3]>>();
225 let content = LazyCell::new(|| {
226 content.map(|content| {
227 let end = content.clip_point(Point::new(0, 256), Bias::Left);
228 let end = content.point_to_offset(end);
229 content.chunks_in_range(0..end).collect::<String>()
230 })
231 });
232
233 self.find_best_match(move |language_name, matcher, current_best_match| {
234 let path_matches_default_suffix = || {
235 let len =
236 matcher
237 .path_suffixes
238 .iter()
239 .fold(0, |acc: usize, path_suffix: &String| {
240 let ext = ".".to_string() + path_suffix;
241 let matched_suffix_len = path_suffixes
242 .iter()
243 .find(|(suffix, _)| suffix.ends_with(&ext) || suffix == path_suffix)
244 .map(|(suffix, _)| suffix.len());
245
246 matched_suffix_len.map_or(acc, |len| acc.max(len))
247 });
248 (len > 0).then_some(len)
249 };
250
251 let path_matches_custom_suffix = || {
252 user_file_types
253 .and_then(|types| types.get(language_name.as_ref()))
254 .and_then(|(custom_suffixes, _)| {
255 path_suffixes
256 .iter()
257 .find(|(_, candidate)| custom_suffixes.is_match_candidate(candidate))
258 .map(|(suffix, _)| suffix.len())
259 })
260 };
261
262 let content_matches = || {
263 matcher.first_line_pattern.as_ref().is_some_and(|pattern| {
264 content
265 .as_ref()
266 .is_some_and(|content| pattern.is_match(content))
267 })
268 };
269
270 // Only return a match for the given file if we have a better match than
271 // the current one.
272 match current_best_match {
273 LanguageMatchPrecedence::PathOrContent(current_len) => {
274 if let Some(len) = path_matches_custom_suffix() {
275 // >= because user config should win tie with system ext len
276 (len >= current_len).then_some(LanguageMatchPrecedence::UserConfigured(len))
277 } else if let Some(len) = path_matches_default_suffix() {
278 // >= because user config should win tie with system ext len
279 (len >= current_len).then_some(LanguageMatchPrecedence::PathOrContent(len))
280 } else {
281 None
282 }
283 }
284 LanguageMatchPrecedence::Undetermined => {
285 if let Some(len) = path_matches_custom_suffix() {
286 Some(LanguageMatchPrecedence::UserConfigured(len))
287 } else if let Some(len) = path_matches_default_suffix() {
288 Some(LanguageMatchPrecedence::PathOrContent(len))
289 } else if content_matches() {
290 Some(LanguageMatchPrecedence::PathOrContent(1))
291 } else {
292 None
293 }
294 }
295 LanguageMatchPrecedence::UserConfigured(_) => None,
296 }
297 })
298 }
299
300 fn find_best_match(
301 &self,
302 callback: impl Fn(
303 &LanguageName,
304 &LanguageMatcher,
305 LanguageMatchPrecedence,
306 ) -> Option<LanguageMatchPrecedence>,
307 ) -> Option<LanguageId> {
308 self.0
309 .iter()
310 .rev()
311 .fold(None, |best_language_match, language| {
312 let current_match_type = best_language_match
313 .as_ref()
314 .map_or(LanguageMatchPrecedence::default(), |(_, score)| *score);
315 let language_score =
316 callback(&language.name, &language.matcher, current_match_type);
317
318 match (language_score, current_match_type) {
319 // no current best, so our candidate is better
320 (
321 Some(
322 LanguageMatchPrecedence::PathOrContent(_)
323 | LanguageMatchPrecedence::UserConfigured(_),
324 ),
325 LanguageMatchPrecedence::Undetermined,
326 ) => language_score.map(|new_score| (language, new_score)),
327
328 // our candidate is better only if the name is longer
329 (
330 Some(LanguageMatchPrecedence::PathOrContent(new_len)),
331 LanguageMatchPrecedence::PathOrContent(current_len),
332 )
333 | (
334 Some(LanguageMatchPrecedence::UserConfigured(new_len)),
335 LanguageMatchPrecedence::UserConfigured(current_len),
336 )
337 | (
338 Some(LanguageMatchPrecedence::PathOrContent(new_len)),
339 LanguageMatchPrecedence::UserConfigured(current_len),
340 ) => {
341 if new_len > current_len {
342 language_score.map(|new_score| (language, new_score))
343 } else {
344 best_language_match
345 }
346 }
347
348 // our candidate is better if the name is longer or equal to
349 (
350 Some(LanguageMatchPrecedence::UserConfigured(new_len)),
351 LanguageMatchPrecedence::PathOrContent(current_len),
352 ) => {
353 if new_len >= current_len {
354 language_score.map(|new_score| (language, new_score))
355 } else {
356 best_language_match
357 }
358 }
359 // no candidate, use current best
360 (None, _) | (Some(LanguageMatchPrecedence::Undetermined), _) => {
361 best_language_match
362 }
363 }
364 })
365 .map(|(available_language, _)| available_language.id())
366 }
367}
368