1use std::borrow::Cow;
2use std::path::Path;
3
4use compact_str::CompactString;
5use rustc_hash::FxHashMap;
6use smallvec::SmallVec;
7
8#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
10pub struct LanguageId(u16);
11
12impl LanguageId {
13 #[must_use]
15 pub const fn index(self) -> usize {
16 self.0 as usize
17 }
18}
19
20#[non_exhaustive]
24pub enum ImportSpec {
25 Query {
28 source: Cow<'static, str>,
30 kind_map: fn(&str) -> crate::imports::ImportKind,
32 },
33 Custom(fn(&str, &tree_sitter::Tree) -> Vec<crate::imports::RawImport>),
36}
37
38#[non_exhaustive]
44pub struct LanguageSpec {
45 pub name: CompactString,
47 pub language: tree_sitter::Language,
49 pub extensions: SmallVec<[CompactString; 4]>,
51 pub tags_query: Option<Cow<'static, str>>,
53 pub injections_query: Option<Cow<'static, str>>,
58 pub merge_adjacent_same_name_definitions: bool,
63 pub imports: Option<ImportSpec>,
65}
66
67impl LanguageSpec {
68 #[must_use]
73 pub fn new<I, E>(
74 name: impl Into<CompactString>,
75 language: tree_sitter::Language,
76 extensions: I,
77 ) -> Self
78 where
79 I: IntoIterator<Item = E>,
80 E: AsRef<str>,
81 {
82 Self {
83 name: name.into(),
84 language,
85 extensions: extensions
86 .into_iter()
87 .map(|extension| CompactString::new(extension.as_ref()))
88 .collect(),
89 tags_query: None,
90 injections_query: None,
91 merge_adjacent_same_name_definitions: false,
92 imports: None,
93 }
94 }
95
96 #[must_use]
98 pub fn with_tags_query(mut self, tags_query: impl Into<Cow<'static, str>>) -> Self {
99 self.tags_query = Some(tags_query.into());
100 self
101 }
102
103 #[must_use]
105 pub fn with_injections_query(mut self, injections_query: impl Into<Cow<'static, str>>) -> Self {
106 self.injections_query = Some(injections_query.into());
107 self
108 }
109
110 #[must_use]
112 pub const fn with_merge_adjacent_same_name_definitions(mut self, enabled: bool) -> Self {
113 self.merge_adjacent_same_name_definitions = enabled;
114 self
115 }
116
117 #[must_use]
119 pub fn with_imports(mut self, imports: ImportSpec) -> Self {
120 self.imports = Some(imports);
121 self
122 }
123}
124
125pub struct LanguageRegistry {
127 specs: Vec<LanguageSpec>,
128 by_extension: FxHashMap<CompactString, LanguageId>,
129 generation: u64,
130}
131
132impl LanguageRegistry {
133 #[must_use]
135 pub fn empty() -> Self {
136 Self {
137 specs: Vec::new(),
138 by_extension: FxHashMap::default(),
139 generation: 0,
140 }
141 }
142
143 pub fn register(&mut self, spec: LanguageSpec) -> LanguageId {
148 let id = LanguageId(
149 u16::try_from(self.specs.len()).expect("language registry exhausted its u16 id space"),
150 );
151
152 for extension in &spec.extensions {
153 self.by_extension.insert(extension.clone(), id);
154 }
155
156 self.specs.push(spec);
157 self.generation += 1;
158 id
159 }
160
161 #[must_use]
163 pub const fn generation(&self) -> u64 {
164 self.generation
165 }
166
167 #[must_use]
169 pub fn for_path(&self, path: &Path) -> Option<LanguageId> {
170 let extension = path.extension()?.to_str()?;
171 self.by_extension.get(extension).copied()
172 }
173
174 #[must_use]
176 pub fn get(&self, id: LanguageId) -> Option<&LanguageSpec> {
177 self.specs.get(id.index())
178 }
179
180 #[must_use]
182 pub fn for_name(&self, name: &str) -> Option<LanguageId> {
183 self.specs
184 .iter()
185 .rposition(|spec| spec.name == name)
186 .and_then(|index| u16::try_from(index).ok())
187 .map(LanguageId)
188 }
189
190 #[must_use]
192 pub fn supports_symbols(&self, path: &Path) -> bool {
193 self.for_path(path)
194 .and_then(|id| self.get(id))
195 .is_some_and(|spec| spec.tags_query.is_some() || spec.injections_query.is_some())
196 }
197
198 #[must_use]
200 pub fn supports_imports(&self, path: &Path) -> bool {
201 self.for_path(path)
202 .and_then(|id| self.get(id))
203 .is_some_and(|spec| spec.imports.is_some() || spec.injections_query.is_some())
204 }
205
206 pub fn iter(&self) -> impl ExactSizeIterator<Item = (LanguageId, &LanguageSpec)> {
208 self.specs.iter().enumerate().map(|(index, spec)| {
209 let id = LanguageId(
210 u16::try_from(index).expect("registered language index must fit in LanguageId"),
211 );
212 (id, spec)
213 })
214 }
215}
216
217impl Default for LanguageRegistry {
218 fn default() -> Self {
219 Self::empty()
220 }
221}