editorconfig_parser/
lib.rs1use std::path::{Path, PathBuf};
2
3use globset::{Glob, GlobMatcher};
4
5#[derive(Debug, Default, Clone)]
6pub struct EditorConfig {
7 root: bool,
9
10 sections: Vec<EditorConfigSection>,
11
12 cwd: Option<PathBuf>,
14}
15
16impl EditorConfig {
17 pub fn root(&self) -> bool {
18 self.root
19 }
20
21 pub fn sections(&self) -> &[EditorConfigSection] {
22 &self.sections
23 }
24
25 pub fn cwd(&self) -> Option<&Path> {
26 self.cwd.as_deref()
27 }
28
29 pub fn with_cwd<P: AsRef<Path>>(mut self, cwd: P) -> Self {
31 self.cwd = Some(cwd.as_ref().to_path_buf());
32 self
33 }
34}
35
36#[derive(Debug, Default, Clone)]
38pub struct EditorConfigSection {
39 pub name: String,
41
42 pub matcher: Option<GlobMatcher>,
43
44 pub properties: EditorConfigProperties,
45}
46
47#[derive(Debug, Default, Clone, Eq, PartialEq)]
48pub enum EditorConfigProperty<T> {
49 #[default]
50 None,
51 Unset,
52 Value(T),
53}
54
55#[derive(Debug, Default, Clone, Eq, PartialEq)]
56pub struct EditorConfigProperties {
57 pub indent_style: EditorConfigProperty<IndentStyle>,
62
63 pub indent_size: EditorConfigProperty<usize>,
67
68 pub tab_width: EditorConfigProperty<usize>,
71
72 pub end_of_line: EditorConfigProperty<EndOfLine>,
75
76 pub charset: EditorConfigProperty<Charset>,
80
81 pub trim_trailing_whitespace: EditorConfigProperty<bool>,
83
84 pub insert_final_newline: EditorConfigProperty<bool>,
87
88 pub max_line_length: EditorConfigProperty<MaxLineLength>,
92
93 pub quote_type: EditorConfigProperty<QuoteType>,
96}
97
98#[derive(Debug, Clone, Copy, Eq, PartialEq)]
99pub enum MaxLineLength {
100 Number(usize),
102 Off,
104}
105
106#[derive(Debug, Clone, Copy, Eq, PartialEq)]
107pub enum IndentStyle {
108 Tab,
109 Space,
110}
111
112#[derive(Debug, Clone, Copy, Eq, PartialEq)]
113pub enum EndOfLine {
114 Lf,
115 Cr,
116 Crlf,
117}
118
119#[derive(Debug, Clone, Copy, Eq, PartialEq)]
120pub enum Charset {
121 Latin1,
122 Utf8,
123 Utf8bom,
124 Utf16be,
125 Utf16le,
126}
127
128#[derive(Debug, Clone, Copy, Eq, PartialEq)]
129pub enum QuoteType {
130 Single,
131 Double,
132 Auto,
133}
134
135impl EditorConfig {
136 pub fn parse(source_text: &str) -> Self {
138 let mut root = false;
144 let mut sections = vec![];
145 let mut preamble = true;
146 for line in source_text.lines() {
147 let line = line.trim();
148 if line.is_empty() {
150 continue;
151 }
152 if line.starts_with([';', '#']) {
154 continue;
155 }
156 if preamble
158 && !line.starts_with('[')
159 && let Some((key, value)) = line.split_once('=')
160 && key.trim_end() == "root"
161 && value.trim_start().eq_ignore_ascii_case("true")
162 {
163 root = true;
164 }
165 if let Some(line) = line.strip_prefix('[') {
167 preamble = false;
168 if let Some(line) = line.strip_suffix(']') {
169 let name = line.to_string();
170 let matcher = Glob::new(&name).ok().map(|glob| glob.compile_matcher());
171 sections.push(EditorConfigSection {
172 name,
173 matcher,
174 ..EditorConfigSection::default()
175 });
176 }
177 }
178 if let Some(section) = sections.last_mut()
180 && let Some((key, value)) = line.split_once('=')
181 {
182 let value = value.trim_start();
183 let properties = &mut section.properties;
184 match key.trim_end() {
185 "indent_style" => {
186 properties.indent_style = IndentStyle::parse(value);
187 }
188 "indent_size" => {
189 properties.indent_size = EditorConfigProperty::<usize>::parse(value);
190 }
191 "tab_width" => {
192 properties.tab_width = EditorConfigProperty::<usize>::parse(value);
193 }
194 "end_of_line" => {
195 properties.end_of_line = EditorConfigProperty::<EndOfLine>::parse(value);
196 }
197 "charset" => {
198 properties.charset = EditorConfigProperty::<Charset>::parse(value);
199 }
200 "trim_trailing_whitespace" => {
201 properties.trim_trailing_whitespace =
202 EditorConfigProperty::<bool>::parse(value);
203 }
204 "insert_final_newline" => {
205 properties.insert_final_newline =
206 EditorConfigProperty::<bool>::parse(value);
207 }
208 "max_line_length" => {
209 properties.max_line_length =
210 EditorConfigProperty::<MaxLineLength>::parse(value);
211 }
212 "quote_type" => {
213 properties.quote_type = QuoteType::parse(value);
214 }
215 _ => {}
216 }
217 }
218 }
219
220 Self { root, sections, cwd: None }
221 }
222
223 pub fn resolve(&self, path: &Path) -> EditorConfigProperties {
226 let path =
227 if let Some(cwd) = &self.cwd { path.strip_prefix(cwd).unwrap_or(path) } else { path };
228 let mut properties = EditorConfigProperties::default();
229 for section in &self.sections {
230 if section.matcher.as_ref().is_some_and(|matcher| matcher.is_match(path)) {
231 properties.override_with(§ion.properties);
232 }
233 }
234 properties
235 }
236}
237
238impl<T: Copy> EditorConfigProperty<T> {
239 fn override_with(&mut self, other: &Self) {
240 match other {
241 Self::Value(value) => {
242 *self = Self::Value(*value);
243 }
244 Self::Unset => {
245 *self = Self::None;
246 }
247 Self::None => {}
248 }
249 }
250}
251
252impl EditorConfigProperties {
253 fn override_with(&mut self, other: &Self) {
254 self.indent_style.override_with(&other.indent_style);
255 self.indent_size.override_with(&other.indent_size);
256 self.tab_width.override_with(&other.tab_width);
257 self.end_of_line.override_with(&other.end_of_line);
258 self.charset.override_with(&other.charset);
259 self.trim_trailing_whitespace.override_with(&other.trim_trailing_whitespace);
260 self.insert_final_newline.override_with(&other.insert_final_newline);
261 self.max_line_length.override_with(&other.max_line_length);
262 self.quote_type.override_with(&other.quote_type);
263 }
264}
265
266impl EditorConfigProperty<usize> {
267 fn parse(s: &str) -> Self {
268 if s.eq_ignore_ascii_case("unset") {
269 Self::Unset
270 } else {
271 s.parse::<usize>().map_or(Self::None, EditorConfigProperty::Value)
272 }
273 }
274}
275
276impl EditorConfigProperty<bool> {
277 fn parse(s: &str) -> Self {
278 if s.eq_ignore_ascii_case("true") {
279 EditorConfigProperty::Value(true)
280 } else if s.eq_ignore_ascii_case("false") {
281 EditorConfigProperty::Value(false)
282 } else if s.eq_ignore_ascii_case("unset") {
283 EditorConfigProperty::Unset
284 } else {
285 EditorConfigProperty::None
286 }
287 }
288}
289
290impl IndentStyle {
291 fn parse(s: &str) -> EditorConfigProperty<Self> {
292 if s.eq_ignore_ascii_case("tab") {
293 EditorConfigProperty::Value(Self::Tab)
294 } else if s.eq_ignore_ascii_case("space") {
295 EditorConfigProperty::Value(Self::Space)
296 } else if s.eq_ignore_ascii_case("unset") {
297 EditorConfigProperty::Unset
298 } else {
299 EditorConfigProperty::None
300 }
301 }
302}
303
304impl EditorConfigProperty<EndOfLine> {
305 fn parse(s: &str) -> Self {
306 if s.eq_ignore_ascii_case("lf") {
307 Self::Value(EndOfLine::Lf)
308 } else if s.eq_ignore_ascii_case("cr") {
309 Self::Value(EndOfLine::Cr)
310 } else if s.eq_ignore_ascii_case("crlf") {
311 Self::Value(EndOfLine::Crlf)
312 } else if s.eq_ignore_ascii_case("unset") {
313 Self::Unset
314 } else {
315 Self::None
316 }
317 }
318}
319
320impl EditorConfigProperty<Charset> {
321 fn parse(s: &str) -> Self {
322 if s.eq_ignore_ascii_case("utf-8") {
323 Self::Value(Charset::Utf8)
324 } else if s.eq_ignore_ascii_case("latin1") {
325 Self::Value(Charset::Latin1)
326 } else if s.eq_ignore_ascii_case("utf-16be") {
327 Self::Value(Charset::Utf16be)
328 } else if s.eq_ignore_ascii_case("utf-16le") {
329 Self::Value(Charset::Utf16le)
330 } else if s.eq_ignore_ascii_case("utf-8-bom") {
331 Self::Value(Charset::Utf8bom)
332 } else if s.eq_ignore_ascii_case("unset") {
333 Self::Unset
334 } else {
335 Self::None
336 }
337 }
338}
339
340impl QuoteType {
341 fn parse(s: &str) -> EditorConfigProperty<Self> {
342 if s.eq_ignore_ascii_case("single") {
343 EditorConfigProperty::Value(Self::Single)
344 } else if s.eq_ignore_ascii_case("double") {
345 EditorConfigProperty::Value(Self::Double)
346 } else if s.eq_ignore_ascii_case("auto") {
347 EditorConfigProperty::Value(Self::Auto)
348 } else if s.eq_ignore_ascii_case("unset") {
349 EditorConfigProperty::Unset
350 } else {
351 EditorConfigProperty::None
352 }
353 }
354}
355
356impl EditorConfigProperty<MaxLineLength> {
357 fn parse(s: &str) -> Self {
358 if s.eq_ignore_ascii_case("off") {
359 Self::Value(MaxLineLength::Off)
360 } else if s.eq_ignore_ascii_case("unset") {
361 Self::Unset
362 } else if let Ok(n) = s.parse::<usize>() {
363 Self::Value(MaxLineLength::Number(n))
364 } else {
365 Self::None
366 }
367 }
368}