1use crate::parser::LangId;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum IndentStyle {
12 Tabs,
13 Spaces(u8),
14}
15
16impl IndentStyle {
17 pub fn as_str(&self) -> &'static str {
19 match self {
20 IndentStyle::Tabs => "\t",
21 IndentStyle::Spaces(2) => " ",
22 IndentStyle::Spaces(4) => " ",
23 IndentStyle::Spaces(8) => " ",
24 IndentStyle::Spaces(n) => {
25 let s: String = " ".repeat(*n as usize);
28 Box::leak(s.into_boxed_str())
29 }
30 }
31 }
32
33 pub fn default_for(lang: LangId) -> Self {
35 match lang {
36 LangId::Python => IndentStyle::Spaces(4),
37 LangId::TypeScript
38 | LangId::Tsx
39 | LangId::JavaScript
40 | LangId::Vue
41 | LangId::Json
42 | LangId::Scala
43 | LangId::Ruby
44 | LangId::Lua
45 | LangId::Scss
46 | LangId::Yaml
47 | LangId::Toml => IndentStyle::Spaces(2),
48 LangId::Rust => IndentStyle::Spaces(4),
49 LangId::Go => IndentStyle::Tabs,
50 LangId::C
51 | LangId::Cpp
52 | LangId::Cuda
53 | LangId::Metal
54 | LangId::Zig
55 | LangId::CSharp
56 | LangId::Bash => IndentStyle::Spaces(4),
57 LangId::Solidity
58 | LangId::ObjC
59 | LangId::Java
60 | LangId::Kotlin
61 | LangId::Swift
62 | LangId::Php
63 | LangId::Perl
64 | LangId::Groovy => IndentStyle::Spaces(4),
65 LangId::Html => IndentStyle::Spaces(2),
66 LangId::Markdown => IndentStyle::Spaces(4),
67 LangId::Pascal | LangId::R => IndentStyle::Spaces(2),
68 }
69 }
70}
71
72pub fn detect_indent(source: &str, lang: LangId) -> IndentStyle {
81 let mut tab_count: u32 = 0;
82 let mut space_count: u32 = 0;
83 let mut indent_widths: [u32; 9] = [0; 9]; for line in source.lines() {
86 if line.is_empty() {
87 continue;
88 }
89 let first = line.as_bytes()[0];
90 if first == b'\t' {
91 tab_count += 1;
92 } else if first == b' ' {
93 space_count += 1;
94 let leading = line.len() - line.trim_start_matches(' ').len();
96 if leading > 0 && leading <= 8 {
97 indent_widths[leading] += 1;
98 }
99 }
100 }
101
102 let total = tab_count + space_count;
103 if total == 0 {
104 return IndentStyle::default_for(lang);
105 }
106
107 if tab_count > total / 2 {
109 return IndentStyle::Tabs;
110 }
111
112 if space_count > total / 2 {
114 let width = determine_space_width(&indent_widths);
118 return IndentStyle::Spaces(width);
119 }
120
121 IndentStyle::default_for(lang)
123}
124
125fn determine_space_width(widths: &[u32; 9]) -> u8 {
131 let smallest = (1..=8usize).find(|&i| widths[i] > 0);
133 let smallest = match smallest {
134 Some(s) => s,
135 None => return 4,
136 };
137
138 let all_multiples = (1..=8).all(|i| widths[i] == 0 || i % smallest == 0);
140
141 if all_multiples && smallest >= 2 {
142 return smallest as u8;
143 }
144
145 for &candidate in &[4u8, 2, 8] {
147 let c = candidate as usize;
148 let mut matching: u32 = 0;
149 let mut non_matching: u32 = 0;
150 for i in 1..=8 {
151 if widths[i] > 0 {
152 if i % c == 0 {
153 matching += widths[i];
154 } else {
155 non_matching += widths[i];
156 }
157 }
158 }
159 if matching > 0 && non_matching == 0 {
160 return candidate;
161 }
162 }
163
164 smallest as u8
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn detect_indent_tabs() {
173 let source = "fn main() {\n\tlet x = 1;\n\tlet y = 2;\n}\n";
174 assert_eq!(detect_indent(source, LangId::Rust), IndentStyle::Tabs);
175 }
176
177 #[test]
178 fn detect_indent_two_spaces() {
179 let source = "class Foo {\n bar() {}\n baz() {}\n}\n";
180 assert_eq!(
181 detect_indent(source, LangId::TypeScript),
182 IndentStyle::Spaces(2)
183 );
184 }
185
186 #[test]
187 fn detect_indent_four_spaces() {
188 let source =
189 "class Foo:\n def bar(self):\n pass\n def baz(self):\n pass\n";
190 assert_eq!(
191 detect_indent(source, LangId::Python),
192 IndentStyle::Spaces(4)
193 );
194 }
195
196 #[test]
197 fn detect_indent_empty_source_uses_default() {
198 assert_eq!(detect_indent("", LangId::Python), IndentStyle::Spaces(4));
199 assert_eq!(
200 detect_indent("", LangId::TypeScript),
201 IndentStyle::Spaces(2)
202 );
203 assert_eq!(detect_indent("", LangId::Go), IndentStyle::Tabs);
204 }
205
206 #[test]
207 fn detect_indent_no_indented_lines_uses_default() {
208 let source = "x = 1\ny = 2\n";
209 assert_eq!(
210 detect_indent(source, LangId::Python),
211 IndentStyle::Spaces(4)
212 );
213 }
214
215 #[test]
216 fn indent_style_as_str() {
217 assert_eq!(IndentStyle::Tabs.as_str(), "\t");
218 assert_eq!(IndentStyle::Spaces(2).as_str(), " ");
219 assert_eq!(IndentStyle::Spaces(4).as_str(), " ");
220 }
221
222 #[test]
223 fn detect_indent_four_spaces_with_nested() {
224 let source = "impl Foo {\n fn bar() {\n let x = 1;\n }\n}\n";
226 assert_eq!(detect_indent(source, LangId::Rust), IndentStyle::Spaces(4));
227 }
228}