1use proc_macro2::LineColumn;
8
9#[derive(Clone, Debug, PartialEq, Eq)]
12pub enum Comment {
13 Line(String),
17 Block(String),
20}
21
22#[derive(Clone, Debug, PartialEq, Eq)]
25pub enum GapPiece {
26 Comment(Comment),
28 Punct(&'static str),
30}
31
32pub fn scan_gap(gap: &str, expected: &[&'static str]) -> Vec<GapPiece> {
44 let mut pieces = Vec::new();
45 let mut next = 0usize;
46 let mut rest = gap;
47 loop {
48 rest = rest.trim_start();
49 if rest.is_empty() {
50 break;
51 }
52 if let Some(after) = rest.strip_prefix("//") {
53 let end = after.find('\n').unwrap_or(after.len());
54 pieces.push(GapPiece::Comment(Comment::Line(
55 after[..end].trim().to_string(),
56 )));
57 rest = &after[end..];
58 } else if let Some((inner, after)) = take_block_comment(rest) {
59 pieces.push(GapPiece::Comment(Comment::Block(normalize_block(inner))));
60 rest = after;
61 } else if next < expected.len() && rest.starts_with(expected[next]) {
62 pieces.push(GapPiece::Punct(expected[next]));
63 rest = &rest[expected[next].len()..];
64 next += 1;
65 } else {
66 break;
69 }
70 }
71 while next < expected.len() {
72 pieces.push(GapPiece::Punct(expected[next]));
73 next += 1;
74 }
75 pieces
76}
77
78pub(crate) fn take_block_comment(source: &str) -> Option<(&str, &str)> {
85 let inner = source.strip_prefix("/*")?;
86 let bytes = inner.as_bytes();
87 let mut depth = 1usize;
88 let mut offset = 0usize;
89 while offset + 1 < bytes.len() {
90 match &bytes[offset..offset + 2] {
91 b"/*" => {
92 depth += 1;
93 offset += 2;
94 }
95 b"*/" => {
96 depth -= 1;
97 if depth == 0 {
98 return Some((&inner[..offset], &inner[offset + 2..]));
99 }
100 offset += 2;
101 }
102 _ => offset += 1,
103 }
104 }
105 Some((inner, ""))
106}
107
108fn normalize_block(inner: &str) -> String {
113 inner
114 .lines()
115 .map(str::trim)
116 .filter(|l| !l.is_empty())
117 .collect::<Vec<_>>()
118 .join("\n")
119}
120
121pub fn line_start_byte_offsets(source: &str) -> Vec<usize> {
126 let mut offsets = vec![0usize];
127 let mut byte = 0usize;
128 for line in source.split_inclusive('\n') {
129 byte += line.len();
130 offsets.push(byte);
131 }
132 offsets
133}
134
135pub fn line_column_to_byte(source: &str, line_starts: &[usize], pos: LineColumn) -> usize {
142 debug_assert!(
143 pos.line >= 1 && pos.line - 1 < line_starts.len(),
144 "pos.line out of range for line_starts"
145 );
146 let line_start = line_starts[pos.line - 1];
147 line_start
148 + source[line_start..]
149 .chars()
150 .take(pos.column)
151 .map(char::len_utf8)
152 .sum::<usize>()
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn comments(pieces: Vec<GapPiece>) -> Vec<Comment> {
160 pieces
161 .into_iter()
162 .filter_map(|p| match p {
163 GapPiece::Comment(c) => Some(c),
164 GapPiece::Punct(_) => None,
165 })
166 .collect()
167 }
168
169 fn puncts(pieces: &[GapPiece]) -> Vec<&'static str> {
170 pieces
171 .iter()
172 .filter_map(|p| match p {
173 GapPiece::Punct(s) => Some(*s),
174 GapPiece::Comment(_) => None,
175 })
176 .collect()
177 }
178
179 #[test]
180 fn empty_gap_yields_just_the_expected_tokens() {
181 let pieces = scan_gap(" ", &["+"]);
182 assert_eq!(puncts(&pieces), vec!["+"]);
183 assert!(comments(pieces).is_empty());
184 }
185
186 #[test]
187 fn a_block_comment_before_the_token_is_recovered_in_order() {
188 let pieces = scan_gap(" /* a */ + ", &["+"]);
189 assert!(matches!(pieces[0], GapPiece::Comment(Comment::Block(ref t)) if t == "a"));
190 assert!(matches!(pieces[1], GapPiece::Punct("+")));
191 }
192
193 #[test]
194 fn a_block_comment_after_the_token_is_recovered_in_order() {
195 let pieces = scan_gap(" + /* b */ ", &["+"]);
196 assert!(matches!(pieces[0], GapPiece::Punct("+")));
197 assert!(matches!(pieces[1], GapPiece::Comment(Comment::Block(ref t)) if t == "b"));
198 }
199
200 #[test]
201 fn comments_on_both_sides_of_the_token_keep_source_order() {
202 let pieces = scan_gap(" /* a */ + /* b */ ", &["+"]);
203 assert!(matches!(pieces[0], GapPiece::Comment(Comment::Block(ref t)) if t == "a"));
204 assert!(matches!(pieces[1], GapPiece::Punct("+")));
205 assert!(matches!(pieces[2], GapPiece::Comment(Comment::Block(ref t)) if t == "b"));
206 }
207
208 #[test]
209 fn two_consecutive_block_comments_are_two_pieces() {
210 let pieces = scan_gap(" /* a */ /* b */ + ", &["+"]);
211 assert_eq!(comments(pieces.clone()).len(), 2);
212 assert!(matches!(pieces[2], GapPiece::Punct("+")));
213 }
214
215 #[test]
217 fn nested_block_comments_preserve_text_and_token_order() {
218 assert_eq!(
219 scan_gap(
220 " /* outer /* α /* deep */ β */ outer */ . /* after /* inner */ end */ ",
221 &["."],
222 ),
223 vec![
224 GapPiece::Comment(Comment::Block(
225 "outer /* α /* deep */ β */ outer".to_string(),
226 )),
227 GapPiece::Punct("."),
228 GapPiece::Comment(Comment::Block("after /* inner */ end".to_string())),
229 ],
230 );
231 }
232
233 #[test]
235 fn block_comment_splitting_preserves_boundaries() {
236 for (source, expected) in [
237 ("", None),
238 ("/", None),
239 ("// line", None),
240 ("/**/.x", Some(("", ".x"))),
241 ("/*/**/*/.x", Some(("/**/", ".x"))),
242 ("/* α /* β */ γ */δ", Some((" α /* β */ γ ", "δ"))),
243 ("/* one *//* two */", Some((" one ", "/* two */"))),
244 ("/*", Some(("", ""))),
245 ("/* α", Some((" α", ""))),
246 ("/* outer /* inner */", Some((" outer /* inner */", ""))),
247 ("/*/*/", Some(("/*/", ""))),
248 ] {
249 assert_eq!(take_block_comment(source), expected, "{source}");
250 }
251 }
252
253 #[test]
254 fn two_consecutive_line_comments_are_two_pieces() {
255 let pieces = scan_gap(" // a\n // b\n + ", &["+"]);
256 assert_eq!(comments(pieces.clone()).len(), 2);
257 assert!(matches!(pieces.last(), Some(GapPiece::Punct("+"))));
258 }
259
260 #[test]
261 fn a_line_comment_is_recovered_as_comment_line() {
262 let pieces = scan_gap(" + // hi\n", &["+"]);
263 assert!(matches!(pieces[0], GapPiece::Punct("+")));
264 assert!(matches!(pieces[1], GapPiece::Comment(Comment::Line(ref t)) if t == "hi"));
265 }
266
267 #[test]
268 fn a_multi_line_block_comment_joins_its_inner_lines() {
269 let pieces = scan_gap(" /*\n one\n two\n*/ + ", &["+"]);
270 assert!(matches!(pieces[0], GapPiece::Comment(Comment::Block(ref t)) if t == "one\ntwo"));
271 }
272
273 #[test]
274 fn multiple_expected_tokens_are_returned_in_order() {
275 let pieces = scan_gap(" ( /* c */ ", &["("]);
277 assert!(matches!(pieces[0], GapPiece::Punct("(")));
278 assert!(matches!(pieces[1], GapPiece::Comment(Comment::Block(ref t)) if t == "c"));
279 }
280
281 #[test]
282 fn missing_expected_token_is_still_synthesized() {
283 let pieces = scan_gap("", &["(", ")"]);
285 assert_eq!(puncts(&pieces), vec!["(", ")"]);
286 }
287
288 #[test]
289 fn line_column_to_byte_maps_a_position_on_the_second_line() {
290 let source = "ab\ncd";
291 let starts = line_start_byte_offsets(source);
292 let pos = LineColumn { line: 2, column: 1 };
294 assert_eq!(line_column_to_byte(source, &starts, pos), 4);
295 }
296}