summaryrefslogtreecommitdiff
path: root/support/texlab/src/syntax/bibtex
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/syntax/bibtex')
-rw-r--r--support/texlab/src/syntax/bibtex/ast.rs431
-rw-r--r--support/texlab/src/syntax/bibtex/cst.rs212
-rw-r--r--support/texlab/src/syntax/bibtex/formatter.rs329
-rw-r--r--support/texlab/src/syntax/bibtex/kind.rs51
-rw-r--r--support/texlab/src/syntax/bibtex/lexer.rs215
-rw-r--r--support/texlab/src/syntax/bibtex/mod.rs396
-rw-r--r--support/texlab/src/syntax/bibtex/parser.rs706
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__empty.snap7
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_complete_parens.snap40
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields.snap13
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields_missing_comma.snap12
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field.snap27
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_number_key.snap27
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_trailing_comma.snap28
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields.snap42
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields_incomplete.snap33
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__junk.snap11
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_casing.snap21
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_complete.snap21
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_missing_end.snap20
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_casing.snap23
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_complete.snap23
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_concatenation.snap43
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_incomplete.snap23
-rw-r--r--support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_missing_quote.snap23
25 files changed, 1121 insertions, 1656 deletions
diff --git a/support/texlab/src/syntax/bibtex/ast.rs b/support/texlab/src/syntax/bibtex/ast.rs
deleted file mode 100644
index 77ee98e26d..0000000000
--- a/support/texlab/src/syntax/bibtex/ast.rs
+++ /dev/null
@@ -1,431 +0,0 @@
-use crate::{
- protocol::{Position, Range, RangeExt},
- syntax::{Span, SyntaxNode},
-};
-use itertools::Itertools;
-use petgraph::graph::{Graph, NodeIndex};
-use serde::{Deserialize, Serialize};
-use std::fmt;
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-pub enum TokenKind {
- PreambleKind,
- StringKind,
- EntryKind,
- Word,
- Command,
- Assign,
- Comma,
- Concat,
- Quote,
- BeginBrace,
- EndBrace,
- BeginParen,
- EndParen,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Token {
- pub span: Span,
- pub kind: TokenKind,
-}
-
-impl SyntaxNode for Token {
- fn range(&self) -> Range {
- self.span.range()
- }
-}
-
-impl Token {
- pub fn new(span: Span, kind: TokenKind) -> Self {
- Self { span, kind }
- }
-
- pub fn text(&self) -> &str {
- &self.span.text
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
-pub struct Root {
- pub range: Range,
-}
-
-impl SyntaxNode for Root {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for Root {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Root")
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Comment {
- pub token: Token,
-}
-
-impl SyntaxNode for Comment {
- fn range(&self) -> Range {
- self.token.range()
- }
-}
-
-impl fmt::Debug for Comment {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Comment({})", self.token.text())
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Preamble {
- pub range: Range,
- pub ty: Token,
- pub left: Option<Token>,
- pub right: Option<Token>,
-}
-
-impl SyntaxNode for Preamble {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for Preamble {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Preamble")
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct String {
- pub range: Range,
- pub ty: Token,
- pub left: Option<Token>,
- pub name: Option<Token>,
- pub assign: Option<Token>,
- pub right: Option<Token>,
-}
-
-impl SyntaxNode for String {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for String {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "String({:?})", self.name.as_ref().map(Token::text))
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Entry {
- pub range: Range,
- pub ty: Token,
- pub left: Option<Token>,
- pub key: Option<Token>,
- pub comma: Option<Token>,
- pub right: Option<Token>,
-}
-
-impl SyntaxNode for Entry {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for Entry {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Entry({:?})", self.key.as_ref().map(Token::text))
- }
-}
-
-impl Entry {
- pub fn is_comment(&self) -> bool {
- self.ty.text().to_lowercase() == "@comment"
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Field {
- pub range: Range,
- pub name: Token,
- pub assign: Option<Token>,
- pub comma: Option<Token>,
-}
-
-impl SyntaxNode for Field {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for Field {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Field({})", self.name.text())
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Word {
- pub token: Token,
-}
-
-impl SyntaxNode for Word {
- fn range(&self) -> Range {
- self.token.range()
- }
-}
-
-impl fmt::Debug for Word {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Word({})", self.token.text())
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Command {
- pub token: Token,
-}
-
-impl SyntaxNode for Command {
- fn range(&self) -> Range {
- self.token.range()
- }
-}
-
-impl fmt::Debug for Command {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Command({})", self.token.text())
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct QuotedContent {
- pub range: Range,
- pub left: Token,
- pub right: Option<Token>,
-}
-
-impl SyntaxNode for QuotedContent {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for QuotedContent {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "QuotedContent")
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct BracedContent {
- pub range: Range,
- pub left: Token,
- pub right: Option<Token>,
-}
-
-impl SyntaxNode for BracedContent {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for BracedContent {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "BracedContent")
- }
-}
-
-#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct Concat {
- pub range: Range,
- pub operator: Token,
-}
-
-impl SyntaxNode for Concat {
- fn range(&self) -> Range {
- self.range
- }
-}
-
-impl fmt::Debug for Concat {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Concat")
- }
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub enum Node {
- Root(Root),
- Comment(Comment),
- Preamble(Box<Preamble>),
- String(Box<String>),
- Entry(Box<Entry>),
- Field(Box<Field>),
- Word(Word),
- Command(Command),
- QuotedContent(QuotedContent),
- BracedContent(BracedContent),
- Concat(Concat),
-}
-
-impl SyntaxNode for Node {
- fn range(&self) -> Range {
- match self {
- Self::Root(root) => root.range(),
- Self::Comment(comment) => comment.range(),
- Self::Preamble(preamble) => preamble.range(),
- Self::String(string) => string.range(),
- Self::Entry(entry) => entry.range(),
- Self::Field(field) => field.range(),
- Self::Word(word) => word.range(),
- Self::Command(cmd) => cmd.range(),
- Self::QuotedContent(content) => content.range(),
- Self::BracedContent(content) => content.range(),
- Self::Concat(concat) => concat.range(),
- }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Tree {
- pub graph: Graph<Node, ()>,
- pub root: NodeIndex,
-}
-
-impl Tree {
- pub fn children(&self, parent: NodeIndex) -> impl Iterator<Item = NodeIndex> {
- self.graph
- .neighbors(parent)
- .sorted_by_key(|child| self.graph[*child].start())
- }
-
- pub fn has_children(&self, parent: NodeIndex) -> bool {
- self.children(parent).next().is_some()
- }
-
- pub fn walk<'a, V: Visitor<'a>>(&'a self, visitor: &mut V, parent: NodeIndex) {
- for child in self.children(parent) {
- visitor.visit(self, child);
- }
- }
-
- pub fn find(&self, pos: Position) -> Vec<NodeIndex> {
- let mut finder = Finder::new(pos);
- finder.visit(self, self.root);
- finder.results
- }
-
- pub fn as_preamble(&self, node: NodeIndex) -> Option<&Preamble> {
- if let Node::Preamble(preamble) = &self.graph[node] {
- Some(preamble)
- } else {
- None
- }
- }
-
- pub fn as_string(&self, node: NodeIndex) -> Option<&String> {
- if let Node::String(string) = &self.graph[node] {
- Some(string)
- } else {
- None
- }
- }
-
- pub fn as_entry(&self, node: NodeIndex) -> Option<&Entry> {
- if let Node::Entry(entry) = &self.graph[node] {
- Some(entry)
- } else {
- None
- }
- }
-
- pub fn as_field(&self, node: NodeIndex) -> Option<&Field> {
- if let Node::Field(field) = &self.graph[node] {
- Some(field)
- } else {
- None
- }
- }
-
- pub fn as_command(&self, node: NodeIndex) -> Option<&Command> {
- if let Node::Command(cmd) = &self.graph[node] {
- Some(cmd)
- } else {
- None
- }
- }
-
- pub fn as_word(&self, node: NodeIndex) -> Option<&Word> {
- if let Node::Word(word) = &self.graph[node] {
- Some(word)
- } else {
- None
- }
- }
-
- pub fn entry_by_key(&self, key: &str) -> Option<NodeIndex> {
- for node in self.children(self.root) {
- if let Some(entry) = self.as_entry(node) {
- if entry.key.as_ref().map(Token::text) == Some(key) {
- return Some(node);
- }
- }
- }
- None
- }
-
- pub fn field_by_name(&self, parent: NodeIndex, name: &str) -> Option<NodeIndex> {
- let name = name.to_lowercase();
- self.as_entry(parent)?;
- for node in self.children(parent) {
- if let Some(field) = self.as_field(node) {
- if field.name.text() == name {
- return Some(node);
- }
- }
- }
- None
- }
-
- pub fn crossref(&self, entry: NodeIndex) -> Option<NodeIndex> {
- let field = self.field_by_name(entry, "crossref")?;
- let content = self.children(field).next()?;
- let key = self.as_word(self.children(content).next()?)?;
- self.entry_by_key(key.token.text())
- }
-}
-
-pub trait Visitor<'a> {
- fn visit(&mut self, tree: &'a Tree, node: NodeIndex);
-}
-
-#[derive(Debug)]
-struct Finder {
- position: Position,
- results: Vec<NodeIndex>,
-}
-
-impl Finder {
- fn new(position: Position) -> Self {
- Self {
- position,
- results: Vec::new(),
- }
- }
-}
-
-impl<'a> Visitor<'a> for Finder {
- fn visit(&mut self, tree: &'a Tree, node: NodeIndex) {
- if tree.graph[node].range().contains(self.position) {
- self.results.push(node);
- tree.walk(self, node);
- }
- }
-}
diff --git a/support/texlab/src/syntax/bibtex/cst.rs b/support/texlab/src/syntax/bibtex/cst.rs
new file mode 100644
index 0000000000..7f824681dc
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/cst.rs
@@ -0,0 +1,212 @@
+use cstree::TextRange;
+
+use crate::syntax::CstNode;
+
+use super::{Language, SyntaxKind::*, SyntaxNode, SyntaxToken};
+
+macro_rules! cst_node {
+ ($name:ident, $($kind:pat),+) => {
+ #[derive(Clone)]
+ #[repr(transparent)]
+ pub struct $name<'a>(&'a SyntaxNode);
+
+ impl<'a> CstNode<'a> for $name<'a> {
+ type Lang = Language;
+
+ fn cast(node: &'a cstree::ResolvedNode<Self::Lang>) -> Option<Self>
+ where
+ Self: Sized,
+ {
+ match node.kind() {
+ $($kind => Some(Self(node)),)+
+ _ => None,
+ }
+ }
+
+ fn syntax(&self) -> &'a cstree::ResolvedNode<Self::Lang> {
+ &self.0
+ }
+
+ fn small_range(&self) -> TextRange {
+ let full_range = self.syntax().text_range();
+ let start = full_range.start();
+ let mut token = self.syntax().last_token();
+ while let Some(current) = token {
+ if !matches!(current.kind(), WHITESPACE | JUNK) {
+ return TextRange::new(start, current.text_range().end());
+ }
+ token = current.prev_token();
+ }
+ TextRange::new(start, start)
+ }
+ }
+ };
+}
+
+pub trait HasCurly<'a>: CstNode<'a, Lang = Language> {
+ fn left_curly(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == L_CURLY.into())
+ }
+
+ fn right_curly(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == R_CURLY.into())
+ }
+}
+
+pub trait HasQuotes<'a>: CstNode<'a, Lang = Language> {
+ fn left_quote(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == QUOTE.into())
+ }
+
+ fn right_quote(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .filter(|node| node.kind() == QUOTE.into())
+ .nth(1)
+ }
+}
+
+pub trait HasDelimiters<'a>: CstNode<'a, Lang = Language> {
+ fn left_delimiter(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| matches!(node.kind(), L_CURLY | L_PAREN))
+ }
+
+ fn right_delimiter(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| matches!(node.kind(), R_CURLY | R_PAREN))
+ }
+}
+
+pub trait HasType<'a>: CstNode<'a, Lang = Language> {
+ fn ty(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| {
+ matches!(
+ node.kind(),
+ PREAMBLE_TYPE | STRING_TYPE | COMMENT_TYPE | ENTRY_TYPE
+ )
+ })
+ }
+}
+
+cst_node!(Root, ROOT);
+
+cst_node!(Junk, JUNK);
+
+cst_node!(Comment, COMMENT);
+
+impl<'a> HasType<'a> for Comment<'a> {}
+
+cst_node!(Preamble, PREAMBLE);
+
+impl<'a> HasType<'a> for Preamble<'a> {}
+
+impl<'a> HasDelimiters<'a> for Preamble<'a> {}
+
+impl<'a> Preamble<'a> {
+ pub fn value(&self) -> Option<Value<'a>> {
+ self.syntax().children().find_map(Value::cast)
+ }
+}
+
+cst_node!(String, STRING);
+
+impl<'a> HasType<'a> for String<'a> {}
+
+impl<'a> HasDelimiters<'a> for String<'a> {}
+
+impl<'a> String<'a> {
+ pub fn name(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == WORD)
+ }
+
+ pub fn equality_sign(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == EQUALITY_SIGN)
+ }
+
+ pub fn value(&self) -> Option<Value<'a>> {
+ self.syntax().children().find_map(Value::cast)
+ }
+}
+
+cst_node!(Entry, ENTRY);
+
+impl<'a> HasType<'a> for Entry<'a> {}
+
+impl<'a> HasDelimiters<'a> for Entry<'a> {}
+
+impl<'a> Entry<'a> {
+ pub fn key(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == WORD)
+ }
+
+ pub fn fields(&self) -> impl Iterator<Item = Field<'a>> {
+ self.syntax().children().filter_map(Field::cast)
+ }
+}
+
+cst_node!(Field, FIELD);
+
+impl<'a> Field<'a> {
+ pub fn name(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == WORD)
+ }
+
+ pub fn equality_sign(&self) -> Option<&'a SyntaxToken> {
+ self.syntax()
+ .children_with_tokens()
+ .filter_map(|node| node.into_token())
+ .find(|node| node.kind() == EQUALITY_SIGN)
+ }
+
+ pub fn value(&self) -> Option<Value<'a>> {
+ self.syntax().children().find_map(Value::cast)
+ }
+}
+
+cst_node!(Value, VALUE);
+
+impl<'a> Value<'a> {
+ pub fn tokens(&self) -> impl Iterator<Item = Token<'a>> {
+ self.syntax().children().filter_map(Token::cast)
+ }
+}
+
+cst_node!(Token, TOKEN);
+
+cst_node!(BraceGroup, BRACE_GROUP);
+
+impl<'a> HasCurly<'a> for BraceGroup<'a> {}
+
+cst_node!(QuoteGroup, QUOTE_GROUP);
+
+impl<'a> HasQuotes<'a> for QuoteGroup<'a> {}
diff --git a/support/texlab/src/syntax/bibtex/formatter.rs b/support/texlab/src/syntax/bibtex/formatter.rs
deleted file mode 100644
index d4965e7cd4..0000000000
--- a/support/texlab/src/syntax/bibtex/formatter.rs
+++ /dev/null
@@ -1,329 +0,0 @@
-use super::ast::*;
-use crate::{protocol::BibtexFormattingOptions, syntax::text::SyntaxNode};
-use petgraph::graph::NodeIndex;
-use std::{i32, string::String as StdString};
-
-#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub struct FormattingParams<'a> {
- pub tab_size: usize,
- pub insert_spaces: bool,
- pub options: &'a BibtexFormattingOptions,
-}
-
-impl<'a> FormattingParams<'a> {
- fn line_length(self) -> i32 {
- let line_length = self.options.line_length.unwrap_or(120);
- if line_length <= 0 {
- i32::MAX
- } else {
- line_length
- }
- }
-
- fn indent(self) -> StdString {
- if self.insert_spaces {
- let mut buffer = StdString::new();
- for _ in 0..self.tab_size {
- buffer.push(' ');
- }
- buffer
- } else {
- "\t".into()
- }
- }
-}
-
-#[derive(Debug, Clone)]
-struct Formatter<'a> {
- params: FormattingParams<'a>,
- indent: StdString,
- output: StdString,
- align: Vec<usize>,
-}
-
-impl<'a> Formatter<'a> {
- fn new(params: FormattingParams<'a>) -> Self {
- Self {
- params,
- indent: params.indent(),
- output: StdString::new(),
- align: Vec::new(),
- }
- }
-
- fn visit_token_lowercase(&mut self, token: &Token) {
- self.output.push_str(token.text().to_lowercase().as_ref());
- }
-
- fn should_insert_space(previous: &Token, current: &Token) -> bool {
- previous.start().line != current.start().line
- || previous.end().character < current.start().character
- }
-}
-
-impl<'a, 'b> Visitor<'b> for Formatter<'a> {
- fn visit(&mut self, tree: &'b Tree, node: NodeIndex) {
- match &tree.graph[node] {
- Node::Root(_) => tree.walk(self, node),
- Node::Comment(comment) => self.output.push_str(comment.token.text()),
- Node::Preamble(preamble) => {
- self.visit_token_lowercase(&preamble.ty);
- self.output.push('{');
- if tree.has_children(node) {
- self.align.push(self.output.chars().count());
- tree.walk(self, node);
- self.output.push('}');
- }
- }
- Node::String(string) => {
- self.visit_token_lowercase(&string.ty);
- self.output.push('{');
- if let Some(name) = &string.name {
- self.output.push_str(name.text());
- self.output.push_str(" = ");
- if tree.has_children(node) {
- self.align.push(self.output.chars().count());
- tree.walk(self, node);
- self.output.push('}');
- }
- }
- }
- Node::Entry(entry) => {
- self.visit_token_lowercase(&entry.ty);
- self.output.push('{');
- if let Some(key) = &entry.key {
- self.output.push_str(key.text());
- self.output.push(',');
- self.output.push('\n');
- tree.walk(self, node);
- self.output.push('}');
- }
- }
- Node::Field(field) => {
- self.output.push_str(&self.indent);
- self.visit_token_lowercase(&field.name);
- self.output.push_str(" = ");
- if tree.has_children(node) {
- let count = field.name.text().chars().count();
- self.align.push(self.params.tab_size as usize + count + 3);
- tree.walk(self, node);
- self.output.push(',');
- self.output.push('\n');
- }
- }
- Node::Word(_)
- | Node::Command(_)
- | Node::BracedContent(_)
- | Node::QuotedContent(_)
- | Node::Concat(_) => {
- let mut analyzer = ContentAnalyzer::default();
- analyzer.visit(tree, node);
- let tokens = analyzer.tokens;
- self.output.push_str(tokens[0].text());
-
- let align = self.align.pop().unwrap_or_default();
- let mut length = align + tokens[0].text().chars().count();
- for i in 1..tokens.len() {
- let previous = tokens[i - 1];
- let current = tokens[i];
- let current_length = current.text().chars().count();
-
- let insert_space = Self::should_insert_space(previous, current);
- let space_length = if insert_space { 1 } else { 0 };
-
- if length + current_length + space_length > self.params.line_length() as usize {
- self.output.push('\n');
- self.output.push_str(self.indent.as_ref());
- for _ in 0..=align - self.params.tab_size {
- self.output.push(' ');
- }
- length = align;
- } else if insert_space {
- self.output.push(' ');
- length += 1;
- }
- self.output.push_str(current.text());
- length += current_length;
- }
- }
- }
- }
-}
-
-#[derive(Debug, Default)]
-struct ContentAnalyzer<'a> {
- tokens: Vec<&'a Token>,
-}
-
-impl<'a> Visitor<'a> for ContentAnalyzer<'a> {
- fn visit(&mut self, tree: &'a Tree, node: NodeIndex) {
- match &tree.graph[node] {
- Node::Root(_)
- | Node::Comment(_)
- | Node::Preamble(_)
- | Node::String(_)
- | Node::Entry(_)
- | Node::Field(_) => tree.walk(self, node),
- Node::Word(word) => self.tokens.push(&word.token),
- Node::Command(cmd) => self.tokens.push(&cmd.token),
- Node::QuotedContent(content) => {
- self.tokens.push(&content.left);
- tree.walk(self, node);
- if let Some(right) = &content.right {
- self.tokens.push(right);
- }
- }
- Node::BracedContent(content) => {
- self.tokens.push(&content.left);
- tree.walk(self, node);
- if let Some(right) = &content.right {
- self.tokens.push(right);
- }
- }
- Node::Concat(concat) => {
- let mut children = tree.children(node);
- let left = children.next().unwrap();
- self.visit(tree, left);
- self.tokens.push(&concat.operator);
- children.for_each(|right| self.visit(tree, right));
- }
- }
- }
-}
-
-pub fn format(tree: &Tree, node: NodeIndex, params: FormattingParams) -> StdString {
- let mut formatter = Formatter::new(params);
- formatter.visit(tree, node);
- formatter.output
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::syntax::bibtex;
- use indoc::indoc;
-
- fn verify(source: &str, expected: &str, line_length: i32) {
- let tree = bibtex::open(source);
- let options = BibtexFormattingOptions {
- line_length: Some(line_length),
- formatter: None,
- };
-
- let mut children = tree.children(tree.root);
- let declaration = children.next().unwrap();
- assert_eq!(children.next(), None);
-
- let actual = format(
- &tree,
- declaration,
- FormattingParams {
- tab_size: 4,
- insert_spaces: true,
- options: &options,
- },
- );
- assert_eq!(actual, expected);
- }
-
- #[test]
- fn wrap_long_lines() {
- let source =
- "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}";
- let expected = indoc!(
- "
- @article{foo,
- bar = {Lorem ipsum dolor
- sit amet,
- consectetur
- adipiscing elit.},
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn line_length_zero() {
- let source =
- "@article{foo, bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.},}";
- let expected = indoc!(
- "
- @article{foo,
- bar = {Lorem ipsum dolor sit amet, consectetur adipiscing elit.},
- }"
- );
- verify(source, expected, 0);
- }
-
- #[test]
- fn trailing_commas() {
- let source = "@article{foo, bar = baz}";
- let expected = indoc!(
- "
- @article{foo,
- bar = baz,
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn insert_braces() {
- let source = "@article{foo, bar = baz,";
- let expected = indoc!(
- "
- @article{foo,
- bar = baz,
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn commands() {
- let source = "@article{foo, bar = \"\\baz\",}";
- let expected = indoc!(
- "@article{foo,
- bar = \"\\baz\",
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn concatenation() {
- let source = "@article{foo, bar = \"baz\" # \"qux\"}";
- let expected = indoc!(
- "
- @article{foo,
- bar = \"baz\" # \"qux\",
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn parentheses() {
- let source = "@article(foo,)";
- let expected = indoc!(
- "
- @article{foo,
- }"
- );
- verify(source, expected, 30);
- }
-
- #[test]
- fn string() {
- let source = "@string{foo=\"bar\"}";
- let expected = "@string{foo = \"bar\"}";
- verify(source, expected, 30);
- }
-
- #[test]
- fn preamble() {
- let source = "@preamble{\n\"foo bar baz\"}";
- let expected = "@preamble{\"foo bar baz\"}";
- verify(source, expected, 30);
- }
-}
diff --git a/support/texlab/src/syntax/bibtex/kind.rs b/support/texlab/src/syntax/bibtex/kind.rs
new file mode 100644
index 0000000000..fcb26b69bd
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/kind.rs
@@ -0,0 +1,51 @@
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
+#[allow(non_camel_case_types)]
+#[repr(u16)]
+pub enum SyntaxKind {
+ ERROR = 0,
+ MISSING,
+
+ WHITESPACE,
+ PREAMBLE_TYPE,
+ STRING_TYPE,
+ COMMENT_TYPE,
+ ENTRY_TYPE,
+ WORD,
+ L_CURLY,
+ R_CURLY,
+ L_PAREN,
+ R_PAREN,
+ COMMA,
+ HASH,
+ QUOTE,
+ EQUALITY_SIGN,
+ COMMAND_NAME,
+
+ JUNK,
+ PREAMBLE,
+ STRING,
+ COMMENT,
+ ENTRY,
+ FIELD,
+ VALUE,
+ TOKEN,
+ BRACE_GROUP,
+ QUOTE_GROUP,
+ ROOT,
+}
+
+impl SyntaxKind {
+ pub fn is_type(&self) -> bool {
+ use SyntaxKind::*;
+ matches!(
+ self,
+ PREAMBLE_TYPE | STRING_TYPE | COMMENT_TYPE | ENTRY_TYPE
+ )
+ }
+}
+
+impl From<SyntaxKind> for cstree::SyntaxKind {
+ fn from(kind: SyntaxKind) -> Self {
+ Self(kind as u16)
+ }
+}
diff --git a/support/texlab/src/syntax/bibtex/lexer.rs b/support/texlab/src/syntax/bibtex/lexer.rs
index e089b15864..dff111d645 100644
--- a/support/texlab/src/syntax/bibtex/lexer.rs
+++ b/support/texlab/src/syntax/bibtex/lexer.rs
@@ -1,173 +1,82 @@
-use super::ast::{Token, TokenKind};
-use crate::syntax::text::CharStream;
+use logos::Logos;
-#[derive(Debug)]
-pub struct Lexer<'a> {
- stream: CharStream<'a>,
-}
+use super::kind::SyntaxKind;
-impl<'a> Lexer<'a> {
- pub fn new(text: &'a str) -> Self {
- Self {
- stream: CharStream::new(text),
- }
- }
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord, Logos)]
+#[allow(non_camel_case_types)]
+#[repr(u16)]
+enum LogosToken {
+ #[regex(r"\s+")]
+ WHITESPACE = 2,
- fn kind(&mut self) -> Token {
- fn is_type_char(c: char) -> bool {
- c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
- }
+ #[regex(r"@[Pp][Rr][Ee][Aa][Mm][Bb][Ll][Ee]")]
+ PREAMBLE_TYPE,
- self.stream.start_span();
- self.stream.next().unwrap();
- while self.stream.satifies(|c| is_type_char(*c)) {
- self.stream.next();
- }
- let span = self.stream.end_span();
- let kind = match span.text.to_lowercase().as_ref() {
- "@preamble" => TokenKind::PreambleKind,
- "@string" => TokenKind::StringKind,
- _ => TokenKind::EntryKind,
- };
- Token::new(span, kind)
- }
+ #[regex(r"@[Ss][Tt][Rr][Ii][Nn][Gg]")]
+ STRING_TYPE,
- fn single_character(&mut self, kind: TokenKind) -> Token {
- self.stream.start_span();
- self.stream.next();
- let span = self.stream.end_span();
- Token::new(span, kind)
- }
+ #[regex(r"@[Cc][Oo][Mm][Mm][Ee][Nn][Tt]")]
+ COMMENT_TYPE,
- fn command(&mut self) -> Token {
- let span = self.stream.command();
- Token::new(span, TokenKind::Command)
- }
+ #[regex(r"@[!\$\&\*\+\-\./:;<>\?@\[\]\\\^_`\|\~a-zA-Z][!\$\&\*\+\-\./:;<>\?@\[\]\\\^_`\|\~a-zA-Z0-9]*|@")]
+ ENTRY_TYPE,
- fn word(&mut self) -> Token {
- fn is_word_char(c: char) -> bool {
- !c.is_whitespace()
- && c != '@'
- && c != '='
- && c != ','
- && c != '#'
- && c != '"'
- && c != '{'
- && c != '}'
- && c != '('
- && c != ')'
- }
+ #[regex(r#"[^\s\{\}\(\),#"=\\]+"#)]
+ #[error]
+ WORD,
- self.stream.start_span();
- while self.stream.satifies(|c| is_word_char(*c)) {
- self.stream.next();
- }
- let span = self.stream.end_span();
- Token::new(span, TokenKind::Word)
- }
-}
+ #[token("{")]
+ L_CURLY,
-impl<'a> Iterator for Lexer<'a> {
- type Item = Token;
-
- fn next(&mut self) -> Option<Token> {
- loop {
- match self.stream.peek() {
- Some('@') => return Some(self.kind()),
- Some('=') => return Some(self.single_character(TokenKind::Assign)),
- Some(',') => return Some(self.single_character(TokenKind::Comma)),
- Some('#') => return Some(self.single_character(TokenKind::Concat)),
- Some('"') => return Some(self.single_character(TokenKind::Quote)),
- Some('{') => return Some(self.single_character(TokenKind::BeginBrace)),
- Some('}') => return Some(self.single_character(TokenKind::EndBrace)),
- Some('(') => return Some(self.single_character(TokenKind::BeginParen)),
- Some(')') => return Some(self.single_character(TokenKind::EndParen)),
- Some('\\') => return Some(self.command()),
- Some(c) => {
- if c.is_whitespace() {
- self.stream.next();
- } else {
- return Some(self.word());
- }
- }
- None => {
- return None;
- }
- }
- }
- }
-}
+ #[token("}")]
+ R_CURLY,
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- protocol::{Position, Range},
- syntax::text::Span,
- };
-
- fn verify<'a>(lexer: &mut Lexer<'a>, line: u64, character: u64, text: &str, kind: TokenKind) {
- let start = Position::new(line, character);
- let end = Position::new(line, character + text.chars().count() as u64);
- let range = Range::new(start, end);
- let span = Span::new(range, text.to_owned());
- let token = Token::new(span, kind);
- assert_eq!(Some(token), lexer.next());
- }
+ #[token("(")]
+ L_PAREN,
- #[test]
- fn word() {
- let mut lexer = Lexer::new("foo bar baz");
- verify(&mut lexer, 0, 0, "foo", TokenKind::Word);
- verify(&mut lexer, 0, 4, "bar", TokenKind::Word);
- verify(&mut lexer, 0, 8, "baz", TokenKind::Word);
- assert_eq!(None, lexer.next());
- }
+ #[token(")")]
+ R_PAREN,
- #[test]
- fn command() {
- let mut lexer = Lexer::new("\\foo\\bar@baz");
- verify(&mut lexer, 0, 0, "\\foo", TokenKind::Command);
- verify(&mut lexer, 0, 4, "\\bar@baz", TokenKind::Command);
- assert_eq!(None, lexer.next());
- }
+ #[token(",")]
+ COMMA,
- #[test]
- fn escape_sequence() {
- let mut lexer = Lexer::new("\\foo*\n\\%\\**");
- verify(&mut lexer, 0, 0, "\\foo*", TokenKind::Command);
- verify(&mut lexer, 1, 0, "\\%", TokenKind::Command);
- verify(&mut lexer, 1, 2, "\\*", TokenKind::Command);
- verify(&mut lexer, 1, 4, "*", TokenKind::Word);
- assert_eq!(None, lexer.next());
- }
+ #[token("#")]
+ HASH,
+
+ #[token("\"")]
+ QUOTE,
+
+ #[token("=")]
+ EQUALITY_SIGN,
- #[test]
- fn delimiter() {
- let mut lexer = Lexer::new("{}()\"");
- verify(&mut lexer, 0, 0, "{", TokenKind::BeginBrace);
- verify(&mut lexer, 0, 1, "}", TokenKind::EndBrace);
- verify(&mut lexer, 0, 2, "(", TokenKind::BeginParen);
- verify(&mut lexer, 0, 3, ")", TokenKind::EndParen);
- verify(&mut lexer, 0, 4, "\"", TokenKind::Quote);
- assert_eq!(None, lexer.next());
+ #[regex(r"\\([^\r\n]|[@a-zA-Z:_]+\*?)?")]
+ COMMAND_NAME,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct Lexer<'a> {
+ tokens: Vec<(SyntaxKind, &'a str)>,
+}
+
+impl<'a> Lexer<'a> {
+ pub fn new(text: &'a str) -> Self {
+ let mut tokens = Vec::new();
+ let mut lexer = LogosToken::lexer(text);
+ while let Some(kind) = lexer.next() {
+ tokens.push((
+ unsafe { std::mem::transmute::<LogosToken, SyntaxKind>(kind) },
+ lexer.slice(),
+ ));
+ }
+ tokens.reverse();
+ Self { tokens }
}
- #[test]
- fn kind() {
- let mut lexer = Lexer::new("@pReAmBlE\n@article\n@string");
- verify(&mut lexer, 0, 0, "@pReAmBlE", TokenKind::PreambleKind);
- verify(&mut lexer, 1, 0, "@article", TokenKind::EntryKind);
- verify(&mut lexer, 2, 0, "@string", TokenKind::StringKind);
- assert_eq!(None, lexer.next());
+ pub fn peek(&self) -> Option<SyntaxKind> {
+ self.tokens.last().map(|(kind, _)| *kind)
}
- #[test]
- fn operator() {
- let mut lexer = Lexer::new("=,#");
- verify(&mut lexer, 0, 0, "=", TokenKind::Assign);
- verify(&mut lexer, 0, 1, ",", TokenKind::Comma);
- verify(&mut lexer, 0, 2, "#", TokenKind::Concat);
- assert_eq!(None, lexer.next());
+ pub fn consume(&mut self) -> Option<(SyntaxKind, &'a str)> {
+ self.tokens.pop()
}
}
diff --git a/support/texlab/src/syntax/bibtex/mod.rs b/support/texlab/src/syntax/bibtex/mod.rs
deleted file mode 100644
index aa7242c1f7..0000000000
--- a/support/texlab/src/syntax/bibtex/mod.rs
+++ /dev/null
@@ -1,396 +0,0 @@
-mod ast;
-mod formatter;
-mod lexer;
-mod parser;
-
-pub use self::{ast::*, formatter::*};
-
-use self::{lexer::Lexer, parser::Parser};
-
-pub fn open(text: &str) -> Tree {
- let lexer = Lexer::new(text);
- let parser = Parser::new(lexer);
- parser.parse()
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::{
- protocol::{Range, RangeExt},
- syntax::text::SyntaxNode,
- };
- use petgraph::graph::NodeIndex;
-
- #[derive(Debug, Default)]
- struct TreeTraversal {
- nodes: Vec<NodeIndex>,
- }
-
- impl<'a> Visitor<'a> for TreeTraversal {
- fn visit(&mut self, tree: &Tree, node: NodeIndex) {
- self.nodes.push(node);
- tree.walk(self, node);
- }
- }
-
- mod range {
- use super::*;
- use indoc::indoc;
-
- fn verify(expected_ranges: Vec<Range>, text: &str) {
- let tree = open(text.trim());
-
- let mut traversal = TreeTraversal::default();
- traversal.visit(&tree, tree.root);
- let actual_ranges: Vec<_> = traversal
- .nodes
- .into_iter()
- .map(|node| tree.graph[node].range())
- .collect();
-
- assert_eq!(actual_ranges, expected_ranges);
- }
-
- #[test]
- fn empty_document() {
- verify(vec![Range::new_simple(0, 0, 0, 0)], "");
- }
-
- #[test]
- fn comment() {
- verify(
- vec![Range::new_simple(0, 0, 0, 3), Range::new_simple(0, 0, 0, 3)],
- "foo",
- );
- }
-
- #[test]
- fn preamble_no_left() {
- verify(
- vec![Range::new_simple(0, 0, 0, 9), Range::new_simple(0, 0, 0, 9)],
- "@preamble",
- );
- }
-
- #[test]
- fn preamble_no_content() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 10),
- Range::new_simple(0, 0, 0, 10),
- ],
- "@preamble{",
- );
- }
-
- #[test]
- fn preamble_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 15),
- Range::new_simple(0, 0, 0, 15),
- Range::new_simple(0, 10, 0, 15),
- Range::new_simple(0, 11, 0, 14),
- ],
- r#"@preamble{"foo""#,
- );
- }
-
- #[test]
- fn preamble_complete() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 10, 0, 15),
- Range::new_simple(0, 11, 0, 14),
- ],
- r#"@preamble{"foo"}"#,
- );
- }
-
- #[test]
- fn string_no_left() {
- verify(
- vec![Range::new_simple(0, 0, 0, 7), Range::new_simple(0, 0, 0, 7)],
- r#"@string"#,
- );
- }
-
- #[test]
- fn string_no_name() {
- verify(
- vec![Range::new_simple(0, 0, 0, 8), Range::new_simple(0, 0, 0, 8)],
- r#"@string{"#,
- );
- }
-
- #[test]
- fn string_no_assign() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 11),
- Range::new_simple(0, 0, 0, 11),
- ],
- r#"@string{foo"#,
- );
- }
-
- #[test]
- fn string_no_value() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 13),
- Range::new_simple(0, 0, 0, 13),
- ],
- r#"@string{foo ="#,
- );
- }
-
- #[test]
- fn string_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 19),
- Range::new_simple(0, 0, 0, 19),
- Range::new_simple(0, 14, 0, 19),
- Range::new_simple(0, 15, 0, 18),
- ],
- r#"@string{foo = "bar""#,
- );
- }
-
- #[test]
- fn string_complete() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 20),
- Range::new_simple(0, 0, 0, 20),
- Range::new_simple(0, 14, 0, 19),
- Range::new_simple(0, 15, 0, 18),
- ],
- r#"@string{foo = "bar"}"#,
- );
- }
-
- #[test]
- fn entry_no_left() {
- verify(
- vec![Range::new_simple(0, 0, 0, 8), Range::new_simple(0, 0, 0, 8)],
- r#"@article"#,
- );
- }
-
- #[test]
- fn entry_no_key() {
- verify(
- vec![Range::new_simple(0, 0, 0, 9), Range::new_simple(0, 0, 0, 9)],
- r#"@article{"#,
- );
- }
-
- #[test]
- fn entry_no_comma() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 12),
- Range::new_simple(0, 0, 0, 12),
- ],
- r#"@article{foo"#,
- );
- }
-
- #[test]
- fn entry_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 13),
- Range::new_simple(0, 0, 0, 13),
- ],
- r#"@article{foo,"#,
- );
- }
-
- #[test]
- fn entry_parentheses() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 14),
- Range::new_simple(0, 0, 0, 14),
- ],
- r#"@article(foo,)"#,
- );
- }
-
- #[test]
- fn field_no_assign() {
- verify(
- vec![
- Range::new_simple(0, 0, 1, 10),
- Range::new_simple(0, 0, 1, 10),
- Range::new_simple(1, 4, 1, 10),
- ],
- indoc!(
- r#"
- @article{foo,
- author
- "#
- ),
- );
- }
-
- #[test]
- fn field_no_value() {
- verify(
- vec![
- Range::new_simple(0, 0, 1, 12),
- Range::new_simple(0, 0, 1, 12),
- Range::new_simple(1, 4, 1, 12),
- ],
- indoc!(
- r#"
- @article{foo,
- author =
- "#
- ),
- );
- }
-
- #[test]
- fn field_no_comma() {
- verify(
- vec![
- Range::new_simple(0, 0, 1, 16),
- Range::new_simple(0, 0, 1, 16),
- Range::new_simple(1, 4, 1, 16),
- Range::new_simple(1, 13, 1, 16),
- ],
- indoc!(
- r#"
- @article{foo,
- author = bar
- "#
- ),
- );
- }
-
- #[test]
- fn field_complete() {
- verify(
- vec![
- Range::new_simple(0, 0, 2, 1),
- Range::new_simple(0, 0, 2, 1),
- Range::new_simple(1, 4, 1, 17),
- Range::new_simple(1, 13, 1, 16),
- ],
- indoc!(
- r#"
- @article{foo,
- author = bar,
- }
- "#
- ),
- );
- }
-
- #[test]
- fn entry_two_fields() {
- verify(
- vec![
- Range::new_simple(0, 0, 3, 1),
- Range::new_simple(0, 0, 3, 1),
- Range::new_simple(1, 4, 1, 17),
- Range::new_simple(1, 13, 1, 16),
- Range::new_simple(2, 4, 2, 16),
- Range::new_simple(2, 12, 2, 15),
- ],
- indoc!(
- r#"
- @article{foo,
- author = bar,
- title = baz,
- }
- "#
- ),
- );
- }
-
- #[test]
- fn quoted_content_no_children() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 12),
- Range::new_simple(0, 0, 0, 12),
- Range::new_simple(0, 10, 0, 11),
- ],
- r#"@preamble{"}"#,
- );
- }
-
- #[test]
- fn quoted_content_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 10, 0, 15),
- Range::new_simple(0, 11, 0, 15),
- ],
- r#"@preamble{"word}"#,
- );
- }
-
- #[test]
- fn braced_content_no_children() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 11),
- Range::new_simple(0, 0, 0, 11),
- Range::new_simple(0, 10, 0, 11),
- ],
- r#"@preamble{{"#,
- );
- }
-
- #[test]
- fn braced_content_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 15),
- Range::new_simple(0, 0, 0, 15),
- Range::new_simple(0, 10, 0, 15),
- Range::new_simple(0, 11, 0, 15),
- ],
- r#"@preamble{{word"#,
- );
- }
-
- #[test]
- fn concat_no_right() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 0, 0, 16),
- Range::new_simple(0, 10, 0, 15),
- Range::new_simple(0, 10, 0, 13),
- ],
- r#"@preamble{foo #}"#,
- );
- }
-
- #[test]
- fn concat_complete() {
- verify(
- vec![
- Range::new_simple(0, 0, 0, 20),
- Range::new_simple(0, 0, 0, 20),
- Range::new_simple(0, 10, 0, 19),
- Range::new_simple(0, 10, 0, 13),
- Range::new_simple(0, 16, 0, 19),
- ],
- r#"@preamble{foo # bar}"#,
- );
- }
- }
-}
diff --git a/support/texlab/src/syntax/bibtex/parser.rs b/support/texlab/src/syntax/bibtex/parser.rs
index b23903aeb3..ffdb1c94a0 100644
--- a/support/texlab/src/syntax/bibtex/parser.rs
+++ b/support/texlab/src/syntax/bibtex/parser.rs
@@ -1,401 +1,413 @@
-use super::ast::*;
-use crate::{
- protocol::{Range, RangeExt},
- syntax::text::SyntaxNode,
+use cstree::GreenNodeBuilder;
+
+use super::{
+ lexer::Lexer,
+ SyntaxKind::{self, *},
+ SyntaxNode,
};
-use petgraph::graph::{Graph, NodeIndex};
-use std::iter::Peekable;
-pub struct Parser<I: Iterator<Item = Token>> {
- graph: Graph<Node, ()>,
- tokens: Peekable<I>,
+#[derive(Debug, Clone)]
+pub struct Parse {
+ pub root: SyntaxNode,
+}
+
+struct Parser<'a> {
+ lexer: Lexer<'a>,
+ builder: GreenNodeBuilder<'static, 'static>,
}
-impl<I: Iterator<Item = Token>> Parser<I> {
- pub fn new(tokens: I) -> Self {
+impl<'a> Parser<'a> {
+ pub fn new(lexer: Lexer<'a>) -> Self {
Self {
- tokens: tokens.peekable(),
- graph: Graph::new(),
+ lexer,
+ builder: GreenNodeBuilder::new(),
}
}
- pub fn parse(mut self) -> Tree {
- let mut children = Vec::new();
+ fn peek(&self) -> Option<SyntaxKind> {
+ self.lexer.peek()
+ }
- while let Some(ref token) = self.tokens.peek() {
- match token.kind {
- TokenKind::PreambleKind => children.push(self.preamble()),
- TokenKind::StringKind => children.push(self.string()),
- TokenKind::EntryKind => children.push(self.entry()),
- _ => children.push(self.comment()),
- }
- }
+ fn eat(&mut self) {
+ let (kind, text) = self.lexer.consume().unwrap();
+ self.builder.token(kind.into(), text);
+ }
- let range = if children.is_empty() {
- Range::new_simple(0, 0, 0, 0)
+ fn expect(&mut self, kind: SyntaxKind) {
+ if self.peek() == Some(kind) {
+ self.eat();
+ self.trivia();
} else {
- let start = self.graph[children[0]].start();
- let end = self.graph[children[children.len() - 1]].end();
- Range::new(start, end)
- };
+ self.builder.token(MISSING.into(), "");
+ }
+ }
- let root = self.graph.add_node(Node::Root(Root { range }));
- self.connect(root, &children);
- Tree {
- graph: self.graph,
- root,
+ pub fn parse(mut self) -> Parse {
+ self.builder.start_node(ROOT.into());
+ while let Some(kind) = self.peek() {
+ match kind {
+ PREAMBLE_TYPE => self.preamble(),
+ STRING_TYPE => self.string(),
+ COMMENT_TYPE => self.comment(),
+ ENTRY_TYPE => self.entry(),
+ _ => self.junk(),
+ }
+ }
+ self.builder.finish_node();
+ let (green, resolver) = self.builder.finish();
+ Parse {
+ root: SyntaxNode::new_root_with_resolver(green, resolver.unwrap()),
}
}
- fn preamble(&mut self) -> NodeIndex {
- let ty = self.tokens.next().unwrap();
+ fn trivia(&mut self) {
+ while self.peek() == Some(WHITESPACE) {
+ self.eat();
+ }
+ }
- let left = self.expect2(TokenKind::BeginBrace, TokenKind::BeginParen);
- if left.is_none() {
- return self.graph.add_node(Node::Preamble(Box::new(Preamble {
- range: ty.range(),
- ty,
- left: None,
- right: None,
- })));
+ fn junk(&mut self) {
+ self.builder.start_node(JUNK.into());
+ while self
+ .lexer
+ .peek()
+ .filter(|&kind| {
+ !matches!(
+ kind,
+ PREAMBLE_TYPE | STRING_TYPE | COMMENT_TYPE | ENTRY_TYPE,
+ )
+ })
+ .is_some()
+ {
+ self.eat();
}
+ self.builder.finish_node();
+ }
- if !self.can_match_content() {
- return self.graph.add_node(Node::Preamble(Box::new(Preamble {
- range: Range::new(ty.start(), left.as_ref().unwrap().end()),
- ty,
- left,
- right: None,
- })));
+ fn left_delimiter_or_missing(&mut self) {
+ if self
+ .lexer
+ .peek()
+ .filter(|&kind| matches!(kind, L_CURLY | L_PAREN))
+ .is_some()
+ {
+ self.eat();
+ self.trivia();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
+ }
- let content = self.content();
-
- let right = self.expect2(TokenKind::EndBrace, TokenKind::EndParen);
- let end = right
- .as_ref()
- .map(Token::end)
- .unwrap_or_else(|| self.graph[content].end());
-
- let parent = self.graph.add_node(Node::Preamble(Box::new(Preamble {
- range: Range::new(ty.start(), end),
- ty,
- left,
- right,
- })));
- self.graph.add_edge(parent, content, ());
- parent
- }
-
- fn string(&mut self) -> NodeIndex {
- let ty = self.tokens.next().unwrap();
-
- let left = self.expect2(TokenKind::BeginBrace, TokenKind::BeginParen);
- if left.is_none() {
- return self.graph.add_node(Node::String(Box::new(String {
- range: ty.range(),
- ty,
- left: None,
- name: None,
- assign: None,
- right: None,
- })));
+ fn right_delimiter_or_missing(&mut self) {
+ if self
+ .lexer
+ .peek()
+ .filter(|&kind| matches!(kind, R_CURLY | R_PAREN))
+ .is_some()
+ {
+ self.eat();
+ self.trivia();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
+ }
- let name = self.expect1(TokenKind::Word);
- if name.is_none() {
- return self.graph.add_node(Node::String(Box::new(String {
- range: Range::new(ty.start(), left.as_ref().unwrap().end()),
- ty,
- left,
- name: None,
- assign: None,
- right: None,
- })));
+ fn value_or_missing(&mut self) {
+ if self
+ .lexer
+ .peek()
+ .filter(|&kind| matches!(kind, L_CURLY | QUOTE | WORD))
+ .is_some()
+ {
+ self.value();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
+ }
+
+ fn preamble(&mut self) {
+ self.builder.start_node(PREAMBLE.into());
+ self.eat();
+ self.trivia();
+ self.left_delimiter_or_missing();
+ self.value_or_missing();
+ self.right_delimiter_or_missing();
+ self.builder.finish_node();
+ }
- let assign = self.expect1(TokenKind::Assign);
- if assign.is_none() {
- return self.graph.add_node(Node::String(Box::new(String {
- range: Range::new(ty.start(), name.as_ref().unwrap().end()),
- ty,
- left,
- name,
- assign: None,
- right: None,
- })));
+ fn string(&mut self) {
+ self.builder.start_node(STRING.into());
+ self.eat();
+ self.trivia();
+ self.left_delimiter_or_missing();
+
+ if self.peek() != Some(WORD) {
+ self.builder.token(MISSING.into(), "");
+ self.builder.finish_node();
+ return;
}
+ self.eat();
+ self.trivia();
+ self.expect(EQUALITY_SIGN);
+
+ self.value_or_missing();
+
+ self.right_delimiter_or_missing();
+ self.builder.finish_node();
+ }
+
+ fn comment(&mut self) {
+ self.builder.start_node(COMMENT.into());
+ self.eat();
+ self.builder.finish_node();
+ }
- if !self.can_match_content() {
- return self.graph.add_node(Node::String(Box::new(String {
- range: Range::new(ty.start(), assign.as_ref().unwrap().end()),
- ty,
- left,
- name,
- assign,
- right: None,
- })));
+ fn entry(&mut self) {
+ self.builder.start_node(ENTRY.into());
+ self.eat();
+ self.trivia();
+
+ self.left_delimiter_or_missing();
+
+ if self.peek() != Some(WORD) {
+ self.builder.token(MISSING.into(), "");
+ self.builder.finish_node();
+ return;
}
- let value = self.content();
-
- let right = self.expect2(TokenKind::EndBrace, TokenKind::EndParen);
- let end = right
- .as_ref()
- .map(Token::end)
- .unwrap_or_else(|| self.graph[value].end());
-
- let parent = self.graph.add_node(Node::String(Box::new(String {
- range: Range::new(ty.start(), end),
- ty,
- left,
- name,
- assign,
- right,
- })));
- self.graph.add_edge(parent, value, ());
- parent
- }
-
- fn entry(&mut self) -> NodeIndex {
- let ty = self.tokens.next().unwrap();
-
- let left = self.expect2(TokenKind::BeginBrace, TokenKind::BeginParen);
- if left.is_none() {
- return self.graph.add_node(Node::Entry(Box::new(Entry {
- range: ty.range(),
- ty,
- left: None,
- key: None,
- comma: None,
- right: None,
- })));
+ self.eat();
+
+ while let Some(kind) = self.peek() {
+ match kind {
+ WHITESPACE => self.eat(),
+ WORD => self.field(),
+ COMMA => self.eat(),
+ _ => break,
+ };
}
- let key = self.expect1(TokenKind::Word);
- if key.is_none() {
- return self.graph.add_node(Node::Entry(Box::new(Entry {
- range: Range::new(ty.start(), left.as_ref().unwrap().end()),
- ty,
- left,
- key: None,
- comma: None,
- right: None,
- })));
- }
+ self.right_delimiter_or_missing();
- let comma = self.expect1(TokenKind::Comma);
- if comma.is_none() {
- return self.graph.add_node(Node::Entry(Box::new(Entry {
- range: Range::new(ty.start(), key.as_ref().unwrap().end()),
- ty,
- left,
- key,
- comma: None,
- right: None,
- })));
- }
+ self.builder.finish_node();
+ }
- let mut fields = Vec::new();
- while self.next_of_kind(TokenKind::Word) {
- fields.push(self.field());
- }
+ fn field(&mut self) {
+ self.builder.start_node(FIELD.into());
+ self.eat();
+ self.trivia();
- let right = self.expect2(TokenKind::EndBrace, TokenKind::EndParen);
-
- let end = right
- .as_ref()
- .map(Token::end)
- .or_else(|| fields.last().map(|field| self.graph[*field].end()))
- .unwrap_or_else(|| comma.as_ref().unwrap().end());
- let parent = self.graph.add_node(Node::Entry(Box::new(Entry {
- range: Range::new(ty.start(), end),
- ty,
- left,
- key,
- comma,
- right,
- })));
- self.connect(parent, &fields);
- parent
- }
-
- fn comment(&mut self) -> NodeIndex {
- let token = self.tokens.next().unwrap();
- self.graph.add_node(Node::Comment(Comment { token }))
- }
-
- fn field(&mut self) -> NodeIndex {
- let name = self.tokens.next().unwrap();
-
- let assign = self.expect1(TokenKind::Assign);
- if assign.is_none() {
- return self.graph.add_node(Node::Field(Box::new(Field {
- range: name.range(),
- name,
- assign: None,
- comma: None,
- })));
+ if self.peek() == Some(EQUALITY_SIGN) {
+ self.eat();
+ self.trivia();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
- if !self.can_match_content() {
- return self.graph.add_node(Node::Field(Box::new(Field {
- range: Range::new(name.start(), assign.as_ref().unwrap().end()),
- name,
- assign,
- comma: None,
- })));
+ if self
+ .lexer
+ .peek()
+ .filter(|&kind| matches!(kind, L_CURLY | QUOTE | WORD))
+ .is_some()
+ {
+ self.value();
+ } else {
+ self.builder.token(MISSING.into(), "");
}
- let content = self.content();
-
- let comma = self.expect1(TokenKind::Comma);
-
- let end = comma
- .as_ref()
- .map(Token::end)
- .unwrap_or_else(|| self.graph[content].end());
- let parent = self.graph.add_node(Node::Field(Box::new(Field {
- range: Range::new(name.start(), end),
- name,
- assign,
- comma,
- })));
- self.graph.add_edge(parent, content, ());
- parent
- }
-
- fn content(&mut self) -> NodeIndex {
- let token = self.tokens.next().unwrap();
- let left = match token.kind {
- TokenKind::PreambleKind
- | TokenKind::StringKind
- | TokenKind::EntryKind
- | TokenKind::Word
- | TokenKind::Assign
- | TokenKind::Comma
- | TokenKind::BeginParen
- | TokenKind::EndParen => self.graph.add_node(Node::Word(Word { token })),
- TokenKind::Command => self.graph.add_node(Node::Command(Command { token })),
- TokenKind::Quote => {
- let mut children = Vec::new();
- while self.can_match_content() {
- if self.next_of_kind(TokenKind::Quote) {
- break;
- }
- children.push(self.content());
- }
- let right = self.expect1(TokenKind::Quote);
-
- let end = right
- .as_ref()
- .map(Token::end)
- .or_else(|| children.last().map(|child| self.graph[*child].end()))
- .unwrap_or_else(|| token.end());
- let parent = self.graph.add_node(Node::QuotedContent(QuotedContent {
- range: Range::new(token.start(), end),
- left: token,
- right,
- }));
- self.connect(parent, &children);
- parent
- }
- TokenKind::BeginBrace => {
- let mut children = Vec::new();
- while self.can_match_content() {
- children.push(self.content());
- }
- let right = self.expect1(TokenKind::EndBrace);
-
- let end = right
- .as_ref()
- .map(Token::end)
- .or_else(|| children.last().map(|child| self.graph[*child].end()))
- .unwrap_or_else(|| token.end());
- let parent = self.graph.add_node(Node::BracedContent(BracedContent {
- range: Range::new(token.start(), end),
- left: token,
- right,
- }));
- self.connect(parent, &children);
- parent
+ self.builder.finish_node();
+ }
+
+ fn value(&mut self) {
+ self.builder.start_node(VALUE.into());
+ self.token();
+ while let Some(kind) = self.peek() {
+ match kind {
+ WHITESPACE => self.eat(),
+ L_CURLY | QUOTE | WORD => self.token(),
+ HASH => self.eat(),
+ _ => break,
}
+ }
+ self.builder.finish_node();
+ }
+
+ fn token(&mut self) {
+ self.builder.start_node(TOKEN.into());
+ match self.peek().unwrap() {
+ L_CURLY => self.brace_group(),
+ QUOTE => self.quote_group(),
+ WORD => self.eat(),
_ => unreachable!(),
};
+ self.builder.finish_node();
+ }
- match self.expect1(TokenKind::Concat) {
- Some(operator) => {
- if self.can_match_content() {
- let right = self.content();
- let parent = self.graph.add_node(Node::Concat(Concat {
- range: Range::new(self.graph[left].start(), self.graph[right].end()),
- operator,
- }));
- self.graph.add_edge(parent, left, ());
- self.graph.add_edge(parent, right, ());
- parent
- } else {
- let parent = self.graph.add_node(Node::Concat(Concat {
- range: Range::new(self.graph[left].start(), operator.end()),
- operator,
- }));
- self.graph.add_edge(parent, left, ());
- parent
- }
- }
- None => left,
+ fn brace_group(&mut self) {
+ self.builder.start_node(BRACE_GROUP.into());
+ self.eat();
+
+ while let Some(kind) = self.peek() {
+ match kind {
+ WHITESPACE => self.eat(),
+ PREAMBLE_TYPE => break,
+ STRING_TYPE => break,
+ COMMENT_TYPE => break,
+ ENTRY_TYPE => break,
+ WORD => self.eat(),
+ L_CURLY => self.brace_group(),
+ R_CURLY => break,
+ L_PAREN => self.eat(),
+ R_PAREN => self.eat(),
+ COMMA => self.eat(),
+ HASH => self.eat(),
+ QUOTE => self.eat(),
+ EQUALITY_SIGN => self.eat(),
+ COMMAND_NAME => self.eat(),
+ _ => unreachable!(),
+ };
}
+
+ self.expect(R_CURLY);
+
+ self.builder.finish_node();
}
- fn connect(&mut self, parent: NodeIndex, children: &[NodeIndex]) {
- for child in children {
- self.graph.add_edge(parent, *child, ());
+ fn quote_group(&mut self) {
+ self.builder.start_node(QUOTE_GROUP.into());
+ self.eat();
+
+ while let Some(kind) = self.peek() {
+ match kind {
+ WHITESPACE => self.eat(),
+ PREAMBLE_TYPE => break,
+ STRING_TYPE => break,
+ COMMENT_TYPE => break,
+ ENTRY_TYPE => break,
+ WORD => self.eat(),
+ L_CURLY => self.brace_group(),
+ R_CURLY => break,
+ L_PAREN => self.eat(),
+ R_PAREN => self.eat(),
+ COMMA => self.eat(),
+ HASH => self.eat(),
+ QUOTE => break,
+ EQUALITY_SIGN => self.eat(),
+ COMMAND_NAME => self.eat(),
+ _ => unreachable!(),
+ };
}
+
+ self.expect(QUOTE);
+ self.builder.finish_node();
}
+}
- fn can_match_content(&mut self) -> bool {
- if let Some(ref token) = self.tokens.peek() {
- match token.kind {
- TokenKind::PreambleKind
- | TokenKind::StringKind
- | TokenKind::EntryKind
- | TokenKind::Word
- | TokenKind::Command
- | TokenKind::Assign
- | TokenKind::Comma
- | TokenKind::Quote
- | TokenKind::BeginBrace
- | TokenKind::BeginParen
- | TokenKind::EndParen => true,
- TokenKind::Concat | TokenKind::EndBrace => false,
- }
- } else {
- false
- }
+pub fn parse(text: &str) -> Parse {
+ Parser::new(Lexer::new(text)).parse()
+}
+
+#[cfg(test)]
+mod tests {
+ use insta::assert_debug_snapshot;
+
+ use super::*;
+
+ fn setup(text: &str) -> SyntaxNode {
+ parse(&text.trim().replace("\r", "")).root
}
- fn expect1(&mut self, kind: TokenKind) -> Option<Token> {
- if let Some(ref token) = self.tokens.peek() {
- if token.kind == kind {
- return self.tokens.next();
- }
- }
- None
+ #[test]
+ fn test_empty() {
+ assert_debug_snapshot!(setup(r#""#));
}
- fn expect2(&mut self, kind1: TokenKind, kind2: TokenKind) -> Option<Token> {
- if let Some(ref token) = self.tokens.peek() {
- if token.kind == kind1 || token.kind == kind2 {
- return self.tokens.next();
- }
- }
- None
+ #[test]
+ fn test_junk() {
+ assert_debug_snapshot!(setup(r#"Hello World!"#));
}
- fn next_of_kind(&mut self, kind: TokenKind) -> bool {
- if let Some(token) = self.tokens.peek() {
- token.kind == kind
- } else {
- false
- }
+ #[test]
+ fn test_preamble_complete() {
+ assert_debug_snapshot!(setup(r#"@preamble{ "Hello World" }"#));
+ }
+
+ #[test]
+ fn test_preamble_missing_end() {
+ assert_debug_snapshot!(setup(r#"@preamble{ "Hello World" "#));
+ }
+
+ #[test]
+ fn test_preamble_casing() {
+ assert_debug_snapshot!(setup(r#"@preAmbLe{ "Hello World" }"#));
+ }
+
+ #[test]
+ fn test_string_complete() {
+ assert_debug_snapshot!(setup(r#"@string{foo = {Hello World}}"#));
+ }
+
+ #[test]
+ fn test_string_incomplete() {
+ assert_debug_snapshot!(setup(r#"@string{foo = {Hello World}"#));
+ }
+
+ #[test]
+ fn test_string_concatenation() {
+ assert_debug_snapshot!(setup(
+ r#"@string{foo = {Hello World}} @string{bar = foo # "!"}"#
+ ));
+ }
+
+ #[test]
+ fn test_string_casing() {
+ assert_debug_snapshot!(setup(r#"@STRING{foo = "Hello World"}"#));
+ }
+
+ #[test]
+ fn test_string_missing_quote() {
+ assert_debug_snapshot!(setup(r#"@STRING{foo = "Hello World}"#));
+ }
+
+ #[test]
+ fn test_entry_no_fields() {
+ assert_debug_snapshot!(setup(r#"@article{foo,}"#));
+ }
+
+ #[test]
+ fn test_entry_no_fields_missing_comma() {
+ assert_debug_snapshot!(setup(r#"@article{foo}"#));
+ }
+
+ #[test]
+ fn test_entry_one_field() {
+ assert_debug_snapshot!(setup(r#"@article{foo, author = {Foo Bar}}"#));
+ }
+
+ #[test]
+ fn test_entry_one_field_number_key() {
+ assert_debug_snapshot!(setup(r#"@article{foo2021, author = {Foo Bar}}"#));
+ }
+
+ #[test]
+ fn test_entry_one_field_trailing_comma() {
+ assert_debug_snapshot!(setup(r#"@article{foo, author = {Foo Bar},}"#));
+ }
+
+ #[test]
+ fn test_entry_two_fields() {
+ assert_debug_snapshot!(setup(
+ r#"@article{foo, author = {Foo Bar}, title = {Hello World}}"#
+ ));
+ }
+
+ #[test]
+ fn test_entry_two_fields_incomplete() {
+ assert_debug_snapshot!(setup(r#"@article{foo, author = {Foo Bar}, t}"#));
+ }
+
+ #[test]
+ fn test_entry_complete_parens() {
+ assert_debug_snapshot!(setup(
+ r#"@article(foo, author = {Foo Bar}, title = {Hello})"#
+ ));
}
}
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__empty.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__empty.snap
new file mode 100644
index 0000000000..55e1954d2f
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__empty.snap
@@ -0,0 +1,7 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"\"#)"
+
+---
+ROOT@0..0
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_complete_parens.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_complete_parens.snap
new file mode 100644
index 0000000000..cfd469f161
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_complete_parens.snap
@@ -0,0 +1,40 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article(foo, author = {Foo Bar}, title = {Hello})\"#)"
+
+---
+ROOT@0..50
+ ENTRY@0..50
+ ENTRY_TYPE@0..8 "@article"
+ L_PAREN@8..9 "("
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ WHITESPACE@13..14 " "
+ FIELD@14..32
+ WORD@14..20 "author"
+ WHITESPACE@20..21 " "
+ EQUALITY_SIGN@21..22 "="
+ WHITESPACE@22..23 " "
+ VALUE@23..32
+ TOKEN@23..32
+ BRACE_GROUP@23..32
+ L_CURLY@23..24 "{"
+ WORD@24..27 "Foo"
+ WHITESPACE@27..28 " "
+ WORD@28..31 "Bar"
+ R_CURLY@31..32 "}"
+ COMMA@32..33 ","
+ WHITESPACE@33..34 " "
+ FIELD@34..49
+ WORD@34..39 "title"
+ WHITESPACE@39..40 " "
+ EQUALITY_SIGN@40..41 "="
+ WHITESPACE@41..42 " "
+ VALUE@42..49
+ TOKEN@42..49
+ BRACE_GROUP@42..49
+ L_CURLY@42..43 "{"
+ WORD@43..48 "Hello"
+ R_CURLY@48..49 "}"
+ R_PAREN@49..50 ")"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields.snap
new file mode 100644
index 0000000000..a98df7e382
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields.snap
@@ -0,0 +1,13 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo,}\"#)"
+
+---
+ROOT@0..14
+ ENTRY@0..14
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ R_CURLY@13..14 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields_missing_comma.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields_missing_comma.snap
new file mode 100644
index 0000000000..b83037aae0
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_no_fields_missing_comma.snap
@@ -0,0 +1,12 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo}\"#)"
+
+---
+ROOT@0..13
+ ENTRY@0..13
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ R_CURLY@12..13 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field.snap
new file mode 100644
index 0000000000..704cbbbd47
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field.snap
@@ -0,0 +1,27 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo, author = {Foo Bar}}\"#)"
+
+---
+ROOT@0..33
+ ENTRY@0..33
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ WHITESPACE@13..14 " "
+ FIELD@14..32
+ WORD@14..20 "author"
+ WHITESPACE@20..21 " "
+ EQUALITY_SIGN@21..22 "="
+ WHITESPACE@22..23 " "
+ VALUE@23..32
+ TOKEN@23..32
+ BRACE_GROUP@23..32
+ L_CURLY@23..24 "{"
+ WORD@24..27 "Foo"
+ WHITESPACE@27..28 " "
+ WORD@28..31 "Bar"
+ R_CURLY@31..32 "}"
+ R_CURLY@32..33 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_number_key.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_number_key.snap
new file mode 100644
index 0000000000..8f00c5ee1c
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_number_key.snap
@@ -0,0 +1,27 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo2021, author = {Foo Bar}}\"#)"
+
+---
+ROOT@0..37
+ ENTRY@0..37
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..16 "foo2021"
+ COMMA@16..17 ","
+ WHITESPACE@17..18 " "
+ FIELD@18..36
+ WORD@18..24 "author"
+ WHITESPACE@24..25 " "
+ EQUALITY_SIGN@25..26 "="
+ WHITESPACE@26..27 " "
+ VALUE@27..36
+ TOKEN@27..36
+ BRACE_GROUP@27..36
+ L_CURLY@27..28 "{"
+ WORD@28..31 "Foo"
+ WHITESPACE@31..32 " "
+ WORD@32..35 "Bar"
+ R_CURLY@35..36 "}"
+ R_CURLY@36..37 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_trailing_comma.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_trailing_comma.snap
new file mode 100644
index 0000000000..af8b1593df
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_one_field_trailing_comma.snap
@@ -0,0 +1,28 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo, author = {Foo Bar},}\"#)"
+
+---
+ROOT@0..34
+ ENTRY@0..34
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ WHITESPACE@13..14 " "
+ FIELD@14..32
+ WORD@14..20 "author"
+ WHITESPACE@20..21 " "
+ EQUALITY_SIGN@21..22 "="
+ WHITESPACE@22..23 " "
+ VALUE@23..32
+ TOKEN@23..32
+ BRACE_GROUP@23..32
+ L_CURLY@23..24 "{"
+ WORD@24..27 "Foo"
+ WHITESPACE@27..28 " "
+ WORD@28..31 "Bar"
+ R_CURLY@31..32 "}"
+ COMMA@32..33 ","
+ R_CURLY@33..34 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields.snap
new file mode 100644
index 0000000000..f47b3b6917
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields.snap
@@ -0,0 +1,42 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo, author = {Foo Bar}, title = {Hello World}}\"#)"
+
+---
+ROOT@0..56
+ ENTRY@0..56
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ WHITESPACE@13..14 " "
+ FIELD@14..32
+ WORD@14..20 "author"
+ WHITESPACE@20..21 " "
+ EQUALITY_SIGN@21..22 "="
+ WHITESPACE@22..23 " "
+ VALUE@23..32
+ TOKEN@23..32
+ BRACE_GROUP@23..32
+ L_CURLY@23..24 "{"
+ WORD@24..27 "Foo"
+ WHITESPACE@27..28 " "
+ WORD@28..31 "Bar"
+ R_CURLY@31..32 "}"
+ COMMA@32..33 ","
+ WHITESPACE@33..34 " "
+ FIELD@34..55
+ WORD@34..39 "title"
+ WHITESPACE@39..40 " "
+ EQUALITY_SIGN@40..41 "="
+ WHITESPACE@41..42 " "
+ VALUE@42..55
+ TOKEN@42..55
+ BRACE_GROUP@42..55
+ L_CURLY@42..43 "{"
+ WORD@43..48 "Hello"
+ WHITESPACE@48..49 " "
+ WORD@49..54 "World"
+ R_CURLY@54..55 "}"
+ R_CURLY@55..56 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields_incomplete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields_incomplete.snap
new file mode 100644
index 0000000000..34a9019582
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__entry_two_fields_incomplete.snap
@@ -0,0 +1,33 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@article{foo, author = {Foo Bar}, t}\"#)"
+
+---
+ROOT@0..36
+ ENTRY@0..36
+ ENTRY_TYPE@0..8 "@article"
+ L_CURLY@8..9 "{"
+ WORD@9..12 "foo"
+ COMMA@12..13 ","
+ WHITESPACE@13..14 " "
+ FIELD@14..32
+ WORD@14..20 "author"
+ WHITESPACE@20..21 " "
+ EQUALITY_SIGN@21..22 "="
+ WHITESPACE@22..23 " "
+ VALUE@23..32
+ TOKEN@23..32
+ BRACE_GROUP@23..32
+ L_CURLY@23..24 "{"
+ WORD@24..27 "Foo"
+ WHITESPACE@27..28 " "
+ WORD@28..31 "Bar"
+ R_CURLY@31..32 "}"
+ COMMA@32..33 ","
+ WHITESPACE@33..34 " "
+ FIELD@34..35
+ WORD@34..35 "t"
+ MISSING@35..35 ""
+ MISSING@35..35 ""
+ R_CURLY@35..36 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__junk.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__junk.snap
new file mode 100644
index 0000000000..e77313cabf
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__junk.snap
@@ -0,0 +1,11 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"Hello World!\"#)"
+
+---
+ROOT@0..12
+ JUNK@0..12
+ WORD@0..5 "Hello"
+ WHITESPACE@5..6 " "
+ WORD@6..12 "World!"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_casing.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_casing.snap
new file mode 100644
index 0000000000..f4669b8a01
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_casing.snap
@@ -0,0 +1,21 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@preAmbLe{ \"Hello World\" }\"#)"
+
+---
+ROOT@0..26
+ PREAMBLE@0..26
+ PREAMBLE_TYPE@0..9 "@preAmbLe"
+ L_CURLY@9..10 "{"
+ WHITESPACE@10..11 " "
+ VALUE@11..25
+ TOKEN@11..25
+ QUOTE_GROUP@11..25
+ QUOTE@11..12 "\""
+ WORD@12..17 "Hello"
+ WHITESPACE@17..18 " "
+ WORD@18..23 "World"
+ QUOTE@23..24 "\""
+ WHITESPACE@24..25 " "
+ R_CURLY@25..26 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_complete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_complete.snap
new file mode 100644
index 0000000000..d1977f2e76
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_complete.snap
@@ -0,0 +1,21 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@preamble{ \"Hello World\" }\"#)"
+
+---
+ROOT@0..26
+ PREAMBLE@0..26
+ PREAMBLE_TYPE@0..9 "@preamble"
+ L_CURLY@9..10 "{"
+ WHITESPACE@10..11 " "
+ VALUE@11..25
+ TOKEN@11..25
+ QUOTE_GROUP@11..25
+ QUOTE@11..12 "\""
+ WORD@12..17 "Hello"
+ WHITESPACE@17..18 " "
+ WORD@18..23 "World"
+ QUOTE@23..24 "\""
+ WHITESPACE@24..25 " "
+ R_CURLY@25..26 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_missing_end.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_missing_end.snap
new file mode 100644
index 0000000000..4ba447a2ae
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__preamble_missing_end.snap
@@ -0,0 +1,20 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@preamble{ \"Hello World\" \"#)"
+
+---
+ROOT@0..24
+ PREAMBLE@0..24
+ PREAMBLE_TYPE@0..9 "@preamble"
+ L_CURLY@9..10 "{"
+ WHITESPACE@10..11 " "
+ VALUE@11..24
+ TOKEN@11..24
+ QUOTE_GROUP@11..24
+ QUOTE@11..12 "\""
+ WORD@12..17 "Hello"
+ WHITESPACE@17..18 " "
+ WORD@18..23 "World"
+ QUOTE@23..24 "\""
+ MISSING@24..24 ""
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_casing.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_casing.snap
new file mode 100644
index 0000000000..1c8b1dbea4
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_casing.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@STRING{foo = \"Hello World\"}\"#)"
+
+---
+ROOT@0..28
+ STRING@0..28
+ STRING_TYPE@0..7 "@STRING"
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ WHITESPACE@11..12 " "
+ EQUALITY_SIGN@12..13 "="
+ WHITESPACE@13..14 " "
+ VALUE@14..27
+ TOKEN@14..27
+ QUOTE_GROUP@14..27
+ QUOTE@14..15 "\""
+ WORD@15..20 "Hello"
+ WHITESPACE@20..21 " "
+ WORD@21..26 "World"
+ QUOTE@26..27 "\""
+ R_CURLY@27..28 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_complete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_complete.snap
new file mode 100644
index 0000000000..506452894d
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_complete.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@string{foo = {Hello World}}\"#)"
+
+---
+ROOT@0..28
+ STRING@0..28
+ STRING_TYPE@0..7 "@string"
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ WHITESPACE@11..12 " "
+ EQUALITY_SIGN@12..13 "="
+ WHITESPACE@13..14 " "
+ VALUE@14..27
+ TOKEN@14..27
+ BRACE_GROUP@14..27
+ L_CURLY@14..15 "{"
+ WORD@15..20 "Hello"
+ WHITESPACE@20..21 " "
+ WORD@21..26 "World"
+ R_CURLY@26..27 "}"
+ R_CURLY@27..28 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_concatenation.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_concatenation.snap
new file mode 100644
index 0000000000..7f457d6964
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_concatenation.snap
@@ -0,0 +1,43 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@string{foo = {Hello World}} @string{bar = foo # \"!\"}\"#)"
+
+---
+ROOT@0..53
+ STRING@0..29
+ STRING_TYPE@0..7 "@string"
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ WHITESPACE@11..12 " "
+ EQUALITY_SIGN@12..13 "="
+ WHITESPACE@13..14 " "
+ VALUE@14..27
+ TOKEN@14..27
+ BRACE_GROUP@14..27
+ L_CURLY@14..15 "{"
+ WORD@15..20 "Hello"
+ WHITESPACE@20..21 " "
+ WORD@21..26 "World"
+ R_CURLY@26..27 "}"
+ R_CURLY@27..28 "}"
+ WHITESPACE@28..29 " "
+ STRING@29..53
+ STRING_TYPE@29..36 "@string"
+ L_CURLY@36..37 "{"
+ WORD@37..40 "bar"
+ WHITESPACE@40..41 " "
+ EQUALITY_SIGN@41..42 "="
+ WHITESPACE@42..43 " "
+ VALUE@43..52
+ TOKEN@43..46
+ WORD@43..46 "foo"
+ WHITESPACE@46..47 " "
+ HASH@47..48 "#"
+ WHITESPACE@48..49 " "
+ TOKEN@49..52
+ QUOTE_GROUP@49..52
+ QUOTE@49..50 "\""
+ WORD@50..51 "!"
+ QUOTE@51..52 "\""
+ R_CURLY@52..53 "}"
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_incomplete.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_incomplete.snap
new file mode 100644
index 0000000000..685d02a179
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_incomplete.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@string{foo = {Hello World}\"#)"
+
+---
+ROOT@0..27
+ STRING@0..27
+ STRING_TYPE@0..7 "@string"
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ WHITESPACE@11..12 " "
+ EQUALITY_SIGN@12..13 "="
+ WHITESPACE@13..14 " "
+ VALUE@14..27
+ TOKEN@14..27
+ BRACE_GROUP@14..27
+ L_CURLY@14..15 "{"
+ WORD@15..20 "Hello"
+ WHITESPACE@20..21 " "
+ WORD@21..26 "World"
+ R_CURLY@26..27 "}"
+ MISSING@27..27 ""
+
diff --git a/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_missing_quote.snap b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_missing_quote.snap
new file mode 100644
index 0000000000..8eeefa9bf9
--- /dev/null
+++ b/support/texlab/src/syntax/bibtex/snapshots/texlab__syntax__bibtex__parser__tests__string_missing_quote.snap
@@ -0,0 +1,23 @@
+---
+source: src/syntax/bibtex/parser.rs
+expression: "setup(r#\"@STRING{foo = \"Hello World}\"#)"
+
+---
+ROOT@0..27
+ STRING@0..27
+ STRING_TYPE@0..7 "@STRING"
+ L_CURLY@7..8 "{"
+ WORD@8..11 "foo"
+ WHITESPACE@11..12 " "
+ EQUALITY_SIGN@12..13 "="
+ WHITESPACE@13..14 " "
+ VALUE@14..26
+ TOKEN@14..26
+ QUOTE_GROUP@14..26
+ QUOTE@14..15 "\""
+ WORD@15..20 "Hello"
+ WHITESPACE@20..21 " "
+ WORD@21..26 "World"
+ MISSING@26..26 ""
+ R_CURLY@26..27 "}"
+