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