Skip to main content

oxilean_parse/indent_tracker/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::types::{
6    IndentBlock, IndentChangeLog, IndentDiff, IndentGuide, IndentLevel, IndentMode, IndentRegion,
7    LayoutContext, TokenSequenceClass,
8};
9
10#[cfg(test)]
11mod tests {
12    use super::*;
13    use crate::indent_tracker::*;
14    #[test]
15    fn test_indent_level_total_width_spaces_only() {
16        let level = IndentLevel::new(4, 0);
17        assert_eq!(level.total_width(4), 4);
18    }
19    #[test]
20    fn test_indent_level_total_width_tabs() {
21        let level = IndentLevel::new(0, 2);
22        assert_eq!(level.total_width(4), 8);
23    }
24    #[test]
25    fn test_indent_level_is_deeper_than() {
26        let shallow = IndentLevel::new(2, 0);
27        let deep = IndentLevel::new(6, 0);
28        assert!(deep.is_deeper_than(&shallow, 4));
29        assert!(!shallow.is_deeper_than(&deep, 4));
30        assert!(!shallow.is_deeper_than(&shallow, 4));
31    }
32    #[test]
33    fn test_indent_stack_push_pop() {
34        let mut stack = IndentStack::new(4);
35        stack.push(IndentLevel::new(0, 0));
36        stack.push(IndentLevel::new(4, 0));
37        assert_eq!(
38            stack
39                .current()
40                .expect("test operation should succeed")
41                .spaces,
42            4
43        );
44        let popped = stack.pop().expect("collection should not be empty");
45        assert_eq!(popped.spaces, 4);
46        assert_eq!(
47            stack
48                .current()
49                .expect("test operation should succeed")
50                .spaces,
51            0
52        );
53    }
54    #[test]
55    fn test_indent_stack_dedent_to() {
56        let mut stack = IndentStack::new(4);
57        stack.push(IndentLevel::new(0, 0));
58        stack.push(IndentLevel::new(4, 0));
59        stack.push(IndentLevel::new(8, 0));
60        let target = IndentLevel::new(4, 0);
61        let popped = stack.dedent_to(&target);
62        assert_eq!(popped, 1);
63        assert_eq!(
64            stack
65                .current()
66                .expect("test operation should succeed")
67                .spaces,
68            4
69        );
70    }
71    #[test]
72    fn test_parse_leading_whitespace_spaces() {
73        let level = WhereBlockTracker::parse_leading_whitespace("    hello");
74        assert_eq!(level.spaces, 4);
75        assert_eq!(level.tabs, 0);
76    }
77    #[test]
78    fn test_parse_leading_whitespace_tabs() {
79        let level = WhereBlockTracker::parse_leading_whitespace("\t\thello");
80        assert_eq!(level.spaces, 0);
81        assert_eq!(level.tabs, 2);
82    }
83    #[test]
84    fn test_where_block_tracker_enter_exit() {
85        let mut tracker = WhereBlockTracker::new();
86        assert!(!tracker.in_where_block);
87        tracker.enter_where(IndentLevel::new(0, 0));
88        assert!(tracker.in_where_block);
89        tracker.add_where_item("foo", IndentLevel::new(2, 0));
90        assert_eq!(tracker.where_items.len(), 1);
91        tracker.exit_where();
92        assert!(!tracker.in_where_block);
93        assert!(tracker.where_items.is_empty());
94    }
95    #[test]
96    fn test_where_block_tracker_is_where_item() {
97        let mut tracker = WhereBlockTracker::new();
98        tracker.enter_where(IndentLevel::new(0, 0));
99        assert!(tracker.is_where_item("  foo : Nat", IndentLevel::new(2, 0)));
100        assert!(!tracker.is_where_item("bar : Nat", IndentLevel::new(0, 0)));
101    }
102}
103/// Classify a block of text lines as a token sequence.
104#[allow(dead_code)]
105pub fn classify_sequence(lines: &[&str]) -> TokenSequenceClass {
106    let has_where = lines
107        .iter()
108        .any(|l| l.trim() == "where" || l.trim().starts_with("where "));
109    let has_let = lines.iter().any(|l| l.trim().starts_with("let "));
110    let has_do = lines
111        .iter()
112        .any(|l| l.trim().starts_with("do ") || l.trim() == "do");
113    let has_def = lines.iter().any(|l| {
114        let t = l.trim();
115        t.starts_with("def ") || t.starts_with("theorem ") || t.starts_with("definition ")
116    });
117    if has_def && has_where {
118        TokenSequenceClass::DefWithWhere
119    } else if has_def {
120        TokenSequenceClass::PlainDef
121    } else if has_let {
122        TokenSequenceClass::LetBlock
123    } else if has_do {
124        TokenSequenceClass::DoBlock
125    } else {
126        TokenSequenceClass::Unknown
127    }
128}
129/// Split a source string into `IndentRegion`s based on consistent indentation.
130#[allow(dead_code)]
131pub fn split_into_regions(src: &str, tab_width: usize) -> Vec<IndentRegion> {
132    let mut regions = Vec::new();
133    let mut current_indent: Option<usize> = None;
134    let mut region_start = 1usize;
135    for (i, line) in src.lines().enumerate() {
136        let lineno = i + 1;
137        if line.trim().is_empty() {
138            continue;
139        }
140        let indent = LayoutContext::indent_col(line, tab_width);
141        match current_indent {
142            None => {
143                current_indent = Some(indent);
144                region_start = lineno;
145            }
146            Some(cur) if cur == indent => {}
147            Some(cur) => {
148                regions.push(IndentRegion::new(region_start, lineno - 1, cur));
149                current_indent = Some(indent);
150                region_start = lineno;
151            }
152        }
153    }
154    if let Some(cur) = current_indent {
155        let line_count = src.lines().count();
156        regions.push(IndentRegion::new(region_start, line_count, cur));
157    }
158    regions
159}
160#[cfg(test)]
161mod additional_tests_2 {
162    use super::*;
163    use crate::indent_tracker::*;
164    #[test]
165    fn test_classify_sequence_def_with_where() {
166        let lines = vec!["def foo := body", "  where", "    helper := 1"];
167        assert_eq!(classify_sequence(&lines), TokenSequenceClass::DefWithWhere);
168    }
169    #[test]
170    fn test_classify_sequence_plain_def() {
171        let lines = vec!["def foo := 1"];
172        assert_eq!(classify_sequence(&lines), TokenSequenceClass::PlainDef);
173    }
174    #[test]
175    fn test_classify_sequence_let_block() {
176        let lines = vec!["let x := 1", "let y := 2"];
177        assert_eq!(classify_sequence(&lines), TokenSequenceClass::LetBlock);
178    }
179    #[test]
180    fn test_classify_sequence_do_block() {
181        let lines = vec!["do", "  x <- foo"];
182        assert_eq!(classify_sequence(&lines), TokenSequenceClass::DoBlock);
183    }
184    #[test]
185    fn test_classify_sequence_unknown() {
186        let lines = vec!["x + y", "z * w"];
187        assert_eq!(classify_sequence(&lines), TokenSequenceClass::Unknown);
188    }
189    #[test]
190    fn test_indent_level_history_undo() {
191        let mut hist = IndentLevelHistory::new();
192        hist.set_levels(vec![IndentLevel::new(0, 0)]);
193        hist.snapshot();
194        hist.set_levels(vec![IndentLevel::new(4, 0)]);
195        assert!(hist.undo());
196        assert_eq!(hist.current()[0].spaces, 0);
197        assert_eq!(hist.undo_depth(), 0);
198        assert!(!hist.undo());
199    }
200    #[test]
201    fn test_indent_level_history_redo() {
202        let mut hist = IndentLevelHistory::new();
203        hist.set_levels(vec![IndentLevel::new(0, 0)]);
204        hist.snapshot();
205        hist.set_levels(vec![IndentLevel::new(4, 0)]);
206        hist.undo();
207        assert!(hist.redo());
208        assert_eq!(hist.current()[0].spaces, 4);
209    }
210    #[test]
211    fn test_indent_level_history_snapshot_clears_future() {
212        let mut hist = IndentLevelHistory::new();
213        hist.set_levels(vec![IndentLevel::new(0, 0)]);
214        hist.snapshot();
215        hist.set_levels(vec![IndentLevel::new(4, 0)]);
216        hist.undo();
217        hist.snapshot();
218        hist.set_levels(vec![IndentLevel::new(8, 0)]);
219        assert_eq!(hist.redo_depth(), 0);
220    }
221    #[test]
222    fn test_indent_region_line_count() {
223        let region = IndentRegion::new(3, 7, 4);
224        assert_eq!(region.line_count(), 5);
225    }
226    #[test]
227    fn test_indent_region_contains_line() {
228        let region = IndentRegion::new(2, 5, 0);
229        assert!(region.contains_line(2));
230        assert!(region.contains_line(5));
231        assert!(!region.contains_line(6));
232    }
233    #[test]
234    fn test_split_into_regions_empty() {
235        let regions = split_into_regions("", 4);
236        assert!(regions.is_empty());
237    }
238    #[test]
239    fn test_split_into_regions_uniform() {
240        let src = "def foo := 1\ndef bar := 2\n";
241        let regions = split_into_regions(src, 4);
242        assert!(!regions.is_empty());
243    }
244    #[test]
245    fn test_indent_level_history_empty_undo() {
246        let mut hist = IndentLevelHistory::new();
247        assert!(!hist.undo());
248    }
249    #[test]
250    fn test_indent_region_single_line() {
251        let region = IndentRegion::new(5, 5, 0);
252        assert_eq!(region.line_count(), 1);
253    }
254    #[test]
255    fn test_token_sequence_class_eq() {
256        assert_eq!(TokenSequenceClass::PlainDef, TokenSequenceClass::PlainDef);
257        assert_ne!(TokenSequenceClass::PlainDef, TokenSequenceClass::DoBlock);
258    }
259    #[test]
260    fn test_layout_context_default_tab_width() {
261        let ctx = LayoutContext::default();
262        assert_eq!(ctx.tab_width, 4);
263    }
264    #[test]
265    fn test_layout_context_push_pop() {
266        let mut ctx = LayoutContext::new(4);
267        ctx.push_rule(LayoutRule::SameBlock(4));
268        assert_eq!(ctx.depth(), 1);
269        let popped = ctx.pop_rule();
270        assert!(matches!(popped, Some(LayoutRule::SameBlock(4))));
271        assert_eq!(ctx.depth(), 0);
272    }
273    #[test]
274    fn test_layout_rule_continuation() {
275        let rule = LayoutRule::Continuation(4);
276        assert!(rule.matches(5));
277        assert!(!rule.matches(4));
278        assert!(!rule.matches(3));
279    }
280    #[test]
281    fn test_layout_rule_close_block() {
282        let rule = LayoutRule::CloseBlock(4);
283        assert!(rule.matches(2));
284        assert!(!rule.matches(4));
285        assert!(!rule.matches(6));
286    }
287    #[test]
288    fn test_indent_mismatch_error_display() {
289        let err = IndentMismatchError::new(10, 4, 3, "test");
290        let s = format!("{}", err);
291        assert!(s.contains("line 10"));
292    }
293    #[test]
294    fn test_indentation_checker_mismatch() {
295        let mut checker = IndentationChecker::new(4);
296        let base = IndentLevel::new(0, 0);
297        let odd = IndentLevel::new(3, 0);
298        checker.check_transition(5, &base, &odd, "test");
299        assert!(!checker.is_clean());
300    }
301    #[test]
302    fn test_indentation_checker_clean() {
303        let mut checker = IndentationChecker::new(4);
304        let base = IndentLevel::new(0, 0);
305        let deeper = IndentLevel::new(4, 0);
306        checker.check_transition(1, &base, &deeper, "test");
307        assert!(checker.is_clean());
308    }
309    #[test]
310    fn test_indent_diff_same() {
311        let a = IndentLevel::new(4, 0);
312        let b = IndentLevel::new(4, 0);
313        assert_eq!(compare_indent(&a, &b, 4), IndentDiff::Same);
314    }
315    #[test]
316    fn test_tab_stop_from_col_on_boundary() {
317        let stops: Vec<usize> = TabStopIterator::from_col(4, 8).take(2).collect();
318        assert_eq!(stops, vec![8, 12]);
319    }
320    #[test]
321    fn test_scope_is_bound() {
322        let mut scope = Scope::new("where", IndentLevel::new(0, 0));
323        scope.add_binding("alpha");
324        assert!(scope.is_bound("alpha"));
325        assert!(!scope.is_bound("beta"));
326    }
327    #[test]
328    fn test_let_binding_has_type() {
329        let b = LetBinding::new("x", 0, true);
330        assert!(b.has_type);
331        assert_eq!(b.col, 0);
332    }
333    #[test]
334    fn test_indent_history_max_min() {
335        let hist = IndentHistory {
336            entries: vec![(1, 0), (2, 4), (3, 8)],
337            tab_width: 4,
338        };
339        assert_eq!(hist.max_indent(), 8);
340        assert_eq!(hist.min_nonzero_indent(), Some(4));
341    }
342    #[test]
343    fn test_do_block_tracker_nested() {
344        let mut t = DoBlockTracker::new(4);
345        t.enter(4);
346        t.enter(8);
347        assert_eq!(t.depth(), 2);
348        t.exit();
349        assert_eq!(t.current_col(), Some(4));
350    }
351    #[test]
352    fn test_hanging_indent_zero_overhang() {
353        let hi = HangingIndent::new(4, 4);
354        assert_eq!(hi.overhang(), 0);
355    }
356    #[test]
357    fn test_multiline_string_has_unescaped_quote() {
358        assert!(MultilineStringTracker::has_unescaped_quote(
359            r#"hello "world""#
360        ));
361        assert!(!MultilineStringTracker::has_unescaped_quote("no quotes"));
362    }
363    #[test]
364    fn test_comment_tracker_nested_block() {
365        let mut ct = CommentTracker::new();
366        ct.process_pair('/', '-');
367        ct.process_pair('/', '-');
368        assert_eq!(ct.block_depth, 2);
369        ct.process_pair('-', '/');
370        assert_eq!(ct.block_depth, 1);
371        ct.process_pair('-', '/');
372        assert!(!ct.in_comment());
373    }
374    #[test]
375    fn test_indent_validator_tabs() {
376        let mut v = IndentValidator::expect_tabs();
377        v.validate("\thello\n");
378        assert!(v.is_valid());
379        v.validate("    bad\n");
380        assert!(!v.is_valid());
381    }
382    #[test]
383    fn test_column_aligner_pad() {
384        let aligner = ColumnAligner::new(10);
385        let padded = aligner.pad("foo", 3);
386        assert_eq!(padded.len(), 10);
387    }
388    #[test]
389    fn test_whitespace_kind_none() {
390        let kind = WhitespaceKind::classify("ab", 1, 1);
391        assert_eq!(kind, WhitespaceKind::None);
392    }
393    #[test]
394    fn test_open_brace_tracker_mismatch() {
395        let mut t = OpenBraceTracker::new();
396        t.push('(', 1, 0);
397        assert!(t.pop(']', 1, 5).is_err());
398    }
399    #[test]
400    fn test_indent_rewriter_no_op() {
401        let rw = IndentRewriter::new(4, 4);
402        let input = "    hello";
403        assert_eq!(rw.rewrite_line(input), input);
404    }
405    #[test]
406    fn test_source_splitter_single_decl() {
407        let src = "def foo := 1\n  body\n";
408        let splitter = SourceSplitter::new(4);
409        let blocks = splitter.split(src);
410        assert_eq!(blocks.len(), 1);
411    }
412    #[test]
413    fn test_indent_normaliser_all_spaces() {
414        let norm = IndentNormaliser::new(4);
415        assert_eq!(norm.normalise_line("  hello"), "  hello");
416    }
417    #[test]
418    fn test_aligned_printer_empty() {
419        let printer = AlignedPrinter::new(":=");
420        assert!(printer.is_empty());
421        assert_eq!(printer.format(), "");
422    }
423    #[test]
424    fn test_gcd_large() {
425        assert_eq!(gcd(100, 75), 25);
426        assert_eq!(gcd(17, 13), 1);
427    }
428    #[test]
429    fn test_indent_delta_change() {
430        let d = IndentDelta {
431            line: 2,
432            before: 0,
433            after: 4,
434        };
435        assert_eq!(d.change(), 4);
436        let d2 = IndentDelta {
437            line: 5,
438            before: 8,
439            after: 4,
440        };
441        assert_eq!(d2.change(), -4);
442    }
443    #[test]
444    fn test_indent_consistency_report_step() {
445        let src = "def foo := 1\n    body\n";
446        let report = IndentConsistencyReport::from_source(src);
447        assert!(report.step > 0 || report.total_indented > 0);
448    }
449    #[test]
450    fn test_line_class_starts_decl() {
451        assert!(LineClass::DeclOpener.starts_decl());
452        assert!(!LineClass::Other.starts_decl());
453        assert!(!LineClass::Blank.starts_decl());
454    }
455    #[test]
456    fn test_line_class_is_ignorable() {
457        assert!(LineClass::Blank.is_ignorable());
458        assert!(LineClass::Comment.is_ignorable());
459        assert!(!LineClass::DeclOpener.is_ignorable());
460    }
461}
462pub fn compare_indent(prev: &IndentLevel, next: &IndentLevel, tab_width: usize) -> IndentDiff {
463    let pw = prev.total_width(tab_width);
464    let nw = next.total_width(tab_width);
465    if nw > pw {
466        IndentDiff::Indent
467    } else if nw == pw {
468        IndentDiff::Same
469    } else {
470        IndentDiff::Dedent
471    }
472}
473#[allow(dead_code)]
474pub fn compute_indent_guides(indent_levels: &[usize], line_col: usize) -> Vec<IndentGuide> {
475    indent_levels
476        .iter()
477        .filter(|&&col| col < line_col)
478        .map(|&col| IndentGuide::new(col, false))
479        .collect()
480}
481#[allow(dead_code)]
482pub(super) fn gcd(a: usize, b: usize) -> usize {
483    if b == 0 {
484        a
485    } else {
486        gcd(b, a % b)
487    }
488}
489#[cfg(test)]
490mod extended_tests {
491    use super::*;
492    use crate::indent_tracker::*;
493    #[test]
494    fn test_layout_rule_same_block() {
495        let rule = LayoutRule::SameBlock(4);
496        assert!(rule.matches(4));
497        assert!(!rule.matches(0));
498        assert_eq!(rule.pivot(), 4);
499    }
500    #[test]
501    fn test_layout_context_indent_col_spaces() {
502        assert_eq!(LayoutContext::indent_col("    hello", 4), 4);
503    }
504    #[test]
505    fn test_layout_context_indent_col_tab() {
506        assert_eq!(LayoutContext::indent_col("\thello", 4), 4);
507    }
508    #[test]
509    fn test_compare_indent() {
510        let a = IndentLevel::new(2, 0);
511        let b = IndentLevel::new(4, 0);
512        assert_eq!(compare_indent(&a, &b, 4), IndentDiff::Indent);
513        assert_eq!(compare_indent(&b, &a, 4), IndentDiff::Dedent);
514    }
515    #[test]
516    fn test_block_parser_simple() {
517        let mut bp = BlockParser::new(4);
518        let lines = vec!["def foo : Nat := 0", "    body", "def bar := 1"];
519        let blocks = bp.parse_blocks(&lines);
520        assert!(!blocks.is_empty());
521    }
522    #[test]
523    fn test_indent_normaliser_tab_expansion() {
524        let norm = IndentNormaliser::new(4);
525        assert_eq!(norm.normalise_line("\thello"), "    hello");
526    }
527    #[test]
528    fn test_scope_tracker_resolve() {
529        let mut tracker = ScopeTracker::new();
530        tracker.enter("where", IndentLevel::new(0, 0));
531        tracker.bind("foo");
532        assert!(tracker.resolve("foo").is_some());
533        assert!(tracker.resolve("bar").is_none());
534        tracker.exit();
535    }
536    #[test]
537    fn test_line_classifier() {
538        assert_eq!(LineClass::classify(""), LineClass::Blank);
539        assert_eq!(LineClass::classify("-- comment"), LineClass::Comment);
540        assert_eq!(LineClass::classify("def foo := 1"), LineClass::DeclOpener);
541        assert_eq!(LineClass::classify("where"), LineClass::WhereKeyword);
542        assert_eq!(LineClass::classify("let x := 5"), LineClass::LetBinding);
543    }
544    #[test]
545    fn test_indent_stats_spaces_only() {
546        let src = "    x\n        y\n    z\n";
547        let stats = IndentStats::analyse(src);
548        assert!(stats.is_spaces_only());
549    }
550    #[test]
551    fn test_do_block_tracker() {
552        let mut tracker = DoBlockTracker::new(4);
553        tracker.enter(4);
554        assert!(tracker.is_statement("    x <- foo"));
555        tracker.exit();
556    }
557    #[test]
558    fn test_let_binding_tracker() {
559        let mut tracker = LetBindingTracker::new();
560        tracker.push(LetBinding::new("x", 0, true));
561        assert!(tracker.resolve("x").is_some());
562    }
563    #[test]
564    fn test_indent_guide() {
565        let levels = vec![0, 4, 8];
566        let guides = compute_indent_guides(&levels, 12);
567        assert_eq!(guides.len(), 3);
568    }
569    #[test]
570    fn test_tab_stop_iterator() {
571        let stops: Vec<usize> = TabStopIterator::new(4).take(5).collect();
572        assert_eq!(stops, vec![0, 4, 8, 12, 16]);
573    }
574    #[test]
575    fn test_indent_delta() {
576        let src = "def foo := 1\n    body\ndef bar := 2\n";
577        let deltas = IndentDelta::compute_all(src, 4);
578        assert!(!deltas.is_empty());
579    }
580    #[test]
581    fn test_hanging_indent() {
582        let hi = HangingIndent::new(0, 4);
583        assert!(hi.is_first(0));
584        assert!(hi.is_continuation(4));
585        assert_eq!(hi.overhang(), 4);
586    }
587    #[test]
588    fn test_line_span() {
589        let span = LineSpan::new(3, 7);
590        assert_eq!(span.len(), 5);
591        assert!(span.contains(5));
592        let merged = span.merge(LineSpan::new(6, 10));
593        assert_eq!(merged.end, 10);
594    }
595    #[test]
596    fn test_indent_validator_spaces() {
597        let mut v = IndentValidator::expect_spaces();
598        v.validate("    hello\n");
599        assert!(v.is_valid());
600    }
601    #[test]
602    fn test_indent_fixer_no_op() {
603        let fixer = IndentFixer::new(4, 4);
604        let src = "def foo := 1\n    body\n";
605        let fixed = fixer.fix(src);
606        assert!(fixed.contains("    body"));
607    }
608    #[test]
609    fn test_whitespace_kind() {
610        let src = "foo   bar";
611        let kind = WhitespaceKind::classify(src, 3, 6);
612        assert!(matches!(kind, WhitespaceKind::MultiSpace(3)));
613        assert!(!kind.contains_newline());
614    }
615    #[test]
616    fn test_aligned_printer() {
617        let mut printer = AlignedPrinter::new(":=");
618        printer.add(0, "foo", "1");
619        printer.add(0, "longname", "2");
620        let output = printer.format();
621        let lines: Vec<&str> = output.lines().collect();
622        let col0 = lines[0].find(":=").expect("lookup should succeed");
623        let col1 = lines[1].find(":=").expect("lookup should succeed");
624        assert_eq!(col0, col1);
625    }
626    #[test]
627    fn test_comment_tracker() {
628        let mut ct = CommentTracker::new();
629        ct.process_pair('-', '-');
630        assert!(ct.in_comment());
631        ct.end_of_line();
632        assert!(!ct.in_comment());
633    }
634    #[test]
635    fn test_open_brace_tracker() {
636        let mut t = OpenBraceTracker::new();
637        t.push('(', 1, 0);
638        assert!(t.pop(')', 1, 5).is_ok());
639        assert!(t.is_balanced());
640    }
641    #[test]
642    fn test_indent_rewriter() {
643        let rw = IndentRewriter::new(2, 4);
644        assert_eq!(rw.rewrite_line("  hello"), "    hello");
645    }
646    #[test]
647    fn test_indent_history() {
648        let src = "def foo := 1\n    body\n";
649        let hist = IndentHistory::from_source(src, 4);
650        assert_eq!(hist.max_indent(), 4);
651    }
652    #[test]
653    fn test_multiline_string_tracker() {
654        let mut t = MultilineStringTracker::new();
655        t.open(5);
656        assert!(t.in_string);
657        t.close();
658        assert!(!t.in_string);
659    }
660    #[test]
661    fn test_source_splitter() {
662        let src = "def foo := 1\ndef bar := 2\n";
663        let splitter = SourceSplitter::new(4);
664        let blocks = splitter.split(src);
665        assert_eq!(blocks.len(), 2);
666    }
667    #[test]
668    fn test_column_aligner() {
669        let items = vec!["a".to_string(), "bb".to_string(), "ccc".to_string()];
670        let aligned = ColumnAligner::align_all(&items);
671        assert!(aligned.iter().all(|s| s.len() == 3));
672    }
673    #[test]
674    fn test_gcd() {
675        assert_eq!(gcd(0, 4), 4);
676        assert_eq!(gcd(8, 4), 4);
677        assert_eq!(gcd(6, 4), 2);
678    }
679    #[test]
680    fn test_indent_consistency_report() {
681        let src = "def foo := 1\n    body\ndef bar := 2\n";
682        let report = IndentConsistencyReport::from_source(src);
683        assert!(report.uses_spaces);
684        assert!(!report.uses_tabs);
685    }
686}
687/// Computes the visual width of a string (tabs expand to tab_width).
688#[allow(dead_code)]
689pub fn visual_width(s: &str, tab_width: usize) -> usize {
690    let mut col = 0;
691    for ch in s.chars() {
692        if ch == '\t' {
693            col = ((col / tab_width) + 1) * tab_width;
694        } else {
695            col += 1;
696        }
697    }
698    col
699}
700/// Converts a string with tabs to spaces.
701#[allow(dead_code)]
702pub fn expand_tabs(s: &str, tab_width: usize) -> String {
703    let mut out = String::new();
704    let mut col = 0;
705    for ch in s.chars() {
706        if ch == '\t' {
707            let next = ((col / tab_width) + 1) * tab_width;
708            for _ in col..next {
709                out.push(' ');
710            }
711            col = next;
712        } else {
713            out.push(ch);
714            col += 1;
715        }
716    }
717    out
718}
719/// Converts spaces to tabs where possible (unexpand).
720#[allow(dead_code)]
721pub fn compress_spaces_to_tabs(s: &str, tab_width: usize) -> String {
722    let leading_spaces = s.len() - s.trim_start_matches(' ').len();
723    let tabs = leading_spaces / tab_width;
724    let remaining = leading_spaces % tab_width;
725    let mut out = "\t".repeat(tabs);
726    for _ in 0..remaining {
727        out.push(' ');
728    }
729    out.push_str(s.trim_start_matches(' '));
730    out
731}
732/// Checks if a source string has consistent indentation (all-spaces or all-tabs).
733#[allow(dead_code)]
734pub fn has_mixed_indentation(source: &str) -> bool {
735    let has_tabs = source.lines().any(|l| l.starts_with('\t'));
736    let has_spaces = source.lines().any(|l| l.starts_with(' '));
737    has_tabs && has_spaces
738}
739/// Computes a "signature" of the indentation pattern of a source string.
740#[allow(dead_code)]
741pub fn indent_signature(source: &str) -> Vec<usize> {
742    source
743        .lines()
744        .map(|l| l.len() - l.trim_start().len())
745        .collect()
746}
747/// Checks if two sources have the same indentation structure.
748#[allow(dead_code)]
749pub fn same_indent_structure(a: &str, b: &str) -> bool {
750    indent_signature(a) == indent_signature(b)
751}
752/// Normalises indentation to use exactly `unit` spaces per level.
753#[allow(dead_code)]
754pub fn normalise_indentation(source: &str, unit: usize) -> String {
755    if unit == 0 {
756        return source.to_string();
757    }
758    // Detect the source indent unit as the smallest non-zero leading whitespace.
759    let source_unit = source
760        .lines()
761        .map(|line| line.len() - line.trim_start().len())
762        .filter(|&n| n > 0)
763        .min()
764        .unwrap_or(unit);
765    source
766        .lines()
767        .map(|line| {
768            let leading = line.len() - line.trim_start().len();
769            let level = leading / source_unit.max(1);
770            format!("{}{}", " ".repeat(level * unit), line.trim_start())
771        })
772        .collect::<Vec<_>>()
773        .join("\n")
774}
775/// Splits source into indent blocks.
776#[allow(dead_code)]
777pub fn split_into_indent_blocks(source: &str) -> Vec<IndentBlock> {
778    let mut blocks: Vec<IndentBlock> = Vec::new();
779    for line in source.lines() {
780        let level = line.len() - line.trim_start().len();
781        if let Some(last) = blocks.last_mut() {
782            if last.level == level {
783                last.add_line(line);
784                continue;
785            }
786        }
787        let mut block = IndentBlock::new(level);
788        block.add_line(line);
789        blocks.push(block);
790    }
791    blocks
792}
793/// Computes the longest common indent prefix of a set of lines.
794#[allow(dead_code)]
795pub fn common_indent(lines: &[&str]) -> usize {
796    lines
797        .iter()
798        .filter(|l| !l.trim().is_empty())
799        .map(|l| l.len() - l.trim_start().len())
800        .min()
801        .unwrap_or(0)
802}
803/// Dedents all lines in a source by `n` spaces.
804#[allow(dead_code)]
805pub fn dedent(source: &str, n: usize) -> String {
806    source
807        .lines()
808        .map(|l| {
809            if l.len() >= n && l[..n].chars().all(|c| c == ' ') {
810                &l[n..]
811            } else {
812                l.trim_start()
813            }
814        })
815        .collect::<Vec<_>>()
816        .join("\n")
817}
818/// Applies a function to the indentation of each line.
819#[allow(dead_code)]
820pub fn map_indent<F: Fn(usize) -> usize>(source: &str, f: F) -> String {
821    source
822        .lines()
823        .map(|l| {
824            let indent = l.len() - l.trim_start().len();
825            let new_indent = f(indent);
826            format!("{}{}", " ".repeat(new_indent), l.trim_start())
827        })
828        .collect::<Vec<_>>()
829        .join("\n")
830}
831/// Detects the dominant indentation mode of a source.
832#[allow(dead_code)]
833pub fn detect_indent_mode(source: &str) -> IndentMode {
834    let tab_lines = source.lines().filter(|l| l.starts_with('\t')).count();
835    let space_lines = source.lines().filter(|l| l.starts_with("  ")).count();
836    if tab_lines > space_lines {
837        IndentMode::Tabs(4)
838    } else {
839        IndentMode::Spaces(2)
840    }
841}
842/// Converts a source from one indent mode to another.
843#[allow(dead_code)]
844pub fn convert_indent_mode(source: &str, from: IndentMode, to: IndentMode) -> String {
845    let unit = from.unit_width();
846    source
847        .lines()
848        .map(|l| {
849            let stripped = l.trim_start();
850            let leading = l.len() - stripped.len();
851            let level = if unit == 0 {
852                0
853            } else {
854                match from {
855                    IndentMode::Tabs(_) => l.chars().take_while(|&c| c == '\t').count(),
856                    IndentMode::Spaces(u) => leading / u,
857                }
858            };
859            format!("{}{}", to.indent_str(level), stripped)
860        })
861        .collect::<Vec<_>>()
862        .join("\n")
863}
864#[cfg(test)]
865mod extended_indent_final_tests {
866    use super::*;
867    use crate::indent_tracker::*;
868    #[test]
869    fn test_virtual_column() {
870        let col = VirtualColumn::new(0, 4);
871        let col2 = col.advance_by(3);
872        assert_eq!(col2.column, 3);
873        let col3 = col2.advance_tab();
874        assert_eq!(col3.column, 4);
875        assert!(col3.is_aligned_to(4));
876    }
877    #[test]
878    fn test_visual_width() {
879        assert_eq!(visual_width("hello", 4), 5);
880        assert_eq!(visual_width("\thello", 4), 9);
881    }
882    #[test]
883    fn test_expand_tabs() {
884        let s = expand_tabs("\thello", 4);
885        assert_eq!(s, "    hello");
886    }
887    #[test]
888    fn test_has_mixed_indentation() {
889        assert!(has_mixed_indentation("  space\n\ttab"));
890        assert!(!has_mixed_indentation("  a\n  b"));
891        assert!(!has_mixed_indentation("\ta\n\tb"));
892    }
893    #[test]
894    fn test_column_oracle() {
895        let mut oracle = ColumnOracle::new(4);
896        oracle.add_reference(4);
897        oracle.add_reference(8);
898        assert_eq!(oracle.next_alignment(0), 4);
899        assert_eq!(oracle.next_alignment(5), 8);
900        assert!(oracle.is_at_reference(4));
901        assert!(!oracle.is_at_reference(3));
902    }
903    #[test]
904    fn test_construct_rule_registry() {
905        let reg = ConstructRuleRegistry::new();
906        assert!(reg.lookup("def").is_some());
907        assert_eq!(reg.body_indent("def"), 2);
908        assert_eq!(reg.body_indent("unknown"), 2);
909    }
910    #[test]
911    fn test_indent_signature() {
912        let sig = indent_signature("a\n  b\n    c");
913        assert_eq!(sig, vec![0, 2, 4]);
914    }
915    #[test]
916    fn test_same_indent_structure() {
917        assert!(same_indent_structure("a\n  b", "x\n  y"));
918        assert!(!same_indent_structure("a\n  b", "a\n    b"));
919    }
920    #[test]
921    fn test_normalise_indentation() {
922        let s = normalise_indentation("a\n    b\n        c", 2);
923        assert_eq!(s, "a\n  b\n    c");
924    }
925    #[test]
926    fn test_split_into_indent_blocks() {
927        let src = "a\n  b\n  c\nd";
928        let blocks = split_into_indent_blocks(src);
929        assert!(blocks.len() >= 2);
930    }
931    #[test]
932    fn test_indent_fence() {
933        let mut fence = IndentFence::new(2);
934        assert!(fence.allows(2));
935        assert!(fence.allows(4));
936        assert!(!fence.allows(0));
937        fence.deactivate();
938        assert!(fence.allows(0));
939    }
940    #[test]
941    fn test_common_indent() {
942        let lines = ["  a", "    b", "  c"];
943        assert_eq!(common_indent(&lines), 2);
944        let empty: Vec<&str> = vec![];
945        assert_eq!(common_indent(&empty), 0);
946    }
947    #[test]
948    fn test_dedent() {
949        let s = dedent("  hello\n  world", 2);
950        assert_eq!(s, "hello\nworld");
951    }
952    #[test]
953    fn test_map_indent() {
954        let s = map_indent("a\n  b\n    c", |n| n + 2);
955        assert_eq!(s, "  a\n    b\n      c");
956    }
957    #[test]
958    fn test_indent_mode() {
959        let m = IndentMode::Spaces(2);
960        assert_eq!(m.unit_width(), 2);
961        assert_eq!(m.indent_str(3), "      ");
962        let t = IndentMode::Tabs(4);
963        assert_eq!(t.indent_str(2), "\t\t");
964    }
965    #[test]
966    fn test_detect_indent_mode() {
967        let with_tabs = "\thello\n\tworld";
968        let m = detect_indent_mode(with_tabs);
969        assert_eq!(m, IndentMode::Tabs(4));
970        let with_spaces = "  hello\n  world";
971        let m2 = detect_indent_mode(with_spaces);
972        assert_eq!(m2, IndentMode::Spaces(2));
973    }
974    #[test]
975    fn test_convert_indent_mode() {
976        let src = "a\n  b\n    c";
977        let converted = convert_indent_mode(src, IndentMode::Spaces(2), IndentMode::Spaces(4));
978        assert_eq!(converted, "a\n    b\n        c");
979    }
980    #[test]
981    fn test_compress_spaces_to_tabs() {
982        let s = compress_spaces_to_tabs("    hello", 4);
983        assert!(s.starts_with('\t'));
984        assert!(s.contains("hello"));
985    }
986}
987/// Scans a source and generates an indent change log.
988#[allow(dead_code)]
989pub fn compute_indent_changes(source: &str) -> IndentChangeLog {
990    let mut log = IndentChangeLog::new();
991    let mut prev = 0usize;
992    for (i, line) in source.lines().enumerate() {
993        if line.trim().is_empty() {
994            continue;
995        }
996        let cur = line.len() - line.trim_start().len();
997        log.record(i, prev, cur);
998        prev = cur;
999    }
1000    log
1001}
1002/// An indent-level mapper: maps each line to its logical block depth.
1003#[allow(dead_code)]
1004pub fn map_to_block_depths(source: &str, unit: usize) -> Vec<usize> {
1005    source
1006        .lines()
1007        .map(|l| {
1008            if unit == 0 {
1009                return 0;
1010            }
1011            let indent = l.len() - l.trim_start().len();
1012            indent / unit
1013        })
1014        .collect()
1015}
1016/// Checks whether a line is a continuation of the previous (hanging indent).
1017#[allow(dead_code)]
1018pub fn is_continuation_line(prev_indent: usize, cur_indent: usize, unit: usize) -> bool {
1019    unit > 0 && cur_indent > prev_indent && (cur_indent - prev_indent) % unit != 0
1020}
1021/// Produces a visual representation of the indent structure.
1022#[allow(dead_code)]
1023pub fn visualise_indent_structure(source: &str) -> String {
1024    source
1025        .lines()
1026        .enumerate()
1027        .map(|(i, l)| {
1028            let indent = l.len() - l.trim_start().len();
1029            let bar = "|".repeat(indent / 2);
1030            format!("{:4} {}{}", i + 1, bar, l.trim_start())
1031        })
1032        .collect::<Vec<_>>()
1033        .join("\n")
1034}
1035/// A simple depth-first indent traversal.
1036#[allow(dead_code)]
1037pub fn traverse_indent_tree(source: &str, unit: usize) -> Vec<(usize, usize, String)> {
1038    source
1039        .lines()
1040        .enumerate()
1041        .filter(|(_, l)| !l.trim().is_empty())
1042        .map(|(i, l)| {
1043            let indent = l.len() - l.trim_start().len();
1044            let depth = if unit == 0 { 0 } else { indent / unit };
1045            (depth, i, l.trim().to_string())
1046        })
1047        .collect()
1048}
1049/// Checks if a source is "well-formed" in terms of indentation.
1050/// Well-formed: every indent increase is exactly `unit` spaces.
1051#[allow(dead_code)]
1052pub fn is_well_formed_indentation(source: &str, unit: usize) -> bool {
1053    if unit == 0 {
1054        return true;
1055    }
1056    let mut prev = 0usize;
1057    for line in source.lines() {
1058        if line.trim().is_empty() {
1059            continue;
1060        }
1061        let cur = line.len() - line.trim_start().len();
1062        if cur > prev && (cur - prev) % unit != 0 {
1063            return false;
1064        }
1065        if cur % unit != 0 {
1066            return false;
1067        }
1068        prev = cur;
1069    }
1070    true
1071}
1072/// Repair indentation: round all indents to the nearest multiple of `unit`.
1073#[allow(dead_code)]
1074pub fn repair_indentation(source: &str, unit: usize) -> String {
1075    if unit == 0 {
1076        return source.to_string();
1077    }
1078    source
1079        .lines()
1080        .map(|l| {
1081            let indent = l.len() - l.trim_start().len();
1082            let rounded = ((indent + unit / 2) / unit) * unit;
1083            format!("{}{}", " ".repeat(rounded), l.trim_start())
1084        })
1085        .collect::<Vec<_>>()
1086        .join("\n")
1087}
1088#[cfg(test)]
1089mod extended_indent_extra_tests {
1090    use super::*;
1091    use crate::indent_tracker::*;
1092    #[test]
1093    fn test_indent_change_log() {
1094        let src = "a\n  b\n    c\n  d\ne";
1095        let log = compute_indent_changes(src);
1096        assert!(log.increases() >= 2);
1097        assert!(log.decreases() >= 1);
1098    }
1099    #[test]
1100    fn test_map_to_block_depths() {
1101        let depths = map_to_block_depths("a\n  b\n    c", 2);
1102        assert_eq!(depths, vec![0, 1, 2]);
1103    }
1104    #[test]
1105    fn test_hanging_indent_state() {
1106        let mut state = HangingIndentState::new(0);
1107        assert_eq!(state.current_indent(), 0);
1108        state.enter_hanging();
1109        assert_eq!(state.current_indent(), 4);
1110        state.exit_hanging();
1111        assert_eq!(state.current_indent(), 0);
1112    }
1113    #[test]
1114    fn test_is_continuation_line() {
1115        assert!(is_continuation_line(0, 6, 4));
1116        assert!(!is_continuation_line(0, 4, 4));
1117    }
1118    #[test]
1119    fn test_visualise_indent_structure() {
1120        let src = "a\n  b\n    c";
1121        let vis = visualise_indent_structure(src);
1122        assert!(vis.contains("a"));
1123        assert!(vis.contains("|"));
1124    }
1125    #[test]
1126    fn test_traverse_indent_tree() {
1127        let src = "a\n  b\n    c\n  d";
1128        let tree = traverse_indent_tree(src, 2);
1129        assert_eq!(tree[0].0, 0);
1130        assert_eq!(tree[1].0, 1);
1131        assert_eq!(tree[2].0, 2);
1132    }
1133    #[test]
1134    fn test_is_well_formed_indentation() {
1135        assert!(is_well_formed_indentation("a\n  b\n    c", 2));
1136        assert!(!is_well_formed_indentation("a\n   b", 2));
1137    }
1138    #[test]
1139    fn test_repair_indentation() {
1140        let repaired = repair_indentation("a\n   b\n     c", 2);
1141        assert!(is_well_formed_indentation(&repaired, 2));
1142    }
1143    #[test]
1144    fn test_indent_zipper() {
1145        let src = "a\n  b\n    c";
1146        let mut z = IndentZipper::from_source(src).expect("test operation should succeed");
1147        assert_eq!(z.current_indent(), 0);
1148        assert!(z.move_down());
1149        assert_eq!(z.current_indent(), 2);
1150        assert!(z.move_down());
1151        assert_eq!(z.current_indent(), 4);
1152        assert!(!z.move_down());
1153        assert!(z.move_up());
1154        assert_eq!(z.current_indent(), 2);
1155    }
1156}