Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:52:05.017Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

available_languages.rs

368 lines · 13.9 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.