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 | LangId::Tsx | LangId::JavaScript | LangId::Vue => {
38 IndentStyle::Spaces(2)
39 }
40 LangId::Rust => IndentStyle::Spaces(4),
41 LangId::Go => IndentStyle::Tabs,
42 LangId::C | LangId::Cpp | LangId::Zig | LangId::CSharp | LangId::Bash => {
43 IndentStyle::Spaces(4)
44 }
45 LangId::Solidity => IndentStyle::Spaces(4),
46 LangId::Html => IndentStyle::Spaces(2),
47 LangId::Markdown => IndentStyle::Spaces(4),
48 }
49 }
50}
51
52pub fn detect_indent(source: &str, lang: LangId) -> IndentStyle {
61 let mut tab_count: u32 = 0;
62 let mut space_count: u32 = 0;
63 let mut indent_widths: [u32; 9] = [0; 9]; for line in source.lines() {
66 if line.is_empty() {
67 continue;
68 }
69 let first = line.as_bytes()[0];
70 if first == b'\t' {
71 tab_count += 1;
72 } else if first == b' ' {
73 space_count += 1;
74 let leading = line.len() - line.trim_start_matches(' ').len();
76 if leading > 0 && leading <= 8 {
77 indent_widths[leading] += 1;
78 }
79 }
80 }
81
82 let total = tab_count + space_count;
83 if total == 0 {
84 return IndentStyle::default_for(lang);
85 }
86
87 if tab_count > total / 2 {
89 return IndentStyle::Tabs;
90 }
91
92 if space_count > total / 2 {
94 let width = determine_space_width(&indent_widths);
98 return IndentStyle::Spaces(width);
99 }
100
101 IndentStyle::default_for(lang)
103}
104
105fn determine_space_width(widths: &[u32; 9]) -> u8 {
111 let smallest = (1..=8usize).find(|&i| widths[i] > 0);
113 let smallest = match smallest {
114 Some(s) => s,
115 None => return 4,
116 };
117
118 let all_multiples = (1..=8).all(|i| widths[i] == 0 || i % smallest == 0);
120
121 if all_multiples && smallest >= 2 {
122 return smallest as u8;
123 }
124
125 for &candidate in &[4u8, 2, 8] {
127 let c = candidate as usize;
128 let mut matching: u32 = 0;
129 let mut non_matching: u32 = 0;
130 for i in 1..=8 {
131 if widths[i] > 0 {
132 if i % c == 0 {
133 matching += widths[i];
134 } else {
135 non_matching += widths[i];
136 }
137 }
138 }
139 if matching > 0 && non_matching == 0 {
140 return candidate;
141 }
142 }
143
144 smallest as u8
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn detect_indent_tabs() {
153 let source = "fn main() {\n\tlet x = 1;\n\tlet y = 2;\n}\n";
154 assert_eq!(detect_indent(source, LangId::Rust), IndentStyle::Tabs);
155 }
156
157 #[test]
158 fn detect_indent_two_spaces() {
159 let source = "class Foo {\n bar() {}\n baz() {}\n}\n";
160 assert_eq!(
161 detect_indent(source, LangId::TypeScript),
162 IndentStyle::Spaces(2)
163 );
164 }
165
166 #[test]
167 fn detect_indent_four_spaces() {
168 let source =
169 "class Foo:\n def bar(self):\n pass\n def baz(self):\n pass\n";
170 assert_eq!(
171 detect_indent(source, LangId::Python),
172 IndentStyle::Spaces(4)
173 );
174 }
175
176 #[test]
177 fn detect_indent_empty_source_uses_default() {
178 assert_eq!(detect_indent("", LangId::Python), IndentStyle::Spaces(4));
179 assert_eq!(
180 detect_indent("", LangId::TypeScript),
181 IndentStyle::Spaces(2)
182 );
183 assert_eq!(detect_indent("", LangId::Go), IndentStyle::Tabs);
184 }
185
186 #[test]
187 fn detect_indent_no_indented_lines_uses_default() {
188 let source = "x = 1\ny = 2\n";
189 assert_eq!(
190 detect_indent(source, LangId::Python),
191 IndentStyle::Spaces(4)
192 );
193 }
194
195 #[test]
196 fn indent_style_as_str() {
197 assert_eq!(IndentStyle::Tabs.as_str(), "\t");
198 assert_eq!(IndentStyle::Spaces(2).as_str(), " ");
199 assert_eq!(IndentStyle::Spaces(4).as_str(), " ");
200 }
201
202 #[test]
203 fn detect_indent_four_spaces_with_nested() {
204 let source = "impl Foo {\n fn bar() {\n let x = 1;\n }\n}\n";
206 assert_eq!(detect_indent(source, LangId::Rust), IndentStyle::Spaces(4));
207 }
208}