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 merge_adjacent_same_name_definitions: bool,
58 pub imports: Option<ImportSpec>,
60}
61
62impl LanguageSpec {
63 #[must_use]
68 pub fn new<I, E>(
69 name: impl Into<CompactString>,
70 language: tree_sitter::Language,
71 extensions: I,
72 ) -> Self
73 where
74 I: IntoIterator<Item = E>,
75 E: AsRef<str>,
76 {
77 Self {
78 name: name.into(),
79 language,
80 extensions: extensions
81 .into_iter()
82 .map(|extension| CompactString::new(extension.as_ref()))
83 .collect(),
84 tags_query: None,
85 merge_adjacent_same_name_definitions: false,
86 imports: None,
87 }
88 }
89
90 #[must_use]
92 pub fn with_tags_query(mut self, tags_query: impl Into<Cow<'static, str>>) -> Self {
93 self.tags_query = Some(tags_query.into());
94 self
95 }
96
97 #[must_use]
99 pub const fn with_merge_adjacent_same_name_definitions(mut self, enabled: bool) -> Self {
100 self.merge_adjacent_same_name_definitions = enabled;
101 self
102 }
103
104 #[must_use]
106 pub fn with_imports(mut self, imports: ImportSpec) -> Self {
107 self.imports = Some(imports);
108 self
109 }
110}
111
112pub struct LanguageRegistry {
114 specs: Vec<LanguageSpec>,
115 by_extension: FxHashMap<CompactString, LanguageId>,
116 generation: u64,
117}
118
119impl LanguageRegistry {
120 #[must_use]
122 pub fn empty() -> Self {
123 Self {
124 specs: Vec::new(),
125 by_extension: FxHashMap::default(),
126 generation: 0,
127 }
128 }
129
130 pub fn register(&mut self, spec: LanguageSpec) -> LanguageId {
135 let id = LanguageId(
136 u16::try_from(self.specs.len()).expect("language registry exhausted its u16 id space"),
137 );
138
139 for extension in &spec.extensions {
140 self.by_extension.insert(extension.clone(), id);
141 }
142
143 self.specs.push(spec);
144 self.generation += 1;
145 id
146 }
147
148 #[must_use]
150 pub const fn generation(&self) -> u64 {
151 self.generation
152 }
153
154 #[must_use]
156 pub fn for_path(&self, path: &Path) -> Option<LanguageId> {
157 let extension = path.extension()?.to_str()?;
158 self.by_extension.get(extension).copied()
159 }
160
161 #[must_use]
163 pub fn get(&self, id: LanguageId) -> Option<&LanguageSpec> {
164 self.specs.get(id.index())
165 }
166
167 #[must_use]
169 pub fn supports_symbols(&self, path: &Path) -> bool {
170 self.for_path(path)
171 .and_then(|id| self.get(id))
172 .is_some_and(|spec| spec.tags_query.is_some())
173 }
174
175 #[must_use]
177 pub fn supports_imports(&self, path: &Path) -> bool {
178 self.for_path(path)
179 .and_then(|id| self.get(id))
180 .is_some_and(|spec| spec.imports.is_some())
181 }
182
183 pub fn iter(&self) -> impl ExactSizeIterator<Item = (LanguageId, &LanguageSpec)> {
185 self.specs.iter().enumerate().map(|(index, spec)| {
186 let id = LanguageId(
187 u16::try_from(index).expect("registered language index must fit in LanguageId"),
188 );
189 (id, spec)
190 })
191 }
192}
193
194impl Default for LanguageRegistry {
195 fn default() -> Self {
196 Self::empty()
197 }
198}