use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
struct IndentStyle {
pub spaces: usize,
pub uses_tabs: bool,
pub width: usize,
}
impl Default for IndentStyle {
fn default() -> Self {
Self {
spaces: 4,
uses_tabs: false,
width: 4,
}
}
}
impl IndentStyle {
pub fn indent_string(&self, level: usize) -> String {
if self.uses_tabs {
"\t".repeat(level)
} else {
" ".repeat(self.spaces * level)
}
}
}
const SUPERMAJORITY_RATIO: f64 = 0.80;
fn detect_indent_style(lines: &[String]) -> IndentStyle {
let mut space_levels: Vec<usize> = Vec::new();
let mut tab_levels: Vec<usize> = Vec::new();
for line in lines.iter() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let leading_spaces = line.chars().take_while(|c| *c == ' ').count();
let leading_tabs = line.chars().take_while(|c| *c == '\t').count();
if leading_tabs > 0 && leading_spaces == 0 {
tab_levels.push(leading_tabs);
} else if leading_spaces > 0 && leading_tabs == 0 {
space_levels.push(leading_spaces);
}
}
let total_indented = tab_levels.len() + space_levels.len();
if total_indented == 0 {
return IndentStyle::default();
}
if tab_levels.len() as f64 / total_indented as f64 > SUPERMAJORITY_RATIO {
let mode_width = mode_usize(&tab_levels).unwrap_or(1).max(1);
return IndentStyle {
spaces: 0,
uses_tabs: true,
width: mode_width,
};
}
let indent_step = detect_space_step(&space_levels);
IndentStyle {
spaces: indent_step,
uses_tabs: false,
width: indent_step,
}
}
fn detect_space_step(levels: &[usize]) -> usize {
if levels.is_empty() {
return 4;
}
let mut freq: HashMap<usize, usize> = HashMap::new();
for &l in levels {
*freq.entry(l).or_default() += 1;
}
let mut candidates: Vec<usize> = freq.keys().copied().filter(|&k| k > 0).collect();
candidates.sort_unstable();
let initial_len = candidates.len();
for i in 0..initial_len.saturating_sub(1) {
let d = candidates[i + 1] - candidates[i];
if d > 0 {
candidates.push(d);
}
}
candidates.push(4);
candidates.sort_unstable();
candidates.dedup();
let mut best_step = 4;
let mut best_score: i64 = -1;
for &candidate in &candidates {
if !(2..=16).contains(&candidate) {
continue;
}
let raw_freq = freq.get(&candidate).copied().unwrap_or(0) as i64;
let score = raw_freq * 1000 - candidate as i64;
if score > best_score {
best_score = score;
best_step = candidate;
}
}
best_step
}
fn mode_usize(values: &[usize]) -> Option<usize> {
let mut counts: HashMap<usize, usize> = HashMap::new();
for v in values {
*counts.entry(*v).or_default() += 1;
}
counts.into_iter().max_by_key(|(_, c)| *c).map(|(v, _)| v)
}
const MAX_SCAN_BACK: usize = 20;
fn detect_expected_indent(all_lines: &[String], start_line: usize) -> (IndentStyle, usize) {
let idx = start_line.saturating_sub(1).min(all_lines.len());
let window_start = idx.saturating_sub(MAX_SCAN_BACK);
let window = &all_lines[window_start..idx];
let indented_in_window = window
.iter()
.filter(|line| {
let trimmed = line.trim();
!trimmed.is_empty()
&& (line.chars().take_while(|c| *c == ' ').count() > 0
|| line.chars().take_while(|c| *c == '\t').count() > 0)
})
.count();
let style = if window_start == 0 && all_lines.len() > idx && indented_in_window < 2 {
let forward_end = (idx + MAX_SCAN_BACK).min(all_lines.len());
let mut combined = Vec::with_capacity(window.len() + forward_end - idx);
combined.extend_from_slice(window);
combined.extend_from_slice(&all_lines[idx..forward_end]);
if combined.len() >= 3 {
detect_indent_style(&combined)
} else {
detect_indent_style(all_lines)
}
} else if window.len() >= 3 {
detect_indent_style(window)
} else {
detect_indent_style(all_lines)
};
let step = if style.uses_tabs {
1
} else {
style.spaces.max(1)
};
let s = start_line.saturating_sub(1);
let scan_start = s.saturating_sub(MAX_SCAN_BACK);
let mut best_level: Option<usize> = None;
let mut best_quality: i32 = -1;
for i in (scan_start..s).rev() {
if i >= all_lines.len() {
continue;
}
let line = &all_lines[i];
if line.trim().is_empty() {
continue;
}
let leading = count_leading_whitespace(line);
let quality = context_quality(leading, step, line);
if quality > best_quality {
let level = round_to_nearest_level(leading, step);
best_level = Some(level);
best_quality = quality;
if leading.is_multiple_of(step) {
break;
}
}
}
let mut context_level = best_level.unwrap_or(0);
if let Some(line) = context_line_before(all_lines, start_line) {
let trimmed = line.trim_end();
let trimmed_no_comment = strip_trailing_comment(trimmed);
let last_brace_pos = trimmed_no_comment.rfind(['{', '}']);
let opens_block = last_brace_pos.is_some_and(|pos| {
trimmed_no_comment.as_bytes()[pos] == b'{'
});
if opens_block
|| trimmed_no_comment.ends_with(':')
|| trimmed_no_comment.ends_with('(')
|| trimmed_no_comment.ends_with('[')
{
context_level += 1;
}
let clb_leading = count_leading_whitespace(line);
let clb_level = round_to_nearest_level(clb_leading, step);
context_level = context_level.max(clb_level);
}
if best_level.is_some() && s < all_lines.len() && !all_lines[s].trim().is_empty() {
let at_line = &all_lines[s];
let at_leading = count_leading_whitespace(at_line);
let at_level = round_to_nearest_level(at_leading, step);
context_level = context_level.max(at_level);
}
(style, context_level)
}
fn context_line_before(all_lines: &[String], start_line: usize) -> Option<&String> {
let s = start_line.saturating_sub(1);
for i in (0..s).rev() {
if i >= all_lines.len() {
continue;
}
if !all_lines[i].trim().is_empty() {
return Some(&all_lines[i]);
}
}
None
}
#[allow(clippy::cast_possible_truncation)] fn context_quality(leading: usize, step: usize, _line: &str) -> i32 {
if step == 0 {
return 0;
}
let remainder = leading % step;
let base = if remainder == 0 {
10
} else if remainder <= 2 {
5
} else {
0
};
let depth_bonus = (leading / step).min(5) as i32;
base + depth_bonus
}
fn round_to_nearest_level(leading: usize, step: usize) -> usize {
if step == 0 {
return 0;
}
let exact = leading / step;
let remainder = leading % step;
if remainder > step / 2 {
exact + 1
} else {
exact
}
}
fn count_leading_whitespace(line: &str) -> usize {
line.chars().take_while(|c| c.is_whitespace()).count()
}
fn strip_trailing_comment(s: &str) -> &str {
if let Some(pos) = s.find("//") {
return &s[..pos];
}
if let Some(pos) = s.find('#') {
return &s[..pos];
}
s
}
pub fn validate_indentation(
all_lines: &[String],
start_line: usize,
_end_line: usize,
replacement_lines: &[String],
) -> (bool, Option<String>, Option<String>) {
let (style, expected_level) = detect_expected_indent(all_lines, start_line);
let expected_indent = style.indent_string(expected_level);
let mut has_content = false;
let mut min_leading = usize::MAX;
for line in replacement_lines.iter().filter(|l| !l.trim().is_empty()) {
has_content = true;
let trimmed = line.trim_start();
if trimmed.starts_with(')') || trimmed.starts_with('}') || trimmed.starts_with(']') {
continue;
}
let leading = line.chars().take_while(|c| c.is_whitespace()).count();
min_leading = min_leading.min(leading);
}
if !has_content {
return (true, None, None);
}
let expected_spaces = expected_indent.len();
let style_desc = if style.uses_tabs {
format!("{} tab(s)", expected_level)
} else {
format!("{} space(s)", expected_spaces)
};
if min_leading < expected_spaces {
let diff = expected_spaces - min_leading;
let warning = format!(
"INDENTATION WARNING: Replacement has {} leading {}, expected {} (diff: {})",
min_leading,
if style.uses_tabs {
"tab(s)"
} else {
"space(s)"
},
style_desc,
diff
);
let fix = replacement_lines
.iter()
.map(|line| {
if line.trim().is_empty() {
line.clone()
} else {
let stripped = line.trim_start();
format!("{}{}", expected_indent, stripped)
}
})
.collect::<Vec<_>>()
.join("\n");
(false, Some(warning), Some(fix))
} else {
(true, None, None)
}
}
pub fn auto_indent_content(
all_lines: &[String],
start_line: usize,
_end_line: usize,
content: &str,
) -> String {
let (style, expected_level) = detect_expected_indent(all_lines, start_line);
let expected_indent = style.indent_string(expected_level);
if expected_indent.is_empty() {
return content.to_string();
}
let content_lines: Vec<&str> = content.lines().collect();
let is_closer = |l: &&str| -> bool {
let t = l.trim();
t.starts_with('}') || t.starts_with(')') || t.starts_with(']')
};
let min_leading = content_lines
.iter()
.filter(|l| !l.trim().is_empty() && !is_closer(l))
.map(|l| l.chars().take_while(|c| c.is_whitespace()).count())
.min()
.unwrap_or(0);
let first_nonempty = content_lines.iter().find(|l| !l.trim().is_empty());
let starts_with_closer = first_nonempty.is_some_and(|l| {
let t = l.trim();
t.starts_with('}') || t.starts_with(')') || t.starts_with(']')
});
let effective_level = if starts_with_closer {
expected_level.saturating_sub(1)
} else {
expected_level
};
let effective_indent = style.indent_string(effective_level);
if min_leading >= effective_indent.len() {
return content.to_string();
}
content_lines
.iter()
.map(|line| {
if line.trim().is_empty() {
(*line).to_string()
} else {
let leading = line.chars().take_while(|c| c.is_whitespace()).count();
let overhang = leading - min_leading;
let indent_char = if style.uses_tabs { '\t' } else { ' ' };
let base = indent_char.to_string().repeat(effective_indent.len());
let overhang_spaces = " ".repeat(overhang);
format!(
"{}{}{}",
base,
overhang_spaces,
line.trim_start()
)
}
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn needs_indent_fix(
all_lines: &[String],
start_line: usize,
_end_line: usize,
content: &str,
) -> bool {
let (style, expected_level) = detect_expected_indent(all_lines, start_line);
let expected_indent = style.indent_string(expected_level);
if expected_indent.is_empty() {
return false;
}
let is_closer = |l: &&str| -> bool {
let t = l.trim();
t.starts_with('}') || t.starts_with(')') || t.starts_with(']')
};
let min_leading = content
.lines()
.filter(|l| !l.trim().is_empty() && !is_closer(l))
.map(|l| l.chars().take_while(|c| c.is_whitespace()).count())
.min()
.unwrap_or(0);
min_leading < expected_indent.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_spaces_indent() {
let lines = vec![
" def foo():".to_string(),
" pass".to_string(),
" def bar():".to_string(),
];
let style = detect_indent_style(&lines);
assert_eq!(style.spaces, 4);
assert!(!style.uses_tabs);
}
#[test]
fn test_detect_tabs_indent() {
let lines: Vec<String> = (0..50).map(|_| "\tfn foo() {".to_string()).collect();
let style = detect_indent_style(&lines);
assert!(style.uses_tabs);
}
#[test]
fn test_detect_2_space_indent() {
let lines = vec![" def foo():".to_string(), " pass".to_string()];
let style = detect_indent_style(&lines);
assert_eq!(style.spaces, 2);
}
#[test]
fn test_detect_8_space_indent() {
let lines = vec![
" fn main() {".to_string(),
" println!();".to_string(),
];
let style = detect_indent_style(&lines);
assert_eq!(style.spaces, 8);
}
#[test]
fn test_detect_indent_empty_file_defaults_spaces() {
let lines: Vec<String> = vec![];
let style = detect_indent_style(&lines);
assert_eq!(style.spaces, 4);
assert!(!style.uses_tabs);
}
#[test]
fn test_supermajority_one_tab_does_not_flip_space_file() {
let mut lines: Vec<String> = (0..29).map(|_| " fn foo() {".to_string()).collect();
lines.push("\tfn rogue() {}".to_string());
let style = detect_indent_style(&lines);
assert!(
!style.uses_tabs,
"Single rogue tab should not flip a space-indented file"
);
}
#[test]
fn test_detect_step_from_level_differences() {
let lines = vec![
"class Foo:".to_string(),
" def a(self):".to_string(),
" pass".to_string(),
" def b(self):".to_string(),
" return 1".to_string(),
];
let style = detect_indent_style(&lines);
assert_eq!(style.spaces, 4);
}
#[test]
fn test_detect_indent_with_anomalous_spacing() {
let lines = vec![
"fn main() {".to_string(),
" let x = 1;".to_string(), " let y = 2;".to_string(), " let z = 3;".to_string(), " let w = 4;".to_string(), ];
let style = detect_indent_style(&lines);
assert!(!style.uses_tabs);
assert!(style.spaces >= 1);
}
#[test]
fn test_expected_indent_after_colon() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let (_, level) = detect_expected_indent(&all_lines, 2);
assert_eq!(level, 1);
}
#[test]
fn test_expected_indent_after_brace() {
let all_lines = vec!["fn main() {\n".to_string(), " let x = 1;\n".to_string()];
let (_, level) = detect_expected_indent(&all_lines, 2);
assert_eq!(level, 1, "Content after `{{` should be indent level 1");
}
#[test]
fn test_expected_indent_after_brace_not_at_eol() {
let all_lines = vec![
"fn foo() { let x = 1;\n".to_string(),
" let y = 2;\n".to_string(),
];
let (_, level) = detect_expected_indent(&all_lines, 2);
assert_eq!(level, 1, "`{{` not at EOL still opens a block for next line");
}
#[test]
fn test_expected_indent_self_contained_block_no_indent() {
let all_lines = vec![
"fn foo() {}\n".to_string(),
"fn bar() {\n".to_string(),
];
let (_, level) = detect_expected_indent(&all_lines, 2);
assert_eq!(level, 0, "Self-contained block on prior line must not increment level");
}
#[test]
fn test_expected_indent_after_closing_brace() {
let all_lines = vec![
"fn outer() {\n".to_string(),
" fn inner() {\n".to_string(),
" pass\n".to_string(),
" }\n".to_string(),
" // editing here\n".to_string(),
];
let (_, level) = detect_expected_indent(&all_lines, 5);
assert_eq!(
level, 1,
"Content after `}}` at outer level should be level 1, not 2"
);
}
#[test]
fn test_expected_indent_top_of_file() {
let all_lines = vec!["fn main() {\n".to_string(), " let x = 1;\n".to_string()];
let (_, level) = detect_expected_indent(&all_lines, 1);
assert_eq!(level, 0);
}
#[test]
fn test_expected_indent_out_of_bounds() {
let all_lines = vec!["fn main() {\n".to_string(), " let x = 1;\n".to_string()];
let (_, level) = detect_expected_indent(&all_lines, 10);
assert_eq!(level, 1);
}
#[test]
fn test_expected_indent_empty_lines() {
let all_lines: Vec<String> = vec![];
let (style, level) = detect_expected_indent(&all_lines, 1);
assert_eq!(level, 0);
assert_eq!(style.spaces, 4);
assert!(!style.uses_tabs);
}
#[test]
fn test_expected_indent_start_line_zero() {
let all_lines = vec!["fn main() {\n".to_string(), " let x = 1;\n".to_string()];
let (_, level) = detect_expected_indent(&all_lines, 0);
assert_eq!(level, 0);
}
#[test]
fn test_expected_indent_deeply_nested() {
let all_lines = vec![
"class Foo:\n".to_string(),
" def bar(self):\n".to_string(),
" if True:\n".to_string(),
" pass\n".to_string(),
];
let (_, level) = detect_expected_indent(&all_lines, 4);
assert_eq!(level, 3);
}
#[test]
fn test_expected_indent_skips_blank_lines() {
let all_lines = vec![
"fn outer() {\n".to_string(),
" let x = 1;\n".to_string(),
"\n".to_string(),
"\n".to_string(),
" // editing here\n".to_string(),
];
let (_, level) = detect_expected_indent(&all_lines, 5);
assert_eq!(level, 1, "Blank lines must be skipped to find real context");
}
#[test]
fn test_expected_indent_one_misindented_line_does_not_poison() {
let all_lines = vec![
"fn main() {\n".to_string(),
" let a = 1;\n".to_string(),
" let bad = 2;\n".to_string(),
" let b = 3;\n".to_string(),
" let c = 4;\n".to_string(),
" let d = 5;\n".to_string(),
" let e = 6;\n".to_string(),
" // editing here\n".to_string(),
];
let (_, level) = detect_expected_indent(&all_lines, 8);
assert_eq!(
level, 1,
"A single misindented line must not poison context detection"
);
}
#[test]
fn test_expected_indent_off_by_one_rounding() {
let all_lines = vec![
"fn main() {\n".to_string(),
" let x = 1;\n".to_string(), ];
let (_, level) = detect_expected_indent(&all_lines, 2);
assert_eq!(
level, 1,
"3 spaces in a 4-space file should round to level 1, not 0"
);
}
#[test]
fn test_expected_indent_scan_back_multiple_lines() {
let all_lines = vec![
"fn main() {\n".to_string(),
" let a = 1;\n".to_string(),
" let b = 2;\n".to_string(),
" let c = 3;\n".to_string(), "\n".to_string(), "// editing here\n".to_string(),
];
let (_, level) = detect_expected_indent(&all_lines, 6);
assert_eq!(
level, 1,
"Must scan past blank+anomaly to find the real context"
);
}
#[test]
fn test_indent_step_from_window_not_global_file() {
let mut lines: Vec<String> = vec![
"fn main() {\n".to_string(),
" let x = 1;\n".to_string(),
" let y = 2;\n".to_string(),
];
for _ in 0..100 {
lines.push(" deep_body();\n".to_string());
}
lines.push(" // editing here\n".to_string());
let (style, _) = detect_expected_indent(&lines, 5);
assert_eq!(
style.spaces, 4,
"Edit near 4-space context should detect 4-space step, not 8 (deep body bias)"
);
}
#[test]
fn test_expected_indent_after_trailing_comma() {
let mut all_lines: Vec<String> = (0..20).map(|_| " let x = 1;\n".to_string()).collect();
all_lines.push(" my_function(\n".to_string());
all_lines.push(" arg1,\n".to_string());
all_lines.push(" arg2,\n".to_string());
let (_, level) = detect_expected_indent(&all_lines, 24);
assert_eq!(
level, 2,
"Comma means same-level continuation, not deeper indent"
);
}
#[test]
fn test_auto_indent_unindented_content() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let content = "print('hello')";
let fixed = auto_indent_content(&all_lines, 2, 2, content);
assert_eq!(fixed, " print('hello')");
}
#[test]
fn test_auto_indent_strips_existing_whitespace() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let content = " print('hello')";
let fixed = auto_indent_content(&all_lines, 2, 2, content);
assert_eq!(fixed, " print('hello')");
}
#[test]
fn test_auto_indent_multiline_content() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let content = "print('hello')\nprint('world')";
let fixed = auto_indent_content(&all_lines, 2, 2, content);
assert_eq!(fixed, " print('hello')\n print('world')");
}
#[test]
fn test_auto_indent_preserves_blank_lines() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let content = "print('a')\n\nprint('b')";
let fixed = auto_indent_content(&all_lines, 2, 2, content);
assert_eq!(fixed, " print('a')\n\n print('b')");
}
#[test]
fn test_auto_indent_mixed_existing_indent() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let content = " line1\n line2\nline3";
let fixed = auto_indent_content(&all_lines, 2, 2, content);
assert_eq!(fixed, " line1\n line2\n line3");
}
#[test]
fn test_auto_indent_preserves_multilevel_structure() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let content = " let x = 1;\n if true {\n run();\n }\n }";
let fixed = auto_indent_content(&all_lines, 2, 2, content);
assert_eq!(fixed, content);
}
#[test]
fn test_auto_indent_shifts_underindented_multilevel() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let content = " let x = 1;\n if true {\n run();\n }\n }";
let fixed = auto_indent_content(&all_lines, 2, 2, content);
assert_eq!(
fixed,
" let x = 1;\n if true {\n run();\n }\n }"
);
}
#[test]
fn test_auto_indent_no_indent_needed() {
let all_lines = vec!["fn main() {\n".to_string(), " let x = 1;\n".to_string()];
let content = "println!(\"hello\");";
let fixed = auto_indent_content(&all_lines, 1, 1, content);
assert_eq!(fixed, "println!(\"hello\");");
}
#[test]
fn test_auto_indent_with_tabs() {
let lines: Vec<String> = (0..30)
.map(|_| "\tfn foo() {".to_string())
.chain(std::iter::once("\t\tpass".to_string()))
.collect();
let content = "print('hello')";
let fixed = auto_indent_content(&lines, 31, 31, content);
assert_eq!(fixed, "\t\tprint('hello')");
}
#[test]
fn test_auto_indent_tabs_strips_existing_spaces() {
let lines: Vec<String> = (0..30)
.map(|_| "\tfn foo() {".to_string())
.chain(std::iter::once("\t\tpass".to_string()))
.collect();
let content = " print('hello')";
let fixed = auto_indent_content(&lines, 31, 31, content);
assert_eq!(fixed, " print('hello')");
}
#[test]
fn test_needs_indent_fix_unindented_content() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
assert!(needs_indent_fix(&all_lines, 2, 2, "print('hello')"));
}
#[test]
fn test_needs_indent_fix_already_indented() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
assert!(!needs_indent_fix(&all_lines, 2, 2, " print('hello')"));
}
#[test]
fn test_needs_indent_fix_partial_indent() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
assert!(needs_indent_fix(&all_lines, 2, 2, " print('hello')"));
}
#[test]
fn test_needs_indent_fix_tab_content() {
let lines: Vec<String> = (0..30)
.map(|_| "\tfn foo() {".to_string())
.chain(std::iter::once("\t\tpass".to_string()))
.collect();
assert!(needs_indent_fix(&lines, 31, 31, "print('hello')"));
assert!(!needs_indent_fix(&lines, 31, 31, "\t\tprint('hello')"));
}
#[test]
fn test_needs_indent_fix_top_level() {
let all_lines = vec!["fn main() {\n".to_string(), " let x = 1;\n".to_string()];
assert!(!needs_indent_fix(&all_lines, 1, 1, "println!(\"hello\");"));
}
#[test]
fn test_validate_indentation_missing() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let replacement = vec!["print('hello')".to_string()];
let (valid, warning, fix) = validate_indentation(&all_lines, 2, 2, &replacement);
assert!(!valid);
assert!(warning.is_some());
assert!(warning.unwrap().contains("4 space"));
assert_eq!(fix.unwrap(), " print('hello')");
}
#[test]
fn test_validate_indentation_correct() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let replacement = vec![" print('hello')".to_string()];
let (valid, warning, _fix) = validate_indentation(&all_lines, 2, 2, &replacement);
assert!(valid);
assert!(warning.is_none());
}
#[test]
fn test_validate_indentation_partial_fix_strips_existing() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let replacement = vec![" print('hello')".to_string()];
let (valid, _warning, fix) = validate_indentation(&all_lines, 2, 2, &replacement);
assert!(!valid);
assert_eq!(fix.unwrap(), " print('hello')");
}
#[test]
fn test_validate_indentation_empty_replacement() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let replacement: Vec<String> = vec!["".to_string()];
let (valid, warning, _fix) = validate_indentation(&all_lines, 2, 2, &replacement);
assert!(valid);
assert!(warning.is_none());
}
#[test]
fn test_validate_indentation_whitespace_only_line() {
let all_lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let replacement = vec![" ".to_string(), "print('x')".to_string()];
let (valid, _warning, _fix) = validate_indentation(&all_lines, 2, 2, &replacement);
assert!(!valid);
}
#[test]
fn test_validate_indentation_closer_tokens_at_lower_indent() {
let all_lines = vec![
"fn outer() {\n".to_string(),
" if true {\n".to_string(),
" do_thing();\n".to_string(),
" }\n".to_string(),
"}\n".to_string(),
];
let replacement = vec![
" more_code();\n".to_string(),
" }\n".to_string(),
"}\n".to_string(),
];
let (valid, warning, _fix) = validate_indentation(&all_lines, 3, 5, &replacement);
assert!(
valid,
"Closer tokens at lower indent are correct, got warning: {:?}",
warning
);
}
#[test]
fn test_validate_indentation_closer_tokens_at_lower_indent_single_line() {
let all_lines = vec![
"fn outer() {\n".to_string(),
" let x = 1;\n".to_string(),
"}\n".to_string(),
];
let replacement = vec!["}".to_string()];
let (valid, _warning, _fix) = validate_indentation(&all_lines, 2, 2, &replacement);
assert!(valid, "Single closing brace at lower indent is correct");
}
#[test]
fn test_indent_string_spaces() {
let style = IndentStyle {
spaces: 4,
uses_tabs: false,
width: 4,
};
assert_eq!(style.indent_string(0), "");
assert_eq!(style.indent_string(1), " ");
assert_eq!(style.indent_string(2), " ");
}
#[test]
fn test_indent_string_tabs() {
let style = IndentStyle {
spaces: 0,
uses_tabs: true,
width: 4,
};
assert_eq!(style.indent_string(0), "");
assert_eq!(style.indent_string(1), "\t");
assert_eq!(style.indent_string(2), "\t\t");
}
#[test]
fn test_round_to_nearest_level() {
assert_eq!(round_to_nearest_level(0, 4), 0);
assert_eq!(round_to_nearest_level(4, 4), 1);
assert_eq!(round_to_nearest_level(6, 4), 1); assert_eq!(round_to_nearest_level(2, 4), 0); assert_eq!(round_to_nearest_level(7, 4), 2); assert_eq!(round_to_nearest_level(3, 4), 1); }
#[test]
fn test_strip_trailing_comment() {
assert_eq!(
strip_trailing_comment(" let x = 1; // comment"),
" let x = 1; "
);
assert_eq!(strip_trailing_comment(" # python comment"), " ");
assert_eq!(strip_trailing_comment(" let x = 1;"), " let x = 1;");
assert_eq!(strip_trailing_comment("x = obj#method"), "x = obj");
}
#[test]
fn bug_auto_indent_closing_brace_should_not_be_indented() {
let lines = vec![
"fn outer() {\n".to_string(),
" let x = 1;\n".to_string(),
" // insert `}` here\n".to_string(),
];
let content = "}";
let fixed = auto_indent_content(&lines, 3, 3, content);
assert_eq!(fixed, "}",
"BUG: auto_indent_content indents closing brace to body level");
}
#[test]
fn bug_auto_indent_closing_brace_in_multi_line() {
let lines = vec![
"fn outer() {\n".to_string(),
" if true {\n".to_string(),
" do_work();\n".to_string(),
" } // closing if\n".to_string(),
" // insert here\n".to_string(),
];
let content = "}\nfn next() {";
let fixed = auto_indent_content(&lines, 5, 5, content);
assert_eq!(fixed, "}\nfn next() {",
"BUG: closing brace and next function should be at level 0");
}
#[test]
fn bug_auto_indent_tabs_overhang_uses_tabs_for_spaces() {
let mut lines: Vec<String> = (0..30)
.map(|_| "\tfn foo() {}".to_string())
.collect();
lines.push("\t\tpass".to_string());
let content = "outer\n inner";
let fixed = auto_indent_content(&lines, 31, 31, content);
assert_eq!(fixed, "\t\touter\n\t\t inner",
"BUG: overhang spaces incorrectly converted to tabs");
}
#[test]
fn bug_auto_indent_whitespace_only_lines() {
let lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let content = "x = 1\n \ny = 2";
let fixed = auto_indent_content(&lines, 2, 2, content);
assert_eq!(fixed, " x = 1\n \n y = 2");
}
#[test]
fn bug_detect_indent_all_blank_lines() {
let lines: Vec<String> = vec![
"\n".to_string(),
" \n".to_string(), "\n".to_string(),
" \n".to_string(), ];
let style = detect_indent_style(&lines);
assert_eq!(style.spaces, 4);
assert!(!style.uses_tabs);
}
#[test]
fn bug_detect_indent_all_zero_indent() {
let lines = vec![
"package main\n".to_string(),
"\n".to_string(),
"func main() {\n".to_string(),
"}\n".to_string(),
];
let style = detect_indent_style(&lines);
assert_eq!(style.spaces, 4);
assert!(!style.uses_tabs);
}
#[test]
fn bug_needs_indent_fix_tab_file_space_content() {
let lines: Vec<String> = (0..30)
.map(|_| "\tfn foo() {}".to_string())
.chain(std::iter::once("\t\tpass".to_string()))
.collect();
assert!(!needs_indent_fix(&lines, 31, 31, " print('hello')"));
}
#[test]
fn bug_auto_indent_empty_file() {
let lines: Vec<String> = vec![];
let content = "fn main() {\n println!(\"hello\");\n}";
let fixed = auto_indent_content(&lines, 1, 1, content);
assert_eq!(fixed, content);
}
#[test]
fn bug_detect_expected_indent_all_blank_context() {
let lines: Vec<String> = vec![
"\n".to_string(),
"\n".to_string(),
"\n".to_string(),
" let x = 1;\n".to_string(), ];
let (style, level) = detect_expected_indent(&lines, 4);
assert_eq!(style.spaces, 4);
assert_eq!(level, 0);
}
#[test]
fn bug_context_quality_step_zero() {
let q = context_quality(8, 0, " let x = 1;");
assert_eq!(q, 0);
}
#[test]
fn bug_round_to_nearest_level_step_zero() {
assert_eq!(round_to_nearest_level(8, 0), 0);
assert_eq!(round_to_nearest_level(0, 0), 0);
}
#[test]
fn bug_validate_indentation_only_closer_tokens() {
let lines = vec![
"fn outer() {\n".to_string(),
" if true {\n".to_string(),
" do_work();\n".to_string(),
" }\n".to_string(),
"}\n".to_string(),
];
let replacement = vec![" }\n".to_string(), "}\n".to_string()];
let (valid, warning, _fix) = validate_indentation(&lines, 4, 5, &replacement);
assert!(valid, "Only-closer-token content should be valid, got warning: {:?}", warning);
}
#[test]
fn bug_detect_indent_mixed_tabs_spaces_per_line_discarded() {
let mut lines: Vec<String> = (0..20).map(|_| " fn foo() {}".to_string()).collect();
lines.push(" \t fn mixed() {}".to_string());
lines.push(" \t fn mixed2() {}".to_string());
lines.push("\t fn mixed3() {}".to_string());
let style = detect_indent_style(&lines);
assert!(!style.uses_tabs, "Mixed lines should be discarded, keeping space style");
assert_eq!(style.spaces, 4);
}
#[test]
fn bug_auto_indent_already_correct_multilevel() {
let lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let content = " x = 1\n if y:\n z()\n w = 2";
let fixed = auto_indent_content(&lines, 2, 2, content);
assert_eq!(fixed, content);
}
#[test]
fn bug_auto_indent_overindented_content() {
let lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let content = " overindented"; let fixed = auto_indent_content(&lines, 2, 2, content);
assert_eq!(fixed, content);
}
#[test]
fn bug_auto_indent_single_empty_line() {
let lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let content = ""; let fixed = auto_indent_content(&lines, 2, 2, content);
assert_eq!(fixed, "");
}
#[test]
fn bug_validate_indentation_overindented_passes() {
let lines = vec!["def foo():\n".to_string(), " pass\n".to_string()];
let replacement = vec![" print('over')".to_string()]; let (valid, _warning, _fix) = validate_indentation(&lines, 2, 2, &replacement);
assert!(valid, "Over-indented content should pass validation");
}
#[test]
fn bug_detect_indent_single_indented_line() {
let lines = vec![" let x = 1;\n".to_string()];
let style = detect_indent_style(&lines);
assert_eq!(style.spaces, 4);
assert!(!style.uses_tabs);
}
#[test]
fn bug_auto_indent_closer_mid_content_preserves_body_indent() {
let lines = vec!["fn outer() {\n".to_string(), " // insert here\n".to_string()];
let content = " do_thing()\n}";
let fixed = auto_indent_content(&lines, 2, 2, content);
assert_eq!(fixed, " do_thing()\n}",
"G2: closer in content must not pull down body indentation");
}
}