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