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}