1use super::instrument::count_braces;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Statement {
20 pub first_line: usize,
22 pub last_line: usize,
24 pub text: String,
27 pub comments: bool,
30 pieces: Vec<Piece>,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35struct Piece {
36 at: usize,
38 line: usize,
39 column: usize,
41 len: usize,
42}
43
44impl Statement {
45 fn starting(line: usize) -> Self {
46 Self {
47 first_line: line,
48 last_line: line,
49 text: String::new(),
50 comments: false,
51 pieces: Vec::new(),
52 }
53 }
54
55 fn push(&mut self, text: &str, line: usize, column: usize) {
56 if text.is_empty() {
57 return;
58 }
59 if !self.text.is_empty() {
60 self.text.push(' ');
61 }
62 self.pieces.push(Piece {
63 at: self.text.len(),
64 line,
65 column,
66 len: text.len(),
67 });
68 self.text.push_str(text);
69 self.last_line = line;
70 }
71
72 pub fn locate(&self, offset: usize) -> (usize, usize) {
75 let Some(piece) = self.pieces.iter().rev().find(|piece| piece.at <= offset) else {
76 return (self.first_line, offset);
77 };
78 (
79 piece.line,
80 piece.column + (offset - piece.at).min(piece.len),
81 )
82 }
83
84 pub fn offset_of(&self, line: usize, column: usize) -> Option<usize> {
88 let piece = self
89 .pieces
90 .iter()
91 .rev()
92 .find(|piece| piece.line == line && piece.column <= column)?;
93 Some(piece.at + (column - piece.column).min(piece.len))
94 }
95
96 pub fn covers(&self, line: usize) -> bool {
98 (self.first_line..=self.last_line).contains(&line)
99 }
100}
101
102fn scan(line: &str, mut quoted: bool) -> (usize, bool) {
105 let bytes = line.as_bytes();
106 for (index, &byte) in bytes.iter().enumerate() {
107 match byte {
108 b'"' => quoted = !quoted,
109 b'-' if bytes.get(index + 1) == Some(&b'-') => return (index, quoted),
110 _ => {}
111 }
112 }
113 (line.len(), quoted)
114}
115
116fn blank_or_comment(line: &str) -> bool {
117 let trimmed = line.trim_start();
118 trimmed.is_empty() || trimmed.starts_with("--")
119}
120
121fn opens_def(line: &str) -> bool {
122 let trimmed = line.trim_start();
123 trimmed.starts_with("def ") || trimmed.trim_end() == "def"
124}
125
126fn takes_a_chain(statement: &Statement) -> bool {
129 let text = statement.text.trim_start();
130 text.starts_with('}') || text.matches('"').count() >= 2
131}
132
133pub fn statements(source: &str) -> Vec<Statement> {
136 let lines: Vec<&str> = source.lines().collect();
137 let mut out = Vec::new();
138 let mut def_depth = 0usize;
139 let mut index = 0;
140 while index < lines.len() {
141 let raw = lines[index];
142 if def_depth > 0 || opens_def(raw) {
143 def_depth += count_braces(raw, '{');
144 def_depth -= count_braces(raw, '}').min(def_depth);
145 out.push(verbatim(raw, index));
146 index += 1;
147 continue;
148 }
149 if blank_or_comment(raw) {
150 out.push(verbatim(raw, index));
151 index += 1;
152 continue;
153 }
154 let (cut, mut quoted) = scan(raw, false);
155 let mut statement = Statement::starting(index);
156 statement.comments = cut < raw.len();
157 statement.push(raw[..cut].trim_end(), index, 0);
158 let mut next = index + 1;
159 loop {
160 if quoted {
161 let Some(line) = lines.get(next) else { break };
163 let (cut, still) = scan(line, true);
164 statement.comments |= cut < line.len();
165 let code = &line[..cut];
166 let lead = code.len() - code.trim_start().len();
167 statement.push(code.trim(), next, lead);
168 statement.last_line = next;
169 quoted = still;
170 next += 1;
171 continue;
172 }
173 let mut ahead = next;
176 while ahead < lines.len() && blank_or_comment(lines[ahead]) {
177 ahead += 1;
178 }
179 let continues = lines
180 .get(ahead)
181 .is_some_and(|line| line.trim_start().starts_with('|'));
182 if !continues || !takes_a_chain(&statement) {
183 break;
184 }
185 statement.comments |= lines[next..ahead]
186 .iter()
187 .any(|line| !line.trim().is_empty());
188 let line = lines[ahead];
189 let (cut, still) = scan(line, false);
190 statement.comments |= cut < line.len();
191 let code = &line[..cut];
192 let lead = code.len() - code.trim_start().len();
193 statement.push(code.trim(), ahead, lead);
194 quoted = still;
195 next = ahead + 1;
196 }
197 out.push(statement);
198 index = next;
199 }
200 out
201}
202
203fn verbatim(line: &str, index: usize) -> Statement {
204 let mut statement = Statement::starting(index);
205 statement.comments = line.trim_start().starts_with("--");
206 statement.pieces.push(Piece {
207 at: 0,
208 line: index,
209 column: 0,
210 len: line.len(),
211 });
212 statement.text = line.to_string();
213 statement
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 fn texts(source: &str) -> Vec<String> {
221 statements(source).into_iter().map(|s| s.text).collect()
222 }
223
224 #[test]
225 fn a_line_is_a_statement() {
226 assert_eq!(
227 texts("bpm 92\n k kick \"x ~\" | gain 0.9\n"),
228 ["bpm 92", " k kick \"x ~\" | gain 0.9"]
229 );
230 }
231
232 #[test]
233 fn notation_may_break_inside_its_quotes() {
234 let source = " pad pad_dark \"<\n [0,2,4] -- Em\n\n [5,7,9]\n >\" @ 3..\n";
235 let all = statements(source);
236 assert_eq!(all.len(), 1);
237 assert_eq!(all[0].text, " pad pad_dark \"< [0,2,4] [5,7,9] >\" @ 3..");
238 assert_eq!((all[0].first_line, all[0].last_line), (0, 4));
239 assert!(all[0].comments);
240 let at = all[0].text.find("[5").unwrap();
241 assert_eq!(all[0].locate(at), (3, 4));
242 assert_eq!(all[0].offset_of(3, 4), Some(at));
243 }
244
245 #[test]
246 fn a_chain_may_continue_on_pipe_lines() {
247 let source = " bass bass \"0 2\"\n | oct -2\n -- the filter opens\n | lpf 600..1800 | ramp 4\nbpm 90\n";
248 assert_eq!(
249 texts(source),
250 [
251 " bass bass \"0 2\" | oct -2 | lpf 600..1800 | ramp 4",
252 "bpm 90"
253 ]
254 );
255 let group = " } | reverb 0.1\n | gain 0.9\n";
256 assert_eq!(texts(group), [" } | reverb 0.1 | gain 0.9"]);
257 }
258
259 #[test]
260 fn a_pipe_line_does_not_join_a_directive() {
261 assert_eq!(texts("bpm 90\n| gain 2\n"), ["bpm 90", "| gain 2"]);
262 }
263
264 #[test]
265 fn trailing_comments_are_dropped_and_comment_lines_kept() {
266 let all = statements("-- intro\n k kick \"x ~\" -- hits\n");
267 assert_eq!(all[0].text, "-- intro");
268 assert_eq!(all[1].text, " k kick \"x ~\"");
269 assert!(all[1].comments);
270 }
271
272 #[test]
273 fn def_blocks_are_left_as_written() {
274 let source = "def thump {\n tone sine gain 0.5\n}\n k thump \"x\"\n";
275 assert_eq!(
276 texts(source),
277 [
278 "def thump {",
279 " tone sine gain 0.5",
280 "}",
281 " k thump \"x\""
282 ]
283 );
284 }
285}