diff options
author | Leander Tentrup <[email protected]> | 2020-04-17 08:37:18 +0100 |
---|---|---|
committer | Leander Tentrup <[email protected]> | 2020-04-20 10:19:15 +0100 |
commit | ac798e1f7cfbc6d27c87bb28e3f1d5b6801796aa (patch) | |
tree | 958ccd22ae5e75047b8189f69cf5878eeb9611ea /crates/ra_syntax/src/ast | |
parent | 29a846464b63259f5152d61a5520bffcc2cb8703 (diff) |
Implement syntax highlighting for format strings
Detailed changes:
1) Implement a lexer for string literals that divides the string in format specifier `{}` including the format specifier modifier.
2) Adapt syntax highlighting to add ranges for the detected sequences.
3) Add a test case for the format string syntax highlighting.
Diffstat (limited to 'crates/ra_syntax/src/ast')
-rw-r--r-- | crates/ra_syntax/src/ast/tokens.rs | 324 |
1 files changed, 324 insertions, 0 deletions
diff --git a/crates/ra_syntax/src/ast/tokens.rs b/crates/ra_syntax/src/ast/tokens.rs index e8320b57e..ec3b4e553 100644 --- a/crates/ra_syntax/src/ast/tokens.rs +++ b/crates/ra_syntax/src/ast/tokens.rs | |||
@@ -172,3 +172,327 @@ impl RawString { | |||
172 | Some(range + contents_range.start()) | 172 | Some(range + contents_range.start()) |
173 | } | 173 | } |
174 | } | 174 | } |
175 | |||
176 | #[derive(Debug)] | ||
177 | pub enum FormatSpecifier { | ||
178 | Open, | ||
179 | Close, | ||
180 | Integer, | ||
181 | Identifier, | ||
182 | Colon, | ||
183 | Fill, | ||
184 | Align, | ||
185 | Sign, | ||
186 | NumberSign, | ||
187 | Zero, | ||
188 | DollarSign, | ||
189 | Dot, | ||
190 | Asterisk, | ||
191 | QuestionMark, | ||
192 | } | ||
193 | |||
194 | pub trait HasFormatSpecifier: AstToken { | ||
195 | fn lex_format_specifier<F>(&self, callback: &mut F) | ||
196 | where | ||
197 | F: FnMut(TextRange, FormatSpecifier), | ||
198 | { | ||
199 | let src = self.text().as_str(); | ||
200 | let initial_len = src.len(); | ||
201 | let mut chars = src.chars(); | ||
202 | |||
203 | while let Some(first_char) = chars.next() { | ||
204 | match first_char { | ||
205 | '{' => { | ||
206 | // Format specifier, see syntax at https://doc.rust-lang.org/std/fmt/index.html#syntax | ||
207 | if chars.clone().next() == Some('{') { | ||
208 | // Escaped format specifier, `{{` | ||
209 | chars.next(); | ||
210 | continue; | ||
211 | } | ||
212 | |||
213 | let start = initial_len - chars.as_str().len() - first_char.len_utf8(); | ||
214 | let end = initial_len - chars.as_str().len(); | ||
215 | callback( | ||
216 | TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), | ||
217 | FormatSpecifier::Open, | ||
218 | ); | ||
219 | |||
220 | let next_char = if let Some(c) = chars.clone().next() { | ||
221 | c | ||
222 | } else { | ||
223 | break; | ||
224 | }; | ||
225 | |||
226 | // check for integer/identifier | ||
227 | match next_char { | ||
228 | '0'..='9' => { | ||
229 | // integer | ||
230 | read_integer(&mut chars, initial_len, callback); | ||
231 | } | ||
232 | 'a'..='z' | 'A'..='Z' | '_' => { | ||
233 | // identifier | ||
234 | read_identifier(&mut chars, initial_len, callback); | ||
235 | } | ||
236 | _ => {} | ||
237 | } | ||
238 | |||
239 | if chars.clone().next() == Some(':') { | ||
240 | skip_char_and_emit( | ||
241 | &mut chars, | ||
242 | initial_len, | ||
243 | FormatSpecifier::Colon, | ||
244 | callback, | ||
245 | ); | ||
246 | |||
247 | // check for fill/align | ||
248 | let mut cloned = chars.clone().take(2); | ||
249 | let first = cloned.next().unwrap_or_default(); | ||
250 | let second = cloned.next().unwrap_or_default(); | ||
251 | match second { | ||
252 | '<' | '^' | '>' => { | ||
253 | // alignment specifier, first char specifies fillment | ||
254 | skip_char_and_emit( | ||
255 | &mut chars, | ||
256 | initial_len, | ||
257 | FormatSpecifier::Fill, | ||
258 | callback, | ||
259 | ); | ||
260 | skip_char_and_emit( | ||
261 | &mut chars, | ||
262 | initial_len, | ||
263 | FormatSpecifier::Align, | ||
264 | callback, | ||
265 | ); | ||
266 | } | ||
267 | _ => match first { | ||
268 | '<' | '^' | '>' => { | ||
269 | skip_char_and_emit( | ||
270 | &mut chars, | ||
271 | initial_len, | ||
272 | FormatSpecifier::Align, | ||
273 | callback, | ||
274 | ); | ||
275 | } | ||
276 | _ => {} | ||
277 | }, | ||
278 | } | ||
279 | |||
280 | // check for sign | ||
281 | match chars.clone().next().unwrap_or_default() { | ||
282 | '+' | '-' => { | ||
283 | skip_char_and_emit( | ||
284 | &mut chars, | ||
285 | initial_len, | ||
286 | FormatSpecifier::Sign, | ||
287 | callback, | ||
288 | ); | ||
289 | } | ||
290 | _ => {} | ||
291 | } | ||
292 | |||
293 | // check for `#` | ||
294 | if let Some('#') = chars.clone().next() { | ||
295 | skip_char_and_emit( | ||
296 | &mut chars, | ||
297 | initial_len, | ||
298 | FormatSpecifier::NumberSign, | ||
299 | callback, | ||
300 | ); | ||
301 | } | ||
302 | |||
303 | // check for `0` | ||
304 | let mut cloned = chars.clone().take(2); | ||
305 | let first = cloned.next(); | ||
306 | let second = cloned.next(); | ||
307 | |||
308 | if first == Some('0') && second != Some('$') { | ||
309 | skip_char_and_emit( | ||
310 | &mut chars, | ||
311 | initial_len, | ||
312 | FormatSpecifier::Zero, | ||
313 | callback, | ||
314 | ); | ||
315 | } | ||
316 | |||
317 | // width | ||
318 | match chars.clone().next().unwrap_or_default() { | ||
319 | '0'..='9' => { | ||
320 | read_integer(&mut chars, initial_len, callback); | ||
321 | if chars.clone().next() == Some('$') { | ||
322 | skip_char_and_emit( | ||
323 | &mut chars, | ||
324 | initial_len, | ||
325 | FormatSpecifier::DollarSign, | ||
326 | callback, | ||
327 | ); | ||
328 | } | ||
329 | } | ||
330 | 'a'..='z' | 'A'..='Z' | '_' => { | ||
331 | read_identifier(&mut chars, initial_len, callback); | ||
332 | if chars.clone().next() != Some('$') { | ||
333 | continue; | ||
334 | } | ||
335 | skip_char_and_emit( | ||
336 | &mut chars, | ||
337 | initial_len, | ||
338 | FormatSpecifier::DollarSign, | ||
339 | callback, | ||
340 | ); | ||
341 | } | ||
342 | _ => {} | ||
343 | } | ||
344 | |||
345 | // precision | ||
346 | if chars.clone().next() == Some('.') { | ||
347 | skip_char_and_emit( | ||
348 | &mut chars, | ||
349 | initial_len, | ||
350 | FormatSpecifier::Dot, | ||
351 | callback, | ||
352 | ); | ||
353 | |||
354 | match chars.clone().next().unwrap_or_default() { | ||
355 | '*' => { | ||
356 | skip_char_and_emit( | ||
357 | &mut chars, | ||
358 | initial_len, | ||
359 | FormatSpecifier::Asterisk, | ||
360 | callback, | ||
361 | ); | ||
362 | } | ||
363 | '0'..='9' => { | ||
364 | read_integer(&mut chars, initial_len, callback); | ||
365 | if chars.clone().next() == Some('$') { | ||
366 | skip_char_and_emit( | ||
367 | &mut chars, | ||
368 | initial_len, | ||
369 | FormatSpecifier::DollarSign, | ||
370 | callback, | ||
371 | ); | ||
372 | } | ||
373 | } | ||
374 | 'a'..='z' | 'A'..='Z' | '_' => { | ||
375 | read_identifier(&mut chars, initial_len, callback); | ||
376 | if chars.clone().next() != Some('$') { | ||
377 | continue; | ||
378 | } | ||
379 | skip_char_and_emit( | ||
380 | &mut chars, | ||
381 | initial_len, | ||
382 | FormatSpecifier::DollarSign, | ||
383 | callback, | ||
384 | ); | ||
385 | } | ||
386 | _ => { | ||
387 | continue; | ||
388 | } | ||
389 | } | ||
390 | } | ||
391 | |||
392 | // type | ||
393 | match chars.clone().next().unwrap_or_default() { | ||
394 | '?' => { | ||
395 | skip_char_and_emit( | ||
396 | &mut chars, | ||
397 | initial_len, | ||
398 | FormatSpecifier::QuestionMark, | ||
399 | callback, | ||
400 | ); | ||
401 | } | ||
402 | 'a'..='z' | 'A'..='Z' | '_' => { | ||
403 | read_identifier(&mut chars, initial_len, callback); | ||
404 | } | ||
405 | _ => {} | ||
406 | } | ||
407 | } | ||
408 | |||
409 | let mut cloned = chars.clone().take(2); | ||
410 | let first = cloned.next(); | ||
411 | let second = cloned.next(); | ||
412 | if first != Some('}') { | ||
413 | continue; | ||
414 | } | ||
415 | if second == Some('}') { | ||
416 | // Escaped format end specifier, `}}` | ||
417 | continue; | ||
418 | } | ||
419 | skip_char_and_emit(&mut chars, initial_len, FormatSpecifier::Close, callback); | ||
420 | } | ||
421 | _ => { | ||
422 | while let Some(next_char) = chars.clone().next() { | ||
423 | match next_char { | ||
424 | '{' => break, | ||
425 | _ => {} | ||
426 | } | ||
427 | chars.next(); | ||
428 | } | ||
429 | } | ||
430 | }; | ||
431 | } | ||
432 | |||
433 | fn skip_char_and_emit<F>( | ||
434 | chars: &mut std::str::Chars, | ||
435 | initial_len: usize, | ||
436 | emit: FormatSpecifier, | ||
437 | callback: &mut F, | ||
438 | ) where | ||
439 | F: FnMut(TextRange, FormatSpecifier), | ||
440 | { | ||
441 | let start = initial_len - chars.as_str().len(); | ||
442 | chars.next(); | ||
443 | let end = initial_len - chars.as_str().len(); | ||
444 | callback( | ||
445 | TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), | ||
446 | emit, | ||
447 | ); | ||
448 | } | ||
449 | |||
450 | fn read_integer<F>(chars: &mut std::str::Chars, initial_len: usize, callback: &mut F) | ||
451 | where | ||
452 | F: FnMut(TextRange, FormatSpecifier), | ||
453 | { | ||
454 | let start = initial_len - chars.as_str().len(); | ||
455 | chars.next(); | ||
456 | while let Some(next_char) = chars.clone().next() { | ||
457 | match next_char { | ||
458 | '0'..='9' => { | ||
459 | chars.next(); | ||
460 | } | ||
461 | _ => { | ||
462 | break; | ||
463 | } | ||
464 | } | ||
465 | } | ||
466 | let end = initial_len - chars.as_str().len(); | ||
467 | callback( | ||
468 | TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), | ||
469 | FormatSpecifier::Integer, | ||
470 | ); | ||
471 | } | ||
472 | fn read_identifier<F>(chars: &mut std::str::Chars, initial_len: usize, callback: &mut F) | ||
473 | where | ||
474 | F: FnMut(TextRange, FormatSpecifier), | ||
475 | { | ||
476 | let start = initial_len - chars.as_str().len(); | ||
477 | chars.next(); | ||
478 | while let Some(next_char) = chars.clone().next() { | ||
479 | match next_char { | ||
480 | 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => { | ||
481 | chars.next(); | ||
482 | } | ||
483 | _ => { | ||
484 | break; | ||
485 | } | ||
486 | } | ||
487 | } | ||
488 | let end = initial_len - chars.as_str().len(); | ||
489 | callback( | ||
490 | TextRange::from_to(TextUnit::from_usize(start), TextUnit::from_usize(end)), | ||
491 | FormatSpecifier::Identifier, | ||
492 | ); | ||
493 | } | ||
494 | } | ||
495 | } | ||
496 | |||
497 | impl HasFormatSpecifier for String {} | ||
498 | impl HasFormatSpecifier for RawString {} | ||