logicaffeine_language/
source_format.rs1use crate::lexer::Lexer;
22use crate::token::{BlockType, TokenType};
23use logicaffeine_base::Interner;
24
25pub fn format_line(line: &str) -> String {
30 let trimmed_start = line.trim_start();
31 let leading = &line[..line.len() - trimmed_start.len()];
32 let mut out = String::with_capacity(line.len());
33 for ch in leading.chars() {
34 if ch == '\t' {
35 out.push_str(" ");
36 } else {
37 out.push(ch);
38 }
39 }
40 out.push_str(trimmed_start.trim_end());
41 if out.chars().all(|c| c == ' ') {
43 out.clear();
44 }
45 out
46}
47
48#[derive(Clone, Copy, PartialEq)]
50enum LinePlan {
51 Code(usize),
53 Plain,
55 Raw,
57}
58
59pub fn format_source(source: &str) -> String {
63 if source.is_empty() {
64 return String::new();
65 }
66
67 let plans = line_plans(source);
68 let mut out: String = source
69 .lines()
70 .enumerate()
71 .map(|(i, line)| match plans.get(i).copied().unwrap_or(LinePlan::Plain) {
72 LinePlan::Raw => line.to_string(),
73 LinePlan::Plain => format_line(line),
74 LinePlan::Code(depth) => {
75 let content = line.trim_start().trim_end();
76 if content.is_empty() {
77 String::new()
78 } else {
79 let mut formatted = " ".repeat(depth * 4);
80 formatted.push_str(content);
81 formatted
82 }
83 }
84 })
85 .collect::<Vec<_>>()
86 .join("\n");
87 if source.ends_with('\n') {
88 out.push('\n');
89 }
90 out
91}
92
93fn line_plans(source: &str) -> Vec<LinePlan> {
95 let line_starts: Vec<usize> = std::iter::once(0)
96 .chain(source.match_indices('\n').map(|(i, _)| i + 1))
97 .collect();
98 let line_of = |offset: usize| match line_starts.binary_search(&offset) {
99 Ok(i) => i,
100 Err(i) => i - 1,
101 };
102 let line_count = source.lines().count();
103 let mut plans = vec![LinePlan::Plain; line_count];
104
105 let mut interner = Interner::new();
106 let mut lexer = Lexer::new(source, &mut interner);
107 let tokens = lexer.tokenize();
108
109 let mut depth: usize = 0;
112 let mut current_line: Option<usize> = None;
113 for token in &tokens {
114 match &token.kind {
115 TokenType::Indent => depth += 1,
116 TokenType::Dedent => depth = depth.saturating_sub(1),
117 TokenType::Newline | TokenType::EOF => {}
118 _ => {
119 let line = line_of(token.span.start);
120 if current_line != Some(line) {
121 current_line = Some(line);
122 if let Some(plan) = plans.get_mut(line) {
123 *plan = LinePlan::Code(depth);
124 }
125 }
126 }
127 }
128 }
129
130 for token in &tokens {
134 let start_line = line_of(token.span.start);
135 let end_line = line_of(token.span.end.saturating_sub(1).max(token.span.start));
136 if end_line > start_line {
137 for line in start_line..=end_line {
138 if let Some(plan) = plans.get_mut(line) {
139 *plan = LinePlan::Raw;
140 }
141 }
142 }
143 }
144
145 let mut in_prose = false;
148 let mut prose_from = 0usize;
149 let mut mark_prose = |plans: &mut Vec<LinePlan>, from: usize, to: usize| {
150 for plan in plans.iter_mut().take(to).skip(from) {
151 *plan = LinePlan::Raw;
152 }
153 };
154 for token in &tokens {
155 if let TokenType::BlockHeader { block_type } = &token.kind {
156 let header_line = line_of(token.span.start);
157 if in_prose {
158 mark_prose(&mut plans, prose_from, header_line);
159 }
160 in_prose = matches!(block_type, BlockType::Note | BlockType::Example);
161 prose_from = header_line + 1;
162 }
163 }
164 if in_prose {
165 mark_prose(&mut plans, prose_from, line_count);
166 }
167
168 plans
169}