mcpls_core/config/
language_map.rs1use std::collections::HashMap;
9use std::path::Path;
10
11use super::{FileExtension, FileName, LanguageId, PatternTarget};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum FileKey {
19 Extension(FileExtension),
21 Name(FileName),
23 Unmappable,
26}
27
28impl FileKey {
29 #[must_use]
49 pub fn of(path: &Path) -> Self {
50 let by_name = || {
51 path.file_name()
52 .and_then(|name| name.to_str())
53 .and_then(|name| FileName::new(name).ok())
54 .map_or(Self::Unmappable, Self::Name)
55 };
56 path.extension().map_or_else(by_name, |extension| {
57 extension
58 .to_str()
59 .and_then(|extension| FileExtension::new(extension).ok())
60 .map_or(Self::Unmappable, Self::Extension)
61 })
62 }
63}
64
65#[derive(Debug, Clone, Default, PartialEq, Eq)]
83pub struct LanguageMap {
84 by_extension: HashMap<FileExtension, LanguageId>,
85 by_name: HashMap<FileName, LanguageId>,
86}
87
88impl LanguageMap {
89 pub fn insert(&mut self, target: PatternTarget, language: LanguageId) {
92 match target {
93 PatternTarget::Extension(extension) => {
94 self.by_extension.insert(extension, language);
95 }
96 PatternTarget::Name(name) => {
97 self.by_name.insert(name, language);
98 }
99 }
100 }
101
102 #[must_use]
105 pub fn detect(&self, path: &Path) -> LanguageId {
106 match FileKey::of(path) {
107 FileKey::Extension(extension) => self.by_extension.get(&extension),
108 FileKey::Name(name) => self.by_name.get(&name),
109 FileKey::Unmappable => None,
110 }
111 .cloned()
112 .unwrap_or(LanguageId::PLAINTEXT)
113 }
114
115 pub fn languages(&self) -> impl Iterator<Item = &LanguageId> {
118 self.by_extension.values().chain(self.by_name.values())
119 }
120}
121
122impl From<HashMap<FileExtension, LanguageId>> for LanguageMap {
123 fn from(by_extension: HashMap<FileExtension, LanguageId>) -> Self {
124 Self {
125 by_extension,
126 by_name: HashMap::new(),
127 }
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 fn map() -> LanguageMap {
136 let mut map = LanguageMap::from(HashMap::from([
137 (
138 FileExtension::from_static("rs"),
139 LanguageId::from_static("rust"),
140 ),
141 (
142 FileExtension::from_static("R"),
143 LanguageId::from_static("r"),
144 ),
145 ]));
146 map.insert(
147 PatternTarget::Name(FileName::from_static("Dockerfile")),
148 LanguageId::from_static("dockerfile"),
149 );
150 map
151 }
152
153 #[test]
154 fn test_detect_by_extension_and_name() {
155 let map = map();
156 assert_eq!(map.detect(Path::new("src/main.rs")), "rust");
157 assert_eq!(map.detect(Path::new("a/b/Dockerfile")), "dockerfile");
158 }
159
160 #[test]
161 fn test_detect_is_case_sensitive() {
162 let map = map();
163 assert_eq!(map.detect(Path::new("x.r")), LanguageId::PLAINTEXT);
164 assert_eq!(map.detect(Path::new("x.R")), "r");
165 assert_eq!(map.detect(Path::new("dockerfile")), LanguageId::PLAINTEXT);
166 }
167
168 #[test]
169 fn test_detect_falls_back_to_plaintext() {
170 let map = map();
171 for unmapped in ["x.unknown", "Makefile", ".gitignore", "x.", "x.r\u{e9}"] {
172 assert_eq!(map.detect(Path::new(unmapped)), LanguageId::PLAINTEXT);
173 }
174 }
175
176 #[test]
177 fn test_a_file_with_an_extension_never_matches_a_name() {
178 let map = map();
179 assert_eq!(
180 map.detect(Path::new("Dockerfile.bak")),
181 LanguageId::PLAINTEXT
182 );
183 }
184
185 #[test]
186 fn test_file_key_of() {
187 assert_eq!(
188 FileKey::of(Path::new("a.tar.gz")),
189 FileKey::Extension(FileExtension::from_static("gz"))
190 );
191 assert_eq!(
192 FileKey::of(Path::new("Makefile")),
193 FileKey::Name(FileName::from_static("Makefile"))
194 );
195 for unmappable in [".gitignore", "x.", "r\u{e9}", "a.\u{e9}", "", ".."] {
196 assert_eq!(FileKey::of(Path::new(unmappable)), FileKey::Unmappable);
197 }
198 }
199
200 #[test]
201 fn test_languages_lists_both_maps() {
202 let map = map();
203 let languages: Vec<&LanguageId> = map.languages().collect();
204 assert_eq!(languages.len(), 3);
205 assert!(languages.contains(&&LanguageId::from_static("dockerfile")));
206 }
207}