detect_indent/
lib.rs

1use lazy_static::lazy_static;
2use regex::Regex;
3use std::collections::HashMap;
4
5#[derive(Debug, PartialEq, Clone, Copy)]
6pub enum IndentKind {
7    Space,
8    Tab,
9}
10
11impl IndentKind {
12    pub fn repeat(&self, times: usize) -> String {
13        match *self {
14            IndentKind::Space => " ".repeat(times),
15            IndentKind::Tab => "\t".repeat(times),
16        }
17    }
18}
19
20#[derive(Debug, PartialEq)]
21pub struct Indent {
22    amount: usize,
23    indent: String,
24    kind: Option<IndentKind>,
25}
26
27impl Indent {
28    pub fn amount(&self) -> usize {
29        self.amount
30    }
31    pub fn indent(&self) -> &str {
32        &self.indent
33    }
34    pub fn kind(&self) -> Option<IndentKind> {
35        self.kind
36    }
37}
38
39#[derive(Debug)]
40struct Usage {
41    used: isize,
42    weight: isize,
43}
44
45fn most_used(indents: &HashMap<isize, Usage>) -> usize {
46    let mut result = 0;
47    let mut max_used = 0;
48    let mut max_weight = 0;
49
50    for (&key, usage) in indents.iter() {
51        if usage.used > max_used || (usage.used == max_used && usage.weight > max_weight) {
52            max_used = usage.used;
53            max_weight = usage.weight;
54            result = key;
55        }
56    }
57
58    assert!(
59        result >= 0,
60        "detect-irdent::most_used cannot return a negative"
61    );
62
63    result as usize
64}
65
66pub fn detect_indent(string: &str) -> Indent {
67    lazy_static! {
68        static ref INDENT_REGEX: Regex = Regex::new(r"^(?:( )+|\t+)").unwrap();
69    }
70
71    let mut spaces = 0;
72    let mut tabs = 0;
73    let mut indents: HashMap<isize, Usage> = HashMap::new();
74
75    let mut prev = 0;
76    let mut current: Option<isize> = None;
77    let mut key;
78
79    for line in string.lines() {
80        if line.is_empty() {
81            continue;
82        }
83        let mut indent = 0;
84
85        match INDENT_REGEX.captures(line) {
86            Some(captures) => {
87                if let Some(capture) = captures.get(0) {
88                    let string = capture.as_str();
89                    indent = string.len();
90
91                    match string.chars().next().unwrap() {
92                        ' ' => spaces += 1,
93                        _ => tabs += 1,
94                    }
95                };
96            }
97
98            None => indent = 0,
99        }
100
101        assert!(
102            indent <= (std::isize::MAX as usize),
103            "indent greater than std::isize::MAX"
104        );
105        let iindent = indent as isize;
106
107        let diff = iindent - prev;
108        prev = iindent;
109
110        if diff != 0 {
111            key = diff.abs();
112            current = Some(key);
113
114            indents
115                .entry(key)
116                .or_insert(Usage { used: 0, weight: 0 })
117                .used += 1;
118        } else if let Some(key) = current {
119            indents.get_mut(&key).unwrap().used += 1;
120        }
121    }
122
123    let amount = most_used(&indents);
124
125    let (kind, indent) = if amount == 0 {
126        (None, "".to_string())
127    } else if spaces >= tabs {
128        (Some(IndentKind::Space), IndentKind::Space.repeat(amount))
129    } else {
130        (Some(IndentKind::Tab), IndentKind::Tab.repeat(amount))
131    };
132
133    Indent {
134        amount,
135        indent,
136        kind,
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    fn indent_from_file(filepath: &str) -> Indent {
145        let contents = std::fs::read_to_string(filepath)
146            .unwrap_or_else(|e| panic!("Could not read file {filepath}: {e:?}"));
147        detect_indent(&contents)
148    }
149
150    #[test]
151    fn mixed_space() {
152        assert_eq!(
153            indent_from_file("fixture/mixed-space.js"),
154            Indent {
155                amount: 4,
156                indent: "    ".to_string(),
157                kind: Some(IndentKind::Space)
158            }
159        );
160    }
161
162    #[test]
163    fn mixed_tab() {
164        assert_eq!(
165            indent_from_file("fixture/mixed-tab.js"),
166            Indent {
167                amount: 1,
168                indent: "\t".to_string(),
169                kind: Some(IndentKind::Tab)
170            }
171        );
172    }
173
174    #[test]
175    fn space() {
176        assert_eq!(
177            indent_from_file("fixture/space.js"),
178            Indent {
179                amount: 4,
180                indent: "    ".to_string(),
181                kind: Some(IndentKind::Space)
182            }
183        );
184    }
185
186    #[test]
187    fn tab_four() {
188        assert_eq!(
189            indent_from_file("fixture/tab-four.js"),
190            Indent {
191                amount: 4,
192                indent: "\t\t\t\t".to_string(),
193                kind: Some(IndentKind::Tab)
194            }
195        );
196    }
197
198    #[test]
199    fn tab() {
200        assert_eq!(
201            indent_from_file("fixture/tab.js"),
202            Indent {
203                amount: 1,
204                indent: "\t".to_string(),
205                kind: Some(IndentKind::Tab)
206            }
207        );
208    }
209
210    #[test]
211    fn vendor_prefixed_css() {
212        assert_eq!(
213            indent_from_file("fixture/vendor-prefixed-css.css"),
214            Indent {
215                amount: 4,
216                indent: "    ".to_string(),
217                kind: Some(IndentKind::Space)
218            }
219        );
220    }
221
222    #[test]
223    fn test_get_most_used() {
224        let mut map = HashMap::new();
225        assert_eq!(most_used(&map), 0);
226        map.insert(1, Usage { used: 1, weight: 1 });
227        assert_eq!(most_used(&map), 1);
228        map.insert(2, Usage { used: 2, weight: 2 });
229        assert_eq!(most_used(&map), 2);
230        map.insert(3, Usage { used: 1, weight: 1 });
231        assert_eq!(most_used(&map), 2);
232        map.insert(4, Usage { used: 1, weight: 1 });
233        assert_eq!(most_used(&map), 2);
234        map.insert(5, Usage { used: 4, weight: 4 });
235        assert_eq!(most_used(&map), 5);
236        map.insert(
237            1,
238            Usage {
239                used: 10,
240                weight: 10,
241            },
242        );
243        assert_eq!(most_used(&map), 1);
244    }
245
246    #[test]
247    fn indent_kind_repeat() {
248        assert_eq!(IndentKind::Space.repeat(0), "");
249        assert_eq!(IndentKind::Space.repeat(1), " ");
250        assert_eq!(IndentKind::Space.repeat(10), "          ");
251
252        assert_eq!(IndentKind::Tab.repeat(0), "");
253        assert_eq!(IndentKind::Tab.repeat(1), "\t");
254        assert_eq!(IndentKind::Tab.repeat(10), "\t\t\t\t\t\t\t\t\t\t");
255    }
256}