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