xi_core_lib/
whitespace.rs

1// Copyright 2018 The xi-editor Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Utilities for detecting and working with indentation.
16
17extern crate xi_rope;
18
19use std::collections::BTreeMap;
20use xi_rope::Rope;
21
22/// An enumeration of legal indentation types.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum Indentation {
25    Tabs,
26    Spaces(usize),
27}
28
29/// A struct representing the mixed indentation error.
30#[derive(Debug)]
31pub struct MixedIndentError;
32
33impl Indentation {
34    /// Parses a rope for indentation settings.
35    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    /// Detects the indentation on a specific line.
68    /// Parses whitespace until first occurrence of something else
69    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
89/// Uses a heuristic to calculate the greatest common denominator of most used indentation depths.
90///
91/// As BTreeMaps are ordered by value, using take on the iterator ensures the indentation levels
92/// most frequently used in the file are extracted.
93fn 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    // Fold results using GCD, skipping numbers which result in gcd returning 1
101    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
111/// Simple implementation to calculate greatest common divisor, based on Euclid's algorithm
112fn 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}