1use super::functions::*;
6
7#[derive(Debug, Clone, Default)]
8pub struct IndentValidator {
9 pub expect_spaces: bool,
10 pub violations: Vec<(usize, char)>,
11}
12impl IndentValidator {
13 pub fn expect_spaces() -> Self {
14 Self {
15 expect_spaces: true,
16 violations: Vec::new(),
17 }
18 }
19 pub fn expect_tabs() -> Self {
20 Self {
21 expect_spaces: false,
22 violations: Vec::new(),
23 }
24 }
25 pub fn validate(&mut self, src: &str) {
26 for (i, line) in src.lines().enumerate() {
27 for ch in line.chars().take_while(|c| c.is_whitespace()) {
28 let violation = if self.expect_spaces {
29 ch == '\t'
30 } else {
31 ch == ' '
32 };
33 if violation {
34 self.violations.push((i + 1, ch));
35 break;
36 }
37 }
38 }
39 }
40 pub fn is_valid(&self) -> bool {
41 self.violations.is_empty()
42 }
43}
44#[derive(Debug, Clone)]
46pub struct IndentRegion {
47 pub start_line: usize,
49 pub end_line: usize,
51 pub indent_width: usize,
53}
54impl IndentRegion {
55 pub fn new(start_line: usize, end_line: usize, indent_width: usize) -> Self {
57 Self {
58 start_line,
59 end_line,
60 indent_width,
61 }
62 }
63 pub fn line_count(&self) -> usize {
65 self.end_line.saturating_sub(self.start_line) + 1
66 }
67 pub fn contains_line(&self, line: usize) -> bool {
69 line >= self.start_line && line <= self.end_line
70 }
71}
72#[derive(Debug, Default)]
73pub struct SourceSplitter {
74 pub tab_width: usize,
75}
76impl SourceSplitter {
77 pub fn new(tab_width: usize) -> Self {
78 Self { tab_width }
79 }
80 pub fn split<'a>(&self, src: &'a str) -> Vec<(usize, &'a str)> {
81 let lines: Vec<&str> = src.lines().collect();
82 let mut result = Vec::new();
83 let mut block_start: Option<usize> = None;
84 let mut block_start_byte: usize = 0;
85 let mut byte_offset = 0usize;
86 for (i, &line) in lines.iter().enumerate() {
87 let class = LineClass::classify(line);
88 if class.starts_decl() {
89 if let Some(start_line) = block_start {
90 result.push((start_line, &src[block_start_byte..byte_offset]));
91 }
92 block_start = Some(i);
93 block_start_byte = byte_offset;
94 }
95 byte_offset += line.len();
96 if i + 1 < lines.len() {
97 byte_offset += 1;
98 }
99 }
100 if let Some(start_line) = block_start {
101 result.push((start_line, &src[block_start_byte..]));
102 }
103 result
104 }
105}
106#[derive(Debug, Clone)]
107pub struct DoBlockTracker {
108 block_columns: Vec<usize>,
109 pub tab_width: usize,
110}
111impl DoBlockTracker {
112 pub fn new(tab_width: usize) -> Self {
113 Self {
114 block_columns: Vec::new(),
115 tab_width,
116 }
117 }
118 pub fn enter(&mut self, col: usize) {
119 self.block_columns.push(col);
120 }
121 pub fn exit(&mut self) -> Option<usize> {
122 self.block_columns.pop()
123 }
124 pub fn current_col(&self) -> Option<usize> {
125 self.block_columns.last().copied()
126 }
127 pub fn is_statement(&self, line: &str) -> bool {
128 let col = LayoutContext::indent_col(line, self.tab_width);
129 self.current_col() == Some(col)
130 }
131 pub fn is_deeper(&self, line: &str) -> bool {
132 let col = LayoutContext::indent_col(line, self.tab_width);
133 self.current_col().is_some_and(|e| col > e)
134 }
135 pub fn is_closer(&self, line: &str) -> bool {
136 let col = LayoutContext::indent_col(line, self.tab_width);
137 self.current_col().is_some_and(|e| col < e)
138 }
139 pub fn depth(&self) -> usize {
140 self.block_columns.len()
141 }
142}
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum LayoutRule {
146 SameBlock(usize),
148 Continuation(usize),
150 CloseBlock(usize),
152}
153impl LayoutRule {
154 pub fn matches(&self, col: usize) -> bool {
156 match self {
157 LayoutRule::SameBlock(c) => col == *c,
158 LayoutRule::Continuation(c) => col > *c,
159 LayoutRule::CloseBlock(c) => col < *c,
160 }
161 }
162 pub fn pivot(&self) -> usize {
164 match self {
165 LayoutRule::SameBlock(c) | LayoutRule::Continuation(c) | LayoutRule::CloseBlock(c) => {
166 *c
167 }
168 }
169 }
170}
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct IndentMismatchError {
173 pub line_number: usize,
174 pub expected: usize,
175 pub actual: usize,
176 pub context: String,
177}
178impl IndentMismatchError {
179 pub fn new(
180 line_number: usize,
181 expected: usize,
182 actual: usize,
183 context: impl Into<String>,
184 ) -> Self {
185 Self {
186 line_number,
187 expected,
188 actual,
189 context: context.into(),
190 }
191 }
192 pub fn message(&self) -> String {
193 format!(
194 "indentation error at line {}: expected {} spaces, found {} ({})",
195 self.line_number, self.expected, self.actual, self.context
196 )
197 }
198}
199#[derive(Debug, Clone, Default)]
201pub struct IndentLevelHistory {
202 past: Vec<Vec<IndentLevel>>,
203 present: Vec<IndentLevel>,
204 future: Vec<Vec<IndentLevel>>,
205}
206impl IndentLevelHistory {
207 pub fn new() -> Self {
209 Self::default()
210 }
211 pub fn snapshot(&mut self) {
213 self.past.push(self.present.clone());
214 self.future.clear();
215 }
216 pub fn set_levels(&mut self, levels: Vec<IndentLevel>) {
218 self.present = levels;
219 }
220 pub fn current(&self) -> &[IndentLevel] {
222 &self.present
223 }
224 pub fn undo(&mut self) -> bool {
226 if let Some(prev) = self.past.pop() {
227 self.future.push(self.present.clone());
228 self.present = prev;
229 true
230 } else {
231 false
232 }
233 }
234 pub fn redo(&mut self) -> bool {
236 if let Some(next) = self.future.pop() {
237 self.past.push(self.present.clone());
238 self.present = next;
239 true
240 } else {
241 false
242 }
243 }
244 pub fn undo_depth(&self) -> usize {
246 self.past.len()
247 }
248 pub fn redo_depth(&self) -> usize {
250 self.future.len()
251 }
252}
253#[derive(Debug, Clone)]
254pub struct Scope {
255 pub kind: String,
256 pub base_indent: IndentLevel,
257 pub bindings: Vec<String>,
258}
259impl Scope {
260 pub fn new(kind: impl Into<String>, base_indent: IndentLevel) -> Self {
261 Self {
262 kind: kind.into(),
263 base_indent,
264 bindings: Vec::new(),
265 }
266 }
267 pub fn add_binding(&mut self, name: impl Into<String>) {
268 self.bindings.push(name.into());
269 }
270 pub fn is_bound(&self, name: &str) -> bool {
271 self.bindings.iter().any(|b| b == name)
272 }
273}
274#[derive(Debug, Clone)]
276pub struct LayoutContext {
277 rules: Vec<LayoutRule>,
278 pub tab_width: usize,
279}
280impl LayoutContext {
281 pub fn new(tab_width: usize) -> Self {
282 Self {
283 rules: Vec::new(),
284 tab_width,
285 }
286 }
287 pub fn push_rule(&mut self, rule: LayoutRule) {
288 self.rules.push(rule);
289 }
290 pub fn pop_rule(&mut self) -> Option<LayoutRule> {
291 self.rules.pop()
292 }
293 pub fn current_rule(&self) -> Option<&LayoutRule> {
294 self.rules.last()
295 }
296 pub fn indent_col(line: &str, tab_width: usize) -> usize {
297 let mut col = 0usize;
298 for ch in line.chars() {
299 match ch {
300 ' ' => col += 1,
301 '\t' => {
302 col = (col / tab_width + 1) * tab_width;
303 }
304 _ => break,
305 }
306 }
307 col
308 }
309 pub fn line_continues_block(&self, line: &str) -> bool {
310 let col = Self::indent_col(line, self.tab_width);
311 match self.current_rule() {
312 Some(rule) => rule.matches(col),
313 None => true,
314 }
315 }
316 pub fn depth(&self) -> usize {
317 self.rules.len()
318 }
319 pub fn clear(&mut self) {
320 self.rules.clear();
321 }
322}
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub enum IndentDiff {
325 Indent,
326 Same,
327 Dedent,
328}
329pub struct IndentStack {
331 pub stack: Vec<IndentLevel>,
332 pub tab_width: usize,
333}
334impl IndentStack {
335 pub fn new(tab_width: usize) -> Self {
336 Self {
337 stack: Vec::new(),
338 tab_width,
339 }
340 }
341 pub fn push(&mut self, level: IndentLevel) {
342 self.stack.push(level);
343 }
344 pub fn pop(&mut self) -> Option<IndentLevel> {
345 self.stack.pop()
346 }
347 pub fn current(&self) -> Option<&IndentLevel> {
349 self.stack.last()
350 }
351 pub fn dedent_to(&mut self, level: &IndentLevel) -> usize {
354 let tw = self.tab_width;
355 let mut count = 0;
356 while let Some(top) = self.stack.last() {
357 if top.total_width(tw) > level.total_width(tw) {
358 self.stack.pop();
359 count += 1;
360 } else {
361 break;
362 }
363 }
364 count
365 }
366}
367#[derive(Debug, Clone, PartialEq, Eq)]
369pub struct IndentLevel {
370 pub spaces: usize,
371 pub tabs: usize,
372}
373impl IndentLevel {
374 pub fn new(spaces: usize, tabs: usize) -> Self {
375 Self { spaces, tabs }
376 }
377 pub fn total_width(&self, tab_width: usize) -> usize {
379 self.tabs * tab_width + self.spaces
380 }
381 pub fn is_deeper_than(&self, other: &IndentLevel, tab_width: usize) -> bool {
383 self.total_width(tab_width) > other.total_width(tab_width)
384 }
385}
386#[allow(dead_code)]
388#[allow(missing_docs)]
389pub struct IndentFence {
390 pub min_indent: usize,
391 pub active: bool,
392}
393impl IndentFence {
394 #[allow(dead_code)]
395 #[allow(missing_docs)]
396 pub fn new(min_indent: usize) -> Self {
397 Self {
398 min_indent,
399 active: true,
400 }
401 }
402 #[allow(dead_code)]
403 #[allow(missing_docs)]
404 pub fn deactivate(&mut self) {
405 self.active = false;
406 }
407 #[allow(dead_code)]
408 #[allow(missing_docs)]
409 pub fn allows(&self, indent: usize) -> bool {
410 !self.active || indent >= self.min_indent
411 }
412}
413#[derive(Debug, Clone)]
414pub struct IndentRewriter {
415 pub from_step: usize,
416 pub to_step: usize,
417}
418impl IndentRewriter {
419 pub fn new(from_step: usize, to_step: usize) -> Self {
420 Self { from_step, to_step }
421 }
422 pub fn rewrite_line(&self, line: &str) -> String {
423 let spaces = line.chars().take_while(|&c| c == ' ').count();
424 if spaces == 0 || self.from_step == 0 {
425 return line.to_string();
426 }
427 let levels = spaces / self.from_step;
428 let remainder = spaces % self.from_step;
429 " ".repeat(levels * self.to_step + remainder) + &line[spaces..]
430 }
431 pub fn rewrite(&self, src: &str) -> String {
432 src.lines()
433 .map(|l| self.rewrite_line(l))
434 .collect::<Vec<_>>()
435 .join("\n")
436 }
437}
438#[derive(Debug, Clone, Default)]
439pub struct MultilineStringTracker {
440 pub in_string: bool,
441 pub open_col: usize,
442 pub line_count: usize,
443}
444impl MultilineStringTracker {
445 pub fn new() -> Self {
446 Self::default()
447 }
448 pub fn open(&mut self, col: usize) {
449 self.in_string = true;
450 self.open_col = col;
451 self.line_count = 0;
452 }
453 pub fn close(&mut self) {
454 self.in_string = false;
455 self.open_col = 0;
456 self.line_count = 0;
457 }
458 pub fn next_line(&mut self) {
459 if self.in_string {
460 self.line_count += 1;
461 }
462 }
463 pub fn has_unescaped_quote(line: &str) -> bool {
464 let mut escaped = false;
465 for ch in line.chars() {
466 if escaped {
467 escaped = false;
468 } else if ch == '\\' {
469 escaped = true;
470 } else if ch == '"' {
471 return true;
472 }
473 }
474 false
475 }
476}
477#[derive(Debug, Clone, Default)]
478pub struct IndentConsistencyReport {
479 pub step: usize,
480 pub uses_tabs: bool,
481 pub uses_spaces: bool,
482 pub is_mixed: bool,
483 pub total_indented: usize,
484}
485impl IndentConsistencyReport {
486 pub fn from_source(src: &str) -> Self {
487 let stats = IndentStats::analyse(src);
488 let deltas = IndentDelta::compute_all(src, 4);
489 let steps: Vec<usize> = deltas
490 .iter()
491 .filter(|d| d.is_increase())
492 .map(|d| d.after - d.before)
493 .collect();
494 let step = steps.iter().copied().fold(0usize, gcd);
495 IndentConsistencyReport {
496 step,
497 uses_tabs: stats.tab_lines > 0 || stats.mixed_lines > 0,
498 uses_spaces: stats.space_lines > 0 || stats.mixed_lines > 0,
499 is_mixed: stats.mixed_lines > 0,
500 total_indented: stats.code_lines(),
501 }
502 }
503}
504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
505pub enum WhitespaceKind {
506 None,
507 Space,
508 MultiSpace(usize),
509 Newline,
510 MultiNewline(usize),
511 Mixed,
512}
513impl WhitespaceKind {
514 pub fn classify(src: &str, start: usize, end: usize) -> WhitespaceKind {
515 let slice = &src[start..end];
516 if slice.is_empty() {
517 return WhitespaceKind::None;
518 }
519 let newline_count = slice.chars().filter(|&c| c == '\n').count();
520 let space_count = slice.chars().filter(|&c| c == ' ').count();
521 if newline_count == 0 {
522 if space_count == 1 {
523 WhitespaceKind::Space
524 } else {
525 WhitespaceKind::MultiSpace(space_count)
526 }
527 } else if space_count == 0 {
528 if newline_count == 1 {
529 WhitespaceKind::Newline
530 } else {
531 WhitespaceKind::MultiNewline(newline_count)
532 }
533 } else {
534 WhitespaceKind::Mixed
535 }
536 }
537 pub fn contains_newline(self) -> bool {
538 matches!(
539 self,
540 WhitespaceKind::Newline | WhitespaceKind::MultiNewline(_) | WhitespaceKind::Mixed
541 )
542 }
543}
544#[derive(Debug)]
545pub struct BlockParser {
546 pub tab_width: usize,
547 block_stack: Vec<usize>,
548}
549impl BlockParser {
550 pub fn new(tab_width: usize) -> Self {
551 Self {
552 tab_width,
553 block_stack: Vec::new(),
554 }
555 }
556 pub fn parse_blocks<'a>(&mut self, lines: &[&'a str]) -> Vec<Vec<&'a str>> {
557 let mut result: Vec<Vec<&'a str>> = Vec::new();
558 let mut current: Vec<&'a str> = Vec::new();
559 for &line in lines {
560 if line.trim().is_empty() {
561 current.push(line);
562 continue;
563 }
564 let col = LayoutContext::indent_col(line, self.tab_width);
565 match self.block_stack.last().copied() {
566 None => {
567 self.block_stack.push(col);
568 current.push(line);
569 }
570 Some(base) => {
571 if col >= base {
572 current.push(line);
573 } else {
574 if !current.is_empty() {
575 result.push(current.clone());
576 current.clear();
577 }
578 self.block_stack.pop();
579 self.block_stack.push(col);
580 current.push(line);
581 }
582 }
583 }
584 }
585 if !current.is_empty() {
586 result.push(current);
587 }
588 result
589 }
590 pub fn reset(&mut self) {
591 self.block_stack.clear();
592 }
593 pub fn depth(&self) -> usize {
594 self.block_stack.len()
595 }
596}
597#[derive(Debug, Clone, Default)]
598pub struct LetBindingTracker {
599 bindings: Vec<LetBinding>,
600}
601impl LetBindingTracker {
602 pub fn new() -> Self {
603 Self {
604 bindings: Vec::new(),
605 }
606 }
607 pub fn push(&mut self, binding: LetBinding) {
608 self.bindings.push(binding);
609 }
610 pub fn pop(&mut self) -> Option<LetBinding> {
611 self.bindings.pop()
612 }
613 pub fn resolve(&self, name: &str) -> Option<&LetBinding> {
614 self.bindings.iter().rev().find(|b| b.name == name)
615 }
616 pub fn all(&self) -> &[LetBinding] {
617 &self.bindings
618 }
619 pub fn clear(&mut self) {
620 self.bindings.clear();
621 }
622 pub fn len(&self) -> usize {
623 self.bindings.len()
624 }
625 pub fn is_empty(&self) -> bool {
626 self.bindings.is_empty()
627 }
628}
629#[derive(Debug, Clone, Default)]
630pub struct OpenBraceTracker {
631 stack: Vec<(char, usize, usize)>,
632}
633impl OpenBraceTracker {
634 pub fn new() -> Self {
635 Self::default()
636 }
637 pub fn push(&mut self, ch: char, line: usize, col: usize) {
638 self.stack.push((ch, line, col));
639 }
640 pub fn pop(
641 &mut self,
642 ch: char,
643 line: usize,
644 col: usize,
645 ) -> Result<(), (char, char, usize, usize)> {
646 let expected = Self::matching_open(ch);
647 match self.stack.last() {
648 Some(&(top, _, _)) if top == expected => {
649 self.stack.pop();
650 Ok(())
651 }
652 Some(&(top, open_line, open_col)) => Err((top, ch, open_line, open_col)),
653 None => Err(('?', ch, line, col)),
654 }
655 }
656 pub fn matching_open(close: char) -> char {
657 match close {
658 ')' => '(',
659 ']' => '[',
660 '}' => '{',
661 _ => '\0',
662 }
663 }
664 pub fn is_balanced(&self) -> bool {
665 self.stack.is_empty()
666 }
667 pub fn unmatched(&self) -> &[(char, usize, usize)] {
668 &self.stack
669 }
670 pub fn clear(&mut self) {
671 self.stack.clear();
672 }
673}
674pub struct WhereBlockTracker {
676 pub indent_stack: IndentStack,
677 pub in_where_block: bool,
678 pub where_items: Vec<(String, usize)>,
680}
681impl WhereBlockTracker {
682 pub fn new() -> Self {
683 Self {
684 indent_stack: IndentStack::new(4),
685 in_where_block: false,
686 where_items: Vec::new(),
687 }
688 }
689 pub fn enter_where(&mut self, base_indent: IndentLevel) {
691 self.in_where_block = true;
692 self.indent_stack.push(base_indent);
693 }
694 pub fn exit_where(&mut self) {
696 self.in_where_block = false;
697 self.indent_stack.pop();
698 self.where_items.clear();
699 }
700 pub fn add_where_item(&mut self, name: &str, indent: IndentLevel) {
702 let tw = self.indent_stack.tab_width;
703 self.where_items
704 .push((name.to_string(), indent.total_width(tw)));
705 }
706 pub fn is_where_item(&self, _line: &str, indent: IndentLevel) -> bool {
708 if !self.in_where_block {
709 return false;
710 }
711 let tw = self.indent_stack.tab_width;
712 match self.indent_stack.current() {
713 Some(base) => indent.is_deeper_than(base, tw),
714 None => false,
715 }
716 }
717 pub fn parse_leading_whitespace(line: &str) -> IndentLevel {
719 let mut spaces = 0usize;
720 let mut tabs = 0usize;
721 for ch in line.chars() {
722 match ch {
723 ' ' => spaces += 1,
724 '\t' => tabs += 1,
725 _ => break,
726 }
727 }
728 IndentLevel::new(spaces, tabs)
729 }
730}
731#[derive(Debug, Clone, PartialEq, Eq)]
734pub enum TokenSequenceClass {
735 DefWithWhere,
737 PlainDef,
739 LetBlock,
741 DoBlock,
743 Unknown,
745}
746#[allow(dead_code)]
748#[allow(missing_docs)]
749#[derive(Clone, Copy, Debug, PartialEq, Eq)]
750pub enum IndentMode {
751 Spaces(usize),
752 Tabs(usize),
753}
754impl IndentMode {
755 #[allow(dead_code)]
756 #[allow(missing_docs)]
757 pub fn unit_width(&self) -> usize {
758 match self {
759 Self::Spaces(n) | Self::Tabs(n) => *n,
760 }
761 }
762 #[allow(dead_code)]
763 #[allow(missing_docs)]
764 pub fn indent_str(&self, level: usize) -> String {
765 match self {
766 Self::Spaces(n) => " ".repeat(n * level),
767 Self::Tabs(_) => "\t".repeat(level),
768 }
769 }
770}
771#[derive(Debug, Clone, Default)]
772pub struct IndentHistory {
773 pub entries: Vec<(usize, usize)>,
774 pub tab_width: usize,
775}
776impl IndentHistory {
777 pub fn new(tab_width: usize) -> Self {
778 Self {
779 entries: Vec::new(),
780 tab_width,
781 }
782 }
783 pub fn record(&mut self, line_number: usize, line: &str) {
784 let col = LayoutContext::indent_col(line, self.tab_width);
785 self.entries.push((line_number, col));
786 }
787 pub fn from_source(src: &str, tab_width: usize) -> Self {
788 let mut hist = Self::new(tab_width);
789 for (i, line) in src.lines().enumerate() {
790 if !line.trim().is_empty() {
791 hist.record(i + 1, line);
792 }
793 }
794 hist
795 }
796 pub fn max_indent(&self) -> usize {
797 self.entries.iter().map(|&(_, w)| w).max().unwrap_or(0)
798 }
799 pub fn min_nonzero_indent(&self) -> Option<usize> {
800 self.entries
801 .iter()
802 .map(|&(_, w)| w)
803 .filter(|&w| w > 0)
804 .min()
805 }
806}
807#[allow(dead_code)]
809#[allow(missing_docs)]
810pub struct VirtualColumn {
811 pub column: usize,
812 pub tab_width: usize,
813}
814impl VirtualColumn {
815 #[allow(dead_code)]
816 #[allow(missing_docs)]
817 pub fn new(column: usize, tab_width: usize) -> Self {
818 Self { column, tab_width }
819 }
820 #[allow(dead_code)]
821 #[allow(missing_docs)]
822 pub fn advance_by(&self, n: usize) -> Self {
823 Self {
824 column: self.column + n,
825 ..*self
826 }
827 }
828 #[allow(dead_code)]
829 #[allow(missing_docs)]
830 pub fn advance_tab(&self) -> Self {
831 let next = ((self.column / self.tab_width) + 1) * self.tab_width;
832 Self {
833 column: next,
834 ..*self
835 }
836 }
837 #[allow(dead_code)]
838 #[allow(missing_docs)]
839 pub fn is_aligned_to(&self, n: usize) -> bool {
840 n > 0 && self.column % n == 0
841 }
842}
843#[derive(Debug, Clone, Default)]
844pub struct CommentTracker {
845 pub block_depth: usize,
846 pub in_line_comment: bool,
847}
848impl CommentTracker {
849 pub fn new() -> Self {
850 Self::default()
851 }
852 pub fn process_pair(&mut self, ch: char, next: char) {
853 if self.in_line_comment {
854 return;
855 }
856 if self.block_depth == 0 && ch == '-' && next == '-' {
857 self.in_line_comment = true;
858 } else if ch == '/' && next == '-' {
859 self.block_depth += 1;
860 } else if self.block_depth > 0 && ch == '-' && next == '/' {
861 self.block_depth -= 1;
862 }
863 }
864 pub fn end_of_line(&mut self) {
865 self.in_line_comment = false;
866 }
867 pub fn in_comment(&self) -> bool {
868 self.in_line_comment || self.block_depth > 0
869 }
870}
871#[derive(Debug, Clone, PartialEq, Eq)]
872pub enum LineClass {
873 Blank,
874 Comment,
875 DeclOpener,
876 WhereKeyword,
877 LetBinding,
878 Continuation,
879 Other,
880}
881impl LineClass {
882 pub fn classify(line: &str) -> LineClass {
883 let trimmed = line.trim_start();
884 if trimmed.is_empty() {
885 return LineClass::Blank;
886 }
887 if trimmed.starts_with("--") {
888 return LineClass::Comment;
889 }
890 let decl_openers = [
891 "def ",
892 "theorem ",
893 "lemma ",
894 "axiom ",
895 "structure ",
896 "class ",
897 "instance ",
898 "definition ",
899 "noncomputable ",
900 "private ",
901 "protected ",
902 "abbrev ",
903 ];
904 for opener in &decl_openers {
905 if trimmed.starts_with(opener) {
906 return LineClass::DeclOpener;
907 }
908 }
909 if let Some(after) = trimmed.strip_prefix("where") {
910 if after.is_empty()
911 || !after
912 .chars()
913 .next()
914 .is_some_and(|c| c.is_alphanumeric() || c == '_')
915 {
916 return LineClass::WhereKeyword;
917 }
918 }
919 if trimmed.starts_with("let ") {
920 return LineClass::LetBinding;
921 }
922 LineClass::Other
923 }
924 pub fn is_ignorable(&self) -> bool {
925 matches!(self, LineClass::Blank | LineClass::Comment)
926 }
927 pub fn starts_decl(&self) -> bool {
928 matches!(self, LineClass::DeclOpener)
929 }
930}
931#[derive(Debug, Clone, Copy, PartialEq, Eq)]
932pub struct LineSpan {
933 pub start: usize,
934 pub end: usize,
935}
936impl LineSpan {
937 pub fn new(start: usize, end: usize) -> Self {
938 assert!(start <= end);
939 Self { start, end }
940 }
941 pub fn single(line: usize) -> Self {
942 Self {
943 start: line,
944 end: line,
945 }
946 }
947 pub fn len(&self) -> usize {
948 self.end - self.start + 1
949 }
950 pub fn is_empty(&self) -> bool {
952 self.end < self.start
953 }
954 pub fn is_single_line(&self) -> bool {
955 self.start == self.end
956 }
957 pub fn contains(&self, line: usize) -> bool {
958 line >= self.start && line <= self.end
959 }
960 pub fn merge(self, other: LineSpan) -> LineSpan {
961 LineSpan::new(self.start.min(other.start), self.end.max(other.end))
962 }
963}
964#[derive(Debug, Clone, PartialEq, Eq)]
965pub struct IndentGuide {
966 pub column: usize,
967 pub is_active: bool,
968}
969impl IndentGuide {
970 pub fn new(column: usize, is_active: bool) -> Self {
971 Self { column, is_active }
972 }
973}
974#[derive(Debug, Clone)]
975pub struct IndentFixer {
976 pub target_step: usize,
977 pub tab_width: usize,
978}
979impl IndentFixer {
980 pub fn new(target_step: usize, tab_width: usize) -> Self {
981 Self {
982 target_step,
983 tab_width,
984 }
985 }
986 pub fn fix(&self, src: &str) -> String {
987 let report = IndentConsistencyReport::from_source(src);
988 let from_step = if report.step > 0 {
989 report.step
990 } else {
991 self.target_step
992 };
993 let norm = IndentNormaliser::new(self.tab_width);
994 let rw = IndentRewriter::new(from_step, self.target_step);
995 rw.rewrite(&norm.normalise(src))
996 }
997}
998#[derive(Debug, Clone, PartialEq, Eq)]
999pub struct HangingIndent {
1000 pub first_col: usize,
1001 pub cont_col: usize,
1002}
1003impl HangingIndent {
1004 pub fn new(first_col: usize, cont_col: usize) -> Self {
1005 Self {
1006 first_col,
1007 cont_col,
1008 }
1009 }
1010 pub fn is_continuation(&self, col: usize) -> bool {
1011 col == self.cont_col
1012 }
1013 pub fn is_first(&self, col: usize) -> bool {
1014 col == self.first_col
1015 }
1016 pub fn overhang(&self) -> usize {
1017 self.cont_col.saturating_sub(self.first_col)
1018 }
1019}
1020#[derive(Debug, Clone)]
1021pub struct IndentNormaliser {
1022 pub tab_width: usize,
1023}
1024impl IndentNormaliser {
1025 pub fn new(tab_width: usize) -> Self {
1026 Self { tab_width }
1027 }
1028 pub fn normalise_line(&self, line: &str) -> String {
1029 let mut col = 0usize;
1030 let mut rest_start = 0;
1031 for (byte_idx, ch) in line.char_indices() {
1032 match ch {
1033 ' ' => {
1034 col += 1;
1035 rest_start = byte_idx + 1;
1036 }
1037 '\t' => {
1038 let next_stop = (col / self.tab_width + 1) * self.tab_width;
1039 col = next_stop;
1040 rest_start = byte_idx + 1;
1041 }
1042 _ => break,
1043 }
1044 }
1045 " ".repeat(col) + &line[rest_start..]
1046 }
1047 pub fn normalise(&self, src: &str) -> String {
1048 src.lines()
1049 .map(|l| self.normalise_line(l))
1050 .collect::<Vec<_>>()
1051 .join("\n")
1052 }
1053}
1054#[allow(dead_code)]
1056#[allow(missing_docs)]
1057pub struct ConstructRuleRegistry {
1058 rules: std::collections::HashMap<String, ConstructIndentRule>,
1059}
1060impl ConstructRuleRegistry {
1061 #[allow(dead_code)]
1062 #[allow(missing_docs)]
1063 pub fn new() -> Self {
1064 let mut reg = Self {
1065 rules: std::collections::HashMap::new(),
1066 };
1067 reg.add(ConstructIndentRule::standard("def"));
1068 reg.add(ConstructIndentRule::standard("theorem"));
1069 reg.add(ConstructIndentRule::standard("match"));
1070 reg.add(ConstructIndentRule::standard("do"));
1071 reg.add(ConstructIndentRule::standard("where"));
1072 reg
1073 }
1074 #[allow(dead_code)]
1075 #[allow(missing_docs)]
1076 pub fn add(&mut self, rule: ConstructIndentRule) {
1077 self.rules.insert(rule.construct_name.clone(), rule);
1078 }
1079 #[allow(dead_code)]
1080 #[allow(missing_docs)]
1081 pub fn lookup(&self, name: &str) -> Option<&ConstructIndentRule> {
1082 self.rules.get(name)
1083 }
1084 #[allow(dead_code)]
1085 #[allow(missing_docs)]
1086 pub fn body_indent(&self, name: &str) -> usize {
1087 self.rules.get(name).map_or(2, |r| r.body_indent)
1088 }
1089}
1090#[allow(dead_code)]
1092pub struct ColumnOracle {
1093 tab_width: usize,
1094 reference_columns: Vec<usize>,
1095}
1096impl ColumnOracle {
1097 #[allow(dead_code)]
1098 #[allow(missing_docs)]
1099 pub fn new(tab_width: usize) -> Self {
1100 Self {
1101 tab_width,
1102 reference_columns: Vec::new(),
1103 }
1104 }
1105 #[allow(dead_code)]
1106 #[allow(missing_docs)]
1107 pub fn add_reference(&mut self, col: usize) {
1108 self.reference_columns.push(col);
1109 }
1110 #[allow(dead_code)]
1111 #[allow(missing_docs)]
1112 pub fn next_alignment(&self, current: usize) -> usize {
1113 self.reference_columns
1114 .iter()
1115 .copied()
1116 .filter(|&c| c > current)
1117 .min()
1118 .unwrap_or(((current / self.tab_width) + 1) * self.tab_width)
1119 }
1120 #[allow(dead_code)]
1121 #[allow(missing_docs)]
1122 pub fn is_at_reference(&self, col: usize) -> bool {
1123 self.reference_columns.contains(&col)
1124 }
1125}
1126#[derive(Debug, Clone)]
1127pub struct LetBinding {
1128 pub name: String,
1129 pub col: usize,
1130 pub has_type: bool,
1131}
1132impl LetBinding {
1133 pub fn new(name: impl Into<String>, col: usize, has_type: bool) -> Self {
1134 Self {
1135 name: name.into(),
1136 col,
1137 has_type,
1138 }
1139 }
1140}
1141#[derive(Debug, Clone)]
1142pub struct TabStopIterator {
1143 pub tab_width: usize,
1144 pub(super) current: usize,
1145}
1146impl TabStopIterator {
1147 pub fn new(tab_width: usize) -> Self {
1148 Self {
1149 tab_width,
1150 current: 0,
1151 }
1152 }
1153 pub fn from_col(tab_width: usize, start_col: usize) -> Self {
1154 let next_stop = if start_col % tab_width == 0 {
1155 start_col
1156 } else {
1157 (start_col / tab_width + 1) * tab_width
1158 };
1159 Self {
1160 tab_width,
1161 current: next_stop,
1162 }
1163 }
1164}
1165#[derive(Debug, Clone, Default)]
1166pub struct ColumnAligner {
1167 pub target_col: usize,
1168}
1169impl ColumnAligner {
1170 pub fn new(target_col: usize) -> Self {
1171 Self { target_col }
1172 }
1173 pub fn pad(&self, s: &str, current_col: usize) -> String {
1174 if current_col >= self.target_col {
1175 format!("{} ", s)
1176 } else {
1177 format!("{}{}", s, " ".repeat(self.target_col - current_col))
1178 }
1179 }
1180 pub fn align_all(items: &[String]) -> Vec<String> {
1181 let max_len = items.iter().map(|s| s.len()).max().unwrap_or(0);
1182 items
1183 .iter()
1184 .map(|s| format!("{}{}", s, " ".repeat(max_len - s.len())))
1185 .collect()
1186 }
1187}
1188#[allow(dead_code)]
1190#[allow(missing_docs)]
1191pub struct IndentBlock {
1192 pub level: usize,
1193 pub lines: Vec<String>,
1194}
1195impl IndentBlock {
1196 #[allow(dead_code)]
1197 #[allow(missing_docs)]
1198 pub fn new(level: usize) -> Self {
1199 Self {
1200 level,
1201 lines: Vec::new(),
1202 }
1203 }
1204 #[allow(dead_code)]
1205 #[allow(missing_docs)]
1206 pub fn add_line(&mut self, line: impl Into<String>) {
1207 self.lines.push(line.into());
1208 }
1209 #[allow(dead_code)]
1210 #[allow(missing_docs)]
1211 pub fn line_count(&self) -> usize {
1212 self.lines.len()
1213 }
1214 #[allow(dead_code)]
1215 #[allow(missing_docs)]
1216 pub fn render(&self) -> String {
1217 self.lines.join("\n")
1218 }
1219}
1220#[derive(Debug, Clone, Default)]
1221pub struct AlignedPrinter {
1222 entries: Vec<(usize, String, String)>,
1223 pub separator: String,
1224}
1225impl AlignedPrinter {
1226 pub fn new(separator: impl Into<String>) -> Self {
1227 Self {
1228 entries: Vec::new(),
1229 separator: separator.into(),
1230 }
1231 }
1232 pub fn add(&mut self, indent: usize, lhs: impl Into<String>, rhs: impl Into<String>) {
1233 self.entries.push((indent, lhs.into(), rhs.into()));
1234 }
1235 pub fn format(&self) -> String {
1236 let max_lhs = self
1237 .entries
1238 .iter()
1239 .map(|(_, lhs, _)| lhs.len())
1240 .max()
1241 .unwrap_or(0);
1242 let mut lines = Vec::new();
1243 for (indent, lhs, rhs) in &self.entries {
1244 let padding = " ".repeat(max_lhs - lhs.len());
1245 let indent_str = " ".repeat(*indent);
1246 lines.push(format!(
1247 "{}{}{} {} {}",
1248 indent_str, lhs, padding, self.separator, rhs
1249 ));
1250 }
1251 lines.join("\n")
1252 }
1253 pub fn clear(&mut self) {
1254 self.entries.clear();
1255 }
1256 pub fn len(&self) -> usize {
1257 self.entries.len()
1258 }
1259 pub fn is_empty(&self) -> bool {
1260 self.entries.is_empty()
1261 }
1262}
1263#[allow(dead_code)]
1265#[allow(missing_docs)]
1266pub struct IndentChangeLog {
1267 events: Vec<(usize, usize, usize)>,
1268}
1269impl IndentChangeLog {
1270 #[allow(dead_code)]
1271 #[allow(missing_docs)]
1272 pub fn new() -> Self {
1273 Self { events: Vec::new() }
1274 }
1275 #[allow(dead_code)]
1276 #[allow(missing_docs)]
1277 pub fn record(&mut self, line: usize, old: usize, new: usize) {
1278 if old != new {
1279 self.events.push((line, old, new));
1280 }
1281 }
1282 #[allow(dead_code)]
1283 #[allow(missing_docs)]
1284 pub fn count(&self) -> usize {
1285 self.events.len()
1286 }
1287 #[allow(dead_code)]
1288 #[allow(missing_docs)]
1289 pub fn increases(&self) -> usize {
1290 self.events.iter().filter(|(_, o, n)| n > o).count()
1291 }
1292 #[allow(dead_code)]
1293 #[allow(missing_docs)]
1294 pub fn decreases(&self) -> usize {
1295 self.events.iter().filter(|(_, o, n)| n < o).count()
1296 }
1297}
1298#[derive(Debug, Clone, PartialEq, Eq)]
1299pub struct IndentDelta {
1300 pub line: usize,
1301 pub before: usize,
1302 pub after: usize,
1303}
1304impl IndentDelta {
1305 pub fn compute_all(src: &str, tab_width: usize) -> Vec<IndentDelta> {
1306 let mut deltas = Vec::new();
1307 let mut prev: Option<usize> = None;
1308 for (i, line) in src.lines().enumerate() {
1309 if line.trim().is_empty() {
1310 continue;
1311 }
1312 let col = LayoutContext::indent_col(line, tab_width);
1313 if let Some(p) = prev {
1314 if col != p {
1315 deltas.push(IndentDelta {
1316 line: i + 1,
1317 before: p,
1318 after: col,
1319 });
1320 }
1321 }
1322 prev = Some(col);
1323 }
1324 deltas
1325 }
1326 pub fn is_increase(&self) -> bool {
1327 self.after > self.before
1328 }
1329 pub fn is_decrease(&self) -> bool {
1330 self.after < self.before
1331 }
1332 pub fn change(&self) -> i64 {
1333 self.after as i64 - self.before as i64
1334 }
1335}
1336#[derive(Debug, Clone, Default)]
1337pub struct IndentStats {
1338 pub space_lines: usize,
1339 pub tab_lines: usize,
1340 pub mixed_lines: usize,
1341 pub blank_lines: usize,
1342 pub most_common_step: usize,
1343}
1344impl IndentStats {
1345 pub fn analyse(src: &str) -> Self {
1346 let mut stats = IndentStats::default();
1347 let mut step_counts: std::collections::HashMap<usize, usize> =
1348 std::collections::HashMap::new();
1349 let mut prev_width: Option<usize> = None;
1350 for line in src.lines() {
1351 let trimmed = line.trim_start();
1352 if trimmed.is_empty() {
1353 stats.blank_lines += 1;
1354 continue;
1355 }
1356 let leading: String = line.chars().take_while(|c| c.is_whitespace()).collect();
1357 let has_tab = leading.contains('\t');
1358 let has_space = leading.contains(' ');
1359 if has_tab && has_space {
1360 stats.mixed_lines += 1;
1361 } else if has_tab {
1362 stats.tab_lines += 1;
1363 } else if has_space {
1364 stats.space_lines += 1;
1365 }
1366 let width: usize = leading.chars().map(|c| if c == '\t' { 4 } else { 1 }).sum();
1367 if let Some(pw) = prev_width {
1368 if width > pw {
1369 *step_counts.entry(width - pw).or_insert(0) += 1;
1370 }
1371 }
1372 prev_width = Some(width);
1373 }
1374 if let Some((&step, _)) = step_counts.iter().max_by_key(|(_, &c)| c) {
1375 stats.most_common_step = step;
1376 }
1377 stats
1378 }
1379 pub fn code_lines(&self) -> usize {
1380 self.space_lines + self.tab_lines + self.mixed_lines
1381 }
1382 pub fn is_spaces_only(&self) -> bool {
1383 self.tab_lines == 0 && self.mixed_lines == 0
1384 }
1385 pub fn is_tabs_only(&self) -> bool {
1386 self.space_lines == 0 && self.mixed_lines == 0
1387 }
1388}
1389#[allow(dead_code)]
1391#[allow(missing_docs)]
1392pub struct HangingIndentState {
1393 pub base_indent: usize,
1394 pub hanging: bool,
1395 pub hanging_indent: usize,
1396}
1397impl HangingIndentState {
1398 #[allow(dead_code)]
1399 #[allow(missing_docs)]
1400 pub fn new(base: usize) -> Self {
1401 Self {
1402 base_indent: base,
1403 hanging: false,
1404 hanging_indent: base + 4,
1405 }
1406 }
1407 #[allow(dead_code)]
1408 #[allow(missing_docs)]
1409 pub fn enter_hanging(&mut self) {
1410 self.hanging = true;
1411 }
1412 #[allow(dead_code)]
1413 #[allow(missing_docs)]
1414 pub fn exit_hanging(&mut self) {
1415 self.hanging = false;
1416 }
1417 #[allow(dead_code)]
1418 #[allow(missing_docs)]
1419 pub fn current_indent(&self) -> usize {
1420 if self.hanging {
1421 self.hanging_indent
1422 } else {
1423 self.base_indent
1424 }
1425 }
1426}
1427#[derive(Debug, Default)]
1428pub struct IndentationChecker {
1429 pub tab_width: usize,
1430 pub errors: Vec<IndentMismatchError>,
1431}
1432impl IndentationChecker {
1433 pub fn new(tab_width: usize) -> Self {
1434 Self {
1435 tab_width,
1436 errors: Vec::new(),
1437 }
1438 }
1439 pub fn check_transition(
1440 &mut self,
1441 line_number: usize,
1442 prev: &IndentLevel,
1443 next: &IndentLevel,
1444 context: &str,
1445 ) {
1446 let tw = self.tab_width;
1447 let pw = prev.total_width(tw);
1448 let nw = next.total_width(tw);
1449 if nw > pw && (nw - pw) % tw != 0 && tw > 0 {
1450 self.errors
1451 .push(IndentMismatchError::new(line_number, pw + tw, nw, context));
1452 }
1453 }
1454 pub fn is_clean(&self) -> bool {
1455 self.errors.is_empty()
1456 }
1457 pub fn clear(&mut self) {
1458 self.errors.clear();
1459 }
1460}
1461#[allow(dead_code)]
1463pub struct IndentZipper {
1464 above: Vec<(usize, String)>,
1465 focus: (usize, String),
1466 below: Vec<(usize, String)>,
1467}
1468impl IndentZipper {
1469 #[allow(dead_code)]
1470 #[allow(missing_docs)]
1471 pub fn from_source(source: &str) -> Option<Self> {
1472 let lines: Vec<_> = source
1473 .lines()
1474 .enumerate()
1475 .filter(|(_, l)| !l.trim().is_empty())
1476 .map(|(_, l)| {
1477 let indent = l.len() - l.trim_start().len();
1478 (indent, l.to_string())
1479 })
1480 .collect();
1481 if lines.is_empty() {
1482 return None;
1483 }
1484 let mut above = lines;
1485 let focus = above.remove(0);
1486 Some(Self {
1487 above: Vec::new(),
1488 focus,
1489 below: above,
1490 })
1491 }
1492 #[allow(dead_code)]
1493 #[allow(missing_docs)]
1494 pub fn move_down(&mut self) -> bool {
1495 if self.below.is_empty() {
1496 return false;
1497 }
1498 let next = self.below.remove(0);
1499 self.above.push(std::mem::replace(&mut self.focus, next));
1500 true
1501 }
1502 #[allow(dead_code)]
1503 #[allow(missing_docs)]
1504 pub fn move_up(&mut self) -> bool {
1505 if self.above.is_empty() {
1506 return false;
1507 }
1508 let prev = self
1509 .above
1510 .pop()
1511 .expect("above is non-empty per is_empty check above");
1512 self.below
1513 .insert(0, std::mem::replace(&mut self.focus, prev));
1514 true
1515 }
1516 #[allow(dead_code)]
1517 #[allow(missing_docs)]
1518 pub fn current_indent(&self) -> usize {
1519 self.focus.0
1520 }
1521 #[allow(dead_code)]
1522 #[allow(missing_docs)]
1523 pub fn current_content(&self) -> &str {
1524 &self.focus.1
1525 }
1526 #[allow(dead_code)]
1527 #[allow(missing_docs)]
1528 pub fn position(&self) -> usize {
1529 self.above.len()
1530 }
1531}
1532#[allow(dead_code)]
1534#[allow(missing_docs)]
1535#[derive(Clone, Debug)]
1536pub struct ConstructIndentRule {
1537 pub construct_name: String,
1538 pub body_indent: usize,
1539 pub continuation_indent: usize,
1540 pub hanging_indent: usize,
1541}
1542impl ConstructIndentRule {
1543 #[allow(dead_code)]
1544 #[allow(missing_docs)]
1545 pub fn new(name: impl Into<String>, body: usize, cont: usize, hang: usize) -> Self {
1546 Self {
1547 construct_name: name.into(),
1548 body_indent: body,
1549 continuation_indent: cont,
1550 hanging_indent: hang,
1551 }
1552 }
1553 #[allow(dead_code)]
1554 #[allow(missing_docs)]
1555 pub fn standard(name: impl Into<String>) -> Self {
1556 Self::new(name, 2, 4, 4)
1557 }
1558}
1559#[derive(Debug, Clone, Default)]
1560pub struct ScopeTracker {
1561 scopes: Vec<Scope>,
1562}
1563impl ScopeTracker {
1564 pub fn new() -> Self {
1565 Self { scopes: Vec::new() }
1566 }
1567 pub fn enter(&mut self, kind: impl Into<String>, indent: IndentLevel) {
1568 self.scopes.push(Scope::new(kind, indent));
1569 }
1570 pub fn exit(&mut self) -> Option<Scope> {
1571 self.scopes.pop()
1572 }
1573 pub fn current(&self) -> Option<&Scope> {
1574 self.scopes.last()
1575 }
1576 pub fn current_mut(&mut self) -> Option<&mut Scope> {
1577 self.scopes.last_mut()
1578 }
1579 pub fn resolve(&self, name: &str) -> Option<&Scope> {
1580 self.scopes.iter().rev().find(|s| s.is_bound(name))
1581 }
1582 pub fn depth(&self) -> usize {
1583 self.scopes.len()
1584 }
1585 pub fn clear(&mut self) {
1586 self.scopes.clear();
1587 }
1588 pub fn bind(&mut self, name: impl Into<String>) {
1589 if let Some(scope) = self.current_mut() {
1590 scope.add_binding(name);
1591 }
1592 }
1593}