1use proc_macro2::LineColumn;
8
9#[derive(Clone, Debug, PartialEq, Eq)]
15pub enum Comment {
16 Line(String),
20 Block(String),
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
31pub enum GapPiece {
32 Comment(Comment),
34 Punct(&'static str),
36}
37
38pub fn scan_gap(gap: &str, expected: &[&'static str]) -> Vec<GapPiece> {
59 let mut pieces = Vec::new();
60 let mut next = 0usize;
61 let mut rest = gap;
62 loop {
63 rest = rest.trim_start();
64 if rest.is_empty() {
65 break;
66 }
67 if let Some(after) = rest.strip_prefix("//") {
68 let end = after.find('\n').unwrap_or(after.len());
69 pieces.push(GapPiece::Comment(Comment::Line(
70 after[..end].trim().to_string(),
71 )));
72 rest = &after[end..];
73 } else if let Some((inner, after)) = take_block_comment(rest) {
74 pieces.push(GapPiece::Comment(Comment::Block(normalize_block(inner))));
75 rest = after;
76 } else if next < expected.len() && rest.starts_with(expected[next]) {
77 pieces.push(GapPiece::Punct(expected[next]));
78 rest = &rest[expected[next].len()..];
79 next += 1;
80 } else {
81 break;
84 }
85 }
86 while next < expected.len() {
87 pieces.push(GapPiece::Punct(expected[next]));
88 next += 1;
89 }
90 pieces
91}
92
93pub(crate) fn take_block_comment(source: &str) -> Option<(&str, &str)> {
100 let inner = source.strip_prefix("/*")?;
101 let bytes = inner.as_bytes();
102 let mut depth = 1usize;
103 let mut offset = 0usize;
104 while offset + 1 < bytes.len() {
105 match &bytes[offset..offset + 2] {
106 b"/*" => {
107 depth += 1;
108 offset += 2;
109 }
110 b"*/" => {
111 depth -= 1;
112 if depth == 0 {
113 return Some((&inner[..offset], &inner[offset + 2..]));
114 }
115 offset += 2;
116 }
117 _ => offset += 1,
118 }
119 }
120 Some((inner, ""))
121}
122
123fn normalize_block(inner: &str) -> String {
128 inner
129 .lines()
130 .map(str::trim)
131 .filter(|l| !l.is_empty())
132 .collect::<Vec<_>>()
133 .join("\n")
134}
135
136pub fn line_start_byte_offsets(source: &str) -> Vec<usize> {
144 let mut offsets = vec![0usize];
145 let mut byte = 0usize;
146 for line in source.split_inclusive('\n') {
147 byte += line.len();
148 offsets.push(byte);
149 }
150 offsets
151}
152
153pub fn line_column_to_byte(source: &str, line_starts: &[usize], pos: LineColumn) -> usize {
169 debug_assert!(
170 pos.line >= 1 && pos.line - 1 < line_starts.len(),
171 "pos.line out of range for line_starts"
172 );
173 let line_start = line_starts[pos.line - 1];
174 line_start
175 + source[line_start..]
176 .chars()
177 .take(pos.column)
178 .map(char::len_utf8)
179 .sum::<usize>()
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 fn comments(pieces: Vec<GapPiece>) -> Vec<Comment> {
187 pieces
188 .into_iter()
189 .filter_map(|p| match p {
190 GapPiece::Comment(c) => Some(c),
191 GapPiece::Punct(_) => None,
192 })
193 .collect()
194 }
195
196 fn puncts(pieces: &[GapPiece]) -> Vec<&'static str> {
197 pieces
198 .iter()
199 .filter_map(|p| match p {
200 GapPiece::Punct(s) => Some(*s),
201 GapPiece::Comment(_) => None,
202 })
203 .collect()
204 }
205
206 #[test]
207 fn empty_gap_yields_just_the_expected_tokens() {
208 let pieces = scan_gap(" ", &["+"]);
209 assert_eq!(puncts(&pieces), vec!["+"]);
210 assert!(comments(pieces).is_empty());
211 }
212
213 #[test]
214 fn a_block_comment_before_the_token_is_recovered_in_order() {
215 let pieces = scan_gap(" /* a */ + ", &["+"]);
216 assert!(matches!(pieces[0], GapPiece::Comment(Comment::Block(ref t)) if t == "a"));
217 assert!(matches!(pieces[1], GapPiece::Punct("+")));
218 }
219
220 #[test]
221 fn a_block_comment_after_the_token_is_recovered_in_order() {
222 let pieces = scan_gap(" + /* b */ ", &["+"]);
223 assert!(matches!(pieces[0], GapPiece::Punct("+")));
224 assert!(matches!(pieces[1], GapPiece::Comment(Comment::Block(ref t)) if t == "b"));
225 }
226
227 #[test]
228 fn comments_on_both_sides_of_the_token_keep_source_order() {
229 let pieces = scan_gap(" /* a */ + /* b */ ", &["+"]);
230 assert!(matches!(pieces[0], GapPiece::Comment(Comment::Block(ref t)) if t == "a"));
231 assert!(matches!(pieces[1], GapPiece::Punct("+")));
232 assert!(matches!(pieces[2], GapPiece::Comment(Comment::Block(ref t)) if t == "b"));
233 }
234
235 #[test]
236 fn two_consecutive_block_comments_are_two_pieces() {
237 let pieces = scan_gap(" /* a */ /* b */ + ", &["+"]);
238 assert_eq!(comments(pieces.clone()).len(), 2);
239 assert!(matches!(pieces[2], GapPiece::Punct("+")));
240 }
241
242 #[test]
244 fn nested_block_comments_preserve_text_and_token_order() {
245 assert_eq!(
246 scan_gap(
247 " /* outer /* α /* deep */ β */ outer */ . /* after /* inner */ end */ ",
248 &["."],
249 ),
250 vec![
251 GapPiece::Comment(Comment::Block(
252 "outer /* α /* deep */ β */ outer".to_string(),
253 )),
254 GapPiece::Punct("."),
255 GapPiece::Comment(Comment::Block("after /* inner */ end".to_string())),
256 ],
257 );
258 }
259
260 #[test]
262 fn block_comment_splitting_preserves_boundaries() {
263 for (source, expected) in [
264 ("", None),
265 ("/", None),
266 ("// line", None),
267 ("/**/.x", Some(("", ".x"))),
268 ("/*/**/*/.x", Some(("/**/", ".x"))),
269 ("/* α /* β */ γ */δ", Some((" α /* β */ γ ", "δ"))),
270 ("/* one *//* two */", Some((" one ", "/* two */"))),
271 ("/*", Some(("", ""))),
272 ("/* α", Some((" α", ""))),
273 ("/* outer /* inner */", Some((" outer /* inner */", ""))),
274 ("/*/*/", Some(("/*/", ""))),
275 ] {
276 assert_eq!(take_block_comment(source), expected, "{source}");
277 }
278 }
279
280 #[test]
281 fn two_consecutive_line_comments_are_two_pieces() {
282 let pieces = scan_gap(" // a\n // b\n + ", &["+"]);
283 assert_eq!(comments(pieces.clone()).len(), 2);
284 assert!(matches!(pieces.last(), Some(GapPiece::Punct("+"))));
285 }
286
287 #[test]
288 fn a_line_comment_is_recovered_as_comment_line() {
289 let pieces = scan_gap(" + // hi\n", &["+"]);
290 assert!(matches!(pieces[0], GapPiece::Punct("+")));
291 assert!(matches!(pieces[1], GapPiece::Comment(Comment::Line(ref t)) if t == "hi"));
292 }
293
294 #[test]
295 fn a_multi_line_block_comment_joins_its_inner_lines() {
296 let pieces = scan_gap(" /*\n one\n two\n*/ + ", &["+"]);
297 assert!(matches!(pieces[0], GapPiece::Comment(Comment::Block(ref t)) if t == "one\ntwo"));
298 }
299
300 #[test]
301 fn multiple_expected_tokens_are_returned_in_order() {
302 let pieces = scan_gap("} /* first */ else // second\n {", &["}", "else", "{"]);
303 assert_eq!(
304 pieces,
305 vec![
306 GapPiece::Punct("}"),
307 GapPiece::Comment(Comment::Block("first".into())),
308 GapPiece::Punct("else"),
309 GapPiece::Comment(Comment::Line("second".into())),
310 GapPiece::Punct("{"),
311 ]
312 );
313 }
314
315 #[test]
316 fn missing_expected_token_is_still_synthesized() {
317 let pieces = scan_gap("", &["(", ")"]);
319 assert_eq!(puncts(&pieces), vec!["(", ")"]);
320 }
321
322 #[test]
323 fn line_column_to_byte_maps_a_position_on_the_second_line() {
324 let source = "ab\ncd";
325 let starts = line_start_byte_offsets(source);
326 let pos = LineColumn { line: 2, column: 1 };
328 assert_eq!(line_column_to_byte(source, &starts, pos), 4);
329 }
330
331 #[test]
333 fn position_conversion_covers_text_boundaries() {
334 for (source, line, column, expected) in [
335 ("", 1, 0, 0),
336 ("é\nβ", 1, 1, 2),
337 ("é\nβ", 2, 1, 5),
338 ("x\n", 2, 0, 2),
339 ] {
340 assert_eq!(
341 line_column_to_byte(
342 source,
343 &line_start_byte_offsets(source),
344 LineColumn { line, column }
345 ),
346 expected
347 );
348 }
349 }
350}