1use 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#[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#[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#[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#[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#[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#[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#[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#[allow(dead_code)]
749pub fn same_indent_structure(a: &str, b: &str) -> bool {
750 indent_signature(a) == indent_signature(b)
751}
752#[allow(dead_code)]
754pub fn normalise_indentation(source: &str, unit: usize) -> String {
755 if unit == 0 {
756 return source.to_string();
757 }
758 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}