| use core::slice; | |
| use rustc_lexer::{self as lex, LiteralKind, Token, TokenKind}; | |
| /// A token pattern used for searching and matching by the [`Cursor`]. | |
| /// | |
| /// In the event that a pattern is a multi-token sequence, earlier tokens will be consumed | |
| /// even if the pattern ultimately isn't matched. e.g. With the sequence `:*` matching | |
| /// `DoubleColon` will consume the first `:` and then fail to match, leaving the cursor at | |
| /// the `*`. | |
| #[derive(Clone, Copy)] | |
| pub enum Pat<'a> { | |
| /// Matches any number of comments and doc comments. | |
| AnyComment, | |
| Ident(&'a str), | |
| CaptureDocLines, | |
| CaptureIdent, | |
| LitStr, | |
| CaptureLitStr, | |
| Bang, | |
| CloseBrace, | |
| CloseBracket, | |
| CloseParen, | |
| Comma, | |
| DoubleColon, | |
| Eq, | |
| FatArrow, | |
| Lifetime, | |
| Lt, | |
| Gt, | |
| OpenBrace, | |
| OpenBracket, | |
| OpenParen, | |
| CaptureOptLifetimeArg, | |
| Pound, | |
| Semi, | |
| } | |
| #[derive(Clone, Copy)] | |
| pub struct Capture { | |
| pub pos: u32, | |
| pub len: u32, | |
| } | |
| impl Capture { | |
| pub const EMPTY: Self = Self { pos: 0, len: 0 }; | |
| } | |
| /// A unidirectional cursor over a token stream that is lexed on demand. | |
| pub struct Cursor<'txt> { | |
| next_token: Token, | |
| pos: u32, | |
| inner: lex::Cursor<'txt>, | |
| text: &'txt str, | |
| } | |
| impl<'txt> Cursor<'txt> { | |
| #[must_use] | |
| pub fn new(text: &'txt str) -> Self { | |
| let mut inner = lex::Cursor::new(text, lex::FrontmatterAllowed::Yes); | |
| Self { | |
| next_token: inner.advance_token(), | |
| pos: 0, | |
| inner, | |
| text, | |
| } | |
| } | |
| /// Gets the text of the captured token assuming it came from this cursor. | |
| #[must_use] | |
| pub fn get_text(&self, capture: Capture) -> &'txt str { | |
| &self.text[capture.pos as usize..(capture.pos + capture.len) as usize] | |
| } | |
| /// Gets the text that makes up the next token in the stream, or the empty string if | |
| /// stream is exhausted. | |
| #[must_use] | |
| pub fn peek_text(&self) -> &'txt str { | |
| &self.text[self.pos as usize..(self.pos + self.next_token.len) as usize] | |
| } | |
| /// Gets the length of the next token in bytes, or zero if the stream is exhausted. | |
| #[must_use] | |
| pub fn peek_len(&self) -> u32 { | |
| self.next_token.len | |
| } | |
| /// Gets the next token in the stream, or [`TokenKind::Eof`] if the stream is | |
| /// exhausted. | |
| #[must_use] | |
| pub fn peek(&self) -> TokenKind { | |
| self.next_token.kind | |
| } | |
| /// Gets the offset of the next token in the source string, or the string's length if | |
| /// the stream is exhausted. | |
| #[must_use] | |
| pub fn pos(&self) -> u32 { | |
| self.pos | |
| } | |
| /// Gets whether the cursor has exhausted its input. | |
| #[must_use] | |
| pub fn at_end(&self) -> bool { | |
| self.next_token.kind == TokenKind::Eof | |
| } | |
| /// Advances the cursor to the next token. If the stream is exhausted this will set | |
| /// the next token to [`TokenKind::Eof`]. | |
| pub fn step(&mut self) { | |
| // `next_token.len` is zero for the eof marker. | |
| self.pos += self.next_token.len; | |
| self.next_token = self.inner.advance_token(); | |
| } | |
| /// Consumes tokens until the given pattern is either fully matched of fails to match. | |
| /// Returns whether the pattern was fully matched. | |
| /// | |
| /// For each capture made by the pattern one item will be taken from the capture | |
| /// sequence with the result placed inside. | |
| #[expect(clippy::too_many_lines)] | |
| fn match_impl(&mut self, pat: Pat<'_>, captures: &mut slice::IterMut<'_, Capture>) -> bool { | |
| loop { | |
| match (pat, self.next_token.kind) { | |
| #[rustfmt::skip] // rustfmt bug: https://github.com/rust-lang/rustfmt/issues/6697 | |
| (_, TokenKind::Whitespace) | |
| | ( | |
| Pat::AnyComment, | |
| TokenKind::BlockComment { terminated: true, .. } | TokenKind::LineComment { .. }, | |
| ) => self.step(), | |
| (Pat::Bang, TokenKind::Bang) | |
| | (Pat::CloseBrace, TokenKind::CloseBrace) | |
| | (Pat::CloseBracket, TokenKind::CloseBracket) | |
| | (Pat::CloseParen, TokenKind::CloseParen) | |
| | (Pat::Comma, TokenKind::Comma) | |
| | (Pat::Eq, TokenKind::Eq) | |
| | (Pat::Lifetime, TokenKind::Lifetime { .. }) | |
| | (Pat::Lt, TokenKind::Lt) | |
| | (Pat::Gt, TokenKind::Gt) | |
| | (Pat::OpenBrace, TokenKind::OpenBrace) | |
| | (Pat::OpenBracket, TokenKind::OpenBracket) | |
| | (Pat::OpenParen, TokenKind::OpenParen) | |
| | (Pat::Pound, TokenKind::Pound) | |
| | (Pat::Semi, TokenKind::Semi) | |
| | ( | |
| Pat::LitStr, | |
| TokenKind::Literal { | |
| kind: LiteralKind::Str { terminated: true } | LiteralKind::RawStr { .. }, | |
| .. | |
| }, | |
| ) => { | |
| self.step(); | |
| return true; | |
| }, | |
| (Pat::Ident(x), TokenKind::Ident) if x == self.peek_text() => { | |
| self.step(); | |
| return true; | |
| }, | |
| (Pat::DoubleColon, TokenKind::Colon) => { | |
| self.step(); | |
| if matches!(self.next_token.kind, TokenKind::Colon) { | |
| self.step(); | |
| return true; | |
| } | |
| return false; | |
| }, | |
| (Pat::FatArrow, TokenKind::Eq) => { | |
| self.step(); | |
| if matches!(self.next_token.kind, TokenKind::Gt) { | |
| self.step(); | |
| return true; | |
| } | |
| return false; | |
| }, | |
| (Pat::CaptureOptLifetimeArg, TokenKind::Lt) => { | |
| self.step(); | |
| loop { | |
| match self.next_token.kind { | |
| TokenKind::Lifetime { .. } => break, | |
| TokenKind::Whitespace => self.step(), | |
| _ => return false, | |
| } | |
| } | |
| *captures.next().unwrap() = Capture { | |
| pos: self.pos, | |
| len: self.next_token.len, | |
| }; | |
| self.step(); | |
| loop { | |
| match self.next_token.kind { | |
| TokenKind::Gt => break, | |
| TokenKind::Whitespace => self.step(), | |
| _ => return false, | |
| } | |
| } | |
| self.step(); | |
| return true; | |
| }, | |
| (Pat::CaptureOptLifetimeArg, _) => { | |
| *captures.next().unwrap() = Capture { pos: 0, len: 0 }; | |
| return true; | |
| }, | |
| #[rustfmt::skip] | |
| ( | |
| Pat::CaptureLitStr, | |
| TokenKind::Literal { | |
| kind: | |
| LiteralKind::Str { terminated: true } | |
| | LiteralKind::RawStr { n_hashes: Some(_) }, | |
| .. | |
| }, | |
| ) | |
| | (Pat::CaptureIdent, TokenKind::Ident) => { | |
| *captures.next().unwrap() = Capture { pos: self.pos, len: self.next_token.len }; | |
| self.step(); | |
| return true; | |
| }, | |
| (Pat::CaptureDocLines, TokenKind::LineComment { doc_style: Some(_) }) => { | |
| let pos = self.pos; | |
| loop { | |
| self.step(); | |
| if !matches!( | |
| self.next_token.kind, | |
| TokenKind::Whitespace | TokenKind::LineComment { doc_style: Some(_) } | |
| ) { | |
| break; | |
| } | |
| } | |
| *captures.next().unwrap() = Capture { | |
| pos, | |
| len: self.pos - pos, | |
| }; | |
| return true; | |
| }, | |
| (Pat::CaptureDocLines, _) => { | |
| *captures.next().unwrap() = Capture::EMPTY; | |
| return true; | |
| }, | |
| (Pat::AnyComment, _) => return true, | |
| _ => return false, | |
| } | |
| } | |
| } | |
| /// Consumes all tokens until the specified identifier is found and returns its | |
| /// position. Returns `None` if the identifier could not be found. | |
| /// | |
| /// The cursor will be positioned immediately after the identifier, or at the end if | |
| /// it is not. | |
| pub fn find_ident(&mut self, ident: &str) -> Option<u32> { | |
| loop { | |
| match self.next_token.kind { | |
| TokenKind::Ident if self.peek_text() == ident => { | |
| let pos = self.pos; | |
| self.step(); | |
| return Some(pos); | |
| }, | |
| TokenKind::Eof => return None, | |
| _ => self.step(), | |
| } | |
| } | |
| } | |
| /// Consumes all tokens until the next identifier is found and captures it. Returns | |
| /// `None` if no identifier could be found. | |
| /// | |
| /// The cursor will be positioned immediately after the identifier, or at the end if | |
| /// it is not. | |
| pub fn find_any_ident(&mut self) -> Option<Capture> { | |
| loop { | |
| match self.next_token.kind { | |
| TokenKind::Ident => { | |
| let res = Capture { | |
| pos: self.pos, | |
| len: self.next_token.len, | |
| }; | |
| self.step(); | |
| return Some(res); | |
| }, | |
| TokenKind::Eof => return None, | |
| _ => self.step(), | |
| } | |
| } | |
| } | |
| /// Consume the returns the position of the next non-whitespace token if it's the | |
| /// specified identifier. Returns `None` otherwise. | |
| pub fn match_ident(&mut self, s: &str) -> Option<u32> { | |
| loop { | |
| match self.next_token.kind { | |
| TokenKind::Ident if s == self.peek_text() => { | |
| let pos = self.pos; | |
| self.step(); | |
| return Some(pos); | |
| }, | |
| TokenKind::Whitespace => self.step(), | |
| _ => return None, | |
| } | |
| } | |
| } | |
| /// Consumes and captures the next non-whitespace token if it's an identifier. Returns | |
| /// `None` otherwise. | |
| pub fn capture_ident(&mut self) -> Option<Capture> { | |
| loop { | |
| match self.next_token.kind { | |
| TokenKind::Ident => { | |
| let pos = self.pos; | |
| let len = self.next_token.len; | |
| self.step(); | |
| return Some(Capture { pos, len }); | |
| }, | |
| TokenKind::Whitespace => self.step(), | |
| _ => return None, | |
| } | |
| } | |
| } | |
| /// Continually attempt to match the pattern on subsequent tokens until a match is | |
| /// found. Returns whether the pattern was successfully matched. | |
| /// | |
| /// Not generally suitable for multi-token patterns or patterns that can match | |
| /// nothing. | |
| #[must_use] | |
| pub fn find_pat(&mut self, pat: Pat<'_>) -> bool { | |
| let mut capture = [].iter_mut(); | |
| while !self.match_impl(pat, &mut capture) { | |
| self.step(); | |
| if self.at_end() { | |
| return false; | |
| } | |
| } | |
| true | |
| } | |
| /// Attempts to match a sequence of patterns at the current position. Returns whether | |
| /// all patterns were successfully matched. | |
| /// | |
| /// Captures will be written to the given slice in the order they're matched. If a | |
| /// capture is matched, but there are no more capture slots this will panic. If the | |
| /// match is completed without filling all the capture slots they will be left | |
| /// unmodified. | |
| /// | |
| /// If the match fails the cursor will be positioned at the first failing token. | |
| #[must_use] | |
| pub fn match_all(&mut self, pats: &[Pat<'_>], captures: &mut [Capture]) -> bool { | |
| let mut captures = captures.iter_mut(); | |
| pats.iter().all(|&p| self.match_impl(p, &mut captures)) | |
| } | |
| /// Attempts to match a single pattern at the current position. Returns whether the | |
| /// pattern was successfully matched. | |
| /// | |
| /// If the pattern attempts to capture anything this will panic. If the match fails | |
| /// the cursor will be positioned at the first failing token. | |
| #[must_use] | |
| pub fn match_pat(&mut self, pat: Pat<'_>) -> bool { | |
| self.match_impl(pat, &mut [].iter_mut()) | |
| } | |
| } |