xi_core_lib/
whitespace.rs1extern crate xi_rope;
18
19use std::collections::BTreeMap;
20use xi_rope::Rope;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum Indentation {
25 Tabs,
26 Spaces(usize),
27}
28
29#[derive(Debug)]
31pub struct MixedIndentError;
32
33impl Indentation {
34 pub fn parse(rope: &Rope) -> Result<Option<Self>, MixedIndentError> {
36 let lines = rope.lines_raw(..);
37 let mut tabs = false;
38 let mut spaces: BTreeMap<usize, usize> = BTreeMap::new();
39
40 for line in lines {
41 match Indentation::parse_line(&line) {
42 Ok(Some(Indentation::Spaces(size))) => {
43 let counter = spaces.entry(size).or_insert(0);
44 *counter += 1;
45 }
46 Ok(Some(Indentation::Tabs)) => tabs = true,
47 Ok(None) => continue,
48 Err(e) => return Err(e),
49 }
50 }
51
52 match (tabs, !spaces.is_empty()) {
53 (true, true) => Err(MixedIndentError),
54 (true, false) => Ok(Some(Indentation::Tabs)),
55 (false, true) => {
56 let tab_size = extract_count(spaces);
57 if tab_size > 0 {
58 Ok(Some(Indentation::Spaces(tab_size)))
59 } else {
60 Ok(None)
61 }
62 }
63 _ => Ok(None),
64 }
65 }
66
67 pub fn parse_line(line: &str) -> Result<Option<Self>, MixedIndentError> {
70 let mut spaces = 0;
71
72 for char in line.as_bytes() {
73 match char {
74 b' ' => spaces += 1,
75 b'\t' if spaces > 0 => return Err(MixedIndentError),
76 b'\t' => return Ok(Some(Indentation::Tabs)),
77 _ => break,
78 }
79 }
80
81 if spaces > 0 {
82 Ok(Some(Indentation::Spaces(spaces)))
83 } else {
84 Ok(None)
85 }
86 }
87}
88
89fn extract_count(spaces: BTreeMap<usize, usize>) -> usize {
94 let mut take_size = 4;
95
96 if spaces.len() < take_size {
97 take_size = spaces.len();
98 }
99
100 spaces.iter().take(take_size).fold(0, |a, (b, _)| {
102 let d = gcd(a, *b);
103 if d == 1 {
104 a
105 } else {
106 d
107 }
108 })
109}
110
111fn gcd(a: usize, b: usize) -> usize {
113 if a == 0 {
114 b
115 } else if b == 0 || a == b {
116 a
117 } else {
118 let mut a = a;
119 let mut b = b;
120
121 while b > 0 {
122 let r = a % b;
123 a = b;
124 b = r;
125 }
126 a
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn gcd_calculates_correctly() {
136 assert_eq!(21, gcd(1071, 462));
137 assert_eq!(6, gcd(270, 192));
138 }
139
140 #[test]
141 fn line_gets_two_spaces() {
142 let result = Indentation::parse_line(" ");
143 let expected = Indentation::Spaces(2);
144
145 assert_eq!(result.unwrap(), Some(expected));
146 }
147
148 #[test]
149 fn line_gets_tabs() {
150 let result = Indentation::parse_line("\t");
151 let expected = Indentation::Tabs;
152
153 assert_eq!(result.unwrap(), Some(expected));
154 }
155
156 #[test]
157 fn line_errors_mixed_indent() {
158 let result = Indentation::parse_line(" \t");
159 assert!(result.is_err());
160 }
161
162 #[test]
163 fn rope_gets_two_spaces() {
164 let result = Indentation::parse(&Rope::from(
165 r#"
166 // This is a comment
167 Testing
168 Indented
169 Even more indented
170 # Comment
171 # Comment
172 # Comment
173 "#,
174 ));
175 let expected = Indentation::Spaces(2);
176
177 assert_eq!(result.unwrap(), Some(expected));
178 }
179
180 #[test]
181 fn rope_gets_four_spaces() {
182 let result = Indentation::parse(&Rope::from(
183 r#"
184 fn my_fun_func(&self,
185 another_arg: usize) -> Fun {
186 /* Random comment describing program behavior */
187 Fun::from(another_arg)
188 }
189 "#,
190 ));
191 let expected = Indentation::Spaces(4);
192
193 assert_eq!(result.unwrap(), Some(expected));
194 }
195
196 #[test]
197 fn rope_returns_none() {
198 let result = Indentation::parse(&Rope::from(
199 r#"# Readme example
200 1. One space.
201But the majority is still 0.
202"#,
203 ));
204
205 assert_eq!(result.unwrap(), None);
206 }
207}